From 9f9af4af434987d57c1339e2b899241a5919fda7 Mon Sep 17 00:00:00 2001 From: JeanCASPAR <55629512+JeanCASPAR@users.noreply.github.com> Date: Sat, 5 Jul 2025 23:23:10 +0200 Subject: [PATCH] Add snippet support for LaTeX and custom macro completer. The snippets are copied from the snippets for TeX. The current keywordCompleter for LaTeX is unused because the highlight rules do not give a list of keywords, but color every LaTeX macro, so the completer cannot use the rules to provide completion. This macro completer is inspired by the text completer, but autocomplete only macros (the current text completer do not consider \ to be a part of a word). The interface of Completer is changed so that the keywordCompleter can forward the triggerCharacters and identifiersRegexps of `session.$mode.completer`. --- ace-internal.d.ts | 4 +-- ace.d.ts | 4 +-- src/autocomplete/util.js | 29 +++++++++++++++----- src/ext/language_tools.js | 22 ++++++++++++++- src/mode/latex.js | 49 ++++++++++++++++++++++++++++++++++ src/snippets/latex.js | 4 +++ src/snippets/latex.snippets.js | 1 + 7 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 src/snippets/latex.js create mode 100644 src/snippets/latex.snippets.js diff --git a/ace-internal.d.ts b/ace-internal.d.ts index ebb8184c245..2b82b6deb8c 100644 --- a/ace-internal.d.ts +++ b/ace-internal.d.ts @@ -976,7 +976,7 @@ export namespace Ace { interface Completer { /** Regular expressions defining valid identifier characters for completion triggers */ - identifierRegexps?: Array, + identifierRegexps?: Array | ((editor: Editor) => Array), /** Main completion method that provides suggestions for the given context */ getCompletions(editor: Editor, @@ -999,7 +999,7 @@ export namespace Ace { /** Unique identifier for this completer */ id?: string; /** Characters that trigger autocompletion when typed */ - triggerCharacters?: string[]; + triggerCharacters?: string[] | ((editor: Editor) => string[]); /** Whether to hide inline preview text */ hideInlinePreview?: boolean; /** Custom insertion handler for completion items */ diff --git a/ace.d.ts b/ace.d.ts index 41cfd990513..b7f9f2bf23b 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -783,7 +783,7 @@ declare module "ace-code" { type CompleterCallback = (error: any, completions: Completion[]) => void; interface Completer { /** Regular expressions defining valid identifier characters for completion triggers */ - identifierRegexps?: Array; + identifierRegexps?: Array | ((editor: Editor) => Array); /** Main completion method that provides suggestions for the given context */ getCompletions(editor: Editor, session: EditSession, position: Point, prefix: string, callback: CompleterCallback): void; /** Returns documentation tooltip for a completion item */ @@ -797,7 +797,7 @@ declare module "ace-code" { /** Unique identifier for this completer */ id?: string; /** Characters that trigger autocompletion when typed */ - triggerCharacters?: string[]; + triggerCharacters?: string[] | ((editor: Editor) => string[]); /** Whether to hide inline preview text */ hideInlinePreview?: boolean; /** Custom insertion handler for completion items */ diff --git a/src/autocomplete/util.js b/src/autocomplete/util.js index 514e2da2743..0c9ce1ac420 100644 --- a/src/autocomplete/util.js +++ b/src/autocomplete/util.js @@ -54,10 +54,18 @@ exports.getCompletionPrefix = function (editor) { var prefix; editor.completers.forEach(function(completer) { if (completer.identifierRegexps) { - completer.identifierRegexps.forEach(function(identifierRegex) { - if (!prefix && identifierRegex) - prefix = this.retrievePrecedingIdentifier(line, pos.column, identifierRegex); - }.bind(this)); + var identifierRegexps; + if (completer.identifierRegexps instanceof Function) { + identifierRegexps = completer.identifierRegexps(editor); + } else { + identifierRegexps = completer.identifierRegexps; + } + if (identifierRegexps && Array.isArray(identifierRegexps)) { + identifierRegexps.forEach(function(identifierRegex) { + if (!prefix && identifierRegex) + prefix = this.retrievePrecedingIdentifier(line, pos.column, identifierRegex); + }.bind(this)); + } } }.bind(this)); return prefix || this.retrievePrecedingIdentifier(line, pos.column); @@ -73,8 +81,17 @@ exports.triggerAutocomplete = function (editor, previousChar) { ? editor.session.getPrecedingCharacter() : previousChar; return editor.completers.some((completer) => { - if (completer.triggerCharacters && Array.isArray(completer.triggerCharacters)) { - return completer.triggerCharacters.includes(previousChar); + if (completer.triggerCharacters) { + var triggerCharacters; + if (completer.triggerCharacters instanceof Function) { + triggerCharacters = completer.triggerCharacters(editor); + } else { + triggerCharacters = completer.triggerCharacters; + } + + if (triggerCharacters && Array.isArray(triggerCharacters)) { + return triggerCharacters.includes(previousChar); + } } }); }; diff --git a/src/ext/language_tools.js b/src/ext/language_tools.js index cc04a46c7b8..a76748bc3d3 100644 --- a/src/ext/language_tools.js +++ b/src/ext/language_tools.js @@ -37,6 +37,16 @@ var MarkerGroup = require("../marker_group").MarkerGroup; var textCompleter = require("../autocomplete/text_completer"); /**@type {import("../../ace-internal").Ace.Completer}*/ var keyWordCompleter = { + identifierRegexps(editor) { + var completer = editor.session.$mode.completer; + if (completer) { + if (completer.identifierRegexps instanceof Function) { + return completer.identifierRegexps(editor); + } else { + return completer.identifierRegexps; + } + } + }, getCompletions: function(editor, session, pos, prefix, callback) { if (session.$mode.completer) { return session.$mode.completer.getCompletions(editor, session, pos, prefix, callback); @@ -49,7 +59,17 @@ var keyWordCompleter = { }); callback(null, completions); }, - id: "keywordCompleter" + id: "keywordCompleter", + triggerCharacters(editor) { + var completer = editor.session.$mode.completer; + if (completer) { + if (completer.triggerCharacters instanceof Function) { + return completer.triggerCharacters(editor); + } else { + return completer.triggerCharacters; + } + } + } }; var transformSnippetTooltip = function(str) { diff --git a/src/mode/latex.js b/src/mode/latex.js index 731c83deeca..9bd4bbadbb7 100644 --- a/src/mode/latex.js +++ b/src/mode/latex.js @@ -19,6 +19,8 @@ oop.inherits(Mode, TextMode); this.lineCommentStart = "%"; this.$id = "ace/mode/latex"; + + this.snippetFileId = "ace/snippets/latex"; this.getMatching = function(session, row, column) { if (row == undefined) @@ -35,6 +37,53 @@ oop.inherits(Mode, TextMode); return this.foldingRules.latexBlock(session, row, column, true); } }; + + function wordDistances(doc, pos) { + var macroName = /\\[a-zA-Z0-9]*/g; + + var words = [...doc.getValue().matchAll(macroName)]; + var wordScores = Object.create(null); + + var textBefore = doc.getTextRange(Range.fromPoints({ + row: 0, + column: 0 + }, pos)); + var prefixPos = [...textBefore.matchAll(macroName)].length - 1; + + var words = [...doc.getValue().matchAll(macroName)]; + var wordScores = Object.create(null); + + var currentWord = words[prefixPos]; + + words.forEach(function (word, idx) { + if (!word || word === currentWord || word === "\\") return; + + var distance = Math.abs(prefixPos - idx); + var score = words.length - distance; + wordScores[word] = Math.max(score, wordScores[word] ?? 0); + }); + return wordScores; + } + + this.completer = { + identifierRegexps: [/[\\a-zA-Z0-0]/], + getCompletions: (editor, session, pos, prefix, callback) => { + var wordScores = wordDistances(session, pos); + var wordList = Object.keys(wordScores); + callback(null, wordList.map(function (word) { + return { + caption: word, + value: word, + score: wordScores[word], + meta: "macro", + completer: this, + completerId: this.id, + }; + }, this)); + }, + triggerCharacters: ["\\"], + id: "latexMacroCompleter", + }; }).call(Mode.prototype); exports.Mode = Mode; diff --git a/src/snippets/latex.js b/src/snippets/latex.js new file mode 100644 index 00000000000..bc8b6e1b1b5 --- /dev/null +++ b/src/snippets/latex.js @@ -0,0 +1,4 @@ +"use strict"; + +exports.snippetText = require("./latex.snippets"); +exports.scope = "latex"; diff --git a/src/snippets/latex.snippets.js b/src/snippets/latex.snippets.js new file mode 100644 index 00000000000..4108be97272 --- /dev/null +++ b/src/snippets/latex.snippets.js @@ -0,0 +1 @@ +module.exports = require("./tex.snippets"); \ No newline at end of file