diff --git a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts index d70f59472ff..24925524387 100644 --- a/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts +++ b/extensions/typescript-language-features/src/commands/selectTypeScriptVersion.ts @@ -8,7 +8,8 @@ import { Lazy } from '../utils/lazy'; import { Command } from './commandManager'; export class SelectTypeScriptVersionCommand implements Command { - public readonly id = 'typescript.selectTypeScriptVersion'; + public static readonly id = 'typescript.selectTypeScriptVersion'; + public readonly id = SelectTypeScriptVersionCommand.id; public constructor( private readonly lazyClientHost: Lazy diff --git a/extensions/typescript-language-features/src/tsServer/projectStatus.ts b/extensions/typescript-language-features/src/tsServer/projectStatus.ts new file mode 100644 index 00000000000..50c04bd9188 --- /dev/null +++ b/extensions/typescript-language-features/src/tsServer/projectStatus.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { CommandManager } from '../commands/commandManager'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import { ActiveJsTsEditorTracker } from '../utils/activeJsTsEditorTracker'; +import { Disposable } from '../utils/dispose'; +import * as languageModeIds from '../utils/languageModeIds'; +import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig'; + +const localize = nls.loadMessageBundle(); + + +namespace ProjectInfoState { + export const enum Type { None, Pending, Resolved } + + export const None = Object.freeze({ type: Type.None } as const); + + export class Pending { + public readonly type = Type.Pending; + + public readonly cancellation = new vscode.CancellationTokenSource(); + + constructor( + public readonly resource: vscode.Uri, + ) { } + } + + export class Resolved { + public readonly type = Type.Resolved; + + constructor( + public readonly resource: vscode.Uri, + public readonly configFile: string, + ) { } + } + + export type State = typeof None | Pending | Resolved; +} + +export class ProjectStatus extends Disposable { + + public readonly openOpenConfigCommandId = '_typescript.openConfig'; + public readonly createConfigCommandId = '_typescript.createConfig'; + + private readonly _statusItem: vscode.LanguageStatusItem; + + private _ready = false; + private _state: ProjectInfoState.State = ProjectInfoState.None; + + constructor( + private readonly _client: ITypeScriptServiceClient, + commandManager: CommandManager, + private readonly _activeTextEditorManager: ActiveJsTsEditorTracker, + ) { + super(); + + this._statusItem = this._register(vscode.languages.createLanguageStatusItem('typescript.projectStatus', [ + languageModeIds.javascript, + languageModeIds.javascriptreact, + languageModeIds.typescript, + languageModeIds.typescriptreact, + ])); + this._statusItem.name = localize('statusItem.name', "Project config"); + this._statusItem.text = 'TSConfig'; + + commandManager.register({ + id: this.openOpenConfigCommandId, + execute: async (rootPath: string) => { + if (this._state.type === ProjectInfoState.Type.Resolved) { + await openProjectConfigOrPromptToCreate(ProjectType.TypeScript, this._client, rootPath, this._state.configFile); + } else if (this._state.type === ProjectInfoState.Type.Pending) { + await openProjectConfigForFile(ProjectType.TypeScript, this._client, this._state.resource); + } + }, + }); + commandManager.register({ + id: this.createConfigCommandId, + execute: async (rootPath: string) => { + await openOrCreateConfig(ProjectType.TypeScript, rootPath, this._client.configuration); + }, + }); + + _activeTextEditorManager.onDidChangeActiveJsTsEditor(this.updateStatus, this, this._disposables); + + this._client.onReady(() => { + this._ready = true; + this.updateStatus(); + }); + } + + private async updateStatus() { + const editor = this._activeTextEditorManager.activeJsTsEditor; + if (!editor) { + this.updateState(ProjectInfoState.None); + return; + } + + const doc = editor.document; + if (languageModeIds.isSupportedLanguageMode(doc)) { + const file = this._client.toOpenedFilePath(doc, { suppressAlertOnFailure: true }); + if (file) { + if (!this._ready) { + return; + } + + const pendingState = new ProjectInfoState.Pending(doc.uri); + this.updateState(pendingState); + + const response = await this._client.execute('projectInfo', { file, needFileNameList: false }, pendingState.cancellation.token); + if (response.type === 'response' && response.body) { + if (this._state === pendingState) { + this.updateState(new ProjectInfoState.Resolved(doc.uri, response.body.configFileName)); + } + } + return; + } + } + + this.updateState(ProjectInfoState.None); + } + + private updateState(newState: ProjectInfoState.State): void { + if (this._state === newState) { + return; + } + + if (this._state.type === ProjectInfoState.Type.Pending) { + this._state.cancellation.cancel(); + this._state.cancellation.dispose(); + } + + this._state = newState; + + const rootPath = this._state.type === ProjectInfoState.Type.Resolved ? this._client.getWorkspaceRootForResource(this._state.resource) : undefined; + if (!rootPath) { + return; + } + + if (this._state.type === ProjectInfoState.Type.Resolved) { + if (isImplicitProjectConfigFile(this._state.configFile)) { + this._statusItem.detail = localize('item.noTsConfig.detail', "None"); + this._statusItem.command = { + command: this.createConfigCommandId, + title: localize('create.command', "Create tsconfig"), + arguments: [rootPath], + }; + return; + } + } + + this._statusItem.detail = this._state.type === ProjectInfoState.Type.Resolved ? vscode.workspace.asRelativePath(this._state.configFile) : ''; + this._statusItem.command = { + command: this.openOpenConfigCommandId, + title: localize('item.command', "Open config file"), + arguments: [rootPath], + }; + } +} diff --git a/extensions/typescript-language-features/src/tsServer/versionStatus.ts b/extensions/typescript-language-features/src/tsServer/versionStatus.ts index 2560f3e8be9..814ef4d4daf 100644 --- a/extensions/typescript-language-features/src/tsServer/versionStatus.ts +++ b/extensions/typescript-language-features/src/tsServer/versionStatus.ts @@ -5,208 +5,42 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { Command, CommandManager } from '../commands/commandManager'; +import { SelectTypeScriptVersionCommand } from '../commands/selectTypeScriptVersion'; import { ITypeScriptServiceClient } from '../typescriptService'; -import { ActiveJsTsEditorTracker } from '../utils/activeJsTsEditorTracker'; -import { coalesce } from '../utils/arrays'; import { Disposable } from '../utils/dispose'; -import { isTypeScriptDocument } from '../utils/languageModeIds'; -import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig'; +import * as languageModeIds from '../utils/languageModeIds'; import { TypeScriptVersion } from './versionProvider'; const localize = nls.loadMessageBundle(); +export class VersionStatus extends Disposable { -namespace ProjectInfoState { - export const enum Type { None, Pending, Resolved } - - export const None = Object.freeze({ type: Type.None } as const); - - export class Pending { - public readonly type = Type.Pending; - - public readonly cancellation = new vscode.CancellationTokenSource(); - - constructor( - public readonly resource: vscode.Uri, - ) { } - } - - export class Resolved { - public readonly type = Type.Resolved; - - constructor( - public readonly resource: vscode.Uri, - public readonly configFile: string, - ) { } - } - - export type State = typeof None | Pending | Resolved; -} - -interface QuickPickItem extends vscode.QuickPickItem { - run(): void; -} - -class ProjectStatusCommand implements Command { - public readonly id = '_typescript.projectStatus'; - - public constructor( - private readonly _client: ITypeScriptServiceClient, - private readonly _delegate: () => ProjectInfoState.State, - ) { } - - public async execute(): Promise { - const info = this._delegate(); - - const result = await vscode.window.showQuickPick(coalesce([ - this.getProjectItem(info), - this.getVersionItem(), - this.getHelpItem(), - ]), { - placeHolder: localize('projectQuickPick.placeholder', "TypeScript Project Info"), - }); - - return result?.run(); - } - - private getVersionItem(): QuickPickItem { - return { - label: localize('projectQuickPick.version.label', "Select TypeScript Version..."), - description: localize('projectQuickPick.version.description', "[current = {0}]", this._client.apiVersion.displayName), - run: () => { - this._client.showVersionPicker(); - } - }; - } - - private getProjectItem(info: ProjectInfoState.State): QuickPickItem | undefined { - const rootPath = info.type === ProjectInfoState.Type.Resolved ? this._client.getWorkspaceRootForResource(info.resource) : undefined; - if (!rootPath) { - return undefined; - } - - if (info.type === ProjectInfoState.Type.Resolved) { - if (isImplicitProjectConfigFile(info.configFile)) { - return { - label: localize('projectQuickPick.project.create', "Create tsconfig"), - detail: localize('projectQuickPick.project.create.description', "This file is currently not part of a tsconfig/jsconfig project"), - run: () => { - openOrCreateConfig(ProjectType.TypeScript, rootPath, this._client.configuration); - } - }; - } - } - - return { - label: localize('projectQuickPick.version.goProjectConfig', "Open tsconfig"), - description: info.type === ProjectInfoState.Type.Resolved ? vscode.workspace.asRelativePath(info.configFile) : undefined, - run: () => { - if (info.type === ProjectInfoState.Type.Resolved) { - openProjectConfigOrPromptToCreate(ProjectType.TypeScript, this._client, rootPath, info.configFile); - } else if (info.type === ProjectInfoState.Type.Pending) { - openProjectConfigForFile(ProjectType.TypeScript, this._client, info.resource); - } - } - }; - } - - private getHelpItem(): QuickPickItem { - return { - label: localize('projectQuickPick.help', "TypeScript help"), - run: () => { - vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919')); // TODO: - } - }; - } -} - -export default class VersionStatus extends Disposable { - - private readonly _statusBarEntry: vscode.StatusBarItem; - - private _ready = false; - private _state: ProjectInfoState.State = ProjectInfoState.None; + private readonly _statusItem: vscode.LanguageStatusItem; constructor( private readonly _client: ITypeScriptServiceClient, - commandManager: CommandManager, - private readonly _activeTextEditorManager: ActiveJsTsEditorTracker, ) { super(); - this._statusBarEntry = this._register(vscode.window.createStatusBarItem('status.typescript', vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */)); - this._statusBarEntry.name = localize('projectInfo.name', "TypeScript: Project Info"); + this._statusItem = this._register(vscode.languages.createLanguageStatusItem('typescript.version', [ + languageModeIds.javascript, + languageModeIds.javascriptreact, + languageModeIds.typescript, + languageModeIds.typescriptreact, + ])); - const command = new ProjectStatusCommand(this._client, () => this._state); - commandManager.register(command); - this._statusBarEntry.command = command.id; - - _activeTextEditorManager.onDidChangeActiveJsTsEditor(this.updateStatus, this, this._disposables); - - this._client.onReady(() => { - this._ready = true; - this.updateStatus(); - }); + this._statusItem.name = localize('versionStatus.name', "TypeScript Version"); + this._statusItem.detail = localize('versionStatus.detail', "TypeScript Version"); this._register(this._client.onTsServerStarted(({ version }) => this.onDidChangeTypeScriptVersion(version))); } private onDidChangeTypeScriptVersion(version: TypeScriptVersion) { - this._statusBarEntry.text = version.displayName; - this._statusBarEntry.tooltip = version.path; - this.updateStatus(); - } - - private async updateStatus() { - const editor = this._activeTextEditorManager.activeJsTsEditor; - if (!editor) { - this.hide(); - return; - } - - const doc = editor.document; - if (isTypeScriptDocument(doc)) { - const file = this._client.toOpenedFilePath(doc, { suppressAlertOnFailure: true }); - if (file) { - this._statusBarEntry.show(); - if (!this._ready) { - return; - } - - const pendingState = new ProjectInfoState.Pending(doc.uri); - this.updateState(pendingState); - - const response = await this._client.execute('projectInfo', { file, needFileNameList: false }, pendingState.cancellation.token); - if (response.type === 'response' && response.body) { - if (this._state === pendingState) { - this.updateState(new ProjectInfoState.Resolved(doc.uri, response.body.configFileName)); - this._statusBarEntry.show(); - } - } - - return; - } - } - - this.hide(); - } - - private hide(): void { - this._statusBarEntry.hide(); - this.updateState(ProjectInfoState.None); - } - - private updateState(newState: ProjectInfoState.State): void { - if (this._state === newState) { - return; - } - - if (this._state.type === ProjectInfoState.Type.Pending) { - this._state.cancellation.cancel(); - this._state.cancellation.dispose(); - } - - this._state = newState; + this._statusItem.text = version.displayName; + this._statusItem.command = { + command: SelectTypeScriptVersionCommand.id, + title: localize('versionStatus.command', "Select Version"), + tooltip: version.path + }; } } diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index be167b5e05e..cdb43f1fc5b 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -17,9 +17,10 @@ import * as Proto from './protocol'; import * as PConst from './protocol.const'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { ProjectStatus } from './tsServer/projectStatus'; import { TsServerProcessFactory } from './tsServer/server'; import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; -import VersionStatus from './tsServer/versionStatus'; +import { VersionStatus } from './tsServer/versionStatus'; import TypeScriptServiceClient from './typescriptServiceClient'; import { ActiveJsTsEditorTracker } from './utils/activeJsTsEditorTracker'; import { coalesce, flatten } from './utils/arrays'; @@ -27,7 +28,7 @@ import { ServiceConfigurationProvider } from './utils/configuration'; import { Disposable } from './utils/dispose'; import * as errorCodes from './utils/errorCodes'; import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription'; -import * as ProjectStatus from './utils/largeProjectStatus'; +import * as LargeProjectStatus from './utils/largeProjectStatus'; import { LogLevelMonitor } from './utils/logLevelMonitor'; import { PluginManager } from './utils/plugins'; import * as typeConverters from './utils/typeConverters'; @@ -92,10 +93,11 @@ export default class TypeScriptServiceClientHost extends Disposable { this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables); this.client.onResendModelsRequested(() => this.populateService(), null, this._disposables); - this._register(new VersionStatus(this.client, services.commandManager, services.activeJsTsEditorTracker)); + this._register(new VersionStatus(this.client)); + this._register(new ProjectStatus(this.client, services.commandManager, services.activeJsTsEditorTracker)); this._register(new AtaProgressReporter(this.client)); this.typingsStatus = this._register(new TypingsStatus(this.client)); - this._register(ProjectStatus.create(this.client)); + this._register(LargeProjectStatus.create(this.client)); this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client, onCaseInsenitiveFileSystem));