From 7d237d010302ebcac37368ebd94b3d8d2bb9f539 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Fri, 14 Apr 2023 09:58:55 +0100 Subject: [PATCH] [cm6] Only swallow text in autocomplete insertion if node is well-formed (#12562) * [cm6] Only swallow text in autocomplete insertion if node is well-formed * [cm6] Use longest common prefix heuristic for autocompletion in ill-formed nodes GitOrigin-RevId: 5adf3dc0314d86b267e2142a1250dece3ab29ef8 --- .../languages/latex/completions/apply.ts | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/languages/latex/completions/apply.ts b/services/web/frontend/js/features/source-editor/languages/latex/completions/apply.ts index a6a9ab80e9..44c5d6ccfe 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/completions/apply.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/completions/apply.ts @@ -7,6 +7,7 @@ import { } from '@codemirror/autocomplete' import { EditorView } from '@codemirror/view' import { prepareSnippetTemplate } from '../snippets' +import { ancestorNodeOfType } from '../../../utils/tree-query' // from https://github.com/codemirror/autocomplete/blob/main/src/closebrackets.ts export const nextChar = (doc: Text, pos: number) => { @@ -39,11 +40,31 @@ export const createCommandApplier = view.dispatch(insertCompletionText(view.state, text, from, to)) } +const longestCommonPrefix = (...strs: string[]) => { + if (strs.length === 0) { + return 0 + } + const minLength = Math.min(...strs.map(str => str.length)) + for (let i = 0; i < minLength; ++i) { + for (let j = 1; j < strs.length; ++j) { + if (strs[j][i] !== strs[0][i]) { + return i + } + } + } + return minLength +} + // apply a completed required parameter, adding a closing brace and extending the range if needed export const createRequiredParameterApplier = (text: string) => (view: EditorView, completion: Completion, from: number, to: number) => { const { doc } = view.state + const argumentNode = ancestorNodeOfType(view.state, from, '$Argument') + const isWellFormedArgumentNode = + argumentNode && + argumentNode.getChild('OpenBrace') && + argumentNode.getChild('CloseBrace') // add a closing brace if needed if (nextChar(doc, to) !== '}') { @@ -51,10 +72,21 @@ export const createRequiredParameterApplier = text += '}' } - // extend over subsequent text that isn't a brace, space, or comma - const match = doc.sliceString(to, doc.lineAt(from).to).match(/^[^}\s,]+/) - if (match) { - to += match[0].length + if (isWellFormedArgumentNode) { + // extend over subsequent text that isn't a brace, space, or comma + const match = doc + .sliceString(to, Math.min(doc.lineAt(from).to, argumentNode.to)) + .match(/^[^}\s,]+/) + if (match) { + to += match[0].length + } + } else { + // Ensure we don't swallow a closing brace + const restOfLine = doc + .sliceString(to, Math.min(doc.lineAt(from).to, from + text.length)) + .split('}')[0] + + to += longestCommonPrefix(text.slice(to - from), restOfLine) } }