Squashed 'lib/vscode/' content from commit e5a624b788

git-subtree-dir: lib/vscode
git-subtree-split: e5a624b788d92b8d34d1392e4c4d9789406efe8f
This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
commit be3e823608
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { PluginManager } from './utils/plugins';
class ApiV0 {
public constructor(
public readonly onCompletionAccepted: vscode.Event<vscode.CompletionItem & { metadata?: any }>,
private readonly _pluginManager: PluginManager,
) { }
configurePlugin(pluginId: string, configuration: {}): void {
this._pluginManager.setConfiguration(pluginId, configuration);
}
}
export interface Api {
getAPI(version: 0): ApiV0 | undefined;
}
export function getExtensionApi(
onCompletionAccepted: vscode.Event<vscode.CompletionItem>,
pluginManager: PluginManager,
): Api {
return {
getAPI(version) {
if (version === 0) {
return new ApiV0(onCompletionAccepted, pluginManager);
}
return undefined;
}
};
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface Command {
readonly id: string | string[];
execute(...args: any[]): void;
}
export class CommandManager {
private readonly commands = new Map<string, vscode.Disposable>();
public dispose() {
for (const registration of this.commands.values()) {
registration.dispose();
}
this.commands.clear();
}
public register<T extends Command>(command: T): T {
for (const id of Array.isArray(command.id) ? command.id : [command.id]) {
this.registerCommand(id, command.execute, command);
}
return command;
}
private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) {
if (this.commands.has(id)) {
return;
}
this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PluginManager } from '../utils/plugins';
import { Command } from './commandManager';
export class ConfigurePluginCommand implements Command {
public readonly id = '_typescript.configurePlugin';
public constructor(
private readonly pluginManager: PluginManager,
) { }
public execute(pluginId: string, configuration: any) {
this.pluginManager.setConfiguration(pluginId, configuration);
}
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { openProjectConfigForFile, ProjectType } from '../utils/tsconfig';
import { Command } from './commandManager';
export class TypeScriptGoToProjectConfigCommand implements Command {
public readonly id = 'typescript.goToProjectConfig';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>,
) { }
public execute() {
const editor = vscode.window.activeTextEditor;
if (editor) {
openProjectConfigForFile(ProjectType.TypeScript, this.lazyClientHost.value.serviceClient, editor.document.uri);
}
}
}
export class JavaScriptGoToProjectConfigCommand implements Command {
public readonly id = 'javascript.goToProjectConfig';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>,
) { }
public execute() {
const editor = vscode.window.activeTextEditor;
if (editor) {
openProjectConfigForFile(ProjectType.JavaScript, this.lazyClientHost.value.serviceClient, editor.document.uri);
}
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { PluginManager } from '../utils/plugins';
import { CommandManager } from './commandManager';
import { ConfigurePluginCommand } from './configurePlugin';
import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration';
import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings';
import { OpenTsServerLogCommand } from './openTsServerLog';
import { ReloadJavaScriptProjectsCommand, ReloadTypeScriptProjectsCommand } from './reloadProject';
import { RestartTsServerCommand } from './restartTsServer';
import { SelectTypeScriptVersionCommand } from './selectTypeScriptVersion';
export function registerBaseCommands(
commandManager: CommandManager,
lazyClientHost: Lazy<TypeScriptServiceClientHost>,
pluginManager: PluginManager
): void {
commandManager.register(new ReloadTypeScriptProjectsCommand(lazyClientHost));
commandManager.register(new ReloadJavaScriptProjectsCommand(lazyClientHost));
commandManager.register(new SelectTypeScriptVersionCommand(lazyClientHost));
commandManager.register(new OpenTsServerLogCommand(lazyClientHost));
commandManager.register(new RestartTsServerCommand(lazyClientHost));
commandManager.register(new TypeScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new JavaScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new ConfigurePluginCommand(pluginManager));
commandManager.register(new LearnMoreAboutRefactoringsCommand());
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { Command } from './commandManager';
export class LearnMoreAboutRefactoringsCommand implements Command {
public static readonly id = '_typescript.learnMoreAboutRefactorings';
public readonly id = LearnMoreAboutRefactoringsCommand.id;
public execute() {
const docUrl = vscode.window.activeTextEditor && isTypeScriptDocument(vscode.window.activeTextEditor.document)
? 'https://go.microsoft.com/fwlink/?linkid=2114477'
: 'https://go.microsoft.com/fwlink/?linkid=2116761';
vscode.env.openExternal(vscode.Uri.parse(docUrl));
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class OpenTsServerLogCommand implements Command {
public readonly id = 'typescript.openTsServerLog';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.openTsServerLogFile();
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class ReloadTypeScriptProjectsCommand implements Command {
public readonly id = 'typescript.reloadProjects';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.reloadProjects();
}
}
export class ReloadJavaScriptProjectsCommand implements Command {
public readonly id = 'javascript.reloadProjects';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.reloadProjects();
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class RestartTsServerCommand implements Command {
public readonly id = 'typescript.restartTsServer';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.restartTsServer();
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class SelectTypeScriptVersionCommand implements Command {
public readonly id = 'typescript.selectTypeScriptVersion';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.showVersionPicker();
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Api, getExtensionApi } from './api';
import { registerBaseCommands } from './commands/index';
import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
import { noopRequestCancellerFactory } from './tsServer/cancellation';
import { noopLogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource } from './tsServer/versionProvider';
import { WorkerServerProcess } from './tsServer/serverProcess.browser';
import API from './utils/api';
import { CommandManager } from './commands/commandManager';
import { TypeScriptServiceConfiguration } from './utils/configuration';
import { PluginManager } from './utils/plugins';
class StaticVersionProvider implements ITypeScriptVersionProvider {
constructor(
private readonly _version: TypeScriptVersion
) { }
updateConfiguration(_configuration: TypeScriptServiceConfiguration): void {
// noop
}
get defaultVersion() { return this._version; }
get bundledVersion() { return this._version; }
readonly globalVersion = undefined;
readonly localVersion = undefined;
readonly localVersions = [];
}
export function activate(
context: vscode.ExtensionContext
): Api {
const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
context.subscriptions.push(new LanguageConfigurationManager());
const onCompletionAccepted = new vscode.EventEmitter<vscode.CompletionItem>();
context.subscriptions.push(onCompletionAccepted);
const versionProvider = new StaticVersionProvider(
new TypeScriptVersion(
TypeScriptVersionSource.Bundled,
vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript-web/tsserver.web.js').toString(),
API.fromSimpleString('4.0.3')));
const lazyClientHost = createLazyClientHost(context, false, {
pluginManager,
commandManager,
logDirectoryProvider: noopLogDirectoryProvider,
cancellerFactory: noopRequestCancellerFactory,
versionProvider,
processFactory: WorkerServerProcess
}, item => {
onCompletionAccepted.fire(item);
});
registerBaseCommands(commandManager, lazyClientHost, pluginManager);
// context.subscriptions.push(task.register(lazyClientHost.map(x => x.serviceClient)));
import('./languageFeatures/tsconfig').then(module => {
context.subscriptions.push(module.register());
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager));
return getExtensionApi(onCompletionAccepted.event, pluginManager);
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as rimraf from 'rimraf';
import * as vscode from 'vscode';
import { Api, getExtensionApi } from './api';
import { registerBaseCommands } from './commands/index';
import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron';
import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron';
import { ChildServerProcess } from './tsServer/serverProcess.electron';
import { DiskTypeScriptVersionProvider } from './tsServer/versionProvider.electron';
import { CommandManager } from './commands/commandManager';
import { onCaseInsenitiveFileSystem } from './utils/fileSystem.electron';
import { PluginManager } from './utils/plugins';
import * as temp from './utils/temp.electron';
export function activate(
context: vscode.ExtensionContext
): Api {
const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
const onCompletionAccepted = new vscode.EventEmitter<vscode.CompletionItem>();
context.subscriptions.push(onCompletionAccepted);
const logDirectoryProvider = new NodeLogDirectoryProvider(context);
const versionProvider = new DiskTypeScriptVersionProvider();
context.subscriptions.push(new LanguageConfigurationManager());
const lazyClientHost = createLazyClientHost(context, onCaseInsenitiveFileSystem(), {
pluginManager,
commandManager,
logDirectoryProvider,
cancellerFactory: nodeRequestCancellerFactory,
versionProvider,
processFactory: ChildServerProcess,
}, item => {
onCompletionAccepted.fire(item);
});
registerBaseCommands(commandManager, lazyClientHost, pluginManager);
import('./task/taskProvider').then(module => {
context.subscriptions.push(module.register(lazyClientHost.map(x => x.serviceClient)));
});
import('./languageFeatures/tsconfig').then(module => {
context.subscriptions.push(module.register());
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager));
return getExtensionApi(onCompletionAccepted.event, pluginManager);
}
export function deactivate() {
rimraf.sync(temp.getInstanceTempDir());
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* 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 'path';
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptCallHierarchySupport implements vscode.CallHierarchyProvider {
public static readonly minVersion = API.v380;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async prepareCallHierarchy(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.CallHierarchyItem | vscode.CallHierarchyItem[] | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('prepareCallHierarchy', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return Array.isArray(response.body)
? response.body.map(fromProtocolCallHierarchyItem)
: fromProtocolCallHierarchyItem(response.body);
}
public async provideCallHierarchyIncomingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyIncomingCall[] | undefined> {
const filepath = this.client.toPath(item.uri);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, item.selectionRange.start);
const response = await this.client.execute('provideCallHierarchyIncomingCalls', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(fromProtocolCallHierchyIncomingCall);
}
public async provideCallHierarchyOutgoingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyOutgoingCall[] | undefined> {
const filepath = this.client.toPath(item.uri);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, item.selectionRange.start);
const response = await this.client.execute('provideCallHierarchyOutgoingCalls', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(fromProtocolCallHierchyOutgoingCall);
}
}
function isSourceFileItem(item: Proto.CallHierarchyItem) {
return item.kind === PConst.Kind.script || item.kind === PConst.Kind.module && item.selectionSpan.start.line === 1 && item.selectionSpan.start.offset === 1;
}
function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): vscode.CallHierarchyItem {
const useFileName = isSourceFileItem(item);
const name = useFileName ? path.basename(item.file) : item.name;
const detail = useFileName ? vscode.workspace.asRelativePath(path.dirname(item.file)) : item.containerName ?? '';
const result = new vscode.CallHierarchyItem(
typeConverters.SymbolKind.fromProtocolScriptElementKind(item.kind),
name,
detail,
vscode.Uri.file(item.file),
typeConverters.Range.fromTextSpan(item.span),
typeConverters.Range.fromTextSpan(item.selectionSpan)
);
const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined;
if (kindModifiers?.has(PConst.KindModifiers.depreacted)) {
result.tags = [vscode.SymbolTag.Deprecated];
}
return result;
}
function fromProtocolCallHierchyIncomingCall(item: Proto.CallHierarchyIncomingCall): vscode.CallHierarchyIncomingCall {
return new vscode.CallHierarchyIncomingCall(
fromProtocolCallHierarchyItem(item.from),
item.fromSpans.map(typeConverters.Range.fromTextSpan)
);
}
function fromProtocolCallHierchyOutgoingCall(item: Proto.CallHierarchyOutgoingCall): vscode.CallHierarchyOutgoingCall {
return new vscode.CallHierarchyOutgoingCall(
fromProtocolCallHierarchyItem(item.to),
item.fromSpans.map(typeConverters.Range.fromTextSpan)
);
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
) {
return conditionalRegistration([
requireMinVersion(client, TypeScriptCallHierarchySupport.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCallHierarchyProvider(selector.semantic,
new TypeScriptCallHierarchySupport(client));
});
}

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ITypeScriptServiceClient } from '../../typescriptService';
import { escapeRegExp } from '../../utils/regexp';
import * as typeConverters from '../../utils/typeConverters';
const localize = nls.loadMessageBundle();
export class ReferencesCodeLens extends vscode.CodeLens {
constructor(
public document: vscode.Uri,
public file: string,
range: vscode.Range
) {
super(range);
}
}
export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensProvider {
public static readonly cancelledCommand: vscode.Command = {
// Cancellation is not an error. Just show nothing until we can properly re-compute the code lens
title: '',
command: ''
};
public static readonly errorCommand: vscode.Command = {
title: localize('referenceErrorLabel', 'Could not determine references'),
command: ''
};
private onDidChangeCodeLensesEmitter = new vscode.EventEmitter<void>();
public constructor(
protected client: ITypeScriptServiceClient,
private cachedResponse: CachedResponse<Proto.NavTreeResponse>
) { }
public get onDidChangeCodeLenses(): vscode.Event<void> {
return this.onDidChangeCodeLensesEmitter.event;
}
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.CodeLens[]> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return [];
}
const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', { file: filepath }, token));
if (response.type !== 'response') {
return [];
}
const tree = response.body;
const referenceableSpans: vscode.Range[] = [];
if (tree && tree.childItems) {
tree.childItems.forEach(item => this.walkNavTree(document, item, null, referenceableSpans));
}
return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span));
}
protected abstract extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null
): vscode.Range | null;
private walkNavTree(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null,
results: vscode.Range[]
): void {
if (!item) {
return;
}
const range = this.extractSymbol(document, item, parent);
if (range) {
results.push(range);
}
(item.childItems || []).forEach(child => this.walkNavTree(document, child, item, results));
}
}
export function getSymbolRange(
document: vscode.TextDocument,
item: Proto.NavigationTree
): vscode.Range | null {
// TS 3.0+ provides a span for just the symbol
if (item.nameSpan) {
return typeConverters.Range.fromTextSpan(item.nameSpan);
}
// In older versions, we have to calculate this manually. See #23924
const span = item.spans && item.spans[0];
if (!span) {
return null;
}
const range = typeConverters.Range.fromTextSpan(span);
const text = document.getText(range);
const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, 'gm');
const match = identifierMatch.exec(text);
const prefixLength = match ? match.index + match[1].length : 0;
const startOffset = document.offsetAt(new vscode.Position(range.start.line, range.start.character)) + prefixLength;
return new vscode.Range(
document.positionAt(startOffset),
document.positionAt(startOffset + item.text.length));
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import * as PConst from '../../protocol.const';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { conditionalRegistration, requireSomeCapability, requireConfiguration } from '../../utils/dependentRegistration';
import { DocumentSelector } from '../../utils/documentSelector';
import * as typeConverters from '../../utils/typeConverters';
import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider';
const localize = nls.loadMessageBundle();
export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
public async resolveCodeLens(
inputCodeLens: vscode.CodeLens,
token: vscode.CancellationToken,
): Promise<vscode.CodeLens> {
const codeLens = inputCodeLens as ReferencesCodeLens;
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
const response = await this.client.execute('implementation', args, token, { lowPriority: true, cancelOnResourceChange: codeLens.document });
if (response.type !== 'response' || !response.body) {
codeLens.command = response.type === 'cancelled'
? TypeScriptBaseCodeLensProvider.cancelledCommand
: TypeScriptBaseCodeLensProvider.errorCommand;
return codeLens;
}
const locations = response.body
.map(reference =>
// Only take first line on implementation: https://github.com/microsoft/vscode/issues/23924
new vscode.Location(this.client.toResource(reference.file),
reference.start.line === reference.end.line
? typeConverters.Range.fromTextSpan(reference)
: new vscode.Range(
typeConverters.Position.fromLocation(reference.start),
new vscode.Position(reference.start.line, 0))))
// Exclude original from implementations
.filter(location =>
!(location.uri.toString() === codeLens.document.toString() &&
location.range.start.line === codeLens.range.start.line &&
location.range.start.character === codeLens.range.start.character));
codeLens.command = this.getCommand(locations, codeLens);
return codeLens;
}
private getCommand(locations: vscode.Location[], codeLens: ReferencesCodeLens): vscode.Command | undefined {
return {
title: this.getTitle(locations),
command: locations.length ? 'editor.action.showReferences' : '',
arguments: [codeLens.document, codeLens.range.start, locations]
};
}
private getTitle(locations: vscode.Location[]): string {
return locations.length === 1
? localize('oneImplementationLabel', '1 implementation')
: localize('manyImplementationLabel', '{0} implementations', locations.length);
}
protected extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
_parent: Proto.NavigationTree | null
): vscode.Range | null {
switch (item.kind) {
case PConst.Kind.interface:
return getSymbolRange(document, item);
case PConst.Kind.class:
case PConst.Kind.method:
case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
if (item.kindModifiers.match(/\babstract\b/g)) {
return getSymbolRange(document, item);
}
break;
}
return null;
}
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return conditionalRegistration([
requireConfiguration(modeId, 'implementationsCodeLens.enabled'),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptImplementationsCodeLensProvider(client, cachedResponse));
});
}

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import * as PConst from '../../protocol.const';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ExectuionTarget } from '../../tsServer/server';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../../utils/dependentRegistration';
import { DocumentSelector } from '../../utils/documentSelector';
import * as typeConverters from '../../utils/typeConverters';
import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider';
const localize = nls.loadMessageBundle();
export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider {
public constructor(
protected client: ITypeScriptServiceClient,
protected _cachedResponse: CachedResponse<Proto.NavTreeResponse>,
private modeId: string
) {
super(client, _cachedResponse);
}
public async resolveCodeLens(inputCodeLens: vscode.CodeLens, token: vscode.CancellationToken): Promise<vscode.CodeLens> {
const codeLens = inputCodeLens as ReferencesCodeLens;
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
const response = await this.client.execute('references', args, token, {
lowPriority: true,
executionTarget: ExectuionTarget.Semantic,
cancelOnResourceChange: codeLens.document,
});
if (response.type !== 'response' || !response.body) {
codeLens.command = response.type === 'cancelled'
? TypeScriptBaseCodeLensProvider.cancelledCommand
: TypeScriptBaseCodeLensProvider.errorCommand;
return codeLens;
}
const locations = response.body.refs
.map(reference =>
typeConverters.Location.fromTextSpan(this.client.toResource(reference.file), reference))
.filter(location =>
// Exclude original definition from references
!(location.uri.toString() === codeLens.document.toString() &&
location.range.start.isEqual(codeLens.range.start)));
codeLens.command = {
title: this.getCodeLensLabel(locations),
command: locations.length ? 'editor.action.showReferences' : '',
arguments: [codeLens.document, codeLens.range.start, locations]
};
return codeLens;
}
private getCodeLensLabel(locations: ReadonlyArray<vscode.Location>): string {
return locations.length === 1
? localize('oneReferenceLabel', '1 reference')
: localize('manyReferenceLabel', '{0} references', locations.length);
}
protected extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null
): vscode.Range | null {
if (parent && parent.kind === PConst.Kind.enum) {
return getSymbolRange(document, item);
}
switch (item.kind) {
case PConst.Kind.function:
const showOnAllFunctions = vscode.workspace.getConfiguration(this.modeId).get<boolean>('referencesCodeLens.showOnAllFunctions');
if (showOnAllFunctions) {
return getSymbolRange(document, item);
}
// fallthrough
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
// Only show references for exported variables
if (/\bexport\b/.test(item.kindModifiers)) {
return getSymbolRange(document, item);
}
break;
case PConst.Kind.class:
if (item.text === '<class>') {
break;
}
return getSymbolRange(document, item);
case PConst.Kind.interface:
case PConst.Kind.type:
case PConst.Kind.enum:
return getSymbolRange(document, item);
case PConst.Kind.method:
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
case PConst.Kind.constructorImplementation:
case PConst.Kind.memberVariable:
// Don't show if child and parent have same start
// For https://github.com/microsoft/vscode/issues/90396
if (parent &&
typeConverters.Position.fromLocation(parent.spans[0].start).isEqual(typeConverters.Position.fromLocation(item.spans[0].start))
) {
return null;
}
// Only show if parent is a class type object (not a literal)
switch (parent?.kind) {
case PConst.Kind.class:
case PConst.Kind.interface:
case PConst.Kind.type:
return getSymbolRange(document, item);
}
break;
}
return null;
}
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return conditionalRegistration([
requireConfiguration(modeId, 'referencesCodeLens.enabled'),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptReferencesCodeLensProvider(client, cachedResponse, modeId));
});
}

View File

@@ -0,0 +1,841 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { applyCodeAction } from '../utils/codeAction';
import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as Previewer from '../utils/previewer';
import { snippetForFunctionCall } from '../utils/snippetForFunctionCall';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import TypingsStatus from '../utils/typingsStatus';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
interface DotAccessorContext {
readonly range: vscode.Range;
readonly text: string;
}
interface CompletionContext {
readonly isNewIdentifierLocation: boolean;
readonly isMemberCompletion: boolean;
readonly isInValidCommitCharacterContext: boolean;
readonly dotAccessorContext?: DotAccessorContext;
readonly enableCallCompletions: boolean;
readonly useCodeSnippetsOnMethodSuggest: boolean,
readonly wordRange: vscode.Range | undefined;
readonly line: string;
readonly useFuzzyWordRangeLogic: boolean,
}
class MyCompletionItem extends vscode.CompletionItem {
public readonly useCodeSnippet: boolean;
constructor(
public readonly position: vscode.Position,
public readonly document: vscode.TextDocument,
public readonly tsEntry: Proto.CompletionEntry,
private readonly completionContext: CompletionContext,
public readonly metadata: any | undefined,
) {
super(tsEntry.name, MyCompletionItem.convertKind(tsEntry.kind));
if (tsEntry.source) {
// De-prioritze auto-imports
// https://github.com/microsoft/vscode/issues/40311
this.sortText = '\uffff' + tsEntry.sortText;
} else {
this.sortText = tsEntry.sortText;
}
this.preselect = tsEntry.isRecommended;
this.position = position;
this.useCodeSnippet = completionContext.useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method);
this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext, position);
this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry);
this.insertText = tsEntry.insertText;
this.filterText = this.getFilterText(completionContext.line, tsEntry.insertText);
if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) {
this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label);
if (!this.range) {
const replacementRange = this.getFuzzyWordRange();
if (replacementRange) {
this.range = {
inserting: completionContext.dotAccessorContext.range,
replacing: completionContext.dotAccessorContext.range.union(replacementRange),
};
} else {
this.range = completionContext.dotAccessorContext.range;
}
this.insertText = this.filterText;
}
}
if (tsEntry.kindModifiers) {
const kindModifiers = parseKindModifier(tsEntry.kindModifiers);
if (kindModifiers.has(PConst.KindModifiers.optional)) {
if (!this.insertText) {
this.insertText = this.label;
}
if (!this.filterText) {
this.filterText = this.label;
}
this.label += '?';
}
if (kindModifiers.has(PConst.KindModifiers.depreacted)) {
this.tags = [vscode.CompletionItemTag.Deprecated];
}
if (kindModifiers.has(PConst.KindModifiers.color)) {
this.kind = vscode.CompletionItemKind.Color;
}
if (tsEntry.kind === PConst.Kind.script) {
for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) {
if (kindModifiers.has(extModifier)) {
if (tsEntry.name.toLowerCase().endsWith(extModifier)) {
this.detail = tsEntry.name;
} else {
this.detail = tsEntry.name + extModifier;
}
break;
}
}
}
}
this.resolveRange();
}
private getRangeFromReplacementSpan(tsEntry: Proto.CompletionEntry, completionContext: CompletionContext, position: vscode.Position) {
if (!tsEntry.replacementSpan) {
return;
}
let replaceRange = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan);
// Make sure we only replace a single line at most
if (!replaceRange.isSingleLine) {
replaceRange = new vscode.Range(replaceRange.start.line, replaceRange.start.character, replaceRange.start.line, completionContext.line.length);
}
return {
inserting: new vscode.Range(replaceRange.start, position),
replacing: replaceRange,
};
}
private getFilterText(line: string, insertText: string | undefined): string | undefined {
// Handle private field completions
if (this.tsEntry.name.startsWith('#')) {
const wordRange = this.completionContext.wordRange;
const wordStart = wordRange ? line.charAt(wordRange.start.character) : undefined;
if (insertText) {
if (insertText.startsWith('this.#')) {
return wordStart === '#' ? insertText : insertText.replace(/^this\.#/, '');
} else {
return insertText;
}
} else {
return wordStart === '#' ? undefined : this.tsEntry.name.replace(/^#/, '');
}
}
// For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164
if (insertText?.startsWith('this.')) {
return undefined;
}
// Handle the case:
// ```
// const xyz = { 'ab c': 1 };
// xyz.ab|
// ```
// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
// the bracketed insert text.
else if (insertText?.startsWith('[')) {
return insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1');
}
// In all other cases, fallback to using the insertText
return insertText;
}
private resolveRange(): void {
if (this.range) {
return;
}
const replaceRange = this.getFuzzyWordRange();
if (replaceRange) {
this.range = {
inserting: new vscode.Range(replaceRange.start, this.position),
replacing: replaceRange
};
}
}
private getFuzzyWordRange() {
if (this.completionContext.useFuzzyWordRangeLogic) {
// Try getting longer, prefix based range for completions that span words
const text = this.completionContext.line.slice(Math.max(0, this.position.character - this.label.length), this.position.character).toLowerCase();
const entryName = this.label.toLowerCase();
for (let i = entryName.length; i >= 0; --i) {
if (text.endsWith(entryName.substr(0, i)) && (!this.completionContext.wordRange || this.completionContext.wordRange.start.character > this.position.character - i)) {
return new vscode.Range(
new vscode.Position(this.position.line, Math.max(0, this.position.character - i)),
this.position);
}
}
}
return this.completionContext.wordRange;
}
private static convertKind(kind: string): vscode.CompletionItemKind {
switch (kind) {
case PConst.Kind.primitiveType:
case PConst.Kind.keyword:
return vscode.CompletionItemKind.Keyword;
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
case PConst.Kind.localVariable:
case PConst.Kind.alias:
case PConst.Kind.parameter:
return vscode.CompletionItemKind.Variable;
case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
return vscode.CompletionItemKind.Field;
case PConst.Kind.function:
case PConst.Kind.localFunction:
return vscode.CompletionItemKind.Function;
case PConst.Kind.method:
case PConst.Kind.constructSignature:
case PConst.Kind.callSignature:
case PConst.Kind.indexSignature:
return vscode.CompletionItemKind.Method;
case PConst.Kind.enum:
return vscode.CompletionItemKind.Enum;
case PConst.Kind.enumMember:
return vscode.CompletionItemKind.EnumMember;
case PConst.Kind.module:
case PConst.Kind.externalModuleName:
return vscode.CompletionItemKind.Module;
case PConst.Kind.class:
case PConst.Kind.type:
return vscode.CompletionItemKind.Class;
case PConst.Kind.interface:
return vscode.CompletionItemKind.Interface;
case PConst.Kind.warning:
return vscode.CompletionItemKind.Text;
case PConst.Kind.script:
return vscode.CompletionItemKind.File;
case PConst.Kind.directory:
return vscode.CompletionItemKind.Folder;
case PConst.Kind.string:
return vscode.CompletionItemKind.Constant;
default:
return vscode.CompletionItemKind.Property;
}
}
private static getCommitCharacters(context: CompletionContext, entry: Proto.CompletionEntry): string[] | undefined {
if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) {
return undefined;
}
const commitCharacters: string[] = [];
switch (entry.kind) {
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
case PConst.Kind.constructSignature:
case PConst.Kind.callSignature:
case PConst.Kind.indexSignature:
case PConst.Kind.enum:
case PConst.Kind.interface:
commitCharacters.push('.', ';');
break;
case PConst.Kind.module:
case PConst.Kind.alias:
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
case PConst.Kind.localVariable:
case PConst.Kind.memberVariable:
case PConst.Kind.class:
case PConst.Kind.function:
case PConst.Kind.method:
case PConst.Kind.keyword:
case PConst.Kind.parameter:
commitCharacters.push('.', ',', ';');
if (context.enableCallCompletions) {
commitCharacters.push('(');
}
break;
}
return commitCharacters.length === 0 ? undefined : commitCharacters;
}
}
class CompositeCommand implements Command {
public static readonly ID = '_typescript.composite';
public readonly id = CompositeCommand.ID;
public execute(...commands: vscode.Command[]) {
for (const command of commands) {
vscode.commands.executeCommand(command.command, ...(command.arguments || []));
}
}
}
class CompletionAcceptedCommand implements Command {
public static readonly ID = '_typescript.onCompletionAccepted';
public readonly id = CompletionAcceptedCommand.ID;
public constructor(
private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
private readonly telemetryReporter: TelemetryReporter,
) { }
public execute(item: vscode.CompletionItem) {
this.onCompletionAccepted(item);
if (item instanceof MyCompletionItem) {
/* __GDPR__
"completions.accept" : {
"isPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('completions.accept', {
isPackageJsonImport: item.tsEntry.isPackageJsonImport ? 'true' : undefined,
});
}
}
}
class ApplyCompletionCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCompletionCodeAction';
public readonly id = ApplyCompletionCodeActionCommand.ID;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async execute(_file: string, codeActions: Proto.CodeAction[]): Promise<boolean> {
if (codeActions.length === 0) {
return true;
}
if (codeActions.length === 1) {
return applyCodeAction(this.client, codeActions[0], nulToken);
}
const selection = await vscode.window.showQuickPick(
codeActions.map(action => ({
label: action.description,
description: '',
action,
})), {
placeHolder: localize('selectCodeAction', 'Select code action to apply')
});
if (selection) {
return applyCodeAction(this.client, selection.action, nulToken);
}
return false;
}
}
interface CompletionConfiguration {
readonly useCodeSnippetsOnMethodSuggest: boolean;
readonly nameSuggestions: boolean;
readonly pathSuggestions: boolean;
readonly autoImportSuggestions: boolean;
}
namespace CompletionConfiguration {
export const useCodeSnippetsOnMethodSuggest = 'suggest.completeFunctionCalls';
export const nameSuggestions = 'suggest.names';
export const pathSuggestions = 'suggest.paths';
export const autoImportSuggestions = 'suggest.autoImports';
export function getConfigurationForResource(
modeId: string,
resource: vscode.Uri
): CompletionConfiguration {
const config = vscode.workspace.getConfiguration(modeId, resource);
return {
useCodeSnippetsOnMethodSuggest: config.get<boolean>(CompletionConfiguration.useCodeSnippetsOnMethodSuggest, false),
pathSuggestions: config.get<boolean>(CompletionConfiguration.pathSuggestions, true),
autoImportSuggestions: config.get<boolean>(CompletionConfiguration.autoImportSuggestions, true),
nameSuggestions: config.get<boolean>(CompletionConfiguration.nameSuggestions, true),
};
}
}
class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider<MyCompletionItem> {
public static readonly triggerCharacters = ['.', '"', '\'', '`', '/', '@', '<', '#'];
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly modeId: string,
private readonly typingsStatus: TypingsStatus,
private readonly fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
private readonly telemetryReporter: TelemetryReporter,
onCompletionAccepted: (item: vscode.CompletionItem) => void
) {
commandManager.register(new ApplyCompletionCodeActionCommand(this.client));
commandManager.register(new CompositeCommand());
commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted, this.telemetryReporter));
}
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): Promise<vscode.CompletionList<MyCompletionItem> | undefined> {
if (this.typingsStatus.isAcquiringTypings) {
return Promise.reject<vscode.CompletionList<MyCompletionItem>>({
label: localize(
{ key: 'acquiringTypingsLabel', comment: ['Typings refers to the *.d.ts typings files that power our IntelliSense. It should not be localized'] },
'Acquiring typings...'),
detail: localize(
{ key: 'acquiringTypingsDetail', comment: ['Typings refers to the *.d.ts typings files that power our IntelliSense. It should not be localized'] },
'Acquiring typings definitions for IntelliSense.')
});
}
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const line = document.lineAt(position.line);
const completionConfiguration = CompletionConfiguration.getConfigurationForResource(this.modeId, document.uri);
if (!this.shouldTrigger(context, line, position)) {
return undefined;
}
const wordRange = document.getWordRangeAtPosition(position);
await this.client.interruptGetErr(() => this.fileConfigurationManager.ensureConfigurationForDocument(document, token));
const args: Proto.CompletionsRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
includeExternalModuleExports: completionConfiguration.autoImportSuggestions,
includeInsertTextCompletions: true,
triggerCharacter: this.getTsTriggerCharacter(context),
};
let isNewIdentifierLocation = true;
let isIncomplete = false;
let isMemberCompletion = false;
let dotAccessorContext: DotAccessorContext | undefined;
let entries: ReadonlyArray<Proto.CompletionEntry>;
let metadata: any | undefined;
let response: ServerResponse.Response<Proto.CompletionInfoResponse> | undefined;
let duration: number | undefined;
if (this.client.apiVersion.gte(API.v300)) {
const startTime = Date.now();
try {
response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token));
} finally {
duration = Date.now() - startTime;
}
if (response.type !== 'response' || !response.body) {
this.logCompletionsTelemetry(duration, response);
return undefined;
}
isNewIdentifierLocation = response.body.isNewIdentifierLocation;
isMemberCompletion = response.body.isMemberCompletion;
if (isMemberCompletion) {
const dotMatch = line.text.slice(0, position.character).match(/\??\.\s*$/) || undefined;
if (dotMatch) {
const range = new vscode.Range(position.translate({ characterDelta: -dotMatch[0].length }), position);
const text = document.getText(range);
dotAccessorContext = { range, text };
}
}
isIncomplete = (response as any).metadata && (response as any).metadata.isIncomplete;
entries = response.body.entries;
metadata = response.metadata;
} else {
const response = await this.client.interruptGetErr(() => this.client.execute('completions', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
entries = response.body;
metadata = response.metadata;
}
const completionContext = {
isNewIdentifierLocation,
isMemberCompletion,
dotAccessorContext,
isInValidCommitCharacterContext: this.isInValidCommitCharacterContext(document, position),
enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest,
wordRange,
line: line.text,
useCodeSnippetsOnMethodSuggest: completionConfiguration.useCodeSnippetsOnMethodSuggest,
useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390),
};
let includesPackageJsonImport = false;
const items: MyCompletionItem[] = [];
for (let entry of entries) {
if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) {
items.push(new MyCompletionItem(position, document, entry, completionContext, metadata));
includesPackageJsonImport = !!entry.isPackageJsonImport;
}
}
if (duration !== undefined) {
this.logCompletionsTelemetry(duration, response, includesPackageJsonImport);
}
return new vscode.CompletionList(items, isIncomplete);
}
private logCompletionsTelemetry(
duration: number,
response: ServerResponse.Response<Proto.CompletionInfoResponse> | undefined,
includesPackageJsonImport?: boolean
) {
/* __GDPR__
"completions.execute" : {
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"updateGraphDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"createAutoImportProviderProgramDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"includesPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('completions.execute', {
duration: duration,
type: response?.type ?? 'unknown',
count: response?.type === 'response' && response.body ? response.body.entries.length : 0,
updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined,
createAutoImportProviderProgramDurationMs: response?.type === 'response' ? (response.performanceData as Proto.PerformanceData & { createAutoImportProviderProgramDurationMs?: number })?.createAutoImportProviderProgramDurationMs : undefined,
includesPackageJsonImport: includesPackageJsonImport ? 'true' : undefined,
});
}
private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined {
switch (context.triggerCharacter) {
case '@': // Workaround for https://github.com/microsoft/TypeScript/issues/27321
return this.client.apiVersion.gte(API.v310) && this.client.apiVersion.lt(API.v320) ? undefined : '@';
case '#': // Workaround for https://github.com/microsoft/TypeScript/issues/36367
return this.client.apiVersion.lt(API.v381) ? undefined : '#';
case '.':
case '"':
case '\'':
case '`':
case '/':
case '<':
return context.triggerCharacter;
}
return undefined;
}
public async resolveCompletionItem(
item: MyCompletionItem,
token: vscode.CancellationToken
): Promise<MyCompletionItem | undefined> {
const filepath = this.client.toOpenedFilePath(item.document);
if (!filepath) {
return undefined;
}
const args: Proto.CompletionDetailsRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(filepath, item.position),
entryNames: [
item.tsEntry.source ? { name: item.tsEntry.name, source: item.tsEntry.source } : item.tsEntry.name
]
};
const response = await this.client.interruptGetErr(() => this.client.execute('completionEntryDetails', args, token));
if (response.type !== 'response' || !response.body || !response.body.length) {
return item;
}
const detail = response.body[0];
if (!item.detail && detail.displayParts.length) {
item.detail = Previewer.plain(detail.displayParts);
}
item.documentation = this.getDocumentation(detail, item);
const codeAction = this.getCodeActions(detail, filepath);
const commands: vscode.Command[] = [{
command: CompletionAcceptedCommand.ID,
title: '',
arguments: [item]
}];
if (codeAction.command) {
commands.push(codeAction.command);
}
item.additionalTextEdits = codeAction.additionalTextEdits;
if (item.useCodeSnippet) {
const shouldCompleteFunction = await this.isValidFunctionCompletionContext(filepath, item.position, item.document, token);
if (shouldCompleteFunction) {
const { snippet, parameterCount } = snippetForFunctionCall(item, detail.displayParts);
item.insertText = snippet;
if (parameterCount > 0) {
//Fix for https://github.com/microsoft/vscode/issues/104059
//Don't show parameter hints if "editor.parameterHints.enabled": false
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) {
commands.push({ title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' });
}
}
}
}
if (commands.length) {
if (commands.length === 1) {
item.command = commands[0];
} else {
item.command = {
command: CompositeCommand.ID,
title: '',
arguments: commands
};
}
}
return item;
}
private getCodeActions(
detail: Proto.CompletionEntryDetails,
filepath: string
): { command?: vscode.Command, additionalTextEdits?: vscode.TextEdit[] } {
if (!detail.codeActions || !detail.codeActions.length) {
return {};
}
// Try to extract out the additionalTextEdits for the current file.
// Also check if we still have to apply other workspace edits and commands
// using a vscode command
const additionalTextEdits: vscode.TextEdit[] = [];
let hasReaminingCommandsOrEdits = false;
for (const tsAction of detail.codeActions) {
if (tsAction.commands) {
hasReaminingCommandsOrEdits = true;
}
// Apply all edits in the current file using `additionalTextEdits`
if (tsAction.changes) {
for (const change of tsAction.changes) {
if (change.fileName === filepath) {
additionalTextEdits.push(...change.textChanges.map(typeConverters.TextEdit.fromCodeEdit));
} else {
hasReaminingCommandsOrEdits = true;
}
}
}
}
let command: vscode.Command | undefined = undefined;
if (hasReaminingCommandsOrEdits) {
// Create command that applies all edits not in the current file.
command = {
title: '',
command: ApplyCompletionCodeActionCommand.ID,
arguments: [filepath, detail.codeActions.map((x): Proto.CodeAction => ({
commands: x.commands,
description: x.description,
changes: x.changes.filter(x => x.fileName !== filepath)
}))]
};
}
return {
command,
additionalTextEdits: additionalTextEdits.length ? additionalTextEdits : undefined
};
}
private isInValidCommitCharacterContext(
document: vscode.TextDocument,
position: vscode.Position
): boolean {
if (this.client.apiVersion.lt(API.v320)) {
// Workaround for https://github.com/microsoft/TypeScript/issues/27742
// Only enable dot completions when previous character not a dot preceded by whitespace.
// Prevents incorrectly completing while typing spread operators.
if (position.character > 1) {
const preText = document.getText(new vscode.Range(
position.line, 0,
position.line, position.character));
return preText.match(/(\s|^)\.$/ig) === null;
}
}
return true;
}
private shouldTrigger(
context: vscode.CompletionContext,
line: vscode.TextLine,
position: vscode.Position
): boolean {
if (context.triggerCharacter && this.client.apiVersion.lt(API.v290)) {
if ((context.triggerCharacter === '"' || context.triggerCharacter === '\'')) {
// make sure we are in something that looks like the start of an import
const pre = line.text.slice(0, position.character);
if (!pre.match(/\b(from|import)\s*["']$/) && !pre.match(/\b(import|require)\(['"]$/)) {
return false;
}
}
if (context.triggerCharacter === '/') {
// make sure we are in something that looks like an import path
const pre = line.text.slice(0, position.character);
if (!pre.match(/\b(from|import)\s*["'][^'"]*$/) && !pre.match(/\b(import|require)\(['"][^'"]*$/)) {
return false;
}
}
if (context.triggerCharacter === '@') {
// make sure we are in something that looks like the start of a jsdoc comment
const pre = line.text.slice(0, position.character);
if (!pre.match(/^\s*\*[ ]?@/) && !pre.match(/\/\*\*+[ ]?@/)) {
return false;
}
}
if (context.triggerCharacter === '<') {
return false;
}
}
return true;
}
private getDocumentation(
detail: Proto.CompletionEntryDetails,
item: MyCompletionItem
): vscode.MarkdownString | undefined {
const documentation = new vscode.MarkdownString();
if (detail.source) {
const importPath = `'${Previewer.plain(detail.source)}'`;
const autoImportLabel = localize('autoImportLabel', 'Auto import from {0}', importPath);
item.detail = `${autoImportLabel}\n${item.detail}`;
}
Previewer.addMarkdownDocumentation(documentation, detail.documentation, detail.tags);
return documentation.value.length ? documentation : undefined;
}
private async isValidFunctionCompletionContext(
filepath: string,
position: vscode.Position,
document: vscode.TextDocument,
token: vscode.CancellationToken
): Promise<boolean> {
// Workaround for https://github.com/microsoft/TypeScript/issues/12677
// Don't complete function calls inside of destructive assignments or imports
try {
const args: Proto.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('quickinfo', args, token);
if (response.type === 'response' && response.body) {
switch (response.body.kind) {
case 'var':
case 'let':
case 'const':
case 'alias':
return false;
}
}
} catch {
// Noop
}
// Don't complete function call if there is already something that looks like a function call
// https://github.com/microsoft/vscode/issues/18131
const after = document.lineAt(position.line).text.slice(position.character);
return after.match(/^[a-z_$0-9]*\s*\(/gi) === null;
}
}
function shouldExcludeCompletionEntry(
element: Proto.CompletionEntry,
completionConfiguration: CompletionConfiguration
) {
return (
(!completionConfiguration.nameSuggestions && element.kind === PConst.Kind.warning)
|| (!completionConfiguration.pathSuggestions &&
(element.kind === PConst.Kind.directory || element.kind === PConst.Kind.script || element.kind === PConst.Kind.externalModuleName))
|| (!completionConfiguration.autoImportSuggestions && element.hasAction)
);
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
typingsStatus: TypingsStatus,
fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter,
onCompletionAccepted: (item: vscode.CompletionItem) => void
) {
return conditionalRegistration([
requireConfiguration(modeId, 'suggest.enabled'),
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new TypeScriptCompletionItemProvider(client, modeId, typingsStatus, fileConfigurationManager, commandManager, telemetryReporter, onCompletionAccepted),
...TypeScriptCompletionItemProvider.triggerCharacters);
});
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ITypeScriptServiceClient } from '../typescriptService';
import * as typeConverters from '../utils/typeConverters';
export default class TypeScriptDefinitionProviderBase {
constructor(
protected readonly client: ITypeScriptServiceClient
) { }
protected async getSymbolLocations(
definitionType: 'definition' | 'implementation' | 'typeDefinition',
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const response = await this.client.execute(definitionType, args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(location =>
typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location));
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import DefinitionProviderBase from './definitionProviderBase';
export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements vscode.DefinitionProvider {
constructor(
client: ITypeScriptServiceClient
) {
super(client);
}
public async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.DefinitionLink[] | vscode.Definition | undefined> {
if (this.client.apiVersion.gte(API.v270)) {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('definitionAndBoundSpan', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const span = response.body.textSpan ? typeConverters.Range.fromTextSpan(response.body.textSpan) : undefined;
return response.body.definitions
.map((location): vscode.DefinitionLink => {
const target = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location);
if (location.contextStart && location.contextEnd) {
return {
originSelectionRange: span,
targetRange: typeConverters.Range.fromLocations(location.contextStart, location.contextEnd),
targetUri: target.uri,
targetSelectionRange: target.range,
};
}
return {
originSelectionRange: span,
targetRange: target.range,
targetUri: target.uri
};
});
}
return this.getSymbolLocations('definition', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerDefinitionProvider(selector.syntax,
new TypeScriptDefinitionProvider(client));
});
}

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ResourceMap } from '../utils/resourceMap';
import { DiagnosticLanguage } from '../utils/languageDescription';
import * as arrays from '../utils/arrays';
import { Disposable } from '../utils/dispose';
function diagnosticsEquals(a: vscode.Diagnostic, b: vscode.Diagnostic): boolean {
if (a === b) {
return true;
}
return a.code === b.code
&& a.message === b.message
&& a.severity === b.severity
&& a.source === b.source
&& a.range.isEqual(b.range)
&& arrays.equals(a.relatedInformation || arrays.empty, b.relatedInformation || arrays.empty, (a, b) => {
return a.message === b.message
&& a.location.range.isEqual(b.location.range)
&& a.location.uri.fsPath === b.location.uri.fsPath;
})
&& arrays.equals(a.tags || arrays.empty, b.tags || arrays.empty);
}
export const enum DiagnosticKind {
Syntax,
Semantic,
Suggestion,
}
class FileDiagnostics {
private readonly _diagnostics = new Map<DiagnosticKind, ReadonlyArray<vscode.Diagnostic>>();
constructor(
public readonly file: vscode.Uri,
public language: DiagnosticLanguage
) { }
public updateDiagnostics(
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): boolean {
if (language !== this.language) {
this._diagnostics.clear();
this.language = language;
}
const existing = this._diagnostics.get(kind);
if (arrays.equals(existing || arrays.empty, diagnostics, diagnosticsEquals)) {
// No need to update
return false;
}
this._diagnostics.set(kind, diagnostics);
return true;
}
public getDiagnostics(settings: DiagnosticSettings): vscode.Diagnostic[] {
if (!settings.getValidate(this.language)) {
return [];
}
return [
...this.get(DiagnosticKind.Syntax),
...this.get(DiagnosticKind.Semantic),
...this.getSuggestionDiagnostics(settings),
];
}
private getSuggestionDiagnostics(settings: DiagnosticSettings) {
const enableSuggestions = settings.getEnableSuggestions(this.language);
return this.get(DiagnosticKind.Suggestion).filter(x => {
if (!enableSuggestions) {
// Still show unused
return x.tags && (x.tags.includes(vscode.DiagnosticTag.Unnecessary) || x.tags.includes(vscode.DiagnosticTag.Deprecated));
}
return true;
});
}
private get(kind: DiagnosticKind): ReadonlyArray<vscode.Diagnostic> {
return this._diagnostics.get(kind) || [];
}
}
interface LanguageDiagnosticSettings {
readonly validate: boolean;
readonly enableSuggestions: boolean;
}
function areLanguageDiagnosticSettingsEqual(currentSettings: LanguageDiagnosticSettings, newSettings: LanguageDiagnosticSettings): boolean {
return currentSettings.validate === newSettings.validate
&& currentSettings.enableSuggestions && currentSettings.enableSuggestions;
}
class DiagnosticSettings {
private static readonly defaultSettings: LanguageDiagnosticSettings = {
validate: true,
enableSuggestions: true
};
private readonly _languageSettings = new Map<DiagnosticLanguage, LanguageDiagnosticSettings>();
public getValidate(language: DiagnosticLanguage): boolean {
return this.get(language).validate;
}
public setValidate(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: value,
enableSuggestions: settings.enableSuggestions,
}));
}
public getEnableSuggestions(language: DiagnosticLanguage): boolean {
return this.get(language).enableSuggestions;
}
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: settings.validate,
enableSuggestions: value
}));
}
private get(language: DiagnosticLanguage): LanguageDiagnosticSettings {
return this._languageSettings.get(language) || DiagnosticSettings.defaultSettings;
}
private update(language: DiagnosticLanguage, f: (x: LanguageDiagnosticSettings) => LanguageDiagnosticSettings): boolean {
const currentSettings = this.get(language);
const newSettings = f(currentSettings);
this._languageSettings.set(language, newSettings);
return areLanguageDiagnosticSettingsEqual(currentSettings, newSettings);
}
}
export class DiagnosticsManager extends Disposable {
private readonly _diagnostics: ResourceMap<FileDiagnostics>;
private readonly _settings = new DiagnosticSettings();
private readonly _currentDiagnostics: vscode.DiagnosticCollection;
private readonly _pendingUpdates: ResourceMap<any>;
private readonly _updateDelay = 50;
constructor(
owner: string,
onCaseInsenitiveFileSystem: boolean
) {
super();
this._diagnostics = new ResourceMap<FileDiagnostics>(undefined, { onCaseInsenitiveFileSystem });
this._pendingUpdates = new ResourceMap<any>(undefined, { onCaseInsenitiveFileSystem });
this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner));
}
public dispose() {
super.dispose();
for (const value of this._pendingUpdates.values) {
clearTimeout(value);
}
this._pendingUpdates.clear();
}
public reInitialize(): void {
this._currentDiagnostics.clear();
this._diagnostics.clear();
}
public setValidate(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setValidate(language, value);
if (didUpdate) {
this.rebuild();
}
}
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setEnableSuggestions(language, value);
if (didUpdate) {
this.rebuild();
}
}
public updateDiagnostics(
file: vscode.Uri,
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): void {
let didUpdate = false;
const entry = this._diagnostics.get(file);
if (entry) {
didUpdate = entry.updateDiagnostics(language, kind, diagnostics);
} else if (diagnostics.length) {
const fileDiagnostics = new FileDiagnostics(file, language);
fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
this._diagnostics.set(file, fileDiagnostics);
didUpdate = true;
}
if (didUpdate) {
this.scheduleDiagnosticsUpdate(file);
}
}
public configFileDiagnosticsReceived(
file: vscode.Uri,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): void {
this._currentDiagnostics.set(file, diagnostics);
}
public delete(resource: vscode.Uri): void {
this._currentDiagnostics.delete(resource);
this._diagnostics.delete(resource);
}
public getDiagnostics(file: vscode.Uri): ReadonlyArray<vscode.Diagnostic> {
return this._currentDiagnostics.get(file) || [];
}
private scheduleDiagnosticsUpdate(file: vscode.Uri) {
if (!this._pendingUpdates.has(file)) {
this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay));
}
}
private updateCurrentDiagnostics(file: vscode.Uri): void {
if (this._pendingUpdates.has(file)) {
clearTimeout(this._pendingUpdates.get(file));
this._pendingUpdates.delete(file);
}
const fileDiagnostics = this._diagnostics.get(file);
this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []);
}
private rebuild(): void {
this._currentDiagnostics.clear();
for (const fileDiagnostic of this._diagnostics.values) {
this._currentDiagnostics.set(fileDiagnostic.file, fileDiagnostic.getDiagnostics(this._settings));
}
}
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { DocumentSelector } from '../utils/documentSelector';
const localize = nls.loadMessageBundle();
interface Directive {
readonly value: string;
readonly description: string;
}
const tsDirectives: Directive[] = [
{
value: '@ts-check',
description: localize(
'ts-check',
"Enables semantic checking in a JavaScript file. Must be at the top of a file.")
}, {
value: '@ts-nocheck',
description: localize(
'ts-nocheck',
"Disables semantic checking in a JavaScript file. Must be at the top of a file.")
}, {
value: '@ts-ignore',
description: localize(
'ts-ignore',
"Suppresses @ts-check errors on the next line of a file.")
}
];
const tsDirectives390: Directive[] = [
...tsDirectives,
{
value: '@ts-expect-error',
description: localize(
'ts-expect-error',
"Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.")
}
];
class DirectiveCommentCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly client: ITypeScriptServiceClient,
) { }
public provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
_token: vscode.CancellationToken
): vscode.CompletionItem[] {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const line = document.lineAt(position.line).text;
const prefix = line.slice(0, position.character);
const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z\-]*)?$/);
if (match) {
const directives = this.client.apiVersion.gte(API.v390)
? tsDirectives390
: tsDirectives;
return directives.map(directive => {
const item = new vscode.CompletionItem(directive.value, vscode.CompletionItemKind.Snippet);
item.detail = directive.description;
item.range = new vscode.Range(position.line, Math.max(0, position.character - (match[1] ? match[1].length : 0)), position.line, position.character);
return item;
});
}
return [];
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new DirectiveCommentCompletionProvider(client),
'@');
}

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import { flatten } from '../utils/arrays';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideDocumentHighlights(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.DocumentHighlight[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const args = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
filesToSearch: [file]
};
const response = await this.client.execute('documentHighlights', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
return flatten(
response.body
.filter(highlight => highlight.file === file)
.map(convertDocumentHighlight));
}
}
function convertDocumentHighlight(highlight: Proto.DocumentHighlightsItem): ReadonlyArray<vscode.DocumentHighlight> {
return highlight.highlightSpans.map(span =>
new vscode.DocumentHighlight(
typeConverters.Range.fromTextSpan(span),
span.kind === 'writtenReference' ? vscode.DocumentHighlightKind.Write : vscode.DocumentHighlightKind.Read));
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return vscode.languages.registerDocumentHighlightProvider(selector.syntax,
new TypeScriptDocumentHighlightProvider(client));
}

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { CachedResponse } from '../tsServer/cachedResponse';
import { ITypeScriptServiceClient } from '../typescriptService';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as typeConverters from '../utils/typeConverters';
const getSymbolKind = (kind: string): vscode.SymbolKind => {
switch (kind) {
case PConst.Kind.module: return vscode.SymbolKind.Module;
case PConst.Kind.class: return vscode.SymbolKind.Class;
case PConst.Kind.enum: return vscode.SymbolKind.Enum;
case PConst.Kind.interface: return vscode.SymbolKind.Interface;
case PConst.Kind.method: return vscode.SymbolKind.Method;
case PConst.Kind.memberVariable: return vscode.SymbolKind.Property;
case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Property;
case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Property;
case PConst.Kind.variable: return vscode.SymbolKind.Variable;
case PConst.Kind.const: return vscode.SymbolKind.Variable;
case PConst.Kind.localVariable: return vscode.SymbolKind.Variable;
case PConst.Kind.function: return vscode.SymbolKind.Function;
case PConst.Kind.localFunction: return vscode.SymbolKind.Function;
case PConst.Kind.constructSignature: return vscode.SymbolKind.Constructor;
case PConst.Kind.constructorImplementation: return vscode.SymbolKind.Constructor;
}
return vscode.SymbolKind.Variable;
};
class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) { }
public async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentSymbol[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.FileRequestArgs = { file };
const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', args, token));
if (response.type !== 'response' || !response.body?.childItems) {
return undefined;
}
// The root represents the file. Ignore this when showing in the UI
const result: vscode.DocumentSymbol[] = [];
for (const item of response.body.childItems) {
TypeScriptDocumentSymbolProvider.convertNavTree(document.uri, result, item);
}
return result;
}
private static convertNavTree(
resource: vscode.Uri,
output: vscode.DocumentSymbol[],
item: Proto.NavigationTree,
): boolean {
let shouldInclude = TypeScriptDocumentSymbolProvider.shouldInclueEntry(item);
if (!shouldInclude && !item.childItems?.length) {
return false;
}
const children = new Set(item.childItems || []);
for (const span of item.spans) {
const range = typeConverters.Range.fromTextSpan(span);
const symbolInfo = TypeScriptDocumentSymbolProvider.convertSymbol(item, range);
for (const child of children) {
if (child.spans.some(span => !!range.intersection(typeConverters.Range.fromTextSpan(span)))) {
const includedChild = TypeScriptDocumentSymbolProvider.convertNavTree(resource, symbolInfo.children, child);
shouldInclude = shouldInclude || includedChild;
children.delete(child);
}
}
if (shouldInclude) {
output.push(symbolInfo);
}
}
return shouldInclude;
}
private static convertSymbol(item: Proto.NavigationTree, range: vscode.Range): vscode.DocumentSymbol {
const selectionRange = item.nameSpan ? typeConverters.Range.fromTextSpan(item.nameSpan) : range;
let label = item.text;
switch (item.kind) {
case PConst.Kind.memberGetAccessor: label = `(get) ${label}`; break;
case PConst.Kind.memberSetAccessor: label = `(set) ${label}`; break;
}
const symbolInfo = new vscode.DocumentSymbol(
label,
'',
getSymbolKind(item.kind),
range,
range.contains(selectionRange) ? selectionRange : range);
const kindModifiers = parseKindModifier(item.kindModifiers);
if (kindModifiers.has(PConst.KindModifiers.depreacted)) {
symbolInfo.tags = [vscode.SymbolTag.Deprecated];
}
return symbolInfo;
}
private static shouldInclueEntry(item: Proto.NavigationTree | Proto.NavigationBarItem): boolean {
if (item.kind === PConst.Kind.alias) {
return false;
}
return !!(item.text && item.text !== '<function>' && item.text !== '<class>');
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return vscode.languages.registerDocumentSymbolProvider(selector.syntax,
new TypeScriptDocumentSymbolProvider(client, cachedResponse), { label: 'TypeScript' });
}

View File

@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { Disposable } from '../utils/dispose';
import * as fileSchemes from '../utils/fileSchemes';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { equals } from '../utils/objects';
import { ResourceMap } from '../utils/resourceMap';
namespace Experimental {
// https://github.com/microsoft/TypeScript/pull/37871/
export interface UserPreferences extends Proto.UserPreferences {
readonly provideRefactorNotApplicableReason?: boolean;
}
// https://github.com/microsoft/TypeScript/issues/41208
export interface FormatCodeSettings extends Proto.FormatCodeSettings {
readonly insertSpaceAfterOpeningAndBeforeClosingEmptyBraces?: boolean;
}
}
interface FileConfiguration {
readonly formatOptions: Proto.FormatCodeSettings;
readonly preferences: Proto.UserPreferences;
}
function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): boolean {
return equals(a, b);
}
export default class FileConfigurationManager extends Disposable {
private readonly formatOptions: ResourceMap<Promise<FileConfiguration | undefined>>;
public constructor(
private readonly client: ITypeScriptServiceClient,
onCaseInsenitiveFileSystem: boolean
) {
super();
this.formatOptions = new ResourceMap(undefined, { onCaseInsenitiveFileSystem });
vscode.workspace.onDidCloseTextDocument(textDocument => {
// When a document gets closed delete the cached formatting options.
// This is necessary since the tsserver now closed a project when its
// last file in it closes which drops the stored formatting options
// as well.
this.formatOptions.delete(textDocument.uri);
}, undefined, this._disposables);
}
public async ensureConfigurationForDocument(
document: vscode.TextDocument,
token: vscode.CancellationToken
): Promise<void> {
const formattingOptions = this.getFormattingOptions(document);
if (formattingOptions) {
return this.ensureConfigurationOptions(document, formattingOptions, token);
}
}
private getFormattingOptions(
document: vscode.TextDocument
): vscode.FormattingOptions | undefined {
const editor = vscode.window.visibleTextEditors.find(editor => editor.document.fileName === document.fileName);
return editor
? {
tabSize: editor.options.tabSize,
insertSpaces: editor.options.insertSpaces
} as vscode.FormattingOptions
: undefined;
}
public async ensureConfigurationOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<void> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return;
}
const currentOptions = this.getFileOptions(document, options);
const cachedOptions = this.formatOptions.get(document.uri);
if (cachedOptions) {
const cachedOptionsValue = await cachedOptions;
if (cachedOptionsValue && areFileConfigurationsEqual(cachedOptionsValue, currentOptions)) {
return;
}
}
let resolve: (x: FileConfiguration | undefined) => void;
this.formatOptions.set(document.uri, new Promise<FileConfiguration | undefined>(r => resolve = r));
const args: Proto.ConfigureRequestArguments = {
file,
...currentOptions,
};
try {
const response = await this.client.execute('configure', args, token);
resolve!(response.type === 'response' ? currentOptions : undefined);
} finally {
resolve!(undefined);
}
}
public async setGlobalConfigurationFromDocument(
document: vscode.TextDocument,
token: vscode.CancellationToken,
): Promise<void> {
const formattingOptions = this.getFormattingOptions(document);
if (!formattingOptions) {
return;
}
const args: Proto.ConfigureRequestArguments = {
file: undefined /*global*/,
...this.getFileOptions(document, formattingOptions),
};
await this.client.execute('configure', args, token);
}
public reset() {
this.formatOptions.clear();
}
private getFileOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions
): FileConfiguration {
return {
formatOptions: this.getFormatOptions(document, options),
preferences: this.getPreferences(document)
};
}
private getFormatOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions
): Experimental.FormatCodeSettings {
const config = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format',
document.uri);
return {
tabSize: options.tabSize,
indentSize: options.tabSize,
convertTabsToSpaces: options.insertSpaces,
// We can use \n here since the editor normalizes later on to its line endings.
newLineCharacter: '\n',
insertSpaceAfterCommaDelimiter: config.get<boolean>('insertSpaceAfterCommaDelimiter'),
insertSpaceAfterConstructor: config.get<boolean>('insertSpaceAfterConstructor'),
insertSpaceAfterSemicolonInForStatements: config.get<boolean>('insertSpaceAfterSemicolonInForStatements'),
insertSpaceBeforeAndAfterBinaryOperators: config.get<boolean>('insertSpaceBeforeAndAfterBinaryOperators'),
insertSpaceAfterKeywordsInControlFlowStatements: config.get<boolean>('insertSpaceAfterKeywordsInControlFlowStatements'),
insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.get<boolean>('insertSpaceAfterFunctionKeywordForAnonymousFunctions'),
insertSpaceBeforeFunctionParenthesis: config.get<boolean>('insertSpaceBeforeFunctionParenthesis'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'),
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingEmptyBraces'),
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'),
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'),
insertSpaceAfterTypeAssertion: config.get<boolean>('insertSpaceAfterTypeAssertion'),
placeOpenBraceOnNewLineForFunctions: config.get<boolean>('placeOpenBraceOnNewLineForFunctions'),
placeOpenBraceOnNewLineForControlBlocks: config.get<boolean>('placeOpenBraceOnNewLineForControlBlocks'),
semicolons: config.get<Proto.SemicolonPreference>('semicolons'),
};
}
private getPreferences(document: vscode.TextDocument): Proto.UserPreferences {
if (this.client.apiVersion.lt(API.v290)) {
return {};
}
const config = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript' : 'javascript',
document.uri);
const preferencesConfig = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences',
document.uri);
const preferences: Experimental.UserPreferences = {
quotePreference: this.getQuoteStylePreference(preferencesConfig),
importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferencesConfig),
importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig),
allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file,
providePrefixAndSuffixTextForRename: preferencesConfig.get<boolean>('renameShorthandProperties', true) === false ? false : preferencesConfig.get<boolean>('useAliasesForRenames', true),
allowRenameOfImportPath: true,
includeAutomaticOptionalChainCompletions: config.get<boolean>('suggest.includeAutomaticOptionalChainCompletions', true),
provideRefactorNotApplicableReason: true,
};
return preferences;
}
private getQuoteStylePreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('quoteStyle')) {
case 'single': return 'single';
case 'double': return 'double';
default: return this.client.apiVersion.gte(API.v333) ? 'auto' : undefined;
}
}
}
function getImportModuleSpecifierPreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('importModuleSpecifier')) {
case 'relative': return 'relative';
case 'non-relative': return 'non-relative';
default: return undefined;
}
}
function getImportModuleSpecifierEndingPreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('importModuleSpecifierEnding')) {
case 'minimal': return 'minimal';
case 'index': return 'index';
case 'js': return 'js';
default: return 'auto';
}
}

View File

@@ -0,0 +1,265 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as errorCodes from '../utils/errorCodes';
import * as fixNames from '../utils/fixNames';
import * as typeConverters from '../utils/typeConverters';
import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
interface AutoFix {
readonly codes: Set<number>;
readonly fixName: string;
}
async function buildIndividualFixes(
fixes: readonly AutoFix[],
edit: vscode.WorkspaceEdit,
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void> {
for (const diagnostic of diagnostics) {
for (const { codes, fixName } of fixes) {
if (token.isCancellationRequested) {
return;
}
if (!codes.has(diagnostic.code as number)) {
continue;
}
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await client.execute('getCodeFixes', args, token);
if (response.type !== 'response') {
continue;
}
const fix = response.body?.find(fix => fix.fixName === fixName);
if (fix) {
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, fix.changes);
break;
}
}
}
}
async function buildCombinedFix(
fixes: readonly AutoFix[],
edit: vscode.WorkspaceEdit,
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void> {
for (const diagnostic of diagnostics) {
for (const { codes, fixName } of fixes) {
if (token.isCancellationRequested) {
return;
}
if (!codes.has(diagnostic.code as number)) {
continue;
}
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await client.execute('getCodeFixes', args, token);
if (response.type !== 'response' || !response.body?.length) {
continue;
}
const fix = response.body?.find(fix => fix.fixName === fixName);
if (!fix) {
continue;
}
if (!fix.fixId) {
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, fix.changes);
return;
}
const combinedArgs: Proto.GetCombinedCodeFixRequestArgs = {
scope: {
type: 'file',
args: { file }
},
fixId: fix.fixId,
};
const combinedResponse = await client.execute('getCombinedCodeFix', combinedArgs, token);
if (combinedResponse.type !== 'response' || !combinedResponse.body) {
return;
}
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, combinedResponse.body.changes);
return;
}
}
}
// #region Source Actions
abstract class SourceAction extends vscode.CodeAction {
abstract build(
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void>;
}
class SourceFixAll extends SourceAction {
static readonly kind = vscode.CodeActionKind.SourceFixAll.append('ts');
constructor() {
super(localize('autoFix.label', 'Fix All'), SourceFixAll.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildIndividualFixes([
{ codes: errorCodes.incorrectlyImplementsInterface, fixName: fixNames.classIncorrectlyImplementsInterface },
{ codes: errorCodes.asyncOnlyAllowedInAsyncFunctions, fixName: fixNames.awaitInSyncFunction },
], this.edit, client, file, diagnostics, token);
await buildCombinedFix([
{ codes: errorCodes.unreachableCode, fixName: fixNames.unreachableCode }
], this.edit, client, file, diagnostics, token);
}
}
class SourceRemoveUnused extends SourceAction {
static readonly kind = vscode.CodeActionKind.Source.append('removeUnused').append('ts');
constructor() {
super(localize('autoFix.unused.label', 'Remove all unused code'), SourceRemoveUnused.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildCombinedFix([
{ codes: errorCodes.variableDeclaredButNeverUsed, fixName: fixNames.unusedIdentifier },
], this.edit, client, file, diagnostics, token);
}
}
class SourceAddMissingImports extends SourceAction {
static readonly kind = vscode.CodeActionKind.Source.append('addMissingImports').append('ts');
constructor() {
super(localize('autoFix.missingImports.label', 'Add all missing imports'), SourceAddMissingImports.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildCombinedFix([
{ codes: errorCodes.cannotFindName, fixName: fixNames.fixImport }
],
this.edit, client, file, diagnostics, token);
}
}
//#endregion
class TypeScriptAutoFixProvider implements vscode.CodeActionProvider {
private static kindProviders = [
SourceFixAll,
SourceRemoveUnused,
SourceAddMissingImports,
];
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly diagnosticsManager: DiagnosticsManager,
) { }
public get metadata(): vscode.CodeActionProviderMetadata {
return {
providedCodeActionKinds: TypeScriptAutoFixProvider.kindProviders.map(x => x.kind),
};
}
public async provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<vscode.CodeAction[] | undefined> {
if (!context.only || !vscode.CodeActionKind.Source.intersects(context.only)) {
return undefined;
}
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const actions = this.getFixAllActions(context.only);
if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) {
return actions;
}
const diagnostics = this.diagnosticsManager.getDiagnostics(document.uri);
if (!diagnostics.length) {
// Actions are a no-op in this case but we still want to return them
return actions;
}
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token);
if (token.isCancellationRequested) {
return undefined;
}
await Promise.all(actions.map(action => action.build(this.client, file, diagnostics, token)));
return actions;
}
private getFixAllActions(only: vscode.CodeActionKind): SourceAction[] {
return TypeScriptAutoFixProvider.kindProviders
.filter(provider => only.intersects(provider.kind))
.map(provider => new provider());
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
diagnosticsManager: DiagnosticsManager,
) {
return conditionalRegistration([
requireMinVersion(client, API.v300),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const provider = new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager);
return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, provider.metadata);
});
}

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { coalesce } from '../utils/arrays';
import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider {
public static readonly minVersion = API.v280;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
async provideFoldingRanges(
document: vscode.TextDocument,
_context: vscode.FoldingContext,
token: vscode.CancellationToken
): Promise<vscode.FoldingRange[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return;
}
const args: Proto.FileRequestArgs = { file };
const response = await this.client.execute('getOutliningSpans', args, token);
if (response.type !== 'response' || !response.body) {
return;
}
return coalesce(response.body.map(span => this.convertOutliningSpan(span, document)));
}
private convertOutliningSpan(
span: Proto.OutliningSpan,
document: vscode.TextDocument
): vscode.FoldingRange | undefined {
const range = typeConverters.Range.fromTextSpan(span.textSpan);
const kind = TypeScriptFoldingProvider.getFoldingRangeKind(span);
// Workaround for #49904
if (span.kind === 'comment') {
const line = document.lineAt(range.start.line).text;
if (line.match(/\/\/\s*#endregion/gi)) {
return undefined;
}
}
const start = range.start.line;
// workaround for #47240
const end = (range.end.character > 0 && ['}', ']'].includes(document.getText(new vscode.Range(range.end.translate(0, -1), range.end))))
? Math.max(range.end.line - 1, range.start.line)
: range.end.line;
return new vscode.FoldingRange(start, end, kind);
}
private static getFoldingRangeKind(span: Proto.OutliningSpan): vscode.FoldingRangeKind | undefined {
switch (span.kind) {
case 'comment': return vscode.FoldingRangeKind.Comment;
case 'region': return vscode.FoldingRangeKind.Region;
case 'imports': return vscode.FoldingRangeKind.Imports;
case 'code':
default: return undefined;
}
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
): vscode.Disposable {
return conditionalRegistration([
requireMinVersion(client, TypeScriptFoldingProvider.minVersion),
], () => {
return vscode.languages.registerFoldingRangeProvider(selector.syntax,
new TypeScriptFoldingProvider(client));
});
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEditProvider, vscode.OnTypeFormattingEditProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingOptionsManager: FileConfigurationManager
) { }
public async provideDocumentRangeFormattingEdits(
document: vscode.TextDocument,
range: vscode.Range,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<vscode.TextEdit[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token);
const args = typeConverters.Range.toFormattingRequestArgs(file, range);
const response = await this.client.execute('format', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(typeConverters.TextEdit.fromCodeEdit);
}
public async provideOnTypeFormattingEdits(
document: vscode.TextDocument,
position: vscode.Position,
ch: string,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<vscode.TextEdit[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token);
const args: Proto.FormatOnKeyRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
key: ch
};
const response = await this.client.execute('formatonkey', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
const result: vscode.TextEdit[] = [];
for (const edit of response.body) {
const textEdit = typeConverters.TextEdit.fromCodeEdit(edit);
const range = textEdit.range;
// Work around for https://github.com/microsoft/TypeScript/issues/6700.
// Check if we have an edit at the beginning of the line which only removes white spaces and leaves
// an empty line. Drop those edits
if (range.start.character === 0 && range.start.line === range.end.line && textEdit.newText === '') {
const lText = document.lineAt(range.start.line).text;
// If the edit leaves something on the line keep the edit (note that the end character is exclusive).
// Keep it also if it removes something else than whitespace
if (lText.trim().length > 0 || lText.length > range.end.character) {
result.push(textEdit);
}
} else {
result.push(textEdit);
}
}
return result;
}
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager
) {
return conditionalRegistration([
requireConfiguration(modeId, 'format.enable'),
], () => {
const formattingProvider = new TypeScriptFormattingProvider(client, fileConfigurationManager);
return vscode.Disposable.from(
vscode.languages.registerOnTypeFormattingEditProvider(selector.syntax, formattingProvider, ';', '}', '\n'),
vscode.languages.registerDocumentRangeFormattingEditProvider(selector.syntax, formattingProvider),
);
});
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { localize } from '../tsServer/versionProvider';
import { ClientCapability, ITypeScriptServiceClient, ServerType } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { markdownDocumentation } from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptHoverProvider implements vscode.HoverProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideHover(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Hover | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.interruptGetErr(() => this.client.execute('quickinfo', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
return new vscode.Hover(
this.getContents(document.uri, response.body, response._serverType),
typeConverters.Range.fromTextSpan(response.body));
}
private getContents(
resource: vscode.Uri,
data: Proto.QuickInfoResponseBody,
source: ServerType | undefined,
) {
const parts: vscode.MarkedString[] = [];
if (data.displayString) {
const displayParts: string[] = [];
if (source === ServerType.Syntax && this.client.hasCapabilityForResource(resource, ClientCapability.Semantic)) {
displayParts.push(
localize({
key: 'loadingPrefix',
comment: ['Prefix displayed for hover entries while the server is still loading']
}, "(loading...)"));
}
displayParts.push(data.displayString);
parts.push({ language: 'typescript', value: displayParts.join(' ') });
}
parts.push(markdownDocumentation(data.documentation, data.tags));
return parts;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
): vscode.Disposable {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerHoverProvider(selector.syntax,
new TypeScriptHoverProvider(client));
});
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import DefinitionProviderBase from './definitionProviderBase';
class TypeScriptImplementationProvider extends DefinitionProviderBase implements vscode.ImplementationProvider {
public provideImplementation(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
return this.getSymbolLocations('implementation', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerImplementationProvider(selector.semantic,
new TypeScriptImplementationProvider(client));
});
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
const localize = nls.loadMessageBundle();
const defaultJsDoc = new vscode.SnippetString(`/**\n * $0\n */`);
class JsDocCompletionItem extends vscode.CompletionItem {
constructor(
public readonly document: vscode.TextDocument,
public readonly position: vscode.Position
) {
super('/** */', vscode.CompletionItemKind.Snippet);
this.detail = localize('typescript.jsDocCompletionItem.documentation', 'JSDoc comment');
this.sortText = '\0';
const line = document.lineAt(position.line).text;
const prefix = line.slice(0, position.character).match(/\/\**\s*$/);
const suffix = line.slice(position.character).match(/^\s*\**\//);
const start = position.translate(0, prefix ? -prefix[0].length : 0);
const range = new vscode.Range(start, position.translate(0, suffix ? suffix[0].length : 0));
this.range = { inserting: range, replacing: range };
}
}
class JsDocCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly client: ITypeScriptServiceClient,
) { }
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.CompletionItem[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
if (!this.isPotentiallyValidDocCompletionPosition(document, position)) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const response = await this.client.execute('docCommentTemplate', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const item = new JsDocCompletionItem(document, position);
// Workaround for #43619
// docCommentTemplate previously returned undefined for empty jsdoc templates.
// TS 2.7 now returns a single line doc comment, which breaks indentation.
if (response.body.newText === '/** */') {
item.insertText = defaultJsDoc;
} else {
item.insertText = templateToSnippet(response.body.newText);
}
return [item];
}
private isPotentiallyValidDocCompletionPosition(
document: vscode.TextDocument,
position: vscode.Position
): boolean {
// Only show the JSdoc completion when the everything before the cursor is whitespace
// or could be the opening of a comment
const line = document.lineAt(position.line).text;
const prefix = line.slice(0, position.character);
if (!/^\s*$|\/\*\*\s*$|^\s*\/\*\*+\s*$/.test(prefix)) {
return false;
}
// And everything after is possibly a closing comment or more whitespace
const suffix = line.slice(position.character);
return /^\s*(\*+\/)?\s*$/.test(suffix);
}
}
export function templateToSnippet(template: string): vscode.SnippetString {
// TODO: use append placeholder
let snippetIndex = 1;
template = template.replace(/\$/g, '\\$');
template = template.replace(/^\s*(?=(\/|[ ]\*))/gm, '');
template = template.replace(/^(\/\*\*\s*\*[ ]*)$/m, (x) => x + `\$0`);
template = template.replace(/\* @param([ ]\{\S+\})?\s+(\S+)\s*$/gm, (_param, type, post) => {
let out = '* @param ';
if (type === ' {any}' || type === ' {*}') {
out += `{\$\{${snippetIndex++}:*\}} `;
} else if (type) {
out += type + ' ';
}
out += post + ` \${${snippetIndex++}}`;
return out;
});
return new vscode.SnippetString(template);
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
): vscode.Disposable {
return conditionalRegistration([
requireConfiguration(modeId, 'suggest.completeJSDocs')
], () => {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new JsDocCompletionProvider(client),
'*');
});
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from typescript-sublime-plugin project, obtained from
* https://github.com/microsoft/TypeScript-Sublime-Plugin/blob/master/TypeScript%20Indent.tmPreferences
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { Disposable } from '../utils/dispose';
import * as languageModeIds from '../utils/languageModeIds';
const jsTsLanguageConfiguration: vscode.LanguageConfiguration = {
indentationRules: {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
},
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
onEnterRules: [
{
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: ' * ' },
}, {
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction: vscode.IndentAction.None, appendText: ' * ' },
}, {
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: { indentAction: vscode.IndentAction.None, appendText: '* ' },
}, {
// e.g. */|
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 },
},
{
// e.g. *-----*/|
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 },
},
{
beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/,
afterText: /^(?!\s*(\bcase\b|\bdefault\b))/,
action: { indentAction: vscode.IndentAction.Indent },
}
]
};
const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
const jsxTagsLanguageConfiguration: vscode.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w\\-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w\\-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: vscode.IndentAction.Indent }
},
{
// `beforeText` only applies to tokens of a given language. Since we are dealing with jsx-tags,
// make sure we apply to the closing `>` of a tag so that mixed language spans
// such as `<div onclick={1}>` are handled properly.
beforeText: /^>$/,
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent }
},
{
beforeText: /^>$/,
action: { indentAction: vscode.IndentAction.Indent }
},
],
};
export class LanguageConfigurationManager extends Disposable {
constructor() {
super();
const standardLanguages = [
languageModeIds.javascript,
languageModeIds.javascriptreact,
languageModeIds.typescript,
languageModeIds.typescriptreact,
];
for (const language of standardLanguages) {
this.registerConfiguration(language, jsTsLanguageConfiguration);
}
this.registerConfiguration(languageModeIds.jsxTags, jsxTagsLanguageConfiguration);
}
private registerConfiguration(language: string, config: vscode.LanguageConfiguration) {
this._register(vscode.languages.setLanguageConfiguration(language, config));
}
}

View File

@@ -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 * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { Command, CommandManager } from '../commands/commandManager';
import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeconverts from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class OrganizeImportsCommand implements Command {
public static readonly Id = '_typescript.organizeImports';
public readonly id = OrganizeImportsCommand.Id;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(file: string): Promise<boolean> {
/* __GDPR__
"organizeImports.execute" : {
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('organizeImports.execute', {});
const args: Proto.OrganizeImportsRequestArgs = {
scope: {
type: 'file',
args: {
file
}
}
};
const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken));
if (response.type !== 'response' || !response.body) {
return false;
}
const edits = typeconverts.WorkspaceEdit.fromFileCodeEdits(this.client, response.body);
return vscode.workspace.applyEdit(edits);
}
}
export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvider {
public static readonly minVersion = API.v280;
public constructor(
private readonly client: ITypeScriptServiceClient,
commandManager: CommandManager,
private readonly fileConfigManager: FileConfigurationManager,
telemetryReporter: TelemetryReporter,
) {
commandManager.register(new OrganizeImportsCommand(client, telemetryReporter));
}
public readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [vscode.CodeActionKind.SourceOrganizeImports]
};
public provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
if (!context.only || !context.only.contains(vscode.CodeActionKind.SourceOrganizeImports)) {
return [];
}
this.fileConfigManager.ensureConfigurationForDocument(document, token);
const action = new vscode.CodeAction(
localize('organizeImportsAction.title', "Organize Imports"),
vscode.CodeActionKind.SourceOrganizeImports);
action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file] };
return [action];
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
commandManager: CommandManager,
fileConfigurationManager: FileConfigurationManager,
telemetryReporter: TelemetryReporter,
) {
return conditionalRegistration([
requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter);
return vscode.languages.registerCodeActionsProvider(selector.semantic,
organizeImportsProvider,
organizeImportsProvider.metadata);
});
}

View File

@@ -0,0 +1,421 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as fixNames from '../utils/fixNames';
import { memoize } from '../utils/memoize';
import { equals } from '../utils/objects';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class ApplyCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCodeActionCommand';
public readonly id = ApplyCodeActionCommand.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(
action: Proto.CodeFixAction
): Promise<boolean> {
/* __GDPR__
"quickFix.execute" : {
"fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('quickFix.execute', {
fixName: action.fixName
});
return applyCodeActionCommands(this.client, action.commands, nulToken);
}
}
class ApplyFixAllCodeAction implements Command {
public static readonly ID = '_typescript.applyFixAllCodeAction';
public readonly id = ApplyFixAllCodeAction.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(
file: string,
tsAction: Proto.CodeFixAction,
): Promise<void> {
if (!tsAction.fixId) {
return;
}
/* __GDPR__
"quickFixAll.execute" : {
"fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('quickFixAll.execute', {
fixName: tsAction.fixName
});
const args: Proto.GetCombinedCodeFixRequestArgs = {
scope: {
type: 'file',
args: { file }
},
fixId: tsAction.fixId,
};
const response = await this.client.execute('getCombinedCodeFix', args, nulToken);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const edit = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body.changes);
await vscode.workspace.applyEdit(edit);
await applyCodeActionCommands(this.client, response.body.commands, nulToken);
}
}
/**
* Unique set of diagnostics keyed on diagnostic range and error code.
*/
class DiagnosticsSet {
public static from(diagnostics: vscode.Diagnostic[]) {
const values = new Map<string, vscode.Diagnostic>();
for (const diagnostic of diagnostics) {
values.set(DiagnosticsSet.key(diagnostic), diagnostic);
}
return new DiagnosticsSet(values);
}
private static key(diagnostic: vscode.Diagnostic) {
const { start, end } = diagnostic.range;
return `${diagnostic.code}-${start.line},${start.character}-${end.line},${end.character}`;
}
private constructor(
private readonly _values: Map<string, vscode.Diagnostic>
) { }
public get values(): Iterable<vscode.Diagnostic> {
return this._values.values();
}
public get size() {
return this._values.size;
}
}
class VsCodeCodeAction extends vscode.CodeAction {
constructor(
public readonly tsAction: Proto.CodeFixAction,
title: string,
kind: vscode.CodeActionKind,
public readonly isFixAll: boolean,
) {
super(title, kind);
}
}
class CodeActionSet {
private readonly _actions = new Set<VsCodeCodeAction>();
private readonly _fixAllActions = new Map<{}, VsCodeCodeAction>();
public get values(): Iterable<VsCodeCodeAction> {
return this._actions;
}
public addAction(action: VsCodeCodeAction) {
for (const existing of this._actions) {
if (action.tsAction.fixName === existing.tsAction.fixName && equals(action.edit, existing.edit)) {
this._actions.delete(existing);
}
}
this._actions.add(action);
if (action.tsAction.fixId) {
// If we have an existing fix all action, then make sure it follows this action
const existingFixAll = this._fixAllActions.get(action.tsAction.fixId);
if (existingFixAll) {
this._actions.delete(existingFixAll);
this._actions.add(existingFixAll);
}
}
}
public addFixAllAction(fixId: {}, action: VsCodeCodeAction) {
const existing = this._fixAllActions.get(fixId);
if (existing) {
// reinsert action at back of actions list
this._actions.delete(existing);
}
this.addAction(action);
this._fixAllActions.set(fixId, action);
}
public hasFixAllAction(fixId: {}) {
return this._fixAllActions.has(fixId);
}
}
class SupportedCodeActionProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async getFixableDiagnosticsForContext(context: vscode.CodeActionContext): Promise<DiagnosticsSet> {
const fixableCodes = await this.fixableDiagnosticCodes;
return DiagnosticsSet.from(
context.diagnostics.filter(diagnostic => typeof diagnostic.code !== 'undefined' && fixableCodes.has(diagnostic.code + '')));
}
@memoize
private get fixableDiagnosticCodes(): Thenable<Set<string>> {
return this.client.execute('getSupportedCodeFixes', null, nulToken)
.then(response => response.type === 'response' ? response.body || [] : [])
.then(codes => new Set(codes));
}
}
class TypeScriptQuickFixProvider implements vscode.CodeActionProvider {
public static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
};
private readonly supportedCodeActionProvider: SupportedCodeActionProvider;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
private readonly diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
commandManager.register(new ApplyCodeActionCommand(client, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
public async provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<vscode.CodeAction[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(context);
if (!fixableDiagnostics.size) {
return [];
}
if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) {
return [];
}
await this.formattingConfigurationManager.ensureConfigurationForDocument(document, token);
const results = new CodeActionSet();
for (const diagnostic of fixableDiagnostics.values) {
await this.getFixesForDiagnostic(document, file, diagnostic, results, token);
}
const allActions = Array.from(results.values);
for (const action of allActions) {
action.isPreferred = isPreferredFix(action, allActions);
}
return allActions;
}
private async getFixesForDiagnostic(
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
results: CodeActionSet,
token: vscode.CancellationToken,
): Promise<CodeActionSet> {
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await this.client.execute('getCodeFixes', args, token);
if (response.type !== 'response' || !response.body) {
return results;
}
for (const tsCodeFix of response.body) {
this.addAllFixesForTsCodeAction(results, document, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
}
return results;
}
private addAllFixesForTsCodeAction(
results: CodeActionSet,
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): CodeActionSet {
results.addAction(this.getSingleFixForTsCodeAction(diagnostic, tsAction));
this.addFixAllForTsCodeAction(results, document, file, diagnostic, tsAction as Proto.CodeFixAction);
return results;
}
private getSingleFixForTsCodeAction(
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): VsCodeCodeAction {
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix, false);
codeAction.edit = getEditForCodeAction(this.client, tsAction);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [tsAction],
title: ''
};
return codeAction;
}
private addFixAllForTsCodeAction(
results: CodeActionSet,
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction,
): CodeActionSet {
if (!tsAction.fixId || this.client.apiVersion.lt(API.v270) || results.hasFixAllAction(tsAction.fixId)) {
return results;
}
// Make sure there are multiple diagnostics of the same type in the file
if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => {
if (x === diagnostic) {
return false;
}
return x.code === diagnostic.code
|| (fixAllErrorCodes.has(x.code as number) && fixAllErrorCodes.get(x.code as number) === fixAllErrorCodes.get(diagnostic.code as number));
})) {
return results;
}
const action = new VsCodeCodeAction(
tsAction,
tsAction.fixAllDescription || localize('fixAllInFileLabel', '{0} (Fix all in file)', tsAction.description),
vscode.CodeActionKind.QuickFix, true);
action.diagnostics = [diagnostic];
action.command = {
command: ApplyFixAllCodeAction.ID,
arguments: [file, tsAction],
title: ''
};
results.addFixAllAction(tsAction.fixId, action);
return results;
}
}
// Some fix all actions can actually fix multiple differnt diagnostics. Make sure we still show the fix all action
// in such cases
const fixAllErrorCodes = new Map<number, number>([
// Missing async
[2339, 2339],
[2345, 2339],
]);
const preferredFixes = new Map<string, { readonly value: number, readonly thereCanOnlyBeOne?: boolean }>([
[fixNames.annotateWithTypeFromJSDoc, { value: 1 }],
[fixNames.constructorForDerivedNeedSuperCall, { value: 1 }],
[fixNames.extendsInterfaceBecomesImplements, { value: 1 }],
[fixNames.awaitInSyncFunction, { value: 1 }],
[fixNames.classIncorrectlyImplementsInterface, { value: 3 }],
[fixNames.classDoesntImplementInheritedAbstractMember, { value: 3 }],
[fixNames.unreachableCode, { value: 1 }],
[fixNames.unusedIdentifier, { value: 1 }],
[fixNames.forgottenThisPropertyAccess, { value: 1 }],
[fixNames.spelling, { value: 2 }],
[fixNames.addMissingAwait, { value: 1 }],
[fixNames.fixImport, { value: 0, thereCanOnlyBeOne: true }],
]);
function isPreferredFix(
action: VsCodeCodeAction,
allActions: readonly VsCodeCodeAction[]
): boolean {
if (action.isFixAll) {
return false;
}
const fixPriority = preferredFixes.get(action.tsAction.fixName);
if (!fixPriority) {
return false;
}
return allActions.every(otherAction => {
if (otherAction === action) {
return true;
}
if (otherAction.isFixAll) {
return true;
}
const otherFixPriority = preferredFixes.get(otherAction.tsAction.fixName);
if (!otherFixPriority || otherFixPriority.value < fixPriority.value) {
return true;
} else if (otherFixPriority.value > fixPriority.value) {
return false;
}
if (fixPriority.thereCanOnlyBeOne && action.tsAction.fixName === otherAction.tsAction.fixName) {
return false;
}
return true;
});
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeActionsProvider(selector.semantic,
new TypeScriptQuickFixProvider(client, fileConfigurationManager, commandManager, diagnosticsManager, telemetryReporter),
TypeScriptQuickFixProvider.metadata);
});
}

View File

@@ -0,0 +1,502 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as fileSchemes from '../utils/fileSchemes';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import FormattingOptionsManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
namespace Experimental {
export interface RefactorActionInfo extends Proto.RefactorActionInfo {
readonly notApplicableReason?: string;
}
}
interface DidApplyRefactoringCommand_Args {
readonly codeAction: InlinedCodeAction
}
class DidApplyRefactoringCommand implements Command {
public static readonly ID = '_typescript.didApplyRefactoring';
public readonly id = DidApplyRefactoringCommand.ID;
constructor(
private readonly telemetryReporter: TelemetryReporter
) { }
public async execute(args: DidApplyRefactoringCommand_Args): Promise<void> {
/* __GDPR__
"refactor.execute" : {
"action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('refactor.execute', {
action: args.codeAction.action,
});
if (!args.codeAction.edit?.size) {
vscode.window.showErrorMessage(localize('refactoringFailed', "Could not apply refactoring"));
return;
}
const renameLocation = args.codeAction.renameLocation;
if (renameLocation) {
await vscode.commands.executeCommand('editor.action.rename', [
args.codeAction.document.uri,
typeConverters.Position.fromLocation(renameLocation)
]);
}
}
}
interface SelectRefactorCommand_Args {
readonly action: vscode.CodeAction;
readonly document: vscode.TextDocument;
readonly info: Proto.ApplicableRefactorInfo;
readonly rangeOrSelection: vscode.Range | vscode.Selection;
}
class SelectRefactorCommand implements Command {
public static readonly ID = '_typescript.selectRefactoring';
public readonly id = SelectRefactorCommand.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly didApplyCommand: DidApplyRefactoringCommand
) { }
public async execute(args: SelectRefactorCommand_Args): Promise<void> {
const file = this.client.toOpenedFilePath(args.document);
if (!file) {
return;
}
const selected = await vscode.window.showQuickPick(args.info.actions.map((action): vscode.QuickPickItem => ({
label: action.name,
description: action.description,
})));
if (!selected) {
return;
}
const tsAction = new InlinedCodeAction(this.client, args.action.title, args.action.kind, args.document, args.info.name, selected.label, args.rangeOrSelection);
await tsAction.resolve(nulToken);
if (tsAction.edit) {
if (!(await vscode.workspace.applyEdit(tsAction.edit))) {
vscode.window.showErrorMessage(localize('refactoringFailed', "Could not apply refactoring"));
return;
}
}
await this.didApplyCommand.execute({ codeAction: tsAction });
}
}
interface CodeActionKind {
readonly kind: vscode.CodeActionKind;
matches(refactor: Proto.RefactorActionInfo): boolean;
}
const Extract_Function = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('function'),
matches: refactor => refactor.name.startsWith('function_')
});
const Extract_Constant = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('constant'),
matches: refactor => refactor.name.startsWith('constant_')
});
const Extract_Type = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('type'),
matches: refactor => refactor.name.startsWith('Extract to type alias')
});
const Extract_Interface = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('interface'),
matches: refactor => refactor.name.startsWith('Extract to interface')
});
const Move_NewFile = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.Refactor.append('move').append('newFile'),
matches: refactor => refactor.name.startsWith('Move to a new file')
});
const Rewrite_Import = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('import'),
matches: refactor => refactor.name.startsWith('Convert namespace import') || refactor.name.startsWith('Convert named imports')
});
const Rewrite_Export = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('export'),
matches: refactor => refactor.name.startsWith('Convert default export') || refactor.name.startsWith('Convert named export')
});
const Rewrite_Arrow_Braces = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('arrow').append('braces'),
matches: refactor => refactor.name.startsWith('Convert default export') || refactor.name.startsWith('Convert named export')
});
const Rewrite_Parameters_ToDestructured = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('parameters').append('toDestructured'),
matches: refactor => refactor.name.startsWith('Convert parameters to destructured object')
});
const Rewrite_Property_GenerateAccessors = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('property').append('generateAccessors'),
matches: refactor => refactor.name.startsWith('Generate \'get\' and \'set\' accessors')
});
const allKnownCodeActionKinds = [
Extract_Function,
Extract_Constant,
Extract_Type,
Extract_Interface,
Move_NewFile,
Rewrite_Import,
Rewrite_Export,
Rewrite_Arrow_Braces,
Rewrite_Parameters_ToDestructured,
Rewrite_Property_GenerateAccessors
];
class InlinedCodeAction extends vscode.CodeAction {
constructor(
public readonly client: ITypeScriptServiceClient,
title: string,
kind: vscode.CodeActionKind | undefined,
public readonly document: vscode.TextDocument,
public readonly refactor: string,
public readonly action: string,
public readonly range: vscode.Range,
) {
super(title, kind);
}
// Filled in during resolve
public renameLocation?: Proto.Location;
public async resolve(token: vscode.CancellationToken): Promise<undefined> {
const file = this.client.toOpenedFilePath(this.document);
if (!file) {
return;
}
const args: Proto.GetEditsForRefactorRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, this.range),
refactor: this.refactor,
action: this.action,
};
const response = await this.client.execute('getEditsForRefactor', args, token);
if (response.type !== 'response' || !response.body) {
return;
}
// Resolve
this.edit = InlinedCodeAction.getWorkspaceEditForRefactoring(this.client, response.body);
this.renameLocation = response.body.renameLocation;
return;
}
private static getWorkspaceEditForRefactoring(
client: ITypeScriptServiceClient,
body: Proto.RefactorEditInfo,
): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
for (const edit of body.edits) {
const resource = client.toResource(edit.fileName);
if (resource.scheme === fileSchemes.file) {
workspaceEdit.createFile(resource, { ignoreIfExists: true });
}
}
typeConverters.WorkspaceEdit.withFileCodeEdits(workspaceEdit, client, body.edits);
return workspaceEdit;
}
}
class SelectCodeAction extends vscode.CodeAction {
constructor(
info: Proto.ApplicableRefactorInfo,
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection
) {
super(info.description, vscode.CodeActionKind.Refactor);
this.command = {
title: info.description,
command: SelectRefactorCommand.ID,
arguments: [<SelectRefactorCommand_Args>{ action: this, document, info, rangeOrSelection }]
};
}
}
type TsCodeAction = InlinedCodeAction | SelectCodeAction;
class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeAction> {
public static readonly minVersion = API.v240;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingOptionsManager: FormattingOptionsManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter
) {
const didApplyRefactoringCommand = commandManager.register(new DidApplyRefactoringCommand(telemetryReporter));
commandManager.register(new SelectRefactorCommand(this.client, didApplyRefactoringCommand));
}
public static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
vscode.CodeActionKind.Refactor,
...allKnownCodeActionKinds.map(x => x.kind),
],
documentation: [
{
kind: vscode.CodeActionKind.Refactor,
command: {
command: LearnMoreAboutRefactoringsCommand.id,
title: localize('refactor.documentation.title', "Learn more about JS/TS refactorings")
}
}
]
};
public async provideCodeActions(
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<TsCodeAction[] | undefined> {
if (!this.shouldTrigger(rangeOrSelection, context)) {
return undefined;
}
if (!this.client.toOpenedFilePath(document)) {
return undefined;
}
const response = await this.client.interruptGetErr(() => {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
this.formattingOptionsManager.ensureConfigurationForDocument(document, token);
const args: Proto.GetApplicableRefactorsRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, rangeOrSelection),
triggerReason: this.toTsTriggerReason(context),
};
return this.client.execute('getApplicableRefactors', args, token);
});
if (response?.type !== 'response' || !response.body) {
return undefined;
}
const actions = this.convertApplicableRefactors(response.body, document, rangeOrSelection);
if (!context.only) {
return actions;
}
return this.pruneInvalidActions(this.appendInvalidActions(actions), context.only, /* numberOfInvalid = */ 5);
}
public async resolveCodeAction(
codeAction: TsCodeAction,
token: vscode.CancellationToken,
): Promise<TsCodeAction> {
if (codeAction instanceof InlinedCodeAction) {
await codeAction.resolve(token);
}
return codeAction;
}
private toTsTriggerReason(context: vscode.CodeActionContext): Proto.RefactorTriggerReason | undefined {
if (!context.only) {
return;
}
return 'invoked';
}
private convertApplicableRefactors(
body: Proto.ApplicableRefactorInfo[],
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection
): TsCodeAction[] {
const actions: TsCodeAction[] = [];
for (const info of body) {
if (info.inlineable === false) {
const codeAction = new SelectCodeAction(info, document, rangeOrSelection);
actions.push(codeAction);
} else {
for (const action of info.actions) {
actions.push(this.refactorActionToCodeAction(action, document, info, rangeOrSelection, info.actions));
}
}
}
return actions;
}
private refactorActionToCodeAction(
action: Experimental.RefactorActionInfo,
document: vscode.TextDocument,
info: Proto.ApplicableRefactorInfo,
rangeOrSelection: vscode.Range | vscode.Selection,
allActions: readonly Proto.RefactorActionInfo[],
): InlinedCodeAction {
const codeAction = new InlinedCodeAction(this.client, action.description, TypeScriptRefactorProvider.getKind(action), document, info.name, action.name, rangeOrSelection);
// https://github.com/microsoft/TypeScript/pull/37871
if (action.notApplicableReason) {
codeAction.disabled = { reason: action.notApplicableReason };
} else {
codeAction.command = {
title: action.description,
command: DidApplyRefactoringCommand.ID,
arguments: [<DidApplyRefactoringCommand_Args>{ codeAction }],
};
}
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
return codeAction;
}
private shouldTrigger(rangeOrSelection: vscode.Range | vscode.Selection, context: vscode.CodeActionContext) {
if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) {
return false;
}
return rangeOrSelection instanceof vscode.Selection;
}
private static getKind(refactor: Proto.RefactorActionInfo) {
const match = allKnownCodeActionKinds.find(kind => kind.matches(refactor));
return match ? match.kind : vscode.CodeActionKind.Refactor;
}
private static isPreferred(
action: Proto.RefactorActionInfo,
allActions: readonly Proto.RefactorActionInfo[],
): boolean {
if (Extract_Constant.matches(action)) {
// Only mark the action with the lowest scope as preferred
const getScope = (name: string) => {
const scope = name.match(/scope_(\d)/)?.[1];
return scope ? +scope : undefined;
};
const scope = getScope(action.name);
if (typeof scope !== 'number') {
return false;
}
return allActions
.filter(otherAtion => otherAtion !== action && Extract_Constant.matches(otherAtion))
.every(otherAction => {
const otherScope = getScope(otherAction.name);
return typeof otherScope === 'number' ? scope < otherScope : true;
});
}
if (Extract_Type.matches(action) || Extract_Interface.matches(action)) {
return true;
}
return false;
}
private appendInvalidActions(actions: vscode.CodeAction[]): vscode.CodeAction[] {
if (this.client.apiVersion.gte(API.v400)) {
// Invalid actions come from TS server instead
return actions;
}
if (!actions.some(action => action.kind && Extract_Constant.kind.contains(action.kind))) {
const disabledAction = new vscode.CodeAction(
localize('extractConstant.disabled.title', "Extract to constant"),
Extract_Constant.kind);
disabledAction.disabled = {
reason: localize('extractConstant.disabled.reason', "The current selection cannot be extracted"),
};
disabledAction.isPreferred = true;
actions.push(disabledAction);
}
if (!actions.some(action => action.kind && Extract_Function.kind.contains(action.kind))) {
const disabledAction = new vscode.CodeAction(
localize('extractFunction.disabled.title', "Extract to function"),
Extract_Function.kind);
disabledAction.disabled = {
reason: localize('extractFunction.disabled.reason', "The current selection cannot be extracted"),
};
actions.push(disabledAction);
}
return actions;
}
private pruneInvalidActions(actions: vscode.CodeAction[], only?: vscode.CodeActionKind, numberOfInvalid?: number): vscode.CodeAction[] {
if (this.client.apiVersion.lt(API.v400)) {
// Older TS version don't return extra actions
return actions;
}
const availableActions: vscode.CodeAction[] = [];
const invalidCommonActions: vscode.CodeAction[] = [];
const invalidUncommonActions: vscode.CodeAction[] = [];
for (const action of actions) {
if (!action.disabled) {
availableActions.push(action);
continue;
}
// These are the common refactors that we should always show if applicable.
if (action.kind && (Extract_Constant.kind.contains(action.kind) || Extract_Function.kind.contains(action.kind))) {
invalidCommonActions.push(action);
continue;
}
// These are the remaining refactors that we can show if we haven't reached the max limit with just common refactors.
invalidUncommonActions.push(action);
}
const prioritizedActions: vscode.CodeAction[] = [];
prioritizedActions.push(...invalidCommonActions);
prioritizedActions.push(...invalidUncommonActions);
const topNInvalid = prioritizedActions.filter(action => !only || (action.kind && only.contains(action.kind))).slice(0, numberOfInvalid);
availableActions.push(...topNInvalid);
return availableActions;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
formattingOptionsManager: FormattingOptionsManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter,
) {
return conditionalRegistration([
requireMinVersion(client, TypeScriptRefactorProvider.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeActionsProvider(selector.semantic,
new TypeScriptRefactorProvider(client, formattingOptionsManager, commandManager, telemetryReporter),
TypeScriptRefactorProvider.metadata);
});
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptReferenceSupport implements vscode.ReferenceProvider {
public constructor(
private readonly client: ITypeScriptServiceClient) { }
public async provideReferences(
document: vscode.TextDocument,
position: vscode.Position,
options: vscode.ReferenceContext,
token: vscode.CancellationToken
): Promise<vscode.Location[]> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return [];
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('references', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
const result: vscode.Location[] = [];
for (const ref of response.body.refs) {
if (!options.includeDeclaration && ref.isDefinition) {
continue;
}
const url = this.client.toResource(ref.file);
const location = typeConverters.Location.fromTextSpan(url, ref);
result.push(location);
}
return result;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerReferenceProvider(selector.syntax,
new TypeScriptReferenceSupport(client));
});
}

View File

@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* 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 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class TypeScriptRenameProvider implements vscode.RenameProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager
) { }
public async prepareRename(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Range | null> {
if (this.client.apiVersion.lt(API.v310)) {
return null;
}
const response = await this.execRename(document, position, token);
if (response?.type !== 'response' || !response.body) {
return null;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.Range>(renameInfo.localizedErrorMessage);
}
return typeConverters.Range.fromTextSpan(renameInfo.triggerSpan);
}
public async provideRenameEdits(
document: vscode.TextDocument,
position: vscode.Position,
newName: string,
token: vscode.CancellationToken
): Promise<vscode.WorkspaceEdit | null> {
const response = await this.execRename(document, position, token);
if (!response || response.type !== 'response' || !response.body) {
return null;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.WorkspaceEdit>(renameInfo.localizedErrorMessage);
}
if (renameInfo.fileToRename) {
const edits = await this.renameFile(renameInfo.fileToRename, newName, token);
if (edits) {
return edits;
} else {
return Promise.reject<vscode.WorkspaceEdit>(localize('fileRenameFail', "An error occurred while renaming file"));
}
}
return this.updateLocs(response.body.locs, newName);
}
public async execRename(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.RenameResponse> | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.RenameRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
findInStrings: false,
findInComments: false
};
return this.client.interruptGetErr(() => {
this.fileConfigurationManager.ensureConfigurationForDocument(document, token);
return this.client.execute('rename', args, token);
});
}
private updateLocs(
locations: ReadonlyArray<Proto.SpanGroup>,
newName: string
) {
const edit = new vscode.WorkspaceEdit();
for (const spanGroup of locations) {
const resource = this.client.toResource(spanGroup.file);
for (const textSpan of spanGroup.locs) {
edit.replace(resource, typeConverters.Range.fromTextSpan(textSpan),
(textSpan.prefixText || '') + newName + (textSpan.suffixText || ''));
}
}
return edit;
}
private async renameFile(
fileToRename: string,
newName: string,
token: vscode.CancellationToken,
): Promise<vscode.WorkspaceEdit | undefined> {
// Make sure we preserve file extension if none provided
if (!path.extname(newName)) {
newName += path.extname(fileToRename);
}
const dirname = path.dirname(fileToRename);
const newFilePath = path.join(dirname, newName);
const args: Proto.GetEditsForFileRenameRequestArgs & { file: string } = {
file: fileToRename,
oldFilePath: fileToRename,
newFilePath: newFilePath,
};
const response = await this.client.execute('getEditsForFileRename', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const edits = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body);
edits.renameFile(vscode.Uri.file(fileToRename), vscode.Uri.file(newFilePath));
return edits;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerRenameProvider(selector.semantic,
new TypeScriptRenameProvider(client, fileConfigurationManager));
});
}

View File

@@ -0,0 +1,283 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// all constants are const
import { TokenEncodingConsts, TokenModifier, TokenType, VersionRequirement } from 'typescript-vscode-sh-plugin/lib/constants';
import * as vscode from 'vscode';
import * as Proto from '../protocol';
import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}.${VersionRequirement.minor}`);
// as we don't do deltas, for performance reasons, don't compute semantic tokens for documents above that limit
const CONTENT_LENGTH_LIMIT = 100000;
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, minTypeScriptVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const provider = new DocumentSemanticTokensProvider(client);
return vscode.Disposable.from(
// register only as a range provider
vscode.languages.registerDocumentRangeSemanticTokensProvider(selector.semantic, provider, provider.getLegend()),
);
});
}
/**
* Prototype of a DocumentSemanticTokensProvider, relying on the experimental `encodedSemanticClassifications-full` request from the TypeScript server.
* As the results retured by the TypeScript server are limited, we also add a Typescript plugin (typescript-vscode-sh-plugin) to enrich the returned token.
* See https://github.com/aeschli/typescript-vscode-sh-plugin.
*/
class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider, vscode.DocumentRangeSemanticTokensProvider {
constructor(private readonly client: ITypeScriptServiceClient) {
}
getLegend(): vscode.SemanticTokensLegend {
return new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);
}
async provideDocumentSemanticTokens(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file || document.getText().length > CONTENT_LENGTH_LIMIT) {
return null;
}
return this._provideSemanticTokens(document, { file, start: 0, length: document.getText().length }, token);
}
async provideDocumentRangeSemanticTokens(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file || (document.offsetAt(range.end) - document.offsetAt(range.start) > CONTENT_LENGTH_LIMIT)) {
return null;
}
const start = document.offsetAt(range.start);
const length = document.offsetAt(range.end) - start;
return this._provideSemanticTokens(document, { file, start, length }, token);
}
async _provideSemanticTokens(document: vscode.TextDocument, requestArg: ExperimentalProtocol.EncodedSemanticClassificationsRequestArgs, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return null;
}
let versionBeforeRequest = document.version;
const response = await (this.client as ExperimentalProtocol.IExtendedTypeScriptServiceClient).execute('encodedSemanticClassifications-full', requestArg, token);
if (response.type !== 'response' || !response.body) {
return null;
}
const versionAfterRequest = document.version;
if (versionBeforeRequest !== versionAfterRequest) {
// cannot convert result's offsets to (line;col) values correctly
// a new request will come in soon...
//
// here we cannot return null, because returning null would remove all semantic tokens.
// we must throw to indicate that the semantic tokens should not be removed.
// using the string busy here because it is not logged to error telemetry if the error text contains busy.
// as the new request will come in right after our response, we first wait for the document activity to stop
await waitForDocumentChangesToEnd(document);
throw new Error('busy');
}
const tokenSpan = response.body.spans;
const builder = new vscode.SemanticTokensBuilder();
let i = 0;
while (i < tokenSpan.length) {
const offset = tokenSpan[i++];
const length = tokenSpan[i++];
const tsClassification = tokenSpan[i++];
let tokenModifiers = 0;
let tokenType = getTokenTypeFromClassification(tsClassification);
if (tokenType !== undefined) {
// it's a classification as returned by the typescript-vscode-sh-plugin
tokenModifiers = getTokenModifierFromClassification(tsClassification);
} else {
// typescript-vscode-sh-plugin is not present
tokenType = tokenTypeMap[tsClassification];
if (tokenType === undefined) {
continue;
}
}
// we can use the document's range conversion methods because the result is at the same version as the document
const startPos = document.positionAt(offset);
const endPos = document.positionAt(offset + length);
for (let line = startPos.line; line <= endPos.line; line++) {
const startCharacter = (line === startPos.line ? startPos.character : 0);
const endCharacter = (line === endPos.line ? endPos.character : document.lineAt(line).text.length);
builder.push(line, startCharacter, endCharacter - startCharacter, tokenType, tokenModifiers);
}
}
return builder.build();
}
}
function waitForDocumentChangesToEnd(document: vscode.TextDocument) {
let version = document.version;
return new Promise<void>((s) => {
let iv = setInterval(_ => {
if (document.version === version) {
clearInterval(iv);
s();
}
version = document.version;
}, 400);
});
}
// typescript-vscode-sh-plugin encodes type and modifiers in the classification:
// TSClassification = (TokenType + 1) << 8 + TokenModifier
function getTokenTypeFromClassification(tsClassification: number): number | undefined {
if (tsClassification > TokenEncodingConsts.modifierMask) {
return (tsClassification >> TokenEncodingConsts.typeOffset) - 1;
}
return undefined;
}
function getTokenModifierFromClassification(tsClassification: number) {
return tsClassification & TokenEncodingConsts.modifierMask;
}
const tokenTypes: string[] = [];
tokenTypes[TokenType.class] = 'class';
tokenTypes[TokenType.enum] = 'enum';
tokenTypes[TokenType.interface] = 'interface';
tokenTypes[TokenType.namespace] = 'namespace';
tokenTypes[TokenType.typeParameter] = 'typeParameter';
tokenTypes[TokenType.type] = 'type';
tokenTypes[TokenType.parameter] = 'parameter';
tokenTypes[TokenType.variable] = 'variable';
tokenTypes[TokenType.enumMember] = 'enumMember';
tokenTypes[TokenType.property] = 'property';
tokenTypes[TokenType.function] = 'function';
tokenTypes[TokenType.member] = 'member';
const tokenModifiers: string[] = [];
tokenModifiers[TokenModifier.async] = 'async';
tokenModifiers[TokenModifier.declaration] = 'declaration';
tokenModifiers[TokenModifier.readonly] = 'readonly';
tokenModifiers[TokenModifier.static] = 'static';
tokenModifiers[TokenModifier.local] = 'local';
tokenModifiers[TokenModifier.defaultLibrary] = 'defaultLibrary';
// make sure token types and modifiers are complete
if (tokenTypes.filter(t => !!t).length !== TokenType._) {
console.warn('typescript-vscode-sh-plugin has added new tokens types.');
}
if (tokenModifiers.filter(t => !!t).length !== TokenModifier._) {
console.warn('typescript-vscode-sh-plugin has added new tokens modifiers.');
}
// mapping for the original ExperimentalProtocol.ClassificationType from TypeScript (only used when plugin is not available)
const tokenTypeMap: number[] = [];
tokenTypeMap[ExperimentalProtocol.ClassificationType.className] = TokenType.class;
tokenTypeMap[ExperimentalProtocol.ClassificationType.enumName] = TokenType.enum;
tokenTypeMap[ExperimentalProtocol.ClassificationType.interfaceName] = TokenType.interface;
tokenTypeMap[ExperimentalProtocol.ClassificationType.moduleName] = TokenType.namespace;
tokenTypeMap[ExperimentalProtocol.ClassificationType.typeParameterName] = TokenType.typeParameter;
tokenTypeMap[ExperimentalProtocol.ClassificationType.typeAliasName] = TokenType.type;
tokenTypeMap[ExperimentalProtocol.ClassificationType.parameterName] = TokenType.parameter;
namespace ExperimentalProtocol {
export interface IExtendedTypeScriptServiceClient {
execute<K extends keyof ExperimentalProtocol.ExtendedTsServerRequests>(
command: K,
args: ExperimentalProtocol.ExtendedTsServerRequests[K][0],
token: vscode.CancellationToken,
config?: ExecConfig
): Promise<ServerResponse.Response<ExperimentalProtocol.ExtendedTsServerRequests[K][1]>>;
}
/**
* A request to get encoded semantic classifications for a span in the file
*/
export interface EncodedSemanticClassificationsRequest extends Proto.FileRequest {
arguments: EncodedSemanticClassificationsRequestArgs;
}
/**
* Arguments for EncodedSemanticClassificationsRequest request.
*/
export interface EncodedSemanticClassificationsRequestArgs extends Proto.FileRequestArgs {
/**
* Start position of the span.
*/
start: number;
/**
* Length of the span.
*/
length: number;
}
export const enum EndOfLineState {
None,
InMultiLineCommentTrivia,
InSingleQuoteStringLiteral,
InDoubleQuoteStringLiteral,
InTemplateHeadOrNoSubstitutionTemplate,
InTemplateMiddleOrTail,
InTemplateSubstitutionPosition,
}
export const enum ClassificationType {
comment = 1,
identifier = 2,
keyword = 3,
numericLiteral = 4,
operator = 5,
stringLiteral = 6,
regularExpressionLiteral = 7,
whiteSpace = 8,
text = 9,
punctuation = 10,
className = 11,
enumName = 12,
interfaceName = 13,
moduleName = 14,
typeParameterName = 15,
typeAliasName = 16,
parameterName = 17,
docCommentTagName = 18,
jsxOpenTagName = 19,
jsxCloseTagName = 20,
jsxSelfClosingTagName = 21,
jsxAttribute = 22,
jsxText = 23,
jsxAttributeStringLiteralValue = 24,
bigintLiteral = 25,
}
export interface EncodedSemanticClassificationsResponse extends Proto.Response {
body?: {
endOfLineState: EndOfLineState;
spans: number[];
};
}
export interface ExtendedTsServerRequests {
'encodedSemanticClassifications-full': [ExperimentalProtocol.EncodedSemanticClassificationsRequestArgs, ExperimentalProtocol.EncodedSemanticClassificationsResponse];
}
}

View File

@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as Previewer from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
public static readonly triggerCharacters = ['(', ',', '<'];
public static readonly retriggerCharacters = [')'];
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideSignatureHelp(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.SignatureHelpContext,
): Promise<vscode.SignatureHelp | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args: Proto.SignatureHelpRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(filepath, position),
triggerReason: toTsTriggerReason(context)
};
const response = await this.client.interruptGetErr(() => this.client.execute('signatureHelp', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
const info = response.body;
const result = new vscode.SignatureHelp();
result.signatures = info.items.map(signature => this.convertSignature(signature));
result.activeSignature = this.getActiveSignature(context, info, result.signatures);
result.activeParameter = this.getActiveParameter(info);
return result;
}
private getActiveSignature(context: vscode.SignatureHelpContext, info: Proto.SignatureHelpItems, signatures: readonly vscode.SignatureInformation[]): number {
// Try matching the previous active signature's label to keep it selected
const previouslyActiveSignature = context.activeSignatureHelp?.signatures[context.activeSignatureHelp.activeSignature];
if (previouslyActiveSignature && context.isRetrigger) {
const existingIndex = signatures.findIndex(other => other.label === previouslyActiveSignature?.label);
if (existingIndex >= 0) {
return existingIndex;
}
}
return info.selectedItemIndex;
}
private getActiveParameter(info: Proto.SignatureHelpItems): number {
const activeSignature = info.items[info.selectedItemIndex];
if (activeSignature && activeSignature.isVariadic) {
return Math.min(info.argumentIndex, activeSignature.parameters.length - 1);
}
return info.argumentIndex;
}
private convertSignature(item: Proto.SignatureHelpItem) {
const signature = new vscode.SignatureInformation(
Previewer.plain(item.prefixDisplayParts),
Previewer.markdownDocumentation(item.documentation, item.tags.filter(x => x.name !== 'param')));
let textIndex = signature.label.length;
const separatorLabel = Previewer.plain(item.separatorDisplayParts);
for (let i = 0; i < item.parameters.length; ++i) {
const parameter = item.parameters[i];
const label = Previewer.plain(parameter.displayParts);
signature.parameters.push(
new vscode.ParameterInformation(
[textIndex, textIndex + label.length],
Previewer.markdownDocumentation(parameter.documentation, [])));
textIndex += label.length;
signature.label += label;
if (i !== item.parameters.length - 1) {
signature.label += separatorLabel;
textIndex += separatorLabel.length;
}
}
signature.label += Previewer.plain(item.suffixDisplayParts);
return signature;
}
}
function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.SignatureHelpTriggerReason {
switch (context.triggerKind) {
case vscode.SignatureHelpTriggerKind.TriggerCharacter:
if (context.triggerCharacter) {
if (context.isRetrigger) {
return { kind: 'retrigger', triggerCharacter: context.triggerCharacter as any };
} else {
return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any };
}
} else {
return { kind: 'invoked' };
}
case vscode.SignatureHelpTriggerKind.ContentChange:
return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
case vscode.SignatureHelpTriggerKind.Invoke:
default:
return { kind: 'invoked' };
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerSignatureHelpProvider(selector.syntax,
new TypeScriptSignatureHelpProvider(client), {
triggerCharacters: TypeScriptSignatureHelpProvider.triggerCharacters,
retriggerCharacters: TypeScriptSignatureHelpProvider.retriggerCharacters
});
});
}

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class SmartSelection implements vscode.SelectionRangeProvider {
public static readonly minVersion = API.v350;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideSelectionRanges(
document: vscode.TextDocument,
positions: vscode.Position[],
token: vscode.CancellationToken,
): Promise<vscode.SelectionRange[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.SelectionRangeRequestArgs = {
file,
locations: positions.map(typeConverters.Position.toLocation)
};
const response = await this.client.execute('selectionRange', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(SmartSelection.convertSelectionRange);
}
private static convertSelectionRange(
selectionRange: Proto.SelectionRange
): vscode.SelectionRange {
return new vscode.SelectionRange(
typeConverters.Range.fromTextSpan(selectionRange.textSpan),
selectionRange.parent ? SmartSelection.convertSelectionRange(selectionRange.parent) : undefined,
);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, SmartSelection.minVersion),
], () => {
return vscode.languages.registerSelectionRangeProvider(selector.syntax, new SmartSelection(client));
});
}

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireMinVersion, requireConfiguration, Condition } from '../utils/dependentRegistration';
import { Disposable } from '../utils/dispose';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TagClosing extends Disposable {
public static readonly minVersion = API.v300;
private _disposed = false;
private _timeout: NodeJS.Timer | undefined = undefined;
private _cancel: vscode.CancellationTokenSource | undefined = undefined;
constructor(
private readonly client: ITypeScriptServiceClient
) {
super();
vscode.workspace.onDidChangeTextDocument(
event => this.onDidChangeTextDocument(event.document, event.contentChanges),
null,
this._disposables);
}
public dispose() {
super.dispose();
this._disposed = true;
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = undefined;
}
if (this._cancel) {
this._cancel.cancel();
this._cancel.dispose();
this._cancel = undefined;
}
}
private onDidChangeTextDocument(
document: vscode.TextDocument,
changes: readonly vscode.TextDocumentContentChangeEvent[]
) {
const activeDocument = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return;
}
if (typeof this._timeout !== 'undefined') {
clearTimeout(this._timeout);
}
if (this._cancel) {
this._cancel.cancel();
this._cancel.dispose();
this._cancel = undefined;
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') {
return;
}
const priorCharacter = lastChange.range.start.character > 0
? document.getText(new vscode.Range(lastChange.range.start.translate({ characterDelta: -1 }), lastChange.range.start))
: '';
if (priorCharacter === '>') {
return;
}
const version = document.version;
this._timeout = setTimeout(async () => {
this._timeout = undefined;
if (this._disposed) {
return;
}
const addedLines = lastChange.text.split(/\r\n|\n/g);
const position = addedLines.length <= 1
? lastChange.range.start.translate({ characterDelta: lastChange.text.length })
: new vscode.Position(lastChange.range.start.line + addedLines.length - 1, addedLines[addedLines.length - 1].length);
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
this._cancel = new vscode.CancellationTokenSource();
const response = await this.client.execute('jsxClosingTag', args, this._cancel.token);
if (response.type !== 'response' || !response.body) {
return;
}
if (this._disposed) {
return;
}
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
const insertion = response.body;
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
activeEditor.insertSnippet(
this.getTagSnippet(insertion),
this.getInsertionPositions(activeEditor, position));
}
}, 100);
}
private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString {
const snippet = new vscode.SnippetString();
snippet.appendPlaceholder('', 0);
snippet.appendText(closingTag.newText);
return snippet;
}
private getInsertionPositions(editor: vscode.TextEditor, position: vscode.Position) {
const activeSelectionPositions = editor.selections.map(s => s.active);
return activeSelectionPositions.some(p => p.isEqual(position))
? activeSelectionPositions
: position;
}
}
function requireActiveDocument(
selector: vscode.DocumentSelector
) {
return new Condition(
() => {
const editor = vscode.window.activeTextEditor;
return !!(editor && vscode.languages.match(selector, editor.document));
},
handler => {
return vscode.Disposable.from(
vscode.window.onDidChangeActiveTextEditor(handler),
vscode.workspace.onDidOpenTextDocument(handler));
});
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, TagClosing.minVersion),
requireConfiguration(modeId, 'autoClosingTags'),
requireActiveDocument(selector.syntax)
], () => new TagClosing(client));
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jsonc from 'jsonc-parser';
import { basename, dirname, join } from 'path';
import * as vscode from 'vscode';
import { coalesce, flatten } from '../utils/arrays';
function mapChildren<R>(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] {
return node && node.type === 'array' && node.children
? node.children.map(f)
: [];
}
class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
public provideDocumentLinks(
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.ProviderResult<vscode.DocumentLink[]> {
const root = jsonc.parseTree(document.getText());
if (!root) {
return null;
}
return coalesce([
this.getExtendsLink(document, root),
...this.getFilesLinks(document, root),
...this.getReferencesLinks(document, root)
]);
}
private getExtendsLink(document: vscode.TextDocument, root: jsonc.Node): vscode.DocumentLink | undefined {
const extendsNode = jsonc.findNodeAtLocation(root, ['extends']);
if (!this.isPathValue(extendsNode)) {
return undefined;
}
if (extendsNode.value.startsWith('.')) {
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.file(join(dirname(document.uri.fsPath), extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')))
);
}
const workspaceFolderPath = vscode.workspace.getWorkspaceFolder(document.uri)!.uri.fsPath;
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.file(join(workspaceFolderPath, 'node_modules', extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')))
);
}
private getFilesLinks(document: vscode.TextDocument, root: jsonc.Node) {
return mapChildren(
jsonc.findNodeAtLocation(root, ['files']),
child => this.pathNodeToLink(document, child));
}
private getReferencesLinks(document: vscode.TextDocument, root: jsonc.Node) {
return mapChildren(
jsonc.findNodeAtLocation(root, ['references']),
child => {
const pathNode = jsonc.findNodeAtLocation(child, ['path']);
if (!this.isPathValue(pathNode)) {
return undefined;
}
return new vscode.DocumentLink(this.getRange(document, pathNode),
basename(pathNode.value).endsWith('.json')
? this.getFileTarget(document, pathNode)
: this.getFolderTarget(document, pathNode));
});
}
private pathNodeToLink(
document: vscode.TextDocument,
node: jsonc.Node | undefined
): vscode.DocumentLink | undefined {
return this.isPathValue(node)
? new vscode.DocumentLink(this.getRange(document, node), this.getFileTarget(document, node))
: undefined;
}
private isPathValue(extendsNode: jsonc.Node | undefined): extendsNode is jsonc.Node {
return extendsNode
&& extendsNode.type === 'string'
&& extendsNode.value
&& !(extendsNode.value as string).includes('*'); // don't treat globs as links.
}
private getFileTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
return vscode.Uri.file(join(dirname(document.uri.fsPath), node!.value));
}
private getFolderTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
return vscode.Uri.file(join(dirname(document.uri.fsPath), node!.value, 'tsconfig.json'));
}
private getRange(document: vscode.TextDocument, node: jsonc.Node) {
const offset = node!.offset;
const start = document.positionAt(offset + 1);
const end = document.positionAt(offset + (node!.length - 1));
return new vscode.Range(start, end);
}
}
export function register() {
const patterns: vscode.GlobPattern[] = [
'**/[jt]sconfig.json',
'**/[jt]sconfig.*.json',
];
const languages = ['json', 'jsonc'];
const selector: vscode.DocumentSelector = flatten(
languages.map(language =>
patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))));
return vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider());
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import DefinitionProviderBase from './definitionProviderBase';
export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements vscode.TypeDefinitionProvider {
public provideTypeDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
return this.getSymbolLocations('typeDefinition', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerTypeDefinitionProvider(selector.syntax,
new TypeScriptTypeDefinitionProvider(client));
});
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* 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 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { Delayer } from '../utils/async';
import { nulToken } from '../utils/cancellation';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { Disposable } from '../utils/dispose';
import * as fileSchemes from '../utils/fileSchemes';
import { doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
const updateImportsOnFileMoveName = 'updateImportsOnFileMove.enabled';
async function isDirectory(resource: vscode.Uri): Promise<boolean> {
try {
return (await vscode.workspace.fs.stat(resource)).type === vscode.FileType.Directory;
} catch {
return false;
}
}
const enum UpdateImportsOnFileMoveSetting {
Prompt = 'prompt',
Always = 'always',
Never = 'never',
}
interface RenameAction {
readonly oldUri: vscode.Uri;
readonly newUri: vscode.Uri;
readonly newFilePath: string;
readonly oldFilePath: string;
readonly jsTsFileThatIsBeingMoved: vscode.Uri;
}
class UpdateImportsOnFileRenameHandler extends Disposable {
public static readonly minVersion = API.v300;
private readonly _delayer = new Delayer(50);
private readonly _pendingRenames = new Set<RenameAction>();
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly _handles: (uri: vscode.Uri) => Promise<boolean>,
) {
super();
this._register(vscode.workspace.onDidRenameFiles(async (e) => {
const [{ newUri, oldUri }] = e.files;
const newFilePath = this.client.toPath(newUri);
if (!newFilePath) {
return;
}
const oldFilePath = this.client.toPath(oldUri);
if (!oldFilePath) {
return;
}
const config = this.getConfiguration(newUri);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
if (setting === UpdateImportsOnFileMoveSetting.Never) {
return;
}
// Try to get a js/ts file that is being moved
// For directory moves, this returns a js/ts file under the directory.
const jsTsFileThatIsBeingMoved = await this.getJsTsFileBeingMoved(newUri);
if (!jsTsFileThatIsBeingMoved || !this.client.toPath(jsTsFileThatIsBeingMoved)) {
return;
}
this._pendingRenames.add({ oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved });
this._delayer.trigger(() => {
vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: localize('renameProgress.title', "Checking for update of JS/TS imports")
}, () => this.flushRenames());
});
}));
}
private async flushRenames(): Promise<void> {
const renames = Array.from(this._pendingRenames);
this._pendingRenames.clear();
for (const group of this.groupRenames(renames)) {
const edits = new vscode.WorkspaceEdit();
const resourcesBeingRenamed: vscode.Uri[] = [];
for (const { oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved } of group) {
const document = await vscode.workspace.openTextDocument(jsTsFileThatIsBeingMoved);
// Make sure TS knows about file
this.client.bufferSyncSupport.closeResource(oldUri);
this.client.bufferSyncSupport.openTextDocument(document);
if (await this.withEditsForFileRename(edits, document, oldFilePath, newFilePath)) {
resourcesBeingRenamed.push(newUri);
}
}
if (edits.size) {
if (await this.confirmActionWithUser(resourcesBeingRenamed)) {
await vscode.workspace.applyEdit(edits);
}
}
}
}
private async confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const config = this.getConfiguration(newResources[0]);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
switch (setting) {
case UpdateImportsOnFileMoveSetting.Always:
return true;
case UpdateImportsOnFileMoveSetting.Never:
return false;
case UpdateImportsOnFileMoveSetting.Prompt:
default:
return this.promptUser(newResources);
}
}
private getConfiguration(resource: vscode.Uri) {
return vscode.workspace.getConfiguration(doesResourceLookLikeATypeScriptFile(resource) ? 'typescript' : 'javascript', resource);
}
private async promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const enum Choice {
None = 0,
Accept = 1,
Reject = 2,
Always = 3,
Never = 4,
}
interface Item extends vscode.MessageItem {
readonly choice: Choice;
}
const response = await vscode.window.showInformationMessage<Item>(
newResources.length === 1
? localize('prompt', "Update imports for '{0}'?", path.basename(newResources[0].fsPath))
: this.getConfirmMessage(localize('promptMoreThanOne', "Update imports for the following {0} files?", newResources.length), newResources), {
modal: true,
}, {
title: localize('reject.title', "No"),
choice: Choice.Reject,
isCloseAffordance: true,
}, {
title: localize('accept.title', "Yes"),
choice: Choice.Accept,
}, {
title: localize('always.title', "Always automatically update imports"),
choice: Choice.Always,
}, {
title: localize('never.title', "Never automatically update imports"),
choice: Choice.Never,
});
if (!response) {
return false;
}
switch (response.choice) {
case Choice.Accept:
{
return true;
}
case Choice.Reject:
{
return false;
}
case Choice.Always:
{
const config = this.getConfiguration(newResources[0]);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Always,
vscode.ConfigurationTarget.Global);
return true;
}
case Choice.Never:
{
const config = this.getConfiguration(newResources[0]);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Never,
vscode.ConfigurationTarget.Global);
return false;
}
}
return false;
}
private async getJsTsFileBeingMoved(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
if (resource.scheme !== fileSchemes.file) {
return undefined;
}
if (await isDirectory(resource)) {
const files = await vscode.workspace.findFiles({
base: resource.fsPath,
pattern: '**/*.{ts,tsx,js,jsx}',
}, '**/node_modules/**', 1);
return files[0];
}
return (await this._handles(resource)) ? resource : undefined;
}
private async withEditsForFileRename(
edits: vscode.WorkspaceEdit,
document: vscode.TextDocument,
oldFilePath: string,
newFilePath: string,
): Promise<boolean> {
const response = await this.client.interruptGetErr(() => {
this.fileConfigurationManager.setGlobalConfigurationFromDocument(document, nulToken);
const args: Proto.GetEditsForFileRenameRequestArgs = {
oldFilePath,
newFilePath,
};
return this.client.execute('getEditsForFileRename', args, nulToken);
});
if (response.type !== 'response' || !response.body.length) {
return false;
}
typeConverters.WorkspaceEdit.withFileCodeEdits(edits, this.client, response.body);
return true;
}
private groupRenames(renames: Iterable<RenameAction>): Iterable<Iterable<RenameAction>> {
const groups = new Map<string, Set<RenameAction>>();
for (const rename of renames) {
// Group renames by type (js/ts) and by workspace.
const key = `${this.client.getWorkspaceRootForResource(rename.jsTsFileThatIsBeingMoved)}@@@${doesResourceLookLikeATypeScriptFile(rename.jsTsFileThatIsBeingMoved)}`;
if (!groups.has(key)) {
groups.set(key, new Set());
}
groups.get(key)!.add(rename);
}
return groups.values();
}
private getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
const MAX_CONFIRM_FILES = 10;
const paths = [start];
paths.push('');
paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
paths.push(localize('moreFile', "...1 additional file not shown"));
} else {
paths.push(localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}
paths.push('');
return paths.join('\n');
}
}
export function register(
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
handles: (uri: vscode.Uri) => Promise<boolean>,
) {
return conditionalRegistration([
requireMinVersion(client, UpdateImportsOnFileRenameHandler.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return new UpdateImportsOnFileRenameHandler(client, fileConfigurationManager, handles);
});
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import * as fileSchemes from '../utils/fileSchemes';
import { doesResourceLookLikeAJavaScriptFile, doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription';
import * as typeConverters from '../utils/typeConverters';
import { parseKindModifier } from '../utils/modifiers';
function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind {
switch (item.kind) {
case PConst.Kind.method: return vscode.SymbolKind.Method;
case PConst.Kind.enum: return vscode.SymbolKind.Enum;
case PConst.Kind.enumMember: return vscode.SymbolKind.EnumMember;
case PConst.Kind.function: return vscode.SymbolKind.Function;
case PConst.Kind.class: return vscode.SymbolKind.Class;
case PConst.Kind.interface: return vscode.SymbolKind.Interface;
case PConst.Kind.type: return vscode.SymbolKind.Class;
case PConst.Kind.memberVariable: return vscode.SymbolKind.Field;
case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Field;
case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Field;
case PConst.Kind.variable: return vscode.SymbolKind.Variable;
default: return vscode.SymbolKind.Variable;
}
}
class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly modeIds: readonly string[],
) { }
public async provideWorkspaceSymbols(
search: string,
token: vscode.CancellationToken
): Promise<vscode.SymbolInformation[]> {
let file: string | undefined;
if (this.searchAllOpenProjects) {
file = undefined;
} else {
const document = this.getDocument();
file = document ? await this.toOpenedFiledPath(document) : undefined;
if (!file && this.client.apiVersion.lt(API.v390)) {
return [];
}
}
const args: Proto.NavtoRequestArgs = {
file,
searchValue: search,
maxResultCount: 256,
};
const response = await this.client.execute('navto', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
return response.body
.filter(item => item.containerName || item.kind !== 'alias')
.map(item => this.toSymbolInformation(item));
}
private get searchAllOpenProjects() {
return this.client.apiVersion.gte(API.v390)
&& vscode.workspace.getConfiguration('typescript').get('workspaceSymbols.scope', 'allOpenProjects') === 'allOpenProjects';
}
private async toOpenedFiledPath(document: vscode.TextDocument) {
if (document.uri.scheme === fileSchemes.git) {
try {
const path = vscode.Uri.file(JSON.parse(document.uri.query)?.path);
if (doesResourceLookLikeATypeScriptFile(path) || doesResourceLookLikeAJavaScriptFile(path)) {
const document = await vscode.workspace.openTextDocument(path);
return this.client.toOpenedFilePath(document);
}
} catch {
// noop
}
}
return this.client.toOpenedFilePath(document);
}
private toSymbolInformation(item: Proto.NavtoItem) {
const label = TypeScriptWorkspaceSymbolProvider.getLabel(item);
const info = new vscode.SymbolInformation(
label,
getSymbolKind(item),
item.containerName || '',
typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item));
const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined;
if (kindModifiers?.has(PConst.KindModifiers.depreacted)) {
info.tags = [vscode.SymbolTag.Deprecated];
}
return info;
}
private static getLabel(item: Proto.NavtoItem) {
const label = item.name;
if (item.kind === 'method' || item.kind === 'function') {
return label + '()';
}
return label;
}
private getDocument(): vscode.TextDocument | undefined {
// typescript wants to have a resource even when asking
// general questions so we check the active editor. If this
// doesn't match we take the first TS document.
const activeDocument = vscode.window.activeTextEditor?.document;
if (activeDocument) {
if (this.modeIds.includes(activeDocument.languageId)) {
return activeDocument;
}
}
const documents = vscode.workspace.textDocuments;
for (const document of documents) {
if (this.modeIds.includes(document.languageId)) {
return document;
}
}
return undefined;
}
}
export function register(
client: ITypeScriptServiceClient,
modeIds: readonly string[],
) {
return vscode.languages.registerWorkspaceSymbolProvider(
new TypeScriptWorkspaceSymbolProvider(client, modeIds));
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { basename } from 'path';
import * as vscode from 'vscode';
import { DiagnosticKind } from './languageFeatures/diagnostics';
import FileConfigurationManager from './languageFeatures/fileConfigurationManager';
import { CachedResponse } from './tsServer/cachedResponse';
import TypeScriptServiceClient from './typescriptServiceClient';
import { CommandManager } from './commands/commandManager';
import { Disposable } from './utils/dispose';
import { DocumentSelector } from './utils/documentSelector';
import * as fileSchemes from './utils/fileSchemes';
import { LanguageDescription } from './utils/languageDescription';
import { TelemetryReporter } from './utils/telemetry';
import TypingsStatus from './utils/typingsStatus';
const validateSetting = 'validate.enable';
const suggestionSetting = 'suggestionActions.enabled';
export default class LanguageProvider extends Disposable {
constructor(
private readonly client: TypeScriptServiceClient,
private readonly description: LanguageDescription,
private readonly commandManager: CommandManager,
private readonly telemetryReporter: TelemetryReporter,
private readonly typingsStatus: TypingsStatus,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {
super();
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables);
this.configurationChanged();
client.onReady(() => this.registerProviders());
}
private get documentSelector(): DocumentSelector {
const semantic: vscode.DocumentFilter[] = [];
const syntax: vscode.DocumentFilter[] = [];
for (const language of this.description.modeIds) {
syntax.push({ language });
for (const scheme of fileSchemes.semanticSupportedSchemes) {
semantic.push({ language, scheme });
}
}
return { semantic, syntax };
}
private async registerProviders(): Promise<void> {
const selector = this.documentSelector;
const cachedResponse = new CachedResponse();
await Promise.all([
import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))),
import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/documentSymbol').then(provider => this._register(provider.register(selector, this.client, cachedResponse))),
import('./languageFeatures/folding').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/formatting').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))),
import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/implementations').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))),
import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description.id, this.client))),
import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))),
import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))),
import('./languageFeatures/fixAll').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager))),
import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))),
import('./languageFeatures/references').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))),
import('./languageFeatures/rename').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))),
import('./languageFeatures/smartSelect').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/signatureHelp').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/tagClosing').then(provider => this._register(provider.register(selector, this.description.id, this.client))),
import('./languageFeatures/typeDefinitions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/semanticTokens').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/callHierarchy').then(provider => this._register(provider.register(selector, this.client))),
]);
}
private configurationChanged(): void {
const config = vscode.workspace.getConfiguration(this.id, null);
this.updateValidate(config.get(validateSetting, true));
this.updateSuggestionDiagnostics(config.get(suggestionSetting, true));
}
public handles(resource: vscode.Uri, doc: vscode.TextDocument): boolean {
if (doc && this.description.modeIds.indexOf(doc.languageId) >= 0) {
return true;
}
const base = basename(resource.fsPath);
return !!base && (!!this.description.configFilePattern && this.description.configFilePattern.test(base));
}
private get id(): string {
return this.description.id;
}
public get diagnosticSource(): string {
return this.description.diagnosticSource;
}
private updateValidate(value: boolean) {
this.client.diagnosticsManager.setValidate(this._diagnosticLanguage, value);
}
private updateSuggestionDiagnostics(value: boolean) {
this.client.diagnosticsManager.setEnableSuggestions(this._diagnosticLanguage, value);
}
public reInitialize(): void {
this.client.diagnosticsManager.reInitialize();
}
public triggerAllDiagnostics(): void {
this.client.bufferSyncSupport.requestAllDiagnostics();
}
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[]): void {
const config = vscode.workspace.getConfiguration(this.id, file);
const reportUnnecessary = config.get<boolean>('showUnused', true);
const reportDeprecated = config.get<boolean>('showDeprecated', true);
this.client.diagnosticsManager.updateDiagnostics(file, this._diagnosticLanguage, diagnosticsKind, diagnostics.filter(diag => {
// Don't both reporting diagnostics we know will not be rendered
if (!reportUnnecessary) {
if (diag.reportUnnecessary && diag.severity === vscode.DiagnosticSeverity.Hint) {
return false;
}
}
if (!reportDeprecated) {
if (diag.reportDeprecated && diag.severity === vscode.DiagnosticSeverity.Hint) {
return false;
}
}
return true;
}));
}
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {
this.client.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics);
}
private get _diagnosticLanguage() {
return this.description.diagnosticLanguage;
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { CommandManager } from './commands/commandManager';
import { OngoingRequestCancellerFactory } from './tsServer/cancellation';
import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { TsServerProcessFactory } from './tsServer/server';
import { ITypeScriptVersionProvider } from './tsServer/versionProvider';
import TypeScriptServiceClientHost from './typeScriptServiceClientHost';
import { flatten } from './utils/arrays';
import * as fileSchemes from './utils/fileSchemes';
import { standardLanguageDescriptions } from './utils/languageDescription';
import { lazy, Lazy } from './utils/lazy';
import ManagedFileContextManager from './utils/managedFileContext';
import { PluginManager } from './utils/plugins';
export function createLazyClientHost(
context: vscode.ExtensionContext,
onCaseInsenitiveFileSystem: boolean,
services: {
pluginManager: PluginManager,
commandManager: CommandManager,
logDirectoryProvider: ILogDirectoryProvider,
cancellerFactory: OngoingRequestCancellerFactory,
versionProvider: ITypeScriptVersionProvider,
processFactory: TsServerProcessFactory,
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
): Lazy<TypeScriptServiceClientHost> {
return lazy(() => {
const clientHost = new TypeScriptServiceClientHost(
standardLanguageDescriptions,
context.workspaceState,
onCaseInsenitiveFileSystem,
services,
onCompletionAccepted);
context.subscriptions.push(clientHost);
return clientHost;
});
}
export function lazilyActivateClient(
lazyClientHost: Lazy<TypeScriptServiceClientHost>,
pluginManager: PluginManager,
): vscode.Disposable {
const disposables: vscode.Disposable[] = [];
const supportedLanguage = flatten([
...standardLanguageDescriptions.map(x => x.modeIds),
...pluginManager.plugins.map(x => x.languages)
]);
let hasActivated = false;
const maybeActivate = (textDocument: vscode.TextDocument): boolean => {
if (!hasActivated && isSupportedDocument(supportedLanguage, textDocument)) {
hasActivated = true;
// Force activation
void lazyClientHost.value;
disposables.push(new ManagedFileContextManager(resource => {
return lazyClientHost.value.serviceClient.toPath(resource);
}));
return true;
}
return false;
};
const didActivate = vscode.workspace.textDocuments.some(maybeActivate);
if (!didActivate) {
const openListener = vscode.workspace.onDidOpenTextDocument(doc => {
if (maybeActivate(doc)) {
openListener.dispose();
}
}, undefined, disposables);
}
return vscode.Disposable.from(...disposables);
}
function isSupportedDocument(
supportedLanguage: readonly string[],
document: vscode.TextDocument
): boolean {
return supportedLanguage.indexOf(document.languageId) >= 0
&& !fileSchemes.disabledSchemes.has(document.uri.scheme);
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Kind {
public static readonly alias = 'alias';
public static readonly callSignature = 'call';
public static readonly class = 'class';
public static readonly const = 'const';
public static readonly constructorImplementation = 'constructor';
public static readonly constructSignature = 'construct';
public static readonly directory = 'directory';
public static readonly enum = 'enum';
public static readonly enumMember = 'enum member';
public static readonly externalModuleName = 'external module name';
public static readonly function = 'function';
public static readonly indexSignature = 'index';
public static readonly interface = 'interface';
public static readonly keyword = 'keyword';
public static readonly let = 'let';
public static readonly localFunction = 'local function';
public static readonly localVariable = 'local var';
public static readonly method = 'method';
public static readonly memberGetAccessor = 'getter';
public static readonly memberSetAccessor = 'setter';
public static readonly memberVariable = 'property';
public static readonly module = 'module';
public static readonly primitiveType = 'primitive type';
public static readonly script = 'script';
public static readonly type = 'type';
public static readonly variable = 'var';
public static readonly warning = 'warning';
public static readonly string = 'string';
public static readonly parameter = 'parameter';
public static readonly typeParameter = 'type parameter';
}
export class DiagnosticCategory {
public static readonly error = 'error';
public static readonly warning = 'warning';
public static readonly suggestion = 'suggestion';
}
export class KindModifiers {
public static readonly optional = 'optional';
public static readonly depreacted = 'deprecated';
public static readonly color = 'color';
public static readonly dtsFile = '.d.ts';
public static readonly tsFile = '.ts';
public static readonly tsxFile = '.tsx';
public static readonly jsFile = '.js';
public static readonly jsxFile = '.jsx';
public static readonly jsonFile = '.json';
public static readonly fileExtensionKindModifiers = [
KindModifiers.dtsFile,
KindModifiers.tsFile,
KindModifiers.tsxFile,
KindModifiers.jsFile,
KindModifiers.jsxFile,
KindModifiers.jsonFile,
];
}
export class DisplayPartKind {
public static readonly functionName = 'functionName';
public static readonly methodName = 'methodName';
public static readonly parameterName = 'parameterName';
public static readonly propertyName = 'propertyName';
public static readonly punctuation = 'punctuation';
public static readonly text = 'text';
}
export enum EventName {
syntaxDiag = 'syntaxDiag',
semanticDiag = 'semanticDiag',
suggestionDiag = 'suggestionDiag',
configFileDiag = 'configFileDiag',
telemetry = 'telemetry',
projectLanguageServiceState = 'projectLanguageServiceState',
projectsUpdatedInBackground = 'projectsUpdatedInBackground',
beginInstallTypes = 'beginInstallTypes',
endInstallTypes = 'endInstallTypes',
typesInstallerInitializationFailed = 'typesInstallerInitializationFailed',
surveyReady = 'surveyReady',
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
}

View File

@@ -0,0 +1,12 @@
import * as Proto from 'typescript/lib/protocol';
export = Proto;
declare enum ServerType {
Syntax = 'syntax',
Semantic = 'semantic',
}
declare module 'typescript/lib/protocol' {
interface Response {
readonly _serverType?: ServerType;
}
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jsonc from 'jsonc-parser';
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { wait } from '../test/testUtils';
import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import { coalesce, flatten } from '../utils/arrays';
import { Disposable } from '../utils/dispose';
import { exists } from '../utils/fs';
import { isTsConfigFileName } from '../utils/languageDescription';
import { Lazy } from '../utils/lazy';
import { isImplicitProjectConfigFile } from '../utils/tsconfig';
import { TSConfig, TsConfigProvider } from './tsconfigProvider';
const localize = nls.loadMessageBundle();
enum AutoDetect {
on = 'on',
off = 'off',
build = 'build',
watch = 'watch'
}
interface TypeScriptTaskDefinition extends vscode.TaskDefinition {
tsconfig: string;
option?: string;
}
/**
* Provides tasks for building `tsconfig.json` files in a project.
*/
class TscTaskProvider extends Disposable implements vscode.TaskProvider {
private readonly projectInfoRequestTimeout = 2000;
private readonly findConfigFilesTimeout = 5000;
private autoDetect = AutoDetect.on;
private readonly tsconfigProvider: TsConfigProvider;
public constructor(
private readonly client: Lazy<ITypeScriptServiceClient>
) {
super();
this.tsconfigProvider = new TsConfigProvider();
this._register(vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this));
this.onConfigurationChanged();
}
public async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
const folders = vscode.workspace.workspaceFolders;
if ((this.autoDetect === AutoDetect.off) || !folders || !folders.length) {
return [];
}
const configPaths: Set<string> = new Set();
const tasks: vscode.Task[] = [];
for (const project of await this.getAllTsConfigs(token)) {
if (!configPaths.has(project.fsPath)) {
configPaths.add(project.fsPath);
tasks.push(...(await this.getTasksForProject(project)));
}
}
return tasks;
}
public async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
const definition = <TypeScriptTaskDefinition>task.definition;
if (/\\tsconfig.*\.json/.test(definition.tsconfig)) {
// Warn that the task has the wrong slash type
vscode.window.showWarningMessage(localize('badTsConfig', "TypeScript Task in tasks.json contains \"\\\\\". TypeScript tasks tsconfig must use \"/\""));
return undefined;
}
const tsconfigPath = definition.tsconfig;
if (!tsconfigPath) {
return undefined;
}
if (task.scope === undefined || task.scope === vscode.TaskScope.Global || task.scope === vscode.TaskScope.Workspace) {
// scope is required to be a WorkspaceFolder for resolveTask
return undefined;
}
const tsconfigUri = task.scope.uri.with({ path: task.scope.uri.path + '/' + tsconfigPath });
const tsconfig: TSConfig = {
uri: tsconfigUri,
fsPath: tsconfigUri.fsPath,
posixPath: tsconfigUri.path,
workspaceFolder: task.scope
};
return this.getTasksForProjectAndDefinition(tsconfig, definition);
}
private async getAllTsConfigs(token: vscode.CancellationToken): Promise<TSConfig[]> {
const configs = flatten(await Promise.all([
this.getTsConfigForActiveFile(token),
this.getTsConfigsInWorkspace(token),
]));
return Promise.all(
configs.map(async config => await exists(config.uri) ? config : undefined),
).then(coalesce);
}
private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise<TSConfig[]> {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (isTsConfigFileName(editor.document.fileName)) {
const uri = editor.document.uri;
return [{
uri,
fsPath: uri.fsPath,
posixPath: uri.path,
workspaceFolder: vscode.workspace.getWorkspaceFolder(uri)
}];
}
}
const file = this.getActiveTypeScriptFile();
if (!file) {
return [];
}
const response = await Promise.race([
this.client.value.execute(
'projectInfo',
{ file, needFileNameList: false },
token),
new Promise<typeof ServerResponse.NoContent>(resolve => setTimeout(() => resolve(ServerResponse.NoContent), this.projectInfoRequestTimeout))
]);
if (response.type !== 'response' || !response.body) {
return [];
}
const { configFileName } = response.body;
if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
const normalizedConfigPath = path.normalize(configFileName);
const uri = vscode.Uri.file(normalizedConfigPath);
const folder = vscode.workspace.getWorkspaceFolder(uri);
return [{
uri,
fsPath: normalizedConfigPath,
posixPath: uri.path,
workspaceFolder: folder
}];
}
return [];
}
private async getTsConfigsInWorkspace(token: vscode.CancellationToken): Promise<TSConfig[]> {
const getConfigsTimeout = new vscode.CancellationTokenSource();
token.onCancellationRequested(() => getConfigsTimeout.cancel());
return Promise.race([
this.tsconfigProvider.getConfigsForWorkspace(getConfigsTimeout.token).then(x => Array.from(x)),
wait(this.findConfigFilesTimeout).then(() => {
getConfigsTimeout.cancel();
return [];
}),
]);
}
private static async getCommand(project: TSConfig): Promise<string> {
if (project.workspaceFolder) {
const localTsc = await TscTaskProvider.getLocalTscAtPath(path.dirname(project.fsPath));
if (localTsc) {
return localTsc;
}
const workspaceTsc = await TscTaskProvider.getLocalTscAtPath(project.workspaceFolder.uri.fsPath);
if (workspaceTsc) {
return workspaceTsc;
}
}
// Use global tsc version
return 'tsc';
}
private static async getLocalTscAtPath(folderPath: string): Promise<string | undefined> {
const platform = process.platform;
const bin = path.join(folderPath, 'node_modules', '.bin');
if (platform === 'win32' && await exists(vscode.Uri.file(path.join(bin, 'tsc.cmd')))) {
return path.join(bin, 'tsc.cmd');
} else if ((platform === 'linux' || platform === 'darwin') && await exists(vscode.Uri.file(path.join(bin, 'tsc')))) {
return path.join(bin, 'tsc');
}
return undefined;
}
private getActiveTypeScriptFile(): string | undefined {
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
return this.client.value.toPath(document.uri);
}
}
return undefined;
}
private getBuildTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], buildTaskidentifier: TypeScriptTaskDefinition): vscode.Task {
const buildTask = new vscode.Task(
buildTaskidentifier,
workspaceFolder || vscode.TaskScope.Workspace,
localize('buildTscLabel', 'build - {0}', label),
'tsc',
new vscode.ShellExecution(command, args),
'$tsc');
buildTask.group = vscode.TaskGroup.Build;
buildTask.isBackground = false;
return buildTask;
}
private getWatchTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], watchTaskidentifier: TypeScriptTaskDefinition) {
const watchTask = new vscode.Task(
watchTaskidentifier,
workspaceFolder || vscode.TaskScope.Workspace,
localize('buildAndWatchTscLabel', 'watch - {0}', label),
'tsc',
new vscode.ShellExecution(command, [...args, '--watch']),
'$tsc-watch');
watchTask.group = vscode.TaskGroup.Build;
watchTask.isBackground = true;
return watchTask;
}
private async getTasksForProject(project: TSConfig): Promise<vscode.Task[]> {
const command = await TscTaskProvider.getCommand(project);
const args = await this.getBuildShellArgs(project);
const label = this.getLabelForTasks(project);
const tasks: vscode.Task[] = [];
if (this.autoDetect === AutoDetect.build || this.autoDetect === AutoDetect.on) {
tasks.push(this.getBuildTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label }));
}
if (this.autoDetect === AutoDetect.watch || this.autoDetect === AutoDetect.on) {
tasks.push(this.getWatchTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label, option: 'watch' }));
}
return tasks;
}
private async getTasksForProjectAndDefinition(project: TSConfig, definition: TypeScriptTaskDefinition): Promise<vscode.Task | undefined> {
const command = await TscTaskProvider.getCommand(project);
const args = await this.getBuildShellArgs(project);
const label = this.getLabelForTasks(project);
let task: vscode.Task | undefined;
if (definition.option === undefined) {
task = this.getBuildTask(project.workspaceFolder, label, command, args, definition);
} else if (definition.option === 'watch') {
task = this.getWatchTask(project.workspaceFolder, label, command, args, definition);
}
return task;
}
private async getBuildShellArgs(project: TSConfig): Promise<Array<string>> {
const defaultArgs = ['-p', project.fsPath];
try {
const bytes = await vscode.workspace.fs.readFile(project.uri);
const text = Buffer.from(bytes).toString('utf-8');
const tsconfig = jsonc.parse(text);
if (tsconfig?.references) {
return ['-b', project.fsPath];
}
} catch {
// noops
}
return defaultArgs;
}
private getLabelForTasks(project: TSConfig): string {
if (project.workspaceFolder) {
const workspaceNormalizedUri = vscode.Uri.file(path.normalize(project.workspaceFolder.uri.fsPath)); // Make sure the drive letter is lowercase
return path.posix.relative(workspaceNormalizedUri.path, project.posixPath);
}
return project.posixPath;
}
private onConfigurationChanged(): void {
const type = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
this.autoDetect = typeof type === 'undefined' ? AutoDetect.on : type;
}
}
export function register(
lazyClient: Lazy<ITypeScriptServiceClient>,
) {
return vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClient));
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface TSConfig {
readonly uri: vscode.Uri;
readonly fsPath: string;
readonly posixPath: string;
readonly workspaceFolder?: vscode.WorkspaceFolder;
}
export class TsConfigProvider {
public async getConfigsForWorkspace(token: vscode.CancellationToken): Promise<Iterable<TSConfig>> {
if (!vscode.workspace.workspaceFolders) {
return [];
}
const configs = new Map<string, TSConfig>();
for (const config of await this.findConfigFiles(token)) {
const root = vscode.workspace.getWorkspaceFolder(config);
if (root) {
configs.set(config.fsPath, {
uri: config,
fsPath: config.fsPath,
posixPath: config.path,
workspaceFolder: root
});
}
}
return configs.values();
}
private async findConfigFiles(token: vscode.CancellationToken): Promise<vscode.Uri[]> {
return await vscode.workspace.findFiles('**/tsconfig*.json', '**/{node_modules,.*}/**', undefined, token);
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { CachedResponse } from '../tsServer/cachedResponse';
import { ServerResponse } from '../typescriptService';
suite('CachedResponse', () => {
test('should cache simple response for same document', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
assertResult(await response.execute(doc, respondWith('test-0')), 'test-0');
assertResult(await response.execute(doc, respondWith('test-1')), 'test-0');
});
test('should invalidate cache for new document', async () => {
const doc1 = await createTextDocument();
const doc2 = await createTextDocument();
const response = new CachedResponse();
assertResult(await response.execute(doc1, respondWith('test-0')), 'test-0');
assertResult(await response.execute(doc1, respondWith('test-1')), 'test-0');
assertResult(await response.execute(doc2, respondWith('test-2')), 'test-2');
assertResult(await response.execute(doc2, respondWith('test-3')), 'test-2');
assertResult(await response.execute(doc1, respondWith('test-4')), 'test-4');
assertResult(await response.execute(doc1, respondWith('test-5')), 'test-4');
});
test('should not cache cancelled responses', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc, () => cancelledResponder.promise);
const result2 = response.execute(doc, respondWith('test-0'));
const result3 = response.execute(doc, respondWith('test-1'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
assert.strictEqual((await result1).type, 'cancelled');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
});
test('should not care if subsequent requests are cancelled if first request is resolved ok', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc, respondWith('test-0'));
const result2 = response.execute(doc, () => cancelledResponder.promise);
const result3 = response.execute(doc, respondWith('test-1'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
assertResult(await result1, 'test-0');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
});
test('should not cache cancelled responses with document changes', async () => {
const doc1 = await createTextDocument();
const doc2 = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const cancelledResponder2 = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc1, () => cancelledResponder.promise);
const result2 = response.execute(doc1, respondWith('test-0'));
const result3 = response.execute(doc1, respondWith('test-1'));
const result4 = response.execute(doc2, () => cancelledResponder2.promise);
const result5 = response.execute(doc2, respondWith('test-2'));
const result6 = response.execute(doc1, respondWith('test-3'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
cancelledResponder2.resolve(new ServerResponse.Cancelled('cancelled'));
assert.strictEqual((await result1).type, 'cancelled');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
assert.strictEqual((await result4).type, 'cancelled');
assertResult(await result5, 'test-2');
assertResult(await result6, 'test-3');
});
});
function respondWith(command: string) {
return async () => createResponse(command);
}
function createTextDocument() {
return vscode.workspace.openTextDocument({ language: 'javascript', content: '' });
}
function assertResult(result: ServerResponse.Response<Proto.Response>, command: string) {
if (result.type === 'response') {
assert.strictEqual(result.command, command);
} else {
assert.fail('Response failed');
}
}
function createResponse(command: string): Proto.Response {
return {
type: 'response',
body: {},
command: command,
request_seq: 1,
success: true,
seq: 1
};
}
function createEventualResponder<T>(): { promise: Promise<T>, resolve: (x: T) => void } {
let resolve: (value: T) => void;
const promise = new Promise<T>(r => { resolve = r; });
return { promise, resolve: resolve! };
}

View File

@@ -0,0 +1,652 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { acceptFirstSuggestion, typeCommitCharacter } from './suggestTestHelpers';
import { assertEditorContents, Config, createTestEditor, joinLines, updateConfig, VsCodeConfiguration, wait, enumerateConfig } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const insertModes = Object.freeze(['insert', 'replace']);
suite('TypeScript Completions', () => {
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.autoClosingBrackets]: 'always',
[Config.typescriptCompleteFunctionCalls]: false,
[Config.insertMode]: 'insert',
[Config.snippetSuggestions]: 'none',
[Config.suggestSelection]: 'first',
[Config.javascriptQuoteStyle]: 'double',
[Config.typescriptQuoteStyle]: 'double',
});
const _disposables: vscode.Disposable[] = [];
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(500);
// Save off config and apply defaults
oldConfig = await updateConfig(testDocumentUri, configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(testDocumentUri, oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Basic var completion', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`const abcdef = 123;`,
`ab$0;`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abcdef = 123;`,
`abcdef;`
),
`config: ${config}`
);
});
});
test('Should treat period as commit character for var completions', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`const abcdef = 123;`,
`ab$0;`
);
await typeCommitCharacter(testDocumentUri, '.', _disposables);
assertEditorContents(editor,
joinLines(
`const abcdef = 123;`,
`abcdef.;`
),
`config: ${config}`);
});
});
test('Should treat paren as commit character for function completions', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`function abcdef() {};`,
`ab$0;`
);
await typeCommitCharacter(testDocumentUri, '(', _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef() {};`,
`abcdef();`
), `config: ${config}`);
});
});
test('Should insert backets when completing dot properties with spaces in name', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
'const x = { "hello world": 1 };',
'x.$0'
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "hello world": 1 };',
'x["hello world"]'
), `config: ${config}`);
});
});
test('Should allow commit characters for backet completions', async () => {
for (const { char, insert } of [
{ char: '.', insert: '.' },
{ char: '(', insert: '()' },
]) {
const editor = await createTestEditor(testDocumentUri,
'const x = { "hello world2": 1 };',
'x.$0'
);
await typeCommitCharacter(testDocumentUri, char, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "hello world2": 1 };',
`x["hello world2"]${insert}`
));
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
}
});
test('Should not prioritize bracket accessor completions. #63100', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
// 'a' should be first entry in completion list
const editor = await createTestEditor(testDocumentUri,
'const x = { "z-z": 1, a: 1 };',
'x.$0'
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "z-z": 1, a: 1 };',
'x.a'
),
`config: ${config}`);
});
});
test('Accepting a string completion should replace the entire string. #53962', async () => {
const editor = await createTestEditor(testDocumentUri,
'interface TFunction {',
` (_: 'abc.abc2', __ ?: {}): string;`,
` (_: 'abc.abc', __?: {}): string;`,
`}`,
'const f: TFunction = (() => { }) as any;',
`f('abc.abc$0')`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'interface TFunction {',
` (_: 'abc.abc2', __ ?: {}): string;`,
` (_: 'abc.abc', __?: {}): string;`,
`}`,
'const f: TFunction = (() => { }) as any;',
`f('abc.abc')`
));
});
test('completeFunctionCalls should complete function parameters when at end of word', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
// Complete with-in word
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcdef$0`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(x, y, z)`
));
});
test.skip('completeFunctionCalls should complete function parameters when within word', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcd$0ef`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(x, y, z)`
));
});
test('completeFunctionCalls should not complete function parameters at end of word if we are already in something that looks like a function call, #18131', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcdef$0(1, 2, 3)`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(1, 2, 3)`
));
});
test.skip('completeFunctionCalls should not complete function parameters within word if we are already in something that looks like a function call, #18131', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcd$0ef(1, 2, 3)`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(1, 2, 3)`
));
});
test('should not de-prioritize `this.member` suggestion, #74164', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` private detail = '';`,
` foo() {`,
` det$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` private detail = '';`,
` foo() {`,
` this.detail`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Member completions for string property name should insert `this.` and use brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this["xyz 123"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Member completions for string property name already using `this.` should add brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this.xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this["xyz 123"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Accepting a completion in word using `insert` mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`const abc = 123;`,
`ab$0c`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = 123;`,
`abcc`
));
});
test('Accepting a completion in word using `replace` mode should replace', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`const abc = 123;`,
`ab$0c`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = 123;`,
`abc`
));
});
test('Accepting a member completion in word using `insert` mode add `this.` and insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`class Foo {`,
` abc = 1;`,
` foo() {`,
` ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class Foo {`,
` abc = 1;`,
` foo() {`,
` this.abcc`,
` }`,
`}`,
));
});
test('Accepting a member completion in word using `replace` mode should add `this.` and replace', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class Foo {`,
` abc = 1;`,
` foo() {`,
` ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class Foo {`,
` abc = 1;`,
` foo() {`,
` this.abc`,
` }`,
`}`,
));
});
test('Accepting string completion inside string using `insert` mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`const abc = { 'xy z': 123 }`,
`abc["x$0y w"]`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = { 'xy z': 123 }`,
`abc["xy zy w"]`
));
});
// Waiting on https://github.com/microsoft/TypeScript/issues/35602
test.skip('Accepting string completion inside string using insert mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`const abc = { 'xy z': 123 }`,
`abc["x$0y w"]`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = { 'xy z': 123 }`,
`abc["xy w"]`
));
});
test('Private field completions on `this.#` should work', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions on `#` should insert `this.`', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` #$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions should not require strict prefix match (#89556)', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions without `this.` should not require strict prefix match (#89556)', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Accepting a completion for async property in `insert` mode should insert and add await', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` this.xyz.ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz).abcc`,
` }`,
`}`,
));
});
test('Accepting a completion for async property in `replace` mode should replace and add await', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` this.xyz.ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz).abc`,
` }`,
`}`,
));
});
test.skip('Accepting a completion for async string property should add await plus brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'ab c': 1 });`,
` async foo() {`,
` this.xyz.ab$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz)["ab c"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Replace should work after this. (#91105)', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` abc = 1`,
` foo() {`,
` this.$0abc`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` abc = 1`,
` foo() {`,
` this.abc`,
` }`,
`}`,
));
});
});

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, wait, joinLines } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const emptyRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
suite('TypeScript Fix All', () => {
const _disposables: vscode.Disposable[] = [];
teardown(async () => {
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Fix all should remove unreachable code', async () => {
const editor = await createTestEditor(testDocumentUri,
`function foo() {`,
` return 1;`,
` return 2;`,
`};`,
`function boo() {`,
` return 3;`,
` return 4;`,
`};`,
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.SourceFixAll
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`function foo() {`,
` return 1;`,
`};`,
`function boo() {`,
` return 3;`,
`};`,
));
});
test('Fix all should implement interfaces', async () => {
const editor = await createTestEditor(testDocumentUri,
`interface I {`,
` x: number;`,
`}`,
`class A implements I {}`,
`class B implements I {}`,
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.SourceFixAll
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`interface I {`,
` x: number;`,
`}`,
`class A implements I {`,
` x: number;`,
`}`,
`class B implements I {`,
` x: number;`,
`}`,
));
});
test('Remove unused should handle nested ununused', async () => {
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`function unused() {`,
` const a = 1;`,
`}`,
`function used() {`,
` const a = 1;`,
`}`,
`used();`
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.Source.append('removeUnused')
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`function used() {`,
`}`,
`used();`
));
});
test('Remove unused should remove unused interfaces', async () => {
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`interface Foo {}`
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.Source.append('removeUnused')
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
``
));
});
});

View File

@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// todo@matt
/* eslint code-no-unexternalized-strings: 0 */
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { snippetForFunctionCall } from '../utils/snippetForFunctionCall';
suite('typescript function call snippets', () => {
test('Should use label as function name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', },
[]
).snippet.value,
'abc()$0');
});
test('Should use insertText string to override function name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: 'def' },
[]
).snippet.value,
'def()$0');
});
test('Should return insertText as-is if it is already a snippet', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: new vscode.SnippetString('bla()$0') },
[]
).snippet.value,
'bla()$0');
});
test('Should return insertText as-is if it is already a snippet', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: new vscode.SnippetString('bla()$0') },
[]
).snippet.value,
'bla()$0');
});
test('Should extract parameter from display parts', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'activate' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "activate", "kind": "text" }, { "text": "(", "kind": "punctuation" }, { "text": "context", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "vscode", "kind": "aliasName" }, { "text": ".", "kind": "punctuation" }, { "text": "ExtensionContext", "kind": "interfaceName" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'activate(${1:context})$0');
});
test('Should extract all parameters from display parts', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "c", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "boolean", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}, ${2:b}, ${3:c})$0');
});
test('Should create empty placeholder at rest parameter', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "...", "kind": "punctuation" }, { "text": "rest", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": "[", "kind": "punctuation" }, { "text": "]", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}$2)$0');
});
test('Should skip over inline function and object types when extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": "x", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}, ${2:b})$0');
});
test('Should skip over return type while extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }]
).snippet.value,
'foo(${1:a})$0');
});
test('Should skip over prefix type while extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "(", "kind": "punctuation" }, { "text": "method", "kind": "text" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "Array", "kind": "localName" }, { "text": "<", "kind": "punctuation" }, { "text": "{", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "dispose", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "}", "kind": "punctuation" }, { "text": ">", "kind": "punctuation" }, { "text": ".", "kind": "punctuation" }, { "text": "foo", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": "searchElement", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "dispose", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "fromIndex", "kind": "parameterName" }, { "text": "?", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }]
).snippet.value,
'foo(${1:searchElement}$2)$0');
});
test('Should complete property names', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'methoda' },
[{ "text": "(", "kind": "punctuation" }, { "text": "method", "kind": "text" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "methoda", "kind": "propertyName" }, { "text": "(", "kind": "punctuation" }, { "text": "x", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'methoda(${1:x})$0');
});
test('Should escape snippet syntax in method name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: '$abc', },
[]
).snippet.value,
'\\$abc()$0');
});
test('Should not include object key signature in completion, #66297', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foobar', },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "[", "kind": "punctuation" }, { "text": "key", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": "]", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foobar(${1:param})$0');
});
test('Should skip over this parameter', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foobar', },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "this", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foobar(${1:param})$0');
});
});

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
const testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle)
timeout: 60000,
});
export = testRunner;

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { acceptFirstSuggestion } from './suggestTestHelpers';
import { assertEditorContents, Config, createTestEditor, CURSOR, enumerateConfig, insertModesValues, joinLines, updateConfig, VsCodeConfiguration, wait } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
suite('JSDoc Completions', () => {
const _disposables: vscode.Disposable[] = [];
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.snippetSuggestions]: 'inline',
});
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(100);
// Save off config and apply defaults
oldConfig = await updateConfig(testDocumentUri, configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(testDocumentUri, oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Should complete jsdoc inside single line comment', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModesValues, async config => {
const editor = await createTestEditor(testDocumentUri,
`/**$0 */`,
`function abcdef(x, y) { }`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`/**`,
` * `,
` * @param x ${CURSOR}`,
` * @param y `,
` */`,
`function abcdef(x, y) { }`,
),
`Config: ${config}`);
});
});
});

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { templateToSnippet } from '../languageFeatures/jsDocCompletions';
const joinLines = (...args: string[]) => args.join('\n');
suite('typescript.jsDocSnippet', () => {
test('Should do nothing for single line input', async () => {
const input = `/** */`;
assert.strictEqual(templateToSnippet(input).value, input);
});
test('Should put cursor inside multiline line input', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * ',
' */'
)).value,
joinLines(
'/**',
' * $0',
' */'
));
});
test('Should add placeholders after each parameter', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * @param a',
' * @param b',
' */'
)).value,
joinLines(
'/**',
' * @param a ${1}',
' * @param b ${2}',
' */'
));
});
test('Should add placeholders for types', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * @param {*} a',
' * @param {*} b',
' */'
)).value,
joinLines(
'/**',
' * @param {${1:*}} a ${2}',
' * @param {${3:*}} b ${4}',
' */'
));
});
test('Should properly escape dollars in parameter names', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * ',
' * @param $arg',
' */'
)).value,
joinLines(
'/**',
' * $0',
' * @param \\$arg ${1}',
' */'
));
});
});

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { CURSOR, withRandomFileEditor, wait, joinLines } from './testUtils';
const onDocumentChange = (doc: vscode.TextDocument): Promise<vscode.TextDocument> => {
return new Promise<vscode.TextDocument>(resolve => {
const sub = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document !== doc) {
return;
}
sub.dispose();
resolve(e.document);
});
});
};
const type = async (document: vscode.TextDocument, text: string): Promise<vscode.TextDocument> => {
const onChange = onDocumentChange(document);
await vscode.commands.executeCommand('type', { text });
await onChange;
return document;
};
suite('OnEnter', () => {
test('should indent after if block with braces', () => {
return withRandomFileEditor(`if (true) {${CURSOR}`, 'js', async (_editor, document) => {
await type(document, '\nx');
assert.strictEqual(
document.getText(),
joinLines(
`if (true) {`,
` x`));
});
});
test('should indent within empty object literal', () => {
return withRandomFileEditor(`({${CURSOR}})`, 'js', async (_editor, document) => {
await type(document, '\nx');
await wait(500);
assert.strictEqual(
document.getText(),
joinLines(`({`,
` x`,
`})`));
});
});
});

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { tagsMarkdownPreview, markdownDocumentation } from '../utils/previewer';
suite('typescript.previewer', () => {
test('Should ignore hyphens after a param tag', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'a - b'
}
]),
'*@param* `a` — b');
});
test('Should parse url jsdoc @link', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo} y {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} z', []).value,
'x [http://www.example.com/foo](http://www.example.com/foo) y [https://api.jquery.com/bind/#bind-eventType-eventData-handler](https://api.jquery.com/bind/#bind-eventType-eventData-handler) z');
});
test('Should parse url jsdoc @link with text', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z', []).value,
'x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
});
test('Should treat @linkcode jsdocs links as monospace', async () => {
assert.strictEqual(
markdownDocumentation('x {@linkcode http://www.example.com/foo} y {@linkplain http://www.example.com/bar} z', []).value,
'x [`http://www.example.com/foo`](http://www.example.com/foo) y [http://www.example.com/bar](http://www.example.com/bar) z');
});
test('Should parse url jsdoc @link in param tag', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'a x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z'
}
]),
'*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
});
test('Should ignore unclosed jsdocs @link', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo y {@link http://www.example.com/bar bar} z', []).value,
'x {@link http://www.example.com/foo y [bar](http://www.example.com/bar) z');
});
test('Should support non-ascii characters in parameter name (#90108)', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'parámetroConDiacríticos this will not'
}
]),
'*@param* `parámetroConDiacríticos` — this will not');
});
});

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, joinLines, retryUntilDocumentChanges, wait } from './testUtils';
suite('TypeScript Quick Fix', () => {
const _disposables: vscode.Disposable[] = [];
teardown(async () => {
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Fix all should not be marked as preferred #97866', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`const a$0 = 1;`,
`const b = 2;`,
);
await retryUntilDocumentChanges(testDocumentUri, { retries: 10, timeout: 500 }, _disposables, () => {
return vscode.commands.executeCommand('editor.action.autoFix');
});
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`const b = 2;`,
));
});
test('Add import should be a preferred fix if there is only one possible import', async () => {
const testDocumentUri = workspaceFile('foo.ts');
await createTestEditor(testDocumentUri,
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`
);
await retryUntilDocumentChanges(testDocumentUri, { retries: 10, timeout: 500 }, _disposables, () => {
return vscode.commands.executeCommand('editor.action.autoFix');
});
// Document should not have been changed here
assert.strictEqual(editor.document.getText(), joinLines(
`import { foo } from "./foo";`,
``,
`export const _ = 1;`,
`foo;`
));
});
test('Add import should not be a preferred fix if are multiple possible imports', async () => {
await createTestEditor(workspaceFile('foo.ts'),
`export const foo = 1;`);
await createTestEditor(workspaceFile('bar.ts'),
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`
);
await wait(3000);
await vscode.commands.executeCommand('editor.action.autoFix');
await wait(500);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`foo;`
));
});
test('Only a single ts-ignore should be returned if there are multiple errors on one line #98274', async () => {
const testDocumentUri = workspaceFile('foojs.js');
const editor = await createTestEditor(testDocumentUri,
`//@ts-check`,
`const a = require('./bla');`);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
const ignoreFixes = fixes?.filter(x => x.title === 'Ignore this error message');
assert.strictEqual(ignoreFixes?.length, 1);
});
test('Should prioritize implement interface over remove unused #94212', async () => {
const testDocumentUri = workspaceFile('foo.ts');
const editor = await createTestEditor(testDocumentUri,
`export interface IFoo { value: string; }`,
`class Foo implements IFoo { }`);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 2);
assert.strictEqual(fixes![0].title, `Implement interface 'IFoo'`);
assert.strictEqual(fixes![1].title, `Remove unused declaration for: 'Foo'`);
});
test('Should prioritize implement abstract class over remove unused #101486', async () => {
const testDocumentUri = workspaceFile('foo.ts');
const editor = await createTestEditor(testDocumentUri,
`export abstract class Foo { abstract foo(): number; }`,
`class ConcreteFoo extends Foo { }`,
);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 2);
assert.strictEqual(fixes![0].title, `Implement inherited abstract class`);
assert.strictEqual(fixes![1].title, `Remove unused declaration for: 'ConcreteFoo'`);
});
test('Add all missing imports should come after other add import fixes #98613', async () => {
await createTestEditor(workspaceFile('foo.ts'),
`export const foo = 1;`);
await createTestEditor(workspaceFile('bar.ts'),
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`,
`foo$0;`
);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
workspaceFile('index.ts'),
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 3);
assert.strictEqual(fixes![0].title, `Import 'foo' from module "./bar"`);
assert.strictEqual(fixes![1].title, `Import 'foo' from module "./foo"`);
assert.strictEqual(fixes![2].title, `Add all missing imports`);
});
});
function workspaceFile(fileName: string) {
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, fileName);
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, wait } from './testUtils';
type VsCodeConfiguration = { [key: string]: any };
async function updateConfig(newConfig: VsCodeConfiguration): Promise<VsCodeConfiguration> {
const oldConfig: VsCodeConfiguration = {};
const config = vscode.workspace.getConfiguration(undefined);
for (const configKey of Object.keys(newConfig)) {
oldConfig[configKey] = config.get(configKey);
await new Promise<void>((resolve, reject) =>
config.update(configKey, newConfig[configKey], vscode.ConfigurationTarget.Global)
.then(() => resolve(), reject));
}
return oldConfig;
}
namespace Config {
export const referencesCodeLens = 'typescript.referencesCodeLens.enabled';
}
suite('TypeScript References', () => {
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.referencesCodeLens]: true,
});
const _disposables: vscode.Disposable[] = [];
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(100);
// Save off config and apply defaults
oldConfig = await updateConfig(configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Should show on basic class', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test1.ts');
await createTestEditor(testDocumentUri,
`class Foo {}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 1);
assert.strictEqual(codeLenses?.[0].range.start.line, 0);
});
test('Should show on basic class properties', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test2.ts');
await createTestEditor(testDocumentUri,
`class Foo {`,
` prop: number;`,
` meth(): void {}`,
`}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 3);
assert.strictEqual(codeLenses?.[0].range.start.line, 0);
assert.strictEqual(codeLenses?.[1].range.start.line, 1);
assert.strictEqual(codeLenses?.[2].range.start.line, 2);
});
test('Should not show on const property', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test3.ts');
await createTestEditor(testDocumentUri,
`const foo = {`,
` prop: 1;`,
` meth(): void {}`,
`}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 0);
});
test.skip('Should not show duplicate references on ES5 class (https://github.com/microsoft/vscode/issues/90396)', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test3.js');
await createTestEditor(testDocumentUri,
`function A() {`,
` console.log("hi");`,
`}`,
`A.x = {};`,
);
await wait(500);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 1);
});
});
function getCodeLenses(document: vscode.Uri): Thenable<readonly vscode.CodeLens[] | undefined> {
return vscode.commands.executeCommand<readonly vscode.CodeLens[]>('vscode.executeCodeLensProvider', document, 100);
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { RequestQueue, RequestQueueingType } from '../tsServer/requestQueue';
suite('RequestQueue', () => {
test('should be empty on creation', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(queue.dequeue(), undefined);
});
suite('RequestQueue.createRequest', () => {
test('should create items with increasing sequence numbers', async () => {
const queue = new RequestQueue();
for (let i = 0; i < 100; ++i) {
const command = `command-${i}`;
const request = queue.createRequest(command, i);
assert.strictEqual(request.seq, i);
assert.strictEqual(request.command, command);
assert.strictEqual(request.arguments, i);
}
});
});
test('should queue normal requests in first in first out order', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
const request1 = queue.createRequest('a', 1);
queue.enqueue({ request: request1, expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
assert.strictEqual(queue.length, 1);
const request2 = queue.createRequest('b', 2);
queue.enqueue({ request: request2, expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
assert.strictEqual(queue.length, 2);
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'a');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'b');
}
{
const item = queue.dequeue();
assert.strictEqual(item, undefined);
assert.strictEqual(queue.length, 0);
}
});
test('should put normal requests in front of low priority requests', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
queue.enqueue({ request: queue.createRequest('low-1', 1), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('low-2', 1), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('normal-1', 2), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
queue.enqueue({ request: queue.createRequest('normal-2', 2), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 3);
assert.strictEqual(item!.request.command, 'normal-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 2);
assert.strictEqual(item!.request.command, 'normal-2');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'low-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'low-2');
}
});
test('should not push fence requests front of low priority requests', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
queue.enqueue({ request: queue.createRequest('low-1', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('fence', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Fence });
queue.enqueue({ request: queue.createRequest('low-2', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('normal', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 3);
assert.strictEqual(item!.request.command, 'low-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 2);
assert.strictEqual(item!.request.command, 'fence');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'normal');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'low-2');
}
});
});

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as stream from 'stream';
import type * as Proto from '../protocol';
import { NodeRequestCanceller } from '../tsServer/cancellation.electron';
import { ProcessBasedTsServer, TsServerProcess } from '../tsServer/server';
import { ServerType } from '../typescriptService';
import { nulToken } from '../utils/cancellation';
import { Logger } from '../utils/logger';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
const NoopTelemetryReporter = new class implements TelemetryReporter {
logTelemetry(): void { /* noop */ }
dispose(): void { /* noop */ }
};
class FakeServerProcess implements TsServerProcess {
private readonly _out: stream.PassThrough;
private readonly writeListeners = new Set<(data: Buffer) => void>();
public stdout: stream.PassThrough;
constructor() {
this._out = new stream.PassThrough();
this.stdout = this._out;
}
public write(data: Proto.Request) {
const listeners = Array.from(this.writeListeners);
this.writeListeners.clear();
setImmediate(() => {
for (const listener of listeners) {
listener(Buffer.from(JSON.stringify(data), 'utf8'));
}
const body = Buffer.from(JSON.stringify({ 'seq': data.seq, 'type': 'response', 'command': data.command, 'request_seq': data.seq, 'success': true }), 'utf8');
this._out.write(Buffer.from(`Content-Length: ${body.length}\r\n\r\n${body}`, 'utf8'));
});
}
onData(_handler: any) { /* noop */ }
onError(_handler: any) { /* noop */ }
onExit(_handler: any) { /* noop */ }
kill(): void { /* noop */ }
public onWrite(): Promise<any> {
return new Promise<string>((resolve) => {
this.writeListeners.add((data) => {
resolve(JSON.parse(data.toString()));
});
});
}
}
suite('Server', () => {
const tracer = new Tracer(new Logger());
test('should send requests with increasing sequence numbers', async () => {
const process = new FakeServerProcess();
const server = new ProcessBasedTsServer('semantic', ServerType.Semantic, process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer);
const onWrite1 = process.onWrite();
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });
assert.strictEqual((await onWrite1).seq, 0);
const onWrite2 = process.onWrite();
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });
assert.strictEqual((await onWrite2).seq, 1);
});
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { onChangedDocument, wait, retryUntilDocumentChanges } from './testUtils';
export async function acceptFirstSuggestion(uri: vscode.Uri, _disposables: vscode.Disposable[]) {
return retryUntilDocumentChanges(uri, { retries: 10, timeout: 0 }, _disposables, async () => {
await vscode.commands.executeCommand('editor.action.triggerSuggest');
await wait(1000);
await vscode.commands.executeCommand('acceptSelectedSuggestion');
});
}
export async function typeCommitCharacter(uri: vscode.Uri, character: string, _disposables: vscode.Disposable[]) {
const didChangeDocument = onChangedDocument(uri, _disposables);
await vscode.commands.executeCommand('editor.action.triggerSuggest');
await wait(3000); // Give time for suggestions to show
await vscode.commands.executeCommand('type', { text: character });
return await didChangeDocument;
}

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import { join } from 'path';
import * as vscode from 'vscode';
function rndName() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
}
export function createRandomFile(contents = '', fileExtension = 'txt'): Thenable<vscode.Uri> {
return new Promise((resolve, reject) => {
const tmpFile = join(os.tmpdir(), rndName() + '.' + fileExtension);
fs.writeFile(tmpFile, contents, (error) => {
if (error) {
return reject(error);
}
resolve(vscode.Uri.file(tmpFile));
});
});
}
export function deleteFile(file: vscode.Uri): Thenable<boolean> {
return new Promise((resolve, reject) => {
fs.unlink(file.fsPath, (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
});
}
export const CURSOR = '$$CURSOR$$';
export function withRandomFileEditor(
contents: string,
fileExtension: string,
run: (editor: vscode.TextEditor, doc: vscode.TextDocument) => Thenable<void>
): Thenable<boolean> {
const cursorIndex = contents.indexOf(CURSOR);
return createRandomFile(contents.replace(CURSOR, ''), fileExtension).then(file => {
return vscode.workspace.openTextDocument(file).then(doc => {
return vscode.window.showTextDocument(doc).then((editor) => {
if (cursorIndex >= 0) {
const pos = doc.positionAt(cursorIndex);
editor.selection = new vscode.Selection(pos, pos);
}
return run(editor, doc).then(_ => {
if (doc.isDirty) {
return doc.save().then(() => {
return deleteFile(file);
});
} else {
return deleteFile(file);
}
});
});
});
});
}
export const wait = (ms: number) => new Promise<void>(resolve => setTimeout(() => resolve(), ms));
export const joinLines = (...args: string[]) => args.join(os.platform() === 'win32' ? '\r\n' : '\n');
export async function createTestEditor(uri: vscode.Uri, ...lines: string[]) {
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
await editor.insertSnippet(new vscode.SnippetString(joinLines(...lines)), new vscode.Range(0, 0, 1000, 0));
return editor;
}
export function assertEditorContents(editor: vscode.TextEditor, expectedDocContent: string, message?: string): void {
const cursorIndex = expectedDocContent.indexOf(CURSOR);
assert.strictEqual(
editor.document.getText(),
expectedDocContent.replace(CURSOR, ''),
message);
if (cursorIndex >= 0) {
const expectedCursorPos = editor.document.positionAt(cursorIndex);
assert.deepEqual(
{ line: editor.selection.active.line, character: editor.selection.active.line },
{ line: expectedCursorPos.line, character: expectedCursorPos.line },
'Cursor position'
);
}
}
export type VsCodeConfiguration = { [key: string]: any };
export async function updateConfig(documentUri: vscode.Uri, newConfig: VsCodeConfiguration): Promise<VsCodeConfiguration> {
const oldConfig: VsCodeConfiguration = {};
const config = vscode.workspace.getConfiguration(undefined, documentUri);
for (const configKey of Object.keys(newConfig)) {
oldConfig[configKey] = config.get(configKey);
await new Promise<void>((resolve, reject) =>
config.update(configKey, newConfig[configKey], vscode.ConfigurationTarget.Global)
.then(() => resolve(), reject));
}
return oldConfig;
}
export const Config = Object.freeze({
autoClosingBrackets: 'editor.autoClosingBrackets',
typescriptCompleteFunctionCalls: 'typescript.suggest.completeFunctionCalls',
insertMode: 'editor.suggest.insertMode',
snippetSuggestions: 'editor.snippetSuggestions',
suggestSelection: 'editor.suggestSelection',
javascriptQuoteStyle: 'javascript.preferences.quoteStyle',
typescriptQuoteStyle: 'typescript.preferences.quoteStyle',
} as const);
export const insertModesValues = Object.freeze(['insert', 'replace']);
export async function enumerateConfig(
documentUri: vscode.Uri,
configKey: string,
values: readonly string[],
f: (message: string) => Promise<void>
): Promise<void> {
for (const value of values) {
const newConfig = { [configKey]: value };
await updateConfig(documentUri, newConfig);
await f(JSON.stringify(newConfig));
}
}
export function onChangedDocument(documentUri: vscode.Uri, disposables: vscode.Disposable[]) {
return new Promise<vscode.TextDocument>(resolve => vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() === documentUri.toString()) {
resolve(e.document);
}
}, undefined, disposables));
}
export async function retryUntilDocumentChanges(
documentUri: vscode.Uri,
options: { retries: number, timeout: number },
disposables: vscode.Disposable[],
exec: () => Thenable<unknown>,
) {
const didChangeDocument = onChangedDocument(documentUri, disposables);
let done = false;
const result = await Promise.race([
didChangeDocument,
(async () => {
for (let i = 0; i < options.retries; ++i) {
await wait(options.timeout);
if (done) {
return;
}
await exec();
}
})(),
]);
done = true;
return result;
}

View File

@@ -0,0 +1,619 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService';
import API from '../utils/api';
import { coalesce } from '../utils/arrays';
import { Delayer } from '../utils/async';
import { nulToken } from '../utils/cancellation';
import { Disposable } from '../utils/dispose';
import * as languageModeIds from '../utils/languageModeIds';
import { ResourceMap } from '../utils/resourceMap';
import * as typeConverters from '../utils/typeConverters';
const enum BufferKind {
TypeScript = 1,
JavaScript = 2,
}
const enum BufferState {
Initial = 1,
Open = 2,
Closed = 2,
}
function mode2ScriptKind(mode: string): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined {
switch (mode) {
case languageModeIds.typescript: return 'TS';
case languageModeIds.typescriptreact: return 'TSX';
case languageModeIds.javascript: return 'JS';
case languageModeIds.javascriptreact: return 'JSX';
}
return undefined;
}
const enum BufferOperationType { Close, Open, Change }
class CloseOperation {
readonly type = BufferOperationType.Close;
constructor(
public readonly args: string
) { }
}
class OpenOperation {
readonly type = BufferOperationType.Open;
constructor(
public readonly args: Proto.OpenRequestArgs
) { }
}
class ChangeOperation {
readonly type = BufferOperationType.Change;
constructor(
public readonly args: Proto.FileCodeEdits
) { }
}
type BufferOperation = CloseOperation | OpenOperation | ChangeOperation;
/**
* Manages synchronization of buffers with the TS server.
*
* If supported, batches together file changes. This allows the TS server to more efficiently process changes.
*/
class BufferSynchronizer {
private readonly _pending: ResourceMap<BufferOperation>;
constructor(
private readonly client: ITypeScriptServiceClient,
onCaseInsenitiveFileSystem: boolean
) {
this._pending = new ResourceMap<BufferOperation>(undefined, {
onCaseInsenitiveFileSystem
});
}
public open(resource: vscode.Uri, args: Proto.OpenRequestArgs) {
if (this.supportsBatching) {
this.updatePending(resource, new OpenOperation(args));
} else {
this.client.executeWithoutWaitingForResponse('open', args);
}
}
/**
* @return Was the buffer open?
*/
public close(resource: vscode.Uri, filepath: string): boolean {
if (this.supportsBatching) {
return this.updatePending(resource, new CloseOperation(filepath));
} else {
const args: Proto.FileRequestArgs = { file: filepath };
this.client.executeWithoutWaitingForResponse('close', args);
return true;
}
}
public change(resource: vscode.Uri, filepath: string, events: readonly vscode.TextDocumentContentChangeEvent[]) {
if (!events.length) {
return;
}
if (this.supportsBatching) {
this.updatePending(resource, new ChangeOperation({
fileName: filepath,
textChanges: events.map((change): Proto.CodeEdit => ({
newText: change.text,
start: typeConverters.Position.toLocation(change.range.start),
end: typeConverters.Position.toLocation(change.range.end),
})).reverse(), // Send the edits end-of-document to start-of-document order
}));
} else {
for (const { range, text } of events) {
const args: Proto.ChangeRequestArgs = {
insertString: text,
...typeConverters.Range.toFormattingRequestArgs(filepath, range)
};
this.client.executeWithoutWaitingForResponse('change', args);
}
}
}
public reset(): void {
this._pending.clear();
}
public beforeCommand(command: string): void {
if (command === 'updateOpen') {
return;
}
this.flush();
}
private flush() {
if (!this.supportsBatching) {
// We've already eagerly synchronized
this._pending.clear();
return;
}
if (this._pending.size > 0) {
const closedFiles: string[] = [];
const openFiles: Proto.OpenRequestArgs[] = [];
const changedFiles: Proto.FileCodeEdits[] = [];
for (const change of this._pending.values) {
switch (change.type) {
case BufferOperationType.Change: changedFiles.push(change.args); break;
case BufferOperationType.Open: openFiles.push(change.args); break;
case BufferOperationType.Close: closedFiles.push(change.args); break;
}
}
this.client.execute('updateOpen', { changedFiles, closedFiles, openFiles }, nulToken, { nonRecoverable: true });
this._pending.clear();
}
}
private get supportsBatching(): boolean {
return this.client.apiVersion.gte(API.v340);
}
private updatePending(resource: vscode.Uri, op: BufferOperation): boolean {
switch (op.type) {
case BufferOperationType.Close:
const existing = this._pending.get(resource);
switch (existing?.type) {
case BufferOperationType.Open:
this._pending.delete(resource);
return false; // Open then close. No need to do anything
}
break;
}
if (this._pending.has(resource)) {
// we saw this file before, make sure we flush before working with it again
this.flush();
}
this._pending.set(resource, op);
return true;
}
}
class SyncedBuffer {
private state = BufferState.Initial;
constructor(
private readonly document: vscode.TextDocument,
public readonly filepath: string,
private readonly client: ITypeScriptServiceClient,
private readonly synchronizer: BufferSynchronizer,
) { }
public open(): void {
const args: Proto.OpenRequestArgs = {
file: this.filepath,
fileContent: this.document.getText(),
projectRootPath: this.client.getWorkspaceRootForResource(this.document.uri),
};
const scriptKind = mode2ScriptKind(this.document.languageId);
if (scriptKind) {
args.scriptKindName = scriptKind;
}
if (this.client.apiVersion.gte(API.v240)) {
const tsPluginsForDocument = this.client.pluginManager.plugins
.filter(x => x.languages.indexOf(this.document.languageId) >= 0);
if (tsPluginsForDocument.length) {
(args as any).plugins = tsPluginsForDocument.map(plugin => plugin.name);
}
}
this.synchronizer.open(this.resource, args);
this.state = BufferState.Open;
}
public get resource(): vscode.Uri {
return this.document.uri;
}
public get lineCount(): number {
return this.document.lineCount;
}
public get kind(): BufferKind {
switch (this.document.languageId) {
case languageModeIds.javascript:
case languageModeIds.javascriptreact:
return BufferKind.JavaScript;
case languageModeIds.typescript:
case languageModeIds.typescriptreact:
default:
return BufferKind.TypeScript;
}
}
/**
* @return Was the buffer open?
*/
public close(): boolean {
if (this.state !== BufferState.Open) {
this.state = BufferState.Closed;
return false;
}
this.state = BufferState.Closed;
return this.synchronizer.close(this.resource, this.filepath);
}
public onContentChanged(events: readonly vscode.TextDocumentContentChangeEvent[]): void {
if (this.state !== BufferState.Open) {
console.error(`Unexpected buffer state: ${this.state}`);
}
this.synchronizer.change(this.resource, this.filepath, events);
}
}
class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
public getForPath(filePath: string): SyncedBuffer | undefined {
return this.get(vscode.Uri.file(filePath));
}
public get allBuffers(): Iterable<SyncedBuffer> {
return this.values;
}
}
class PendingDiagnostics extends ResourceMap<number> {
public getOrderedFileSet(): ResourceMap<void> {
const orderedResources = Array.from(this.entries)
.sort((a, b) => a.value - b.value)
.map(entry => entry.resource);
const map = new ResourceMap<void>(undefined, this.config);
for (const resource of orderedResources) {
map.set(resource, undefined);
}
return map;
}
}
class GetErrRequest {
public static executeGetErrRequest(
client: ITypeScriptServiceClient,
files: ResourceMap<void>,
onDone: () => void
) {
return new GetErrRequest(client, files, onDone);
}
private _done: boolean = false;
private readonly _token: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();
private constructor(
client: ITypeScriptServiceClient,
public readonly files: ResourceMap<void>,
onDone: () => void
) {
const allFiles = coalesce(Array.from(files.entries)
.filter(entry => client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic))
.map(entry => client.normalizedPath(entry.resource)));
if (!allFiles.length || !client.capabilities.has(ClientCapability.Semantic)) {
this._done = true;
setImmediate(onDone);
} else {
const request = client.configuration.enableProjectDiagnostics
// Note that geterrForProject is almost certainly not the api we want here as it ends up computing far
// too many diagnostics
? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token)
: client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token);
request.finally(() => {
if (this._done) {
return;
}
this._done = true;
onDone();
});
}
}
public cancel(): any {
if (!this._done) {
this._token.cancel();
}
this._token.dispose();
}
}
export default class BufferSyncSupport extends Disposable {
private readonly client: ITypeScriptServiceClient;
private _validateJavaScript: boolean = true;
private _validateTypeScript: boolean = true;
private readonly modeIds: Set<string>;
private readonly syncedBuffers: SyncedBufferMap;
private readonly pendingDiagnostics: PendingDiagnostics;
private readonly diagnosticDelayer: Delayer<any>;
private pendingGetErr: GetErrRequest | undefined;
private listening: boolean = false;
private readonly synchronizer: BufferSynchronizer;
constructor(
client: ITypeScriptServiceClient,
modeIds: readonly string[],
onCaseInsenitiveFileSystem: boolean
) {
super();
this.client = client;
this.modeIds = new Set<string>(modeIds);
this.diagnosticDelayer = new Delayer<any>(300);
const pathNormalizer = (path: vscode.Uri) => this.client.normalizedPath(path);
this.syncedBuffers = new SyncedBufferMap(pathNormalizer, { onCaseInsenitiveFileSystem });
this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer, { onCaseInsenitiveFileSystem });
this.synchronizer = new BufferSynchronizer(client, onCaseInsenitiveFileSystem);
this.updateConfiguration();
vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this, this._disposables);
}
private readonly _onDelete = this._register(new vscode.EventEmitter<vscode.Uri>());
public readonly onDelete = this._onDelete.event;
private readonly _onWillChange = this._register(new vscode.EventEmitter<vscode.Uri>());
public readonly onWillChange = this._onWillChange.event;
public listen(): void {
if (this.listening) {
return;
}
this.listening = true;
vscode.workspace.onDidOpenTextDocument(this.openTextDocument, this, this._disposables);
vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this._disposables);
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this._disposables);
vscode.window.onDidChangeVisibleTextEditors(e => {
for (const { document } of e) {
const syncedBuffer = this.syncedBuffers.get(document.uri);
if (syncedBuffer) {
this.requestDiagnostic(syncedBuffer);
}
}
}, this, this._disposables);
vscode.workspace.textDocuments.forEach(this.openTextDocument, this);
}
public handles(resource: vscode.Uri): boolean {
return this.syncedBuffers.has(resource);
}
public ensureHasBuffer(resource: vscode.Uri): boolean {
if (this.syncedBuffers.has(resource)) {
return true;
}
const existingDocument = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString());
if (existingDocument) {
return this.openTextDocument(existingDocument);
}
return false;
}
public toVsCodeResource(resource: vscode.Uri): vscode.Uri {
const filepath = this.client.normalizedPath(resource);
for (const buffer of this.syncedBuffers.allBuffers) {
if (buffer.filepath === filepath) {
return buffer.resource;
}
}
return resource;
}
public toResource(filePath: string): vscode.Uri {
const buffer = this.syncedBuffers.getForPath(filePath);
if (buffer) {
return buffer.resource;
}
return vscode.Uri.file(filePath);
}
public reset(): void {
this.pendingGetErr?.cancel();
this.pendingDiagnostics.clear();
this.synchronizer.reset();
}
public reinitialize(): void {
this.reset();
for (const buffer of this.syncedBuffers.allBuffers) {
buffer.open();
}
}
public openTextDocument(document: vscode.TextDocument): boolean {
if (!this.modeIds.has(document.languageId)) {
return false;
}
const resource = document.uri;
const filepath = this.client.normalizedPath(resource);
if (!filepath) {
return false;
}
if (this.syncedBuffers.has(resource)) {
return true;
}
const syncedBuffer = new SyncedBuffer(document, filepath, this.client, this.synchronizer);
this.syncedBuffers.set(resource, syncedBuffer);
syncedBuffer.open();
this.requestDiagnostic(syncedBuffer);
return true;
}
public closeResource(resource: vscode.Uri): void {
const syncedBuffer = this.syncedBuffers.get(resource);
if (!syncedBuffer) {
return;
}
this.pendingDiagnostics.delete(resource);
this.pendingGetErr?.files.delete(resource);
this.syncedBuffers.delete(resource);
const wasBufferOpen = syncedBuffer.close();
this._onDelete.fire(resource);
if (wasBufferOpen) {
this.requestAllDiagnostics();
}
}
public interuptGetErr<R>(f: () => R): R {
if (!this.pendingGetErr
|| this.client.configuration.enableProjectDiagnostics // `geterr` happens on seperate server so no need to cancel it.
) {
return f();
}
this.pendingGetErr.cancel();
this.pendingGetErr = undefined;
const result = f();
this.triggerDiagnostics();
return result;
}
public beforeCommand(command: string): void {
this.synchronizer.beforeCommand(command);
}
private onDidCloseTextDocument(document: vscode.TextDocument): void {
this.closeResource(document.uri);
}
private onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent): void {
const syncedBuffer = this.syncedBuffers.get(e.document.uri);
if (!syncedBuffer) {
return;
}
this._onWillChange.fire(syncedBuffer.resource);
syncedBuffer.onContentChanged(e.contentChanges);
const didTrigger = this.requestDiagnostic(syncedBuffer);
if (!didTrigger && this.pendingGetErr) {
// In this case we always want to re-trigger all diagnostics
this.pendingGetErr.cancel();
this.pendingGetErr = undefined;
this.triggerDiagnostics();
}
}
public requestAllDiagnostics() {
for (const buffer of this.syncedBuffers.allBuffers) {
if (this.shouldValidate(buffer)) {
this.pendingDiagnostics.set(buffer.resource, Date.now());
}
}
this.triggerDiagnostics();
}
public getErr(resources: readonly vscode.Uri[]): any {
const handledResources = resources.filter(resource => this.handles(resource));
if (!handledResources.length) {
return;
}
for (const resource of handledResources) {
this.pendingDiagnostics.set(resource, Date.now());
}
this.triggerDiagnostics();
}
private triggerDiagnostics(delay: number = 200) {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics();
}, delay);
}
private requestDiagnostic(buffer: SyncedBuffer): boolean {
if (!this.shouldValidate(buffer)) {
return false;
}
this.pendingDiagnostics.set(buffer.resource, Date.now());
const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800);
this.triggerDiagnostics(delay);
return true;
}
public hasPendingDiagnostics(resource: vscode.Uri): boolean {
return this.pendingDiagnostics.has(resource);
}
private sendPendingDiagnostics(): void {
const orderedFileSet = this.pendingDiagnostics.getOrderedFileSet();
if (this.pendingGetErr) {
this.pendingGetErr.cancel();
for (const { resource } of this.pendingGetErr.files.entries) {
if (this.syncedBuffers.get(resource)) {
orderedFileSet.set(resource, undefined);
}
}
this.pendingGetErr = undefined;
}
// Add all open TS buffers to the geterr request. They might be visible
for (const buffer of this.syncedBuffers.values) {
orderedFileSet.set(buffer.resource, undefined);
}
if (orderedFileSet.size) {
const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => {
if (this.pendingGetErr === getErr) {
this.pendingGetErr = undefined;
}
});
}
this.pendingDiagnostics.clear();
}
private updateConfiguration() {
const jsConfig = vscode.workspace.getConfiguration('javascript', null);
const tsConfig = vscode.workspace.getConfiguration('typescript', null);
this._validateJavaScript = jsConfig.get<boolean>('validate.enable', true);
this._validateTypeScript = tsConfig.get<boolean>('validate.enable', true);
}
private shouldValidate(buffer: SyncedBuffer) {
switch (buffer.kind) {
case BufferKind.JavaScript:
return this._validateJavaScript;
case BufferKind.TypeScript:
default:
return this._validateTypeScript;
}
}
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ServerResponse } from '../typescriptService';
type Resolve<T extends Proto.Response> = () => Promise<ServerResponse.Response<T>>;
/**
* Caches a class of TS Server request based on document.
*/
export class CachedResponse<T extends Proto.Response> {
private response?: Promise<ServerResponse.Response<T>>;
private version: number = -1;
private document: string = '';
/**
* Execute a request. May return cached value or resolve the new value
*
* Caller must ensure that all input `resolve` functions return equivilent results (keyed only off of document).
*/
public execute(
document: vscode.TextDocument,
resolve: Resolve<T>
): Promise<ServerResponse.Response<T>> {
if (this.response && this.matches(document)) {
// Chain so that on cancellation we fall back to the next resolve
return this.response = this.response.then(result => result.type === 'cancelled' ? resolve() : result);
}
return this.reset(document, resolve);
}
private matches(document: vscode.TextDocument): boolean {
return this.version === document.version && this.document === document.uri.toString();
}
private async reset(
document: vscode.TextDocument,
resolve: Resolve<T>
): Promise<ServerResponse.Response<T>> {
this.version = document.version;
this.document = document.uri.toString();
return this.response = resolve();
}
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { ServerResponse } from '../typescriptService';
export interface CallbackItem<R> {
readonly onSuccess: (value: R) => void;
readonly onError: (err: Error) => void;
readonly queuingStartTime: number;
readonly isAsync: boolean;
}
export class CallbackMap<R extends Proto.Response> {
private readonly _callbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
private readonly _asyncCallbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
public destroy(cause: string): void {
const cancellation = new ServerResponse.Cancelled(cause);
for (const callback of this._callbacks.values()) {
callback.onSuccess(cancellation);
}
this._callbacks.clear();
for (const callback of this._asyncCallbacks.values()) {
callback.onSuccess(cancellation);
}
this._asyncCallbacks.clear();
}
public add(seq: number, callback: CallbackItem<ServerResponse.Response<R> | undefined>, isAsync: boolean) {
if (isAsync) {
this._asyncCallbacks.set(seq, callback);
} else {
this._callbacks.set(seq, callback);
}
}
public fetch(seq: number): CallbackItem<ServerResponse.Response<R> | undefined> | undefined {
const callback = this._callbacks.get(seq) || this._asyncCallbacks.get(seq);
this.delete(seq);
return callback;
}
private delete(seq: number) {
if (!this._callbacks.delete(seq)) {
this._asyncCallbacks.delete(seq);
}
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* 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 { getTempFile } from '../utils/temp.electron';
import Tracer from '../utils/tracer';
import { OngoingRequestCanceller, OngoingRequestCancellerFactory } from './cancellation';
export class NodeRequestCanceller implements OngoingRequestCanceller {
public readonly cancellationPipeName: string;
public constructor(
private readonly _serverId: string,
private readonly _tracer: Tracer,
) {
this.cancellationPipeName = getTempFile('tscancellation');
}
public tryCancelOngoingRequest(seq: number): boolean {
if (!this.cancellationPipeName) {
return false;
}
this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
try {
fs.writeFileSync(this.cancellationPipeName + seq, '');
} catch {
// noop
}
return true;
}
}
export const nodeRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
create(serverId: string, tracer: Tracer): OngoingRequestCanceller {
return new NodeRequestCanceller(serverId, tracer);
}
};

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Tracer from '../utils/tracer';
export interface OngoingRequestCanceller {
readonly cancellationPipeName: string | undefined;
tryCancelOngoingRequest(seq: number): boolean;
}
export interface OngoingRequestCancellerFactory {
create(serverId: string, tracer: Tracer): OngoingRequestCanceller;
}
const noopRequestCanceller = new class implements OngoingRequestCanceller {
public readonly cancellationPipeName = undefined;
public tryCancelOngoingRequest(_seq: number): boolean {
return false;
}
};
export const noopRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
create(_serverId: string, _tracer: Tracer): OngoingRequestCanceller {
return noopRequestCanceller;
}
};

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import * as vscode from 'vscode';
import { ILogDirectoryProvider } from './logDirectoryProvider';
import { memoize } from '../utils/memoize';
export class NodeLogDirectoryProvider implements ILogDirectoryProvider {
public constructor(
private readonly context: vscode.ExtensionContext
) { }
public getNewLogDirectory(): string | undefined {
const root = this.logDirectory();
if (root) {
try {
return fs.mkdtempSync(path.join(root, `tsserver-log-`));
} catch (e) {
return undefined;
}
}
return undefined;
}
@memoize
private logDirectory(): string | undefined {
try {
const path = this.context.logPath;
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
return this.context.logPath;
} catch {
return undefined;
}
}
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ILogDirectoryProvider {
getNewLogDirectory(): string | undefined;
}
export const noopLogDirectoryProvider = new class implements ILogDirectoryProvider {
public getNewLogDirectory(): undefined {
return undefined;
}
};

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
export enum RequestQueueingType {
/**
* Normal request that is executed in order.
*/
Normal = 1,
/**
* Request that normal requests jump in front of in the queue.
*/
LowPriority = 2,
/**
* A fence that blocks request reordering.
*
* Fences are not reordered. Unlike a normal request, a fence will never jump in front of a low priority request
* in the request queue.
*/
Fence = 3,
}
export interface RequestItem {
readonly request: Proto.Request;
readonly expectsResponse: boolean;
readonly isAsync: boolean;
readonly queueingType: RequestQueueingType;
}
export class RequestQueue {
private readonly queue: RequestItem[] = [];
private sequenceNumber: number = 0;
public get length(): number {
return this.queue.length;
}
public enqueue(item: RequestItem): void {
if (item.queueingType === RequestQueueingType.Normal) {
let index = this.queue.length - 1;
while (index >= 0) {
if (this.queue[index].queueingType !== RequestQueueingType.LowPriority) {
break;
}
--index;
}
this.queue.splice(index + 1, 0, item);
} else {
// Only normal priority requests can be reordered. All other requests just go to the end.
this.queue.push(item);
}
}
public dequeue(): RequestItem | undefined {
return this.queue.shift();
}
public tryDeletePendingRequest(seq: number): boolean {
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].request.seq === seq) {
this.queue.splice(i, 1);
return true;
}
}
return false;
}
public createRequest(command: string, args: any): Proto.Request {
return {
seq: this.sequenceNumber++,
type: 'request',
command: command,
arguments: args
};
}
}

View File

@@ -0,0 +1,628 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { EventName } from '../protocol.const';
import { CallbackMap } from '../tsServer/callbackMap';
import { RequestItem, RequestQueue, RequestQueueingType } from '../tsServer/requestQueue';
import { TypeScriptServerError } from '../tsServer/serverError';
import { ServerResponse, ServerType, TypeScriptRequests } from '../typescriptService';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { OngoingRequestCanceller } from './cancellation';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
export enum ExectuionTarget {
Semantic,
Syntax
}
export interface ITypeScriptServer {
readonly onEvent: vscode.Event<Proto.Event>;
readonly onExit: vscode.Event<any>;
readonly onError: vscode.Event<any>;
readonly tsServerLogFile: string | undefined;
kill(): void;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
dispose(): void;
}
export interface TsServerDelegate {
onFatalError(command: string, error: Error): void;
}
export const enum TsServerProcessKind {
Main = 'main',
Syntax = 'syntax',
Semantic = 'semantic',
Diagnostics = 'diagnostics'
}
export interface TsServerProcessFactory {
fork(
tsServerPath: string,
args: readonly string[],
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
): TsServerProcess;
}
export interface TsServerProcess {
write(serverRequest: Proto.Request): void;
onData(handler: (data: Proto.Response) => void): void;
onExit(handler: (code: number | null) => void): void;
onError(handler: (error: Error) => void): void;
kill(): void;
}
export class ProcessBasedTsServer extends Disposable implements ITypeScriptServer {
private readonly _requestQueue = new RequestQueue();
private readonly _callbacks = new CallbackMap<Proto.Response>();
private readonly _pendingResponses = new Set<number>();
constructor(
private readonly _serverId: string,
private readonly _serverSource: ServerType,
private readonly _process: TsServerProcess,
private readonly _tsServerLogFile: string | undefined,
private readonly _requestCanceller: OngoingRequestCanceller,
private readonly _version: TypeScriptVersion,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _tracer: Tracer,
) {
super();
this._process.onData(msg => {
this.dispatchMessage(msg);
});
this._process.onExit(code => {
this._onExit.fire(code);
this._callbacks.destroy('server exited');
});
this._process.onError(error => {
this._onError.fire(error);
this._callbacks.destroy('server errored');
});
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this._tsServerLogFile; }
private write(serverRequest: Proto.Request) {
this._process.write(serverRequest);
}
public dispose() {
super.dispose();
this._callbacks.destroy('server disposed');
this._pendingResponses.clear();
}
public kill() {
this._process.kill();
}
private dispatchMessage(message: Proto.Message) {
try {
switch (message.type) {
case 'response':
if (this._serverSource) {
this.dispatchResponse({
...(message as Proto.Response),
_serverType: this._serverSource
});
} else {
this.dispatchResponse(message as Proto.Response);
}
break;
case 'event':
const event = message as Proto.Event;
if (event.event === 'requestCompleted') {
const seq = (event as Proto.RequestCompletedEvent).body.request_seq;
const callback = this._callbacks.fetch(seq);
if (callback) {
this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, callback);
callback.onSuccess(undefined);
}
} else {
this._tracer.traceEvent(this._serverId, event);
this._onEvent.fire(event);
}
break;
default:
throw new Error(`Unknown message type ${message.type} received`);
}
} finally {
this.sendNextRequests();
}
}
private tryCancelRequest(seq: number, command: string): boolean {
try {
if (this._requestQueue.tryDeletePendingRequest(seq)) {
this.logTrace(`Canceled request with sequence number ${seq}`);
return true;
}
if (this._requestCanceller.tryCancelOngoingRequest(seq)) {
return true;
}
this.logTrace(`Tried to cancel request with sequence number ${seq}. But request got already delivered.`);
return false;
} finally {
const callback = this.fetchCallback(seq);
if (callback) {
callback.onSuccess(new ServerResponse.Cancelled(`Cancelled request ${seq} - ${command}`));
}
}
}
private dispatchResponse(response: Proto.Response) {
const callback = this.fetchCallback(response.request_seq);
if (!callback) {
return;
}
this._tracer.traceResponse(this._serverId, response, callback);
if (response.success) {
callback.onSuccess(response);
} else if (response.message === 'No content available.') {
// Special case where response itself is successful but there is not any data to return.
callback.onSuccess(ServerResponse.NoContent);
} else {
callback.onError(TypeScriptServerError.create(this._serverId, this._version, response));
}
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
const request = this._requestQueue.createRequest(command, args);
const requestInfo: RequestItem = {
request,
expectsResponse: executeInfo.expectsResult,
isAsync: executeInfo.isAsync,
queueingType: ProcessBasedTsServer.getQueueingType(command, executeInfo.lowPriority)
};
let result: Promise<ServerResponse.Response<Proto.Response>> | undefined;
if (executeInfo.expectsResult) {
result = new Promise<ServerResponse.Response<Proto.Response>>((resolve, reject) => {
this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response<Proto.Response> | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync);
if (executeInfo.token) {
executeInfo.token.onCancellationRequested(() => {
this.tryCancelRequest(request.seq, command);
});
}
}).catch((err: Error) => {
if (err instanceof TypeScriptServerError) {
if (!executeInfo.token || !executeInfo.token.isCancellationRequested) {
/* __GDPR__
"languageServiceErrorResponse" : {
"${include}": [
"${TypeScriptCommonProperties}",
"${TypeScriptRequestErrorProperties}"
]
}
*/
this._telemetryReporter.logTelemetry('languageServiceErrorResponse', err.telemetry);
}
}
throw err;
});
}
this._requestQueue.enqueue(requestInfo);
this.sendNextRequests();
return result;
}
private sendNextRequests(): void {
while (this._pendingResponses.size === 0 && this._requestQueue.length > 0) {
const item = this._requestQueue.dequeue();
if (item) {
this.sendRequest(item);
}
}
}
private sendRequest(requestItem: RequestItem): void {
const serverRequest = requestItem.request;
this._tracer.traceRequest(this._serverId, serverRequest, requestItem.expectsResponse, this._requestQueue.length);
if (requestItem.expectsResponse && !requestItem.isAsync) {
this._pendingResponses.add(requestItem.request.seq);
}
try {
this.write(serverRequest);
} catch (err) {
const callback = this.fetchCallback(serverRequest.seq);
if (callback) {
callback.onError(err);
}
}
}
private fetchCallback(seq: number) {
const callback = this._callbacks.fetch(seq);
if (!callback) {
return undefined;
}
this._pendingResponses.delete(seq);
return callback;
}
private logTrace(message: string) {
this._tracer.logTrace(this._serverId, message);
}
private static readonly fenceCommands = new Set(['change', 'close', 'open', 'updateOpen']);
private static getQueueingType(
command: string,
lowPriority?: boolean
): RequestQueueingType {
if (ProcessBasedTsServer.fenceCommands.has(command)) {
return RequestQueueingType.Fence;
}
return lowPriority ? RequestQueueingType.LowPriority : RequestQueueingType.Normal;
}
}
interface ExecuteInfo {
readonly isAsync: boolean;
readonly token?: vscode.CancellationToken;
readonly expectsResult: boolean;
readonly lowPriority?: boolean;
readonly executionTarget?: ExectuionTarget;
}
class RequestRouter {
private static readonly sharedCommands = new Set<keyof TypeScriptRequests>([
'change',
'close',
'open',
'updateOpen',
'configure',
]);
constructor(
private readonly servers: ReadonlyArray<{
readonly server: ITypeScriptServer;
canRun?(command: keyof TypeScriptRequests, executeInfo: ExecuteInfo): void;
}>,
private readonly delegate: TsServerDelegate,
) { }
public execute(command: keyof TypeScriptRequests, args: any, executeInfo: ExecuteInfo): Promise<ServerResponse.Response<Proto.Response>> | undefined {
if (RequestRouter.sharedCommands.has(command) && typeof executeInfo.executionTarget === 'undefined') {
// Dispatch shared commands to all servers but only return from first one
const requestStates: RequestState.State[] = this.servers.map(() => RequestState.Unresolved);
// Also make sure we never cancel requests to just one server
let token: vscode.CancellationToken | undefined = undefined;
if (executeInfo.token) {
const source = new vscode.CancellationTokenSource();
executeInfo.token.onCancellationRequested(() => {
if (requestStates.some(state => state === RequestState.Resolved)) {
// Don't cancel.
// One of the servers completed this request so we don't want to leave the other
// in a different state.
return;
}
source.cancel();
});
token = source.token;
}
let firstRequest: Promise<ServerResponse.Response<Proto.Response>> | undefined;
for (let serverIndex = 0; serverIndex < this.servers.length; ++serverIndex) {
const server = this.servers[serverIndex].server;
const request = server.executeImpl(command, args, { ...executeInfo, token });
if (serverIndex === 0) {
firstRequest = request;
}
if (request) {
request
.then(result => {
requestStates[serverIndex] = RequestState.Resolved;
const erroredRequest = requestStates.find(state => state.type === RequestState.Type.Errored) as RequestState.Errored | undefined;
if (erroredRequest) {
// We've gone out of sync
this.delegate.onFatalError(command, erroredRequest.err);
}
return result;
}, err => {
requestStates[serverIndex] = new RequestState.Errored(err);
if (requestStates.some(state => state === RequestState.Resolved)) {
// We've gone out of sync
this.delegate.onFatalError(command, err);
}
throw err;
});
}
}
return firstRequest;
}
for (const { canRun, server } of this.servers) {
if (!canRun || canRun(command, executeInfo)) {
return server.executeImpl(command, args, executeInfo);
}
}
throw new Error(`Could not find server for command: '${command}'`);
}
}
export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer {
private static readonly diagnosticEvents = new Set<string>([
EventName.configFileDiag,
EventName.syntaxDiag,
EventName.semanticDiag,
EventName.suggestionDiag
]);
private readonly getErrServer: ITypeScriptServer;
private readonly mainServer: ITypeScriptServer;
private readonly router: RequestRouter;
public constructor(
servers: { getErr: ITypeScriptServer, primary: ITypeScriptServer },
delegate: TsServerDelegate,
) {
super();
this.getErrServer = servers.getErr;
this.mainServer = servers.primary;
this.router = new RequestRouter(
[
{ server: this.getErrServer, canRun: (command) => ['geterr', 'geterrForProject'].includes(command) },
{ server: this.mainServer, canRun: undefined /* gets all other commands */ }
],
delegate);
this._register(this.getErrServer.onEvent(e => {
if (GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
// Ignore all other events
}));
this._register(this.mainServer.onEvent(e => {
if (!GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
// Ignore all other events
}));
this._register(this.getErrServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onExit(e => {
this._onExit.fire(e);
this.getErrServer.kill();
}));
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this.mainServer.tsServerLogFile; }
public kill(): void {
this.getErrServer.kill();
this.mainServer.kill();
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
return this.router.execute(command, args, executeInfo);
}
}
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
/**
* Commands that should always be run on the syntax server.
*/
private static readonly syntaxAlwaysCommands = new Set<keyof TypeScriptRequests>([
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
/**
* Commands that should always be run on the semantic server.
*/
private static readonly semanticCommands = new Set<keyof TypeScriptRequests>([
'geterr',
'geterrForProject',
'projectInfo',
'configurePlugin',
]);
/**
* Commands that can be run on the syntax server but would benefit from being upgraded to the semantic server.
*/
private static readonly syntaxAllowedCommands = new Set<keyof TypeScriptRequests>([
'completions',
'completionEntryDetails',
'completionInfo',
'definition',
'definitionAndBoundSpan',
'documentHighlights',
'implementation',
'navto',
'quickinfo',
'references',
'rename',
'signatureHelp',
]);
private readonly syntaxServer: ITypeScriptServer;
private readonly semanticServer: ITypeScriptServer;
private readonly router: RequestRouter;
private _projectLoading = true;
public constructor(
servers: { syntax: ITypeScriptServer, semantic: ITypeScriptServer },
delegate: TsServerDelegate,
enableDynamicRouting: boolean,
) {
super();
this.syntaxServer = servers.syntax;
this.semanticServer = servers.semantic;
this.router = new RequestRouter(
[
{
server: this.syntaxServer,
canRun: (command, execInfo) => {
switch (execInfo.executionTarget) {
case ExectuionTarget.Semantic: return false;
case ExectuionTarget.Syntax: return true;
}
if (SyntaxRoutingTsServer.syntaxAlwaysCommands.has(command)) {
return true;
}
if (SyntaxRoutingTsServer.semanticCommands.has(command)) {
return false;
}
if (enableDynamicRouting && this.projectLoading && SyntaxRoutingTsServer.syntaxAllowedCommands.has(command)) {
return true;
}
return false;
}
}, {
server: this.semanticServer,
canRun: undefined /* gets all other commands */
}
],
delegate);
this._register(this.syntaxServer.onEvent(e => {
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onEvent(e => {
switch (e.event) {
case EventName.projectLoadingStart:
this._projectLoading = true;
break;
case EventName.projectLoadingFinish:
case EventName.semanticDiag:
case EventName.syntaxDiag:
case EventName.suggestionDiag:
case EventName.configFileDiag:
this._projectLoading = false;
break;
}
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onExit(e => {
this._onExit.fire(e);
this.syntaxServer.kill();
}));
this._register(this.semanticServer.onError(e => this._onError.fire(e)));
}
private get projectLoading() { return this._projectLoading; }
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
public kill(): void {
this.syntaxServer.kill();
this.semanticServer.kill();
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
return this.router.execute(command, args, executeInfo);
}
}
namespace RequestState {
export const enum Type { Unresolved, Resolved, Errored }
export const Unresolved = { type: Type.Unresolved } as const;
export const Resolved = { type: Type.Resolved } as const;
export class Errored {
readonly type = Type.Errored;
constructor(
public readonly err: Error
) { }
}
export type State = typeof Unresolved | typeof Resolved | Errored;
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { TypeScriptVersion } from './versionProvider';
export class TypeScriptServerError extends Error {
public static create(
serverId: string,
version: TypeScriptVersion,
response: Proto.Response
): TypeScriptServerError {
const parsedResult = TypeScriptServerError.parseErrorText(response);
return new TypeScriptServerError(serverId, version, response, parsedResult?.message, parsedResult?.stack, parsedResult?.sanitizedStack);
}
private constructor(
public readonly serverId: string,
public readonly version: TypeScriptVersion,
private readonly response: Proto.Response,
public readonly serverMessage: string | undefined,
public readonly serverStack: string | undefined,
private readonly sanitizedStack: string | undefined
) {
super(`<${serverId}> TypeScript Server Error (${version.displayName})\n${serverMessage}\n${serverStack}`);
}
public get serverErrorText() { return this.response.message; }
public get serverCommand() { return this.response.command; }
public get telemetry() {
// The "sanitizedstack" has been purged of error messages, paths, and file names (other than tsserver)
// and, thus, can be classified as SystemMetaData, rather than CallstackOrException.
/* __GDPR__FRAGMENT__
"TypeScriptRequestErrorProperties" : {
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"serverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
}
*/
return {
command: this.serverCommand,
serverid: this.serverId,
sanitizedstack: this.sanitizedStack || '',
} as const;
}
/**
* Given a `errorText` from a tsserver request indicating failure in handling a request,
* prepares a payload for telemetry-logging.
*/
private static parseErrorText(response: Proto.Response) {
const errorText = response.message;
if (errorText) {
const errorPrefix = 'Error processing request. ';
if (errorText.startsWith(errorPrefix)) {
const prefixFreeErrorText = errorText.substr(errorPrefix.length);
const newlineIndex = prefixFreeErrorText.indexOf('\n');
if (newlineIndex >= 0) {
// Newline expected between message and stack.
const stack = prefixFreeErrorText.substring(newlineIndex + 1);
return {
message: prefixFreeErrorText.substring(0, newlineIndex),
stack,
sanitizedStack: TypeScriptServerError.sanitizeStack(stack)
};
}
}
}
return undefined;
}
/**
* Drop everything but ".js" and line/column numbers (though retain "tsserver" if that's the filename).
*/
private static sanitizeStack(message: string | undefined) {
if (!message) {
return '';
}
const regex = /(\btsserver)?(\.(?:ts|tsx|js|jsx)(?::\d+(?::\d+)?)?)\)?$/igm;
let serverStack = '';
while (true) {
const match = regex.exec(message);
if (!match) {
break;
}
// [1] is 'tsserver' or undefined
// [2] is '.js:{line_number}:{column_number}'
serverStack += `${match[1] || 'suppressed'}${match[2]}\n`;
}
return serverStack;
}
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { TsServerProcess, TsServerProcessKind } from './server';
declare const Worker: any;
declare type Worker = any;
export class WorkerServerProcess implements TsServerProcess {
public static fork(
tsServerPath: string,
args: readonly string[],
_kind: TsServerProcessKind,
_configuration: TypeScriptServiceConfiguration,
) {
const worker = new Worker(tsServerPath);
return new WorkerServerProcess(worker, [
...args,
// Explicitly give TS Server its path so it can
// load local resources
'--executingFilePath', tsServerPath,
]);
}
private _onDataHandlers = new Set<(data: Proto.Response) => void>();
private _onErrorHandlers = new Set<(err: Error) => void>();
private _onExitHandlers = new Set<(code: number | null) => void>();
public constructor(
private readonly worker: Worker,
args: readonly string[],
) {
worker.addEventListener('message', (msg: any) => {
for (const handler of this._onDataHandlers) {
handler(msg.data);
}
});
worker.postMessage(args);
}
write(serverRequest: Proto.Request): void {
this.worker.postMessage(serverRequest);
}
onData(handler: (response: Proto.Response) => void): void {
this._onDataHandlers.add(handler);
}
onError(handler: (err: Error) => void): void {
this._onErrorHandlers.add(handler);
// Todo: not implemented
}
onExit(handler: (code: number | null) => void): void {
this._onExitHandlers.add(handler);
// Todo: not implemented
}
kill(): void {
this.worker.terminate();
}
}

View File

@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import type { Readable } from 'stream';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { TsServerProcess, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
const localize = nls.loadMessageBundle();
const defaultSize: number = 8192;
const contentLength: string = 'Content-Length: ';
const contentLengthSize: number = Buffer.byteLength(contentLength, 'utf8');
const blank: number = Buffer.from(' ', 'utf8')[0];
const backslashR: number = Buffer.from('\r', 'utf8')[0];
const backslashN: number = Buffer.from('\n', 'utf8')[0];
class ProtocolBuffer {
private index: number = 0;
private buffer: Buffer = Buffer.allocUnsafe(defaultSize);
public append(data: string | Buffer): void {
let toAppend: Buffer | null = null;
if (Buffer.isBuffer(data)) {
toAppend = data;
} else {
toAppend = Buffer.from(data, 'utf8');
}
if (this.buffer.length - this.index >= toAppend.length) {
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
} else {
let newSize = (Math.ceil((this.index + toAppend.length) / defaultSize) + 1) * defaultSize;
if (this.index === 0) {
this.buffer = Buffer.allocUnsafe(newSize);
toAppend.copy(this.buffer, 0, 0, toAppend.length);
} else {
this.buffer = Buffer.concat([this.buffer.slice(0, this.index), toAppend], newSize);
}
}
this.index += toAppend.length;
}
public tryReadContentLength(): number {
let result = -1;
let current = 0;
// we are utf8 encoding...
while (current < this.index && (this.buffer[current] === blank || this.buffer[current] === backslashR || this.buffer[current] === backslashN)) {
current++;
}
if (this.index < current + contentLengthSize) {
return result;
}
current += contentLengthSize;
let start = current;
while (current < this.index && this.buffer[current] !== backslashR) {
current++;
}
if (current + 3 >= this.index || this.buffer[current + 1] !== backslashN || this.buffer[current + 2] !== backslashR || this.buffer[current + 3] !== backslashN) {
return result;
}
let data = this.buffer.toString('utf8', start, current);
result = parseInt(data);
this.buffer = this.buffer.slice(current + 4);
this.index = this.index - (current + 4);
return result;
}
public tryReadContent(length: number): string | null {
if (this.index < length) {
return null;
}
let result = this.buffer.toString('utf8', 0, length);
let sourceStart = length;
while (sourceStart < this.index && (this.buffer[sourceStart] === backslashR || this.buffer[sourceStart] === backslashN)) {
sourceStart++;
}
this.buffer.copy(this.buffer, 0, sourceStart);
this.index = this.index - sourceStart;
return result;
}
}
class Reader<T> extends Disposable {
private readonly buffer: ProtocolBuffer = new ProtocolBuffer();
private nextMessageLength: number = -1;
public constructor(readable: Readable) {
super();
readable.on('data', data => this.onLengthData(data));
}
private readonly _onError = this._register(new vscode.EventEmitter<Error>());
public readonly onError = this._onError.event;
private readonly _onData = this._register(new vscode.EventEmitter<T>());
public readonly onData = this._onData.event;
private onLengthData(data: Buffer | string): void {
if (this.isDisposed) {
return;
}
try {
this.buffer.append(data);
while (true) {
if (this.nextMessageLength === -1) {
this.nextMessageLength = this.buffer.tryReadContentLength();
if (this.nextMessageLength === -1) {
return;
}
}
const msg = this.buffer.tryReadContent(this.nextMessageLength);
if (msg === null) {
return;
}
this.nextMessageLength = -1;
const json = JSON.parse(msg);
this._onData.fire(json);
}
} catch (e) {
this._onError.fire(e);
}
}
}
export class ChildServerProcess extends Disposable implements TsServerProcess {
private readonly _reader: Reader<Proto.Response>;
public static fork(
tsServerPath: string,
args: readonly string[],
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
): ChildServerProcess {
if (!fs.existsSync(tsServerPath)) {
vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', tsServerPath));
versionManager.reset();
tsServerPath = versionManager.currentVersion.tsServerPath;
}
const childProcess = child_process.fork(tsServerPath, args, {
silent: true,
cwd: undefined,
env: this.generatePatchedEnv(process.env, tsServerPath),
execArgv: this.getExecArgv(kind, configuration),
});
return new ChildServerProcess(childProcess);
}
private static generatePatchedEnv(env: any, modulePath: string): any {
const newEnv = Object.assign({}, env);
newEnv['ELECTRON_RUN_AS_NODE'] = '1';
newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');
// Ensure we always have a PATH set
newEnv['PATH'] = newEnv['PATH'] || process.env.PATH;
return newEnv;
}
private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): string[] {
const args: string[] = [];
const debugPort = this.getDebugPort(kind);
if (debugPort) {
const inspectFlag = ChildServerProcess.getTssDebugBrk() ? '--inspect-brk' : '--inspect';
args.push(`${inspectFlag}=${debugPort}`);
}
if (configuration.maxTsServerMemory) {
args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`);
}
return args;
}
private static getDebugPort(kind: TsServerProcessKind): number | undefined {
if (kind === TsServerProcessKind.Syntax) {
// We typically only want to debug the main semantic server
return undefined;
}
const value = ChildServerProcess.getTssDebugBrk() || ChildServerProcess.getTssDebug();
if (value) {
const port = parseInt(value);
if (!isNaN(port)) {
return port;
}
}
return undefined;
}
private static getTssDebug(): string | undefined {
return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG' : 'TSS_DEBUG'];
}
private static getTssDebugBrk(): string | undefined {
return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'];
}
private constructor(
private readonly _process: child_process.ChildProcess,
) {
super();
this._reader = this._register(new Reader<Proto.Response>(this._process.stdout!));
}
write(serverRequest: Proto.Request): void {
this._process.stdin!.write(JSON.stringify(serverRequest) + '\r\n', 'utf8');
}
onData(handler: (data: Proto.Response) => void): void {
this._reader.onData(handler);
}
onExit(handler: (code: number | null) => void): void {
this._process.on('exit', handler);
}
onError(handler: (err: Error) => void): void {
this._process.on('error', handler);
this._reader.onError(handler);
}
kill(): void {
this._process.kill();
this._reader.dispose();
}
}

View File

@@ -0,0 +1,267 @@
/*---------------------------------------------------------------------------------------------
* 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 'path';
import * as vscode from 'vscode';
import { OngoingRequestCancellerFactory } from '../tsServer/cancellation';
import { ClientCapabilities, ClientCapability, ServerType } from '../typescriptService';
import API from '../utils/api';
import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration';
import { Logger } from '../utils/logger';
import { isWeb } from '../utils/platform';
import { TypeScriptPluginPathsProvider } from '../utils/pluginPathsProvider';
import { PluginManager } from '../utils/plugins';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { ILogDirectoryProvider } from './logDirectoryProvider';
import { GetErrRoutingTsServer, ITypeScriptServer, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerProcessFactory, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
const enum CompositeServerType {
/** Run a single server that handles all commands */
Single,
/** Run a separate server for syntax commands */
SeparateSyntax,
/** Use a separate syntax server while the project is loading */
DynamicSeparateSyntax,
/** Only enable the syntax server */
SyntaxOnly
}
export class TypeScriptServerSpawner {
public constructor(
private readonly _versionProvider: ITypeScriptVersionProvider,
private readonly _versionManager: TypeScriptVersionManager,
private readonly _logDirectoryProvider: ILogDirectoryProvider,
private readonly _pluginPathsProvider: TypeScriptPluginPathsProvider,
private readonly _logger: Logger,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _tracer: Tracer,
private readonly _factory: TsServerProcessFactory,
) { }
public spawn(
version: TypeScriptVersion,
capabilities: ClientCapabilities,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager,
cancellerFactory: OngoingRequestCancellerFactory,
delegate: TsServerDelegate,
): ITypeScriptServer {
let primaryServer: ITypeScriptServer;
const serverType = this.getCompositeServerType(version, capabilities, configuration);
switch (serverType) {
case CompositeServerType.SeparateSyntax:
case CompositeServerType.DynamicSeparateSyntax:
{
const enableDynamicRouting = serverType === CompositeServerType.DynamicSeparateSyntax;
primaryServer = new SyntaxRoutingTsServer({
syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory),
semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration, pluginManager, cancellerFactory),
}, delegate, enableDynamicRouting);
break;
}
case CompositeServerType.Single:
{
primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration, pluginManager, cancellerFactory);
break;
}
case CompositeServerType.SyntaxOnly:
{
primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory);
break;
}
}
if (this.shouldUseSeparateDiagnosticsServer(configuration)) {
return new GetErrRoutingTsServer({
getErr: this.spawnTsServer(TsServerProcessKind.Diagnostics, version, configuration, pluginManager, cancellerFactory),
primary: primaryServer,
}, delegate);
}
return primaryServer;
}
private getCompositeServerType(
version: TypeScriptVersion,
capabilities: ClientCapabilities,
configuration: TypeScriptServiceConfiguration,
): CompositeServerType {
if (!capabilities.has(ClientCapability.Semantic)) {
return CompositeServerType.SyntaxOnly;
}
switch (configuration.separateSyntaxServer) {
case SeparateSyntaxServerConfiguration.Disabled:
return CompositeServerType.Single;
case SeparateSyntaxServerConfiguration.Enabled:
if (version.apiVersion?.gte(API.v340)) {
return version.apiVersion?.gte(API.v400)
? CompositeServerType.DynamicSeparateSyntax
: CompositeServerType.SeparateSyntax;
}
return CompositeServerType.Single;
}
}
private shouldUseSeparateDiagnosticsServer(
configuration: TypeScriptServiceConfiguration,
): boolean {
return configuration.enableProjectDiagnostics;
}
private spawnTsServer(
kind: TsServerProcessKind,
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager,
cancellerFactory: OngoingRequestCancellerFactory,
): ITypeScriptServer {
const apiVersion = version.apiVersion || API.defaultVersion;
const canceller = cancellerFactory.create(kind, this._tracer);
const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, version, apiVersion, pluginManager, canceller.cancellationPipeName);
if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) {
if (tsServerLogFile) {
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
} else {
this._logger.error(`<${kind}> Could not create log directory`);
}
}
this._logger.info(`<${kind}> Forking...`);
const process = this._factory.fork(version.tsServerPath, args, kind, configuration, this._versionManager);
this._logger.info(`<${kind}> Starting...`);
return new ProcessBasedTsServer(
kind,
this.kindToServerType(kind),
process!,
tsServerLogFile,
canceller,
version,
this._telemetryReporter,
this._tracer);
}
private kindToServerType(kind: TsServerProcessKind): ServerType {
switch (kind) {
case TsServerProcessKind.Syntax:
return ServerType.Syntax;
case TsServerProcessKind.Main:
case TsServerProcessKind.Semantic:
case TsServerProcessKind.Diagnostics:
default:
return ServerType.Semantic;
}
}
private getTsServerArgs(
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
currentVersion: TypeScriptVersion,
apiVersion: API,
pluginManager: PluginManager,
cancellationPipeName: string | undefined,
): { args: string[], tsServerLogFile: string | undefined } {
const args: string[] = [];
let tsServerLogFile: string | undefined;
if (kind === TsServerProcessKind.Syntax) {
if (apiVersion.gte(API.v401)) {
args.push('--serverMode', 'partialSemantic');
} else {
args.push('--syntaxOnly');
}
}
if (apiVersion.gte(API.v250)) {
args.push('--useInferredProjectPerProjectRoot');
} else {
args.push('--useSingleInferredProject');
}
if (configuration.disableAutomaticTypeAcquisition || kind === TsServerProcessKind.Syntax || kind === TsServerProcessKind.Diagnostics) {
args.push('--disableAutomaticTypingAcquisition');
}
if (kind === TsServerProcessKind.Semantic || kind === TsServerProcessKind.Main) {
args.push('--enableTelemetry');
}
if (cancellationPipeName) {
args.push('--cancellationPipeName', cancellationPipeName + '*');
}
if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) {
if (isWeb()) {
args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel));
} else {
const logDir = this._logDirectoryProvider.getNewLogDirectory();
if (logDir) {
tsServerLogFile = path.join(logDir, `tsserver.log`);
args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel));
args.push('--logFile', tsServerLogFile);
}
}
}
if (!isWeb()) {
const pluginPaths = this._pluginPathsProvider.getPluginPaths();
if (pluginManager.plugins.length) {
args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(','));
const isUsingBundledTypeScriptVersion = currentVersion.path === this._versionProvider.defaultVersion.path;
for (const plugin of pluginManager.plugins) {
if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) {
pluginPaths.push(plugin.path);
}
}
}
if (pluginPaths.length !== 0) {
args.push('--pluginProbeLocations', pluginPaths.join(','));
}
}
if (configuration.npmLocation) {
args.push('--npmLocation', `"${configuration.npmLocation}"`);
}
if (apiVersion.gte(API.v260)) {
args.push('--locale', TypeScriptServerSpawner.getTsLocale(configuration));
}
if (apiVersion.gte(API.v291)) {
args.push('--noGetErrOnBackgroundUpdate');
}
if (apiVersion.gte(API.v345)) {
args.push('--validateDefaultNpmLocation');
}
return { args, tsServerLogFile };
}
private static isLoggingEnabled(configuration: TypeScriptServiceConfiguration) {
return configuration.tsServerLogLevel !== TsServerLogLevel.Off;
}
private static getTsLocale(configuration: TypeScriptServiceConfiguration): string {
return configuration.locale
? configuration.locale
: vscode.env.language;
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
const localize = nls.loadMessageBundle();
const useWorkspaceTsdkStorageKey = 'typescript.useWorkspaceTsdk';
const suppressPromptWorkspaceTsdkStorageKey = 'typescript.suppressPromptWorkspaceTsdk';
interface QuickPickItem extends vscode.QuickPickItem {
run(): void;
}
export class TypeScriptVersionManager extends Disposable {
private _currentVersion: TypeScriptVersion;
public constructor(
private configuration: TypeScriptServiceConfiguration,
private readonly versionProvider: ITypeScriptVersionProvider,
private readonly workspaceState: vscode.Memento
) {
super();
this._currentVersion = this.versionProvider.defaultVersion;
if (this.useWorkspaceTsdkSetting) {
const localVersion = this.versionProvider.localVersion;
if (localVersion) {
this._currentVersion = localVersion;
}
}
if (this.isInPromptWorkspaceTsdkState(configuration)) {
setImmediate(() => {
this.promptUseWorkspaceTsdk();
});
}
}
private readonly _onDidPickNewVersion = this._register(new vscode.EventEmitter<void>());
public readonly onDidPickNewVersion = this._onDidPickNewVersion.event;
public updateConfiguration(nextConfiguration: TypeScriptServiceConfiguration) {
const lastConfiguration = this.configuration;
this.configuration = nextConfiguration;
if (
!this.isInPromptWorkspaceTsdkState(lastConfiguration)
&& this.isInPromptWorkspaceTsdkState(nextConfiguration)
) {
this.promptUseWorkspaceTsdk();
}
}
public get currentVersion(): TypeScriptVersion {
return this._currentVersion;
}
public reset(): void {
this._currentVersion = this.versionProvider.bundledVersion;
}
public async promptUserForVersion(): Promise<void> {
const selected = await vscode.window.showQuickPick<QuickPickItem>([
this.getBundledPickItem(),
...this.getLocalPickItems(),
LearnMorePickItem,
], {
placeHolder: localize(
'selectTsVersion',
"Select the TypeScript version used for JavaScript and TypeScript language features"),
});
return selected?.run();
}
private getBundledPickItem(): QuickPickItem {
const bundledVersion = this.versionProvider.defaultVersion;
return {
label: (!this.useWorkspaceTsdkSetting
? '• '
: '') + localize('useVSCodeVersionOption', "Use VS Code's Version"),
description: bundledVersion.displayName,
detail: bundledVersion.pathLabel,
run: async () => {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, false);
this.updateActiveVersion(bundledVersion);
},
};
}
private getLocalPickItems(): QuickPickItem[] {
return this.versionProvider.localVersions.map(version => {
return {
label: (this.useWorkspaceTsdkSetting && this.currentVersion.eq(version)
? '• '
: '') + localize('useWorkspaceVersionOption', "Use Workspace Version"),
description: version.displayName,
detail: version.pathLabel,
run: async () => {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
const tsConfig = vscode.workspace.getConfiguration('typescript');
await tsConfig.update('tsdk', version.pathLabel, false);
this.updateActiveVersion(version);
},
};
});
}
private async promptUseWorkspaceTsdk(): Promise<void> {
const workspaceVersion = this.versionProvider.localVersion;
if (workspaceVersion === undefined) {
throw new Error('Could not prompt to use workspace TypeScript version because no workspace version is specified');
}
const allowIt = localize('allow', 'Allow');
const dismissPrompt = localize('dismiss', 'Dismiss');
const suppressPrompt = localize('suppress prompt', 'Never in this Workspace');
const result = await vscode.window.showInformationMessage(localize('promptUseWorkspaceTsdk', 'This workspace contains a TypeScript version. Would you like to use the workspace TypeScript version for TypeScript and JavaScript language features?'),
allowIt,
dismissPrompt,
suppressPrompt
);
if (result === allowIt) {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
this.updateActiveVersion(workspaceVersion);
} else if (result === suppressPrompt) {
await this.workspaceState.update(suppressPromptWorkspaceTsdkStorageKey, true);
}
}
private updateActiveVersion(pickedVersion: TypeScriptVersion) {
const oldVersion = this.currentVersion;
this._currentVersion = pickedVersion;
if (!oldVersion.eq(pickedVersion)) {
this._onDidPickNewVersion.fire();
}
}
private get useWorkspaceTsdkSetting(): boolean {
return this.workspaceState.get<boolean>(useWorkspaceTsdkStorageKey, false);
}
private get suppressPromptWorkspaceTsdkSetting(): boolean {
return this.workspaceState.get<boolean>(suppressPromptWorkspaceTsdkStorageKey, false);
}
private isInPromptWorkspaceTsdkState(configuration: TypeScriptServiceConfiguration) {
return (
configuration.localTsdk !== null
&& configuration.enablePromptUseWorkspaceTsdk === true
&& this.suppressPromptWorkspaceTsdkSetting === false
&& this.useWorkspaceTsdkSetting === false
);
}
}
const LearnMorePickItem: QuickPickItem = {
label: localize('learnMore', 'Learn more about managing TypeScript versions'),
description: '',
run: () => {
vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919'));
}
};

View File

@@ -0,0 +1,198 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import * as vscode from 'vscode';
import API from '../utils/api';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver';
import { ITypeScriptVersionProvider, localize, TypeScriptVersion, TypeScriptVersionSource } from './versionProvider';
export class DiskTypeScriptVersionProvider implements ITypeScriptVersionProvider {
public constructor(
private configuration?: TypeScriptServiceConfiguration
) { }
public updateConfiguration(configuration: TypeScriptServiceConfiguration): void {
this.configuration = configuration;
}
public get defaultVersion(): TypeScriptVersion {
return this.globalVersion || this.bundledVersion;
}
public get globalVersion(): TypeScriptVersion | undefined {
if (this.configuration?.globalTsdk) {
const globals = this.loadVersionsFromSetting(TypeScriptVersionSource.UserSetting, this.configuration.globalTsdk);
if (globals && globals.length) {
return globals[0];
}
}
return this.contributedTsNextVersion;
}
public get localVersion(): TypeScriptVersion | undefined {
const tsdkVersions = this.localTsdkVersions;
if (tsdkVersions && tsdkVersions.length) {
return tsdkVersions[0];
}
const nodeVersions = this.localNodeModulesVersions;
if (nodeVersions && nodeVersions.length === 1) {
return nodeVersions[0];
}
return undefined;
}
public get localVersions(): TypeScriptVersion[] {
const allVersions = this.localTsdkVersions.concat(this.localNodeModulesVersions);
const paths = new Set<string>();
return allVersions.filter(x => {
if (paths.has(x.path)) {
return false;
}
paths.add(x.path);
return true;
});
}
public get bundledVersion(): TypeScriptVersion {
const version = this.getContributedVersion(TypeScriptVersionSource.Bundled, 'vscode.typescript-language-features', ['..', 'node_modules']);
if (version) {
return version;
}
vscode.window.showErrorMessage(localize(
'noBundledServerFound',
'VS Code\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.'));
throw new Error('Could not find bundled tsserver.js');
}
private get contributedTsNextVersion(): TypeScriptVersion | undefined {
return this.getContributedVersion(TypeScriptVersionSource.TsNightlyExtension, 'ms-vscode.vscode-typescript-next', ['node_modules']);
}
private getContributedVersion(source: TypeScriptVersionSource, extensionId: string, pathToTs: readonly string[]): TypeScriptVersion | undefined {
try {
const extension = vscode.extensions.getExtension(extensionId);
if (extension) {
const serverPath = path.join(extension.extensionPath, ...pathToTs, 'typescript', 'lib', 'tsserver.js');
const bundledVersion = new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), '');
if (bundledVersion.isValid) {
return bundledVersion;
}
}
} catch {
// noop
}
return undefined;
}
private get localTsdkVersions(): TypeScriptVersion[] {
const localTsdk = this.configuration?.localTsdk;
return localTsdk ? this.loadVersionsFromSetting(TypeScriptVersionSource.WorkspaceSetting, localTsdk) : [];
}
private loadVersionsFromSetting(source: TypeScriptVersionSource, tsdkPathSetting: string): TypeScriptVersion[] {
if (path.isAbsolute(tsdkPathSetting)) {
const serverPath = path.join(tsdkPathSetting, 'tsserver.js');
return [
new TypeScriptVersion(source,
serverPath,
DiskTypeScriptVersionProvider.getApiVersion(serverPath),
tsdkPathSetting)
];
}
const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting);
if (workspacePath !== undefined) {
const serverPath = path.join(workspacePath, 'tsserver.js');
return [
new TypeScriptVersion(source,
serverPath,
DiskTypeScriptVersionProvider.getApiVersion(serverPath),
tsdkPathSetting)
];
}
return this.loadTypeScriptVersionsFromPath(source, tsdkPathSetting);
}
private get localNodeModulesVersions(): TypeScriptVersion[] {
return this.loadTypeScriptVersionsFromPath(TypeScriptVersionSource.NodeModules, path.join('node_modules', 'typescript', 'lib'))
.filter(x => x.isValid);
}
private loadTypeScriptVersionsFromPath(source: TypeScriptVersionSource, relativePath: string): TypeScriptVersion[] {
if (!vscode.workspace.workspaceFolders) {
return [];
}
const versions: TypeScriptVersion[] = [];
for (const root of vscode.workspace.workspaceFolders) {
let label: string = relativePath;
if (vscode.workspace.workspaceFolders.length > 1) {
label = path.join(root.name, relativePath);
}
const serverPath = path.join(root.uri.fsPath, relativePath, 'tsserver.js');
versions.push(new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), label));
}
return versions;
}
private static getApiVersion(serverPath: string): API | undefined {
const version = DiskTypeScriptVersionProvider.getTypeScriptVersion(serverPath);
if (version) {
return version;
}
// Allow TS developers to provide custom version
const tsdkVersion = vscode.workspace.getConfiguration().get<string | undefined>('typescript.tsdk_version', undefined);
if (tsdkVersion) {
return API.fromVersionString(tsdkVersion);
}
return undefined;
}
private static getTypeScriptVersion(serverPath: string): API | undefined {
if (!fs.existsSync(serverPath)) {
return undefined;
}
const p = serverPath.split(path.sep);
if (p.length <= 2) {
return undefined;
}
const p2 = p.slice(0, -2);
const modulePath = p2.join(path.sep);
let fileName = path.join(modulePath, 'package.json');
if (!fs.existsSync(fileName)) {
// Special case for ts dev versions
if (path.basename(modulePath) === 'built') {
fileName = path.join(modulePath, '..', 'package.json');
}
}
if (!fs.existsSync(fileName)) {
return undefined;
}
const contents = fs.readFileSync(fileName).toString();
let desc: any = null;
try {
desc = JSON.parse(contents);
} catch (err) {
return undefined;
}
if (!desc || !desc.version) {
return undefined;
}
return desc.version ? API.fromVersionString(desc.version) : undefined;
}
}

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import API from '../utils/api';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
export const localize = nls.loadMessageBundle();
export const enum TypeScriptVersionSource {
Bundled = 'bundled',
TsNightlyExtension = 'ts-nightly-extension',
NodeModules = 'node-modules',
UserSetting = 'user-setting',
WorkspaceSetting = 'workspace-setting',
}
export class TypeScriptVersion {
constructor(
public readonly source: TypeScriptVersionSource,
public readonly path: string,
public readonly apiVersion: API | undefined,
private readonly _pathLabel?: string,
) { }
public get tsServerPath(): string {
return this.path;
}
public get pathLabel(): string {
return this._pathLabel ?? this.path;
}
public get isValid(): boolean {
return this.apiVersion !== undefined;
}
public eq(other: TypeScriptVersion): boolean {
if (this.path !== other.path) {
return false;
}
if (this.apiVersion === other.apiVersion) {
return true;
}
if (!this.apiVersion || !other.apiVersion) {
return false;
}
return this.apiVersion.eq(other.apiVersion);
}
public get displayName(): string {
const version = this.apiVersion;
return version ? version.displayName : localize(
'couldNotLoadTsVersion', 'Could not load the TypeScript version at this path');
}
}
export interface ITypeScriptVersionProvider {
updateConfiguration(configuration: TypeScriptServiceConfiguration): void;
readonly defaultVersion: TypeScriptVersion;
readonly globalVersion: TypeScriptVersion | undefined;
readonly localVersion: TypeScriptVersion | undefined;
readonly localVersions: readonly TypeScriptVersion[];
readonly bundledVersion: TypeScriptVersion;
}

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import { ITypeScriptServiceClient } from '../typescriptService';
import { coalesce } from '../utils/arrays';
import { Disposable } from '../utils/dispose';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig';
import { TypeScriptVersion } from './versionProvider';
const localize = nls.loadMessageBundle();
namespace ProjectInfoState {
export const enum Type { None, Pending, Resolved }
export const None = Object.freeze({ type: Type.None } as const);
export class Pending {
public readonly type = Type.Pending;
public readonly cancellation = new vscode.CancellationTokenSource();
constructor(
public readonly resource: vscode.Uri,
) { }
}
export class Resolved {
public readonly type = Type.Resolved;
constructor(
public readonly resource: vscode.Uri,
public readonly configFile: string,
) { }
}
export type State = typeof None | Pending | Resolved;
}
interface QuickPickItem extends vscode.QuickPickItem {
run(): void;
}
class ProjectStatusCommand implements Command {
public readonly id = '_typescript.projectStatus';
public constructor(
private readonly _client: ITypeScriptServiceClient,
private readonly _delegate: () => ProjectInfoState.State,
) { }
public async execute(): Promise<void> {
const info = this._delegate();
const result = await vscode.window.showQuickPick<QuickPickItem>(coalesce([
this.getProjectItem(info),
this.getVersionItem(),
this.getHelpItem(),
]), {
placeHolder: localize('projectQuickPick.placeholder', "TypeScript Project Info"),
});
return result?.run();
}
private getVersionItem(): QuickPickItem {
return {
label: localize('projectQuickPick.version.label', "Select TypeScript Version..."),
description: this._client.apiVersion.displayName,
run: () => {
this._client.showVersionPicker();
}
};
}
private getProjectItem(info: ProjectInfoState.State): QuickPickItem | undefined {
const rootPath = info.type === ProjectInfoState.Type.Resolved ? this._client.getWorkspaceRootForResource(info.resource) : undefined;
if (!rootPath) {
return undefined;
}
if (info.type === ProjectInfoState.Type.Resolved) {
if (isImplicitProjectConfigFile(info.configFile)) {
return {
label: localize('projectQuickPick.project.create', "Create tsconfig"),
detail: localize('projectQuickPick.project.create.description', "This file is currently not part of a tsconfig/jsconfig project"),
run: () => {
openOrCreateConfig(ProjectType.TypeScript, rootPath, this._client.configuration);
}
};
}
}
return {
label: localize('projectQuickPick.version.goProjectConfig', "Open tsconfig"),
description: info.type === ProjectInfoState.Type.Resolved ? vscode.workspace.asRelativePath(info.configFile) : undefined,
run: () => {
if (info.type === ProjectInfoState.Type.Resolved) {
openProjectConfigOrPromptToCreate(ProjectType.TypeScript, this._client, rootPath, info.configFile);
} else if (info.type === ProjectInfoState.Type.Pending) {
openProjectConfigForFile(ProjectType.TypeScript, this._client, info.resource);
}
}
};
}
private getHelpItem(): QuickPickItem {
return {
label: localize('projectQuickPick.help', "TypeScript help"),
run: () => {
vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919')); // TODO:
}
};
}
}
export default class VersionStatus extends Disposable {
private readonly _statusBarEntry: vscode.StatusBarItem;
private _ready = false;
private _state: ProjectInfoState.State = ProjectInfoState.None;
constructor(
private readonly _client: ITypeScriptServiceClient,
commandManager: CommandManager,
) {
super();
this._statusBarEntry = this._register(vscode.window.createStatusBarItem({
id: 'status.typescript',
name: localize('projectInfo.name', "TypeScript: Project Info"),
alignment: vscode.StatusBarAlignment.Right,
priority: 99 /* to the right of editor status (100) */
}));
const command = new ProjectStatusCommand(this._client, () => this._state);
commandManager.register(command);
this._statusBarEntry.command = command.id;
vscode.window.onDidChangeActiveTextEditor(this.updateStatus, this, this._disposables);
this._client.onReady(() => {
this._ready = true;
this.updateStatus();
});
this._register(this._client.onTsServerStarted(({ version }) => this.onDidChangeTypeScriptVersion(version)));
}
private onDidChangeTypeScriptVersion(version: TypeScriptVersion) {
this._statusBarEntry.text = version.displayName;
this._statusBarEntry.tooltip = version.path;
this.updateStatus();
}
private async updateStatus() {
if (!vscode.window.activeTextEditor) {
this.hide();
return;
}
const doc = vscode.window.activeTextEditor.document;
if (isTypeScriptDocument(doc)) {
const file = this._client.normalizedPath(doc.uri);
if (file) {
this._statusBarEntry.show();
if (!this._ready) {
return;
}
const pendingState = new ProjectInfoState.Pending(doc.uri);
this.updateState(pendingState);
const response = await this._client.execute('projectInfo', { file, needFileNameList: false }, pendingState.cancellation.token);
if (response.type === 'response' && response.body) {
if (this._state === pendingState) {
this.updateState(new ProjectInfoState.Resolved(doc.uri, response.body.configFileName));
this._statusBarEntry.show();
}
}
return;
}
}
if (!vscode.window.activeTextEditor.viewColumn) {
// viewColumn is undefined for the debug/output panel, but we still want
// to show the version info in the existing editor
return;
}
this.hide();
}
private hide(): void {
this._statusBarEntry.hide();
this.updateState(ProjectInfoState.None);
}
private updateState(newState: ProjectInfoState.State): void {
if (this._state === newState) {
return;
}
if (this._state.type === ProjectInfoState.Type.Pending) {
this._state.cancellation.cancel();
this._state.cancellation.dispose();
}
this._state = newState;
}
}

View File

@@ -0,0 +1,318 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from typescript-sublime-plugin project, obtained from
* https://github.com/microsoft/TypeScript-Sublime-Plugin/blob/master/TypeScript%20Indent.tmPreferences
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { DiagnosticKind } from './languageFeatures/diagnostics';
import FileConfigurationManager from './languageFeatures/fileConfigurationManager';
import LanguageProvider from './languageProvider';
import * as Proto from './protocol';
import * as PConst from './protocol.const';
import { OngoingRequestCancellerFactory } from './tsServer/cancellation';
import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { TsServerProcessFactory } from './tsServer/server';
import { ITypeScriptVersionProvider } from './tsServer/versionProvider';
import VersionStatus from './tsServer/versionStatus';
import TypeScriptServiceClient from './typescriptServiceClient';
import { coalesce, flatten } from './utils/arrays';
import { CommandManager } from './commands/commandManager';
import { Disposable } from './utils/dispose';
import * as errorCodes from './utils/errorCodes';
import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription';
import { PluginManager } from './utils/plugins';
import * as typeConverters from './utils/typeConverters';
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
import * as ProjectStatus from './utils/largeProjectStatus';
namespace Experimental {
export interface Diagnostic extends Proto.Diagnostic {
readonly reportsDeprecated?: {}
}
}
// Style check diagnostics that can be reported as warnings
const styleCheckDiagnostics = new Set([
...errorCodes.variableDeclaredButNeverUsed,
...errorCodes.propertyDeclaretedButNeverUsed,
...errorCodes.allImportsAreUnused,
...errorCodes.unreachableCode,
...errorCodes.unusedLabel,
...errorCodes.fallThroughCaseInSwitch,
...errorCodes.notAllCodePathsReturnAValue,
]);
export default class TypeScriptServiceClientHost extends Disposable {
private readonly client: TypeScriptServiceClient;
private readonly languages: LanguageProvider[] = [];
private readonly languagePerId = new Map<string, LanguageProvider>();
private readonly typingsStatus: TypingsStatus;
private readonly fileConfigurationManager: FileConfigurationManager;
private reportStyleCheckAsWarnings: boolean = true;
private readonly commandManager: CommandManager;
constructor(
descriptions: LanguageDescription[],
workspaceState: vscode.Memento,
onCaseInsenitiveFileSystem: boolean,
services: {
pluginManager: PluginManager,
commandManager: CommandManager,
logDirectoryProvider: ILogDirectoryProvider,
cancellerFactory: OngoingRequestCancellerFactory,
versionProvider: ITypeScriptVersionProvider,
processFactory: TsServerProcessFactory,
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {
super();
this.commandManager = services.commandManager;
const allModeIds = this.getAllModeIds(descriptions, services.pluginManager);
this.client = this._register(new TypeScriptServiceClient(
workspaceState,
onCaseInsenitiveFileSystem,
services,
allModeIds));
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
this.diagnosticsReceived(kind, resource, diagnostics);
}, null, this._disposables);
this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables);
this.client.onResendModelsRequested(() => this.populateService(), null, this._disposables);
this._register(new VersionStatus(this.client, services.commandManager));
this._register(new AtaProgressReporter(this.client));
this.typingsStatus = this._register(new TypingsStatus(this.client));
this._register(ProjectStatus.create(this.client));
this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client, onCaseInsenitiveFileSystem));
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted);
this.languages.push(manager);
this._register(manager);
this.languagePerId.set(description.id, manager);
}
import('./languageFeatures/updatePathsOnRename').then(module =>
this._register(module.register(this.client, this.fileConfigurationManager, uri => this.handles(uri))));
import('./languageFeatures/workspaceSymbols').then(module =>
this._register(module.register(this.client, allModeIds)));
this.client.ensureServiceStarted();
this.client.onReady(() => {
const languages = new Set<string>();
for (const plugin of services.pluginManager.plugins) {
if (plugin.configNamespace && plugin.languages.length) {
this.registerExtensionLanguageProvider({
id: plugin.configNamespace,
modeIds: Array.from(plugin.languages),
diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript',
isExternal: true
}, onCompletionAccepted);
} else {
for (const language of plugin.languages) {
languages.add(language);
}
}
}
if (languages.size) {
this.registerExtensionLanguageProvider({
id: 'typescript-plugins',
modeIds: Array.from(languages.values()),
diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript',
isExternal: true
}, onCompletionAccepted);
}
});
this.client.onTsServerStarted(() => {
this.triggerAllDiagnostics();
});
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables);
this.configurationChanged();
}
private registerExtensionLanguageProvider(description: LanguageDescription, onCompletionAccepted: (item: vscode.CompletionItem) => void) {
const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted);
this.languages.push(manager);
this._register(manager);
this.languagePerId.set(description.id, manager);
}
private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) {
const allModeIds = flatten([
...descriptions.map(x => x.modeIds),
...pluginManager.plugins.map(x => x.languages)
]);
return allModeIds;
}
public get serviceClient(): TypeScriptServiceClient {
return this.client;
}
public reloadProjects(): void {
this.client.executeWithoutWaitingForResponse('reloadProjects', null);
this.triggerAllDiagnostics();
}
public async handles(resource: vscode.Uri): Promise<boolean> {
const provider = await this.findLanguage(resource);
if (provider) {
return true;
}
return this.client.bufferSyncSupport.handles(resource);
}
private configurationChanged(): void {
const typescriptConfig = vscode.workspace.getConfiguration('typescript');
this.reportStyleCheckAsWarnings = typescriptConfig.get('reportStyleChecksAsWarnings', true);
}
private async findLanguage(resource: vscode.Uri): Promise<LanguageProvider | undefined> {
try {
const doc = await vscode.workspace.openTextDocument(resource);
return this.languages.find(language => language.handles(resource, doc));
} catch {
return undefined;
}
}
private triggerAllDiagnostics() {
for (const language of this.languagePerId.values()) {
language.triggerAllDiagnostics();
}
}
private populateService(): void {
this.fileConfigurationManager.reset();
for (const language of this.languagePerId.values()) {
language.reInitialize();
}
}
private async diagnosticsReceived(
kind: DiagnosticKind,
resource: vscode.Uri,
diagnostics: Proto.Diagnostic[]
): Promise<void> {
const language = await this.findLanguage(resource);
if (language) {
language.diagnosticsReceived(
kind,
resource,
this.createMarkerDatas(diagnostics, language.diagnosticSource));
}
}
private configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void {
// See https://github.com/microsoft/TypeScript/issues/10384
const body = event.body;
if (!body || !body.diagnostics || !body.configFile) {
return;
}
this.findLanguage(this.client.toResource(body.configFile)).then(language => {
if (!language) {
return;
}
language.configFileDiagnosticsReceived(this.client.toResource(body.configFile), body.diagnostics.map(tsDiag => {
const range = tsDiag.start && tsDiag.end ? typeConverters.Range.fromTextSpan(tsDiag) : new vscode.Range(0, 0, 0, 1);
const diagnostic = new vscode.Diagnostic(range, body.diagnostics[0].text, this.getDiagnosticSeverity(tsDiag));
diagnostic.source = language.diagnosticSource;
return diagnostic;
}));
});
}
private createMarkerDatas(
diagnostics: Proto.Diagnostic[],
source: string
): (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[] {
return diagnostics.map(tsDiag => this.tsDiagnosticToVsDiagnostic(tsDiag, source));
}
private tsDiagnosticToVsDiagnostic(diagnostic: Experimental.Diagnostic, source: string): vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any } {
const { start, end, text } = diagnostic;
const range = new vscode.Range(typeConverters.Position.fromLocation(start), typeConverters.Position.fromLocation(end));
const converted = new vscode.Diagnostic(range, text, this.getDiagnosticSeverity(diagnostic));
converted.source = diagnostic.source || source;
if (diagnostic.code) {
converted.code = diagnostic.code;
}
const relatedInformation = diagnostic.relatedInformation;
if (relatedInformation) {
converted.relatedInformation = coalesce(relatedInformation.map((info: any) => {
const span = info.span;
if (!span) {
return undefined;
}
return new vscode.DiagnosticRelatedInformation(typeConverters.Location.fromTextSpan(this.client.toResource(span.file), span), info.message);
}));
}
const tags: vscode.DiagnosticTag[] = [];
if (diagnostic.reportsUnnecessary) {
tags.push(vscode.DiagnosticTag.Unnecessary);
}
if (diagnostic.reportsDeprecated) {
tags.push(vscode.DiagnosticTag.Deprecated);
}
converted.tags = tags.length ? tags : undefined;
const resultConverted = converted as vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any };
resultConverted.reportUnnecessary = diagnostic.reportsUnnecessary;
resultConverted.reportDeprecated = diagnostic.reportsDeprecated;
return resultConverted;
}
private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): vscode.DiagnosticSeverity {
if (this.reportStyleCheckAsWarnings
&& this.isStyleCheckDiagnostic(diagnostic.code)
&& diagnostic.category === PConst.DiagnosticCategory.error
) {
return vscode.DiagnosticSeverity.Warning;
}
switch (diagnostic.category) {
case PConst.DiagnosticCategory.error:
return vscode.DiagnosticSeverity.Error;
case PConst.DiagnosticCategory.warning:
return vscode.DiagnosticSeverity.Warning;
case PConst.DiagnosticCategory.suggestion:
return vscode.DiagnosticSeverity.Hint;
default:
return vscode.DiagnosticSeverity.Error;
}
}
private isStyleCheckDiagnostic(code: number | undefined): boolean {
return typeof code === 'number' && styleCheckDiagnostics.has(code);
}
}

View File

@@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as Proto from './protocol';
import BufferSyncSupport from './tsServer/bufferSyncSupport';
import { ExectuionTarget } from './tsServer/server';
import { TypeScriptVersion } from './tsServer/versionProvider';
import API from './utils/api';
import { TypeScriptServiceConfiguration } from './utils/configuration';
import { PluginManager } from './utils/plugins';
import { TelemetryReporter } from './utils/telemetry';
export enum ServerType {
Syntax = 'syntax',
Semantic = 'semantic',
}
export namespace ServerResponse {
export class Cancelled {
public readonly type = 'cancelled';
constructor(
public readonly reason: string
) { }
}
export const NoContent = { type: 'noContent' } as const;
export type Response<T extends Proto.Response> = T | Cancelled | typeof NoContent;
}
interface StandardTsServerRequests {
'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse];
'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse];
'completionInfo': [Proto.CompletionsRequestArgs, Proto.CompletionInfoResponse];
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse];
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse];
'definition': [Proto.FileLocationRequestArgs, Proto.DefinitionResponse];
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanResponse];
'docCommentTemplate': [Proto.FileLocationRequestArgs, Proto.DocCommandTemplateResponse];
'documentHighlights': [Proto.DocumentHighlightsRequestArgs, Proto.DocumentHighlightsResponse];
'format': [Proto.FormatRequestArgs, Proto.FormatResponse];
'formatonkey': [Proto.FormatOnKeyRequestArgs, Proto.FormatResponse];
'getApplicableRefactors': [Proto.GetApplicableRefactorsRequestArgs, Proto.GetApplicableRefactorsResponse];
'getCodeFixes': [Proto.CodeFixRequestArgs, Proto.CodeFixResponse];
'getCombinedCodeFix': [Proto.GetCombinedCodeFixRequestArgs, Proto.GetCombinedCodeFixResponse];
'getEditsForFileRename': [Proto.GetEditsForFileRenameRequestArgs, Proto.GetEditsForFileRenameResponse];
'getEditsForRefactor': [Proto.GetEditsForRefactorRequestArgs, Proto.GetEditsForRefactorResponse];
'getOutliningSpans': [Proto.FileRequestArgs, Proto.OutliningSpansResponse];
'getSupportedCodeFixes': [null, Proto.GetSupportedCodeFixesResponse];
'implementation': [Proto.FileLocationRequestArgs, Proto.ImplementationResponse];
'jsxClosingTag': [Proto.JsxClosingTagRequestArgs, Proto.JsxClosingTagResponse];
'navto': [Proto.NavtoRequestArgs, Proto.NavtoResponse];
'navtree': [Proto.FileRequestArgs, Proto.NavTreeResponse];
'organizeImports': [Proto.OrganizeImportsRequestArgs, Proto.OrganizeImportsResponse];
'projectInfo': [Proto.ProjectInfoRequestArgs, Proto.ProjectInfoResponse];
'quickinfo': [Proto.FileLocationRequestArgs, Proto.QuickInfoResponse];
'references': [Proto.FileLocationRequestArgs, Proto.ReferencesResponse];
'rename': [Proto.RenameRequestArgs, Proto.RenameResponse];
'selectionRange': [Proto.SelectionRangeRequestArgs, Proto.SelectionRangeResponse];
'signatureHelp': [Proto.SignatureHelpRequestArgs, Proto.SignatureHelpResponse];
'typeDefinition': [Proto.FileLocationRequestArgs, Proto.TypeDefinitionResponse];
'updateOpen': [Proto.UpdateOpenRequestArgs, Proto.Response];
'prepareCallHierarchy': [Proto.FileLocationRequestArgs, Proto.PrepareCallHierarchyResponse];
'provideCallHierarchyIncomingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyIncomingCallsResponse];
'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse];
}
interface NoResponseTsServerRequests {
'open': [Proto.OpenRequestArgs, null];
'close': [Proto.FileRequestArgs, null];
'change': [Proto.ChangeRequestArgs, null];
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
'reloadProjects': [null, null];
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
}
interface AsyncTsServerRequests {
'geterr': [Proto.GeterrRequestArgs, Proto.Response];
'geterrForProject': [Proto.GeterrForProjectRequestArgs, Proto.Response];
}
export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
export type ExecConfig = {
readonly lowPriority?: boolean;
readonly nonRecoverable?: boolean;
readonly cancelOnResourceChange?: vscode.Uri;
readonly executionTarget?: ExectuionTarget;
};
export enum ClientCapability {
/**
* Basic syntax server. All clients should support this.
*/
Syntax,
/**
* Advanced syntax server that can provide single file IntelliSense.
*/
EnhancedSyntax,
/**
* Complete, multi-file semantic server
*/
Semantic,
}
export class ClientCapabilities {
private readonly capabilities: ReadonlySet<ClientCapability>;
constructor(...capabilities: ClientCapability[]) {
this.capabilities = new Set(capabilities);
}
public has(capability: ClientCapability): boolean {
return this.capabilities.has(capability);
}
}
export interface ITypeScriptServiceClient {
/**
* Convert a resource (VS Code) to a normalized path (TypeScript).
*
* Does not try handling case insensitivity.
*/
normalizedPath(resource: vscode.Uri): string | undefined;
/**
* Map a resource to a normalized path
*
* This will attempt to handle case insensitivity.
*/
toPath(resource: vscode.Uri): string | undefined;
/**
* Convert a path to a resource.
*/
toResource(filepath: string): vscode.Uri;
/**
* Tries to ensure that a vscode document is open on the TS server.
*
* @return The normalized path or `undefined` if the document is not open on the server.
*/
toOpenedFilePath(document: vscode.TextDocument): string | undefined;
/**
* Checks if `resource` has a given capability.
*/
hasCapabilityForResource(resource: vscode.Uri, capability: ClientCapability): boolean;
getWorkspaceRootForResource(resource: vscode.Uri): string | undefined;
readonly onTsServerStarted: vscode.Event<{ version: TypeScriptVersion, usedApiVersion: API }>;
readonly onProjectLanguageServiceStateChanged: vscode.Event<Proto.ProjectLanguageServiceStateEventBody>;
readonly onDidBeginInstallTypings: vscode.Event<Proto.BeginInstallTypesEventBody>;
readonly onDidEndInstallTypings: vscode.Event<Proto.EndInstallTypesEventBody>;
readonly onTypesInstallerInitializationFailed: vscode.Event<Proto.TypesInstallerInitializationFailedEventBody>;
readonly capabilities: ClientCapabilities;
readonly onDidChangeCapabilities: vscode.Event<void>;
onReady(f: () => void): Promise<void>;
showVersionPicker(): void;
readonly apiVersion: API;
readonly pluginManager: PluginManager;
readonly configuration: TypeScriptServiceConfiguration;
readonly bufferSyncSupport: BufferSyncSupport;
readonly telemetryReporter: TelemetryReporter;
execute<K extends keyof StandardTsServerRequests>(
command: K,
args: StandardTsServerRequests[K][0],
token: vscode.CancellationToken,
config?: ExecConfig
): Promise<ServerResponse.Response<StandardTsServerRequests[K][1]>>;
executeWithoutWaitingForResponse<K extends keyof NoResponseTsServerRequests>(
command: K,
args: NoResponseTsServerRequests[K][0]
): void;
executeAsync<K extends keyof AsyncTsServerRequests>(
command: K,
args: AsyncTsServerRequests[K][0],
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.Response>>;
/**
* Cancel on going geterr requests and re-queue them after `f` has been evaluated.
*/
interruptGetErr<R>(f: () => R): R;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface ObjectMap<V> {
[key: string]: V;
}

View File

@@ -0,0 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as semver from 'semver';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export default class API {
public static fromSimpleString(value: string): API {
return new API(value, value, value);
}
public static readonly defaultVersion = API.fromSimpleString('1.0.0');
public static readonly v240 = API.fromSimpleString('2.4.0');
public static readonly v250 = API.fromSimpleString('2.5.0');
public static readonly v260 = API.fromSimpleString('2.6.0');
public static readonly v270 = API.fromSimpleString('2.7.0');
public static readonly v280 = API.fromSimpleString('2.8.0');
public static readonly v290 = API.fromSimpleString('2.9.0');
public static readonly v291 = API.fromSimpleString('2.9.1');
public static readonly v292 = API.fromSimpleString('2.9.2');
public static readonly v300 = API.fromSimpleString('3.0.0');
public static readonly v310 = API.fromSimpleString('3.1.0');
public static readonly v314 = API.fromSimpleString('3.1.4');
public static readonly v320 = API.fromSimpleString('3.2.0');
public static readonly v330 = API.fromSimpleString('3.3.0');
public static readonly v333 = API.fromSimpleString('3.3.3');
public static readonly v340 = API.fromSimpleString('3.4.0');
public static readonly v345 = API.fromSimpleString('3.4.5');
public static readonly v350 = API.fromSimpleString('3.5.0');
public static readonly v380 = API.fromSimpleString('3.8.0');
public static readonly v381 = API.fromSimpleString('3.8.1');
public static readonly v390 = API.fromSimpleString('3.9.0');
public static readonly v400 = API.fromSimpleString('4.0.0');
public static readonly v401 = API.fromSimpleString('4.0.1');
public static fromVersionString(versionString: string): API {
let version = semver.valid(versionString);
if (!version) {
return new API(localize('invalidVersion', 'invalid version'), '1.0.0', '1.0.0');
}
// Cut off any prerelease tag since we sometimes consume those on purpose.
const index = versionString.indexOf('-');
if (index >= 0) {
version = version.substr(0, index);
}
return new API(versionString, version, versionString);
}
private constructor(
/**
* Human readable string for the current version. Displayed in the UI
*/
public readonly displayName: string,
/**
* Semver version, e.g. '3.9.0'
*/
public readonly version: string,
/**
* Full version string including pre-release tags, e.g. '3.9.0-beta'
*/
public readonly fullVersionString: string,
) { }
public eq(other: API): boolean {
return semver.eq(this.version, other.version);
}
public gte(other: API): boolean {
return semver.gte(this.version, other.version);
}
public lt(other: API): boolean {
return !this.gte(other);
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const empty = Object.freeze([]);
export function equals<T>(
a: ReadonlyArray<T>,
b: ReadonlyArray<T>,
itemEquals: (a: T, b: T) => boolean = (a, b) => a === b
): boolean {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
return a.every((x, i) => itemEquals(x, b[i]));
}
export function flatten<T>(array: ReadonlyArray<T>[]): T[] {
return Array.prototype.concat.apply([], array);
}
export function coalesce<T>(array: ReadonlyArray<T | undefined>): T[] {
return <T[]>array.filter(e => !!e);
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ITask<T> {
(): T;
}
export class Delayer<T> {
public defaultDelay: number;
private timeout: any; // Timer
private completionPromise: Promise<T | null> | null;
private onSuccess: ((value: T | PromiseLike<T> | undefined) => void) | null;
private task: ITask<T> | null;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onSuccess = null;
this.task = null;
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T | null> {
this.task = task;
if (delay >= 0) {
this.cancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T | undefined>((resolve) => {
this.onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
this.onSuccess = null;
const result = this.task && this.task();
this.task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
if (this.onSuccess) {
this.onSuccess(undefined);
}
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
const noopDisposable = vscode.Disposable.from();
export const nulToken: vscode.CancellationToken = {
isCancellationRequested: false,
onCancellationRequested: () => noopDisposable
};

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import * as typeConverters from './typeConverters';
export function getEditForCodeAction(
client: ITypeScriptServiceClient,
action: Proto.CodeAction
): vscode.WorkspaceEdit | undefined {
return action.changes && action.changes.length
? typeConverters.WorkspaceEdit.fromFileCodeEdits(client, action.changes)
: undefined;
}
export async function applyCodeAction(
client: ITypeScriptServiceClient,
action: Proto.CodeAction,
token: vscode.CancellationToken
): Promise<boolean> {
const workspaceEdit = getEditForCodeAction(client, action);
if (workspaceEdit) {
if (!(await vscode.workspace.applyEdit(workspaceEdit))) {
return false;
}
}
return applyCodeActionCommands(client, action.commands, token);
}
export async function applyCodeActionCommands(
client: ITypeScriptServiceClient,
commands: ReadonlyArray<{}> | undefined,
token: vscode.CancellationToken,
): Promise<boolean> {
if (commands && commands.length) {
for (const command of commands) {
await client.execute('applyCodeActionCommand', { command }, token);
}
}
return true;
}

View File

@@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as objects from '../utils/objects';
import * as arrays from './arrays';
export enum TsServerLogLevel {
Off,
Normal,
Terse,
Verbose,
}
export namespace TsServerLogLevel {
export function fromString(value: string): TsServerLogLevel {
switch (value && value.toLowerCase()) {
case 'normal':
return TsServerLogLevel.Normal;
case 'terse':
return TsServerLogLevel.Terse;
case 'verbose':
return TsServerLogLevel.Verbose;
case 'off':
default:
return TsServerLogLevel.Off;
}
}
export function toString(value: TsServerLogLevel): string {
switch (value) {
case TsServerLogLevel.Normal:
return 'normal';
case TsServerLogLevel.Terse:
return 'terse';
case TsServerLogLevel.Verbose:
return 'verbose';
case TsServerLogLevel.Off:
default:
return 'off';
}
}
}
export const enum SeparateSyntaxServerConfiguration {
Disabled,
Enabled,
}
export class TypeScriptServiceConfiguration {
public readonly locale: string | null;
public readonly globalTsdk: string | null;
public readonly localTsdk: string | null;
public readonly npmLocation: string | null;
public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off;
public readonly tsServerPluginPaths: readonly string[];
public readonly checkJs: boolean;
public readonly experimentalDecorators: boolean;
public readonly disableAutomaticTypeAcquisition: boolean;
public readonly separateSyntaxServer: SeparateSyntaxServerConfiguration;
public readonly enableProjectDiagnostics: boolean;
public readonly maxTsServerMemory: number;
public readonly enablePromptUseWorkspaceTsdk: boolean;
public readonly watchOptions: protocol.WatchOptions | undefined;
public readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined;
public static loadFromWorkspace(): TypeScriptServiceConfiguration {
return new TypeScriptServiceConfiguration();
}
private constructor() {
const configuration = vscode.workspace.getConfiguration();
this.locale = TypeScriptServiceConfiguration.extractLocale(configuration);
this.globalTsdk = TypeScriptServiceConfiguration.extractGlobalTsdk(configuration);
this.localTsdk = TypeScriptServiceConfiguration.extractLocalTsdk(configuration);
this.npmLocation = TypeScriptServiceConfiguration.readNpmLocation(configuration);
this.tsServerLogLevel = TypeScriptServiceConfiguration.readTsServerLogLevel(configuration);
this.tsServerPluginPaths = TypeScriptServiceConfiguration.readTsServerPluginPaths(configuration);
this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration);
this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration);
this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration);
this.separateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration);
this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration);
this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration);
this.enablePromptUseWorkspaceTsdk = TypeScriptServiceConfiguration.readEnablePromptUseWorkspaceTsdk(configuration);
this.watchOptions = TypeScriptServiceConfiguration.readWatchOptions(configuration);
this.includePackageJsonAutoImports = TypeScriptServiceConfiguration.readIncludePackageJsonAutoImports(configuration);
}
public isEqualTo(other: TypeScriptServiceConfiguration): boolean {
return this.locale === other.locale
&& this.globalTsdk === other.globalTsdk
&& this.localTsdk === other.localTsdk
&& this.npmLocation === other.npmLocation
&& this.tsServerLogLevel === other.tsServerLogLevel
&& this.checkJs === other.checkJs
&& this.experimentalDecorators === other.experimentalDecorators
&& this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition
&& arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths)
&& this.separateSyntaxServer === other.separateSyntaxServer
&& this.enableProjectDiagnostics === other.enableProjectDiagnostics
&& this.maxTsServerMemory === other.maxTsServerMemory
&& objects.equals(this.watchOptions, other.watchOptions)
&& this.enablePromptUseWorkspaceTsdk === other.enablePromptUseWorkspaceTsdk
&& this.includePackageJsonAutoImports === other.includePackageJsonAutoImports;
}
private static fixPathPrefixes(inspectValue: string): string {
const pathPrefixes = ['~' + path.sep];
for (const pathPrefix of pathPrefixes) {
if (inspectValue.startsWith(pathPrefix)) {
return path.join(os.homedir(), inspectValue.slice(pathPrefix.length));
}
}
return inspectValue;
}
private static extractGlobalTsdk(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsdk');
if (inspect && typeof inspect.globalValue === 'string') {
return this.fixPathPrefixes(inspect.globalValue);
}
return null;
}
private static extractLocalTsdk(configuration: vscode.WorkspaceConfiguration): string | null {
const inspect = configuration.inspect('typescript.tsdk');
if (inspect && typeof inspect.workspaceValue === 'string') {
return this.fixPathPrefixes(inspect.workspaceValue);
}
return null;
}
private static readTsServerLogLevel(configuration: vscode.WorkspaceConfiguration): TsServerLogLevel {
const setting = configuration.get<string>('typescript.tsserver.log', 'off');
return TsServerLogLevel.fromString(setting);
}
private static readTsServerPluginPaths(configuration: vscode.WorkspaceConfiguration): string[] {
return configuration.get<string[]>('typescript.tsserver.pluginPaths', []);
}
private static readCheckJs(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('javascript.implicitProjectConfig.checkJs', false);
}
private static readExperimentalDecorators(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('javascript.implicitProjectConfig.experimentalDecorators', false);
}
private static readNpmLocation(configuration: vscode.WorkspaceConfiguration): string | null {
return configuration.get<string | null>('typescript.npm', null);
}
private static readDisableAutomaticTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.disableAutomaticTypeAcquisition', false);
}
private static extractLocale(configuration: vscode.WorkspaceConfiguration): string | null {
return configuration.get<string | null>('typescript.locale', null);
}
private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): SeparateSyntaxServerConfiguration {
const value = configuration.get('typescript.tsserver.useSeparateSyntaxServer', true);
if (value === true) {
return SeparateSyntaxServerConfiguration.Enabled;
}
return SeparateSyntaxServerConfiguration.Disabled;
}
private static readEnableProjectDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.experimental.enableProjectDiagnostics', false);
}
private static readWatchOptions(configuration: vscode.WorkspaceConfiguration): protocol.WatchOptions | undefined {
return configuration.get<protocol.WatchOptions>('typescript.tsserver.watchOptions');
}
private static readIncludePackageJsonAutoImports(configuration: vscode.WorkspaceConfiguration): 'auto' | 'on' | 'off' | undefined {
return configuration.get<'auto' | 'on' | 'off'>('typescript.preferences.includePackageJsonAutoImports');
}
private static readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number {
const defaultMaxMemory = 3072;
const minimumMaxMemory = 128;
const memoryInMB = configuration.get<number>('typescript.tsserver.maxTsServerMemory', defaultMaxMemory);
if (!Number.isSafeInteger(memoryInMB)) {
return defaultMaxMemory;
}
return Math.max(memoryInMB, minimumMaxMemory);
}
private static readEnablePromptUseWorkspaceTsdk(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.enablePromptUseWorkspaceTsdk', false);
}
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService';
import API from './api';
import { Disposable } from './dispose';
export class Condition extends Disposable {
private _value: boolean;
constructor(
private readonly getValue: () => boolean,
onUpdate: (handler: () => void) => void,
) {
super();
this._value = this.getValue();
onUpdate(() => {
const newValue = this.getValue();
if (newValue !== this._value) {
this._value = newValue;
this._onDidChange.fire();
}
});
}
public get value(): boolean { return this._value; }
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
public readonly onDidChange = this._onDidChange.event;
}
class ConditionalRegistration {
private registration: vscode.Disposable | undefined = undefined;
public constructor(
private readonly conditions: readonly Condition[],
private readonly doRegister: () => vscode.Disposable
) {
for (const condition of conditions) {
condition.onDidChange(() => this.update());
}
this.update();
}
public dispose() {
this.registration?.dispose();
this.registration = undefined;
}
private update() {
const enabled = this.conditions.every(condition => condition.value);
if (enabled) {
if (!this.registration) {
this.registration = this.doRegister();
}
} else {
if (this.registration) {
this.registration.dispose();
this.registration = undefined;
}
}
}
}
export function conditionalRegistration(
conditions: readonly Condition[],
doRegister: () => vscode.Disposable,
): vscode.Disposable {
return new ConditionalRegistration(conditions, doRegister);
}
export function requireMinVersion(
client: ITypeScriptServiceClient,
minVersion: API,
) {
return new Condition(
() => client.apiVersion.gte(minVersion),
client.onTsServerStarted
);
}
export function requireConfiguration(
language: string,
configValue: string,
) {
return new Condition(
() => {
const config = vscode.workspace.getConfiguration(language, null);
return !!config.get<boolean>(configValue);
},
vscode.workspace.onDidChangeConfiguration
);
}
export function requireSomeCapability(
client: ITypeScriptServiceClient,
...capabilities: readonly ClientCapability[]
) {
return new Condition(
() => capabilities.some(requiredCapability => client.capabilities.has(requiredCapability)),
client.onDidChangeCapabilities
);
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]) {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: vscode.Disposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface DocumentSelector {
/**
* Selector for files which only require a basic syntax server.
*/
readonly syntax: readonly vscode.DocumentFilter[];
/**
* Selector for files which require semantic server support.
*/
readonly semantic: readonly vscode.DocumentFilter[];
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const variableDeclaredButNeverUsed = new Set([6196, 6133]);
export const propertyDeclaretedButNeverUsed = new Set([6138]);
export const allImportsAreUnused = new Set([6192]);
export const unreachableCode = new Set([7027]);
export const unusedLabel = new Set([7028]);
export const fallThroughCaseInSwitch = new Set([7029]);
export const notAllCodePathsReturnAValue = new Set([7030]);
export const incorrectlyImplementsInterface = new Set([2420]);
export const cannotFindName = new Set([2552, 2304]);
export const extendsShouldBeImplements = new Set([2689]);
export const asyncOnlyAllowedInAsyncFunctions = new Set([1308]);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const file = 'file';
export const untitled = 'untitled';
export const git = 'git';
/** Live share scheme */
export const vsls = 'vsls';
export const walkThroughSnippet = 'walkThroughSnippet';
export const semanticSupportedSchemes = [
file,
untitled,
walkThroughSnippet,
];
/**
* File scheme for which JS/TS language feature should be disabled
*/
export const disabledSchemes = new Set([
git,
vsls
]);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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 { getTempFile } from './temp.electron';
export const onCaseInsenitiveFileSystem = (() => {
let value: boolean | undefined;
return (): boolean => {
if (typeof value === 'undefined') {
if (process.platform === 'win32') {
value = true;
} else if (process.platform !== 'darwin') {
value = false;
} else {
const temp = getTempFile('typescript-case-check');
fs.writeFileSync(temp, '');
value = fs.existsSync(temp.toUpperCase());
}
}
return value;
};
})();

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const annotateWithTypeFromJSDoc = 'annotateWithTypeFromJSDoc';
export const constructorForDerivedNeedSuperCall = 'constructorForDerivedNeedSuperCall';
export const extendsInterfaceBecomesImplements = 'extendsInterfaceBecomesImplements';
export const awaitInSyncFunction = 'fixAwaitInSyncFunction';
export const classIncorrectlyImplementsInterface = 'fixClassIncorrectlyImplementsInterface';
export const classDoesntImplementInheritedAbstractMember = 'fixClassDoesntImplementInheritedAbstractMember';
export const unreachableCode = 'fixUnreachableCode';
export const unusedIdentifier = 'unusedIdentifier';
export const forgottenThisPropertyAccess = 'forgottenThisPropertyAccess';
export const spelling = 'spelling';
export const fixImport = 'import';
export const addMissingAwait = 'addMissingAwait';

Some files were not shown because too many files have changed in this diff Show More