diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index d11929f7aae..65f94a78032 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -322,7 +322,7 @@ function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], pat } function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, wordStart: number): FuzzyScore2 { - const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true }); + const score = fuzzyScore(query.normalized, query.normalizedLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true }); if (!score) { return NO_SCORE2; } @@ -811,7 +811,7 @@ export interface IPreparedQueryPiece { /** * In addition to the normalized path, will have - * whitespace and wildcards removed. + * whitespace, wildcards, quotes, ellipsis, and trailing hash characters removed. */ normalized: string; normalizedLowercase: string; @@ -905,7 +905,8 @@ function normalizeQuery(original: string): { pathNormalized: string; normalized: // - wildcards: are used for fuzzy matching // - whitespace: are used to separate queries // - ellipsis: sometimes used to indicate any path segments - const normalized = pathNormalized.replace(/[\*\u2026\s"]/g, ''); + // - trailing hash: used by some language servers (e.g. rust-analyzer) as query modifiers + const normalized = pathNormalized.replace(/[\*\u2026\s"]/g, '').replace(/(?<=.)#$/, ''); return { pathNormalized, diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 81a3773baa7..f120298e22b 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -1141,6 +1141,10 @@ suite('Fuzzy Scorer', () => { test('prepareQuery', () => { assert.strictEqual(prepareQuery(' f*a ').normalized, 'fa'); assert.strictEqual(prepareQuery(' f…a ').normalized, 'fa'); + assert.strictEqual(prepareQuery('main#').normalized, 'main'); + assert.strictEqual(prepareQuery('main#').original, 'main#'); + assert.strictEqual(prepareQuery('foo*').normalized, 'foo'); + assert.strictEqual(prepareQuery('foo*').original, 'foo*'); assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts'); assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); @@ -1295,5 +1299,59 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(score[1][1], 8); }); + test('Workspace symbol search with special characters (#, *)', function () { + // Simulates the scenario from the issue where rust-analyzer uses # and * as query modifiers + // The original query (with special chars) should reach the language server + // but normalized query (without special chars) should be used for fuzzy matching + + // Test #: User types "main#", language server returns "main" symbol + let query = prepareQuery('main#'); + assert.strictEqual(query.original, 'main#'); // Sent to language server + assert.strictEqual(query.normalized, 'main'); // Used for fuzzy matching + let [score, matches] = _doScore2('main', 'main#'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "main" symbol when query is "main#"'); + assert.ok(matches.length > 0); + + // Test *: User types "foo*", language server returns "foo" symbol + query = prepareQuery('foo*'); + assert.strictEqual(query.original, 'foo*'); // Sent to language server + assert.strictEqual(query.normalized, 'foo'); // Used for fuzzy matching + [score, matches] = _doScore2('foo', 'foo*'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "foo" symbol when query is "foo*"'); + assert.ok(matches.length > 0); + + // Test both: User types "MyClass#*", should match "MyClass" + query = prepareQuery('MyClass#*'); + assert.strictEqual(query.original, 'MyClass#*'); + assert.strictEqual(query.normalized, 'MyClass'); + [score, matches] = _doScore2('MyClass', 'MyClass#*'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "MyClass" symbol when query is "MyClass#*"'); + assert.ok(matches.length > 0); + + // Test fuzzy matching still works: User types "MC#", should match "MyClass" + query = prepareQuery('MC#'); + assert.strictEqual(query.original, 'MC#'); + assert.strictEqual(query.normalized, 'MC'); + [score, matches] = _doScore2('MyClass', 'MC#'); + assert.ok(typeof score === 'number' && score > 0, 'Should fuzzy match "MyClass" symbol when query is "MC#"'); + assert.ok(matches.length > 0); + + // Make sure leading # or # in the middle are not removed. + query = prepareQuery('#SpecialFunction'); + assert.strictEqual(query.original, '#SpecialFunction'); + assert.strictEqual(query.normalized, '#SpecialFunction'); + [score, matches] = _doScore2('#SpecialFunction', '#SpecialFunction'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "#SpecialFunction" symbol when query is "#SpecialFunction"'); + assert.ok(matches.length > 0); + + // Make sure standalone # is not removed + query = prepareQuery('#'); + assert.strictEqual(query.original, '#'); + assert.strictEqual(query.normalized, '#', 'Standalone # should not be removed'); + [score, matches] = _doScore2('#', '#'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "#" symbol when query is "#"'); + assert.ok(matches.length > 0); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index 9f8048c1a6f..4b91cd3f2d7 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -175,8 +175,6 @@ export class DetachedTerminalCommandMirror extends DetachedTerminalMirror implem return { lineCount: 0 }; } const detached = await this._getTerminal(); - detached.xterm.clearBuffer(); - detached.xterm.clearSearchDecorations?.(); await new Promise(resolve => { detached.xterm.write(vt.text, () => resolve()); }); @@ -238,8 +236,6 @@ export class DetachedTerminalSnapshotMirror extends DetachedTerminalMirror { return { lineCount: this._lastRenderedLineCount ?? output.lineCount }; } const terminal = await this._getTerminal(); - terminal.xterm.clearBuffer(); - terminal.xterm.clearSearchDecorations?.(); if (this._container) { this._applyTheme(this._container); }