Skip to content

Commit cff1ac5

Browse files
committed
fix: implement smart toggle wrap functionality for MarkdownEditor
1 parent aa929f8 commit cff1ac5

File tree

2 files changed

+189
-59
lines changed

2 files changed

+189
-59
lines changed

custom/MarkdownEditor.vue

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { callAdminForthApi } from '@/utils';
1818
import * as monaco from 'monaco-editor';
1919
import TurndownService from 'turndown';
2020
import { gfm, tables } from 'turndown-plugin-gfm';
21+
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
2122
2223
const props = defineProps<{
2324
column: any,
@@ -233,55 +234,6 @@ function getTurndownService(): TurndownService {
233234
return turndownService;
234235
}
235236
236-
function toggleWrap(ed: monaco.editor.IStandaloneCodeEditor, left: string, right = left) {
237-
const m = ed.getModel();
238-
if (!m) return;
239-
240-
const selections = ed.getSelections() || [];
241-
if (!selections.length) return;
242-
243-
const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [];
244-
const nextSelections: monaco.Selection[] = [];
245-
246-
for (const sel of selections) {
247-
const text = m.getValueInRange(sel);
248-
249-
if (sel.isEmpty()) {
250-
edits.push({ range: sel, text: `${left}${right}` });
251-
const pos = sel.getStartPosition();
252-
const col = pos.column + left.length;
253-
nextSelections.push(new monaco.Selection(pos.lineNumber, col, pos.lineNumber, col));
254-
continue;
255-
}
256-
257-
const isWrapped = text.startsWith(left) && text.endsWith(right) && text.length >= left.length + right.length;
258-
259-
if (isWrapped) {
260-
const unwrapped = text.slice(left.length, text.length - right.length);
261-
edits.push({ range: sel, text: unwrapped });
262-
263-
const start = sel.getStartPosition();
264-
nextSelections.push(new monaco.Selection(start.lineNumber, start.column, start.lineNumber, start.column + unwrapped.length));
265-
} else {
266-
edits.push({ range: sel, text: `${left}${text}${right}` });
267-
268-
const start = sel.getStartPosition();
269-
nextSelections.push(
270-
new monaco.Selection(
271-
start.lineNumber,
272-
start.column + left.length,
273-
start.lineNumber,
274-
start.column + left.length + text.length,
275-
),
276-
);
277-
}
278-
}
279-
280-
ed.pushUndoStop();
281-
ed.executeEdits('md-format', edits);
282-
ed.pushUndoStop();
283-
ed.setSelections(nextSelections);
284-
}
285237
286238
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
287239
let model: monaco.editor.ITextModel | null = null;
@@ -514,22 +466,26 @@ onMounted(async () => {
514466
},
515467
scrollBeyondLastColumn: 0,
516468
});
517-
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyI, () => {
518-
toggleWrap(editor!, '*');
519-
});
520-
521469
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyB, () => {
522-
toggleWrap(editor!, '**');
470+
toggleWrapSmart(editor!, '**');
523471
});
524-
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
525-
toggleWrap(editor!, '<u>', '</u>');
472+
473+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyI, () => {
474+
toggleWrapSmart(editor!, '*');
526475
});
476+
527477
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE, () => {
528-
toggleWrap(editor!, '`');
478+
toggleWrapSmart(editor!, '`');
529479
});
530-
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Shift | monaco.KeyCode.KeyX, () => {
531-
toggleWrap(editor!, '~~');
480+
481+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyX, () => {
482+
toggleWrapSmart(editor!, '~~');
483+
});
484+
485+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
486+
toggleWrapSmart(editor!, '<u>', '</u>');
532487
});
488+
533489
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
534490
const selection = editor!.getSelection();
535491
if (!selection) return;
@@ -538,6 +494,7 @@ onMounted(async () => {
538494
const markdownLink = `[${escaped}](url)`;
539495
editor!.executeEdits('insert-link', [{ range: selection, text: markdownLink, forceMoveMarkers: true }]);
540496
});
497+
541498
debug('Monaco editor created', {
542499
hasUploadPluginInstanceId: Boolean(props.meta?.uploadPluginInstanceId),
543500
});
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as monaco from 'monaco-editor';
2+
3+
function trimTrailingEolSelection(
4+
m: monaco.editor.ITextModel,
5+
sel: monaco.Selection,
6+
): monaco.Selection {
7+
if (!sel.isEmpty() && sel.endColumn === 1 && sel.endLineNumber > sel.startLineNumber) {
8+
const prevLine = sel.endLineNumber - 1;
9+
const endCol = m.getLineMaxColumn(prevLine); // after last char
10+
return new monaco.Selection(sel.startLineNumber, sel.startColumn, prevLine, endCol);
11+
}
12+
return sel;
13+
}
14+
15+
function rangeFromOffsets(m: monaco.editor.ITextModel, startOffset: number, endOffset: number): monaco.Range {
16+
const s = m.getPositionAt(startOffset);
17+
const e = m.getPositionAt(endOffset);
18+
return new monaco.Range(s.lineNumber, s.column, e.lineNumber, e.column);
19+
}
20+
21+
export function toggleWrapSmart(
22+
ed: monaco.editor.IStandaloneCodeEditor,
23+
left: string,
24+
right: string = left,
25+
) {
26+
let m = ed.getModel();
27+
if (!m) return;
28+
29+
const selections = ed.getSelections() || [];
30+
if (!selections.length) return;
31+
32+
const leftLen = left.length;
33+
const rightLen = right.length;
34+
35+
const indexed = selections.map((s, idx) => ({ s, idx }));
36+
37+
indexed.sort((a, b) => {
38+
const ma = ed.getModel();
39+
if (!ma) return 0;
40+
const ao = ma.getOffsetAt(a.s.getStartPosition());
41+
const bo = ma.getOffsetAt(b.s.getStartPosition());
42+
if (ao !== bo) return bo - ao;
43+
const ae = ma.getOffsetAt(a.s.getEndPosition());
44+
const be = ma.getOffsetAt(b.s.getEndPosition());
45+
return be - ae;
46+
});
47+
48+
const nextSelections: Array<monaco.Selection | null> = new Array(selections.length).fill(null);
49+
50+
const getTextByOffsets = (startOffset: number, endOffset: number) => {
51+
const mm = ed.getModel();
52+
if (!mm) return '';
53+
return mm.getValueInRange(rangeFromOffsets(mm, startOffset, endOffset));
54+
};
55+
56+
ed.pushUndoStop();
57+
58+
for (const item of indexed) {
59+
m = ed.getModel();
60+
if (!m) break;
61+
62+
let sel = item.s;
63+
sel = trimTrailingEolSelection(m, sel);
64+
65+
const startPos = sel.getStartPosition();
66+
const endPos = sel.getEndPosition();
67+
const startOffset = m.getOffsetAt(startPos);
68+
const endOffset = m.getOffsetAt(endPos);
69+
70+
const isEmpty = sel.isEmpty();
71+
const modelLen = m.getValueLength();
72+
73+
const hasAdjacentWrap = () => {
74+
if (startOffset < leftLen) return false;
75+
if (endOffset + rightLen > modelLen) return false;
76+
77+
const l = getTextByOffsets(startOffset - leftLen, startOffset);
78+
const r = getTextByOffsets(endOffset, endOffset + rightLen);
79+
return l === left && r === right;
80+
};
81+
82+
const removeAdjacentWrap = () => {
83+
const leftRange = rangeFromOffsets(m!, startOffset - leftLen, startOffset);
84+
const rightRange = rangeFromOffsets(m!, endOffset, endOffset + rightLen);
85+
86+
ed.executeEdits('md-toggle-wrap', [
87+
{ range: rightRange, text: '', forceMoveMarkers: true },
88+
{ range: leftRange, text: '', forceMoveMarkers: true },
89+
]);
90+
91+
const mm = ed.getModel()!;
92+
const ns = startOffset - leftLen;
93+
const ne = endOffset - leftLen;
94+
95+
const s = mm.getPositionAt(ns);
96+
const e = mm.getPositionAt(ne);
97+
nextSelections[item.idx] = new monaco.Selection(s.lineNumber, s.column, e.lineNumber, e.column);
98+
};
99+
100+
if (isEmpty) {
101+
if (startOffset >= leftLen && startOffset + rightLen <= modelLen) {
102+
const l = getTextByOffsets(startOffset - leftLen, startOffset);
103+
const r = getTextByOffsets(startOffset, startOffset + rightLen);
104+
if (l === left && r === right) {
105+
const leftRange = rangeFromOffsets(m, startOffset - leftLen, startOffset);
106+
const rightRange = rangeFromOffsets(m, startOffset, startOffset + rightLen);
107+
108+
ed.executeEdits('md-toggle-wrap', [
109+
{ range: rightRange, text: '', forceMoveMarkers: true },
110+
{ range: leftRange, text: '', forceMoveMarkers: true },
111+
]);
112+
113+
const mm = ed.getModel()!;
114+
const p = mm.getPositionAt(startOffset - leftLen);
115+
nextSelections[item.idx] = new monaco.Selection(p.lineNumber, p.column, p.lineNumber, p.column);
116+
continue;
117+
}
118+
}
119+
120+
const insertRange = rangeFromOffsets(m, startOffset, startOffset);
121+
ed.executeEdits('md-toggle-wrap', [
122+
{ range: insertRange, text: `${left}${right}`, forceMoveMarkers: true },
123+
]);
124+
125+
const mm = ed.getModel()!;
126+
const p = mm.getPositionAt(startOffset + leftLen);
127+
nextSelections[item.idx] = new monaco.Selection(p.lineNumber, p.column, p.lineNumber, p.column);
128+
continue;
129+
}
130+
131+
const selectedText = m.getValueInRange(sel);
132+
133+
const isExplicitWrapped =
134+
selectedText.length >= leftLen + rightLen &&
135+
selectedText.startsWith(left) &&
136+
selectedText.endsWith(right);
137+
138+
if (isExplicitWrapped) {
139+
const unwrapped = selectedText.slice(leftLen, selectedText.length - rightLen);
140+
141+
ed.executeEdits('md-toggle-wrap', [
142+
{ range: sel, text: unwrapped, forceMoveMarkers: true },
143+
]);
144+
145+
const mm = ed.getModel()!;
146+
const s = mm.getPositionAt(startOffset);
147+
const e = mm.getPositionAt(startOffset + unwrapped.length);
148+
nextSelections[item.idx] = new monaco.Selection(s.lineNumber, s.column, e.lineNumber, e.column);
149+
continue;
150+
}
151+
152+
if (hasAdjacentWrap()) {
153+
removeAdjacentWrap();
154+
continue;
155+
}
156+
157+
ed.executeEdits('md-toggle-wrap', [
158+
{ range: sel, text: `${left}${selectedText}${right}`, forceMoveMarkers: true },
159+
]);
160+
161+
const mm = ed.getModel()!;
162+
const ns = startOffset + leftLen;
163+
const ne = endOffset + leftLen;
164+
const s = mm.getPositionAt(ns);
165+
const e = mm.getPositionAt(ne);
166+
nextSelections[item.idx] = new monaco.Selection(s.lineNumber, s.column, e.lineNumber, e.column);
167+
}
168+
169+
ed.pushUndoStop();
170+
171+
const finalSelections = nextSelections.map((s, i) => s ?? selections[i]);
172+
ed.setSelections(finalSelections);
173+
}

0 commit comments

Comments
 (0)