From 502966a74c7767caca99eddd292d17aba092ba89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1rth=C3=A1zi=20Andr=C3=A1s?= Date: Fri, 14 Aug 2026 22:17:47 +0200 Subject: [PATCH] feat(autocomplete): match completions against their filterText The completion list matches every item against the text it displays, so a completion whose caption differs from what the user types to reach it is filtered out entirely. This is the common case for language server completions, where `CompletionItem.filterText` exists precisely to decouple the two: an `iframe` caption reached by typing ` -1) { penalty = fullMatchIndex; } else { - // caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf + // char iteration is faster in Chrome but slower in Firefox, so lets use indexOf for (var j = 0; j < needle.length; j++) { // TODO add penalty on case mismatch - var i1 = caption.indexOf(lower[j], lastIndex + 1); - var i2 = caption.indexOf(upper[j], lastIndex + 1); + var i1 = matchText.indexOf(lower[j], lastIndex + 1); + var i2 = matchText.indexOf(upper[j], lastIndex + 1); index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2; if (index < 0) continue loop; diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index f598f29bccb..0ba765cad30 100644 --- a/src/autocomplete_test.js +++ b/src/autocomplete_test.js @@ -201,6 +201,45 @@ module.exports = { assert.equal(editor.getValue(), "", + filterText: "" + } + ]; + callback(null, completions); + } + } + ]; + + editor.moveCursorTo(0, 1); + sendKey("ifra"); + await lang.sleep(10); + + // the prefix is ""); + done(); + }, "test: symbols after selection are not removed when replacement range is present": async function (done) { editor = initEditor("{}"); editor.completers = [ @@ -874,6 +913,36 @@ module.exports = { user.type(" value"); assert.equal(completer.popup.isOpen, true); }, + "test: should filter using filterText even if ignoreCaption true": function() { + editor = initEditor("hello world\n"); + + var completer = { + getCompletions: function (editor, session, pos, prefix, callback) { + var completions = [ + { + caption: "caption", + value: "value", + filterText: "filter" + } + ]; + callback(null, completions); + } + }; + + editor.completers = [completer]; + + var autocomplete = Autocomplete.for(editor); + autocomplete.ignoreCaption = true; + + // Neither the caption nor the value is matched once the completion carries a filterText. + user.type(" val"); + assert.equal(autocomplete.popup, undefined); + + // Should filter using the filterText instead. + user.type(" filt"); + assert.equal(autocomplete.popup.isOpen, true); + assert.equal(autocomplete.popup.data.length, 1); + }, "test: should skip filter if skipFilter flag is set to true in completion": function() { editor = initEditor("hello world\n"); diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts index 08e4c19aebf..31b9995c904 100644 --- a/types/ace-modules.d.ts +++ b/types/ace-modules.d.ts @@ -3437,6 +3437,13 @@ declare module "ace-code/src/autocomplete" { * - a boolean value to decide if the popup item is going to skip the filtering process done using prefix text. */ skipFilter?: boolean; + /** + * - the text this completion is filtered and scored against, in place of the text it + * displays. Follows the semantics of the language server protocol's `CompletionItem.filterText`, and takes precedence + * over the `ignoreCaption` option. An empty string is ignored. The match highlight in the popup is still derived from + * the caption, so a completion reached through text that does not occur in its caption is shown without a highlight. + */ + filterText?: string; /** * - An object specifying the range of text to be replaced with the new completion value (experimental) */