Skip to content

Commit dbac7e4

Browse files
authored
feat: preserve selection including formatting after markdown toggle (#774)
close #773
1 parent bf4be31 commit dbac7e4

File tree

3 files changed

+108
-25
lines changed

3 files changed

+108
-25
lines changed

src/components/ai/AIAssistantPanel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ async function sendMessage() {
562562
<!-- ============ 输入框 ============ -->
563563
<div v-if="!configVisible" class="relative mt-2">
564564
<div
565-
class="bg-background item-start border-border flex flex-col items-baseline gap-2 border rounded-xl px-3 py-2 pr-12 shadow-inner"
565+
class="item-start bg-background border-border flex flex-col items-baseline gap-2 border rounded-xl px-3 py-2 pr-12 shadow-inner"
566566
>
567567
<Textarea
568568
v-model="input"

src/utils/editor.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type CodeMirror from 'codemirror'
2+
3+
interface ToggleFormatOptions {
4+
prefix: string
5+
suffix: string
6+
check?: (selected: string) => boolean
7+
afterInsertCursorOffset?: number
8+
}
9+
10+
export function toggleFormat(
11+
editor: CodeMirror.Editor,
12+
{
13+
prefix,
14+
suffix,
15+
check,
16+
afterInsertCursorOffset = 0,
17+
}: ToggleFormatOptions,
18+
): void {
19+
const selected = editor.getSelection()
20+
const from = editor.getCursor(`from`)
21+
const to = editor.getCursor(`to`)
22+
const isFormatted = check?.(selected) ?? false
23+
24+
let newText: string
25+
let newFrom = { ...from }
26+
let newTo = { ...to }
27+
28+
if (isFormatted) {
29+
// Remove formatting (e.g. **abc** -> abc)
30+
newText = selected.slice(prefix.length, selected.length - suffix.length)
31+
editor.replaceSelection(newText)
32+
33+
// Adjust selection to original
34+
newTo.ch = newFrom.ch + newText.length
35+
editor.setSelection(newFrom, newTo)
36+
}
37+
else {
38+
// Apply formatting
39+
newText = `${prefix}${selected}${suffix}`
40+
editor.replaceSelection(newText)
41+
42+
// Select the whole formatted string (**abc**)
43+
newFrom = { ...from }
44+
newTo = {
45+
line: to.line,
46+
ch: to.ch + prefix.length + suffix.length,
47+
}
48+
editor.setSelection(newFrom, newTo)
49+
50+
// Optional cursor shift (e.g. for `]()` links)
51+
if (afterInsertCursorOffset !== 0) {
52+
const { line, ch } = editor.getCursor()
53+
editor.setCursor({ line, ch: ch + afterInsertCursorOffset })
54+
}
55+
}
56+
}

src/views/CodemirrorEditor.vue

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
formatDoc,
99
toBase64,
1010
} from '@/utils'
11+
import { toggleFormat } from '@/utils/editor'
1112
import fileApi from '@/utils/file'
1213
import CodeMirror from 'codemirror'
1314
import { Eye, List, Pen } from 'lucide-vue-next'
@@ -215,52 +216,78 @@ function initEditor() {
215216
autoCloseBrackets: true,
216217
extraKeys: {
217218
[`${shiftKey}-${altKey}-F`]: function autoFormat(editor) {
218-
formatDoc(editor.getValue()).then((doc) => {
219+
const value = editor.getValue()
220+
formatDoc(value).then((doc: string) => {
219221
editor.setValue(doc)
220222
})
221223
},
224+
222225
[`${ctrlKey}-B`]: function bold(editor) {
223-
const selected = editor.getSelection()
224-
editor.replaceSelection(`**${selected}**`)
226+
toggleFormat(editor, {
227+
prefix: `**`,
228+
suffix: `**`,
229+
check: s => s.startsWith(`**`) && s.endsWith(`**`),
230+
})
225231
},
232+
226233
[`${ctrlKey}-I`]: function italic(editor) {
227-
const selected = editor.getSelection()
228-
editor.replaceSelection(`*${selected}*`)
234+
toggleFormat(editor, {
235+
prefix: `*`,
236+
suffix: `*`,
237+
check: s => s.startsWith(`*`) && s.endsWith(`*`),
238+
})
229239
},
240+
230241
[`${ctrlKey}-D`]: function del(editor) {
231-
const selected = editor.getSelection()
232-
editor.replaceSelection(`~~${selected}~~`)
242+
toggleFormat(editor, {
243+
prefix: `~~`,
244+
suffix: `~~`,
245+
check: s => s.startsWith(`~~`) && s.endsWith(`~~`),
246+
})
233247
},
248+
234249
[`${ctrlKey}-K`]: function link(editor) {
235-
const selected = editor.getSelection()
236-
editor.replaceSelection(`[${selected}]()`)
237-
// now will slightly move the cursor to the left to fill in the link
238-
const { line, ch } = editor.getCursor()
239-
editor.setCursor({ line, ch: ch - 1 })
250+
toggleFormat(editor, {
251+
prefix: `[`,
252+
suffix: `]()`,
253+
check: s => s.startsWith(`[`) && s.endsWith(`]()`),
254+
afterInsertCursorOffset: -1,
255+
})
240256
},
257+
241258
[`${ctrlKey}-E`]: function code(editor) {
242-
const selected = editor.getSelection()
243-
editor.replaceSelection(`\`${selected}\``)
259+
toggleFormat(editor, {
260+
prefix: `\``,
261+
suffix: `\``,
262+
check: s => s.startsWith(`\``) && s.endsWith(`\``),
263+
})
244264
},
265+
266+
// 标题:单行逻辑,手动处理
245267
[`${ctrlKey}-H`]: function heading(editor) {
246268
const selected = editor.getSelection()
247-
editor.replaceSelection(`# ${selected}`)
269+
const replaced = selected.startsWith(`# `) ? selected.slice(2) : `# ${selected}`
270+
editor.replaceSelection(replaced)
248271
},
272+
249273
[`${ctrlKey}-U`]: function unorderedList(editor) {
250274
const selected = editor.getSelection()
251-
const lines = selected.split(`\n`).map(line => `- ${line}`)
252-
editor.replaceSelection(lines.join(`\n`))
275+
const lines = selected.split(`\n`)
276+
const isList = lines.every(line => line.trim().startsWith(`- `))
277+
const updated = isList
278+
? lines.map(line => line.replace(/^- +/, ``)).join(`\n`)
279+
: lines.map(line => `- ${line}`).join(`\n`)
280+
editor.replaceSelection(updated)
253281
},
254282
255283
[`${ctrlKey}-O`]: function orderedList(editor) {
256284
const selected = editor.getSelection()
257-
const lines = selected.split(`\n`).map((line, i) => `${i + 1}. ${line}`)
258-
editor.replaceSelection(lines.join(`\n`))
259-
},
260-
// 预备弃用
261-
[`${ctrlKey}-L`]: function code(editor) {
262-
const selected = editor.getSelection()
263-
editor.replaceSelection(`\`${selected}\``)
285+
const lines = selected.split(`\n`)
286+
const isList = lines.every(line => /^\d+\.\s/.test(line.trim()))
287+
const updated = isList
288+
? lines.map(line => line.replace(/^\d+\.\s+/, ``)).join(`\n`)
289+
: lines.map((line, i) => `${i + 1}. ${line}`).join(`\n`)
290+
editor.replaceSelection(updated)
264291
},
265292
},
266293
})

0 commit comments

Comments
 (0)