mirror of
https://github.com/coder/code-server.git
synced 2026-05-08 05:17:27 +02:00
Squashed 'lib/vscode/' content from commit e5a624b788
git-subtree-dir: lib/vscode git-subtree-split: e5a624b788d92b8d34d1392e4c4d9789406efe8f
This commit is contained in:
36
extensions/typescript-language-features/src/api.ts
Normal file
36
extensions/typescript-language-features/src/api.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
66
extensions/typescript-language-features/src/extension.ts
Normal file
66
extensions/typescript-language-features/src/extension.ts
Normal 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());
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
'@');
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
'*');
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
154
extensions/typescript-language-features/src/languageProvider.ts
Normal file
154
extensions/typescript-language-features/src/languageProvider.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
12
extensions/typescript-language-features/src/protocol.d.ts
vendored
Normal file
12
extensions/typescript-language-features/src/protocol.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
303
extensions/typescript-language-features/src/task/taskProvider.ts
Normal file
303
extensions/typescript-language-features/src/task/taskProvider.ts
Normal 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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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! };
|
||||
}
|
||||
@@ -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`,
|
||||
` }`,
|
||||
`}`,
|
||||
));
|
||||
});
|
||||
});
|
||||
139
extensions/typescript-language-features/src/test/fixAll.test.ts
Normal file
139
extensions/typescript-language-features/src/test/fixAll.test.ts
Normal 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;`,
|
||||
``
|
||||
));
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
28
extensions/typescript-language-features/src/test/index.ts
Normal file
28
extensions/typescript-language-features/src/test/index.ts
Normal 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;
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}',
|
||||
' */'
|
||||
));
|
||||
});
|
||||
});
|
||||
@@ -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`,
|
||||
`})`));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
173
extensions/typescript-language-features/src/test/testUtils.ts
Normal file
173
extensions/typescript-language-features/src/test/testUtils.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
628
extensions/typescript-language-features/src/tsServer/server.ts
Normal file
628
extensions/typescript-language-features/src/tsServer/server.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
267
extensions/typescript-language-features/src/tsServer/spawner.ts
Normal file
267
extensions/typescript-language-features/src/tsServer/spawner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
201
extensions/typescript-language-features/src/typescriptService.ts
Normal file
201
extensions/typescript-language-features/src/typescriptService.ts
Normal 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
8
extensions/typescript-language-features/src/typings/collections.d.ts
vendored
Normal file
8
extensions/typescript-language-features/src/typings/collections.d.ts
vendored
Normal 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;
|
||||
}
|
||||
6
extensions/typescript-language-features/src/typings/ref.d.ts
vendored
Normal file
6
extensions/typescript-language-features/src/typings/ref.d.ts
vendored
Normal 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'/>
|
||||
82
extensions/typescript-language-features/src/utils/api.ts
Normal file
82
extensions/typescript-language-features/src/utils/api.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
28
extensions/typescript-language-features/src/utils/arrays.ts
Normal file
28
extensions/typescript-language-features/src/utils/arrays.ts
Normal 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);
|
||||
}
|
||||
62
extensions/typescript-language-features/src/utils/async.ts
Normal file
62
extensions/typescript-language-features/src/utils/async.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
42
extensions/typescript-language-features/src/utils/dispose.ts
Normal file
42
extensions/typescript-language-features/src/utils/dispose.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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
|
||||
]);
|
||||
@@ -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;
|
||||
};
|
||||
})();
|
||||
@@ -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
Reference in New Issue
Block a user