Compare commits
10
Commits
9f2b42c25c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0bc77be47 | ||
|
|
6d1a2764c0 | ||
|
|
da856c50ae | ||
|
|
fd90b7606a | ||
|
|
6c80ee4884 | ||
|
|
9bba2ad2af | ||
|
|
6cf5d04662 | ||
|
|
fd91eacb0d | ||
|
|
a9aed84b36 | ||
|
|
56c9a4d5f6 |
No files matched your search
@@ -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.
|
||||
@@ -2464,8 +2464,8 @@ export class CommandCenter {
|
||||
opts.signCommit = undefined;
|
||||
|
||||
if (config.get<boolean>('enableCommitSigning') === true) {
|
||||
const commitGpgSign = await repository.getConfig('commit.gpgsign');
|
||||
opts.signCommit = /^(false|no|off|0)$/i.test(commitGpgSign) ? undefined : true;
|
||||
const commitGpgSign = await repository.getConfig('commit.gpgsign', 'bool');
|
||||
opts.signCommit = commitGpgSign === 'false' ? undefined : true;
|
||||
}
|
||||
|
||||
if (config.get<boolean>('alwaysSignOff')) {
|
||||
|
||||
@@ -1411,13 +1411,17 @@ export class Repository {
|
||||
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}`];
|
||||
|
||||
if (scope) {
|
||||
args.push(`--${scope}`);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
args.push(`--${type}`);
|
||||
}
|
||||
|
||||
args.push(key);
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -1146,8 +1146,8 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Config(true), () => this.repository.getConfigs('local'));
|
||||
}
|
||||
|
||||
getConfig(key: string): Promise<string> {
|
||||
return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key));
|
||||
getConfig(key: string, type?: string): Promise<string> {
|
||||
return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key, null, {}, type));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user