Skip to content

Commit

Permalink
Add TSC Task Provider (#27093)
Browse files Browse the repository at this point in the history
* extract standardLanguageDescriptions to constant

* Remove client variable

* Move VersionStatus into a class

* Add TSC Task Provider

Fixes #26079

Adds a task provider for building typescript projects.

* Make ts loading lazy
  • Loading branch information
mjbvz authored and dbaeumer committed May 26, 2017
1 parent 5860e3d commit 64d676c
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 76 deletions.
5 changes: 4 additions & 1 deletion extensions/typescript/package.json
Expand Up @@ -34,7 +34,10 @@
"onCommand:typescript.selectTypeScriptVersion",
"onCommand:javascript.goToProjectConfig",
"onCommand:typescript.goToProjectConfig",
"onCommand:typescript.openTsServerLog"
"onCommand:typescript.openTsServerLog",
"onCommand:workbench.action.tasks.runTask",
"onCommand:workbench.action.tasks.build",
"onCommand:workbench.action.tasks.test"
],
"main": "./out/typescriptMain",
"contributes": {
Expand Down
6 changes: 3 additions & 3 deletions extensions/typescript/src/features/jsDocCompletionProvider.ts
Expand Up @@ -88,15 +88,15 @@ export class TryCompleteJsDocCommand {
static COMMAND_NAME = '_typeScript.tryCompleteJsDoc';

constructor(
private client: ITypescriptServiceClient
private lazyClient: () => ITypescriptServiceClient
) { }

/**
* Try to insert a jsdoc comment, using a template provide by typescript
* if possible, otherwise falling back to a default comment format.
*/
public tryCompleteJsDoc(resource: Uri, start: Position, shouldGetJSDocFromTSServer: boolean): Thenable<boolean> {
const file = this.client.normalizePath(resource);
const file = this.lazyClient().normalizePath(resource);
if (!file) {
return Promise.resolve(false);
}
Expand Down Expand Up @@ -126,7 +126,7 @@ export class TryCompleteJsDocCommand {
offset: position.character + 1
};
return Promise.race([
this.client.execute('docCommentTemplate', args),
this.lazyClient().execute('docCommentTemplate', args),
new Promise((_, reject) => setTimeout(reject, 250))
]).then((res: DocCommandTemplateResponse) => {
if (!res || !res.body) {
Expand Down
110 changes: 110 additions & 0 deletions extensions/typescript/src/features/taskProvider.ts
@@ -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.
*--------------------------------------------------------------------------------------------*/

'use strict';

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';

import * as Proto from '../protocol';
import TypeScriptServiceClient from '../typescriptServiceClient';


const exists = (file: string): Promise<boolean> =>
new Promise<boolean>((resolve, _reject) => {
fs.exists(file, (value: boolean) => {
resolve(value);
});
});

export default class TypeScriptTaskProvider implements vscode.TaskProvider {

public constructor(
private readonly lazyClient: () => TypeScriptServiceClient
) { }

async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
const rootPath = vscode.workspace.rootPath;
if (!rootPath) {
return [];
}

const projects = (await this.getConfigForActiveFile(token)).concat(await this.getConfigsForWorkspace());
const command = await this.getCommand();

return projects
.filter((x, i) => projects.indexOf(x) === i)
.map(configFile => {
const configFileName = path.relative(rootPath, configFile);
const buildTask = new vscode.ShellTask(`tsc: build ${configFileName}`, `${command} -p ${configFile}`, '$tsc');
buildTask.group = vscode.TaskGroup.Build;
return buildTask;
});
}


private async getConfigForActiveFile(token: vscode.CancellationToken): Promise<string[]> {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) {
return [editor.document.fileName];
}
}

const file = this.getActiveTypeScriptFile();
if (!file) {
return [];
}

const res: Proto.ProjectInfoResponse = await this.lazyClient().execute(
'projectInfo',
{ file, needFileNameList: false } as protocol.ProjectInfoRequestArgs,
token);

if (!res || !res.body) {
return [];
}

const { configFileName } = res.body;
if (configFileName && configFileName.indexOf('/dev/null/') !== 0) {
return [configFileName];
}
return [];
}

private async getConfigsForWorkspace(): Promise<string[]> {
if (!vscode.workspace.rootPath) {
return [];
}
const rootTsConfig = path.join(vscode.workspace.rootPath, 'tsconfig.json');
if (!await exists(rootTsConfig)) {
return [];
}
return [rootTsConfig];
}

private async getCommand(): Promise<string> {
const platform = process.platform;
if (platform === 'win32' && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc.cmd'))) {
return path.join('.', 'node_modules', '.bin', 'tsc.cmd');
} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc'))) {
return path.join('.', 'node_modules', '.bin', 'tsc');
} else {
return 'tsc';
}
}

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.lazyClient().normalizePath(document.uri);
}
}
return null;
}
}
109 changes: 71 additions & 38 deletions extensions/typescript/src/typescriptMain.ts
Expand Up @@ -41,13 +41,14 @@ import CodeActionProvider from './features/codeActionProvider';
import ReferenceCodeLensProvider from './features/referencesCodeLensProvider';
import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider';
import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider';
import TypeScriptTaskProvider from './features/taskProvider';

import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider';

import * as BuildStatus from './utils/buildStatus';
import * as ProjectStatus from './utils/projectStatus';
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
import * as VersionStatus from './utils/versionStatus';
import VersionStatus from './utils/versionStatus';
import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from "./utils/plugins";

interface LanguageDescription {
Expand All @@ -67,72 +68,100 @@ interface ProjectConfigMessageItem extends MessageItem {
id: ProjectConfigAction;
}

const MODE_ID_TS = 'typescript';
const MODE_ID_TSX = 'typescriptreact';
const MODE_ID_JS = 'javascript';
const MODE_ID_JSX = 'javascriptreact';

const standardLanguageDescriptions: LanguageDescription[] = [
{
id: 'typescript',
diagnosticSource: 'ts',
modeIds: [MODE_ID_TS, MODE_ID_TSX],
configFile: 'tsconfig.json'
}, {
id: 'javascript',
diagnosticSource: 'js',
modeIds: [MODE_ID_JS, MODE_ID_JSX],
configFile: 'jsconfig.json'
}
];

export function activate(context: ExtensionContext): void {
const MODE_ID_TS = 'typescript';
const MODE_ID_TSX = 'typescriptreact';
const MODE_ID_JS = 'javascript';
const MODE_ID_JSX = 'javascriptreact';

const plugins = getContributedTypeScriptServerPlugins();
const clientHost = new TypeScriptServiceClientHost([
{
id: 'typescript',
diagnosticSource: 'ts',
modeIds: [MODE_ID_TS, MODE_ID_TSX],
configFile: 'tsconfig.json'
},
{
id: 'javascript',
diagnosticSource: 'js',
modeIds: [MODE_ID_JS, MODE_ID_JSX],
configFile: 'jsconfig.json'
}
], context.storagePath, context.globalState, context.workspaceState, plugins);
context.subscriptions.push(clientHost);

const client = clientHost.serviceClient;
const lazyClientHost = (() => {
let clientHost: TypeScriptServiceClientHost | undefined;
return () => {
if (!clientHost) {
clientHost = new TypeScriptServiceClientHost(standardLanguageDescriptions, context.storagePath, context.globalState, context.workspaceState, plugins);
context.subscriptions.push(clientHost);

const host = clientHost;
clientHost.serviceClient.onReady().then(() => {
context.subscriptions.push(ProjectStatus.create(host.serviceClient,
path => new Promise<boolean>(resolve => setTimeout(() => resolve(host.handles(path)), 750)),
context.workspaceState));
}, () => {
// Nothing to do here. The client did show a message;
});
}
return clientHost;
};
})();


context.subscriptions.push(commands.registerCommand('typescript.reloadProjects', () => {
clientHost.reloadProjects();
lazyClientHost().reloadProjects();
}));

context.subscriptions.push(commands.registerCommand('javascript.reloadProjects', () => {
clientHost.reloadProjects();
lazyClientHost().reloadProjects();
}));

context.subscriptions.push(commands.registerCommand('typescript.selectTypeScriptVersion', () => {
client.onVersionStatusClicked();
lazyClientHost().serviceClient.onVersionStatusClicked();
}));

context.subscriptions.push(commands.registerCommand('typescript.openTsServerLog', () => {
client.openTsServerLogFile();
lazyClientHost().serviceClient.openTsServerLogFile();
}));

context.subscriptions.push(commands.registerCommand('typescript.restartTsServer', () => {
client.restartTsServer();
lazyClientHost().serviceClient.restartTsServer();
}));

context.subscriptions.push(workspace.registerTaskProvider(new TypeScriptTaskProvider(() => lazyClientHost().serviceClient)));

const goToProjectConfig = (isTypeScript: boolean) => {
const editor = window.activeTextEditor;
if (editor) {
clientHost.goToProjectConfig(isTypeScript, editor.document.uri);
lazyClientHost().goToProjectConfig(isTypeScript, editor.document.uri);
}
};
context.subscriptions.push(commands.registerCommand('typescript.goToProjectConfig', goToProjectConfig.bind(null, true)));
context.subscriptions.push(commands.registerCommand('javascript.goToProjectConfig', goToProjectConfig.bind(null, false)));

const jsDocCompletionCommand = new TryCompleteJsDocCommand(client);
const jsDocCompletionCommand = new TryCompleteJsDocCommand(() => lazyClientHost().serviceClient);
context.subscriptions.push(commands.registerCommand(TryCompleteJsDocCommand.COMMAND_NAME, jsDocCompletionCommand.tryCompleteJsDoc, jsDocCompletionCommand));

window.onDidChangeActiveTextEditor(VersionStatus.showHideStatus, null, context.subscriptions);
client.onReady().then(() => {
context.subscriptions.push(ProjectStatus.create(client,
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
context.workspaceState));
}, () => {
// Nothing to do here. The client did show a message;
});
const supportedLanguage = [].concat.apply([], standardLanguageDescriptions.map(x => x.modeIds).concat(plugins.map(x => x.languages)));
function didOpenTextDocument(textDocument: TextDocument): boolean {
if (supportedLanguage.indexOf(textDocument.languageId) >= 0) {
openListener.dispose();
// Force activation
void lazyClientHost();
return true;
}
return false;
};
const openListener = workspace.onDidOpenTextDocument(didOpenTextDocument);
for (let textDocument of workspace.textDocuments) {
if (didOpenTextDocument(textDocument)) {
break;
}
}

BuildStatus.update({ queueLength: 0 });
}

Expand Down Expand Up @@ -423,6 +452,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
private languages: LanguageProvider[] = [];
private languagePerId: ObjectMap<LanguageProvider>;
private readonly disposables: Disposable[] = [];
private readonly versionStatus: VersionStatus;

constructor(
descriptions: LanguageDescription[],
Expand All @@ -446,7 +476,10 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
configFileWatcher.onDidDelete(handleProjectCreateOrDelete, this, this.disposables);
configFileWatcher.onDidChange(handleProjectChange, this, this.disposables);

this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, plugins, this.disposables);
this.versionStatus = new VersionStatus();
this.disposables.push(this.versionStatus);

this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, this.versionStatus, plugins, this.disposables);
this.languagePerId = Object.create(null);
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description);
Expand Down
10 changes: 6 additions & 4 deletions extensions/typescript/src/typescriptServiceClient.ts
Expand Up @@ -18,7 +18,7 @@ import { ITypescriptServiceClient, ITypescriptServiceClientHost, API } from './t
import { TypeScriptServerPlugin } from './utils/plugins';
import Logger from './utils/logger';

import * as VersionStatus from './utils/versionStatus';
import VersionStatus from './utils/versionStatus';
import * as is from './utils/is';

import * as nls from 'vscode-nls';
Expand Down Expand Up @@ -141,7 +141,9 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
host: ITypescriptServiceClientHost,
storagePath: string | undefined,
globalState: Memento,
private workspaceState: Memento,
private readonly workspaceState: Memento,
private readonly versionStatus: VersionStatus,

private plugins: TypeScriptServerPlugin[],
disposables: Disposable[]
) {
Expand Down Expand Up @@ -404,8 +406,8 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
const label = version || localize('versionNumber.custom', 'custom');
const tooltip = modulePath;
this.modulePath = modulePath;
VersionStatus.showHideStatus();
VersionStatus.setInfo(label, tooltip);
this.versionStatus.showHideStatus();
this.versionStatus.setInfo(label, tooltip);

// This is backwards compatibility code to move the setting from the local
// store into the workspace setting file.
Expand Down

0 comments on commit 64d676c

Please sign in to comment.