Compare commits

...
10 Commits
Author SHA1 Message Date
dualfroz e0bc77be47 Describe this repository 2026-10-22 21:05:00 +02:00
dualfroz 6d1a2764c0 yaml: fix the anchor group in increaseIndentPattern
The pattern had an HTML-escaped "&" where it meant "&", so a key
followed by an anchor (foo: &anchor) never increased the indent.
2026-10-17 04:24:09 +02:00
upstream da856c50ae Upstream microsoft/vscode state of the touched files before 187c44cba3 2026-10-17 04:24:09 +02:00
dualfroz fd90b7606a terminal: don't wrap $() icons of contributed profiles again
Extensions may contribute a terminal profile icon as "$(debug)", which
is the documented syntax and what the profile quick pick already
parses. The settings enum description wrapped that string in $() once
more, so the default profile setting showed $($(debug)).

Parse string icons with ThemeIcon.fromString first and fall back to
the plain id.
2026-10-17 04:24:09 +02:00
upstream 6c80ee4884 Upstream microsoft/vscode state of the touched files before d11f02896b 2026-10-17 04:24:09 +02:00
dualfroz 9bba2ad2af search: always pass a session to file search providers
FileSearchProviderOptions.session is declared as an object, but
searches without a cache key got undefined, so providers that key a
WeakMap on the session threw.

Give those searches their own session and cancel it once the search
is done, like clearCache does for cached sessions.
2026-10-17 04:24:09 +02:00
upstream 6cf5d04662 Upstream microsoft/vscode state of the touched files before b3e78797d4 2026-10-17 04:24:09 +02:00
dualfroz fd91eacb0d chat: ellipsize long model names in quick chat
Quick chat puts the input toolbars in the same row as the editor and
gives the editor whatever width the toolbars leave. A long model name
made the toolbars wider than the row, so the editor shrank to nothing
and the send button and the side actions were pushed out of view.

Cap the model name width in quick chat so it ellipsizes, like it does
in the chat view.
2026-10-17 03:49:19 +02:00
upstream a9aed84b36 Upstream microsoft/vscode state of the touched files before 8907b7e4d7 2026-10-17 03:49:19 +02:00
dualfroz 56c9a4d5f6 git: let git parse the commit.gpgsign boolean 2026-10-13 23:18:11 +02:00
11 changed files with 7810 additions and 5 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
# Contributions to microsoft/vscode
This repository holds only my commits to the upstream project, with their original author, date and diff.
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
+2 -2
View File
@@ -2464,8 +2464,8 @@ export class CommandCenter {
opts.signCommit = undefined; opts.signCommit = undefined;
if (config.get<boolean>('enableCommitSigning') === true) { if (config.get<boolean>('enableCommitSigning') === true) {
const commitGpgSign = await repository.getConfig('commit.gpgsign'); const commitGpgSign = await repository.getConfig('commit.gpgsign', 'bool');
opts.signCommit = /^(false|no|off|0)$/i.test(commitGpgSign) ? undefined : true; opts.signCommit = commitGpgSign === 'false' ? undefined : true;
} }
if (config.get<boolean>('alwaysSignOff')) { if (config.get<boolean>('alwaysSignOff')) {
+5 -1
View File
@@ -1411,13 +1411,17 @@ export class Repository {
return this.git.spawn(args, options); return this.git.spawn(args, options);
} }
async config(command: string, scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> { async config(command: string, scope: string, key: string, value: any = null, options: SpawnOptions = {}, type?: string): Promise<string> {
const args = ['config', `--${command}`]; const args = ['config', `--${command}`];
if (scope) { if (scope) {
args.push(`--${scope}`); args.push(`--${scope}`);
} }
if (type) {
args.push(`--${type}`);
}
args.push(key); args.push(key);
if (value) { if (value) {
+2 -2
View File
@@ -1146,8 +1146,8 @@ export class Repository implements Disposable {
return this.run(Operation.Config(true), () => this.repository.getConfigs('local')); return this.run(Operation.Config(true), () => this.repository.getConfigs('local'));
} }
getConfig(key: string): Promise<string> { getConfig(key: string, type?: string): Promise<string> {
return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key)); return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key, null, {}, type));
} }
getGlobalConfig(key: string): Promise<string> { getGlobalConfig(key: string): Promise<string> {
@@ -0,0 +1,35 @@
{
"comments": {
"lineComment": "#"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"folding": {
"offSide": true,
"markers": {
"start": "^\\s*#\\s*region\\b",
"end": "^\\s*#\\s*endregion\\b"
}
},
"indentationRules": {
"increaseIndentPattern": "^\\s*.*(:|-) ?(&\\w+)?(\\{[^}\"']*|\\([^)\"']*)?$",
"decreaseIndentPattern": "^\\s+\\}$"
}
}
@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../base/common/codicons.js';
import { isUriComponents, URI } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { IExtensionTerminalProfile, ITerminalProfile, TerminalIcon } from './terminal.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { isObject, isString, type SingleOrMany } from '../../../base/common/types.js';
export function createProfileSchemaEnums(detectedProfiles: ITerminalProfile[], extensionProfiles?: readonly IExtensionTerminalProfile[]): {
values: (string | null)[] | undefined;
markdownDescriptions: string[] | undefined;
} {
const result: { name: string | null; description: string }[] = [{
name: null,
description: localize('terminalAutomaticProfile', 'Automatically detect the default')
}];
result.push(...detectedProfiles.map(e => {
return {
name: e.profileName,
description: createProfileDescription(e)
};
}));
if (extensionProfiles) {
result.push(...extensionProfiles.map(extensionProfile => {
return {
name: extensionProfile.title,
description: createExtensionProfileDescription(extensionProfile)
};
}));
}
return {
values: result.map(e => e.name),
markdownDescriptions: result.map(e => e.description)
};
}
function createProfileDescription(profile: ITerminalProfile): string {
let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.profileName}\n- path: ${profile.path}`;
if (profile.args) {
if (isString(profile.args)) {
description += `\n- args: "${profile.args}"`;
} else {
description += `\n- args: [${profile.args.length === 0 ? '' : `'${profile.args.join(`','`)}'`}]`;
}
}
if (profile.overrideName !== undefined) {
description += `\n- overrideName: ${profile.overrideName}`;
}
if (profile.color) {
description += `\n- color: ${profile.color}`;
}
if (profile.env) {
description += `\n- env: ${JSON.stringify(profile.env)}`;
}
return description;
}
function createExtensionProfileDescription(profile: IExtensionTerminalProfile): string {
const icon = isString(profile.icon) ? ThemeIcon.fromString(profile.icon) ?? profile.icon : profile.icon;
const description = `$(${ThemeIcon.isThemeIcon(icon) ? icon.id : icon ? icon : Codicon.terminal.id}) ${profile.title}\n- extensionIdentifier: ${profile.extensionIdentifier}`;
return description;
}
export function terminalProfileArgsMatch(args1: SingleOrMany<string> | undefined, args2: SingleOrMany<string> | undefined): boolean {
if (!args1 && !args2) {
return true;
} else if (isString(args1) && isString(args2)) {
return args1 === args2;
} else if (Array.isArray(args1) && Array.isArray(args2)) {
if (args1.length !== args2.length) {
return false;
}
for (let i = 0; i < args1.length; i++) {
if (args1[i] !== args2[i]) {
return false;
}
}
return true;
}
return false;
}
export function terminalIconsEqual(a?: TerminalIcon, b?: TerminalIcon): boolean {
if (!a && !b) {
return true;
} else if (!a || !b) {
return false;
}
if (ThemeIcon.isThemeIcon(a) && ThemeIcon.isThemeIcon(b)) {
return a.id === b.id && a.color === b.color;
}
if (
isObject(a) && !URI.isUri(a) && !ThemeIcon.isThemeIcon(a) &&
isObject(b) && !URI.isUri(b) && !ThemeIcon.isThemeIcon(b)
) {
const castedA = (a as { light: unknown; dark: unknown });
const castedB = (b as { light: unknown; dark: unknown });
if ((URI.isUri(castedA.light) || isUriComponents(castedA.light)) && (URI.isUri(castedA.dark) || isUriComponents(castedA.dark))
&& (URI.isUri(castedB.light) || isUriComponents(castedB.light)) && (URI.isUri(castedB.dark) || isUriComponents(castedB.dark))) {
return castedA.light.path === castedB.light.path && castedA.dark.path === castedB.dark.path;
}
}
if ((URI.isUri(a) && URI.isUri(b)) || (isUriComponents(a) || isUriComponents(b))) {
const castedA = (a as { scheme: unknown; path: unknown });
const castedB = (b as { scheme: unknown; path: unknown });
return castedA.path === castedB.path && castedA.scheme === castedB.scheme;
}
return false;
}
@@ -0,0 +1,118 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import { Codicon } from '../../../../base/common/codicons.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { IExtensionTerminalProfile, ITerminalProfile } from '../../common/terminal.js';
import { createProfileSchemaEnums } from '../../common/terminalProfiles.js';
suite('terminalProfiles', () => {
ensureNoDisposablesAreLeakedInTestSuite();
suite('createProfileSchemaEnums', () => {
test('should return an empty array when there are no profiles', () => {
deepStrictEqual(createProfileSchemaEnums([]), {
values: [
null
],
markdownDescriptions: [
'Automatically detect the default'
]
});
});
test('should return a single entry when there is one profile', () => {
const profile: ITerminalProfile = {
profileName: 'name',
path: 'path',
isDefault: true
};
deepStrictEqual(createProfileSchemaEnums([profile]), {
values: [
null,
'name'
],
markdownDescriptions: [
'Automatically detect the default',
'$(terminal) name\n- path: path'
]
});
});
test('should show all profile information', () => {
const profile: ITerminalProfile = {
profileName: 'name',
path: 'path',
isDefault: true,
args: ['a', 'b'],
color: 'terminal.ansiRed',
env: {
c: 'd',
e: 'f'
},
icon: Codicon.zap,
overrideName: true
};
deepStrictEqual(createProfileSchemaEnums([profile]), {
values: [
null,
'name'
],
markdownDescriptions: [
'Automatically detect the default',
`$(zap) name\n- path: path\n- args: ['a','b']\n- overrideName: true\n- color: terminal.ansiRed\n- env: {\"c\":\"d\",\"e\":\"f\"}`
]
});
});
test('should return a multiple entries when there are multiple profiles', () => {
const profile1: ITerminalProfile = {
profileName: 'name',
path: 'path',
isDefault: true
};
const profile2: ITerminalProfile = {
profileName: 'foo',
path: 'bar',
isDefault: false
};
deepStrictEqual(createProfileSchemaEnums([profile1, profile2]), {
values: [
null,
'name',
'foo'
],
markdownDescriptions: [
'Automatically detect the default',
'$(terminal) name\n- path: path',
'$(terminal) foo\n- path: bar'
]
});
});
test('should not wrap an extension profile icon that already uses the $() syntax', () => {
const extensionProfiles: IExtensionTerminalProfile[] = [{
extensionIdentifier: 'ms-vscode.js-debug',
id: 'extension.js-debug.debugTerminal',
title: 'JavaScript Debug Terminal',
icon: '$(debug)'
}, {
extensionIdentifier: 'publisher.extension',
id: 'extension.terminal',
title: 'Extension Terminal',
icon: 'zap'
}];
deepStrictEqual(createProfileSchemaEnums([], extensionProfiles), {
values: [
null,
'JavaScript Debug Terminal',
'Extension Terminal'
],
markdownDescriptions: [
'Automatically detect the default',
'$(debug) JavaScript Debug Terminal\n- extensionIdentifier: ms-vscode.js-debug',
'$(zap) Extension Terminal\n- extensionIdentifier: publisher.extension'
]
});
});
});
});
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,592 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { extname, join } from '../../../../../base/common/path.js';
import assert from 'assert';
import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ILanguageConfigurationService } from '../../../../../editor/common/languages/languageConfigurationRegistry.js';
import { getReindentEditOperations } from '../../../../../editor/contrib/indentation/common/indentation.js';
import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextModel } from '../../../../../editor/test/common/testTextModel.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ILanguageConfiguration, LanguageConfigurationFileHandler } from '../../common/languageConfigurationExtensionPoint.js';
import { parse } from '../../../../../base/common/json.js';
import { IRange } from '../../../../../editor/common/core/range.js';
import { ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js';
import { trimTrailingWhitespace } from '../../../../../editor/common/commands/trimTrailingWhitespaceCommand.js';
import { execSync } from 'child_process';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from '../../../../../editor/common/languages.js';
import { NullState } from '../../../../../editor/common/languages/nullTokenize.js';
import { MetadataConsts, StandardTokenType } from '../../../../../editor/common/encodedTokenAttributes.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { FileAccess } from '../../../../../base/common/network.js';
function getIRange(range: IRange): IRange {
return {
startLineNumber: range.startLineNumber,
startColumn: range.startColumn,
endLineNumber: range.endLineNumber,
endColumn: range.endColumn
};
}
const enum LanguageId {
TypeScript = 'ts-test',
Yaml = 'yaml-test'
}
function forceTokenizationFromLineToLine(model: ITextModel, startLine: number, endLine: number): void {
for (let line = startLine; line <= endLine; line++) {
model.tokenization.forceTokenization(line);
}
}
function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable {
const disposables = new DisposableStore();
const languageService = instantiationService.get(ILanguageService);
disposables.add(registerLanguageConfiguration(instantiationService, languageId));
disposables.add(languageService.registerLanguage({ id: languageId }));
return disposables;
}
function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable {
const languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
let configPath: string;
switch (languageId) {
case LanguageId.TypeScript:
configPath = FileAccess.asFileUri('vs/workbench/contrib/codeEditor/test/node/language-configuration.json').fsPath;
break;
case LanguageId.Yaml:
configPath = join(FileAccess.asFileUri('').fsPath, '..', 'extensions', 'yaml', 'language-configuration.json');
break;
default:
throw new Error('Unknown languageId');
}
const configContent = fs.readFileSync(configPath, { encoding: 'utf-8' });
const parsedConfig = <ILanguageConfiguration>parse(configContent, []);
const languageConfig = LanguageConfigurationFileHandler.extractValidConfig(languageId, parsedConfig);
return languageConfigurationService.register(languageId, languageConfig);
}
interface StandardTokenTypeData {
startIndex: number;
standardTokenType: StandardTokenType;
}
function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: LanguageId): IDisposable {
let lineIndex = 0;
const languageService = instantiationService.get(ILanguageService);
const tokenizationSupport: ITokenizationSupport = {
getInitialState: () => NullState,
tokenize: undefined!,
tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {
const tokensOnLine = tokens[lineIndex++];
const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId);
const result = new Uint32Array(2 * tokensOnLine.length);
for (let i = 0; i < tokensOnLine.length; i++) {
result[2 * i] = tokensOnLine[i].startIndex;
result[2 * i + 1] =
((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET)
| (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET));
}
return new EncodedTokenizationResult(result, [], state);
}
};
return TokenizationRegistry.register(languageId, tokenizationSupport);
}
suite('Language Configuration Parsing', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('Folding markers support object regex syntax with flags', () => {
const parsed = LanguageConfigurationFileHandler.extractValidConfig('testLang', {
folding: {
markers: {
start: { pattern: '^\\s*#region\\b', flags: 'i' },
end: { pattern: '^\\s*#endregion\\b', flags: 'i' }
}
}
});
assert.ok(parsed.folding?.markers);
assert.strictEqual(parsed.folding?.markers?.start.flags, 'i');
assert.strictEqual(parsed.folding?.markers?.end.flags, 'i');
assert.ok(parsed.folding?.markers?.start.test('#REGION'));
assert.ok(parsed.folding?.markers?.end.test('#ENDREGION'));
});
test('Auto-closing pairs support beforeText regex syntax with flags', () => {
const parsed = LanguageConfigurationFileHandler.extractValidConfig('testLang', {
autoClosingPairs: [{
open: 'begin',
close: 'end',
beforeText: { pattern: '(?:^|\\s)begin$', flags: 'i' }
}]
});
assert.deepStrictEqual(
parsed.autoClosingPairs?.map(pair => ({
open: pair.open,
close: pair.close,
beforeText: pair.beforeText ? { source: pair.beforeText.source, flags: pair.beforeText.flags } : undefined
})),
[{ open: 'begin', close: 'end', beforeText: { source: '(?:^|\\s)begin$', flags: 'i' } }]
);
});
});
suite('Auto-Reindentation - TypeScript/JavaScript', () => {
const languageId = LanguageId.TypeScript;
const options: IRelaxedTextModelCreationOptions = {};
let disposables: DisposableStore;
let instantiationService: TestInstantiationService;
let languageConfigurationService: ILanguageConfigurationService;
setup(() => {
disposables = new DisposableStore();
instantiationService = createModelServices(disposables);
languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
disposables.add(instantiationService);
disposables.add(registerLanguage(instantiationService, languageId));
disposables.add(registerLanguageConfiguration(instantiationService, languageId));
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
// Test which can be ran to find cases of incorrect indentation...
test.skip('Find Cases of Incorrect Indentation with the Reindent Lines Command', () => {
// ./scripts/test.sh --inspect --grep='Find Cases of Incorrect Indentation with the Reindent Lines Command' --timeout=15000
function walkDirectoryAndReindent(directory: string, languageId: string) {
const files = fs.readdirSync(directory, { withFileTypes: true });
const directoriesToRecurseOn: string[] = [];
for (const file of files) {
if (file.isDirectory()) {
directoriesToRecurseOn.push(join(directory, file.name));
} else {
const filePathName = join(directory, file.name);
const fileExtension = extname(filePathName);
if (fileExtension !== '.ts') {
continue;
}
const fileContents = fs.readFileSync(filePathName, { encoding: 'utf-8' });
const modelOptions: IRelaxedTextModelCreationOptions = {
tabSize: 4,
insertSpaces: false
};
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, modelOptions));
const lineCount = model.getLineCount();
const editOperations: ISingleEditOperation[] = [];
for (let line = 1; line <= lineCount - 1; line++) {
/*
NOTE: Uncomment in order to ignore incorrect JS DOC indentation
const lineContent = model.getLineContent(line);
const trimmedLineContent = lineContent.trim();
if (trimmedLineContent.length === 0 || trimmedLineContent.startsWith('*') || trimmedLineContent.startsWith('/*')) {
continue;
}
*/
const lineContent = model.getLineContent(line);
const trimmedLineContent = lineContent.trim();
if (trimmedLineContent.length === 0) {
continue;
}
const editOperation = getReindentEditOperations(model, languageConfigurationService, line, line + 1);
/*
NOTE: Uncomment in order to see actual incorrect indentation diff
model.applyEdits(editOperation);
*/
editOperations.push(...editOperation);
}
model.applyEdits(editOperations);
model.applyEdits(trimTrailingWhitespace(model, [], true));
fs.writeFileSync(filePathName, model.getValue());
}
}
for (const directory of directoriesToRecurseOn) {
walkDirectoryAndReindent(directory, languageId);
}
}
walkDirectoryAndReindent('/Users/aiday/Desktop/Test/vscode-test', 'ts-test');
const output = execSync('cd /Users/aiday/Desktop/Test/vscode-test && git diff --shortstat', { encoding: 'utf-8' });
console.log('\ngit diff --shortstat:\n', output);
});
// Unit tests for increase and decrease indent patterns...
/**
* First increase indent and decrease indent patterns:
*
* - decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/
* - In (https://macromates.com/manual/en/appendix)
* Either we have white space before the closing bracket, or we have a multi line comment ending on that line followed by whitespaces
* This is followed by any character.
* Textmate decrease indent pattern is as follows: /^(.*\*\/)?\s*\}[;\s]*$/
* Presumably allowing multi line comments ending on that line implies that } is itself not part of a multi line comment
*
* - increaseIndentPattern: /^.*\{[^}"']*$/
* - In (https://macromates.com/manual/en/appendix)
* This regex means that we increase the indent when we have any characters followed by the opening brace, followed by characters
* except for closing brace }, double quotes " or single quote '.
* The } is checked in order to avoid the indentation in the following case `int arr[] = { 1, 2, 3 };`
* The double quote and single quote are checked in order to avoid the indentation in the following case: str = "foo {";
*/
test('Issue #25437', () => {
// issue: https://github.com/microsoft/vscode/issues/25437
// fix: https://github.com/microsoft/vscode/commit/8c82a6c6158574e098561c28d470711f1b484fc8
// explanation: var foo = `{`; should not increase indentation
// increaseIndentPattern: /^.*\{[^}"']*$/ -> /^.*\{[^}"'`]*$/
const fileContents = [
'const foo = `{`;',
' ',
].join('\n');
const tokens: StandardTokenTypeData[][] = [
[
{ startIndex: 0, standardTokenType: StandardTokenType.Other },
{ startIndex: 5, standardTokenType: StandardTokenType.Other },
{ startIndex: 6, standardTokenType: StandardTokenType.Other },
{ startIndex: 9, standardTokenType: StandardTokenType.Other },
{ startIndex: 10, standardTokenType: StandardTokenType.Other },
{ startIndex: 11, standardTokenType: StandardTokenType.Other },
{ startIndex: 12, standardTokenType: StandardTokenType.String },
{ startIndex: 13, standardTokenType: StandardTokenType.String },
{ startIndex: 14, standardTokenType: StandardTokenType.String },
{ startIndex: 15, standardTokenType: StandardTokenType.Other },
{ startIndex: 16, standardTokenType: StandardTokenType.Other }
],
[
{ startIndex: 0, standardTokenType: StandardTokenType.Other },
{ startIndex: 4, standardTokenType: StandardTokenType.Other }]
];
disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId));
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
forceTokenizationFromLineToLine(model, 1, 2);
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
'startLineNumber': 2,
'startColumn': 1,
'endLineNumber': 2,
'endColumn': 5,
});
assert.deepStrictEqual(operation.text, '');
});
test('Enriching the hover', () => {
// issue: -
// fix: https://github.com/microsoft/vscode/commit/19ae0932c45b1096443a8c1335cf1e02eb99e16d
// explanation:
// - decrease indent on ) and ] also
// - increase indent on ( and [ also
// decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ -> /^(.*\*\/)?\s*[\}\]\)].*$/
// increaseIndentPattern: /^.*\{[^}"'`]*$/ -> /^.*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
let fileContents = [
'function foo(',
' bar: string',
' ){}',
].join('\n');
let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
let operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
'startLineNumber': 3,
'startColumn': 1,
'endLineNumber': 3,
'endColumn': 5,
});
assert.deepStrictEqual(operation.text, '');
fileContents = [
'function foo(',
'bar: string',
'){}',
].join('\n');
model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
'startLineNumber': 2,
'startColumn': 1,
'endLineNumber': 2,
'endColumn': 1,
});
assert.deepStrictEqual(operation.text, ' ');
});
test('Issue #86176', () => {
// issue: https://github.com/microsoft/vscode/issues/86176
// fix: https://github.com/microsoft/vscode/commit/d89e2e17a5d1ba37c99b1d3929eb6180a5bfc7a8
// explanation: When quotation marks are present on the first line of an if statement or for loop, following line should not be indented
// increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/
// explanation: after open brace, do not decrease indent if it is followed on the same line by "<whitespace characters> // <any characters>"
// todo@aiday-mar: should also apply for when it follows ( and [
const fileContents = [
`if () { // '`,
`x = 4`,
`}`
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
'startLineNumber': 2,
'startColumn': 1,
'endLineNumber': 2,
'endColumn': 1,
});
assert.deepStrictEqual(operation.text, ' ');
});
test('Issue #141816', () => {
// issue: https://github.com/microsoft/vscode/issues/141816
// fix: https://github.com/microsoft/vscode/pull/141997/files
// explanation: if (, [, {, is followed by a forward slash then assume we are in a regex pattern, and do not indent
// increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/
// -> Final current increase indent pattern at of writing
const fileContents = [
'const r = /{/;',
' ',
].join('\n');
const tokens: StandardTokenTypeData[][] = [
[
{ startIndex: 0, standardTokenType: StandardTokenType.Other },
{ startIndex: 5, standardTokenType: StandardTokenType.Other },
{ startIndex: 6, standardTokenType: StandardTokenType.Other },
{ startIndex: 7, standardTokenType: StandardTokenType.Other },
{ startIndex: 8, standardTokenType: StandardTokenType.Other },
{ startIndex: 9, standardTokenType: StandardTokenType.RegEx },
{ startIndex: 10, standardTokenType: StandardTokenType.RegEx },
{ startIndex: 11, standardTokenType: StandardTokenType.RegEx },
{ startIndex: 12, standardTokenType: StandardTokenType.RegEx },
{ startIndex: 13, standardTokenType: StandardTokenType.Other },
{ startIndex: 14, standardTokenType: StandardTokenType.Other }
],
[
{ startIndex: 0, standardTokenType: StandardTokenType.Other },
{ startIndex: 4, standardTokenType: StandardTokenType.Other }
]
];
disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId));
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
forceTokenizationFromLineToLine(model, 1, 2);
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 1);
const operation = editOperations[0];
assert.deepStrictEqual(getIRange(operation.range), {
'startLineNumber': 2,
'startColumn': 1,
'endLineNumber': 2,
'endColumn': 4,
});
assert.deepStrictEqual(operation.text, '');
});
test('Issue #29886', () => {
// issue: https://github.com/microsoft/vscode/issues/29886
// fix: https://github.com/microsoft/vscode/commit/7910b3d7bab8a721aae98dc05af0b5e1ea9d9782
// decreaseIndentPattern: /^(.*\*\/)?\s*[\}\]\)].*$/ -> /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/
// -> Final current decrease indent pattern at the time of writing
// explanation: Positive lookahead: (?= «pattern») matches if pattern matches what comes after the current location in the input string.
// Negative lookahead: (?! «pattern») matches if pattern does not match what comes after the current location in the input string
// The change proposed is to not decrease the indent if there is a multi-line comment ending on the same line before the closing parentheses
const fileContents = [
'function foo() {',
' bar(/* */)',
'};',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test('Issue #209859: do not do reindentation for tokens inside of a string', () => {
// issue: https://github.com/microsoft/vscode/issues/209859
const tokens: StandardTokenTypeData[][] = [
[
{ startIndex: 0, standardTokenType: StandardTokenType.Other },
{ startIndex: 12, standardTokenType: StandardTokenType.String },
],
[
{ startIndex: 0, standardTokenType: StandardTokenType.String },
],
[
{ startIndex: 0, standardTokenType: StandardTokenType.String },
],
[
{ startIndex: 0, standardTokenType: StandardTokenType.String },
]
];
disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId));
const fileContents = [
'const foo = `some text',
' which is strangely',
' indented. It should',
' not be reindented.`'
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
forceTokenizationFromLineToLine(model, 1, 4);
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
// Failing tests inferred from the current regexes...
test.skip('Incorrect deindentation after `*/}` string', () => {
// explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent
// Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent
const fileContents = [
`const obj = {`,
` obj1: {`,
` brace : '*/}'`,
` }`,
`}`,
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
// Failing tests from issues...
test.skip('Issue #56275', () => {
// issue: https://github.com/microsoft/vscode/issues/56275
// explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent
// Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent
let fileContents = [
'function foo() {',
' var bar = (/b*/);',
'}',
].join('\n');
let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
fileContents = [
'function foo() {',
' var bar = "/b*/)";',
'}',
].join('\n');
model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test.skip('Issue #116843', () => {
// issue: https://github.com/microsoft/vscode/issues/116843
// related: https://github.com/microsoft/vscode/issues/43244
// explanation: When you have an arrow function, you don't have { or }, but you would expect indentation to still be done in that way
// TODO: requires exploring indent/outdent pairs instead
const fileContents = [
'const add1 = (n) =>',
' n + 1;',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test.skip('Issue #185252', () => {
// issue: https://github.com/microsoft/vscode/issues/185252
// explanation: Reindenting the comment correctly
const fileContents = [
'/*',
' * This is a comment.',
' */',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
test.skip('Issue 43244: incorrect indentation when signature of function call spans several lines', () => {
// issue: https://github.com/microsoft/vscode/issues/43244
const fileContents = [
'function callSomeOtherFunction(one: number, two: number) { }',
'function someFunction() {',
' callSomeOtherFunction(4,',
' 5)',
'}',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.length, 0);
});
});
suite('Auto-Reindentation - YAML', () => {
const languageId = LanguageId.Yaml;
const options: IRelaxedTextModelCreationOptions = { tabSize: 2, indentSize: 2, insertSpaces: true };
let disposables: DisposableStore;
let instantiationService: TestInstantiationService;
let languageConfigurationService: ILanguageConfigurationService;
setup(() => {
disposables = new DisposableStore();
instantiationService = createModelServices(disposables);
languageConfigurationService = instantiationService.get(ILanguageConfigurationService);
disposables.add(instantiationService);
disposables.add(registerLanguage(instantiationService, languageId));
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
test('Issue #330394: indent after a key with an anchor', () => {
// issue: https://github.com/microsoft/vscode/issues/330394
const fileContents = [
'foo: &anchor',
'bar: 1',
].join('\n');
const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options));
const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());
assert.deepStrictEqual(editOperations.map(operation => ({ range: getIRange(operation.range), text: operation.text })), [{
range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 },
text: ' '
}]);
});
});
@@ -0,0 +1,433 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from '../../../../base/common/path.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
import * as strings from '../../../../base/common/strings.js';
import * as glob from '../../../../base/common/glob.js';
import * as resources from '../../../../base/common/resources.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { URI } from '../../../../base/common/uri.js';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from './search.js';
import { FileSearchProviderFolderOptions, FileSearchProvider2, FileSearchProviderOptions } from './searchExtTypes.js';
import { OldFileSearchProviderConverter } from './searchExtConversionTypes.js';
import { FolderQuerySearchTree } from './folderQuerySearchTree.js';
interface IInternalFileMatch {
base: URI;
original?: URI;
relativePath?: string; // Not present for extraFiles or absolute path matches
basename: string;
size?: number;
}
interface IDirectoryEntry {
base: URI;
relativePath: string;
basename: string;
}
interface FolderQueryInfo {
queryTester: QueryGlobTester;
noSiblingsClauses: boolean;
folder: URI;
tree: IDirectoryTree;
}
interface IDirectoryTree {
rootEntries: IDirectoryEntry[];
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}
class FileSearchEngine {
private filePattern?: string;
private includePattern?: glob.ParsedExpression;
private maxResults?: number;
private exists?: boolean;
private isLimitHit = false;
private resultCount = 0;
private isCanceled = false;
private activeCancellationTokens: Set<CancellationTokenSource>;
private globalExcludePattern?: glob.ParsedExpression;
constructor(private config: IFileQuery, private provider: FileSearchProvider2, private sessionLifecycle?: SessionLifecycle) {
this.filePattern = config.filePattern;
const globOptions = config.ignoreGlobCase ? { ignoreCase: true } : undefined;
this.includePattern = config.includePattern && glob.parse(config.includePattern, globOptions);
this.maxResults = config.maxResults || undefined;
this.exists = config.exists;
this.activeCancellationTokens = new Set<CancellationTokenSource>();
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern, globOptions);
}
cancel(): void {
this.isCanceled = true;
this.activeCancellationTokens.forEach(t => t.cancel());
this.activeCancellationTokens = new Set();
}
search(_onResult: (match: IInternalFileMatch) => void): Promise<IInternalSearchComplete> {
const folderQueries = this.config.folderQueries || [];
return new Promise((resolve, reject) => {
const onResult = (match: IInternalFileMatch) => {
this.resultCount++;
_onResult(match);
};
// Support that the file pattern is a full path to a file that exists
if (this.isCanceled) {
return resolve({ limitHit: this.isLimitHit });
}
// For each extra file
if (this.config.extraFileResources) {
this.config.extraFileResources
.forEach(extraFile => {
const extraFileStr = extraFile.toString(); // ?
const basename = path.basename(extraFileStr);
if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) {
return; // excluded
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { base: extraFile, basename });
});
}
// For each root folder'
// NEW: can just call with an array of folder info
this.doSearch(folderQueries, onResult).then(stats => {
resolve({
limitHit: this.isLimitHit,
stats: stats || undefined // Only looking at single-folder workspace stats...
});
}, (err: Error) => {
reject(new Error(toErrorMessage(err)));
});
});
}
private async doSearch(fqs: IFolderQuery<URI>[], onResult: (match: IInternalFileMatch) => void): Promise<IFileSearchProviderStats | null> {
const cancellation = new CancellationTokenSource();
const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq));
const session = this.provider instanceof OldFileSearchProviderConverter ? this.sessionLifecycle?.tokenSource.token : this.sessionLifecycle?.obj;
const options: FileSearchProviderOptions = {
folderOptions,
maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS,
session
};
const getFolderQueryInfo = (fq: IFolderQuery) => {
const queryTester = new QueryGlobTester(this.config, fq);
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
return { queryTester, noSiblingsClauses, folder: fq.folder, tree: this.initDirectoryTree() };
};
const folderMappings: FolderQuerySearchTree<FolderQueryInfo> = new FolderQuerySearchTree<FolderQueryInfo>(fqs, getFolderQueryInfo);
let providerSW: StopWatch;
try {
this.activeCancellationTokens.add(cancellation);
providerSW = StopWatch.create();
const results = await this.provider.provideFileSearchResults(
this.config.filePattern || '',
options,
cancellation.token);
const providerTime = providerSW.elapsed();
const postProcessSW = StopWatch.create();
if (this.isCanceled && !this.isLimitHit) {
return null;
}
if (results) {
results.forEach(result => {
const fqFolderInfo = folderMappings.findQueryFragmentAwareSubstr(result)!;
const relativePath = path.posix.relative(fqFolderInfo.folder.path, result.path);
if (fqFolderInfo.noSiblingsClauses) {
const basename = path.basename(result.path);
this.matchFile(onResult, { base: fqFolderInfo.folder, relativePath, basename });
return;
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(fqFolderInfo.tree, fqFolderInfo.folder, relativePath, onResult);
});
}
if (this.isCanceled && !this.isLimitHit) {
return null;
}
folderMappings.forEachFolderQueryInfo(e => {
this.matchDirectoryTree(e.tree, e.queryTester, onResult);
});
return {
providerTime,
postProcessTime: postProcessSW.elapsed()
};
} finally {
cancellation.dispose();
this.activeCancellationTokens.delete(cancellation);
}
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): FileSearchProviderFolderOptions {
const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
let excludePattern = fq.excludePattern?.map(e => ({
folder: e.folder,
patterns: resolvePatternsForProvider(this.config.excludePattern, e.pattern)
}));
if (!excludePattern?.length) {
excludePattern = [{
folder: undefined,
patterns: resolvePatternsForProvider(this.config.excludePattern, undefined)
}];
}
const excludes = excludeToGlobPattern(excludePattern);
return {
folder: fq.folder,
excludes,
includes,
useIgnoreFiles: {
local: !fq.disregardIgnoreFiles,
parent: !fq.disregardParentIgnoreFiles,
global: !fq.disregardGlobalIgnoreFiles
},
followSymlinks: !fq.ignoreSymlinks,
};
}
private initDirectoryTree(): IDirectoryTree {
const tree: IDirectoryTree = {
rootEntries: [],
pathToEntries: Object.create(null)
};
tree.pathToEntries['.'] = tree.rootEntries;
return tree;
}
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) {
// Support relative paths to files from a root resource (ignores excludes)
if (this.filePattern && strings.equals(relativeFile, this.filePattern, this.config.ignoreGlobCase)) {
const basename = path.basename(this.filePattern);
this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
}
function add(relativePath: string) {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
let entries = pathToEntries[dirname];
if (!entries) {
entries = pathToEntries[dirname] = [];
add(dirname);
}
entries.push({
base,
relativePath,
basename
});
}
add(relativeFile);
}
private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) {
const self = this;
const filePattern = this.filePattern;
const ignoreGlobCase = this.config.ignoreGlobCase;
function matchDirectory(entries: IDirectoryEntry[]) {
const hasSibling = hasSiblingFn(() => entries.map(entry => entry.basename));
for (let i = 0, n = entries.length; i < n; i++) {
const entry = entries[i];
const { relativePath, basename } = entry;
// Check exclude pattern
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what they
// are searching for and we want to include the result in that case anyway
if (queryTester.matchesExcludesSync(relativePath, basename, !strings.equals(filePattern, basename, ignoreGlobCase) ? hasSibling : undefined)) {
continue;
}
const sub = pathToEntries[relativePath];
if (sub) {
matchDirectory(sub);
} else {
if (strings.equals(relativePath, filePattern, ignoreGlobCase)) {
continue; // ignore file if its path matches with the file pattern because that is already matched above
}
self.matchFile(onResult, entry);
}
if (self.isLimitHit) {
break;
}
}
}
matchDirectory(rootEntries);
}
private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void {
if (!this.includePattern || (candidate.relativePath && this.includePattern(candidate.relativePath, candidate.basename))) {
if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) {
this.isLimitHit = true;
this.cancel();
}
if (!this.isLimitHit) {
onResult(candidate);
}
}
}
}
interface IInternalSearchComplete {
limitHit: boolean;
stats?: IFileSearchProviderStats;
}
/**
* For backwards compatibility, store both a cancellation token and a session object. The session object is the new implementation, where
*/
class SessionLifecycle {
private _obj: object | undefined;
public readonly tokenSource: CancellationTokenSource;
constructor() {
this._obj = new Object();
this.tokenSource = new CancellationTokenSource();
}
public get obj() {
if (this._obj) {
return this._obj;
}
throw new Error('Session object has been dereferenced.');
}
cancel() {
this.tokenSource.cancel();
this._obj = undefined; // dereference
}
}
export class FileSearchManager {
private static readonly BATCH_SIZE = 512;
private readonly sessions = new Map<string, SessionLifecycle>();
fileSearch(config: IFileQuery, provider: FileSearchProvider2, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
const engine = new FileSearchEngine(config, provider, sessionTokenSource);
let resultCount = 0;
const onInternalResult = (batch: IInternalFileMatch[]) => {
resultCount += batch.length;
onBatch(batch.map(m => this.rawMatchToSearchItem(m)));
};
return this.doSearch(engine, FileSearchManager.BATCH_SIZE, onInternalResult, token).finally(() => {
if (!config.cacheKey) {
sessionTokenSource.cancel();
}
}).then(
result => {
return {
limitHit: result.limitHit,
stats: result.stats ? {
fromCache: false,
type: 'fileSearchProvider',
resultCount,
detailStats: result.stats
} : undefined,
messages: []
};
});
}
clearCache(cacheKey: string): void {
// cancel the token
this.sessions.get(cacheKey)?.cancel();
// with no reference to this, it will be removed from WeakMaps
this.sessions.delete(cacheKey);
}
private getSessionTokenSource(cacheKey: string | undefined): SessionLifecycle {
if (!cacheKey) {
return new SessionLifecycle();
}
if (!this.sessions.has(cacheKey)) {
this.sessions.set(cacheKey, new SessionLifecycle());
}
return this.sessions.get(cacheKey)!;
}
private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch {
if (match.relativePath) {
return {
resource: resources.joinPath(match.base, match.relativePath)
};
} else {
// extraFileResources
return {
resource: match.base
};
}
}
private doSearch(engine: FileSearchEngine, batchSize: number, onResultBatch: (matches: IInternalFileMatch[]) => void, token: CancellationToken): Promise<IInternalSearchComplete> {
const listener = token.onCancellationRequested(() => {
engine.cancel();
});
const _onResult = (match: IInternalFileMatch) => {
if (match) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
onResultBatch(batch);
batch = [];
}
}
};
let batch: IInternalFileMatch[] = [];
return engine.search(_onResult).then(result => {
if (batch.length) {
onResultBatch(batch);
}
listener.dispose();
return result;
}, error => {
if (batch.length) {
onResultBatch(batch);
}
listener.dispose();
return Promise.reject(error);
});
}
}