mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-21 09:08:53 +01:00
Rename typescript to typescript-language-features
This commit is contained in:
176
extensions/typescript-language-features/src/commands.ts
Normal file
176
extensions/typescript-language-features/src/commands.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import TypeScriptServiceClientHost from './typeScriptServiceClientHost';
|
||||
import { Command } from './utils/commandManager';
|
||||
import { Lazy } from './utils/lazy';
|
||||
import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './utils/tsconfig';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectTypeScriptVersionCommand implements Command {
|
||||
public readonly id = 'typescript.selectTypeScriptVersion';
|
||||
|
||||
public constructor(
|
||||
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
|
||||
) { }
|
||||
|
||||
public execute() {
|
||||
this.lazyClientHost.value.serviceClient.onVersionStatusClicked();
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenTsServerLogCommand implements Command {
|
||||
public readonly id = 'typescript.openTsServerLog';
|
||||
|
||||
public constructor(
|
||||
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
|
||||
) { }
|
||||
|
||||
public execute() {
|
||||
this.lazyClientHost.value.serviceClient.openTsServerLogFile();
|
||||
}
|
||||
}
|
||||
|
||||
export class RestartTsServerCommand implements Command {
|
||||
public readonly id = 'typescript.restartTsServer';
|
||||
|
||||
public constructor(
|
||||
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
|
||||
) { }
|
||||
|
||||
public execute() {
|
||||
this.lazyClientHost.value.serviceClient.restartTsServer();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
goToProjectConfig(this.lazyClientHost.value, true, 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) {
|
||||
goToProjectConfig(this.lazyClientHost.value, false, editor.document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function goToProjectConfig(
|
||||
clientHost: TypeScriptServiceClientHost,
|
||||
isTypeScriptProject: boolean,
|
||||
resource: vscode.Uri
|
||||
): Promise<void> {
|
||||
const client = clientHost.serviceClient;
|
||||
const rootPath = client.getWorkspaceRootForResource(resource);
|
||||
if (!rootPath) {
|
||||
vscode.window.showInformationMessage(
|
||||
localize(
|
||||
'typescript.projectConfigNoWorkspace',
|
||||
'Please open a folder in VS Code to use a TypeScript or JavaScript project'));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = client.normalizePath(resource);
|
||||
// TSServer errors when 'projectInfo' is invoked on a non js/ts file
|
||||
if (!file || !clientHost.handles(resource)) {
|
||||
vscode.window.showWarningMessage(
|
||||
localize(
|
||||
'typescript.projectConfigUnsupportedFile',
|
||||
'Could not determine TypeScript or JavaScript project. Unsupported file type'));
|
||||
return;
|
||||
}
|
||||
|
||||
let res: protocol.ProjectInfoResponse | undefined = undefined;
|
||||
try {
|
||||
res = await client.execute('projectInfo', { file, needFileNameList: false } as protocol.ProjectInfoRequestArgs);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
if (!res || !res.body) {
|
||||
vscode.window.showWarningMessage(localize('typescript.projectConfigCouldNotGetInfo', 'Could not determine TypeScript or JavaScript project'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { configFileName } = res.body;
|
||||
if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
|
||||
const doc = await vscode.workspace.openTextDocument(configFileName);
|
||||
vscode.window.showTextDocument(doc, vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
enum ProjectConfigAction {
|
||||
None,
|
||||
CreateConfig,
|
||||
LearnMore
|
||||
}
|
||||
|
||||
interface ProjectConfigMessageItem extends vscode.MessageItem {
|
||||
id: ProjectConfigAction;
|
||||
}
|
||||
|
||||
const selected = await vscode.window.showInformationMessage<ProjectConfigMessageItem>(
|
||||
(isTypeScriptProject
|
||||
? localize('typescript.noTypeScriptProjectConfig', 'File is not part of a TypeScript project. Click [here]({0}) to learn more.', 'https://go.microsoft.com/fwlink/?linkid=841896')
|
||||
: localize('typescript.noJavaScriptProjectConfig', 'File is not part of a JavaScript project Click [here]({0}) to learn more.', 'https://go.microsoft.com/fwlink/?linkid=759670')
|
||||
), {
|
||||
title: isTypeScriptProject
|
||||
? localize('typescript.configureTsconfigQuickPick', 'Configure tsconfig.json')
|
||||
: localize('typescript.configureJsconfigQuickPick', 'Configure jsconfig.json'),
|
||||
id: ProjectConfigAction.CreateConfig
|
||||
});
|
||||
|
||||
switch (selected && selected.id) {
|
||||
case ProjectConfigAction.CreateConfig:
|
||||
openOrCreateConfigFile(isTypeScriptProject, rootPath, client.configuration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
117
extensions/typescript-language-features/src/extension.ts
Normal file
117
extensions/typescript-language-features/src/extension.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 './utils/commandManager';
|
||||
import TypeScriptServiceClientHost from './typeScriptServiceClientHost';
|
||||
import * as commands from './commands';
|
||||
|
||||
import TypeScriptTaskProviderManager from './features/taskProvider';
|
||||
import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from './utils/plugins';
|
||||
import * as ProjectStatus from './utils/projectStatus';
|
||||
import * as languageModeIds from './utils/languageModeIds';
|
||||
import * as languageConfigurations from './utils/languageConfigurations';
|
||||
import { standardLanguageDescriptions } from './utils/languageDescription';
|
||||
import ManagedFileContextManager from './utils/managedFileContext';
|
||||
import { lazy, Lazy } from './utils/lazy';
|
||||
import * as fileSchemes from './utils/fileSchemes';
|
||||
import LogDirectoryProvider from './utils/logDirectoryProvider';
|
||||
import { OrganizeImportsCommand, OrganizeImportsContextManager } from './features/organizeImports';
|
||||
|
||||
export function activate(
|
||||
context: vscode.ExtensionContext
|
||||
): void {
|
||||
const plugins = getContributedTypeScriptServerPlugins();
|
||||
|
||||
const commandManager = new CommandManager();
|
||||
context.subscriptions.push(commandManager);
|
||||
|
||||
const lazyClientHost = createLazyClientHost(context, plugins, commandManager);
|
||||
|
||||
registerCommands(commandManager, lazyClientHost);
|
||||
context.subscriptions.push(new TypeScriptTaskProviderManager(lazyClientHost.map(x => x.serviceClient)));
|
||||
context.subscriptions.push(vscode.languages.setLanguageConfiguration(languageModeIds.jsxTags, languageConfigurations.jsxTags));
|
||||
|
||||
const supportedLanguage = [].concat.apply([], standardLanguageDescriptions.map(x => x.modeIds).concat(plugins.map(x => x.languages)));
|
||||
function didOpenTextDocument(textDocument: vscode.TextDocument): boolean {
|
||||
if (isSupportedDocument(supportedLanguage, textDocument)) {
|
||||
openListener.dispose();
|
||||
// Force activation
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
void lazyClientHost.value;
|
||||
|
||||
context.subscriptions.push(new ManagedFileContextManager(resource => {
|
||||
return lazyClientHost.value.serviceClient.normalizePath(resource);
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const openListener = vscode.workspace.onDidOpenTextDocument(didOpenTextDocument, undefined, context.subscriptions);
|
||||
for (const textDocument of vscode.workspace.textDocuments) {
|
||||
if (didOpenTextDocument(textDocument)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createLazyClientHost(
|
||||
context: vscode.ExtensionContext,
|
||||
plugins: TypeScriptServerPlugin[],
|
||||
commandManager: CommandManager
|
||||
): Lazy<TypeScriptServiceClientHost> {
|
||||
return lazy(() => {
|
||||
const logDirectoryProvider = new LogDirectoryProvider(context);
|
||||
const clientHost = new TypeScriptServiceClientHost(
|
||||
standardLanguageDescriptions,
|
||||
context.workspaceState,
|
||||
plugins,
|
||||
commandManager,
|
||||
logDirectoryProvider);
|
||||
|
||||
context.subscriptions.push(clientHost);
|
||||
|
||||
const organizeImportsContext = new OrganizeImportsContextManager();
|
||||
clientHost.serviceClient.onTsServerStarted(api => {
|
||||
organizeImportsContext.onDidChangeApiVersion(api);
|
||||
}, null, context.subscriptions);
|
||||
|
||||
clientHost.serviceClient.onReady(() => {
|
||||
context.subscriptions.push(
|
||||
ProjectStatus.create(
|
||||
clientHost.serviceClient,
|
||||
clientHost.serviceClient.telemetryReporter,
|
||||
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
|
||||
context.workspaceState));
|
||||
});
|
||||
|
||||
return clientHost;
|
||||
});
|
||||
}
|
||||
|
||||
function registerCommands(
|
||||
commandManager: CommandManager,
|
||||
lazyClientHost: Lazy<TypeScriptServiceClientHost>
|
||||
) {
|
||||
commandManager.register(new commands.ReloadTypeScriptProjectsCommand(lazyClientHost));
|
||||
commandManager.register(new commands.ReloadJavaScriptProjectsCommand(lazyClientHost));
|
||||
commandManager.register(new commands.SelectTypeScriptVersionCommand(lazyClientHost));
|
||||
commandManager.register(new commands.OpenTsServerLogCommand(lazyClientHost));
|
||||
commandManager.register(new commands.RestartTsServerCommand(lazyClientHost));
|
||||
commandManager.register(new commands.TypeScriptGoToProjectConfigCommand(lazyClientHost));
|
||||
commandManager.register(new commands.JavaScriptGoToProjectConfigCommand(lazyClientHost));
|
||||
commandManager.register(new OrganizeImportsCommand(lazyClientHost));
|
||||
}
|
||||
|
||||
function isSupportedDocument(
|
||||
supportedLanguage: string[],
|
||||
document: vscode.TextDocument
|
||||
): boolean {
|
||||
if (supportedLanguage.indexOf(document.languageId) < 0) {
|
||||
return false;
|
||||
}
|
||||
return fileSchemes.isSupportedScheme(document.uri.scheme);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Position, Event, EventEmitter } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import { escapeRegExp } from '../utils/regexp';
|
||||
|
||||
export class ReferencesCodeLens extends CodeLens {
|
||||
constructor(
|
||||
public document: Uri,
|
||||
public file: string,
|
||||
range: Range
|
||||
) {
|
||||
super(range);
|
||||
}
|
||||
}
|
||||
|
||||
export class CachedNavTreeResponse {
|
||||
private response?: Promise<Proto.NavTreeResponse>;
|
||||
private version: number = -1;
|
||||
private document: string = '';
|
||||
|
||||
public execute(
|
||||
document: TextDocument,
|
||||
f: () => Promise<Proto.NavTreeResponse>
|
||||
) {
|
||||
if (this.matches(document)) {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
return this.update(document, f());
|
||||
}
|
||||
|
||||
private matches(document: TextDocument): boolean {
|
||||
return this.version === document.version && this.document === document.uri.toString();
|
||||
}
|
||||
|
||||
private update(
|
||||
document: TextDocument,
|
||||
response: Promise<Proto.NavTreeResponse>
|
||||
): Promise<Proto.NavTreeResponse> {
|
||||
this.response = response;
|
||||
this.version = document.version;
|
||||
this.document = document.uri.toString();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider {
|
||||
private enabled: boolean = true;
|
||||
private onDidChangeCodeLensesEmitter = new EventEmitter<void>();
|
||||
|
||||
public constructor(
|
||||
protected client: ITypeScriptServiceClient,
|
||||
private cachedResponse: CachedNavTreeResponse
|
||||
) { }
|
||||
|
||||
public get onDidChangeCodeLenses(): Event<void> {
|
||||
return this.onDidChangeCodeLensesEmitter.event;
|
||||
}
|
||||
|
||||
protected setEnabled(enabled: false): void {
|
||||
if (this.enabled !== enabled) {
|
||||
this.enabled = enabled;
|
||||
this.onDidChangeCodeLensesEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
||||
if (!this.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', { file: filepath }, token));
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tree = response.body;
|
||||
const referenceableSpans: 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));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null
|
||||
): Range | null;
|
||||
|
||||
private walkNavTree(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null,
|
||||
results: 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: TS currently requires the position for 'references 'to be inside of the identifer
|
||||
* Massage the range to make sure this is the case
|
||||
*/
|
||||
protected getSymbolRange(document: TextDocument, item: Proto.NavigationTree): Range | null {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 Position(range.start.line, range.start.character)) + prefixLength;
|
||||
return new Range(
|
||||
document.positionAt(startOffset),
|
||||
document.positionAt(startOffset + item.text.length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { workspace, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, Disposable, Uri } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import { Delayer } from '../utils/async';
|
||||
import * as languageModeIds from '../utils/languageModeIds';
|
||||
import { disposeAll } from '../utils/dipose';
|
||||
|
||||
interface IDiagnosticRequestor {
|
||||
requestDiagnostic(resource: Uri): void;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class SyncedBuffer {
|
||||
|
||||
constructor(
|
||||
private readonly document: TextDocument,
|
||||
private readonly filepath: string,
|
||||
private readonly diagnosticRequestor: IDiagnosticRequestor,
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public open(): void {
|
||||
const args: Proto.OpenRequestArgs = {
|
||||
file: this.filepath,
|
||||
fileContent: this.document.getText(),
|
||||
};
|
||||
|
||||
if (this.client.apiVersion.has203Features()) {
|
||||
const scriptKind = mode2ScriptKind(this.document.languageId);
|
||||
if (scriptKind) {
|
||||
args.scriptKindName = scriptKind;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client.apiVersion.has230Features()) {
|
||||
args.projectRootPath = this.client.getWorkspaceRootForResource(this.document.uri);
|
||||
}
|
||||
|
||||
if (this.client.apiVersion.has240Features()) {
|
||||
const tsPluginsForDocument = this.client.plugins
|
||||
.filter(x => x.languages.indexOf(this.document.languageId) >= 0);
|
||||
|
||||
if (tsPluginsForDocument.length) {
|
||||
(args as any).plugins = tsPluginsForDocument.map(plugin => plugin.name);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.execute('open', args, false);
|
||||
}
|
||||
|
||||
public get lineCount(): number {
|
||||
return this.document.lineCount;
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
const args: Proto.FileRequestArgs = {
|
||||
file: this.filepath
|
||||
};
|
||||
this.client.execute('close', args, false);
|
||||
}
|
||||
|
||||
public onContentChanged(events: TextDocumentContentChangeEvent[]): void {
|
||||
const filePath = this.client.normalizePath(this.document.uri);
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const range = event.range;
|
||||
const text = event.text;
|
||||
const args: Proto.ChangeRequestArgs = {
|
||||
file: filePath,
|
||||
line: range.start.line + 1,
|
||||
offset: range.start.character + 1,
|
||||
endLine: range.end.line + 1,
|
||||
endOffset: range.end.character + 1,
|
||||
insertString: text
|
||||
};
|
||||
this.client.execute('change', args, false);
|
||||
}
|
||||
this.diagnosticRequestor.requestDiagnostic(this.document.uri);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncedBufferMap {
|
||||
private readonly _map = new Map<string, SyncedBuffer>();
|
||||
|
||||
constructor(
|
||||
private readonly _normalizePath: (resource: Uri) => string | null
|
||||
) { }
|
||||
|
||||
public has(resource: Uri): boolean {
|
||||
const file = this._normalizePath(resource);
|
||||
return !!file && this._map.has(file);
|
||||
}
|
||||
|
||||
public get(resource: Uri): SyncedBuffer | undefined {
|
||||
const file = this._normalizePath(resource);
|
||||
return file ? this._map.get(file) : undefined;
|
||||
}
|
||||
|
||||
public set(resource: Uri, buffer: SyncedBuffer) {
|
||||
const file = this._normalizePath(resource);
|
||||
if (file) {
|
||||
this._map.set(file, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public delete(resource: Uri): void {
|
||||
const file = this._normalizePath(resource);
|
||||
if (file) {
|
||||
this._map.delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
public get allBuffers(): Iterable<SyncedBuffer> {
|
||||
return this._map.values();
|
||||
}
|
||||
|
||||
public get allResources(): Iterable<string> {
|
||||
return this._map.keys();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface Diagnostics {
|
||||
delete(resource: Uri): void;
|
||||
}
|
||||
|
||||
export default class BufferSyncSupport {
|
||||
|
||||
private readonly client: ITypeScriptServiceClient;
|
||||
|
||||
private _validate: boolean;
|
||||
private readonly modeIds: Set<string>;
|
||||
private readonly diagnostics: Diagnostics;
|
||||
private readonly disposables: Disposable[] = [];
|
||||
private readonly syncedBuffers: SyncedBufferMap;
|
||||
|
||||
private readonly pendingDiagnostics = new Map<string, number>();
|
||||
private readonly diagnosticDelayer: Delayer<any>;
|
||||
|
||||
constructor(
|
||||
client: ITypeScriptServiceClient,
|
||||
modeIds: string[],
|
||||
diagnostics: Diagnostics,
|
||||
validate: boolean
|
||||
) {
|
||||
this.client = client;
|
||||
this.modeIds = new Set<string>(modeIds);
|
||||
this.diagnostics = diagnostics;
|
||||
this._validate = validate;
|
||||
|
||||
this.diagnosticDelayer = new Delayer<any>(300);
|
||||
|
||||
this.syncedBuffers = new SyncedBufferMap(path => this.client.normalizePath(path));
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
workspace.onDidOpenTextDocument(this.onDidOpenTextDocument, this, this.disposables);
|
||||
workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
|
||||
workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
|
||||
workspace.textDocuments.forEach(this.onDidOpenTextDocument, this);
|
||||
}
|
||||
|
||||
public set validate(value: boolean) {
|
||||
this._validate = value;
|
||||
}
|
||||
|
||||
public handles(resource: Uri): boolean {
|
||||
return this.syncedBuffers.has(resource);
|
||||
}
|
||||
|
||||
public reOpenDocuments(): void {
|
||||
for (const buffer of this.syncedBuffers.allBuffers) {
|
||||
buffer.open();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
disposeAll(this.disposables);
|
||||
}
|
||||
|
||||
private onDidOpenTextDocument(document: TextDocument): void {
|
||||
if (!this.modeIds.has(document.languageId)) {
|
||||
return;
|
||||
}
|
||||
const resource = document.uri;
|
||||
const filepath = this.client.normalizePath(resource);
|
||||
if (!filepath) {
|
||||
return;
|
||||
}
|
||||
const syncedBuffer = new SyncedBuffer(document, filepath, this, this.client);
|
||||
this.syncedBuffers.set(resource, syncedBuffer);
|
||||
syncedBuffer.open();
|
||||
this.requestDiagnostic(resource);
|
||||
}
|
||||
|
||||
private onDidCloseTextDocument(document: TextDocument): void {
|
||||
const resource = document.uri;
|
||||
const syncedBuffer = this.syncedBuffers.get(resource);
|
||||
if (!syncedBuffer) {
|
||||
return;
|
||||
}
|
||||
this.diagnostics.delete(resource);
|
||||
this.syncedBuffers.delete(resource);
|
||||
syncedBuffer.close();
|
||||
if (!fs.existsSync(resource.fsPath)) {
|
||||
this.requestAllDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
private onDidChangeTextDocument(e: TextDocumentChangeEvent): void {
|
||||
const syncedBuffer = this.syncedBuffers.get(e.document.uri);
|
||||
if (syncedBuffer) {
|
||||
syncedBuffer.onContentChanged(e.contentChanges);
|
||||
}
|
||||
}
|
||||
|
||||
public requestAllDiagnostics() {
|
||||
if (!this._validate) {
|
||||
return;
|
||||
}
|
||||
for (const filePath of this.syncedBuffers.allResources) {
|
||||
this.pendingDiagnostics.set(filePath, Date.now());
|
||||
}
|
||||
this.diagnosticDelayer.trigger(() => {
|
||||
this.sendPendingDiagnostics();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
public requestDiagnostic(resource: Uri): void {
|
||||
if (!this._validate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = this.client.normalizePath(resource);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingDiagnostics.set(file, Date.now());
|
||||
const buffer = this.syncedBuffers.get(resource);
|
||||
let delay = 300;
|
||||
if (buffer) {
|
||||
const lineCount = buffer.lineCount;
|
||||
delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800);
|
||||
}
|
||||
this.diagnosticDelayer.trigger(() => {
|
||||
this.sendPendingDiagnostics();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private sendPendingDiagnostics(): void {
|
||||
if (!this._validate) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(this.pendingDiagnostics.entries())
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.map(entry => entry[0]);
|
||||
|
||||
// Add all open TS buffers to the geterr request. They might be visible
|
||||
for (const file of this.syncedBuffers.allResources) {
|
||||
if (!this.pendingDiagnostics.get(file)) {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
const args: Proto.GeterrRequestArgs = {
|
||||
delay: 0,
|
||||
files: files
|
||||
};
|
||||
this.client.execute('geterr', args, false);
|
||||
}
|
||||
this.pendingDiagnostics.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 TypingsStatus from '../utils/typingsStatus';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
import * as Previewer from '../utils/previewer';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { applyCodeAction } from '../utils/codeAction';
|
||||
import { CommandManager, Command } from '../utils/commandManager';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
class MyCompletionItem extends vscode.CompletionItem {
|
||||
public readonly useCodeSnippet: boolean;
|
||||
|
||||
constructor(
|
||||
public readonly position: vscode.Position,
|
||||
public readonly document: vscode.TextDocument,
|
||||
line: string,
|
||||
public readonly tsEntry: Proto.CompletionEntry,
|
||||
enableDotCompletions: boolean,
|
||||
useCodeSnippetsOnMethodSuggest: boolean
|
||||
) {
|
||||
super(tsEntry.name);
|
||||
|
||||
if (tsEntry.isRecommended) {
|
||||
// Make sure isRecommended property always comes first
|
||||
// https://github.com/Microsoft/vscode/issues/40325
|
||||
this.sortText = '\0' + tsEntry.sortText;
|
||||
} else 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.kind = MyCompletionItem.convertKind(tsEntry.kind);
|
||||
this.position = position;
|
||||
this.commitCharacters = MyCompletionItem.getCommitCharacters(enableDotCompletions, !useCodeSnippetsOnMethodSuggest, tsEntry.kind);
|
||||
this.useCodeSnippet = useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method);
|
||||
|
||||
if (tsEntry.replacementSpan) {
|
||||
this.range = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan);
|
||||
}
|
||||
|
||||
if (tsEntry.insertText) {
|
||||
this.insertText = tsEntry.insertText;
|
||||
|
||||
if (tsEntry.replacementSpan) {
|
||||
this.range = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan);
|
||||
if (this.insertText[0] === '[') { // o.x -> o['x']
|
||||
this.filterText = '.' + this.label;
|
||||
}
|
||||
|
||||
// Make sure we only replace a single line at most
|
||||
if (!this.range.isSingleLine) {
|
||||
this.range = new vscode.Range(this.range.start.line, this.range.start.character, this.range.start.line, line.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tsEntry.kindModifiers && tsEntry.kindModifiers.match(/\boptional\b/)) {
|
||||
if (!this.insertText) {
|
||||
this.insertText = this.label;
|
||||
}
|
||||
|
||||
if (!this.filterText) {
|
||||
this.filterText = this.label;
|
||||
}
|
||||
this.label += '?';
|
||||
}
|
||||
}
|
||||
|
||||
public resolve(): void {
|
||||
if (!this.range) {
|
||||
// Try getting longer, prefix based range for completions that span words
|
||||
const wordRange = this.document.getWordRangeAtPosition(this.position);
|
||||
const text = this.document.getText(new vscode.Range(this.position.line, Math.max(0, this.position.character - this.label.length), this.position.line, this.position.character)).toLowerCase();
|
||||
const entryName = this.label.toLowerCase();
|
||||
for (let i = entryName.length; i >= 0; --i) {
|
||||
if (text.endsWith(entryName.substr(0, i)) && (!wordRange || wordRange.start.character > this.position.character - i)) {
|
||||
this.range = new vscode.Range(this.position.line, Math.max(0, this.position.character - i), this.position.line, this.position.character);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return vscode.CompletionItemKind.Constant;
|
||||
case PConst.Kind.let:
|
||||
case PConst.Kind.variable:
|
||||
case PConst.Kind.localVariable:
|
||||
case PConst.Kind.alias:
|
||||
return vscode.CompletionItemKind.Variable;
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
return vscode.CompletionItemKind.Field;
|
||||
case PConst.Kind.function:
|
||||
return vscode.CompletionItemKind.Function;
|
||||
case PConst.Kind.memberFunction:
|
||||
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.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:
|
||||
case PConst.Kind.file:
|
||||
case PConst.Kind.script:
|
||||
return vscode.CompletionItemKind.File;
|
||||
case PConst.Kind.directory:
|
||||
return vscode.CompletionItemKind.Folder;
|
||||
}
|
||||
return vscode.CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
private static getCommitCharacters(
|
||||
enableDotCompletions: boolean,
|
||||
enableCallCompletions: boolean,
|
||||
kind: string
|
||||
): string[] | undefined {
|
||||
switch (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:
|
||||
return enableDotCompletions ? ['.'] : undefined;
|
||||
|
||||
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.memberFunction:
|
||||
return enableDotCompletions ? (enableCallCompletions ? ['.', '('] : ['.']) : undefined;
|
||||
}
|
||||
|
||||
return 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]);
|
||||
}
|
||||
|
||||
interface MyQuickPickItem extends vscode.QuickPickItem {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const selection = await vscode.window.showQuickPick<MyQuickPickItem>(
|
||||
codeActions.map((action, i): MyQuickPickItem => ({
|
||||
label: action.description,
|
||||
description: '',
|
||||
index: i
|
||||
})), {
|
||||
placeHolder: localize('selectCodeAction', 'Select code action to apply')
|
||||
}
|
||||
);
|
||||
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const action = codeActions[selection.index];
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
return applyCodeAction(this.client, action);
|
||||
}
|
||||
}
|
||||
|
||||
interface CompletionConfiguration {
|
||||
readonly useCodeSnippetsOnMethodSuggest: boolean;
|
||||
readonly nameSuggestions: boolean;
|
||||
readonly quickSuggestionsForPaths: boolean;
|
||||
readonly autoImportSuggestions: boolean;
|
||||
}
|
||||
|
||||
namespace CompletionConfiguration {
|
||||
export const useCodeSnippetsOnMethodSuggest = 'useCodeSnippetsOnMethodSuggest';
|
||||
export const nameSuggestions = 'nameSuggestions';
|
||||
export const quickSuggestionsForPaths = 'quickSuggestionsForPaths';
|
||||
export const autoImportSuggestions = 'autoImportSuggestions.enabled';
|
||||
|
||||
export function getConfigurationForResource(
|
||||
resource: vscode.Uri
|
||||
): CompletionConfiguration {
|
||||
// TS settings are shared by both JS and TS.
|
||||
const typeScriptConfig = vscode.workspace.getConfiguration('typescript', resource);
|
||||
return {
|
||||
useCodeSnippetsOnMethodSuggest: typeScriptConfig.get<boolean>(CompletionConfiguration.useCodeSnippetsOnMethodSuggest, false),
|
||||
quickSuggestionsForPaths: typeScriptConfig.get<boolean>(CompletionConfiguration.quickSuggestionsForPaths, true),
|
||||
autoImportSuggestions: typeScriptConfig.get<boolean>(CompletionConfiguration.autoImportSuggestions, true),
|
||||
nameSuggestions: vscode.workspace.getConfiguration('javascript', resource).get(CompletionConfiguration.nameSuggestions, true)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider {
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
private readonly typingsStatus: TypingsStatus,
|
||||
commandManager: CommandManager
|
||||
) {
|
||||
commandManager.register(new ApplyCompletionCodeActionCommand(this.client));
|
||||
}
|
||||
|
||||
public async provideCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken,
|
||||
context: vscode.CompletionContext
|
||||
): Promise<vscode.CompletionItem[]> {
|
||||
if (this.typingsStatus.isAcquiringTypings) {
|
||||
return Promise.reject<vscode.CompletionItem[]>({
|
||||
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.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const line = document.lineAt(position.line);
|
||||
const completionConfiguration = CompletionConfiguration.getConfigurationForResource(document.uri);
|
||||
|
||||
if (!this.shouldTrigger(context, completionConfiguration, line, position)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args: Proto.CompletionsRequestArgs = {
|
||||
...typeConverters.Position.toFileLocationRequestArgs(file, position),
|
||||
includeExternalModuleExports: completionConfiguration.autoImportSuggestions,
|
||||
includeInsertTextCompletions: true
|
||||
};
|
||||
|
||||
let msg: Proto.CompletionEntry[] | undefined = undefined;
|
||||
try {
|
||||
const response = await this.client.execute('completions', args, token);
|
||||
msg = response.body;
|
||||
if (!msg) {
|
||||
return [];
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const enableDotCompletions = this.shouldEnableDotCompletions(document, position);
|
||||
|
||||
const completionItems: vscode.CompletionItem[] = [];
|
||||
for (const element of msg) {
|
||||
if (element.kind === PConst.Kind.warning && !completionConfiguration.nameSuggestions) {
|
||||
continue;
|
||||
}
|
||||
if (!completionConfiguration.autoImportSuggestions && element.hasAction) {
|
||||
continue;
|
||||
}
|
||||
const item = new MyCompletionItem(position, document, line.text, element, enableDotCompletions, completionConfiguration.useCodeSnippetsOnMethodSuggest);
|
||||
completionItems.push(item);
|
||||
}
|
||||
|
||||
return completionItems;
|
||||
}
|
||||
|
||||
public async resolveCompletionItem(
|
||||
item: vscode.CompletionItem,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CompletionItem | undefined> {
|
||||
if (!(item instanceof MyCompletionItem)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filepath = this.client.normalizePath(item.document.uri);
|
||||
if (!filepath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
item.resolve();
|
||||
|
||||
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
|
||||
]
|
||||
};
|
||||
|
||||
let response: Proto.CompletionDetailsResponse;
|
||||
try {
|
||||
response = await this.client.execute('completionEntryDetails', args, token);
|
||||
} catch {
|
||||
return item;
|
||||
}
|
||||
|
||||
const details = response.body;
|
||||
if (!details || !details.length || !details[0]) {
|
||||
return item;
|
||||
}
|
||||
const detail = details[0];
|
||||
item.detail = detail.displayParts.length ? Previewer.plain(detail.displayParts) : undefined;
|
||||
item.documentation = this.getDocumentation(detail, item);
|
||||
|
||||
const { command, additionalTextEdits } = this.getCodeActions(detail, filepath);
|
||||
item.command = command;
|
||||
item.additionalTextEdits = additionalTextEdits;
|
||||
|
||||
if (detail && item.useCodeSnippet) {
|
||||
const shouldCompleteFunction = await this.isValidFunctionCompletionContext(filepath, item.position);
|
||||
if (shouldCompleteFunction) {
|
||||
item.insertText = this.snippetForFunctionCall(item, detail);
|
||||
}
|
||||
}
|
||||
|
||||
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 shouldEnableDotCompletions(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): boolean {
|
||||
// TODO: Workaround for https://github.com/Microsoft/TypeScript/issues/13456
|
||||
// Only enable dot completions when previous character is an identifier.
|
||||
// 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 - 1));
|
||||
return preText.match(/[a-z_$\)\]\}]\s*$/ig) !== null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private shouldTrigger(
|
||||
context: vscode.CompletionContext,
|
||||
config: CompletionConfiguration,
|
||||
line: vscode.TextLine,
|
||||
position: vscode.Position
|
||||
): boolean {
|
||||
if (context.triggerCharacter === '"' || context.triggerCharacter === '\'') {
|
||||
if (!config.quickSuggestionsForPaths) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 === '/') {
|
||||
if (!config.quickSuggestionsForPaths) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
): Promise<boolean> {
|
||||
// Workaround for https://github.com/Microsoft/TypeScript/issues/12677
|
||||
// Don't complete function calls inside of destructive assigments or imports
|
||||
try {
|
||||
const infoResponse = await this.client.execute('quickinfo', typeConverters.Position.toFileLocationRequestArgs(filepath, position));
|
||||
const info = infoResponse.body;
|
||||
switch (info && info.kind) {
|
||||
case 'var':
|
||||
case 'let':
|
||||
case 'const':
|
||||
case 'alias':
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private snippetForFunctionCall(
|
||||
item: vscode.CompletionItem,
|
||||
detail: Proto.CompletionEntryDetails
|
||||
): vscode.SnippetString {
|
||||
let hasOptionalParameters = false;
|
||||
let hasAddedParameters = false;
|
||||
|
||||
const snippet = new vscode.SnippetString();
|
||||
const methodName = detail.displayParts.find(part => part.kind === 'methodName');
|
||||
snippet.appendText((methodName && methodName.text) || item.label || item.insertText as string);
|
||||
snippet.appendText('(');
|
||||
|
||||
let parenCount = 0;
|
||||
let i = 0;
|
||||
for (; i < detail.displayParts.length; ++i) {
|
||||
const part = detail.displayParts[i];
|
||||
// Only take top level paren names
|
||||
if (part.kind === 'parameterName' && parenCount === 1) {
|
||||
const next = detail.displayParts[i + 1];
|
||||
// Skip optional parameters
|
||||
const nameIsFollowedByOptionalIndicator = next && next.text === '?';
|
||||
if (!nameIsFollowedByOptionalIndicator) {
|
||||
if (hasAddedParameters) {
|
||||
snippet.appendText(', ');
|
||||
}
|
||||
hasAddedParameters = true;
|
||||
snippet.appendPlaceholder(part.text);
|
||||
}
|
||||
hasOptionalParameters = hasOptionalParameters || nameIsFollowedByOptionalIndicator;
|
||||
} else if (part.kind === 'punctuation') {
|
||||
if (part.text === '(') {
|
||||
++parenCount;
|
||||
} else if (part.text === ')') {
|
||||
--parenCount;
|
||||
} else if (part.text === '...' && parenCount === 1) {
|
||||
// Found rest parmeter. Do not fill in any further arguments
|
||||
hasOptionalParameters = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasOptionalParameters) {
|
||||
snippet.appendTabstop();
|
||||
}
|
||||
snippet.appendText(')');
|
||||
snippet.appendTabstop(0);
|
||||
return snippet;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DefinitionProvider, TextDocument, Position, CancellationToken, Definition } from 'vscode';
|
||||
|
||||
import DefinitionProviderBase from './definitionProviderBase';
|
||||
|
||||
export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements DefinitionProvider {
|
||||
public provideDefinition(document: TextDocument, position: Position, token: CancellationToken | boolean): Promise<Definition | undefined> {
|
||||
return this.getSymbolLocations('definition', document, position, token);
|
||||
}
|
||||
}
|
||||
@@ -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 { TextDocument, Position, CancellationToken, Location } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptDefinitionProviderBase {
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
protected async getSymbolLocations(
|
||||
definitionType: 'definition' | 'implementation' | 'typeDefinition',
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
token: CancellationToken | boolean
|
||||
): Promise<Location[] | undefined> {
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
|
||||
try {
|
||||
const response = await this.client.execute(definitionType, args, token);
|
||||
const locations: Proto.FileSpan[] = (response && response.body) || [];
|
||||
return locations.map(location =>
|
||||
typeConverters.Location.fromTextSpan(this.client.asUrl(location.file), location));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Diagnostic, DiagnosticCollection, languages, Uri } from 'vscode';
|
||||
|
||||
class DiagnosticSet {
|
||||
private _map: ObjectMap<Diagnostic[]> = Object.create(null);
|
||||
|
||||
public set(
|
||||
file: Uri,
|
||||
diagnostics: Diagnostic[]
|
||||
) {
|
||||
this._map[this.key(file)] = diagnostics;
|
||||
}
|
||||
|
||||
public get(
|
||||
file: Uri
|
||||
): Diagnostic[] {
|
||||
return this._map[this.key(file)] || [];
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._map = Object.create(null);
|
||||
}
|
||||
|
||||
private key(file: Uri): string {
|
||||
return file.toString(true);
|
||||
}
|
||||
}
|
||||
|
||||
export enum DiagnosticKind {
|
||||
Syntax,
|
||||
Semantic,
|
||||
Suggestion
|
||||
}
|
||||
|
||||
const allDiagnosticKinds = [DiagnosticKind.Syntax, DiagnosticKind.Semantic, DiagnosticKind.Suggestion];
|
||||
|
||||
export class DiagnosticsManager {
|
||||
|
||||
private readonly diagnostics = new Map<DiagnosticKind, DiagnosticSet>();
|
||||
private readonly currentDiagnostics: DiagnosticCollection;
|
||||
private _validate: boolean = true;
|
||||
|
||||
constructor(
|
||||
language: string
|
||||
) {
|
||||
for (const kind of allDiagnosticKinds) {
|
||||
this.diagnostics.set(kind, new DiagnosticSet());
|
||||
}
|
||||
|
||||
this.currentDiagnostics = languages.createDiagnosticCollection(language);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.currentDiagnostics.dispose();
|
||||
}
|
||||
|
||||
public reInitialize(): void {
|
||||
this.currentDiagnostics.clear();
|
||||
|
||||
for (const diagnosticSet of this.diagnostics.values()) {
|
||||
diagnosticSet.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public set validate(value: boolean) {
|
||||
if (this._validate === value) {
|
||||
return;
|
||||
}
|
||||
this._validate = value;
|
||||
if (!value) {
|
||||
this.currentDiagnostics.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public diagnosticsReceived(
|
||||
kind: DiagnosticKind,
|
||||
file: Uri,
|
||||
syntaxDiagnostics: Diagnostic[]
|
||||
): void {
|
||||
const diagnostics = this.diagnostics.get(kind);
|
||||
if (diagnostics) {
|
||||
diagnostics.set(file, syntaxDiagnostics);
|
||||
this.updateCurrentDiagnostics(file);
|
||||
}
|
||||
}
|
||||
|
||||
public configFileDiagnosticsReceived(file: Uri, diagnostics: Diagnostic[]): void {
|
||||
this.currentDiagnostics.set(file, diagnostics);
|
||||
}
|
||||
|
||||
public delete(resource: Uri): void {
|
||||
this.currentDiagnostics.delete(resource);
|
||||
}
|
||||
|
||||
private updateCurrentDiagnostics(file: Uri) {
|
||||
if (!this._validate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allDiagnostics = allDiagnosticKinds.reduce((sum, kind) => {
|
||||
sum.push(...this.diagnostics.get(kind)!.get(file));
|
||||
return sum;
|
||||
}, [] as Diagnostic[]);
|
||||
this.currentDiagnostics.set(file, allDiagnostics);
|
||||
}
|
||||
|
||||
public getDiagnostics(file: Uri): Diagnostic[] {
|
||||
return this.currentDiagnostics.get(file) || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Position, CompletionItemProvider, CompletionItemKind, TextDocument, CancellationToken, CompletionItem, Range } from 'vscode';
|
||||
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface Directive {
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const directives: 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.')
|
||||
}
|
||||
];
|
||||
|
||||
export default class DirectiveCommentCompletionProvider implements CompletionItemProvider {
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
) { }
|
||||
|
||||
public provideCompletionItems(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
_token: CancellationToken
|
||||
): CompletionItem[] {
|
||||
if (!this.client.apiVersion.has230Features()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
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) {
|
||||
return directives.map(directive => {
|
||||
const item = new CompletionItem(directive.value, CompletionItemKind.Snippet);
|
||||
item.detail = directive.description;
|
||||
item.range = new Range(position.line, Math.max(0, position.character - (match[1] ? match[1].length : 0)), position.line, position.character);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public resolveCompletionItem(
|
||||
item: CompletionItem,
|
||||
_token: CancellationToken
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -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 { DocumentHighlightProvider, DocumentHighlight, DocumentHighlightKind, TextDocument, Position, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async provideDocumentHighlights(
|
||||
resource: TextDocument,
|
||||
position: Position,
|
||||
token: CancellationToken
|
||||
): Promise<DocumentHighlight[]> {
|
||||
const file = this.client.normalizePath(resource.uri);
|
||||
if (!file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
|
||||
try {
|
||||
const response = await this.client.execute('occurrences', args, token);
|
||||
if (response && response.body) {
|
||||
return response.body
|
||||
.filter(x => !x.isInString)
|
||||
.map(documentHighlightFromOccurance);
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function documentHighlightFromOccurance(occurrence: Proto.OccurrencesResponseItem): DocumentHighlight {
|
||||
return new DocumentHighlight(
|
||||
typeConverters.Range.fromTextSpan(occurrence),
|
||||
occurrence.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read);
|
||||
}
|
||||
@@ -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 { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, CancellationToken, Uri } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
const outlineTypeTable: { [kind: string]: SymbolKind } = Object.create(null);
|
||||
outlineTypeTable[PConst.Kind.module] = SymbolKind.Module;
|
||||
outlineTypeTable[PConst.Kind.class] = SymbolKind.Class;
|
||||
outlineTypeTable[PConst.Kind.enum] = SymbolKind.Enum;
|
||||
outlineTypeTable[PConst.Kind.interface] = SymbolKind.Interface;
|
||||
outlineTypeTable[PConst.Kind.memberFunction] = SymbolKind.Method;
|
||||
outlineTypeTable[PConst.Kind.memberVariable] = SymbolKind.Property;
|
||||
outlineTypeTable[PConst.Kind.memberGetAccessor] = SymbolKind.Property;
|
||||
outlineTypeTable[PConst.Kind.memberSetAccessor] = SymbolKind.Property;
|
||||
outlineTypeTable[PConst.Kind.variable] = SymbolKind.Variable;
|
||||
outlineTypeTable[PConst.Kind.const] = SymbolKind.Variable;
|
||||
outlineTypeTable[PConst.Kind.localVariable] = SymbolKind.Variable;
|
||||
outlineTypeTable[PConst.Kind.variable] = SymbolKind.Variable;
|
||||
outlineTypeTable[PConst.Kind.function] = SymbolKind.Function;
|
||||
outlineTypeTable[PConst.Kind.localFunction] = SymbolKind.Function;
|
||||
|
||||
|
||||
export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient) { }
|
||||
|
||||
public async provideDocumentSymbols(resource: TextDocument, token: CancellationToken): Promise<SymbolInformation[]> {
|
||||
const filepath = this.client.normalizePath(resource.uri);
|
||||
if (!filepath) {
|
||||
return [];
|
||||
}
|
||||
const args: Proto.FileRequestArgs = {
|
||||
file: filepath
|
||||
};
|
||||
|
||||
try {
|
||||
const result: SymbolInformation[] = [];
|
||||
if (this.client.apiVersion.has206Features()) {
|
||||
const response = await this.client.execute('navtree', args, token);
|
||||
if (response.body) {
|
||||
// The root represents the file. Ignore this when showing in the UI
|
||||
let tree = response.body;
|
||||
if (tree.childItems) {
|
||||
tree.childItems.forEach(item => TypeScriptDocumentSymbolProvider.convertNavTree(resource.uri, result, item));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const response = await this.client.execute('navbar', args, token);
|
||||
if (response.body) {
|
||||
let foldingMap: ObjectMap<SymbolInformation> = Object.create(null);
|
||||
response.body.forEach(item => TypeScriptDocumentSymbolProvider.convertNavBar(resource.uri, 0, foldingMap, result, item));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static convertNavBar(resource: Uri, indent: number, foldingMap: ObjectMap<SymbolInformation>, bucket: SymbolInformation[], item: Proto.NavigationBarItem, containerLabel?: string): void {
|
||||
let realIndent = indent + item.indent;
|
||||
let key = `${realIndent}|${item.text}`;
|
||||
if (realIndent !== 0 && !foldingMap[key] && TypeScriptDocumentSymbolProvider.shouldInclueEntry(item.text)) {
|
||||
let result = new SymbolInformation(item.text,
|
||||
outlineTypeTable[item.kind as string] || SymbolKind.Variable,
|
||||
containerLabel ? containerLabel : '',
|
||||
typeConverters.Location.fromTextSpan(resource, item.spans[0]));
|
||||
foldingMap[key] = result;
|
||||
bucket.push(result);
|
||||
}
|
||||
if (item.childItems && item.childItems.length > 0) {
|
||||
for (const child of item.childItems) {
|
||||
TypeScriptDocumentSymbolProvider.convertNavBar(resource, realIndent + 1, foldingMap, bucket, child, item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static convertNavTree(resource: Uri, bucket: SymbolInformation[], item: Proto.NavigationTree, containerLabel?: string): void {
|
||||
const result = new SymbolInformation(item.text,
|
||||
outlineTypeTable[item.kind as string] || SymbolKind.Variable,
|
||||
containerLabel ? containerLabel : '',
|
||||
typeConverters.Location.fromTextSpan(resource, item.spans[0])
|
||||
);
|
||||
if (item.childItems && item.childItems.length > 0) {
|
||||
for (const child of item.childItems) {
|
||||
TypeScriptDocumentSymbolProvider.convertNavTree(resource, bucket, child, result.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (TypeScriptDocumentSymbolProvider.shouldInclueEntry(result.name)) {
|
||||
bucket.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
private static shouldInclueEntry(name: string): boolean {
|
||||
return !!(name && name !== '<function>' && name !== '<class>');
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
|
||||
export default class TypeScriptFoldingProvider implements vscode.FoldingProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
async provideFoldingRanges(
|
||||
document: vscode.TextDocument,
|
||||
_context: vscode.FoldingContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.FoldingRangeList | undefined> {
|
||||
if (!this.client.apiVersion.has280Features()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args: Proto.FileRequestArgs = { file };
|
||||
const response: Proto.OutliningSpansResponse = await this.client.execute('getOutliningSpans', args, token);
|
||||
if (!response || !response.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new vscode.FoldingRangeList(response.body.map(span => {
|
||||
const range = typeConverters.Range.fromTextSpan(span.textSpan);
|
||||
return new vscode.FoldingRange(range.start.line, range.end.line);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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 { workspace as Workspace, FormattingOptions, TextDocument, CancellationToken, window, Disposable, workspace } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as languageIds from '../utils/languageModeIds';
|
||||
|
||||
namespace FormattingConfiguration {
|
||||
export function equals(a: Proto.FormatCodeSettings, b: Proto.FormatCodeSettings): boolean {
|
||||
let keys = Object.keys(a);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
if ((a as any)[key] !== (b as any)[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default class FormattingConfigurationManager {
|
||||
private onDidCloseTextDocumentSub: Disposable | undefined;
|
||||
private formatOptions: { [key: string]: Proto.FormatCodeSettings | undefined; } = Object.create(null);
|
||||
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) {
|
||||
this.onDidCloseTextDocumentSub = Workspace.onDidCloseTextDocument((textDocument) => {
|
||||
const key = textDocument.uri.toString();
|
||||
// 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.
|
||||
delete this.formatOptions[key];
|
||||
});
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.onDidCloseTextDocumentSub) {
|
||||
this.onDidCloseTextDocumentSub.dispose();
|
||||
this.onDidCloseTextDocumentSub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async ensureFormatOptionsForDocument(
|
||||
document: TextDocument,
|
||||
token: CancellationToken | undefined
|
||||
): Promise<void> {
|
||||
const editor = window.visibleTextEditors.find(editor => editor.document.fileName === document.fileName);
|
||||
if (editor) {
|
||||
const formattingOptions = {
|
||||
tabSize: editor.options.tabSize,
|
||||
insertSpaces: editor.options.insertSpaces
|
||||
} as FormattingOptions;
|
||||
return this.ensureFormatOptions(document, formattingOptions, token);
|
||||
}
|
||||
}
|
||||
|
||||
public async ensureFormatOptions(
|
||||
document: TextDocument,
|
||||
options: FormattingOptions,
|
||||
token: CancellationToken | undefined
|
||||
): Promise<void> {
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = document.uri.toString();
|
||||
const cachedOptions = this.formatOptions[key];
|
||||
const formatOptions = this.getFormatOptions(document, options);
|
||||
|
||||
if (cachedOptions && FormattingConfiguration.equals(cachedOptions, formatOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args: Proto.ConfigureRequestArguments = {
|
||||
file: file,
|
||||
formatOptions: formatOptions
|
||||
};
|
||||
await this.client.execute('configure', args, token);
|
||||
this.formatOptions[key] = formatOptions;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.formatOptions = Object.create(null);
|
||||
}
|
||||
|
||||
private getFormatOptions(
|
||||
document: TextDocument,
|
||||
options: FormattingOptions
|
||||
): Proto.FormatCodeSettings {
|
||||
const config = workspace.getConfiguration(
|
||||
document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact
|
||||
? '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'),
|
||||
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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DocumentRangeFormattingEditProvider, OnTypeFormattingEditProvider, FormattingOptions, TextDocument, Position, Range, CancellationToken, TextEdit, WorkspaceConfiguration, Disposable, languages, workspace, DocumentSelector } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import FormattingConfigurationManager from './formattingConfigurationManager';
|
||||
|
||||
export class TypeScriptFormattingProvider implements DocumentRangeFormattingEditProvider, OnTypeFormattingEditProvider {
|
||||
private enabled: boolean = true;
|
||||
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
private readonly formattingOptionsManager: FormattingConfigurationManager
|
||||
) { }
|
||||
|
||||
public updateConfiguration(config: WorkspaceConfiguration): void {
|
||||
this.enabled = config.get('format.enable', true);
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
private async doFormat(
|
||||
document: TextDocument,
|
||||
options: FormattingOptions,
|
||||
args: Proto.FormatRequestArgs,
|
||||
token: CancellationToken
|
||||
): Promise<TextEdit[]> {
|
||||
await this.formattingOptionsManager.ensureFormatOptions(document, options, token);
|
||||
try {
|
||||
const response = await this.client.execute('format', args, token);
|
||||
if (response.body) {
|
||||
return response.body.map(typeConverters.TextEdit.fromCodeEdit);
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public async provideDocumentRangeFormattingEdits(
|
||||
document: TextDocument,
|
||||
range: Range,
|
||||
options: FormattingOptions,
|
||||
token: CancellationToken
|
||||
): Promise<TextEdit[]> {
|
||||
const absPath = this.client.normalizePath(document.uri);
|
||||
if (!absPath) {
|
||||
return [];
|
||||
}
|
||||
const args: Proto.FormatRequestArgs = {
|
||||
file: absPath,
|
||||
line: range.start.line + 1,
|
||||
offset: range.start.character + 1,
|
||||
endLine: range.end.line + 1,
|
||||
endOffset: range.end.character + 1
|
||||
};
|
||||
return this.doFormat(document, options, args, token);
|
||||
}
|
||||
|
||||
public async provideOnTypeFormattingEdits(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
ch: string,
|
||||
options: FormattingOptions,
|
||||
token: CancellationToken
|
||||
): Promise<TextEdit[]> {
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.formattingOptionsManager.ensureFormatOptions(document, options, token);
|
||||
|
||||
const args: Proto.FormatOnKeyRequestArgs = {
|
||||
file: filepath,
|
||||
line: position.line + 1,
|
||||
offset: position.character + 1,
|
||||
key: ch
|
||||
};
|
||||
try {
|
||||
const response = await this.client.execute('formatonkey', args, token);
|
||||
const edits = response.body;
|
||||
const result: TextEdit[] = [];
|
||||
if (!edits) {
|
||||
return result;
|
||||
}
|
||||
for (const edit of edits) {
|
||||
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;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class FormattingProviderManager {
|
||||
private formattingProviderRegistration: Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly modeId: string,
|
||||
private readonly formattingProvider: TypeScriptFormattingProvider,
|
||||
private readonly selector: DocumentSelector
|
||||
) { }
|
||||
|
||||
public dispose() {
|
||||
if (this.formattingProviderRegistration) {
|
||||
this.formattingProviderRegistration.dispose();
|
||||
this.formattingProviderRegistration = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public updateConfiguration(): void {
|
||||
const config = workspace.getConfiguration(this.modeId);
|
||||
this.formattingProvider.updateConfiguration(config);
|
||||
|
||||
if (!this.formattingProvider.isEnabled() && this.formattingProviderRegistration) {
|
||||
this.formattingProviderRegistration.dispose();
|
||||
this.formattingProviderRegistration = undefined;
|
||||
} else if (this.formattingProvider.isEnabled() && !this.formattingProviderRegistration) {
|
||||
this.formattingProviderRegistration = languages.registerDocumentRangeFormattingEditProvider(this.selector, this.formattingProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { HoverProvider, Hover, TextDocument, Position, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import { tagsMarkdownPreview } from '../utils/previewer';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptHoverProvider implements HoverProvider {
|
||||
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async provideHover(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
token: CancellationToken
|
||||
): Promise<Hover | undefined> {
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return undefined;
|
||||
}
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
|
||||
try {
|
||||
const response = await this.client.execute('quickinfo', args, token);
|
||||
if (response && response.body) {
|
||||
const data = response.body;
|
||||
return new Hover(
|
||||
TypeScriptHoverProvider.getContents(data),
|
||||
typeConverters.Range.fromTextSpan(data));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static getContents(
|
||||
data: Proto.QuickInfoResponseBody
|
||||
) {
|
||||
const parts = [];
|
||||
|
||||
if (data.displayString) {
|
||||
parts.push({ language: 'typescript', value: data.displayString });
|
||||
}
|
||||
|
||||
const tags = tagsMarkdownPreview(data.tags);
|
||||
parts.push(data.documentation + (tags ? '\n\n' + tags : ''));
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ImplementationProvider, TextDocument, Position, CancellationToken, Definition } from 'vscode';
|
||||
|
||||
import DefinitionProviderBase from './definitionProviderBase';
|
||||
|
||||
export default class TypeScriptImplementationProvider extends DefinitionProviderBase implements ImplementationProvider {
|
||||
public provideImplementation(document: TextDocument, position: Position, token: CancellationToken | boolean): Promise<Definition | undefined> {
|
||||
return this.getSymbolLocations('implementation', document, position, token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeLens, CancellationToken, TextDocument, Range, Location, workspace } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
|
||||
import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens, CachedNavTreeResponse } from './baseCodeLensProvider';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
|
||||
public constructor(
|
||||
client: ITypeScriptServiceClient,
|
||||
private readonly language: string,
|
||||
cachedResponse: CachedNavTreeResponse
|
||||
) {
|
||||
super(client, cachedResponse);
|
||||
}
|
||||
|
||||
public updateConfiguration(): void {
|
||||
const config = workspace.getConfiguration(this.language);
|
||||
this.setEnabled(config.get('implementationsCodeLens.enabled', false));
|
||||
}
|
||||
|
||||
public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
||||
if (!this.client.apiVersion.has220Features()) {
|
||||
return [];
|
||||
}
|
||||
return super.provideCodeLenses(document, token);
|
||||
}
|
||||
|
||||
public resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise<CodeLens> {
|
||||
const codeLens = inputCodeLens as ReferencesCodeLens;
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
|
||||
return this.client.execute('implementation', args, token).then(response => {
|
||||
if (!response || !response.body) {
|
||||
throw codeLens;
|
||||
}
|
||||
|
||||
const locations = response.body
|
||||
.map(reference =>
|
||||
// Only take first line on implementation: https://github.com/Microsoft/vscode/issues/23924
|
||||
new Location(this.client.asUrl(reference.file),
|
||||
reference.start.line === reference.end.line
|
||||
? typeConverters.Range.fromTextSpan(reference)
|
||||
: new Range(
|
||||
reference.start.line - 1, reference.start.offset - 1,
|
||||
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 = {
|
||||
title: locations.length === 1
|
||||
? localize('oneImplementationLabel', '1 implementation')
|
||||
: localize('manyImplementationLabel', '{0} implementations', locations.length),
|
||||
command: locations.length ? 'editor.action.showReferences' : '',
|
||||
arguments: [codeLens.document, codeLens.range.start, locations]
|
||||
};
|
||||
return codeLens;
|
||||
}).catch(() => {
|
||||
codeLens.command = {
|
||||
title: localize('implementationsErrorLabel', 'Could not determine implementations'),
|
||||
command: ''
|
||||
};
|
||||
return codeLens;
|
||||
});
|
||||
}
|
||||
|
||||
protected extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
_parent: Proto.NavigationTree | null
|
||||
): Range | null {
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.interface:
|
||||
return super.getSymbolRange(document, item);
|
||||
|
||||
case PConst.Kind.class:
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
if (item.kindModifiers.match(/\babstract\b/g)) {
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Position, Range, CompletionItemProvider, CompletionItemKind, TextDocument, CancellationToken, CompletionItem, window, Uri, TextEditor, SnippetString, workspace } from 'vscode';
|
||||
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as Proto from '../protocol';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import { Command, CommandManager } from '../utils/commandManager';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const configurationNamespace = 'jsDocCompletion';
|
||||
|
||||
namespace Configuration {
|
||||
export const enabled = 'enabled';
|
||||
}
|
||||
|
||||
class JsDocCompletionItem extends CompletionItem {
|
||||
constructor(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
shouldGetJSDocFromTSServer: boolean,
|
||||
) {
|
||||
super('/** */', CompletionItemKind.Snippet);
|
||||
this.detail = localize('typescript.jsDocCompletionItem.documentation', 'JSDoc comment');
|
||||
this.insertText = '';
|
||||
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);
|
||||
this.range = new Range(
|
||||
start,
|
||||
position.translate(0, suffix ? suffix[0].length : 0));
|
||||
|
||||
this.command = {
|
||||
title: 'Try Complete JSDoc',
|
||||
command: TryCompleteJsDocCommand.COMMAND_NAME,
|
||||
arguments: [document.uri, start, shouldGetJSDocFromTSServer]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class JsDocCompletionProvider implements CompletionItemProvider {
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
commandManager: CommandManager
|
||||
) {
|
||||
commandManager.register(new TryCompleteJsDocCommand(client));
|
||||
}
|
||||
|
||||
public async provideCompletionItems(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
token: CancellationToken
|
||||
): Promise<CompletionItem[]> {
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: unregister provider when disabled
|
||||
const enableJsDocCompletions = workspace.getConfiguration(configurationNamespace, document.uri).get<boolean>(Configuration.enabled, true);
|
||||
if (!enableJsDocCompletions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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 (prefix.match(/^\s*$|\/\*\*\s*$|^\s*\/\*\*+\s*$/) === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args: Proto.FileRequestArgs = {
|
||||
file
|
||||
};
|
||||
const response = await Promise.race([
|
||||
this.client.execute('navtree', args, token),
|
||||
new Promise<Proto.NavTreeResponse>((resolve) => setTimeout(resolve, 250))
|
||||
]);
|
||||
if (!response || !response.body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const body = response.body;
|
||||
|
||||
function matchesPosition(tree: Proto.NavigationTree): boolean {
|
||||
if (!tree.spans.length) {
|
||||
return false;
|
||||
}
|
||||
const span = typeConverters.Range.fromTextSpan(tree.spans[0]);
|
||||
if (position.line === span.start.line - 1 || position.line === span.start.line) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return tree.childItems ? tree.childItems.some(matchesPosition) : false;
|
||||
}
|
||||
|
||||
if (!matchesPosition(body)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [new JsDocCompletionItem(document, position, enableJsDocCompletions)];
|
||||
}
|
||||
|
||||
public resolveCompletionItem(item: CompletionItem, _token: CancellationToken) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
class TryCompleteJsDocCommand implements Command {
|
||||
public static readonly COMMAND_NAME = '_typeScript.tryCompleteJsDoc';
|
||||
public readonly id = TryCompleteJsDocCommand.COMMAND_NAME;
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Try to insert a jsdoc comment, using a template provide by typescript
|
||||
* if possible, otherwise falling back to a default comment format.
|
||||
*/
|
||||
public async execute(resource: Uri, start: Position, shouldGetJSDocFromTSServer: boolean): Promise<boolean> {
|
||||
const file = this.client.normalizePath(resource);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor || editor.document.uri.fsPath !== resource.fsPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shouldGetJSDocFromTSServer) {
|
||||
return this.tryInsertDefaultDoc(editor, start);
|
||||
}
|
||||
|
||||
const didInsertFromTemplate = await this.tryInsertJsDocFromTemplate(editor, file, start);
|
||||
if (didInsertFromTemplate) {
|
||||
return true;
|
||||
}
|
||||
return this.tryInsertDefaultDoc(editor, start);
|
||||
}
|
||||
|
||||
private async tryInsertJsDocFromTemplate(editor: TextEditor, file: string, position: Position): Promise<boolean> {
|
||||
const snippet = await TryCompleteJsDocCommand.getSnippetTemplate(this.client, file, position);
|
||||
if (!snippet) {
|
||||
return false;
|
||||
}
|
||||
return editor.insertSnippet(
|
||||
snippet,
|
||||
position,
|
||||
{ undoStopBefore: false, undoStopAfter: true });
|
||||
}
|
||||
|
||||
public static getSnippetTemplate(client: ITypeScriptServiceClient, file: string, position: Position): Promise<SnippetString | undefined> {
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
|
||||
return Promise.race([
|
||||
client.execute('docCommentTemplate', args),
|
||||
new Promise<Proto.DocCommandTemplateResponse>((_, reject) => setTimeout(reject, 250))
|
||||
]).then((res: Proto.DocCommandTemplateResponse) => {
|
||||
if (!res || !res.body) {
|
||||
return undefined;
|
||||
}
|
||||
// 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 (res.body.newText === '/** */') {
|
||||
return undefined;
|
||||
}
|
||||
return TryCompleteJsDocCommand.templateToSnippet(res.body.newText);
|
||||
}, () => undefined);
|
||||
}
|
||||
|
||||
private static templateToSnippet(template: string): SnippetString {
|
||||
// TODO: use append placeholder
|
||||
let snippetIndex = 1;
|
||||
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 SnippetString(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the default JSDoc
|
||||
*/
|
||||
private tryInsertDefaultDoc(editor: TextEditor, position: Position): Thenable<boolean> {
|
||||
const snippet = new SnippetString(`/**\n * $0\n */`);
|
||||
return editor.insertSnippet(snippet, position, { undoStopBefore: false, undoStopAfter: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Command } from '../utils/commandManager';
|
||||
import * as typeconverts from '../utils/typeConverters';
|
||||
|
||||
import { isSupportedLanguageMode } from '../utils/languageModeIds';
|
||||
import API from '../utils/api';
|
||||
import { Lazy } from '../utils/lazy';
|
||||
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
|
||||
|
||||
export class OrganizeImportsCommand implements Command {
|
||||
public static readonly ID = 'typescript.organizeImports';
|
||||
public readonly id = OrganizeImportsCommand.ID;
|
||||
|
||||
constructor(
|
||||
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
|
||||
) { }
|
||||
|
||||
public async execute(): Promise<boolean> {
|
||||
// Don't force activation
|
||||
if (!this.lazyClientHost.hasValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = this.lazyClientHost.value.serviceClient;
|
||||
if (!client.apiVersion.has280Features()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || !isSupportedLanguageMode(editor.document)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const file = client.normalizePath(editor.document.uri);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const args: Proto.OrganizeImportsRequestArgs = {
|
||||
scope: {
|
||||
type: 'file',
|
||||
args: {
|
||||
file
|
||||
}
|
||||
}
|
||||
};
|
||||
const response = await client.execute('organizeImports', args);
|
||||
if (!response || !response.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const edits = typeconverts.WorkspaceEdit.fromFromFileCodeEdits(client, response.body);
|
||||
return await vscode.workspace.applyEdit(edits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When clause context set when the ts version supports organize imports.
|
||||
*/
|
||||
const contextName = 'typescript.canOrganizeImports';
|
||||
|
||||
export class OrganizeImportsContextManager {
|
||||
|
||||
private currentValue: boolean = false;
|
||||
|
||||
public onDidChangeApiVersion(apiVersion: API): any {
|
||||
this.updateContext(apiVersion.has280Features());
|
||||
}
|
||||
|
||||
private updateContext(newValue: boolean) {
|
||||
if (newValue === this.currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('setContext', contextName, newValue);
|
||||
this.currentValue = newValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import FormattingConfigurationManager from './formattingConfigurationManager';
|
||||
import { getEditForCodeAction, applyCodeActionCommands } from '../utils/codeAction';
|
||||
import { Command, CommandManager } from '../utils/commandManager';
|
||||
import { DiagnosticsManager } from './diagnostics';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
class ApplyCodeActionCommand implements Command {
|
||||
public static readonly ID = '_typescript.applyCodeActionCommand';
|
||||
public readonly id = ApplyCodeActionCommand.ID;
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async execute(
|
||||
actions: Proto.CodeAction
|
||||
): Promise<boolean> {
|
||||
return applyCodeActionCommands(this.client, actions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ApplyFixAllCodeAction implements Command {
|
||||
public static readonly ID = '_typescript.applyFixAllCodeAction';
|
||||
public readonly id = ApplyFixAllCodeAction.ID;
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async execute(
|
||||
file: string,
|
||||
tsAction: Proto.CodeFixAction,
|
||||
): Promise<void> {
|
||||
if (!tsAction.fixId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args: Proto.GetCombinedCodeFixRequestArgs = {
|
||||
scope: {
|
||||
type: 'file',
|
||||
args: { file }
|
||||
},
|
||||
fixId: tsAction.fixId
|
||||
};
|
||||
|
||||
try {
|
||||
const combinedCodeFixesResponse = await this.client.execute('getCombinedCodeFix', args);
|
||||
if (!combinedCodeFixesResponse.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const edit = typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, combinedCodeFixesResponse.body.changes);
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
|
||||
if (combinedCodeFixesResponse.command) {
|
||||
await vscode.commands.executeCommand(ApplyCodeActionCommand.ID, combinedCodeFixesResponse.command);
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
class SupportedCodeActionProvider {
|
||||
private _supportedCodeActions?: Thenable<Set<number>>;
|
||||
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async getFixableDiagnosticsForContext(context: vscode.CodeActionContext): Promise<vscode.Diagnostic[]> {
|
||||
const supportedActions = await this.supportedCodeActions;
|
||||
const fixableDiagnostics = DiagnosticsSet.from(context.diagnostics.filter(diagnostic => supportedActions.has(+diagnostic.code)));
|
||||
return Array.from(fixableDiagnostics.values);
|
||||
}
|
||||
|
||||
private get supportedCodeActions(): Thenable<Set<number>> {
|
||||
if (!this._supportedCodeActions) {
|
||||
this._supportedCodeActions = this.client.execute('getSupportedCodeFixes', null, undefined)
|
||||
.then(response => response.body || [])
|
||||
.then(codes => codes.map(code => +code).filter(code => !isNaN(code)))
|
||||
.then(codes => new Set(codes));
|
||||
}
|
||||
return this._supportedCodeActions;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TypeScriptQuickFixProvider implements vscode.CodeActionProvider {
|
||||
|
||||
private readonly supportedCodeActionProvider: SupportedCodeActionProvider;
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
private readonly formattingConfigurationManager: FormattingConfigurationManager,
|
||||
commandManager: CommandManager,
|
||||
private readonly diagnosticsManager: DiagnosticsManager
|
||||
) {
|
||||
commandManager.register(new ApplyCodeActionCommand(client));
|
||||
commandManager.register(new ApplyFixAllCodeAction(client));
|
||||
|
||||
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
|
||||
}
|
||||
|
||||
public async provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
_range: vscode.Range,
|
||||
context: vscode.CodeActionContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CodeAction[]> {
|
||||
if (!this.client.apiVersion.has213Features()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(context);
|
||||
if (!fixableDiagnostics.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.formattingConfigurationManager.ensureFormatOptionsForDocument(document, token);
|
||||
|
||||
const results: vscode.CodeAction[] = [];
|
||||
for (const diagnostic of fixableDiagnostics) {
|
||||
results.push(...await this.getFixesForDiagnostic(document, file, diagnostic, token));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private async getFixesForDiagnostic(
|
||||
document: vscode.TextDocument,
|
||||
file: string,
|
||||
diagnostic: vscode.Diagnostic,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<Iterable<vscode.CodeAction>> {
|
||||
const args: Proto.CodeFixRequestArgs = {
|
||||
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
|
||||
errorCodes: [+diagnostic.code]
|
||||
};
|
||||
const codeFixesResponse = await this.client.execute('getCodeFixes', args, token);
|
||||
if (codeFixesResponse.body) {
|
||||
const results: vscode.CodeAction[] = [];
|
||||
for (const tsCodeFix of codeFixesResponse.body) {
|
||||
results.push(...await this.getAllFixesForTsCodeAction(document, file, diagnostic, tsCodeFix));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getAllFixesForTsCodeAction(
|
||||
document: vscode.TextDocument,
|
||||
file: string,
|
||||
diagnostic: vscode.Diagnostic,
|
||||
tsAction: Proto.CodeFixAction
|
||||
): Promise<Iterable<vscode.CodeAction>> {
|
||||
const singleFix = this.getSingleFixForTsCodeAction(diagnostic, tsAction);
|
||||
const fixAll = await this.getFixAllForTsCodeAction(document, file, diagnostic, tsAction);
|
||||
return fixAll ? [singleFix, fixAll] : [singleFix];
|
||||
}
|
||||
|
||||
private getSingleFixForTsCodeAction(
|
||||
diagnostic: vscode.Diagnostic,
|
||||
tsAction: Proto.CodeFixAction
|
||||
): vscode.CodeAction {
|
||||
const codeAction = new vscode.CodeAction(tsAction.description, vscode.CodeActionKind.QuickFix);
|
||||
codeAction.edit = getEditForCodeAction(this.client, tsAction);
|
||||
codeAction.diagnostics = [diagnostic];
|
||||
if (tsAction.commands) {
|
||||
codeAction.command = {
|
||||
command: ApplyCodeActionCommand.ID,
|
||||
arguments: [tsAction],
|
||||
title: tsAction.description
|
||||
};
|
||||
}
|
||||
return codeAction;
|
||||
}
|
||||
|
||||
private async getFixAllForTsCodeAction(
|
||||
document: vscode.TextDocument,
|
||||
file: string,
|
||||
diagnostic: vscode.Diagnostic,
|
||||
tsAction: Proto.CodeFixAction,
|
||||
): Promise<vscode.CodeAction | undefined> {
|
||||
if (!tsAction.fixId || !this.client.apiVersion.has270Features()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Make sure there are multiple diagnostics of the same type in the file
|
||||
if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => x.code === diagnostic.code && x !== diagnostic)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = new vscode.CodeAction(
|
||||
localize('fixAllInFileLabel', '{0} (Fix all in file)', tsAction.description),
|
||||
vscode.CodeActionKind.QuickFix);
|
||||
action.diagnostics = [diagnostic];
|
||||
|
||||
action.command = {
|
||||
command: ApplyFixAllCodeAction.ID,
|
||||
arguments: [file, tsAction],
|
||||
title: ''
|
||||
};
|
||||
return action;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
import FormattingOptionsManager from './formattingConfigurationManager';
|
||||
import { CommandManager, Command } from '../utils/commandManager';
|
||||
|
||||
class ApplyRefactoringCommand implements Command {
|
||||
public static readonly ID = '_typescript.applyRefactoring';
|
||||
public readonly id = ApplyRefactoringCommand.ID;
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
private readonly formattingOptionsManager: FormattingOptionsManager
|
||||
) { }
|
||||
|
||||
public async execute(
|
||||
document: vscode.TextDocument,
|
||||
file: string,
|
||||
refactor: string,
|
||||
action: string,
|
||||
range: vscode.Range
|
||||
): Promise<boolean> {
|
||||
await this.formattingOptionsManager.ensureFormatOptionsForDocument(document, undefined);
|
||||
|
||||
const args: Proto.GetEditsForRefactorRequestArgs = {
|
||||
...typeConverters.Range.toFileRangeRequestArgs(file, range),
|
||||
refactor,
|
||||
action
|
||||
};
|
||||
const response = await this.client.execute('getEditsForRefactor', args);
|
||||
if (!response || !response.body || !response.body.edits.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const edit = typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, response.body.edits);
|
||||
if (!(await vscode.workspace.applyEdit(edit))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renameLocation = response.body.renameLocation;
|
||||
if (renameLocation) {
|
||||
if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.uri.fsPath === document.uri.fsPath) {
|
||||
const pos = typeConverters.Position.fromLocation(renameLocation);
|
||||
vscode.window.activeTextEditor.selection = new vscode.Selection(pos, pos);
|
||||
await vscode.commands.executeCommand('editor.action.rename');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class SelectRefactorCommand implements Command {
|
||||
public static readonly ID = '_typescript.selectRefactoring';
|
||||
public readonly id = SelectRefactorCommand.ID;
|
||||
|
||||
constructor(
|
||||
private readonly doRefactoring: ApplyRefactoringCommand
|
||||
) { }
|
||||
|
||||
public async execute(
|
||||
document: vscode.TextDocument,
|
||||
file: string,
|
||||
info: Proto.ApplicableRefactorInfo,
|
||||
range: vscode.Range
|
||||
): Promise<boolean> {
|
||||
const selected = await vscode.window.showQuickPick(info.actions.map((action): vscode.QuickPickItem => ({
|
||||
label: action.name,
|
||||
description: action.description
|
||||
})));
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
return this.doRefactoring.execute(document, file, info.name, selected.label, range);
|
||||
}
|
||||
}
|
||||
|
||||
export default class TypeScriptRefactorProvider implements vscode.CodeActionProvider {
|
||||
private static readonly extractFunctionKind = vscode.CodeActionKind.RefactorExtract.append('function');
|
||||
private static readonly extractConstantKind = vscode.CodeActionKind.RefactorExtract.append('constant');
|
||||
|
||||
constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
formattingOptionsManager: FormattingOptionsManager,
|
||||
commandManager: CommandManager
|
||||
) {
|
||||
const doRefactoringCommand = commandManager.register(new ApplyRefactoringCommand(this.client, formattingOptionsManager));
|
||||
commandManager.register(new SelectRefactorCommand(doRefactoringCommand));
|
||||
}
|
||||
|
||||
public async provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
_range: vscode.Range,
|
||||
context: vscode.CodeActionContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CodeAction[]> {
|
||||
if (!this.client.apiVersion.has240Features()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!vscode.window.activeTextEditor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file || editor.document.uri.fsPath !== document.uri.fsPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (editor.selection.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const range = editor.selection;
|
||||
const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(file, range);
|
||||
try {
|
||||
const response = await this.client.execute('getApplicableRefactors', args, token);
|
||||
if (!response || !response.body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: vscode.CodeAction[] = [];
|
||||
for (const info of response.body) {
|
||||
if (info.inlineable === false) {
|
||||
const codeAction = new vscode.CodeAction(info.description, vscode.CodeActionKind.Refactor);
|
||||
codeAction.command = {
|
||||
title: info.description,
|
||||
command: SelectRefactorCommand.ID,
|
||||
arguments: [document, file, info, range]
|
||||
};
|
||||
actions.push(codeAction);
|
||||
} else {
|
||||
for (const action of info.actions) {
|
||||
const codeAction = new vscode.CodeAction(action.description, TypeScriptRefactorProvider.getKind(action));
|
||||
codeAction.command = {
|
||||
title: action.description,
|
||||
command: ApplyRefactoringCommand.ID,
|
||||
arguments: [document, file, info.name, action.name, range]
|
||||
};
|
||||
actions.push(codeAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static getKind(refactor: Proto.RefactorActionInfo) {
|
||||
if (refactor.name.startsWith('function_')) {
|
||||
return TypeScriptRefactorProvider.extractFunctionKind;
|
||||
} else if (refactor.name.startsWith('constant_')) {
|
||||
return TypeScriptRefactorProvider.extractConstantKind;
|
||||
}
|
||||
return vscode.CodeActionKind.Refactor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ReferenceProvider, Location, TextDocument, Position, CancellationToken } from 'vscode';
|
||||
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptReferenceSupport implements ReferenceProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient) { }
|
||||
|
||||
public async provideReferences(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
options: { includeDeclaration: boolean },
|
||||
token: CancellationToken
|
||||
): Promise<Location[]> {
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
|
||||
try {
|
||||
const msg = await this.client.execute('references', args, token);
|
||||
if (!msg.body) {
|
||||
return [];
|
||||
}
|
||||
const result: Location[] = [];
|
||||
const has203Features = this.client.apiVersion.has203Features();
|
||||
for (const ref of msg.body.refs) {
|
||||
if (!options.includeDeclaration && has203Features && ref.isDefinition) {
|
||||
continue;
|
||||
}
|
||||
const url = this.client.asUrl(ref.file);
|
||||
const location = typeConverters.Location.fromTextSpan(url, ref);
|
||||
result.push(location);
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeLens, CancellationToken, TextDocument, Range, workspace } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import * as PConst from '../protocol.const';
|
||||
|
||||
import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens, CachedNavTreeResponse } from './baseCodeLensProvider';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider {
|
||||
public constructor(
|
||||
client: ITypeScriptServiceClient,
|
||||
private readonly language: string,
|
||||
cachedResponse: CachedNavTreeResponse
|
||||
) {
|
||||
super(client, cachedResponse);
|
||||
}
|
||||
|
||||
public updateConfiguration(): void {
|
||||
const config = workspace.getConfiguration(this.language);
|
||||
this.setEnabled(config.get('referencesCodeLens.enabled', false));
|
||||
}
|
||||
|
||||
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
||||
if (!this.client.apiVersion.has206Features()) {
|
||||
return [];
|
||||
}
|
||||
return super.provideCodeLenses(document, token);
|
||||
}
|
||||
|
||||
public resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise<CodeLens> {
|
||||
const codeLens = inputCodeLens as ReferencesCodeLens;
|
||||
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
|
||||
return this.client.execute('references', args, token).then(response => {
|
||||
if (!response || !response.body) {
|
||||
throw codeLens;
|
||||
}
|
||||
|
||||
const locations = response.body.refs
|
||||
.map(reference =>
|
||||
typeConverters.Location.fromTextSpan(this.client.asUrl(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: locations.length === 1
|
||||
? localize('oneReferenceLabel', '1 reference')
|
||||
: localize('manyReferenceLabel', '{0} references', locations.length),
|
||||
command: locations.length ? 'editor.action.showReferences' : '',
|
||||
arguments: [codeLens.document, codeLens.range.start, locations]
|
||||
};
|
||||
return codeLens;
|
||||
}).catch(() => {
|
||||
codeLens.command = {
|
||||
title: localize('referenceErrorLabel', 'Could not determine references'),
|
||||
command: ''
|
||||
};
|
||||
return codeLens;
|
||||
});
|
||||
}
|
||||
|
||||
protected extractSymbol(
|
||||
document: TextDocument,
|
||||
item: Proto.NavigationTree,
|
||||
parent: Proto.NavigationTree | null
|
||||
): Range | null {
|
||||
if (parent && parent.kind === PConst.Kind.enum) {
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
|
||||
switch (item.kind) {
|
||||
case PConst.Kind.const:
|
||||
case PConst.Kind.let:
|
||||
case PConst.Kind.variable:
|
||||
case PConst.Kind.function:
|
||||
// Only show references for exported variables
|
||||
if (!item.kindModifiers.match(/\bexport\b/)) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.class:
|
||||
if (item.text === '<class>') {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case PConst.Kind.memberFunction:
|
||||
case PConst.Kind.memberVariable:
|
||||
case PConst.Kind.memberGetAccessor:
|
||||
case PConst.Kind.memberSetAccessor:
|
||||
case PConst.Kind.constructorImplementation:
|
||||
case PConst.Kind.interface:
|
||||
case PConst.Kind.type:
|
||||
case PConst.Kind.enum:
|
||||
return super.getSymbolRange(document, item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RenameProvider, WorkspaceEdit, TextDocument, Position, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptRenameProvider implements RenameProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async provideRenameEdits(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
newName: string,
|
||||
token: CancellationToken
|
||||
): Promise<WorkspaceEdit | null> {
|
||||
const file = this.client.normalizePath(document.uri);
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const args: Proto.RenameRequestArgs = {
|
||||
...typeConverters.Position.toFileLocationRequestArgs(file, position),
|
||||
findInStrings: false,
|
||||
findInComments: false
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.client.execute('rename', args, token);
|
||||
if (!response.body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renameInfo = response.body.info;
|
||||
if (!renameInfo.canRename) {
|
||||
return Promise.reject<WorkspaceEdit>(renameInfo.localizedErrorMessage);
|
||||
}
|
||||
|
||||
return this.toWorkspaceEdit(response.body.locs, newName);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private toWorkspaceEdit(
|
||||
locations: ReadonlyArray<Proto.SpanGroup>,
|
||||
newName: string
|
||||
) {
|
||||
const result = new WorkspaceEdit();
|
||||
for (const spanGroup of locations) {
|
||||
const resource = this.client.asUrl(spanGroup.file);
|
||||
if (resource) {
|
||||
for (const textSpan of spanGroup.locs) {
|
||||
result.replace(resource, typeConverters.Range.fromTextSpan(textSpan), newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SignatureHelpProvider, SignatureHelp, SignatureInformation, ParameterInformation, TextDocument, Position, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Previewer from '../utils/previewer';
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
export default class TypeScriptSignatureHelpProvider implements SignatureHelpProvider {
|
||||
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient
|
||||
) { }
|
||||
|
||||
public async provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Promise<SignatureHelp | undefined | null> {
|
||||
const filepath = this.client.normalizePath(document.uri);
|
||||
if (!filepath) {
|
||||
return null;
|
||||
}
|
||||
const args: Proto.SignatureHelpRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
|
||||
|
||||
const response = await this.client.execute('signatureHelp', args, token);
|
||||
const info = response.body;
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = new SignatureHelp();
|
||||
result.activeSignature = info.selectedItemIndex;
|
||||
result.activeParameter = info.argumentIndex;
|
||||
|
||||
info.items.forEach((item, i) => {
|
||||
// keep active parameter in bounds
|
||||
if (i === info.selectedItemIndex && item.isVariadic) {
|
||||
result.activeParameter = Math.min(info.argumentIndex, item.parameters.length - 1);
|
||||
}
|
||||
|
||||
const signature = new SignatureInformation('');
|
||||
signature.label += Previewer.plain(item.prefixDisplayParts);
|
||||
|
||||
item.parameters.forEach((p, i, a) => {
|
||||
const parameter = new ParameterInformation(
|
||||
Previewer.plain(p.displayParts),
|
||||
Previewer.plain(p.documentation));
|
||||
|
||||
signature.label += parameter.label;
|
||||
signature.parameters.push(parameter);
|
||||
if (i < a.length - 1) {
|
||||
signature.label += Previewer.plain(item.separatorDisplayParts);
|
||||
}
|
||||
});
|
||||
signature.label += Previewer.plain(item.suffixDisplayParts);
|
||||
signature.documentation = Previewer.markdownDocumentation(item.documentation, item.tags);
|
||||
result.signatures.push(signature);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider';
|
||||
import { isImplicitProjectConfigFile } from '../utils/tsconfig';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Lazy } from '../utils/lazy';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
type AutoDetect = 'on' | 'off' | 'build' | 'watch';
|
||||
|
||||
|
||||
const exists = (file: string): Promise<boolean> =>
|
||||
new Promise<boolean>((resolve, _reject) => {
|
||||
fs.exists(file, (value: boolean) => {
|
||||
resolve(value);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
interface TypeScriptTaskDefinition extends vscode.TaskDefinition {
|
||||
tsconfig: string;
|
||||
option?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides tasks for building `tsconfig.json` files in a project.
|
||||
*/
|
||||
class TscTaskProvider implements vscode.TaskProvider {
|
||||
private autoDetect: AutoDetect = 'on';
|
||||
private readonly tsconfigProvider: TsConfigProvider;
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly client: Lazy<ITypeScriptServiceClient>
|
||||
) {
|
||||
this.tsconfigProvider = new TsConfigProvider();
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
|
||||
this.onConfigurationChanged();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(x => x.dispose());
|
||||
}
|
||||
|
||||
public async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!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.path)) {
|
||||
configPaths.add(project.path);
|
||||
tasks.push(...(await this.getTasksForProject(project)));
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public resolveTask(_task: vscode.Task): vscode.Task | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getAllTsConfigs(token: vscode.CancellationToken): Promise<TSConfig[]> {
|
||||
const out = new Set<TSConfig>();
|
||||
const configs = (await this.getTsConfigForActiveFile(token)).concat(await this.getTsConfigsInWorkspace());
|
||||
for (const config of configs) {
|
||||
if (await exists(config.path)) {
|
||||
out.add(config);
|
||||
}
|
||||
}
|
||||
return Array.from(out);
|
||||
}
|
||||
|
||||
private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise<TSConfig[]> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) {
|
||||
const uri = editor.document.uri;
|
||||
return [{
|
||||
path: uri.fsPath,
|
||||
workspaceFolder: vscode.workspace.getWorkspaceFolder(uri)
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
const file = this.getActiveTypeScriptFile();
|
||||
if (!file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res: Proto.ProjectInfoResponse = await this.client.value.execute(
|
||||
'projectInfo',
|
||||
{ file, needFileNameList: false },
|
||||
token);
|
||||
|
||||
if (!res || !res.body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { configFileName } = res.body;
|
||||
if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
|
||||
const normalizedConfigPath = path.normalize(configFileName);
|
||||
const uri = vscode.Uri.file(normalizedConfigPath);
|
||||
const folder = vscode.workspace.getWorkspaceFolder(uri);
|
||||
return [{
|
||||
path: normalizedConfigPath,
|
||||
workspaceFolder: folder
|
||||
}];
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getTsConfigsInWorkspace(): Promise<TSConfig[]> {
|
||||
return Array.from(await this.tsconfigProvider.getConfigsForWorkspace());
|
||||
}
|
||||
|
||||
private static async getCommand(project: TSConfig): Promise<string> {
|
||||
if (project.workspaceFolder) {
|
||||
const localTsc = await TscTaskProvider.getLocalTscAtPath(path.dirname(project.path));
|
||||
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(path.join(bin, 'tsc.cmd'))) {
|
||||
return path.join(bin, 'tsc.cmd');
|
||||
} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(bin, 'tsc'))) {
|
||||
return path.join(bin, 'tsc');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getActiveTypeScriptFile(): string | null {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
const document = editor.document;
|
||||
if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
|
||||
return this.client.value.normalizePath(document.uri);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getTasksForProject(project: TSConfig): Promise<vscode.Task[]> {
|
||||
const command = await TscTaskProvider.getCommand(project);
|
||||
const label = this.getLabelForTasks(project);
|
||||
|
||||
const tasks: vscode.Task[] = [];
|
||||
|
||||
if (this.autoDetect === 'build' || this.autoDetect === 'on') {
|
||||
const buildTaskidentifier: TypeScriptTaskDefinition = { type: 'typescript', tsconfig: label };
|
||||
const buildTask = new vscode.Task(
|
||||
buildTaskidentifier,
|
||||
project.workspaceFolder || vscode.TaskScope.Workspace,
|
||||
localize('buildTscLabel', 'build - {0}', label),
|
||||
'tsc',
|
||||
new vscode.ShellExecution(command, ['-p', project.path]),
|
||||
'$tsc');
|
||||
buildTask.group = vscode.TaskGroup.Build;
|
||||
buildTask.isBackground = false;
|
||||
tasks.push(buildTask);
|
||||
}
|
||||
|
||||
if (this.autoDetect === 'watch' || this.autoDetect === 'on') {
|
||||
const watchTaskidentifier: TypeScriptTaskDefinition = { type: 'typescript', tsconfig: label, option: 'watch' };
|
||||
const watchTask = new vscode.Task(
|
||||
watchTaskidentifier,
|
||||
project.workspaceFolder || vscode.TaskScope.Workspace,
|
||||
localize('buildAndWatchTscLabel', 'watch - {0}', label),
|
||||
'tsc',
|
||||
new vscode.ShellExecution(command, ['--watch', '-p', project.path]),
|
||||
'$tsc-watch');
|
||||
watchTask.group = vscode.TaskGroup.Build;
|
||||
watchTask.isBackground = true;
|
||||
tasks.push(watchTask);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private getLabelForTasks(project: TSConfig): string {
|
||||
if (project.workspaceFolder) {
|
||||
const projectFolder = project.workspaceFolder;
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const relativePath = path.relative(project.workspaceFolder.uri.fsPath, project.path);
|
||||
if (workspaceFolders && workspaceFolders.length > 1) {
|
||||
// Use absolute path when we have multiple folders with the same name
|
||||
if (workspaceFolders.filter(x => x.name === projectFolder.name).length > 1) {
|
||||
return path.join(project.workspaceFolder.uri.fsPath, relativePath);
|
||||
} else {
|
||||
return path.join(project.workspaceFolder.name, relativePath);
|
||||
}
|
||||
} else {
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
return project.path;
|
||||
}
|
||||
|
||||
private onConfigurationChanged(): void {
|
||||
const type = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
|
||||
this.autoDetect = typeof type === 'undefined' ? 'on' : type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages registrations of TypeScript task providers with VS Code.
|
||||
*/
|
||||
export default class TypeScriptTaskProviderManager {
|
||||
private taskProviderSub: vscode.Disposable | undefined = undefined;
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly client: Lazy<ITypeScriptServiceClient>
|
||||
) {
|
||||
vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
|
||||
this.onConfigurationChanged();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.taskProviderSub) {
|
||||
this.taskProviderSub.dispose();
|
||||
this.taskProviderSub = undefined;
|
||||
}
|
||||
this.disposables.forEach(x => x.dispose());
|
||||
}
|
||||
|
||||
private onConfigurationChanged() {
|
||||
const autoDetect = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
|
||||
if (this.taskProviderSub && autoDetect === 'off') {
|
||||
this.taskProviderSub.dispose();
|
||||
this.taskProviderSub = undefined;
|
||||
} else if (!this.taskProviderSub && autoDetect !== 'off') {
|
||||
this.taskProviderSub = vscode.workspace.registerTaskProvider('typescript', new TscTaskProvider(this.client));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TypeDefinitionProvider, TextDocument, Position, CancellationToken, Definition } from 'vscode';
|
||||
|
||||
import DefinitionProviderBase from './definitionProviderBase';
|
||||
|
||||
export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements TypeDefinitionProvider {
|
||||
public provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken | boolean): Promise<Definition | undefined> {
|
||||
return this.getSymbolLocations('typeDefinition', document, position, token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, SymbolKind, CancellationToken } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from '../utils/typeConverters';
|
||||
|
||||
function getSymbolKind(item: Proto.NavtoItem): SymbolKind {
|
||||
switch (item.kind) {
|
||||
case 'method': return SymbolKind.Method;
|
||||
case 'enum': return SymbolKind.Enum;
|
||||
case 'function': return SymbolKind.Function;
|
||||
case 'class': return SymbolKind.Class;
|
||||
case 'interface': return SymbolKind.Interface;
|
||||
case 'var': return SymbolKind.Variable;
|
||||
default: return SymbolKind.Variable;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbolProvider {
|
||||
public constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
private readonly modeIds: string[]
|
||||
) { }
|
||||
|
||||
public async provideWorkspaceSymbols(
|
||||
search: string,
|
||||
token: CancellationToken
|
||||
): Promise<SymbolInformation[]> {
|
||||
const uri = this.getUri();
|
||||
if (!uri) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filepath = this.client.normalizePath(uri);
|
||||
if (!filepath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args: Proto.NavtoRequestArgs = {
|
||||
file: filepath,
|
||||
searchValue: search
|
||||
};
|
||||
const response = await this.client.execute('navto', args, token);
|
||||
if (!response.body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: SymbolInformation[] = [];
|
||||
for (const item of response.body) {
|
||||
if (!item.containerName && item.kind === 'alias') {
|
||||
continue;
|
||||
}
|
||||
const label = TypeScriptWorkspaceSymbolProvider.getLabel(item);
|
||||
result.push(new SymbolInformation(label, getSymbolKind(item), item.containerName || '',
|
||||
typeConverters.Location.fromTextSpan(this.client.asUrl(item.file), item)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static getLabel(item: Proto.NavtoItem) {
|
||||
let label = item.name;
|
||||
if (item.kind === 'method' || item.kind === 'function') {
|
||||
label += '()';
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
private getUri(): Uri | 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 editor = window.activeTextEditor;
|
||||
if (editor) {
|
||||
const document = editor.document;
|
||||
if (document && this.modeIds.indexOf(document.languageId) >= 0) {
|
||||
return document.uri;
|
||||
}
|
||||
}
|
||||
|
||||
const documents = workspace.textDocuments;
|
||||
for (const document of documents) {
|
||||
if (this.modeIds.indexOf(document.languageId) >= 0) {
|
||||
return document.uri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
243
extensions/typescript-language-features/src/languageProvider.ts
Normal file
243
extensions/typescript-language-features/src/languageProvider.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { languages, workspace, Diagnostic, Disposable, Uri, TextDocument, DocumentFilter } from 'vscode';
|
||||
import { basename } from 'path';
|
||||
|
||||
import TypeScriptServiceClient from './typescriptServiceClient';
|
||||
|
||||
import BufferSyncSupport from './features/bufferSyncSupport';
|
||||
|
||||
import TypingsStatus from './utils/typingsStatus';
|
||||
import FormattingConfigurationManager from './features/formattingConfigurationManager';
|
||||
import * as languageConfigurations from './utils/languageConfigurations';
|
||||
import { CommandManager } from './utils/commandManager';
|
||||
import { DiagnosticsManager, DiagnosticKind } from './features/diagnostics';
|
||||
import { LanguageDescription } from './utils/languageDescription';
|
||||
import * as fileSchemes from './utils/fileSchemes';
|
||||
import { CachedNavTreeResponse } from './features/baseCodeLensProvider';
|
||||
import { memoize } from './utils/memoize';
|
||||
import { disposeAll } from './utils/dipose';
|
||||
|
||||
const validateSetting = 'validate.enable';
|
||||
const foldingSetting = 'typescript.experimental.syntaxFolding';
|
||||
|
||||
export default class LanguageProvider {
|
||||
private readonly diagnosticsManager: DiagnosticsManager;
|
||||
private readonly bufferSyncSupport: BufferSyncSupport;
|
||||
private readonly formattingOptionsManager: FormattingConfigurationManager;
|
||||
|
||||
private readonly toUpdateOnConfigurationChanged: ({ updateConfiguration: () => void })[] = [];
|
||||
|
||||
private _validate: boolean = true;
|
||||
|
||||
private readonly disposables: Disposable[] = [];
|
||||
private readonly versionDependentDisposables: Disposable[] = [];
|
||||
|
||||
private foldingProviderRegistration: Disposable | undefined = void 0;
|
||||
|
||||
constructor(
|
||||
private readonly client: TypeScriptServiceClient,
|
||||
private readonly description: LanguageDescription,
|
||||
commandManager: CommandManager,
|
||||
typingsStatus: TypingsStatus
|
||||
) {
|
||||
this.formattingOptionsManager = new FormattingConfigurationManager(client);
|
||||
this.bufferSyncSupport = new BufferSyncSupport(client, description.modeIds, {
|
||||
delete: (resource) => {
|
||||
this.diagnosticsManager.delete(resource);
|
||||
}
|
||||
}, this._validate);
|
||||
|
||||
this.diagnosticsManager = new DiagnosticsManager(description.id);
|
||||
|
||||
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables);
|
||||
this.configurationChanged();
|
||||
|
||||
client.onReady(async () => {
|
||||
await this.registerProviders(client, commandManager, typingsStatus);
|
||||
this.bufferSyncSupport.listen();
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
disposeAll(this.disposables);
|
||||
disposeAll(this.versionDependentDisposables);
|
||||
|
||||
this.diagnosticsManager.dispose();
|
||||
this.bufferSyncSupport.dispose();
|
||||
this.formattingOptionsManager.dispose();
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get documentSelector(): DocumentFilter[] {
|
||||
const documentSelector = [];
|
||||
for (const language of this.description.modeIds) {
|
||||
for (const scheme of fileSchemes.supportedSchemes) {
|
||||
documentSelector.push({ language, scheme });
|
||||
}
|
||||
}
|
||||
return documentSelector;
|
||||
}
|
||||
|
||||
private async registerProviders(
|
||||
client: TypeScriptServiceClient,
|
||||
commandManager: CommandManager,
|
||||
typingsStatus: TypingsStatus
|
||||
): Promise<void> {
|
||||
const selector = this.documentSelector;
|
||||
const config = workspace.getConfiguration(this.id);
|
||||
|
||||
this.disposables.push(languages.registerCompletionItemProvider(selector,
|
||||
new (await import('./features/completionItemProvider')).default(client, typingsStatus, commandManager),
|
||||
'.', '"', '\'', '/', '@'));
|
||||
|
||||
this.disposables.push(languages.registerCompletionItemProvider(selector, new (await import('./features/directiveCommentCompletionProvider')).default(client), '@'));
|
||||
|
||||
const { TypeScriptFormattingProvider, FormattingProviderManager } = await import('./features/formattingProvider');
|
||||
const formattingProvider = new TypeScriptFormattingProvider(client, this.formattingOptionsManager);
|
||||
formattingProvider.updateConfiguration(config);
|
||||
this.disposables.push(languages.registerOnTypeFormattingEditProvider(selector, formattingProvider, ';', '}', '\n'));
|
||||
|
||||
const formattingProviderManager = new FormattingProviderManager(this.description.id, formattingProvider, selector);
|
||||
formattingProviderManager.updateConfiguration();
|
||||
this.disposables.push(formattingProviderManager);
|
||||
this.toUpdateOnConfigurationChanged.push(formattingProviderManager);
|
||||
|
||||
const cachedResponse = new CachedNavTreeResponse();
|
||||
|
||||
this.disposables.push(languages.registerCompletionItemProvider(selector, new (await import('./features/jsDocCompletionProvider')).default(client, commandManager), '*'));
|
||||
this.disposables.push(languages.registerHoverProvider(selector, new (await import('./features/hoverProvider')).default(client)));
|
||||
this.disposables.push(languages.registerDefinitionProvider(selector, new (await import('./features/definitionProvider')).default(client)));
|
||||
this.disposables.push(languages.registerDocumentHighlightProvider(selector, new (await import('./features/documentHighlightProvider')).default(client)));
|
||||
this.disposables.push(languages.registerReferenceProvider(selector, new (await import('./features/referenceProvider')).default(client)));
|
||||
this.disposables.push(languages.registerDocumentSymbolProvider(selector, new (await import('./features/documentSymbolProvider')).default(client)));
|
||||
this.disposables.push(languages.registerSignatureHelpProvider(selector, new (await import('./features/signatureHelpProvider')).default(client), '(', ','));
|
||||
this.disposables.push(languages.registerRenameProvider(selector, new (await import('./features/renameProvider')).default(client)));
|
||||
this.disposables.push(languages.registerCodeActionsProvider(selector, new (await import('./features/quickFixProvider')).default(client, this.formattingOptionsManager, commandManager, this.diagnosticsManager)));
|
||||
this.disposables.push(languages.registerCodeActionsProvider(selector, new (await import('./features/refactorProvider')).default(client, this.formattingOptionsManager, commandManager)));
|
||||
|
||||
await this.initFoldingProvider();
|
||||
this.disposables.push(workspace.onDidChangeConfiguration(c => {
|
||||
if (c.affectsConfiguration(foldingSetting)) {
|
||||
this.initFoldingProvider();
|
||||
}
|
||||
}));
|
||||
this.disposables.push({ dispose: () => this.foldingProviderRegistration && this.foldingProviderRegistration.dispose() });
|
||||
|
||||
this.registerVersionDependentProviders();
|
||||
|
||||
const referenceCodeLensProvider = new (await import('./features/referencesCodeLensProvider')).default(client, this.description.id, cachedResponse);
|
||||
referenceCodeLensProvider.updateConfiguration();
|
||||
this.toUpdateOnConfigurationChanged.push(referenceCodeLensProvider);
|
||||
this.disposables.push(languages.registerCodeLensProvider(selector, referenceCodeLensProvider));
|
||||
|
||||
const implementationCodeLensProvider = new (await import('./features/implementationsCodeLensProvider')).default(client, this.description.id, cachedResponse);
|
||||
implementationCodeLensProvider.updateConfiguration();
|
||||
this.toUpdateOnConfigurationChanged.push(implementationCodeLensProvider);
|
||||
this.disposables.push(languages.registerCodeLensProvider(selector, implementationCodeLensProvider));
|
||||
|
||||
this.disposables.push(languages.registerWorkspaceSymbolProvider(new (await import('./features/workspaceSymbolProvider')).default(client, this.description.modeIds)));
|
||||
|
||||
if (!this.description.isExternal) {
|
||||
for (const modeId of this.description.modeIds) {
|
||||
this.disposables.push(languages.setLanguageConfiguration(modeId, languageConfigurations.jsTsLanguageConfiguration));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initFoldingProvider(): Promise<void> {
|
||||
let enable = workspace.getConfiguration().get(foldingSetting, false);
|
||||
if (enable) {
|
||||
if (!this.foldingProviderRegistration) {
|
||||
this.foldingProviderRegistration = languages.registerFoldingProvider(this.documentSelector, new (await import('./features/folderingProvider')).default(this.client));
|
||||
}
|
||||
} else {
|
||||
if (this.foldingProviderRegistration) {
|
||||
this.foldingProviderRegistration.dispose();
|
||||
this.foldingProviderRegistration = void 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private configurationChanged(): void {
|
||||
const config = workspace.getConfiguration(this.id);
|
||||
this.updateValidate(config.get(validateSetting, true));
|
||||
|
||||
for (const toUpdate of this.toUpdateOnConfigurationChanged) {
|
||||
toUpdate.updateConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
public handles(resource: Uri, doc: TextDocument): boolean {
|
||||
if (doc && this.description.modeIds.indexOf(doc.languageId) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.bufferSyncSupport.handles(resource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const base = basename(resource.fsPath);
|
||||
return !!base && base === this.description.configFile;
|
||||
}
|
||||
|
||||
private get id(): string {
|
||||
return this.description.id;
|
||||
}
|
||||
|
||||
public get diagnosticSource(): string {
|
||||
return this.description.diagnosticSource;
|
||||
}
|
||||
|
||||
private updateValidate(value: boolean) {
|
||||
if (this._validate === value) {
|
||||
return;
|
||||
}
|
||||
this._validate = value;
|
||||
this.bufferSyncSupport.validate = value;
|
||||
this.diagnosticsManager.validate = value;
|
||||
if (value) {
|
||||
this.triggerAllDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
public reInitialize(): void {
|
||||
this.diagnosticsManager.reInitialize();
|
||||
this.bufferSyncSupport.reOpenDocuments();
|
||||
this.bufferSyncSupport.requestAllDiagnostics();
|
||||
this.formattingOptionsManager.reset();
|
||||
this.registerVersionDependentProviders();
|
||||
}
|
||||
|
||||
private async registerVersionDependentProviders(): Promise<void> {
|
||||
disposeAll(this.versionDependentDisposables);
|
||||
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = this.documentSelector;
|
||||
if (this.client.apiVersion.has220Features()) {
|
||||
this.versionDependentDisposables.push(languages.registerImplementationProvider(selector, new (await import('./features/implementationProvider')).default(this.client)));
|
||||
}
|
||||
|
||||
if (this.client.apiVersion.has213Features()) {
|
||||
this.versionDependentDisposables.push(languages.registerTypeDefinitionProvider(selector, new (await import('./features/typeDefinitionProvider')).default(this.client)));
|
||||
}
|
||||
}
|
||||
|
||||
public triggerAllDiagnostics(): void {
|
||||
this.bufferSyncSupport.requestAllDiagnostics();
|
||||
}
|
||||
|
||||
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: Uri, syntaxDiagnostics: Diagnostic[]): void {
|
||||
this.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file, syntaxDiagnostics);
|
||||
}
|
||||
|
||||
public configFileDiagnosticsReceived(file: Uri, diagnostics: Diagnostic[]): void {
|
||||
this.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 externalModuleName = 'external module name';
|
||||
public static readonly file = 'file';
|
||||
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 memberFunction = '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';
|
||||
}
|
||||
|
||||
|
||||
export class DiagnosticCategory {
|
||||
public static readonly error = 'error';
|
||||
public static readonly warning = 'warning';
|
||||
public static readonly suggestion = 'suggestion';
|
||||
}
|
||||
2
extensions/typescript-language-features/src/protocol.d.ts
vendored
Normal file
2
extensions/typescript-language-features/src/protocol.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import * as Proto from 'typescript/lib/protocol';
|
||||
export = Proto;
|
||||
@@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { workspace, Memento, Diagnostic, Range, Disposable, Uri, DiagnosticSeverity } from 'vscode';
|
||||
|
||||
import * as Proto from './protocol';
|
||||
import * as PConst from './protocol.const';
|
||||
|
||||
import TypeScriptServiceClient from './typescriptServiceClient';
|
||||
import LanguageProvider from './languageProvider';
|
||||
|
||||
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
|
||||
import VersionStatus from './utils/versionStatus';
|
||||
import { TypeScriptServerPlugin } from './utils/plugins';
|
||||
import * as typeConverters from './utils/typeConverters';
|
||||
import { CommandManager } from './utils/commandManager';
|
||||
import { LanguageDescription } from './utils/languageDescription';
|
||||
import LogDirectoryProvider from './utils/logDirectoryProvider';
|
||||
import { disposeAll } from './utils/dipose';
|
||||
import { DiagnosticKind } from './features/diagnostics';
|
||||
|
||||
// Style check diagnostics that can be reported as warnings
|
||||
const styleCheckDiagnostics = [
|
||||
6133, // variable is declared but never used
|
||||
6138, // property is declared but its value is never read
|
||||
7027, // unreachable code detected
|
||||
7028, // unused label
|
||||
7029, // fall through case in switch
|
||||
7030 // not all code paths return a value
|
||||
];
|
||||
|
||||
export default class TypeScriptServiceClientHost {
|
||||
private readonly ataProgressReporter: AtaProgressReporter;
|
||||
private readonly typingsStatus: TypingsStatus;
|
||||
private readonly client: TypeScriptServiceClient;
|
||||
private readonly languages: LanguageProvider[] = [];
|
||||
private readonly languagePerId = new Map<string, LanguageProvider>();
|
||||
private readonly disposables: Disposable[] = [];
|
||||
private readonly versionStatus: VersionStatus;
|
||||
private reportStyleCheckAsWarnings: boolean = true;
|
||||
|
||||
constructor(
|
||||
descriptions: LanguageDescription[],
|
||||
workspaceState: Memento,
|
||||
plugins: TypeScriptServerPlugin[],
|
||||
private readonly commandManager: CommandManager,
|
||||
logDirectoryProvider: LogDirectoryProvider
|
||||
) {
|
||||
const handleProjectCreateOrDelete = () => {
|
||||
this.client.execute('reloadProjects', null, false);
|
||||
this.triggerAllDiagnostics();
|
||||
};
|
||||
const handleProjectChange = () => {
|
||||
setTimeout(() => {
|
||||
this.triggerAllDiagnostics();
|
||||
}, 1500);
|
||||
};
|
||||
const configFileWatcher = workspace.createFileSystemWatcher('**/[tj]sconfig.json');
|
||||
this.disposables.push(configFileWatcher);
|
||||
configFileWatcher.onDidCreate(handleProjectCreateOrDelete, this, this.disposables);
|
||||
configFileWatcher.onDidDelete(handleProjectCreateOrDelete, this, this.disposables);
|
||||
configFileWatcher.onDidChange(handleProjectChange, this, this.disposables);
|
||||
|
||||
this.client = new TypeScriptServiceClient(workspaceState, version => this.versionStatus.onDidChangeTypeScriptVersion(version), plugins, logDirectoryProvider);
|
||||
this.disposables.push(this.client);
|
||||
|
||||
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.versionStatus = new VersionStatus(resource => this.client.normalizePath(resource));
|
||||
this.disposables.push(this.versionStatus);
|
||||
|
||||
this.typingsStatus = new TypingsStatus(this.client);
|
||||
this.ataProgressReporter = new AtaProgressReporter(this.client);
|
||||
|
||||
for (const description of descriptions) {
|
||||
const manager = new LanguageProvider(this.client, description, this.commandManager, this.typingsStatus);
|
||||
this.languages.push(manager);
|
||||
this.disposables.push(manager);
|
||||
this.languagePerId.set(description.id, manager);
|
||||
}
|
||||
|
||||
this.client.ensureServiceStarted();
|
||||
this.client.onReady(() => {
|
||||
if (!this.client.apiVersion.has230Features()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const languages = new Set<string>();
|
||||
for (const plugin of plugins) {
|
||||
for (const language of plugin.languages) {
|
||||
languages.add(language);
|
||||
}
|
||||
}
|
||||
if (languages.size) {
|
||||
const description: LanguageDescription = {
|
||||
id: 'typescript-plugins',
|
||||
modeIds: Array.from(languages.values()),
|
||||
diagnosticSource: 'ts-plugins',
|
||||
isExternal: true
|
||||
};
|
||||
const manager = new LanguageProvider(this.client, description, this.commandManager, this.typingsStatus);
|
||||
this.languages.push(manager);
|
||||
this.disposables.push(manager);
|
||||
this.languagePerId.set(description.id, manager);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.onTsServerStarted(() => {
|
||||
this.triggerAllDiagnostics();
|
||||
});
|
||||
|
||||
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables);
|
||||
this.configurationChanged();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
disposeAll(this.disposables);
|
||||
this.typingsStatus.dispose();
|
||||
this.ataProgressReporter.dispose();
|
||||
}
|
||||
|
||||
public get serviceClient(): TypeScriptServiceClient {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public reloadProjects(): void {
|
||||
this.client.execute('reloadProjects', null, false);
|
||||
this.triggerAllDiagnostics();
|
||||
}
|
||||
|
||||
public handles(resource: Uri): boolean {
|
||||
return !!this.findLanguage(resource);
|
||||
}
|
||||
|
||||
private configurationChanged(): void {
|
||||
const config = workspace.getConfiguration('typescript');
|
||||
this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true);
|
||||
}
|
||||
|
||||
private async findLanguage(resource: Uri): Promise<LanguageProvider | undefined> {
|
||||
try {
|
||||
const doc = await 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 {
|
||||
// See https://github.com/Microsoft/TypeScript/issues/5530
|
||||
workspace.saveAll(false).then(() => {
|
||||
for (const language of this.languagePerId.values()) {
|
||||
language.reInitialize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async diagnosticsReceived(
|
||||
kind: DiagnosticKind,
|
||||
resource: 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.asUrl(body.configFile))).then(language => {
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
if (body.diagnostics.length === 0) {
|
||||
language.configFileDiagnosticsReceived(this.client.asUrl(body.configFile), []);
|
||||
} else if (body.diagnostics.length >= 1) {
|
||||
workspace.openTextDocument(Uri.file(body.configFile)).then((document) => {
|
||||
let curly: [number, number, number] | undefined = undefined;
|
||||
let nonCurly: [number, number, number] | undefined = undefined;
|
||||
let diagnostic: Diagnostic;
|
||||
for (let index = 0; index < document.lineCount; index++) {
|
||||
const line = document.lineAt(index);
|
||||
const text = line.text;
|
||||
const firstNonWhitespaceCharacterIndex = line.firstNonWhitespaceCharacterIndex;
|
||||
if (firstNonWhitespaceCharacterIndex < text.length) {
|
||||
if (text.charAt(firstNonWhitespaceCharacterIndex) === '{') {
|
||||
curly = [index, firstNonWhitespaceCharacterIndex, firstNonWhitespaceCharacterIndex + 1];
|
||||
break;
|
||||
} else {
|
||||
const matches = /\s*([^\s]*)(?:\s*|$)/.exec(text.substr(firstNonWhitespaceCharacterIndex));
|
||||
if (matches && matches.length >= 1) {
|
||||
nonCurly = [index, firstNonWhitespaceCharacterIndex, firstNonWhitespaceCharacterIndex + matches[1].length];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const match = curly || nonCurly;
|
||||
if (match) {
|
||||
diagnostic = new Diagnostic(new Range(match[0], match[1], match[0], match[2]), body.diagnostics[0].text);
|
||||
} else {
|
||||
diagnostic = new Diagnostic(new Range(0, 0, 0, 0), body.diagnostics[0].text);
|
||||
}
|
||||
if (diagnostic) {
|
||||
diagnostic.source = language.diagnosticSource;
|
||||
language.configFileDiagnosticsReceived(this.client.asUrl(body.configFile), [diagnostic]);
|
||||
}
|
||||
}, _error => {
|
||||
language.configFileDiagnosticsReceived(this.client.asUrl(body.configFile), [new Diagnostic(new Range(0, 0, 0, 0), body.diagnostics[0].text)]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createMarkerDatas(diagnostics: Proto.Diagnostic[], source: string): Diagnostic[] {
|
||||
return diagnostics.map(tsDiag => this.tsDiagnosticToVsDiagnostic(tsDiag, source));
|
||||
}
|
||||
|
||||
private tsDiagnosticToVsDiagnostic(diagnostic: Proto.Diagnostic, source: string) {
|
||||
const { start, end, text } = diagnostic;
|
||||
const range = new Range(typeConverters.Position.fromLocation(start), typeConverters.Position.fromLocation(end));
|
||||
const converted = new Diagnostic(range, text);
|
||||
converted.severity = this.getDiagnosticSeverity(diagnostic);
|
||||
converted.source = diagnostic.source || source;
|
||||
if (diagnostic.code) {
|
||||
converted.code = diagnostic.code;
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): DiagnosticSeverity {
|
||||
if (this.reportStyleCheckAsWarnings && this.isStyleCheckDiagnostic(diagnostic.code)) {
|
||||
return DiagnosticSeverity.Warning;
|
||||
}
|
||||
|
||||
switch (diagnostic.category) {
|
||||
case PConst.DiagnosticCategory.error:
|
||||
return DiagnosticSeverity.Error;
|
||||
|
||||
case PConst.DiagnosticCategory.warning:
|
||||
return DiagnosticSeverity.Warning;
|
||||
|
||||
case PConst.DiagnosticCategory.suggestion:
|
||||
return DiagnosticSeverity.Hint;
|
||||
|
||||
default:
|
||||
return DiagnosticSeverity.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private isStyleCheckDiagnostic(code: number | undefined): boolean {
|
||||
return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, Uri, Event } from 'vscode';
|
||||
import * as Proto from './protocol';
|
||||
import API from './utils/api';
|
||||
import { TypeScriptServerPlugin } from './utils/plugins';
|
||||
import { TypeScriptServiceConfiguration } from './utils/configuration';
|
||||
import Logger from './utils/logger';
|
||||
|
||||
export interface ITypeScriptServiceClient {
|
||||
normalizePath(resource: Uri): string | null;
|
||||
asUrl(filepath: string): Uri;
|
||||
getWorkspaceRootForResource(resource: Uri): string | undefined;
|
||||
|
||||
onTsServerStarted: Event<API>;
|
||||
onProjectLanguageServiceStateChanged: Event<Proto.ProjectLanguageServiceStateEventBody>;
|
||||
onDidBeginInstallTypings: Event<Proto.BeginInstallTypesEventBody>;
|
||||
onDidEndInstallTypings: Event<Proto.EndInstallTypesEventBody>;
|
||||
onTypesInstallerInitializationFailed: Event<Proto.TypesInstallerInitializationFailedEventBody>;
|
||||
|
||||
apiVersion: API;
|
||||
plugins: TypeScriptServerPlugin[];
|
||||
configuration: TypeScriptServiceConfiguration;
|
||||
logger: Logger;
|
||||
|
||||
execute(command: 'configure', args: Proto.ConfigureRequestArguments, token?: CancellationToken): Promise<Proto.ConfigureResponse>;
|
||||
execute(command: 'open', args: Proto.OpenRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'close', args: Proto.FileRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'change', args: Proto.ChangeRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'geterr', args: Proto.GeterrRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'quickinfo', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.QuickInfoResponse>;
|
||||
execute(command: 'completions', args: Proto.CompletionsRequestArgs, token?: CancellationToken): Promise<Proto.CompletionsResponse>;
|
||||
execute(command: 'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken): Promise<Proto.CompletionDetailsResponse>;
|
||||
execute(command: 'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken): Promise<Proto.SignatureHelpResponse>;
|
||||
execute(command: 'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.DefinitionResponse>;
|
||||
execute(command: 'implementation', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.ImplementationResponse>;
|
||||
execute(command: 'typeDefinition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.TypeDefinitionResponse>;
|
||||
execute(command: 'references', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.ReferencesResponse>;
|
||||
execute(command: 'navto', args: Proto.NavtoRequestArgs, token?: CancellationToken): Promise<Proto.NavtoResponse>;
|
||||
execute(command: 'navbar', args: Proto.FileRequestArgs, token?: CancellationToken): Promise<Proto.NavBarResponse>;
|
||||
execute(command: 'format', args: Proto.FormatRequestArgs, token?: CancellationToken): Promise<Proto.FormatResponse>;
|
||||
execute(command: 'formatonkey', args: Proto.FormatOnKeyRequestArgs, token?: CancellationToken): Promise<Proto.FormatResponse>;
|
||||
execute(command: 'rename', args: Proto.RenameRequestArgs, token?: CancellationToken): Promise<Proto.RenameResponse>;
|
||||
execute(command: 'occurrences', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.OccurrencesResponse>;
|
||||
execute(command: 'projectInfo', args: Proto.ProjectInfoRequestArgs, token?: CancellationToken): Promise<Proto.ProjectInfoResponse>;
|
||||
execute(command: 'reloadProjects', args: any, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'reload', args: Proto.ReloadRequestArgs, expectedResult: boolean, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'compilerOptionsForInferredProjects', args: Proto.SetCompilerOptionsForInferredProjectsArgs, token?: CancellationToken): Promise<any>;
|
||||
execute(command: 'navtree', args: Proto.FileRequestArgs, token?: CancellationToken): Promise<Proto.NavTreeResponse>;
|
||||
execute(command: 'getCodeFixes', args: Proto.CodeFixRequestArgs, token?: CancellationToken): Promise<Proto.GetCodeFixesResponse>;
|
||||
execute(command: 'getSupportedCodeFixes', args: null, token?: CancellationToken): Promise<Proto.GetSupportedCodeFixesResponse>;
|
||||
execute(command: 'getCombinedCodeFix', args: Proto.GetCombinedCodeFixRequestArgs, token?: CancellationToken): Promise<Proto.GetCombinedCodeFixResponse>;
|
||||
execute(command: 'docCommentTemplate', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.DocCommandTemplateResponse>;
|
||||
execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise<Proto.GetApplicableRefactorsResponse>;
|
||||
execute(command: 'getEditsForRefactor', args: Proto.GetEditsForRefactorRequestArgs, token?: CancellationToken): Promise<Proto.GetEditsForRefactorResponse>;
|
||||
execute(command: 'applyCodeActionCommand', args: Proto.ApplyCodeActionCommandRequestArgs, token?: CancellationToken): Promise<Proto.ApplyCodeActionCommandResponse>;
|
||||
execute(command: 'organizeImports', args: Proto.OrganizeImportsRequestArgs, token?: CancellationToken): Promise<Proto.OrganizeImportsResponse>;
|
||||
execute(command: 'getOutliningSpans', args: Proto.FileRequestArgs, token: CancellationToken): Promise<Proto.OutliningSpansResponse>;
|
||||
execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise<any>;
|
||||
}
|
||||
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;
|
||||
}
|
||||
8
extensions/typescript-language-features/src/typings/ref.d.ts
vendored
Normal file
8
extensions/typescript-language-features/src/typings/ref.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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
104
extensions/typescript-language-features/src/utils/api.ts
Normal file
104
extensions/typescript-language-features/src/utils/api.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
|
||||
import { memoize } from './memoize';
|
||||
|
||||
|
||||
export default class API {
|
||||
public static readonly defaultVersion = new API('1.0.0', '1.0.0');
|
||||
|
||||
public static fromVersionString(versionString: string): API {
|
||||
let version = semver.valid(versionString);
|
||||
if (!version) {
|
||||
return new API(localize('invalidVersion', 'invalid version'), '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);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly versionString: string,
|
||||
private readonly version: string
|
||||
) { }
|
||||
|
||||
@memoize
|
||||
public has203Features(): boolean {
|
||||
return semver.gte(this.version, '2.0.3');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has206Features(): boolean {
|
||||
return semver.gte(this.version, '2.0.6');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has208Features(): boolean {
|
||||
return semver.gte(this.version, '2.0.8');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has213Features(): boolean {
|
||||
return semver.gte(this.version, '2.1.3');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has220Features(): boolean {
|
||||
return semver.gte(this.version, '2.2.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has222Features(): boolean {
|
||||
return semver.gte(this.version, '2.2.2');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has230Features(): boolean {
|
||||
return semver.gte(this.version, '2.3.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has234Features(): boolean {
|
||||
return semver.gte(this.version, '2.3.4');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has240Features(): boolean {
|
||||
return semver.gte(this.version, '2.4.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has250Features(): boolean {
|
||||
return semver.gte(this.version, '2.5.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has260Features(): boolean {
|
||||
return semver.gte(this.version, '2.6.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has262Features(): boolean {
|
||||
return semver.gte(this.version, '2.6.2');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has270Features(): boolean {
|
||||
return semver.gte(this.version, '2.7.0');
|
||||
}
|
||||
|
||||
@memoize
|
||||
public has280Features(): boolean {
|
||||
return semver.gte(this.version, '2.8.0');
|
||||
}
|
||||
}
|
||||
17
extensions/typescript-language-features/src/utils/arrays.ts
Normal file
17
extensions/typescript-language-features/src/utils/arrays.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
|
||||
if (one.length !== other.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = one.length; i < len; i++) {
|
||||
if (!itemEquals(one[i], other[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
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 | Thenable<T>) => 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>((resolve) => {
|
||||
this.onSuccess = resolve;
|
||||
}).then(() => {
|
||||
this.completionPromise = null;
|
||||
this.onSuccess = null;
|
||||
var 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,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { WorkspaceEdit, workspace } from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import * as typeConverters from './typeConverters';
|
||||
|
||||
export function getEditForCodeAction(
|
||||
client: ITypeScriptServiceClient,
|
||||
action: Proto.CodeAction
|
||||
): WorkspaceEdit | undefined {
|
||||
return action.changes && action.changes.length
|
||||
? typeConverters.WorkspaceEdit.fromFromFileCodeEdits(client, action.changes)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export async function applyCodeAction(
|
||||
client: ITypeScriptServiceClient,
|
||||
action: Proto.CodeAction
|
||||
): Promise<boolean> {
|
||||
const workspaceEdit = getEditForCodeAction(client, action);
|
||||
if (workspaceEdit) {
|
||||
if (!(await workspace.applyEdit(workspaceEdit))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return applyCodeActionCommands(client, action);
|
||||
}
|
||||
|
||||
export async function applyCodeActionCommands(
|
||||
client: ITypeScriptServiceClient,
|
||||
action: Proto.CodeAction
|
||||
): Promise<boolean> {
|
||||
if (action.commands && action.commands.length) {
|
||||
for (const command of action.commands) {
|
||||
const response = await client.execute('applyCodeActionCommand', { command });
|
||||
if (!response || !response.body) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
export interface Command {
|
||||
readonly id: 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 {
|
||||
this.registerCommand(command.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,130 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { WorkspaceConfiguration, workspace } from 'vscode';
|
||||
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 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: string[];
|
||||
public readonly checkJs: boolean;
|
||||
public readonly experimentalDecorators: boolean;
|
||||
public readonly disableAutomaticTypeAcquisition: boolean;
|
||||
|
||||
public static loadFromWorkspace(): TypeScriptServiceConfiguration {
|
||||
return new TypeScriptServiceConfiguration();
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
const configuration = 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static extractGlobalTsdk(configuration: WorkspaceConfiguration): string | null {
|
||||
const inspect = configuration.inspect('typescript.tsdk');
|
||||
if (inspect && inspect.globalValue && 'string' === typeof inspect.globalValue) {
|
||||
return inspect.globalValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static extractLocalTsdk(configuration: WorkspaceConfiguration): string | null {
|
||||
const inspect = configuration.inspect('typescript.tsdk');
|
||||
if (inspect && inspect.workspaceValue && 'string' === typeof inspect.workspaceValue) {
|
||||
return inspect.workspaceValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readTsServerLogLevel(configuration: WorkspaceConfiguration): TsServerLogLevel {
|
||||
const setting = configuration.get<string>('typescript.tsserver.log', 'off');
|
||||
return TsServerLogLevel.fromString(setting);
|
||||
}
|
||||
|
||||
private static readTsServerPluginPaths(configuration: WorkspaceConfiguration): string[] {
|
||||
return configuration.get<string[]>('typescript.tsserver.pluginPaths', []);
|
||||
}
|
||||
|
||||
private static readCheckJs(configuration: WorkspaceConfiguration): boolean {
|
||||
return configuration.get<boolean>('javascript.implicitProjectConfig.checkJs', false);
|
||||
}
|
||||
|
||||
private static readExperimentalDecorators(configuration: WorkspaceConfiguration): boolean {
|
||||
return configuration.get<boolean>('javascript.implicitProjectConfig.experimentalDecorators', false);
|
||||
}
|
||||
|
||||
private static readNpmLocation(configuration: WorkspaceConfiguration): string | null {
|
||||
return configuration.get<string | null>('typescript.npm', null);
|
||||
}
|
||||
|
||||
private static readDisableAutomaticTypeAcquisition(configuration: WorkspaceConfiguration): boolean {
|
||||
return configuration.get<boolean>('typescript.disableAutomaticTypeAcquisition', false);
|
||||
}
|
||||
|
||||
private static extractLocale(configuration: WorkspaceConfiguration): string | null {
|
||||
return configuration.get<string | null>('typescript.locale', null);
|
||||
}
|
||||
}
|
||||
15
extensions/typescript-language-features/src/utils/dipose.ts
Normal file
15
extensions/typescript-language-features/src/utils/dipose.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
153
extensions/typescript-language-features/src/utils/electron.ts
Normal file
153
extensions/typescript-language-features/src/utils/electron.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import path = require('path');
|
||||
import os = require('os');
|
||||
import net = require('net');
|
||||
import cp = require('child_process');
|
||||
import Logger from './logger';
|
||||
|
||||
export interface IForkOptions {
|
||||
cwd?: string;
|
||||
execArgv?: string[];
|
||||
}
|
||||
|
||||
export function makeRandomHexString(length: number): string {
|
||||
let chars = ['0', '1', '2', '3', '4', '5', '6', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const idx = Math.floor(chars.length * Math.random());
|
||||
result += chars[idx];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generatePipeName(): string {
|
||||
return getPipeName(makeRandomHexString(40));
|
||||
}
|
||||
|
||||
function getPipeName(name: string): string {
|
||||
const fullName = 'vscode-' + name;
|
||||
if (process.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\' + fullName + '-sock';
|
||||
}
|
||||
|
||||
// Mac/Unix: use socket file
|
||||
return path.join(os.tmpdir(), fullName + '.sock');
|
||||
}
|
||||
|
||||
export function getTempFile(name: string): string {
|
||||
const fullName = 'vscode-' + name;
|
||||
return path.join(os.tmpdir(), fullName + '.sock');
|
||||
}
|
||||
|
||||
|
||||
function generatePatchedEnv(
|
||||
env: any,
|
||||
stdInPipeName: string,
|
||||
stdOutPipeName: string,
|
||||
stdErrPipeName: string
|
||||
): any {
|
||||
const newEnv = Object.assign({}, env);
|
||||
|
||||
// Set the two unique pipe names and the electron flag as process env
|
||||
newEnv['STDIN_PIPE_NAME'] = stdInPipeName;
|
||||
newEnv['STDOUT_PIPE_NAME'] = stdOutPipeName;
|
||||
newEnv['STDERR_PIPE_NAME'] = stdErrPipeName;
|
||||
newEnv['ELECTRON_RUN_AS_NODE'] = '1';
|
||||
|
||||
// Ensure we always have a PATH set
|
||||
newEnv['PATH'] = newEnv['PATH'] || process.env.PATH;
|
||||
return newEnv;
|
||||
}
|
||||
|
||||
export function fork(
|
||||
modulePath: string,
|
||||
args: string[],
|
||||
options: IForkOptions,
|
||||
logger: Logger,
|
||||
callback: (error: any, cp: cp.ChildProcess | null) => void,
|
||||
): void {
|
||||
|
||||
let callbackCalled = false;
|
||||
const resolve = (result: cp.ChildProcess) => {
|
||||
if (callbackCalled) {
|
||||
return;
|
||||
}
|
||||
callbackCalled = true;
|
||||
callback(null, result);
|
||||
};
|
||||
const reject = (err: any) => {
|
||||
if (callbackCalled) {
|
||||
return;
|
||||
}
|
||||
callbackCalled = true;
|
||||
callback(err, null);
|
||||
};
|
||||
|
||||
// Generate three unique pipe names
|
||||
const stdInPipeName = generatePipeName();
|
||||
const stdOutPipeName = generatePipeName();
|
||||
const stdErrPipeName = generatePipeName();
|
||||
|
||||
|
||||
const newEnv = generatePatchedEnv(process.env, stdInPipeName, stdOutPipeName, stdErrPipeName);
|
||||
newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');
|
||||
let childProcess: cp.ChildProcess;
|
||||
|
||||
// Begin listening to stderr pipe
|
||||
let stdErrServer = net.createServer((stdErrStream) => {
|
||||
// From now on the childProcess.stderr is available for reading
|
||||
childProcess.stderr = stdErrStream;
|
||||
});
|
||||
stdErrServer.listen(stdErrPipeName);
|
||||
|
||||
// Begin listening to stdout pipe
|
||||
let stdOutServer = net.createServer((stdOutStream) => {
|
||||
// The child process will write exactly one chunk with content `ready` when it has installed a listener to the stdin pipe
|
||||
|
||||
stdOutStream.once('data', (_chunk: Buffer) => {
|
||||
// The child process is sending me the `ready` chunk, time to connect to the stdin pipe
|
||||
childProcess.stdin = <any>net.connect(stdInPipeName);
|
||||
|
||||
// From now on the childProcess.stdout is available for reading
|
||||
childProcess.stdout = stdOutStream;
|
||||
|
||||
resolve(childProcess);
|
||||
});
|
||||
});
|
||||
stdOutServer.listen(stdOutPipeName);
|
||||
|
||||
let serverClosed = false;
|
||||
const closeServer = () => {
|
||||
if (serverClosed) {
|
||||
return;
|
||||
}
|
||||
serverClosed = true;
|
||||
stdOutServer.close();
|
||||
stdErrServer.close();
|
||||
};
|
||||
|
||||
// Create the process
|
||||
logger.info('Forking TSServer', `PATH: ${newEnv['PATH']}`);
|
||||
|
||||
const bootstrapperPath = require.resolve('./electronForkStart');
|
||||
childProcess = cp.fork(bootstrapperPath, [modulePath].concat(args), {
|
||||
silent: true,
|
||||
cwd: options.cwd,
|
||||
env: newEnv,
|
||||
execArgv: options.execArgv
|
||||
});
|
||||
|
||||
childProcess.once('error', (err: Error) => {
|
||||
closeServer();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
childProcess.once('exit', (err: Error) => {
|
||||
closeServer();
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
var net = require('net'),
|
||||
fs = require('fs');
|
||||
|
||||
var ENABLE_LOGGING = false;
|
||||
|
||||
var log = (function () {
|
||||
if (!ENABLE_LOGGING) {
|
||||
return function () { };
|
||||
}
|
||||
var isFirst = true;
|
||||
var LOG_LOCATION = 'C:\\stdFork.log';
|
||||
return function log(str: any) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
fs.writeFileSync(LOG_LOCATION, str + '\n');
|
||||
return;
|
||||
}
|
||||
fs.appendFileSync(LOG_LOCATION, str + '\n');
|
||||
};
|
||||
})();
|
||||
|
||||
var stdInPipeName = process.env['STDIN_PIPE_NAME'];
|
||||
var stdOutPipeName = process.env['STDOUT_PIPE_NAME'];
|
||||
var stdErrPipeName = process.env['STDERR_PIPE_NAME'];
|
||||
|
||||
log('STDIN_PIPE_NAME: ' + stdInPipeName);
|
||||
log('STDOUT_PIPE_NAME: ' + stdOutPipeName);
|
||||
log('STDERR_PIPE_NAME: ' + stdErrPipeName);
|
||||
log('ELECTRON_RUN_AS_NODE: ' + process.env['ELECTRON_RUN_AS_NODE']);
|
||||
|
||||
// stdout redirection to named pipe
|
||||
(function () {
|
||||
log('Beginning stdout redirection...');
|
||||
|
||||
// Create a writing stream to the stdout pipe
|
||||
var stdOutStream = net.connect(stdOutPipeName);
|
||||
|
||||
// unref stdOutStream to behave like a normal standard out
|
||||
stdOutStream.unref();
|
||||
|
||||
// handle process.stdout
|
||||
(<any>process).__defineGetter__('stdout', function () { return stdOutStream; });
|
||||
|
||||
// Create a writing stream to the stderr pipe
|
||||
var stdErrStream = net.connect(stdErrPipeName);
|
||||
|
||||
// unref stdErrStream to behave like a normal standard out
|
||||
stdErrStream.unref();
|
||||
|
||||
// handle process.stderr
|
||||
(<any>process).__defineGetter__('stderr', function () { return stdErrStream; });
|
||||
|
||||
var fsWriteSyncString = function (fd: number, str: string, _position: number, encoding?: string) {
|
||||
// fs.writeSync(fd, string[, position[, encoding]]);
|
||||
var buf = Buffer.from(str, encoding || 'utf8');
|
||||
return fsWriteSyncBuffer(fd, buf, 0, buf.length);
|
||||
};
|
||||
|
||||
var fsWriteSyncBuffer = function (fd: number, buffer: Buffer, off: number, len: number) {
|
||||
off = Math.abs(off | 0);
|
||||
len = Math.abs(len | 0);
|
||||
|
||||
// fs.writeSync(fd, buffer, offset, length[, position]);
|
||||
var buffer_length = buffer.length;
|
||||
|
||||
if (off > buffer_length) {
|
||||
throw new Error('offset out of bounds');
|
||||
}
|
||||
if (len > buffer_length) {
|
||||
throw new Error('length out of bounds');
|
||||
}
|
||||
if (((off + len) | 0) < off) {
|
||||
throw new Error('off + len overflow');
|
||||
}
|
||||
if (buffer_length - off < len) {
|
||||
// Asking for more than is left over in the buffer
|
||||
throw new Error('off + len > buffer.length');
|
||||
}
|
||||
|
||||
var slicedBuffer = buffer;
|
||||
if (off !== 0 || len !== buffer_length) {
|
||||
slicedBuffer = buffer.slice(off, off + len);
|
||||
}
|
||||
|
||||
if (fd === 1) {
|
||||
stdOutStream.write(slicedBuffer);
|
||||
} else {
|
||||
stdErrStream.write(slicedBuffer);
|
||||
}
|
||||
return slicedBuffer.length;
|
||||
};
|
||||
|
||||
// handle fs.writeSync(1, ...)
|
||||
var originalWriteSync = fs.writeSync;
|
||||
fs.writeSync = function (fd: number, data: any, _position: number, _encoding?: string) {
|
||||
if (fd !== 1 && fd !== 2) {
|
||||
return originalWriteSync.apply(fs, arguments);
|
||||
}
|
||||
// usage:
|
||||
// fs.writeSync(fd, buffer, offset, length[, position]);
|
||||
// OR
|
||||
// fs.writeSync(fd, string[, position[, encoding]]);
|
||||
|
||||
if (data instanceof Buffer) {
|
||||
return fsWriteSyncBuffer.apply(null, arguments);
|
||||
}
|
||||
|
||||
// For compatibility reasons with fs.writeSync, writing null will write "null", etc
|
||||
if (typeof data !== 'string') {
|
||||
data += '';
|
||||
}
|
||||
|
||||
return fsWriteSyncString.apply(null, arguments);
|
||||
};
|
||||
|
||||
log('Finished defining process.stdout, process.stderr and fs.writeSync');
|
||||
})();
|
||||
|
||||
// stdin redirection to named pipe
|
||||
(function () {
|
||||
|
||||
// Begin listening to stdin pipe
|
||||
var server = net.createServer(function (stream: any) {
|
||||
// Stop accepting new connections, keep the existing one alive
|
||||
server.close();
|
||||
|
||||
log('Parent process has connected to my stdin. All should be good now.');
|
||||
|
||||
// handle process.stdin
|
||||
(<any>process).__defineGetter__('stdin', function () {
|
||||
return stream;
|
||||
});
|
||||
|
||||
// Remove myself from process.argv
|
||||
process.argv.splice(1, 1);
|
||||
|
||||
// Load the actual program
|
||||
var program = process.argv[1];
|
||||
log('Loading program: ' + program);
|
||||
|
||||
// Unset the custom environmental variables that should not get inherited
|
||||
delete process.env['STDIN_PIPE_NAME'];
|
||||
delete process.env['STDOUT_PIPE_NAME'];
|
||||
delete process.env['STDERR_PIPE_NAME'];
|
||||
delete process.env['ELECTRON_RUN_AS_NODE'];
|
||||
|
||||
require(program);
|
||||
|
||||
log('Finished loading program.');
|
||||
|
||||
var stdinIsReferenced = true;
|
||||
var timer = setInterval(function () {
|
||||
var listenerCount = (
|
||||
stream.listeners('data').length +
|
||||
stream.listeners('end').length +
|
||||
stream.listeners('close').length +
|
||||
stream.listeners('error').length
|
||||
);
|
||||
// log('listenerCount: ' + listenerCount);
|
||||
if (listenerCount <= 1) {
|
||||
// No more "actual" listeners, only internal node
|
||||
if (stdinIsReferenced) {
|
||||
stdinIsReferenced = false;
|
||||
// log('unreferencing stream!!!');
|
||||
stream.unref();
|
||||
}
|
||||
} else {
|
||||
// There are "actual" listeners
|
||||
if (!stdinIsReferenced) {
|
||||
stdinIsReferenced = true;
|
||||
stream.ref();
|
||||
}
|
||||
}
|
||||
// log(
|
||||
// '' + stream.listeners('data').length +
|
||||
// ' ' + stream.listeners('end').length +
|
||||
// ' ' + stream.listeners('close').length +
|
||||
// ' ' + stream.listeners('error').length
|
||||
// );
|
||||
}, 1000);
|
||||
|
||||
if ((<any>timer).unref) {
|
||||
(<any>timer).unref();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
server.listen(stdInPipeName, function () {
|
||||
// signal via stdout that the parent process can now begin writing to stdin pipe
|
||||
process.stdout.write('ready');
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const file = 'file';
|
||||
|
||||
export const untitled = 'untitled';
|
||||
|
||||
export const walkThroughSnippet = 'walkThroughSnippet';
|
||||
|
||||
export const supportedSchemes = [
|
||||
file,
|
||||
untitled,
|
||||
walkThroughSnippet
|
||||
];
|
||||
|
||||
export function isSupportedScheme(scheme: string): boolean {
|
||||
return supportedSchemes.indexOf(scheme) >= 0;
|
||||
}
|
||||
18
extensions/typescript-language-features/src/utils/is.ts
Normal file
18
extensions/typescript-language-features/src/utils/is.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
export function defined(value: any): boolean {
|
||||
return typeof value !== 'undefined';
|
||||
}
|
||||
|
||||
export function boolean(value: any): value is boolean {
|
||||
return value === true || value === false;
|
||||
}
|
||||
|
||||
export function string(value: any): value is string {
|
||||
return toString.call(value) === '[object String]';
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IndentAction } from 'vscode';
|
||||
|
||||
export const jsTsLanguageConfiguration = {
|
||||
indentationRules: {
|
||||
// ^(.*\*/)?\s*\}.*$
|
||||
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/,
|
||||
// ^.*\{[^}"']*$
|
||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
||||
},
|
||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
||||
onEnterRules: [
|
||||
{
|
||||
// e.g. /** | */
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
afterText: /^\s*\*\/$/,
|
||||
action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }
|
||||
}, {
|
||||
// e.g. /** ...|
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
action: { indentAction: IndentAction.None, appendText: ' * ' }
|
||||
}, {
|
||||
// e.g. * ...|
|
||||
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
|
||||
action: { indentAction: IndentAction.None, appendText: '* ' }
|
||||
}, {
|
||||
// e.g. */|
|
||||
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
},
|
||||
{
|
||||
// e.g. *-----*/|
|
||||
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||
|
||||
export const jsxTags = {
|
||||
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: IndentAction.IndentOutdent }
|
||||
},
|
||||
{
|
||||
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
|
||||
action: { indentAction: IndentAction.Indent }
|
||||
}
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as languageModeIds from './languageModeIds';
|
||||
|
||||
export interface LanguageDescription {
|
||||
id: string;
|
||||
diagnosticSource: string;
|
||||
modeIds: string[];
|
||||
configFile?: string;
|
||||
isExternal?: boolean;
|
||||
}
|
||||
|
||||
export const standardLanguageDescriptions: LanguageDescription[] = [
|
||||
{
|
||||
id: 'typescript',
|
||||
diagnosticSource: 'ts',
|
||||
modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact],
|
||||
configFile: 'tsconfig.json'
|
||||
}, {
|
||||
id: 'javascript',
|
||||
diagnosticSource: 'js',
|
||||
modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact],
|
||||
configFile: 'jsconfig.json'
|
||||
}
|
||||
];
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const typescript = 'typescript';
|
||||
export const typescriptreact = 'typescriptreact';
|
||||
export const javascript = 'javascript';
|
||||
export const javascriptreact = 'javascriptreact';
|
||||
export const jsxTags = 'jsx-tags';
|
||||
|
||||
|
||||
export function isSupportedLanguageMode(doc: vscode.TextDocument) {
|
||||
return vscode.languages.match([typescript, typescriptreact, javascript, javascriptreact], doc) > 0;
|
||||
}
|
||||
39
extensions/typescript-language-features/src/utils/lazy.ts
Normal file
39
extensions/typescript-language-features/src/utils/lazy.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface Lazy<T> {
|
||||
value: T;
|
||||
hasValue: boolean;
|
||||
map<R>(f: (x: T) => R): Lazy<R>;
|
||||
}
|
||||
|
||||
class LazyValue<T> implements Lazy<T> {
|
||||
private _hasValue: boolean = false;
|
||||
private _value?: T;
|
||||
|
||||
constructor(
|
||||
private readonly _getValue: () => T
|
||||
) { }
|
||||
|
||||
get value(): T {
|
||||
if (!this._hasValue) {
|
||||
this._hasValue = true;
|
||||
this._value = this._getValue();
|
||||
}
|
||||
return this._value!;
|
||||
}
|
||||
|
||||
get hasValue(): boolean {
|
||||
return this._hasValue;
|
||||
}
|
||||
|
||||
public map<R>(f: (x: T) => R): Lazy<R> {
|
||||
return new LazyValue(() => f(this.value));
|
||||
}
|
||||
}
|
||||
|
||||
export function lazy<T>(getValue: () => T): Lazy<T> {
|
||||
return new LazyValue<T>(getValue);
|
||||
}
|
||||
@@ -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 * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { memoize } from './memoize';
|
||||
|
||||
export default class LogDirectoryProvider {
|
||||
public constructor(
|
||||
private readonly context: vscode.ExtensionContext
|
||||
) { }
|
||||
|
||||
public async getNewLogDirectory(): Promise<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.logDirectory;
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
}
|
||||
return this.context.logDirectory;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
extensions/typescript-language-features/src/utils/logger.ts
Normal file
58
extensions/typescript-language-features/src/utils/logger.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OutputChannel, window } from 'vscode';
|
||||
import * as is from './is';
|
||||
import { memoize } from './memoize';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export default class Logger {
|
||||
|
||||
@memoize
|
||||
private get output(): OutputChannel {
|
||||
return window.createOutputChannel(localize('channelName', 'TypeScript'));
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
if (is.string(data.stack)) {
|
||||
return data.stack;
|
||||
}
|
||||
return (data as Error).message;
|
||||
}
|
||||
if (is.boolean(data.success) && !data.success && is.string(data.message)) {
|
||||
return data.message;
|
||||
}
|
||||
if (is.string(data)) {
|
||||
return data;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public warn(message: string, data?: any): void {
|
||||
this.logLevel('Warn', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
// See https://github.com/Microsoft/TypeScript/issues/10496
|
||||
if (data && data.message === 'No content available.') {
|
||||
return;
|
||||
}
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: string, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${(new Date().toLocaleTimeString())}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { isSupportedLanguageMode } from './languageModeIds';
|
||||
|
||||
/**
|
||||
* When clause context set when the current file is managed by vscode's built-in typescript extension.
|
||||
*/
|
||||
const isManagedFile_contextName = 'typescript.isManagedFile';
|
||||
|
||||
export default class ManagedFileContextManager {
|
||||
|
||||
private isInManagedFileContext: boolean = false;
|
||||
|
||||
private readonly onDidChangeActiveTextEditorSub: vscode.Disposable;
|
||||
|
||||
public constructor(
|
||||
private readonly normalizePath: (resource: vscode.Uri) => string | null
|
||||
) {
|
||||
this.onDidChangeActiveTextEditorSub = vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this);
|
||||
|
||||
this.onDidChangeActiveTextEditor(vscode.window.activeTextEditor);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.onDidChangeActiveTextEditorSub.dispose();
|
||||
}
|
||||
|
||||
private onDidChangeActiveTextEditor(editor?: vscode.TextEditor): any {
|
||||
if (editor) {
|
||||
const isManagedFile = isSupportedLanguageMode(editor.document) && this.normalizePath(editor.document.uri) !== null;
|
||||
this.updateContext(isManagedFile);
|
||||
}
|
||||
}
|
||||
|
||||
private updateContext(newValue: boolean) {
|
||||
if (newValue === this.isInManagedFileContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('setContext', isManagedFile_contextName, newValue);
|
||||
this.isInManagedFileContext = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
34
extensions/typescript-language-features/src/utils/memoize.ts
Normal file
34
extensions/typescript-language-features/src/utils/memoize.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function memoize(_target: any, key: string, descriptor: any) {
|
||||
let fnKey: string | undefined = undefined;
|
||||
let fn: Function | undefined = undefined;
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
} else {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
const memoizeKey = `$memoize$${key}`;
|
||||
|
||||
descriptor[fnKey] = function (...args: any[]) {
|
||||
if (!this.hasOwnProperty(memoizeKey)) {
|
||||
Object.defineProperty(this, memoizeKey, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: fn!.apply(this, args)
|
||||
});
|
||||
}
|
||||
|
||||
return this[memoizeKey];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { workspace } from 'vscode';
|
||||
|
||||
import { TypeScriptServiceConfiguration } from './configuration';
|
||||
import { RelativeWorkspacePathResolver } from './relativePathResolver';
|
||||
|
||||
export class TypeScriptPluginPathsProvider {
|
||||
public readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver();
|
||||
|
||||
public constructor(
|
||||
private configuration: TypeScriptServiceConfiguration
|
||||
) { }
|
||||
|
||||
public updateConfiguration(configuration: TypeScriptServiceConfiguration): void {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public getPluginPaths(): string[] {
|
||||
const pluginPaths = [];
|
||||
for (const pluginPath of this.configuration.tsServerPluginPaths) {
|
||||
pluginPaths.push(...this.resolvePluginPath(pluginPath));
|
||||
}
|
||||
return pluginPaths;
|
||||
}
|
||||
|
||||
private resolvePluginPath(pluginPath: string): string[] {
|
||||
if (path.isAbsolute(pluginPath)) {
|
||||
return [pluginPath];
|
||||
}
|
||||
|
||||
const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(pluginPath);
|
||||
if (workspacePath !== undefined) {
|
||||
return [workspacePath];
|
||||
}
|
||||
|
||||
return (workspace.workspaceFolders || [])
|
||||
.map(workspaceFolder => path.join(workspaceFolder.uri.fsPath, pluginPath));
|
||||
}
|
||||
}
|
||||
29
extensions/typescript-language-features/src/utils/plugins.ts
Normal file
29
extensions/typescript-language-features/src/utils/plugins.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extensions } from 'vscode';
|
||||
|
||||
export interface TypeScriptServerPlugin {
|
||||
readonly path: string;
|
||||
readonly name: string;
|
||||
readonly languages: string[];
|
||||
}
|
||||
|
||||
export function getContributedTypeScriptServerPlugins(): TypeScriptServerPlugin[] {
|
||||
const plugins: TypeScriptServerPlugin[] = [];
|
||||
for (const extension of extensions.all) {
|
||||
const pack = extension.packageJSON;
|
||||
if (pack.contributes && pack.contributes.typescriptServerPlugins && Array.isArray(pack.contributes.typescriptServerPlugins)) {
|
||||
for (const plugin of pack.contributes.typescriptServerPlugins) {
|
||||
plugins.push({
|
||||
name: plugin.name,
|
||||
path: extension.extensionPath,
|
||||
languages: Array.isArray(plugin.languages) ? plugin.languages : [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import { MarkdownString } from 'vscode';
|
||||
|
||||
function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined {
|
||||
if (!tag.text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (tag.name) {
|
||||
case 'example':
|
||||
case 'default':
|
||||
// Convert to markdown code block if it not already one
|
||||
if (tag.text.match(/^\s*[~`]{3}/g)) {
|
||||
return tag.text;
|
||||
}
|
||||
return '```\n' + tag.text + '\n```';
|
||||
}
|
||||
|
||||
return tag.text;
|
||||
}
|
||||
|
||||
function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
|
||||
switch (tag.name) {
|
||||
case 'param':
|
||||
const body = (tag.text || '').split(/^([\w\.]+)\s*/);
|
||||
if (body && body.length === 3) {
|
||||
const param = body[1];
|
||||
const doc = body[2];
|
||||
const label = `*@${tag.name}* \`${param}\``;
|
||||
if (!doc) {
|
||||
return label;
|
||||
}
|
||||
return label + (doc.match(/\r\n|\n/g) ? ' \n' + doc : ` — ${doc}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic tag
|
||||
const label = `*@${tag.name}*`;
|
||||
const text = getTagBodyText(tag);
|
||||
if (!text) {
|
||||
return label;
|
||||
}
|
||||
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
|
||||
}
|
||||
|
||||
export function plain(parts: Proto.SymbolDisplayPart[]): string {
|
||||
if (!parts) {
|
||||
return '';
|
||||
}
|
||||
return parts.map(part => part.text).join('');
|
||||
}
|
||||
|
||||
export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string {
|
||||
return (tags || [])
|
||||
.map(getTagDocumentation)
|
||||
.join(' \n\n');
|
||||
}
|
||||
|
||||
export function markdownDocumentation(
|
||||
documentation: Proto.SymbolDisplayPart[],
|
||||
tags: Proto.JSDocTagInfo[]
|
||||
): MarkdownString {
|
||||
const out = new MarkdownString();
|
||||
addMarkdownDocumentation(out, documentation, tags);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function addMarkdownDocumentation(
|
||||
out: MarkdownString,
|
||||
documentation: Proto.SymbolDisplayPart[],
|
||||
tags: Proto.JSDocTagInfo[]
|
||||
): MarkdownString {
|
||||
out.appendMarkdown(plain(documentation));
|
||||
const tagsPreview = tagsMarkdownPreview(tags);
|
||||
if (tagsPreview) {
|
||||
out.appendMarkdown('\n\n' + tagsPreview);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { loadMessageBundle } from 'vscode-nls';
|
||||
import { dirname } from 'path';
|
||||
import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './tsconfig';
|
||||
import * as languageModeIds from '../utils/languageModeIds';
|
||||
import TelemetryReporter from './telemetry';
|
||||
|
||||
const localize = loadMessageBundle();
|
||||
const selector = [languageModeIds.javascript, languageModeIds.javascriptreact];
|
||||
|
||||
|
||||
interface Hint {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ProjectHintedMap {
|
||||
[k: string]: boolean;
|
||||
}
|
||||
|
||||
const fileLimit = 500;
|
||||
|
||||
class ExcludeHintItem {
|
||||
public configFileName?: string;
|
||||
private _item: vscode.StatusBarItem;
|
||||
private _currentHint?: Hint;
|
||||
|
||||
constructor(
|
||||
private readonly telemetryReporter: TelemetryReporter
|
||||
) {
|
||||
this._item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 98 /* to the right of typescript version status (99) */);
|
||||
this._item.command = 'js.projectStatus.command';
|
||||
}
|
||||
|
||||
public getCurrentHint(): Hint {
|
||||
return this._currentHint!;
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this._item.hide();
|
||||
}
|
||||
|
||||
public show(largeRoots?: string) {
|
||||
this._currentHint = {
|
||||
message: largeRoots
|
||||
? localize('hintExclude', "To enable project-wide JavaScript/TypeScript language features, exclude folders with many files, like: {0}", largeRoots)
|
||||
: localize('hintExclude.generic', "To enable project-wide JavaScript/TypeScript language features, exclude large folders with source files that you do not work on.")
|
||||
};
|
||||
this._item.tooltip = this._currentHint.message;
|
||||
this._item.text = localize('large.label', "Configure Excludes");
|
||||
this._item.tooltip = localize('hintExclude.tooltip', "To enable project-wide JavaScript/TypeScript language features, exclude large folders with source files that you do not work on.");
|
||||
this._item.color = '#A5DF3B';
|
||||
this._item.show();
|
||||
/* __GDPR__
|
||||
"js.hintProjectExcludes" : {}
|
||||
*/
|
||||
this.telemetryReporter.logTelemetry('js.hintProjectExcludes');
|
||||
}
|
||||
}
|
||||
|
||||
function createLargeProjectMonitorForProject(item: ExcludeHintItem, client: ITypeScriptServiceClient, isOpen: (resource: vscode.Uri) => Promise<boolean>, memento: vscode.Memento): vscode.Disposable[] {
|
||||
const toDispose: vscode.Disposable[] = [];
|
||||
const projectHinted: ProjectHintedMap = Object.create(null);
|
||||
|
||||
const projectHintIgnoreList = memento.get<string[]>('projectHintIgnoreList', []);
|
||||
for (let path of projectHintIgnoreList) {
|
||||
if (path === null) {
|
||||
path = 'undefined';
|
||||
}
|
||||
projectHinted[path] = true;
|
||||
}
|
||||
|
||||
function onEditor(editor: vscode.TextEditor | undefined): void {
|
||||
if (!editor
|
||||
|| !vscode.languages.match(selector, editor.document)
|
||||
|| !client.normalizePath(editor.document.uri)) {
|
||||
|
||||
item.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = client.normalizePath(editor.document.uri);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
isOpen(editor.document.uri).then(value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return client.execute('projectInfo', { file, needFileNameList: true } as protocol.ProjectInfoRequestArgs).then(res => {
|
||||
if (!res.body) {
|
||||
return;
|
||||
}
|
||||
let { configFileName, fileNames } = res.body;
|
||||
|
||||
if (projectHinted[configFileName] === true || !fileNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileNames.length > fileLimit || res.body.languageServiceDisabled) {
|
||||
let largeRoots = computeLargeRoots(configFileName, fileNames).map(f => `'/${f}/'`).join(', ');
|
||||
item.show(largeRoots);
|
||||
projectHinted[configFileName] = true;
|
||||
} else {
|
||||
item.hide();
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
client.logger.warn(err);
|
||||
});
|
||||
}
|
||||
|
||||
toDispose.push(vscode.workspace.onDidChangeTextDocument(e => {
|
||||
delete projectHinted[e.document.fileName];
|
||||
}));
|
||||
|
||||
toDispose.push(vscode.window.onDidChangeActiveTextEditor(onEditor));
|
||||
onEditor(vscode.window.activeTextEditor);
|
||||
|
||||
return toDispose;
|
||||
}
|
||||
|
||||
function createLargeProjectMonitorFromTypeScript(item: ExcludeHintItem, client: ITypeScriptServiceClient): vscode.Disposable {
|
||||
|
||||
interface LargeProjectMessageItem extends vscode.MessageItem {
|
||||
index: number;
|
||||
}
|
||||
|
||||
return client.onProjectLanguageServiceStateChanged(body => {
|
||||
if (body.languageServiceEnabled) {
|
||||
item.hide();
|
||||
} else {
|
||||
item.show();
|
||||
const configFileName = body.projectName;
|
||||
if (configFileName) {
|
||||
item.configFileName = configFileName;
|
||||
vscode.window.showWarningMessage<LargeProjectMessageItem>(item.getCurrentHint().message,
|
||||
{
|
||||
title: localize('large.label', "Configure Excludes"),
|
||||
index: 0
|
||||
}).then(selected => {
|
||||
if (selected && selected.index === 0) {
|
||||
onConfigureExcludesSelected(client, configFileName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onConfigureExcludesSelected(
|
||||
client: ITypeScriptServiceClient,
|
||||
configFileName: string
|
||||
) {
|
||||
if (!isImplicitProjectConfigFile(configFileName)) {
|
||||
vscode.workspace.openTextDocument(configFileName)
|
||||
.then(vscode.window.showTextDocument);
|
||||
} else {
|
||||
const root = client.getWorkspaceRootForResource(vscode.Uri.file(configFileName));
|
||||
if (root) {
|
||||
openOrCreateConfigFile(
|
||||
configFileName.match(/tsconfig\.?.*\.json/) !== null,
|
||||
root,
|
||||
client.configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function create(
|
||||
client: ITypeScriptServiceClient,
|
||||
telemetryReporter: TelemetryReporter,
|
||||
isOpen: (resource: vscode.Uri) => Promise<boolean>,
|
||||
memento: vscode.Memento
|
||||
) {
|
||||
const toDispose: vscode.Disposable[] = [];
|
||||
|
||||
const item = new ExcludeHintItem(telemetryReporter);
|
||||
toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => {
|
||||
if (item.configFileName) {
|
||||
onConfigureExcludesSelected(client, item.configFileName);
|
||||
}
|
||||
let { message } = item.getCurrentHint();
|
||||
return vscode.window.showInformationMessage(message);
|
||||
}));
|
||||
|
||||
if (client.apiVersion.has213Features()) {
|
||||
toDispose.push(createLargeProjectMonitorFromTypeScript(item, client));
|
||||
} else {
|
||||
toDispose.push(...createLargeProjectMonitorForProject(item, client, isOpen, memento));
|
||||
}
|
||||
|
||||
return vscode.Disposable.from(...toDispose);
|
||||
}
|
||||
|
||||
function computeLargeRoots(configFileName: string, fileNames: string[]): string[] {
|
||||
|
||||
let roots: { [first: string]: number } = Object.create(null);
|
||||
let dir = dirname(configFileName);
|
||||
|
||||
// console.log(dir, fileNames);
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
if (fileName.indexOf(dir) === 0) {
|
||||
let first = fileName.substring(dir.length + 1);
|
||||
first = first.substring(0, first.indexOf('/'));
|
||||
if (first) {
|
||||
roots[first] = (roots[first] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data: { root: string; count: number }[] = [];
|
||||
for (let key in roots) {
|
||||
data.push({ root: key, count: roots[key] });
|
||||
}
|
||||
|
||||
data
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.filter(s => s.root === 'src' || s.root === 'test' || s.root === 'tests');
|
||||
|
||||
let result: string[] = [];
|
||||
let sum = 0;
|
||||
for (const e of data) {
|
||||
sum += e.count;
|
||||
result.push(e.root);
|
||||
if (fileNames.length - sum < fileLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function escapeRegExp(text: string) {
|
||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||
}
|
||||
@@ -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 path from 'path';
|
||||
import { workspace } from 'vscode';
|
||||
|
||||
export class RelativeWorkspacePathResolver {
|
||||
public asAbsoluteWorkspacePath(relativePath: string): string | undefined {
|
||||
for (const root of workspace.workspaceFolders || []) {
|
||||
const rootPrefixes = [`./${root.name}/`, `${root.name}/`, `.\\${root.name}\\`, `${root.name}\\`];
|
||||
for (const rootPrefix of rootPrefixes) {
|
||||
if (relativePath.startsWith(rootPrefix)) {
|
||||
return path.join(root.uri.fsPath, relativePath.replace(rootPrefix, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 VsCodeTelemetryReporter from 'vscode-extension-telemetry';
|
||||
import { memoize } from './memoize';
|
||||
|
||||
interface IPackageInfo {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly aiKey: string;
|
||||
}
|
||||
|
||||
export default class TelemetryReporter {
|
||||
private _reporter: VsCodeTelemetryReporter | null = null;
|
||||
|
||||
dispose() {
|
||||
if (this._reporter) {
|
||||
this._reporter.dispose();
|
||||
this._reporter = null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly clientVersionDelegate: () => string
|
||||
) { }
|
||||
|
||||
public logTelemetry(eventName: string, properties?: { [prop: string]: string }) {
|
||||
const reporter = this.reporter;
|
||||
if (reporter) {
|
||||
if (!properties) {
|
||||
properties = {};
|
||||
}
|
||||
properties['version'] = this.clientVersionDelegate();
|
||||
|
||||
reporter.sendTelemetryEvent(eventName, properties);
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get reporter(): VsCodeTelemetryReporter | null {
|
||||
if (this.packageInfo && this.packageInfo.aiKey) {
|
||||
this._reporter = new VsCodeTelemetryReporter(
|
||||
this.packageInfo.name,
|
||||
this.packageInfo.version,
|
||||
this.packageInfo.aiKey);
|
||||
return this._reporter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get packageInfo(): IPackageInfo | null {
|
||||
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
||||
const extensionPackage = require(packagePath);
|
||||
if (extensionPackage) {
|
||||
return {
|
||||
name: extensionPackage.name,
|
||||
version: extensionPackage.version,
|
||||
aiKey: extensionPackage.aiKey
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
93
extensions/typescript-language-features/src/utils/tracer.ts
Normal file
93
extensions/typescript-language-features/src/utils/tracer.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace } from 'vscode';
|
||||
|
||||
import * as Proto from '../protocol';
|
||||
import Logger from './logger';
|
||||
|
||||
|
||||
enum Trace {
|
||||
Off,
|
||||
Messages,
|
||||
Verbose
|
||||
}
|
||||
|
||||
namespace Trace {
|
||||
export function fromString(value: string): Trace {
|
||||
value = value.toLowerCase();
|
||||
switch (value) {
|
||||
case 'off':
|
||||
return Trace.Off;
|
||||
case 'messages':
|
||||
return Trace.Messages;
|
||||
case 'verbose':
|
||||
return Trace.Verbose;
|
||||
default:
|
||||
return Trace.Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Tracer {
|
||||
private trace?: Trace;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
this.updateConfiguration();
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
this.trace = Tracer.readTrace();
|
||||
}
|
||||
|
||||
private static readTrace(): Trace {
|
||||
let result: Trace = Trace.fromString(workspace.getConfiguration().get<string>('typescript.tsserver.trace', 'off'));
|
||||
if (result === Trace.Off && !!process.env.TSS_TRACE) {
|
||||
result = Trace.Messages;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public traceRequest(request: Proto.Request, responseExpected: boolean, queueLength: number): void {
|
||||
if (this.trace === Trace.Off) {
|
||||
return;
|
||||
}
|
||||
let data: string | undefined = undefined;
|
||||
if (this.trace === Trace.Verbose && request.arguments) {
|
||||
data = `Arguments: ${JSON.stringify(request.arguments, null, 4)}`;
|
||||
}
|
||||
this.logTrace(`Sending request: ${request.command} (${request.seq}). Response expected: ${responseExpected ? 'yes' : 'no'}. Current queue length: ${queueLength}`, data);
|
||||
}
|
||||
|
||||
public traceResponse(response: Proto.Response, startTime: number): void {
|
||||
if (this.trace === Trace.Off) {
|
||||
return;
|
||||
}
|
||||
let data: string | undefined = undefined;
|
||||
if (this.trace === Trace.Verbose && response.body) {
|
||||
data = `Result: ${JSON.stringify(response.body, null, 4)}`;
|
||||
}
|
||||
this.logTrace(`Response received: ${response.command} (${response.request_seq}). Request took ${Date.now() - startTime} ms. Success: ${response.success} ${!response.success ? '. Message: ' + response.message : ''}`, data);
|
||||
}
|
||||
|
||||
public traceEvent(event: Proto.Event): void {
|
||||
if (this.trace === Trace.Off) {
|
||||
return;
|
||||
}
|
||||
let data: string | undefined = undefined;
|
||||
if (this.trace === Trace.Verbose && event.body) {
|
||||
data = `Data: ${JSON.stringify(event.body, null, 4)}`;
|
||||
}
|
||||
this.logTrace(`Event received: ${event.event} (${event.seq}).`, data);
|
||||
}
|
||||
|
||||
public logTrace(message: string, data?: any): void {
|
||||
if (this.trace !== Trace.Off) {
|
||||
this.logger.logLevel('Trace', message, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as Proto from '../protocol';
|
||||
|
||||
import { TypeScriptServiceConfiguration } from './configuration';
|
||||
|
||||
export function isImplicitProjectConfigFile(configFileName: string) {
|
||||
return configFileName.indexOf('/dev/null/') === 0;
|
||||
}
|
||||
|
||||
export function inferredProjectConfig(
|
||||
config: TypeScriptServiceConfiguration
|
||||
): Proto.ExternalProjectCompilerOptions {
|
||||
const base: Proto.ExternalProjectCompilerOptions = {
|
||||
module: 'commonjs' as Proto.ModuleKind,
|
||||
target: 'es2016' as Proto.ScriptTarget,
|
||||
jsx: 'preserve' as Proto.JsxEmit
|
||||
};
|
||||
|
||||
if (config.checkJs) {
|
||||
base.checkJs = true;
|
||||
}
|
||||
|
||||
if (config.experimentalDecorators) {
|
||||
base.experimentalDecorators = true;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
function inferredProjectConfigSnippet(
|
||||
config: TypeScriptServiceConfiguration
|
||||
) {
|
||||
const baseConfig = inferredProjectConfig(config);
|
||||
const compilerOptions = Object.keys(baseConfig).map(key => `"${key}": ${JSON.stringify(baseConfig[key])}`);
|
||||
return new vscode.SnippetString(`{
|
||||
"compilerOptions": {
|
||||
${compilerOptions.join(',\n\t\t')}$0
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}`);
|
||||
}
|
||||
|
||||
export async function openOrCreateConfigFile(
|
||||
isTypeScriptProject: boolean,
|
||||
rootPath: string,
|
||||
config: TypeScriptServiceConfiguration
|
||||
): Promise<vscode.TextEditor | null> {
|
||||
const configFile = vscode.Uri.file(path.join(rootPath, isTypeScriptProject ? 'tsconfig.json' : 'jsconfig.json'));
|
||||
const col = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
|
||||
try {
|
||||
const doc = await vscode.workspace.openTextDocument(configFile);
|
||||
return vscode.window.showTextDocument(doc, col);
|
||||
} catch {
|
||||
const doc = await vscode.workspace.openTextDocument(configFile.with({ scheme: 'untitled' }));
|
||||
const editor = await vscode.window.showTextDocument(doc, col);
|
||||
if (editor.document.getText().length === 0) {
|
||||
await editor.insertSnippet(inferredProjectConfigSnippet(config));
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
@@ -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 * as vscode from 'vscode';
|
||||
|
||||
export interface TSConfig {
|
||||
path: string;
|
||||
workspaceFolder?: vscode.WorkspaceFolder;
|
||||
}
|
||||
|
||||
export default class TsConfigProvider {
|
||||
public async getConfigsForWorkspace(): Promise<Iterable<TSConfig>> {
|
||||
if (!vscode.workspace.workspaceFolders) {
|
||||
return [];
|
||||
}
|
||||
const configs = new Map<string, TSConfig>();
|
||||
for (const config of await vscode.workspace.findFiles('**/tsconfig*.json', '**/node_modules/**')) {
|
||||
const root = vscode.workspace.getWorkspaceFolder(config);
|
||||
if (root) {
|
||||
configs.set(config.fsPath, {
|
||||
path: config.fsPath,
|
||||
workspaceFolder: root
|
||||
});
|
||||
}
|
||||
}
|
||||
return configs.values();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Helpers for converting FROM vscode types TO ts types
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as Proto from '../protocol';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
|
||||
export namespace Range {
|
||||
export const fromTextSpan = (span: Proto.TextSpan): vscode.Range =>
|
||||
new vscode.Range(
|
||||
span.start.line - 1, span.start.offset - 1,
|
||||
span.end.line - 1, span.end.offset - 1);
|
||||
|
||||
export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({
|
||||
file,
|
||||
startLine: range.start.line + 1,
|
||||
startOffset: range.start.character + 1,
|
||||
endLine: range.end.line + 1,
|
||||
endOffset: range.end.character + 1
|
||||
});
|
||||
}
|
||||
|
||||
export namespace Position {
|
||||
export const fromLocation = (tslocation: Proto.Location): vscode.Position =>
|
||||
new vscode.Position(tslocation.line - 1, tslocation.offset - 1);
|
||||
|
||||
export const toFileLocationRequestArgs = (file: string, position: vscode.Position): Proto.FileLocationRequestArgs => ({
|
||||
file,
|
||||
line: position.line + 1,
|
||||
offset: position.character + 1
|
||||
});
|
||||
}
|
||||
|
||||
export namespace Location {
|
||||
export const fromTextSpan = (resource: vscode.Uri, tsTextSpan: Proto.TextSpan): vscode.Location =>
|
||||
new vscode.Location(resource, Range.fromTextSpan(tsTextSpan));
|
||||
}
|
||||
|
||||
export namespace TextEdit {
|
||||
export const fromCodeEdit = (edit: Proto.CodeEdit): vscode.TextEdit =>
|
||||
new vscode.TextEdit(
|
||||
Range.fromTextSpan(edit),
|
||||
edit.newText);
|
||||
}
|
||||
|
||||
export namespace WorkspaceEdit {
|
||||
export function fromFromFileCodeEdits(
|
||||
client: ITypeScriptServiceClient,
|
||||
edits: Iterable<Proto.FileCodeEdits>
|
||||
): vscode.WorkspaceEdit {
|
||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
||||
for (const edit of edits) {
|
||||
for (const textChange of edit.textChanges) {
|
||||
workspaceEdit.replace(client.asUrl(edit.fileName),
|
||||
Range.fromTextSpan(textChange),
|
||||
textChange.newText);
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceEdit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MessageItem, workspace, Disposable, ProgressLocation, window } from 'vscode';
|
||||
import { ITypeScriptServiceClient } from '../typescriptService';
|
||||
import { loadMessageBundle } from 'vscode-nls';
|
||||
|
||||
const localize = loadMessageBundle();
|
||||
|
||||
const typingsInstallTimeout = 30 * 1000;
|
||||
|
||||
export default class TypingsStatus extends Disposable {
|
||||
private _acquiringTypings: { [eventId: string]: NodeJS.Timer } = Object.create({});
|
||||
private _client: ITypeScriptServiceClient;
|
||||
private _subscriptions: Disposable[] = [];
|
||||
|
||||
constructor(client: ITypeScriptServiceClient) {
|
||||
super(() => this.dispose());
|
||||
this._client = client;
|
||||
|
||||
this._subscriptions.push(
|
||||
this._client.onDidBeginInstallTypings(event => this.onBeginInstallTypings(event.eventId)));
|
||||
|
||||
this._subscriptions.push(
|
||||
this._client.onDidEndInstallTypings(event => this.onEndInstallTypings(event.eventId)));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._subscriptions.forEach(x => x.dispose());
|
||||
|
||||
for (const eventId of Object.keys(this._acquiringTypings)) {
|
||||
clearTimeout(this._acquiringTypings[eventId]);
|
||||
}
|
||||
}
|
||||
|
||||
public get isAcquiringTypings(): boolean {
|
||||
return Object.keys(this._acquiringTypings).length > 0;
|
||||
}
|
||||
|
||||
private onBeginInstallTypings(eventId: number): void {
|
||||
if (this._acquiringTypings[eventId]) {
|
||||
return;
|
||||
}
|
||||
this._acquiringTypings[eventId] = setTimeout(() => {
|
||||
this.onEndInstallTypings(eventId);
|
||||
}, typingsInstallTimeout);
|
||||
}
|
||||
|
||||
private onEndInstallTypings(eventId: number): void {
|
||||
const timer = this._acquiringTypings[eventId];
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
delete this._acquiringTypings[eventId];
|
||||
}
|
||||
}
|
||||
|
||||
export class AtaProgressReporter {
|
||||
|
||||
private _promises = new Map<number, Function>();
|
||||
private _disposable: Disposable;
|
||||
|
||||
constructor(client: ITypeScriptServiceClient) {
|
||||
this._disposable = Disposable.from(
|
||||
client.onDidBeginInstallTypings(e => this._onBegin(e.eventId)),
|
||||
client.onDidEndInstallTypings(e => this._onEndOrTimeout(e.eventId)),
|
||||
client.onTypesInstallerInitializationFailed(_ => this.onTypesInstallerInitializationFailed()));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposable.dispose();
|
||||
this._promises.forEach(value => value());
|
||||
}
|
||||
|
||||
private _onBegin(eventId: number): void {
|
||||
const handle = setTimeout(() => this._onEndOrTimeout(eventId), typingsInstallTimeout);
|
||||
const promise = new Promise(resolve => {
|
||||
this._promises.set(eventId, () => {
|
||||
clearTimeout(handle);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Window,
|
||||
title: localize('installingPackages', "Fetching data for better TypeScript IntelliSense")
|
||||
}, () => promise);
|
||||
}
|
||||
|
||||
private _onEndOrTimeout(eventId: number): void {
|
||||
const resolve = this._promises.get(eventId);
|
||||
if (resolve) {
|
||||
this._promises.delete(eventId);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private onTypesInstallerInitializationFailed() {
|
||||
interface MyMessageItem extends MessageItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
if (workspace.getConfiguration('typescript').get<boolean>('check.npmIsInstalled', true)) {
|
||||
window.showWarningMessage<MyMessageItem>(
|
||||
localize(
|
||||
'typesInstallerInitializationFailed.title',
|
||||
"Could not install typings files for JavaScript language features. Please ensure that NPM is installed or configure 'typescript.npm' in your user settings. Click [here]({0}) to learn more.",
|
||||
'https://go.microsoft.com/fwlink/?linkid=847635'
|
||||
), {
|
||||
title: localize('typesInstallerInitializationFailed.doNotCheckAgain', "Don't Show Again"),
|
||||
id: 1
|
||||
}
|
||||
).then(selected => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
switch (selected.id) {
|
||||
case 1:
|
||||
const tsConfig = workspace.getConfiguration('typescript');
|
||||
tsConfig.update('check.npmIsInstalled', false, true);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { TypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
|
||||
import { Memento, commands, Uri, window, QuickPickItem, workspace } from 'vscode';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const useWorkspaceTsdkStorageKey = 'typescript.useWorkspaceTsdk';
|
||||
|
||||
interface MyQuickPickItem extends QuickPickItem {
|
||||
id: MessageAction;
|
||||
version?: TypeScriptVersion;
|
||||
}
|
||||
|
||||
enum MessageAction {
|
||||
useLocal,
|
||||
useBundled,
|
||||
learnMore
|
||||
}
|
||||
|
||||
export class TypeScriptVersionPicker {
|
||||
private _currentVersion: TypeScriptVersion;
|
||||
|
||||
public constructor(
|
||||
private readonly versionProvider: TypeScriptVersionProvider,
|
||||
private readonly workspaceState: Memento
|
||||
) {
|
||||
this._currentVersion = this.versionProvider.defaultVersion;
|
||||
|
||||
if (this.useWorkspaceTsdkSetting) {
|
||||
const localVersion = this.versionProvider.localVersion;
|
||||
if (localVersion) {
|
||||
this._currentVersion = localVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get useWorkspaceTsdkSetting(): boolean {
|
||||
return this.workspaceState.get<boolean>(useWorkspaceTsdkStorageKey, false);
|
||||
}
|
||||
|
||||
public get currentVersion(): TypeScriptVersion {
|
||||
return this._currentVersion;
|
||||
}
|
||||
|
||||
public useBundledVersion(): void {
|
||||
this._currentVersion = this.versionProvider.bundledVersion;
|
||||
}
|
||||
|
||||
public async show(firstRun?: boolean): Promise<{ oldVersion?: TypeScriptVersion, newVersion?: TypeScriptVersion }> {
|
||||
const pickOptions: MyQuickPickItem[] = [];
|
||||
|
||||
const shippedVersion = this.versionProvider.defaultVersion;
|
||||
pickOptions.push({
|
||||
label: (!this.useWorkspaceTsdkSetting
|
||||
? '• '
|
||||
: '') + localize('useVSCodeVersionOption', 'Use VS Code\'s Version'),
|
||||
description: shippedVersion.versionString,
|
||||
detail: shippedVersion.pathLabel,
|
||||
id: MessageAction.useBundled
|
||||
});
|
||||
|
||||
for (const version of this.versionProvider.localVersions) {
|
||||
pickOptions.push({
|
||||
label: (this.useWorkspaceTsdkSetting && this.currentVersion.path === version.path
|
||||
? '• '
|
||||
: '') + localize('useWorkspaceVersionOption', 'Use Workspace Version'),
|
||||
description: version.versionString,
|
||||
detail: version.pathLabel,
|
||||
id: MessageAction.useLocal,
|
||||
version: version
|
||||
});
|
||||
}
|
||||
|
||||
pickOptions.push({
|
||||
label: localize('learnMore', 'Learn More'),
|
||||
description: '',
|
||||
id: MessageAction.learnMore
|
||||
});
|
||||
|
||||
const selected = await window.showQuickPick<MyQuickPickItem>(pickOptions, {
|
||||
placeHolder: localize(
|
||||
'selectTsVersion',
|
||||
'Select the TypeScript version used for JavaScript and TypeScript language features'),
|
||||
ignoreFocusOut: firstRun
|
||||
});
|
||||
|
||||
if (!selected) {
|
||||
return { oldVersion: this.currentVersion };
|
||||
}
|
||||
|
||||
switch (selected.id) {
|
||||
case MessageAction.useLocal:
|
||||
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
|
||||
if (selected.version) {
|
||||
const tsConfig = workspace.getConfiguration('typescript');
|
||||
await tsConfig.update('tsdk', selected.version.pathLabel, false);
|
||||
|
||||
const previousVersion = this.currentVersion;
|
||||
this._currentVersion = selected.version;
|
||||
return { oldVersion: previousVersion, newVersion: selected.version };
|
||||
}
|
||||
return { oldVersion: this.currentVersion };
|
||||
|
||||
case MessageAction.useBundled:
|
||||
await this.workspaceState.update(useWorkspaceTsdkStorageKey, false);
|
||||
const previousVersion = this.currentVersion;
|
||||
this._currentVersion = shippedVersion;
|
||||
return { oldVersion: previousVersion, newVersion: shippedVersion };
|
||||
|
||||
|
||||
case MessageAction.learnMore:
|
||||
commands.executeCommand('vscode.open', Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919'));
|
||||
return { oldVersion: this.currentVersion };
|
||||
|
||||
default:
|
||||
return { oldVersion: this.currentVersion };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { workspace, window } from 'vscode';
|
||||
|
||||
import { TypeScriptServiceConfiguration } from './configuration';
|
||||
import { RelativeWorkspacePathResolver } from './relativePathResolver';
|
||||
import API from './api';
|
||||
|
||||
|
||||
export class TypeScriptVersion {
|
||||
constructor(
|
||||
public readonly path: string,
|
||||
private readonly _pathLabel?: string
|
||||
) { }
|
||||
|
||||
public get tsServerPath(): string {
|
||||
return path.join(this.path, 'tsserver.js');
|
||||
}
|
||||
|
||||
public get pathLabel(): string {
|
||||
return typeof this._pathLabel === 'undefined' ? this.path : this._pathLabel;
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.version !== undefined;
|
||||
}
|
||||
|
||||
public get version(): API | undefined {
|
||||
const version = this.getTypeScriptVersion(this.tsServerPath);
|
||||
if (version) {
|
||||
return version;
|
||||
}
|
||||
|
||||
// Allow TS developers to provide custom version
|
||||
const tsdkVersion = workspace.getConfiguration().get<string | undefined>('typescript.tsdk_version', undefined);
|
||||
if (tsdkVersion) {
|
||||
return API.fromVersionString(tsdkVersion);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get versionString(): string {
|
||||
const version = this.version;
|
||||
return version ? version.versionString : localize(
|
||||
'couldNotLoadTsVersion', 'Could not load the TypeScript version at this path');
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class TypeScriptVersionProvider {
|
||||
private readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver();
|
||||
|
||||
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(this.configuration.globalTsdk);
|
||||
if (globals && globals.length) {
|
||||
return globals[0];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
const bundledVersion = new TypeScriptVersion(
|
||||
path.dirname(require.resolve('typescript/lib/tsserver.js')),
|
||||
'');
|
||||
if (bundledVersion.isValid) {
|
||||
return bundledVersion;
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
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 localTsdkVersions(): TypeScriptVersion[] {
|
||||
const localTsdk = this.configuration.localTsdk;
|
||||
return localTsdk ? this.loadVersionsFromSetting(localTsdk) : [];
|
||||
}
|
||||
|
||||
private loadVersionsFromSetting(tsdkPathSetting: string): TypeScriptVersion[] {
|
||||
if (path.isAbsolute(tsdkPathSetting)) {
|
||||
return [new TypeScriptVersion(tsdkPathSetting)];
|
||||
}
|
||||
|
||||
const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting);
|
||||
if (workspacePath !== undefined) {
|
||||
return [new TypeScriptVersion(workspacePath, tsdkPathSetting)];
|
||||
}
|
||||
|
||||
return this.loadTypeScriptVersionsFromPath(tsdkPathSetting);
|
||||
}
|
||||
|
||||
private get localNodeModulesVersions(): TypeScriptVersion[] {
|
||||
return this.loadTypeScriptVersionsFromPath(path.join('node_modules', 'typescript', 'lib'))
|
||||
.filter(x => x.isValid);
|
||||
}
|
||||
|
||||
private loadTypeScriptVersionsFromPath(relativePath: string): TypeScriptVersion[] {
|
||||
if (!workspace.workspaceFolders) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const versions: TypeScriptVersion[] = [];
|
||||
for (const root of workspace.workspaceFolders) {
|
||||
let label: string = relativePath;
|
||||
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 1) {
|
||||
label = path.join(root.name, relativePath);
|
||||
}
|
||||
|
||||
versions.push(new TypeScriptVersion(path.join(root.uri.fsPath, relativePath), label));
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
@@ -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 { TypeScriptVersion } from './versionProvider';
|
||||
import * as languageModeIds from './languageModeIds';
|
||||
|
||||
export default class VersionStatus {
|
||||
private readonly _onChangeEditorSub: vscode.Disposable;
|
||||
private readonly _versionBarEntry: vscode.StatusBarItem;
|
||||
|
||||
constructor(
|
||||
private readonly _normalizePath: (resource: vscode.Uri) => string | null
|
||||
) {
|
||||
this._versionBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */);
|
||||
this._onChangeEditorSub = vscode.window.onDidChangeActiveTextEditor(this.showHideStatus, this);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._versionBarEntry.dispose();
|
||||
this._onChangeEditorSub.dispose();
|
||||
}
|
||||
|
||||
public onDidChangeTypeScriptVersion(version: TypeScriptVersion) {
|
||||
this.showHideStatus();
|
||||
this._versionBarEntry.text = version.versionString;
|
||||
this._versionBarEntry.tooltip = version.path;
|
||||
this._versionBarEntry.command = 'typescript.selectTypeScriptVersion';
|
||||
}
|
||||
|
||||
private showHideStatus() {
|
||||
if (!vscode.window.activeTextEditor) {
|
||||
this._versionBarEntry.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = vscode.window.activeTextEditor.document;
|
||||
if (vscode.languages.match([languageModeIds.typescript, languageModeIds.typescriptreact], doc)) {
|
||||
if (this._normalizePath(doc.uri)) {
|
||||
this._versionBarEntry.show();
|
||||
} else {
|
||||
this._versionBarEntry.hide();
|
||||
}
|
||||
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._versionBarEntry.hide();
|
||||
}
|
||||
}
|
||||
@@ -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 stream from 'stream';
|
||||
|
||||
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 = <Buffer>data;
|
||||
} else {
|
||||
toAppend = Buffer.from(<string>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;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICallback<T> {
|
||||
(data: T): void;
|
||||
}
|
||||
|
||||
export class Reader<T> {
|
||||
|
||||
private readonly buffer: ProtocolBuffer = new ProtocolBuffer();
|
||||
private nextMessageLength: number = -1;
|
||||
|
||||
public constructor(
|
||||
private readonly readable: stream.Readable,
|
||||
private readonly callback: ICallback<T>,
|
||||
private readonly onError: (error: any) => void
|
||||
) {
|
||||
this.readable.on('data', (data: Buffer) => {
|
||||
this.onLengthData(data);
|
||||
});
|
||||
}
|
||||
|
||||
private onLengthData(data: Buffer): void {
|
||||
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.callback(json);
|
||||
}
|
||||
} catch (e) {
|
||||
this.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user