Skip to content

Commit 64d676c

Browse files
mjbvzdbaeumer
authored andcommitted
Add TSC Task Provider (microsoft#27093)
* extract standardLanguageDescriptions to constant * Remove client variable * Move VersionStatus into a class * Add TSC Task Provider Fixes microsoft#26079 Adds a task provider for building typescript projects. * Make ts loading lazy
1 parent 5860e3d commit 64d676c

7 files changed

Lines changed: 235 additions & 76 deletions

File tree

extensions/typescript/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"onCommand:typescript.selectTypeScriptVersion",
3535
"onCommand:javascript.goToProjectConfig",
3636
"onCommand:typescript.goToProjectConfig",
37-
"onCommand:typescript.openTsServerLog"
37+
"onCommand:typescript.openTsServerLog",
38+
"onCommand:workbench.action.tasks.runTask",
39+
"onCommand:workbench.action.tasks.build",
40+
"onCommand:workbench.action.tasks.test"
3841
],
3942
"main": "./out/typescriptMain",
4043
"contributes": {

extensions/typescript/src/features/jsDocCompletionProvider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ export class TryCompleteJsDocCommand {
8888
static COMMAND_NAME = '_typeScript.tryCompleteJsDoc';
8989

9090
constructor(
91-
private client: ITypescriptServiceClient
91+
private lazyClient: () => ITypescriptServiceClient
9292
) { }
9393

9494
/**
9595
* Try to insert a jsdoc comment, using a template provide by typescript
9696
* if possible, otherwise falling back to a default comment format.
9797
*/
9898
public tryCompleteJsDoc(resource: Uri, start: Position, shouldGetJSDocFromTSServer: boolean): Thenable<boolean> {
99-
const file = this.client.normalizePath(resource);
99+
const file = this.lazyClient().normalizePath(resource);
100100
if (!file) {
101101
return Promise.resolve(false);
102102
}
@@ -126,7 +126,7 @@ export class TryCompleteJsDocCommand {
126126
offset: position.character + 1
127127
};
128128
return Promise.race([
129-
this.client.execute('docCommentTemplate', args),
129+
this.lazyClient().execute('docCommentTemplate', args),
130130
new Promise((_, reject) => setTimeout(reject, 250))
131131
]).then((res: DocCommandTemplateResponse) => {
132132
if (!res || !res.body) {
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
import * as fs from 'fs';
9+
import * as path from 'path';
10+
import * as vscode from 'vscode';
11+
12+
import * as Proto from '../protocol';
13+
import TypeScriptServiceClient from '../typescriptServiceClient';
14+
15+
16+
const exists = (file: string): Promise<boolean> =>
17+
new Promise<boolean>((resolve, _reject) => {
18+
fs.exists(file, (value: boolean) => {
19+
resolve(value);
20+
});
21+
});
22+
23+
export default class TypeScriptTaskProvider implements vscode.TaskProvider {
24+
25+
public constructor(
26+
private readonly lazyClient: () => TypeScriptServiceClient
27+
) { }
28+
29+
async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
30+
const rootPath = vscode.workspace.rootPath;
31+
if (!rootPath) {
32+
return [];
33+
}
34+
35+
const projects = (await this.getConfigForActiveFile(token)).concat(await this.getConfigsForWorkspace());
36+
const command = await this.getCommand();
37+
38+
return projects
39+
.filter((x, i) => projects.indexOf(x) === i)
40+
.map(configFile => {
41+
const configFileName = path.relative(rootPath, configFile);
42+
const buildTask = new vscode.ShellTask(`tsc: build ${configFileName}`, `${command} -p ${configFile}`, '$tsc');
43+
buildTask.group = vscode.TaskGroup.Build;
44+
return buildTask;
45+
});
46+
}
47+
48+
49+
private async getConfigForActiveFile(token: vscode.CancellationToken): Promise<string[]> {
50+
const editor = vscode.window.activeTextEditor;
51+
if (editor) {
52+
if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) {
53+
return [editor.document.fileName];
54+
}
55+
}
56+
57+
const file = this.getActiveTypeScriptFile();
58+
if (!file) {
59+
return [];
60+
}
61+
62+
const res: Proto.ProjectInfoResponse = await this.lazyClient().execute(
63+
'projectInfo',
64+
{ file, needFileNameList: false } as protocol.ProjectInfoRequestArgs,
65+
token);
66+
67+
if (!res || !res.body) {
68+
return [];
69+
}
70+
71+
const { configFileName } = res.body;
72+
if (configFileName && configFileName.indexOf('/dev/null/') !== 0) {
73+
return [configFileName];
74+
}
75+
return [];
76+
}
77+
78+
private async getConfigsForWorkspace(): Promise<string[]> {
79+
if (!vscode.workspace.rootPath) {
80+
return [];
81+
}
82+
const rootTsConfig = path.join(vscode.workspace.rootPath, 'tsconfig.json');
83+
if (!await exists(rootTsConfig)) {
84+
return [];
85+
}
86+
return [rootTsConfig];
87+
}
88+
89+
private async getCommand(): Promise<string> {
90+
const platform = process.platform;
91+
if (platform === 'win32' && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc.cmd'))) {
92+
return path.join('.', 'node_modules', '.bin', 'tsc.cmd');
93+
} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc'))) {
94+
return path.join('.', 'node_modules', '.bin', 'tsc');
95+
} else {
96+
return 'tsc';
97+
}
98+
}
99+
100+
private getActiveTypeScriptFile(): string | null {
101+
const editor = vscode.window.activeTextEditor;
102+
if (editor) {
103+
const document = editor.document;
104+
if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
105+
return this.lazyClient().normalizePath(document.uri);
106+
}
107+
}
108+
return null;
109+
}
110+
}

extensions/typescript/src/typescriptMain.ts

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ import CodeActionProvider from './features/codeActionProvider';
4141
import ReferenceCodeLensProvider from './features/referencesCodeLensProvider';
4242
import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider';
4343
import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider';
44+
import TypeScriptTaskProvider from './features/taskProvider';
4445

4546
import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider';
4647

4748
import * as BuildStatus from './utils/buildStatus';
4849
import * as ProjectStatus from './utils/projectStatus';
4950
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
50-
import * as VersionStatus from './utils/versionStatus';
51+
import VersionStatus from './utils/versionStatus';
5152
import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from "./utils/plugins";
5253

5354
interface LanguageDescription {
@@ -67,72 +68,100 @@ interface ProjectConfigMessageItem extends MessageItem {
6768
id: ProjectConfigAction;
6869
}
6970

71+
const MODE_ID_TS = 'typescript';
72+
const MODE_ID_TSX = 'typescriptreact';
73+
const MODE_ID_JS = 'javascript';
74+
const MODE_ID_JSX = 'javascriptreact';
75+
76+
const standardLanguageDescriptions: LanguageDescription[] = [
77+
{
78+
id: 'typescript',
79+
diagnosticSource: 'ts',
80+
modeIds: [MODE_ID_TS, MODE_ID_TSX],
81+
configFile: 'tsconfig.json'
82+
}, {
83+
id: 'javascript',
84+
diagnosticSource: 'js',
85+
modeIds: [MODE_ID_JS, MODE_ID_JSX],
86+
configFile: 'jsconfig.json'
87+
}
88+
];
7089

7190
export function activate(context: ExtensionContext): void {
72-
const MODE_ID_TS = 'typescript';
73-
const MODE_ID_TSX = 'typescriptreact';
74-
const MODE_ID_JS = 'javascript';
75-
const MODE_ID_JSX = 'javascriptreact';
76-
7791
const plugins = getContributedTypeScriptServerPlugins();
78-
const clientHost = new TypeScriptServiceClientHost([
79-
{
80-
id: 'typescript',
81-
diagnosticSource: 'ts',
82-
modeIds: [MODE_ID_TS, MODE_ID_TSX],
83-
configFile: 'tsconfig.json'
84-
},
85-
{
86-
id: 'javascript',
87-
diagnosticSource: 'js',
88-
modeIds: [MODE_ID_JS, MODE_ID_JSX],
89-
configFile: 'jsconfig.json'
90-
}
91-
], context.storagePath, context.globalState, context.workspaceState, plugins);
92-
context.subscriptions.push(clientHost);
9392

94-
const client = clientHost.serviceClient;
93+
const lazyClientHost = (() => {
94+
let clientHost: TypeScriptServiceClientHost | undefined;
95+
return () => {
96+
if (!clientHost) {
97+
clientHost = new TypeScriptServiceClientHost(standardLanguageDescriptions, context.storagePath, context.globalState, context.workspaceState, plugins);
98+
context.subscriptions.push(clientHost);
99+
100+
const host = clientHost;
101+
clientHost.serviceClient.onReady().then(() => {
102+
context.subscriptions.push(ProjectStatus.create(host.serviceClient,
103+
path => new Promise<boolean>(resolve => setTimeout(() => resolve(host.handles(path)), 750)),
104+
context.workspaceState));
105+
}, () => {
106+
// Nothing to do here. The client did show a message;
107+
});
108+
}
109+
return clientHost;
110+
};
111+
})();
112+
95113

96114
context.subscriptions.push(commands.registerCommand('typescript.reloadProjects', () => {
97-
clientHost.reloadProjects();
115+
lazyClientHost().reloadProjects();
98116
}));
99117

100118
context.subscriptions.push(commands.registerCommand('javascript.reloadProjects', () => {
101-
clientHost.reloadProjects();
119+
lazyClientHost().reloadProjects();
102120
}));
103121

104122
context.subscriptions.push(commands.registerCommand('typescript.selectTypeScriptVersion', () => {
105-
client.onVersionStatusClicked();
123+
lazyClientHost().serviceClient.onVersionStatusClicked();
106124
}));
107125

108126
context.subscriptions.push(commands.registerCommand('typescript.openTsServerLog', () => {
109-
client.openTsServerLogFile();
127+
lazyClientHost().serviceClient.openTsServerLogFile();
110128
}));
111129

112130
context.subscriptions.push(commands.registerCommand('typescript.restartTsServer', () => {
113-
client.restartTsServer();
131+
lazyClientHost().serviceClient.restartTsServer();
114132
}));
115133

134+
context.subscriptions.push(workspace.registerTaskProvider(new TypeScriptTaskProvider(() => lazyClientHost().serviceClient)));
135+
116136
const goToProjectConfig = (isTypeScript: boolean) => {
117137
const editor = window.activeTextEditor;
118138
if (editor) {
119-
clientHost.goToProjectConfig(isTypeScript, editor.document.uri);
139+
lazyClientHost().goToProjectConfig(isTypeScript, editor.document.uri);
120140
}
121141
};
122142
context.subscriptions.push(commands.registerCommand('typescript.goToProjectConfig', goToProjectConfig.bind(null, true)));
123143
context.subscriptions.push(commands.registerCommand('javascript.goToProjectConfig', goToProjectConfig.bind(null, false)));
124144

125-
const jsDocCompletionCommand = new TryCompleteJsDocCommand(client);
145+
const jsDocCompletionCommand = new TryCompleteJsDocCommand(() => lazyClientHost().serviceClient);
126146
context.subscriptions.push(commands.registerCommand(TryCompleteJsDocCommand.COMMAND_NAME, jsDocCompletionCommand.tryCompleteJsDoc, jsDocCompletionCommand));
127147

128-
window.onDidChangeActiveTextEditor(VersionStatus.showHideStatus, null, context.subscriptions);
129-
client.onReady().then(() => {
130-
context.subscriptions.push(ProjectStatus.create(client,
131-
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
132-
context.workspaceState));
133-
}, () => {
134-
// Nothing to do here. The client did show a message;
135-
});
148+
const supportedLanguage = [].concat.apply([], standardLanguageDescriptions.map(x => x.modeIds).concat(plugins.map(x => x.languages)));
149+
function didOpenTextDocument(textDocument: TextDocument): boolean {
150+
if (supportedLanguage.indexOf(textDocument.languageId) >= 0) {
151+
openListener.dispose();
152+
// Force activation
153+
void lazyClientHost();
154+
return true;
155+
}
156+
return false;
157+
};
158+
const openListener = workspace.onDidOpenTextDocument(didOpenTextDocument);
159+
for (let textDocument of workspace.textDocuments) {
160+
if (didOpenTextDocument(textDocument)) {
161+
break;
162+
}
163+
}
164+
136165
BuildStatus.update({ queueLength: 0 });
137166
}
138167

@@ -423,6 +452,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
423452
private languages: LanguageProvider[] = [];
424453
private languagePerId: ObjectMap<LanguageProvider>;
425454
private readonly disposables: Disposable[] = [];
455+
private readonly versionStatus: VersionStatus;
426456

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

449-
this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, plugins, this.disposables);
479+
this.versionStatus = new VersionStatus();
480+
this.disposables.push(this.versionStatus);
481+
482+
this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, this.versionStatus, plugins, this.disposables);
450483
this.languagePerId = Object.create(null);
451484
for (const description of descriptions) {
452485
const manager = new LanguageProvider(this.client, description);

extensions/typescript/src/typescriptServiceClient.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { ITypescriptServiceClient, ITypescriptServiceClientHost, API } from './t
1818
import { TypeScriptServerPlugin } from './utils/plugins';
1919
import Logger from './utils/logger';
2020

21-
import * as VersionStatus from './utils/versionStatus';
21+
import VersionStatus from './utils/versionStatus';
2222
import * as is from './utils/is';
2323

2424
import * as nls from 'vscode-nls';
@@ -141,7 +141,9 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
141141
host: ITypescriptServiceClientHost,
142142
storagePath: string | undefined,
143143
globalState: Memento,
144-
private workspaceState: Memento,
144+
private readonly workspaceState: Memento,
145+
private readonly versionStatus: VersionStatus,
146+
145147
private plugins: TypeScriptServerPlugin[],
146148
disposables: Disposable[]
147149
) {
@@ -404,8 +406,8 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
404406
const label = version || localize('versionNumber.custom', 'custom');
405407
const tooltip = modulePath;
406408
this.modulePath = modulePath;
407-
VersionStatus.showHideStatus();
408-
VersionStatus.setInfo(label, tooltip);
409+
this.versionStatus.showHideStatus();
410+
this.versionStatus.setInfo(label, tooltip);
409411

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

0 commit comments

Comments
 (0)