From 39c60f42fd3bc8317b1f50611d752242ada2b0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 15:39:22 +0200 Subject: [PATCH 01/22] fix makelist --- .../codeEditor/extensions/customKeyMaps.js | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 6fe479348..ca2e60ebc 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -197,18 +197,31 @@ const makeLink = (view)=>{ return true; }; -const makeList = (type)=>(view)=>{ +const makeList = (type) => (view) => { const { from, to } = view.state.selection.main; + const startLine = view.state.doc.lineAt(from); + const endLine = view.state.doc.lineAt(to); const lines = []; - for (let l = from; l <= to; l++) { - const lineText = view.state.doc.line(l + 1).text; - lines.push(lineText); + + for (let lineNo = startLine.number; lineNo <= endLine.number; lineNo++) { + lines.push(view.state.doc.line(lineNo).text); } const joined = lines.join('\n'); - let newText; - if(type === 'UL') newText = joined.replace(/^/gm, '- '); - else newText = joined.replace(/^/gm, (m, i)=>`${i + 1}. `); - view.dispatch({ changes: { from, to, insert: newText } }); + + const newText = type === 'UL' + ? joined.replace(/^/gm, '- ') + : joined.replace(/^/gm, (_, offset) => { + const lineNumber = joined.slice(0, offset).split('\n').length; + return `${lineNumber}. `; + }); + + view.dispatch({ + changes: { + from: startLine.from, + to: endLine.to, + insert: newText + } + }); return true; }; @@ -260,8 +273,8 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Shift-Mod-m', run: makeDiv }, { key: 'Mod-/', run: makeComment }, { key: 'Mod-k', run: makeLink }, - { key: 'Mod-l', run: makeList('UL') }, - { key: 'Shift-Mod-l', run: makeList('OL') }, + { key: 'Mod-Shift-u', run: makeList('UL') }, + { key: 'Mod-Shift-o', run: makeList('OL') }, { key: 'Shift-Mod-1', run: makeHeader(1) }, { key: 'Shift-Mod-2', run: makeHeader(2) }, { key: 'Shift-Mod-3', run: makeHeader(3) }, From 13b13b03e05155541e81e43236377084a0ec765b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 15:43:59 +0200 Subject: [PATCH 02/22] fix newPage and newColumn --- .../codeEditor/extensions/customKeyMaps.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index ca2e60ebc..a5858150d 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -235,13 +235,21 @@ const makeHeader = (level)=>(view)=>{ const newColumn = (view)=>{ const { from, to } = view.state.selection.main; - view.dispatch({ changes: { from, to, insert: '\n\\column\n\n' } }); + const insert = '\n\\column\n\n' ; + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length } + }); return true; }; -const newPage = (view)=>{ +const newPage = (view) => { const { from, to } = view.state.selection.main; - view.dispatch({ changes: { from, to, insert: '\n\\page\n\n' } }); + const insert = '\n\\page\n\n'; + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length } + }); return true; }; From 6243c27b0827875d697d4a4119d084108dd8f294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 20 Sep 2026 20:43:14 +0200 Subject: [PATCH 03/22] fix wrapselection to apply cursor in center of selection on every instance of cursors correctly. --- .../codeEditor/extensions/customKeyMaps.js | 72 +++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index a5858150d..768a66f81 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 300 }] */ +/* eslint max-lines: ["error", { "max": 400 }] */ import { keymap } from '@codemirror/view'; import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands'; import { EditorSelection } from '@codemirror/state'; @@ -92,25 +92,67 @@ const insertTab = (view)=>{ return true; }; -const wrapSelection = (prefix, suffix)=>(view)=>{ - const changes = []; +const wrapSelection = (prefix, suffix) => (view) => { + view.dispatch( + view.state.changeByRange((range) => { + const { from, to } = range; + const doc = view.state.doc; - for (const range of view.state.selection.ranges) { - const { from, to } = range; - const selected = view.state.doc.sliceString(from, to); + if (from === to) { + return { + changes: { + from, + to, + insert: prefix + suffix + }, + range: EditorSelection.cursor(from + prefix.length) + }; + } - let text; + const before = doc.sliceString( + Math.max(0, from - prefix.length), + from + ); - if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) { - text = selected.slice(prefix.length, -suffix.length); - } else {text = `${prefix}${selected}${suffix}`;} + const after = doc.sliceString( + to, + to + suffix.length + ); - changes.push({ from, to, insert: text }); - } + if (before === prefix && after === suffix) { + return { + changes: [ + { + from: from - prefix.length, + to, + insert: "" + }, + { + from: to, + to: to + suffix.length, + insert: "" + } + ], + range: EditorSelection.range( + from - prefix.length, + to - prefix.length + ) + }; + } - view.dispatch({ - changes - }); + return { + changes: { + from, + to, + insert: prefix + doc.sliceString(from, to) + suffix + }, + range: EditorSelection.range( + from + prefix.length, + to + prefix.length + ) + }; + }) + ); return true; }; From 304d78526e58f1674fb1fdebcbe01251cb6bec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 15:10:23 +0200 Subject: [PATCH 04/22] fix header hotkey cursor position --- client/components/codeEditor/extensions/customKeyMaps.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 768a66f81..90bed6c51 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -271,7 +271,12 @@ const makeHeader = (level)=>(view)=>{ const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to); const text = `${'#'.repeat(level)} ${selected}`; - view.dispatch({ changes: { from, to, insert: text } }); + if(selected.length === 0) { + view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length } }); + } else { + view.dispatch({ changes: { from, to, insert: text } }); + } + return true; }; From cf83806759384700187066b2990e7fbe9dbaf699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 16:48:55 +0200 Subject: [PATCH 05/22] fix wrapping of wrapselection for selection to accomodate prefixes and suffixes --- .../codeEditor/extensions/customKeyMaps.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 90bed6c51..e48a8430d 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -96,9 +96,11 @@ const wrapSelection = (prefix, suffix) => (view) => { view.dispatch( view.state.changeByRange((range) => { const { from, to } = range; + const noSelection = from === to; + const doc = view.state.doc; - if (from === to) { + if (noSelection) { return { changes: { from, @@ -110,32 +112,32 @@ const wrapSelection = (prefix, suffix) => (view) => { } const before = doc.sliceString( - Math.max(0, from - prefix.length), - from + Math.max(0, from), + from + prefix.length ); const after = doc.sliceString( - to, - to + suffix.length + to - suffix.length, + to ); - - if (before === prefix && after === suffix) { + const alreadyWrapped = before === prefix && after === suffix; + if (alreadyWrapped) { return { changes: [ { - from: from - prefix.length, - to, + from: from, + to: from + prefix.length, insert: "" }, { - from: to, - to: to + suffix.length, + from: to - suffix.length, + to: to, insert: "" } ], range: EditorSelection.range( - from - prefix.length, - to - prefix.length + from, + to - prefix.length - suffix.length, ) }; } @@ -147,8 +149,8 @@ const wrapSelection = (prefix, suffix) => (view) => { insert: prefix + doc.sliceString(from, to) + suffix }, range: EditorSelection.range( - from + prefix.length, - to + prefix.length + from, + to + suffix.length*2 ) }; }) From a39b86edf06e0a22a60c96f657197cf0d5842c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 22:24:44 +0200 Subject: [PATCH 06/22] better make link, better makespan make div make space and make nsb --- .../codeEditor/extensions/customKeyMaps.js | 238 ++++++++++-------- 1 file changed, 135 insertions(+), 103 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index e48a8430d..f806a737f 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -14,25 +14,24 @@ export async function formatCSS(view) { const code = empty ? fullDoc : selection; let formatted = await prettier.format(code, { - parser: 'css', - plugins: [postcssPlugin], + parser : 'css', + plugins : [postcssPlugin], // formatting options - tabWidth: 2, - useTabs: false, - printWidth: 100, - singleQuote: false, - trailingComma: 'all', - bracketSpacing: true, - endOfLine: 'lf' + tabWidth : 2, + useTabs : false, + printWidth : 100, + singleQuote : false, + trailingComma : 'all', + bracketSpacing : true, + endOfLine : 'lf' }); //format manually single declaration rules to span one line. //Prettier can't do it by default, this is crude but it works formatted = formatted.replace( - /([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g, - (_, selector, decl, whitespace) => - `${selector} { ${decl.trim()} }${whitespace}` + /([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g, + (_, selector, decl, whitespace)=>`${selector} { ${decl.trim()} }${whitespace}` ); if(formatted === code) return true; @@ -57,6 +56,13 @@ export async function formatCSS(view) { return true; } + +const forEachSelection = (view, fn) => { + view.dispatch(view.state.changeByRange((range) => fn(view.state, range))); + return true; +}; + + const insertTab = (view)=>{ // If any selection spans multiple lines, delegates to CodeMirror's indentMore // Otherwise inserts two spaces at each cursor/selection @@ -92,22 +98,22 @@ const insertTab = (view)=>{ return true; }; -const wrapSelection = (prefix, suffix) => (view) => { +const wrapSelection = (prefix, suffix)=>(view)=>{ view.dispatch( - view.state.changeByRange((range) => { + view.state.changeByRange((range)=>{ const { from, to } = range; const noSelection = from === to; - + const doc = view.state.doc; - if (noSelection) { + if(noSelection) { return { - changes: { + changes : { from, to, - insert: prefix + suffix + insert : prefix + suffix }, - range: EditorSelection.cursor(from + prefix.length) + range : EditorSelection.cursor(from + prefix.length) }; } @@ -121,21 +127,21 @@ const wrapSelection = (prefix, suffix) => (view) => { to ); const alreadyWrapped = before === prefix && after === suffix; - if (alreadyWrapped) { + if(alreadyWrapped) { return { - changes: [ + changes : [ { - from: from, - to: from + prefix.length, - insert: "" + from : from, + to : from + prefix.length, + insert : '' }, { - from: to - suffix.length, - to: to, - insert: "" + from : to - suffix.length, + to : to, + insert : '' } ], - range: EditorSelection.range( + range : EditorSelection.range( from, to - prefix.length - suffix.length, ) @@ -143,14 +149,14 @@ const wrapSelection = (prefix, suffix) => (view) => { } return { - changes: { + changes : { from, to, - insert: prefix + doc.sliceString(from, to) + suffix + insert : prefix + doc.sliceString(from, to) + suffix }, - range: EditorSelection.range( + range : EditorSelection.range( from, - to + suffix.length*2 + to + suffix.length + prefix.length ) }; }) @@ -159,89 +165,115 @@ const wrapSelection = (prefix, suffix) => (view) => { return true; }; -const makeNbsp = (view)=>{ - const { from } = view.state.selection.main; - - const prev2 = from >= 2 - ? view.state.doc.sliceString(from - 2, from) - : ''; +const makeNbsp = (view) => forEachSelection(view, (state, range) => { + const { from, to } = range; + const prev2 = from >= 2 ? state.doc.sliceString(from - 2, from) : ''; const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>'; - view.dispatch({ - changes : { from, to: from, insert }, - selection : { anchor: from + insert.length }, - }); + return { + changes : { from, to, insert }, + range : EditorSelection.cursor(from + insert.length) + }; +}); - return true; -}; - -const makeSpace = (view)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); +const makeSpace = (view) => forEachSelection(view, (state, range) => { + const { from, to } = range; + const selected = state.doc.sliceString(from, to); const match = selected.match(/^{{width:(\d+)% }}$/); - let newText = '{{width:10% }}'; + + let insert = '{{width:10% }}'; + if(match) { const percent = Math.min(parseInt(match[1], 10) + 10, 100); - newText = `{{width:${percent}% }}`; + insert = `{{width:${percent}% }}`; } - view.dispatch({ changes: { from, to, insert: newText } }); - return true; -}; -const removeSpace = (view)=>{ + return { + changes : { from, to, insert }, + range : EditorSelection.range(from, from + insert.length) + }; +}); + +const removeSpace = (view) => forEachSelection(view, (state, range) => { const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to); const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { const percent = parseInt(match[1], 10) - 10; const newText = percent > 0 ? `{{width:${percent}% }}` : ''; - view.dispatch({ changes: { from, to, insert: newText } }); + return{ changes: { from, to, insert: newText } }; } return true; -}; +}); -const makeSpan = (view)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); - const text = selected.startsWith('{{') && selected.endsWith('}}') - ? selected.slice(2, -2) - : `{{${selected}}}`; - view.dispatch({ changes: { from, to, insert: text } }); - return true; -}; +const makeLink = (view) => { + const ranges = view.state.selection.ranges; -const makeDiv = (view)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); - const text = selected.startsWith('{{') && selected.endsWith('}}') - ? selected.slice(2, -2) - : `{{\n${selected}\n}}`; - view.dispatch({ changes: { from, to, insert: text } }); - return true; -}; + // Multiple selections: pair them as alt text + URL + if (ranges.length > 1) { + const changes = []; -const makeComment = (view)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); - const isHtmlComment = selected.startsWith(''); - const text = isHtmlComment - ? selected.slice(4, -3) - : ``; - view.dispatch({ changes: { from, to, insert: text } }); - return true; -}; + for (let i = 0; i < ranges.length - 1; i += 2) { + const alt = view.state.doc.sliceString( + ranges[i].from, + ranges[i].to + ).trim(); -const makeLink = (view)=>{ + const url = view.state.doc.sliceString( + ranges[i + 1].from, + ranges[i + 1].to + ).trim(); + + changes.push({ + from: ranges[i].from, + to: ranges[i + 1].to, + insert: `[${alt}](${url})` + }); + } + + view.dispatch({ changes }); + return true; + } + + // Single selection: existing behavior const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to).trim(); + const isLink = /^\[(.*)\]\((.*)\)$/.exec(selected); - const text = isLink ? `${isLink[1]} ${isLink[2]}` : `[${selected || 'alt text'}](url)`; - view.dispatch({ changes: { from, to, insert: text } }); + + let text; + + if (isLink) { + text = `${isLink[1]} ${isLink[2]}`; + } else { + const isUrl = + /^(https?:\/\/|www\.)\S+$/i.test(selected) || + /^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected); + + if (isUrl) { + const url = selected; + const domain = url + .replace(/^https?:\/\//i, '') + .replace(/^www\./i, '') + .split(/[/?#]/)[0]; + + const name = domain.split('.')[0]; + + text = `[${name}](${url})`; + } else { + text = `[${selected || 'alt text'}](url)`; + } + } + + view.dispatch({ + changes: { from, to, insert: text } + }); + return true; }; -const makeList = (type) => (view) => { +const makeList = (type)=>(view)=>{ const { from, to } = view.state.selection.main; const startLine = view.state.doc.lineAt(from); const endLine = view.state.doc.lineAt(to); @@ -254,16 +286,16 @@ const makeList = (type) => (view) => { const newText = type === 'UL' ? joined.replace(/^/gm, '- ') - : joined.replace(/^/gm, (_, offset) => { + : joined.replace(/^/gm, (_, offset)=>{ const lineNumber = joined.slice(0, offset).split('\n').length; return `${lineNumber}. `; }); view.dispatch({ - changes: { - from: startLine.from, - to: endLine.to, - insert: newText + changes : { + from : startLine.from, + to : endLine.to, + insert : newText } }); return true; @@ -278,26 +310,26 @@ const makeHeader = (level)=>(view)=>{ } else { view.dispatch({ changes: { from, to, insert: text } }); } - + return true; }; const newColumn = (view)=>{ const { from, to } = view.state.selection.main; const insert = '\n\\column\n\n' ; - view.dispatch({ - changes: { from, to, insert }, - selection: { anchor: from + insert.length } + view.dispatch({ + changes : { from, to, insert }, + selection : { anchor: from + insert.length } }); return true; }; -const newPage = (view) => { +const newPage = (view)=>{ const { from, to } = view.state.selection.main; const insert = '\n\\page\n\n'; view.dispatch({ - changes: { from, to, insert }, - selection: { anchor: from + insert.length } + changes : { from, to, insert }, + selection : { anchor: from + insert.length } }); return true; }; @@ -326,9 +358,9 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Mod-.', run: makeNbsp }, { key: 'Shift-Mod-.', run: makeSpace }, { key: 'Shift-Mod-,', run: removeSpace }, - { key: 'Mod-m', run: makeSpan }, - { key: 'Shift-Mod-m', run: makeDiv }, - { key: 'Mod-/', run: makeComment }, + { key: 'Mod-m', run: wrapSelection('{{', '}}') }, + { key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') }, + { key: 'Mod-/', run: wrapSelection('') }, { key: 'Mod-k', run: makeLink }, { key: 'Mod-Shift-u', run: makeList('UL') }, { key: 'Mod-Shift-o', run: makeList('OL') }, From 3ec85b6f5244a21fe7d82e9621a17fdb58403559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 22:50:42 +0200 Subject: [PATCH 07/22] last fixes --- .../codeEditor/extensions/customKeyMaps.js | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index f806a737f..d8c4edcce 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -57,12 +57,11 @@ export async function formatCSS(view) { return true; } -const forEachSelection = (view, fn) => { - view.dispatch(view.state.changeByRange((range) => fn(view.state, range))); +const forEachSelection = (view, fn)=>{ + view.dispatch(view.state.changeByRange((range)=>fn(view.state, range))); return true; }; - const insertTab = (view)=>{ // If any selection spans multiple lines, delegates to CodeMirror's indentMore // Otherwise inserts two spaces at each cursor/selection @@ -165,7 +164,7 @@ const wrapSelection = (prefix, suffix)=>(view)=>{ return true; }; -const makeNbsp = (view) => forEachSelection(view, (state, range) => { +const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const prev2 = from >= 2 ? state.doc.sliceString(from - 2, from) : ''; @@ -177,7 +176,7 @@ const makeNbsp = (view) => forEachSelection(view, (state, range) => { }; }); -const makeSpace = (view) => forEachSelection(view, (state, range) => { +const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const selected = state.doc.sliceString(from, to); const match = selected.match(/^{{width:(\d+)% }}$/); @@ -195,23 +194,23 @@ const makeSpace = (view) => forEachSelection(view, (state, range) => { }; }); -const removeSpace = (view) => forEachSelection(view, (state, range) => { +const removeSpace = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to); const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { const percent = parseInt(match[1], 10) - 10; const newText = percent > 0 ? `{{width:${percent}% }}` : ''; - return{ changes: { from, to, insert: newText } }; + return { changes: { from, to, insert: newText } }; } return true; }); -const makeLink = (view) => { +const makeLink = (view)=>{ const ranges = view.state.selection.ranges; // Multiple selections: pair them as alt text + URL - if (ranges.length > 1) { + if(ranges.length > 1) { const changes = []; for (let i = 0; i < ranges.length - 1; i += 2) { @@ -226,9 +225,9 @@ const makeLink = (view) => { ).trim(); changes.push({ - from: ranges[i].from, - to: ranges[i + 1].to, - insert: `[${alt}](${url})` + from : ranges[i].from, + to : ranges[i + 1].to, + insert : `[${alt}](${url})` }); } @@ -244,14 +243,14 @@ const makeLink = (view) => { let text; - if (isLink) { + if(isLink) { text = `${isLink[1]} ${isLink[2]}`; } else { const isUrl = /^(https?:\/\/|www\.)\S+$/i.test(selected) || /^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected); - if (isUrl) { + if(isUrl) { const url = selected; const domain = url .replace(/^https?:\/\//i, '') @@ -267,7 +266,7 @@ const makeLink = (view) => { } view.dispatch({ - changes: { from, to, insert: text } + changes : { from, to, insert: text } }); return true; @@ -301,38 +300,35 @@ const makeList = (type)=>(view)=>{ return true; }; -const makeHeader = (level)=>(view)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); - const text = `${'#'.repeat(level)} ${selected}`; +const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ + const { from, to } = range; + const selected = state.doc.sliceString(from, to); + const insert = `${'#'.repeat(level)} ${selected}`; if(selected.length === 0) { - view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length } }); + return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from + insert.length) }; } else { - view.dispatch({ changes: { from, to, insert: text } }); + return { changes: { from, to, insert: insert }, range}; } +}); - return true; -}; - -const newColumn = (view)=>{ - const { from, to } = view.state.selection.main; +const newColumn = (view)=>forEachSelection(view, (state, range)=>{ + const { from, to } = range; const insert = '\n\\column\n\n' ; - view.dispatch({ + return { changes : { from, to, insert }, - selection : { anchor: from + insert.length } - }); - return true; -}; + range: EditorSelection.cursor(from + insert.length) + }; +}); -const newPage = (view)=>{ - const { from, to } = view.state.selection.main; +const newPage = (view)=>forEachSelection(view, (state, range)=>{ + const { from, to } = range; const insert = '\n\\page\n\n'; - view.dispatch({ - changes : { from, to, insert }, - selection : { anchor: from + insert.length } - }); - return true; -}; + + return { + changes: { from, to, insert }, + range: EditorSelection.cursor(from + insert.length) + }; +}); export const generalKeymap = Prec.high(keymap.of([ { key: 'Tab', run: insertTab }, //runs indentMore if multiple lines selected in a single selection From 7b1c576eb707831e4752f8884ebb9539f95c80cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 22:58:08 +0200 Subject: [PATCH 08/22] wrapselection to work with foreachselection --- .../codeEditor/extensions/customKeyMaps.js | 108 ++++++++---------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index d8c4edcce..8c904f7e7 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -97,72 +97,58 @@ const insertTab = (view)=>{ return true; }; -const wrapSelection = (prefix, suffix)=>(view)=>{ - view.dispatch( - view.state.changeByRange((range)=>{ - const { from, to } = range; - const noSelection = from === to; +const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( + view, + (state, range)=>{ + const { from, to } = range; + const noSelection = from === to; + const selected = state.doc.sliceString(from, to); - const doc = view.state.doc; - - if(noSelection) { - return { - changes : { - from, - to, - insert : prefix + suffix - }, - range : EditorSelection.cursor(from + prefix.length) - }; - } - - const before = doc.sliceString( - Math.max(0, from), - from + prefix.length - ); - - const after = doc.sliceString( - to - suffix.length, - to - ); - const alreadyWrapped = before === prefix && after === suffix; - if(alreadyWrapped) { - return { - changes : [ - { - from : from, - to : from + prefix.length, - insert : '' - }, - { - from : to - suffix.length, - to : to, - insert : '' - } - ], - range : EditorSelection.range( - from, - to - prefix.length - suffix.length, - ) - }; - } + if(noSelection) { + const insert = prefix + suffix; return { - changes : { - from, - to, - insert : prefix + doc.sliceString(from, to) + suffix - }, - range : EditorSelection.range( - from, - to + suffix.length + prefix.length - ) + changes : { from, to, insert }, + range : EditorSelection.cursor(from + prefix.length) }; - }) - ); + } + + const hasWrapper = + selected.startsWith(prefix) && + selected.endsWith(suffix); + + if(hasWrapper) { + return { + changes : [ + { + from : to - suffix.length, + to, + insert : '' + }, + { + from, + to : from + prefix.length, + insert : '' + } + ], + range : EditorSelection.range(from, to - prefix.length - suffix.length) + }; + } + + return { + changes : { + from, + to, + insert : prefix + selected + suffix + }, + range : EditorSelection.range( + from, + to + prefix.length + suffix.length + ) + }; + } +); - return true; -}; const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; From 12c8bdf5cdfaf8c005ce358827f731e5c64e7591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:00:17 +0200 Subject: [PATCH 09/22] inserttab to use foreachselection --- .../codeEditor/extensions/customKeyMaps.js | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 8c904f7e7..42e97a82e 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -64,37 +64,26 @@ const forEachSelection = (view, fn)=>{ const insertTab = (view)=>{ // If any selection spans multiple lines, delegates to CodeMirror's indentMore - // Otherwise inserts two spaces at each cursor/selection - const shouldIndent = view.state.selection.ranges.some((range)=>view.state.doc.lineAt(range.from).number !== + // Otherwise inserts two spaces at each cursor/selection + const shouldIndent = view.state.selection.ranges.some((range)=> + view.state.doc.lineAt(range.from).number !== view.state.doc.lineAt(range.to).number ); if(shouldIndent) return indentMore(view); - const changes = []; + return forEachSelection(view, (state, range)=>{ + const { from, to } = range; - for (const range of view.state.selection.ranges) { - changes.push({ - from : range.from, - to : range.to, - insert : ' ' // Insert two spaces, not a tab char! - }); - } - // Create a transaction so we can map old positions to - // their new positions after the edits are applied - const mappedChanges = view.state.update({ changes }); - - view.dispatch({ - changes, - selection : EditorSelection.create( - view.state.selection.ranges.map((range)=>EditorSelection.cursor( - mappedChanges.changes.mapPos(range.from, -1) + 2 - ) - ) - ) + return { + changes : { + from, + to, + insert : ' ' // Insert two spaces, not a tab char! + }, + range : EditorSelection.cursor(from + 2) + }; }); - - return true; }; const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( From a87f5428da1a21f9e32bced2d319da3c58207781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:01:27 +0200 Subject: [PATCH 10/22] changes to removespace --- client/components/codeEditor/extensions/customKeyMaps.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 42e97a82e..c73f974c8 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -170,15 +170,14 @@ const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ }); const removeSpace = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); + const { from, to } = range; + const selected = state.doc.sliceString(from, to); const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { const percent = parseInt(match[1], 10) - 10; - const newText = percent > 0 ? `{{width:${percent}% }}` : ''; - return { changes: { from, to, insert: newText } }; + const insert = percent > 0 ? `{{width:${percent}% }}` : ''; + return { changes: { from, to, insert: insert } }; } - return true; }); const makeLink = (view)=>{ From dbfbe652d3e99a9f3463210dd8688177bdc6523a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:16:36 +0200 Subject: [PATCH 11/22] makelist work --- .../codeEditor/extensions/customKeyMaps.js | 84 +++++++++++-------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index c73f974c8..74e2923c0 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -6,6 +6,9 @@ import { Prec } from '@codemirror/state'; import * as prettier from 'prettier/standalone'; import * as postcssPlugin from 'prettier/plugins/postcss'; + +import { ChangeSet } from '@codemirror/state'; + export async function formatCSS(view) { try { const { from, to, empty } = view.state.selection.main; @@ -65,8 +68,7 @@ const forEachSelection = (view, fn)=>{ const insertTab = (view)=>{ // If any selection spans multiple lines, delegates to CodeMirror's indentMore // Otherwise inserts two spaces at each cursor/selection - const shouldIndent = view.state.selection.ranges.some((range)=> - view.state.doc.lineAt(range.from).number !== + const shouldIndent = view.state.selection.ranges.some((range)=>view.state.doc.lineAt(range.from).number !== view.state.doc.lineAt(range.to).number ); @@ -110,13 +112,13 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( return { changes : [ { - from : to - suffix.length, + from : to - suffix.length, to, insert : '' }, { from, - to : from + prefix.length, + to : from + prefix.length, insert : '' } ], @@ -246,33 +248,49 @@ const makeLink = (view)=>{ return true; }; -const makeList = (type)=>(view)=>{ - const { from, to } = view.state.selection.main; - const startLine = view.state.doc.lineAt(from); - const endLine = view.state.doc.lineAt(to); - const lines = []; - for (let lineNo = startLine.number; lineNo <= endLine.number; lineNo++) { - lines.push(view.state.doc.line(lineNo).text); +const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ + const { from, to } = selectionRange; + + const startLine = state.doc.lineAt(from); + const endLine = state.doc.lineAt(to); + + const selectedLines = []; + + for ( + let lineNumber = startLine.number; + lineNumber <= endLine.number; + lineNumber++ + ) { + selectedLines.push(state.doc.line(lineNumber)); } - const joined = lines.join('\n'); - const newText = type === 'UL' - ? joined.replace(/^/gm, '- ') - : joined.replace(/^/gm, (_, offset)=>{ - const lineNumber = joined.slice(0, offset).split('\n').length; - return `${lineNumber}. `; - }); + const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix) + ); - view.dispatch({ - changes : { - from : startLine.from, - to : endLine.to, - insert : newText - } + const changes = selectedLines.map((line)=>{ + const shouldRemovePrefix = + allLinesHavePrefix && line.text.startsWith(prefix); + + return { + from : line.from, + to : shouldRemovePrefix + ? line.from + prefix.length + : line.from, + insert : shouldRemovePrefix ? '' : prefix, + }; }); - return true; -}; + + const changeSet = ChangeSet.of(changes, state.doc.length); + + return { + changes, + range : EditorSelection.range( + changeSet.mapPos(from, -1), + changeSet.mapPos(to, 1) + ), + }; +}); const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; @@ -281,7 +299,7 @@ const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ if(selected.length === 0) { return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from + insert.length) }; } else { - return { changes: { from, to, insert: insert }, range}; + return { changes: { from, to, insert: insert }, range }; } }); @@ -289,8 +307,8 @@ const newColumn = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const insert = '\n\\column\n\n' ; return { - changes : { from, to, insert }, - range: EditorSelection.cursor(from + insert.length) + changes : { from, to, insert }, + range : EditorSelection.cursor(from + insert.length) }; }); @@ -299,8 +317,8 @@ const newPage = (view)=>forEachSelection(view, (state, range)=>{ const insert = '\n\\page\n\n'; return { - changes: { from, to, insert }, - range: EditorSelection.cursor(from + insert.length) + changes : { from, to, insert }, + range : EditorSelection.cursor(from + insert.length) }; }); @@ -332,8 +350,8 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') }, { key: 'Mod-/', run: wrapSelection('') }, { key: 'Mod-k', run: makeLink }, - { key: 'Mod-Shift-u', run: makeList('UL') }, - { key: 'Mod-Shift-o', run: makeList('OL') }, + { key: 'Mod-Shift-u', run: makeList('- ') }, + { key: 'Mod-Shift-o', run: makeList('1. ') }, { key: 'Shift-Mod-1', run: makeHeader(1) }, { key: 'Shift-Mod-2', run: makeHeader(2) }, { key: 'Shift-Mod-3', run: makeHeader(3) }, From 3d36361a8dbdc948662f108211c8d79df1d2471f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:28:21 +0200 Subject: [PATCH 12/22] make makelink use proper shortcut and work for multiple lines --- .../codeEditor/extensions/customKeyMaps.js | 96 ++++++++----------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 74e2923c0..2df14210f 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -182,72 +182,60 @@ const removeSpace = (view)=>forEachSelection(view, (state, range)=>{ } }); -const makeLink = (view)=>{ - const ranges = view.state.selection.ranges; +const makeLink = (view)=>forEachSelection(view, (state, range)=>{ + const { from, to } = range; + const selected = state.doc.sliceString(from, to).trim(); - // Multiple selections: pair them as alt text + URL - if(ranges.length > 1) { - const changes = []; + // If the selection is already a Markdown link, unwrap it + const existingLink = /^\[(.*)\]\((.*)\)$/.exec(selected); - for (let i = 0; i < ranges.length - 1; i += 2) { - const alt = view.state.doc.sliceString( - ranges[i].from, - ranges[i].to - ).trim(); + if(existingLink) { + const [, text, url] = existingLink; + const insert = `${text} ${url}`; - const url = view.state.doc.sliceString( - ranges[i + 1].from, - ranges[i + 1].to - ).trim(); - - changes.push({ - from : ranges[i].from, - to : ranges[i + 1].to, - insert : `[${alt}](${url})` - }); - } - - view.dispatch({ changes }); - return true; + return { + changes : { + from, + to, + insert, + }, + range : EditorSelection.range( + from, + from + insert.length + ), + }; } - // Single selection: existing behavior - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to).trim(); - - const isLink = /^\[(.*)\]\((.*)\)$/.exec(selected); - - let text; - - if(isLink) { - text = `${isLink[1]} ${isLink[2]}`; - } else { - const isUrl = + // If the selection is a URL, use its domain as the link text. + const isUrl = /^(https?:\/\/|www\.)\S+$/i.test(selected) || /^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected); - if(isUrl) { - const url = selected; - const domain = url + if(isUrl) { + const domain = selected .replace(/^https?:\/\//i, '') .replace(/^www\./i, '') .split(/[/?#]/)[0]; - const name = domain.split('.')[0]; + const name = domain.split('.')[0]; - text = `[${name}](${url})`; - } else { - text = `[${selected || 'alt text'}](url)`; - } + const insert = `[${name}](${selected})`; + + return { + changes : { from, to, insert }, + range : EditorSelection.range(from, from + insert.length), + }; } - view.dispatch({ - changes : { from, to, insert: text } - }); - - return true; -}; + // If the selection is just text, use it as alt text and set url as placeholder + const insert = `[${selected || 'alt text'}](url)`; + return { + changes : { from, to, insert }, + range : EditorSelection.range(from, from + insert.length), + }; +} +); const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ const { from, to } = selectionRange; @@ -265,12 +253,10 @@ const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange selectedLines.push(state.doc.line(lineNumber)); } - const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix) - ); + const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix)); const changes = selectedLines.map((line)=>{ - const shouldRemovePrefix = - allLinesHavePrefix && line.text.startsWith(prefix); + const shouldRemovePrefix = allLinesHavePrefix && line.text.startsWith(prefix); return { from : line.from, @@ -349,7 +335,7 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Mod-m', run: wrapSelection('{{', '}}') }, { key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') }, { key: 'Mod-/', run: wrapSelection('') }, - { key: 'Mod-k', run: makeLink }, + { key: 'Mod-Shift-k', run: makeLink }, { key: 'Mod-Shift-u', run: makeList('- ') }, { key: 'Mod-Shift-o', run: makeList('1. ') }, { key: 'Shift-Mod-1', run: makeHeader(1) }, From adf43c7c406e570b7992be4a0bef051491738b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:34:51 +0200 Subject: [PATCH 13/22] simplify newcolumn and newpage --- .../codeEditor/extensions/customKeyMaps.js | 62 ++++--------------- 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 2df14210f..8967f42c6 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -1,14 +1,11 @@ /* eslint max-lines: ["error", { "max": 400 }] */ import { keymap } from '@codemirror/view'; import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands'; -import { EditorSelection } from '@codemirror/state'; +import { EditorSelection, ChangeSet } from '@codemirror/state'; import { Prec } from '@codemirror/state'; import * as prettier from 'prettier/standalone'; import * as postcssPlugin from 'prettier/plugins/postcss'; - -import { ChangeSet } from '@codemirror/state'; - export async function formatCSS(view) { try { const { from, to, empty } = view.state.selection.main; @@ -76,7 +73,6 @@ const insertTab = (view)=>{ return forEachSelection(view, (state, range)=>{ const { from, to } = range; - return { changes : { from, @@ -88,13 +84,11 @@ const insertTab = (view)=>{ }); }; -const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( - view, - (state, range)=>{ +const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view,(state, range)=>{ const { from, to } = range; - const noSelection = from === to; const selected = state.doc.sliceString(from, to); + const noSelection = from === to; if(noSelection) { const insert = prefix + suffix; @@ -104,9 +98,7 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( }; } - const hasWrapper = - selected.startsWith(prefix) && - selected.endsWith(suffix); + const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix); if(hasWrapper) { return { @@ -140,7 +132,6 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection( } ); - const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; @@ -156,15 +147,13 @@ const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const selected = state.doc.sliceString(from, to); - const match = selected.match(/^{{width:(\d+)% }}$/); - + let insert = '{{width:10% }}'; - + const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { const percent = Math.min(parseInt(match[1], 10) + 10, 100); insert = `{{width:${percent}% }}`; } - return { changes : { from, to, insert }, range : EditorSelection.range(from, from + insert.length) @@ -192,13 +181,8 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ if(existingLink) { const [, text, url] = existingLink; const insert = `${text} ${url}`; - return { - changes : { - from, - to, - insert, - }, + changes : { from, to, insert }, range : EditorSelection.range( from, from + insert.length @@ -218,9 +202,7 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ .split(/[/?#]/)[0]; const name = domain.split('.')[0]; - const insert = `[${name}](${selected})`; - return { changes : { from, to, insert }, range : EditorSelection.range(from, from + insert.length), @@ -229,7 +211,6 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ // If the selection is just text, use it as alt text and set url as placeholder const insert = `[${selected || 'alt text'}](url)`; - return { changes : { from, to, insert }, range : EditorSelection.range(from, from + insert.length), @@ -242,16 +223,9 @@ const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange const startLine = state.doc.lineAt(from); const endLine = state.doc.lineAt(to); - const selectedLines = []; - for ( - let lineNumber = startLine.number; - lineNumber <= endLine.number; - lineNumber++ - ) { - selectedLines.push(state.doc.line(lineNumber)); - } + for ( let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++ ) { selectedLines.push(state.doc.line(lineNumber)) } const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix)); @@ -283,25 +257,15 @@ const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ const selected = state.doc.sliceString(from, to); const insert = `${'#'.repeat(level)} ${selected}`; if(selected.length === 0) { - return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from + insert.length) }; + return { changes: { from, to, insert: insert },range: EditorSelection.cursor(from + insert.length) }; } else { return { changes: { from, to, insert: insert }, range }; } }); -const newColumn = (view)=>forEachSelection(view, (state, range)=>{ +const newBreak = (type)=>(view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; - const insert = '\n\\column\n\n' ; - return { - changes : { from, to, insert }, - range : EditorSelection.cursor(from + insert.length) - }; -}); - -const newPage = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const insert = '\n\\page\n\n'; - + const insert = `\n\\${type}\n\n`; return { changes : { from, to, insert }, range : EditorSelection.cursor(from + insert.length) @@ -344,6 +308,6 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Shift-Mod-4', run: makeHeader(4) }, { key: 'Shift-Mod-5', run: makeHeader(5) }, { key: 'Shift-Mod-6', run: makeHeader(6) }, - { key: 'Mod-Enter', run: newPage }, - { key: 'Shift-Mod-Enter', run: newColumn }, + { key: 'Mod-Enter', run: newBreak('page') }, + { key: 'Shift-Mod-Enter', run: newBreak('column') }, ])); From cd23312bce09f2f9941f5c1fe3eb63ae76f93182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:36:04 +0200 Subject: [PATCH 14/22] lint --- .../codeEditor/extensions/customKeyMaps.js | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 8967f42c6..d1cf18294 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -84,52 +84,52 @@ const insertTab = (view)=>{ }); }; -const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view,(state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to); +const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, range)=>{ + const { from, to } = range; + const selected = state.doc.sliceString(from, to); - const noSelection = from === to; - if(noSelection) { - const insert = prefix + suffix; - - return { - changes : { from, to, insert }, - range : EditorSelection.cursor(from + prefix.length) - }; - } - - const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix); - - if(hasWrapper) { - return { - changes : [ - { - from : to - suffix.length, - to, - insert : '' - }, - { - from, - to : from + prefix.length, - insert : '' - } - ], - range : EditorSelection.range(from, to - prefix.length - suffix.length) - }; - } + const noSelection = from === to; + if(noSelection) { + const insert = prefix + suffix; return { - changes : { - from, - to, - insert : prefix + selected + suffix - }, - range : EditorSelection.range( - from, - to + prefix.length + suffix.length - ) + changes : { from, to, insert }, + range : EditorSelection.cursor(from + prefix.length) }; } + + const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix); + + if(hasWrapper) { + return { + changes : [ + { + from : to - suffix.length, + to, + insert : '' + }, + { + from, + to : from + prefix.length, + insert : '' + } + ], + range : EditorSelection.range(from, to - prefix.length - suffix.length) + }; + } + + return { + changes : { + from, + to, + insert : prefix + selected + suffix + }, + range : EditorSelection.range( + from, + to + prefix.length + suffix.length + ) + }; +} ); const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ @@ -147,7 +147,7 @@ const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const selected = state.doc.sliceString(from, to); - + let insert = '{{width:10% }}'; const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { @@ -183,7 +183,7 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ const insert = `${text} ${url}`; return { changes : { from, to, insert }, - range : EditorSelection.range( + range : EditorSelection.range( from, from + insert.length ), @@ -225,7 +225,7 @@ const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange const endLine = state.doc.lineAt(to); const selectedLines = []; - for ( let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++ ) { selectedLines.push(state.doc.line(lineNumber)) } + for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++) { selectedLines.push(state.doc.line(lineNumber)); } const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix)); @@ -257,7 +257,7 @@ const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ const selected = state.doc.sliceString(from, to); const insert = `${'#'.repeat(level)} ${selected}`; if(selected.length === 0) { - return { changes: { from, to, insert: insert },range: EditorSelection.cursor(from + insert.length) }; + return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from + insert.length) }; } else { return { changes: { from, to, insert: insert }, range }; } From 1127925adaedc43609ded0284353149baa2b4711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:38:26 +0200 Subject: [PATCH 15/22] fit in 300 lines --- .../codeEditor/extensions/customKeyMaps.js | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index d1cf18294..70fcdc175 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -53,7 +53,6 @@ export async function formatCSS(view) { } catch (err) { console.error('Error formatting css: ', err); } - return true; } @@ -99,7 +98,6 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, r } const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix); - if(hasWrapper) { return { changes : [ @@ -119,18 +117,10 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, r } return { - changes : { - from, - to, - insert : prefix + selected + suffix - }, - range : EditorSelection.range( - from, - to + prefix.length + suffix.length - ) + changes : { from, to, insert: prefix + selected + suffix }, + range : EditorSelection.range(from, to + prefix.length + suffix.length) }; -} -); +}); const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; @@ -177,7 +167,6 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ // If the selection is already a Markdown link, unwrap it const existingLink = /^\[(.*)\]\((.*)\)$/.exec(selected); - if(existingLink) { const [, text, url] = existingLink; const insert = `${text} ${url}`; @@ -194,7 +183,6 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ const isUrl = /^(https?:\/\/|www\.)\S+$/i.test(selected) || /^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected); - if(isUrl) { const domain = selected .replace(/^https?:\/\//i, '') @@ -215,8 +203,7 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ changes : { from, to, insert }, range : EditorSelection.range(from, from + insert.length), }; -} -); +}); const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ const { from, to } = selectionRange; @@ -240,7 +227,6 @@ const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange insert : shouldRemovePrefix ? '' : prefix, }; }); - const changeSet = ChangeSet.of(changes, state.doc.length); return { From 16e5150bd0bade3c42b1f86575f4adb89a2bce92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:38:54 +0200 Subject: [PATCH 16/22] revert eslint change --- client/components/codeEditor/extensions/customKeyMaps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 70fcdc175..a0f4ce031 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 400 }] */ +/* eslint max-lines: ["error", { "max": 300 }] */ import { keymap } from '@codemirror/view'; import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands'; import { EditorSelection, ChangeSet } from '@codemirror/state'; From bbcef8eb098f7dc4b1b7bcb2c3a1707aa28d8c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:43:44 +0200 Subject: [PATCH 17/22] lint more --- client/components/codeEditor/extensions/customKeyMaps.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index a0f4ce031..74a36fc9d 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -259,7 +259,7 @@ const newBreak = (type)=>(view)=>forEachSelection(view, (state, range)=>{ }); export const generalKeymap = Prec.high(keymap.of([ - { key: 'Tab', run: insertTab }, //runs indentMore if multiple lines selected in a single selection + { key: 'Tab', run: insertTab }, { key: 'Shift-Tab', run: indentLess }, { key: 'Mod-z', run: undo }, //it may be unnecessary { key: 'Mod-Shift-z', run: redo }, @@ -273,7 +273,6 @@ export const cssKeymap = Prec.highest(keymap.of([ ])); export const markdownKeymap = Prec.highest(keymap.of([ - { key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold { key: 'Mod-i', run: wrapSelection('*', '*') }, // makeItalic { key: 'Mod-u', run: wrapSelection('', '') }, // makeUnderline @@ -285,7 +284,7 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Mod-m', run: wrapSelection('{{', '}}') }, { key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') }, { key: 'Mod-/', run: wrapSelection('') }, - { key: 'Mod-Shift-k', run: makeLink }, + { key: 'Mod-Shift-k', run: makeLink }, { key: 'Mod-Shift-u', run: makeList('- ') }, { key: 'Mod-Shift-o', run: makeList('1. ') }, { key: 'Shift-Mod-1', run: makeHeader(1) }, From 3347513e2b62689878c41eb26917b7912fcbbc4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:48:59 +0200 Subject: [PATCH 18/22] simplify makeheader --- .../codeEditor/extensions/customKeyMaps.js | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 74a36fc9d..d1c814417 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -205,7 +205,7 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ }; }); -const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ +const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ const { from, to } = selectionRange; const startLine = state.doc.lineAt(from); @@ -238,17 +238,6 @@ const makeList = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange }; }); -const makeHeader = (level)=>(view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to); - const insert = `${'#'.repeat(level)} ${selected}`; - if(selected.length === 0) { - return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from + insert.length) }; - } else { - return { changes: { from, to, insert: insert }, range }; - } -}); - const newBreak = (type)=>(view)=>forEachSelection(view, (state, range)=>{ const { from, to } = range; const insert = `\n\\${type}\n\n`; @@ -285,14 +274,14 @@ export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') }, { key: 'Mod-/', run: wrapSelection('') }, { key: 'Mod-Shift-k', run: makeLink }, - { key: 'Mod-Shift-u', run: makeList('- ') }, - { key: 'Mod-Shift-o', run: makeList('1. ') }, - { key: 'Shift-Mod-1', run: makeHeader(1) }, - { key: 'Shift-Mod-2', run: makeHeader(2) }, - { key: 'Shift-Mod-3', run: makeHeader(3) }, - { key: 'Shift-Mod-4', run: makeHeader(4) }, - { key: 'Shift-Mod-5', run: makeHeader(5) }, - { key: 'Shift-Mod-6', run: makeHeader(6) }, + { key: 'Mod-Shift-u', run: addPrefixAtLineStart('- ') }, + { key: 'Mod-Shift-o', run: addPrefixAtLineStart('1. ') }, + { key: 'Shift-Mod-1', run: addPrefixAtLineStart('# ') }, + { key: 'Shift-Mod-2', run: addPrefixAtLineStart('## ') }, + { key: 'Shift-Mod-3', run: addPrefixAtLineStart('### ') }, + { key: 'Shift-Mod-4', run: addPrefixAtLineStart('#### ') }, + { key: 'Shift-Mod-5', run: addPrefixAtLineStart('##### ') }, + { key: 'Shift-Mod-6', run: addPrefixAtLineStart('###### ') }, { key: 'Mod-Enter', run: newBreak('page') }, { key: 'Shift-Mod-Enter', run: newBreak('column') }, ])); From 15ebd6913acca41fdeb87a0fa05e184f07c05ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:52:47 +0200 Subject: [PATCH 19/22] lint more --- .../codeEditor/extensions/customKeyMaps.js | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index d1c814417..b1fd87855 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -73,14 +73,11 @@ const insertTab = (view)=>{ return forEachSelection(view, (state, range)=>{ const { from, to } = range; return { - changes : { - from, - to, - insert : ' ' // Insert two spaces, not a tab char! - }, - range : EditorSelection.cursor(from + 2) + changes : { from, to, insert: ' ' }, + range : EditorSelection.cursor(from + 2) }; }); + // Insert two spaces, not a tab char! }; const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, range)=>{ @@ -101,16 +98,8 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, r if(hasWrapper) { return { changes : [ - { - from : to - suffix.length, - to, - insert : '' - }, - { - from, - to : from + prefix.length, - insert : '' - } + { from: to - suffix.length, to, insert: '' }, + { from, to: from + prefix.length, insert: '' } ], range : EditorSelection.range(from, to - prefix.length - suffix.length) }; From 0eee4e04ffe99d13e138b0f74a632318a5cc592c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 27 Sep 2026 23:55:19 +0200 Subject: [PATCH 20/22] further lint (i'm crazy ok?) --- .../components/codeEditor/extensions/customKeyMaps.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index b1fd87855..3987942f3 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -35,11 +35,9 @@ export async function formatCSS(view) { ); if(formatted === code) return true; - const dom = view.dom; - dom.classList.add('cm-flash'); - + view.dom.classList.add('cm-flash'); setTimeout(()=>{ - dom.classList.remove('cm-flash'); + view.dom.classList.remove('cm-flash'); view.dispatch({ changes : { @@ -67,17 +65,15 @@ const insertTab = (view)=>{ const shouldIndent = view.state.selection.ranges.some((range)=>view.state.doc.lineAt(range.from).number !== view.state.doc.lineAt(range.to).number ); - if(shouldIndent) return indentMore(view); return forEachSelection(view, (state, range)=>{ const { from, to } = range; return { - changes : { from, to, insert: ' ' }, + changes : { from, to, insert: ' ' }, // Insert two spaces, not a tab char! range : EditorSelection.cursor(from + 2) }; }); - // Insert two spaces, not a tab char! }; const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, range)=>{ From cf174376034457e277688baa674ec5e245ad129f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Mon, 28 Sep 2026 23:17:10 +0200 Subject: [PATCH 21/22] move from to and selected to foreachselection --- .../codeEditor/extensions/customKeyMaps.js | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 3987942f3..74ad61ff2 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -55,7 +55,7 @@ export async function formatCSS(view) { } const forEachSelection = (view, fn)=>{ - view.dispatch(view.state.changeByRange((range)=>fn(view.state, range))); + view.dispatch(view.state.changeByRange((range)=>fn(view.state, range.from, range.to, view.state.doc.sliceString(range.from, range.to)))); return true; }; @@ -67,8 +67,7 @@ const insertTab = (view)=>{ ); if(shouldIndent) return indentMore(view); - return forEachSelection(view, (state, range)=>{ - const { from, to } = range; + return forEachSelection(view, (state, from, to, selected)=>{ return { changes : { from, to, insert: ' ' }, // Insert two spaces, not a tab char! range : EditorSelection.cursor(from + 2) @@ -76,10 +75,7 @@ const insertTab = (view)=>{ }); }; -const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to); - +const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{ const noSelection = from === to; if(noSelection) { const insert = prefix + suffix; @@ -107,9 +103,7 @@ const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, r }; }); -const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - +const makeNbsp = (view)=>forEachSelection(view, (state, from, to, selected)=>{ const prev2 = from >= 2 ? state.doc.sliceString(from - 2, from) : ''; const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>'; @@ -119,10 +113,7 @@ const makeNbsp = (view)=>forEachSelection(view, (state, range)=>{ }; }); -const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to); - +const makeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{ let insert = '{{width:10% }}'; const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { @@ -135,21 +126,17 @@ const makeSpace = (view)=>forEachSelection(view, (state, range)=>{ }; }); -const removeSpace = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to); +const removeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{ const match = selected.match(/^{{width:(\d+)% }}$/); if(match) { const percent = parseInt(match[1], 10) - 10; const insert = percent > 0 ? `{{width:${percent}% }}` : ''; - return { changes: { from, to, insert: insert } }; + if(insert.length === 0) return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from) }; + else return { changes: { from, to, insert: insert }, range }; } }); -const makeLink = (view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; - const selected = state.doc.sliceString(from, to).trim(); - +const makeLink = (view)=>forEachSelection(view, (state, from, to, selected)=>{ // If the selection is already a Markdown link, unwrap it const existingLink = /^\[(.*)\]\((.*)\)$/.exec(selected); if(existingLink) { @@ -190,9 +177,7 @@ const makeLink = (view)=>forEachSelection(view, (state, range)=>{ }; }); -const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, selectionRange)=>{ - const { from, to } = selectionRange; - +const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{ const startLine = state.doc.lineAt(from); const endLine = state.doc.lineAt(to); const selectedLines = []; @@ -223,8 +208,7 @@ const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, se }; }); -const newBreak = (type)=>(view)=>forEachSelection(view, (state, range)=>{ - const { from, to } = range; +const newBreak = (type)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{ const insert = `\n\\${type}\n\n`; return { changes : { from, to, insert }, From ac881c01d3122bb43aba74eb09d102acaaa98e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Mon, 28 Sep 2026 23:24:08 +0200 Subject: [PATCH 22/22] simplify removespace --- client/components/codeEditor/extensions/customKeyMaps.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 74ad61ff2..af7fa7131 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -131,8 +131,7 @@ const removeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=> if(match) { const percent = parseInt(match[1], 10) - 10; const insert = percent > 0 ? `{{width:${percent}% }}` : ''; - if(insert.length === 0) return { changes: { from, to, insert: insert }, range: EditorSelection.cursor(from) }; - else return { changes: { from, to, insert: insert }, range }; + return { changes: { from, to, insert: insert }, range: EditorSelection.range(from, from + insert.length) }; } });