diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 7469fd570..bc50df0ce 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -16,7 +16,6 @@ nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFo const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export class CopilotHoverProvider implements vscode.HoverProvider { - private client: DefaultClient; private currentDocument: vscode.TextDocument | undefined; private currentPosition: vscode.Position | undefined; private currentCancellationToken: vscode.CancellationToken | undefined; @@ -30,8 +29,8 @@ export class CopilotHoverProvider implements vscode.HoverProvider { private chatModelId: string | undefined; // Save the selected model ID to avoid trying the same unavailable model repeatedly. // Flag to avoid querying the LanguageModelChat repeatedly if no model is found private checkedChatModel: boolean = false; - constructor(client: DefaultClient) { - this.client = client; + + constructor(private client: DefaultClient) { } public async getCachedChatModel(): Promise { @@ -171,7 +170,6 @@ export class CopilotHoverProvider implements vscode.HoverProvider { position: Position.create(position.line, position.character) }; - await this.client.ready; if (this.currentCancellationToken?.isCancellationRequested) { throw new vscode.CancellationError(); } diff --git a/Extension/src/LanguageServer/Providers/HoverProvider.ts b/Extension/src/LanguageServer/Providers/HoverProvider.ts index 4bc8bae19..91b5a1706 100644 --- a/Extension/src/LanguageServer/Providers/HoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/HoverProvider.ts @@ -10,11 +10,10 @@ import { RequestCancelled, ServerCancelled } from '../protocolFilter'; import { CppSettings } from '../settings'; export class HoverProvider implements vscode.HoverProvider { - private client: DefaultClient; private lastContent: vscode.MarkdownString[] | undefined; private readonly hasContent = new ManualSignal(true); - constructor(client: DefaultClient) { - this.client = client; + + constructor(private client: DefaultClient) { } public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { @@ -36,7 +35,6 @@ export class HoverProvider implements vscode.HoverProvider { textDocument: { uri: document.uri.toString() }, position: Position.create(position.line, position.character) }; - await this.client.ready; let hoverResult: vscode.Hover; try { hoverResult = await this.client.languageClient.sendRequest(HoverRequest, params, token); diff --git a/Extension/src/LanguageServer/Providers/callHierarchyProvider.ts b/Extension/src/LanguageServer/Providers/callHierarchyProvider.ts index 61b7b110b..93ae4a574 100644 --- a/Extension/src/LanguageServer/Providers/callHierarchyProvider.ts +++ b/Extension/src/LanguageServer/Providers/callHierarchyProvider.ts @@ -214,15 +214,11 @@ export async function sendCallHierarchyCallsFromRequest(client: DefaultClient, i export class CallHierarchyProvider implements vscode.CallHierarchyProvider { // Indicates whether a request is from an entry root node (e.g. top function in the call tree). private isEntryRootNodeTelemetry: boolean = false; - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async prepareCallHierarchy(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - await this.client.ready; - workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest); workspaceReferences.clearViews(); @@ -261,7 +257,6 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider { } public async provideCallHierarchyIncomingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise { - await this.client.ready; workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest); const CallHierarchyCallsToEvent: string = "CallHierarchyCallsTo"; @@ -316,8 +311,6 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider { return undefined; } - await this.client.ready; - const result: vscode.CallHierarchyOutgoingCall[] | undefined = await sendCallHierarchyCallsFromRequest(this.client, item, token); if (token.isCancellationRequested || result === undefined) { diff --git a/Extension/src/LanguageServer/Providers/codeActionProvider.ts b/Extension/src/LanguageServer/Providers/codeActionProvider.ts index 613a16d95..858215cb1 100644 --- a/Extension/src/LanguageServer/Providers/codeActionProvider.ts +++ b/Extension/src/LanguageServer/Providers/codeActionProvider.ts @@ -40,9 +40,7 @@ export const GetCodeActionsRequest: RequestType('cpptools/getCodeActions'); export class CodeActionProvider implements vscode.CodeActionProvider { - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } private static inlineMacroKind: vscode.CodeActionKind = vscode.CodeActionKind.RefactorInline.append("macro"); @@ -51,7 +49,6 @@ export class CodeActionProvider implements vscode.CodeActionProvider { public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise<(vscode.Command | vscode.CodeAction)[]> { - await this.client.ready; let r: Range; if (range instanceof vscode.Selection) { if (range.active.isBefore(range.anchor)) { diff --git a/Extension/src/LanguageServer/Providers/documentFormattingEditProvider.ts b/Extension/src/LanguageServer/Providers/documentFormattingEditProvider.ts index cc16590c9..d683f9339 100644 --- a/Extension/src/LanguageServer/Providers/documentFormattingEditProvider.ts +++ b/Extension/src/LanguageServer/Providers/documentFormattingEditProvider.ts @@ -11,9 +11,7 @@ import { CppSettings, OtherSettings } from '../settings'; import { makeVscodeTextEdits } from '../utils'; export class DocumentFormattingEditProvider implements vscode.DocumentFormattingEditProvider { - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise { @@ -21,7 +19,6 @@ export class DocumentFormattingEditProvider implements vscode.DocumentFormatting if (settings.formattingEngine === "disabled") { return []; } - await this.client.ready; const filePath: string = document.uri.fsPath; if (options.onChanges) { let insertSpacesSet: boolean = false; diff --git a/Extension/src/LanguageServer/Providers/documentRangeFormattingEditProvider.ts b/Extension/src/LanguageServer/Providers/documentRangeFormattingEditProvider.ts index 2fcc5763b..eec2158b4 100644 --- a/Extension/src/LanguageServer/Providers/documentRangeFormattingEditProvider.ts +++ b/Extension/src/LanguageServer/Providers/documentRangeFormattingEditProvider.ts @@ -11,9 +11,7 @@ import { CppSettings } from '../settings'; import { makeVscodeTextEdits } from '../utils'; export class DocumentRangeFormattingEditProvider implements vscode.DocumentRangeFormattingEditProvider { - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, @@ -22,7 +20,6 @@ export class DocumentRangeFormattingEditProvider implements vscode.DocumentRange if (settings.formattingEngine === "disabled") { return []; } - await this.client.ready; const filePath: string = document.uri.fsPath; const useVcFormat: boolean = settings.useVcFormat(document); const configCallBack = async (editorConfigSettings: any | undefined) => { diff --git a/Extension/src/LanguageServer/Providers/documentSymbolProvider.ts b/Extension/src/LanguageServer/Providers/documentSymbolProvider.ts index 4d4c3019d..a4d1ef154 100644 --- a/Extension/src/LanguageServer/Providers/documentSymbolProvider.ts +++ b/Extension/src/LanguageServer/Providers/documentSymbolProvider.ts @@ -58,14 +58,12 @@ export class DocumentSymbolProvider implements vscode.DocumentSymbolProvider { public async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { const client: Client = clients.getClientFor(document.uri); if (client instanceof DefaultClient) { - const defaultClient: DefaultClient = client; - await client.ready; const params: GetDocumentSymbolRequestParams = { uri: document.uri.toString() }; let response: GetDocumentSymbolResult; try { - response = await defaultClient.languageClient.sendRequest(GetDocumentSymbolRequest, params, token); + response = await client.languageClient.sendRequest(GetDocumentSymbolRequest, params, token); } catch (e: any) { if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { throw new vscode.CancellationError(); diff --git a/Extension/src/LanguageServer/Providers/findAllReferencesProvider.ts b/Extension/src/LanguageServer/Providers/findAllReferencesProvider.ts index 664cf80c6..664779bfa 100644 --- a/Extension/src/LanguageServer/Providers/findAllReferencesProvider.ts +++ b/Extension/src/LanguageServer/Providers/findAllReferencesProvider.ts @@ -56,14 +56,10 @@ export async function sendFindAllReferencesRequest(client: DefaultClient, uri: v } export class FindAllReferencesProvider implements vscode.ReferenceProvider { - private client: DefaultClient; - - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise { - await this.client.ready; workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest); // Listen to a cancellation for this request. When this request is cancelled, diff --git a/Extension/src/LanguageServer/Providers/foldingRangeProvider.ts b/Extension/src/LanguageServer/Providers/foldingRangeProvider.ts index c7065122a..ce97b35d9 100644 --- a/Extension/src/LanguageServer/Providers/foldingRangeProvider.ts +++ b/Extension/src/LanguageServer/Providers/foldingRangeProvider.ts @@ -14,7 +14,6 @@ interface FoldingRangeRequestInfo { } export class FoldingRangeProvider implements vscode.FoldingRangeProvider { - private client: DefaultClient; public onDidChangeFoldingRangesEvent = new vscode.EventEmitter(); public onDidChangeFoldingRanges?: vscode.Event; @@ -22,12 +21,11 @@ export class FoldingRangeProvider implements vscode.FoldingRangeProvider { // for the same file without waiting for the prior request to complete or cancelling them. private pendingRequests: Map = new Map(); - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { this.onDidChangeFoldingRanges = this.onDidChangeFoldingRangesEvent.event; } + async provideFoldingRanges(document: vscode.TextDocument, context: vscode.FoldingContext, token: vscode.CancellationToken): Promise { - await this.client.ready; const settings: CppSettings = new CppSettings(); if (!settings.codeFolding) { return []; diff --git a/Extension/src/LanguageServer/Providers/onTypeFormattingEditProvider.ts b/Extension/src/LanguageServer/Providers/onTypeFormattingEditProvider.ts index 085becf30..7190f5b95 100644 --- a/Extension/src/LanguageServer/Providers/onTypeFormattingEditProvider.ts +++ b/Extension/src/LanguageServer/Providers/onTypeFormattingEditProvider.ts @@ -11,9 +11,7 @@ import { CppSettings } from '../settings'; import { makeVscodeTextEdits } from '../utils'; export class OnTypeFormattingEditProvider implements vscode.OnTypeFormattingEditProvider { - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideOnTypeFormattingEdits(document: vscode.TextDocument, position: vscode.Position, ch: string, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise { @@ -21,7 +19,6 @@ export class OnTypeFormattingEditProvider implements vscode.OnTypeFormattingEdit if (settings.formattingEngine === "disabled") { return []; } - await this.client.ready; const filePath: string = document.uri.fsPath; const useVcFormat: boolean = settings.useVcFormat(document); const configCallBack = async (editorConfigSettings: any | undefined) => { diff --git a/Extension/src/LanguageServer/Providers/renameProvider.ts b/Extension/src/LanguageServer/Providers/renameProvider.ts index ea93ba1c2..a15bbdb72 100644 --- a/Extension/src/LanguageServer/Providers/renameProvider.ts +++ b/Extension/src/LanguageServer/Providers/renameProvider.ts @@ -18,14 +18,10 @@ const RenameRequest: RequestType = new RequestType('cpptools/rename'); export class RenameProvider implements vscode.RenameProvider { - private client: DefaultClient; - - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, _token: vscode.CancellationToken): Promise { - await this.client.ready; workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest); const settings: CppSettings = new CppSettings(); diff --git a/Extension/src/LanguageServer/Providers/semanticTokensProvider.ts b/Extension/src/LanguageServer/Providers/semanticTokensProvider.ts index 1a04f9017..2bdbf1634 100644 --- a/Extension/src/LanguageServer/Providers/semanticTokensProvider.ts +++ b/Extension/src/LanguageServer/Providers/semanticTokensProvider.ts @@ -5,8 +5,7 @@ import * as vscode from 'vscode'; import { ManualPromise } from '../../Utility/Async/manualPromise'; -interface FileData -{ +interface FileData { version: number; promise: ManualPromise; tokenBuilder: vscode.SemanticTokensBuilder; diff --git a/Extension/src/LanguageServer/Providers/workspaceSymbolProvider.ts b/Extension/src/LanguageServer/Providers/workspaceSymbolProvider.ts index 92a53225f..306bf01f0 100644 --- a/Extension/src/LanguageServer/Providers/workspaceSymbolProvider.ts +++ b/Extension/src/LanguageServer/Providers/workspaceSymbolProvider.ts @@ -10,9 +10,7 @@ import { RequestCancelled, ServerCancelled } from '../protocolFilter'; import { makeVscodeLocation } from '../utils'; export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { - private client: DefaultClient; - constructor(client: DefaultClient) { - this.client = client; + constructor(private client: DefaultClient) { } public async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise { diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 133890cfa..d3a51aa48 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -23,19 +23,16 @@ import { WorkspaceSymbolProvider } from './Providers/workspaceSymbolProvider'; // End provider imports import { CodeSnippet, Trait } from '@github/copilot-language-server'; -import { ok } from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import { SourceFileConfiguration, SourceFileConfigurationItem, Version, WorkspaceBrowseConfiguration } from 'vscode-cpptools'; import { IntelliSenseStatus, Status } from 'vscode-cpptools/out/testApi'; -import { CloseAction, DidOpenTextDocumentParams, ErrorAction, LanguageClientOptions, NotificationType, Position, Range, RequestType, ResponseError, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; -import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; +import { CloseAction, DidOpenTextDocumentParams, ErrorAction, NotificationType, Position, Range, RequestType, ResponseError, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; +import * as rpc from 'vscode-languageclient/node'; import * as nls from 'vscode-nls'; import { DebugConfigurationProvider } from '../Debugger/configurationProvider'; import { ManualPromise } from '../Utility/Async/manualPromise'; -import { ManualSignal } from '../Utility/Async/manualSignal'; import { logAndReturn } from '../Utility/Async/returns'; -import { is } from '../Utility/System/guards'; import * as util from '../common'; import { isWindows } from '../constants'; import { instrument, isInstrumentationEnabled } from '../instrumentation'; @@ -60,6 +57,7 @@ import { CustomConfigurationProvider1, getCustomConfigProviders, isSameProviderE import { DataBinding } from './dataBinding'; import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; import { CppSourceStr, clients, configPrefix, initializeIntervalTimer, isWritingCrashCallStack, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; +import { LanguageClient } from './languageClient'; import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; import { PersistentFolderState, PersistentState, PersistentWorkspaceState } from './persistentState'; import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; @@ -88,7 +86,7 @@ export function hasTrustedCompilerPaths(): boolean { } // Data shared by all clients. -let languageClient: LanguageClient; +const languageClient: LanguageClient = new LanguageClient(); let firstClientStarted: Promise<{ wasShutdown: boolean }>; let languageClientHasCrashed: boolean = false; let languageClientCrashedNeedsRestart: boolean = false; @@ -781,7 +779,6 @@ class ClientModel { export interface Client { readonly ready: Promise; - enqueue(task: () => Promise): Promise; InitializingWorkspaceChanged: vscode.Event; IndexingWorkspaceChanged: vscode.Event; ParsingWorkspaceChanged: vscode.Event; @@ -893,7 +890,6 @@ export function createNullClient(): Client { } export class DefaultClient implements Client { - private innerLanguageClient?: LanguageClient; // The "client" that launches and communicates with our language "server" process. private disposables: vscode.Disposable[] = []; private documentFormattingProviderDisposable: vscode.Disposable | undefined; private formattingRangeProviderDisposable: vscode.Disposable | undefined; @@ -909,7 +905,6 @@ export class DefaultClient implements Client { private rootRealPath: string; private workspaceStoragePath: string; private trackedDocuments = new Map(); - private isSupported: boolean = true; private inactiveRegionsDecorations = new Map(); private settingsTracker: SettingsTracker; private loggingLevel: number = 1; @@ -934,20 +929,6 @@ export class DefaultClient implements Client { private showConfigureIntelliSenseButton: boolean = false; private pendingTagParsingCalls: PendingTagParsingCall[] = []; - /** A queue of asynchronous tasks that need to be processed befofe ready is considered active. */ - private static queue = new Array<[ManualPromise, () => Promise] | [ManualPromise]>(); - - /** returns a promise that waits initialization and/or a change to configuration to complete (i.e. language client is ready-to-use) */ - private static readonly isStarted = new ManualSignal(true); - - /** - * Indicates if the blocking task dispatcher is currently running - * - * This will be in the Set state when the dispatcher is not running (i.e. if you await this it will be resolved immediately) - * If the dispatcher is running, this will be in the Reset state (i.e. if you await this it will be resolved when the dispatcher is done) - */ - private static readonly dispatching = new ManualSignal(); - // The "model" that is displayed via the UI (status bar). private model: ClientModel = new ClientModel(); @@ -964,7 +945,7 @@ export class DefaultClient implements Client { public get ReferencesCommandModeChanged(): vscode.Event { return this.model.referencesCommandMode.ValueChanged; } public get TagParserStatusChanged(): vscode.Event { return this.model.parsingWorkspaceStatus.ValueChanged; } public get ActiveConfigChanged(): vscode.Event { return this.model.activeConfigName.ValueChanged; } - public isInitialized(): boolean { return this.innerLanguageClient !== undefined; } + public isInitialized(): boolean { return this.languageClient.isInitialized; } public getShowConfigureIntelliSenseButton(): boolean { return this.showConfigureIntelliSenseButton; } public setShowConfigureIntelliSenseButton(show: boolean): void { this.showConfigureIntelliSenseButton = show; } @@ -999,10 +980,7 @@ export class DefaultClient implements Client { } public get languageClient(): LanguageClient { - if (!this.innerLanguageClient) { - throw new Error("Attempting to use languageClient before initialized"); - } - return this.innerLanguageClient; + return languageClient; } public get configuration(): configs.CppProperties { @@ -1376,19 +1354,18 @@ export class DefaultClient implements Client { if (firstClientStarted === undefined || languageClientCrashedNeedsRestart) { if (languageClientCrashedNeedsRestart) { languageClientCrashedNeedsRestart = false; - // if we're recovering, the isStarted needs to be reset. + // if we're recovering, the isStarted needs to be reset // because we're starting the first client again. - DefaultClient.isStarted.reset(); + this.languageClient.isStarted = false; + this.languageClient.setLanguageClient(undefined); } firstClientStarted = this.createLanguageClient(); util.setProgress(util.getProgressExecutableStarted()); isFirstClient = true; } void this.init(rootUri, isFirstClient).catch(logAndReturn.undefined); - } catch (errJS) { const err: NodeJS.ErrnoException = errJS as NodeJS.ErrnoException; - this.isSupported = false; // Running on an OS we don't support yet. if (!failureMessageShown) { failureMessageShown = true; let additionalInfo: string; @@ -1408,8 +1385,7 @@ export class DefaultClient implements Client { ui = getUI(); ui.bind(this); if ((await firstClientStarted).wasShutdown) { - this.isSupported = false; - DefaultClient.isStarted.resolve(); + this.languageClient.isStarted = true; return; } @@ -1421,7 +1397,10 @@ export class DefaultClient implements Client { this.innerConfiguration.CompileCommandsChanged((e) => this.onCompileCommandsChanged(e)); this.disposables.push(this.innerConfiguration); - this.innerLanguageClient = languageClient; + // This could be set earlier, but the task provider expects it to also mean that `this.innerConfiguration` is set. + this.languageClient.isStarted = true; + compilerDefaults = await this.requestCompiler(); + telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", this.settingsTracker.getUserModifiedSettings()); failureMessageShown = false; @@ -1488,7 +1467,6 @@ export class DefaultClient implements Client { }); // The configurations will not be sent to the language server until the default include paths and frameworks have been set. // The event handlers must be set before this happens. - compilerDefaults = await this.requestCompiler(); DefaultClient.updateClientConfigurations(); clients.forEach(client => { if (client instanceof DefaultClient) { @@ -1498,14 +1476,11 @@ export class DefaultClient implements Client { }); } } catch (err) { - this.isSupported = false; // Running on an OS we don't support yet. if (!failureMessageShown) { failureMessageShown = true; void vscode.window.showErrorMessage(localize("unable.to.start", "Unable to start the C/C++ language server. IntelliSense features will be disabled. Error: {0}", String(err))); } } - - DefaultClient.isStarted.resolve(); } private getWorkspaceFolderSettings(workspaceFolderUri: vscode.Uri | undefined, workspaceFolder: vscode.WorkspaceFolder | undefined, settings: CppSettings, otherSettings: OtherSettings): WorkspaceFolderSettingsParams { @@ -1693,7 +1668,7 @@ export class DefaultClient implements Client { // from a sanitizer build to files in that directory (see getSanitizerServerEnv). This is // undefined -- i.e. the environment is inherited unchanged -- for normal builds. const sanitizerServerEnv: NodeJS.ProcessEnv | undefined = getSanitizerServerEnv(); - const serverOptions: ServerOptions = { + const serverOptions: rpc.ServerOptions = { run: { command: serverModule, options: { detached: false, cwd: util.getExtensionFilePath("bin"), env: sanitizerServerEnv } }, debug: { command: serverModule, args: [serverName], options: { detached: true, cwd: util.getExtensionFilePath("bin"), env: sanitizerServerEnv } } }; @@ -1756,7 +1731,7 @@ export class DefaultClient implements Client { loggingLevel: this.loggingLevel }; - const clientOptions: LanguageClientOptions = { + const clientOptions: rpc.LanguageClientOptions = { documentSelector: [ { scheme: 'file', language: 'c' }, { scheme: 'file', language: 'cpp' }, @@ -1843,34 +1818,40 @@ export class DefaultClient implements Client { // Refresh initializing state in UI. this.model.isInitializingWorkspace.Value = true; - // Create the language client - languageClient = new LanguageClient(`cpptools`, serverOptions, clientOptions); - languageClient.onNotification(DebugProtocolNotification, logDebugProtocol); - languageClient.onNotification(DebugLogNotification, logLocalized); - languageClient.onNotification(LogTelemetryNotification, (e) => void this.logTelemetry(e)); - languageClient.onNotification(ShowMessageWindowNotification, showMessageWindow); - languageClient.registerProposedFeatures(); - await languageClient.start(); + // Create the language client that spawns cpptools and configures RPC over stdin/stdout. + // This is the ONLY place outside of the LanguageClient wrapper where the VS Code LanguageClient class should be used directly. + // All other code should use the LanguageClient wrapper class. + const client = new rpc.LanguageClient(`cpptools`, serverOptions, clientOptions); + client.onNotification(DebugProtocolNotification, logDebugProtocol); + client.onNotification(DebugLogNotification, logLocalized); + client.onNotification(LogTelemetryNotification, (e) => void this.logTelemetry(e)); + client.onNotification(ShowMessageWindowNotification, showMessageWindow); + client.registerProposedFeatures(); + await client.start(); if (usesCrashHandler()) { - watchForCrashes(await languageClient.sendRequest(PreInitializationRequest, null)); + watchForCrashes(await client.sendRequest(PreInitializationRequest, null)); } else if (os.platform() === "win32") { const settings: CppSettings = new CppSettings(); if ((settings.windowsErrorReportingMode === "default" && !languageClientHasCrashed) || settings.windowsErrorReportingMode === "enabled") { - await languageClient.sendRequest(PreInitializationRequest, null); + await client.sendRequest(PreInitializationRequest, null); } } // Move initialization to a separate message, so we can see log output from it. // A request is used in order to wait for completion and ensure that no subsequent // higher priority message may be processed before the Initialization request. - const initializeResult = await languageClient.sendRequest(InitializationRequest, cppInitializationParams); + const initializeResult = await client.sendRequest(InitializationRequest, cppInitializationParams); // If the server requested shutdown, then reload with the failsafe (null) client. if (initializeResult.shouldShutdown) { - await languageClient.stop(); + await client.stop(); await clients.recreateClients(true); + } else { + // Don't set the inner language client on the wrapper until after initialization is complete. + // This ensures the order of the initialization messages. + languageClient.setLanguageClient(client); } return { wasShutdown: initializeResult.shouldShutdown }; @@ -1878,7 +1859,6 @@ export class DefaultClient implements Client { public async sendDidChangeSettings(): Promise { // Send settings json to native side. - await this.ready; await this.languageClient.sendNotification(DidChangeSettingsNotification, this.getAllSettings()); } @@ -2218,7 +2198,6 @@ export class DefaultClient implements Client { } public async logDiagnostics(): Promise { - await this.ready; const response: GetDiagnosticsResult = await this.languageClient.sendRequest(GetDiagnosticsRequest, null); const diagnosticsChannel: vscode.OutputChannel = getDiagnosticsChannel(); diagnosticsChannel.clear(); @@ -2286,16 +2265,12 @@ export class DefaultClient implements Client { diagnosticsChannel.show(false); } - public async rescanFolder(): Promise { - await this.ready; + public rescanFolder(): Promise { return this.languageClient.sendNotification(RescanFolderNotification); } public async provideCustomConfigurations(docUris: vscode.Uri[], batchId: number): Promise { let isProviderRegistered: boolean = false; - const onFinished: () => void = () => { - void this.languageClient.sendNotification(FinishedRequestCustomConfig, { batchId, isProviderRegistered }); - }; try { const providerId: string | undefined = this.configurationProvider; if (!providerId) { @@ -2309,7 +2284,7 @@ export class DefaultClient implements Client { const resultCode = await this.provideCustomConfigurationAsync(docUris, provider); telemetry.logLanguageServerEvent('provideCustomConfigurations', { providerId, resultCode }); } finally { - onFinished(); + void this.languageClient.sendNotification(FinishedRequestCustomConfig, { batchId, isProviderRegistered }); } } @@ -2501,9 +2476,8 @@ export class DefaultClient implements Client { * the UI results and always re-requests (no caching). */ - public async getIncludes(uri: vscode.Uri, maxDepth: number): Promise { + public getIncludes(uri: vscode.Uri, maxDepth: number): Promise { const params: GetIncludesParams = { fileUri: uri.toString(), maxDepth }; - await this.ready; return this.languageClient.sendRequest(IncludesRequest, params); } @@ -2524,82 +2498,11 @@ export class DefaultClient implements Client { } /** - * a Promise that can be awaited to know when it's ok to proceed. - * - * This is a lighter-weight complement to `enqueue()` - * - * Use `await .ready` when you need to ensure that the client is initialized, and to run in order - * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. - * - * This is lightweight, because if the queue is empty, then the only thing to wait for is the client itself to be initialized + * A Promise that can be awaited to know when the language client (cpptools) is up and running. + * It also implies that `this.innerConfiguration` is set. */ get ready(): Promise { - if (!DefaultClient.dispatching.isCompleted || DefaultClient.queue.length) { - // if the dispatcher has stuff going on, then we need to stick in a promise into the queue so we can - // be notified when it's our turn - const p = new ManualPromise(); - DefaultClient.queue.push([p as ManualPromise]); - return p; - } - - // otherwise, we're only waiting for the client to be in an initialized state, in which case just wait for that. - return DefaultClient.isStarted; - } - - /** - * Enqueue a task to ensure that the order is maintained. The tasks are executed sequentially after the client is ready. - * - * this is a bit more expensive than `.ready` - this ensures the task is absolutely finished executing before allowing - * the dispatcher to move forward. - * - * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. - * Use `await .ready` when you need to ensure that the client is initialized, and still run in order. - */ - enqueue(task: () => Promise) { - ok(this.isSupported, localize("unsupported.client", "Unsupported client")); - - // create a placeholder promise that is resolved when the task is complete. - const result = new ManualPromise(); - - // add the task to the queue - DefaultClient.queue.push([result, task]); - - // if we're not already dispatching, start - if (DefaultClient.dispatching.isSet) { - // start dispatching - void DefaultClient.dispatch(); - } - - // return the placeholder promise to the caller. - return result as Promise; - } - - /** - * The dispatch loop asynchronously processes items in the async queue in order, and ensures that tasks are dispatched in the - * order they were inserted. - */ - private static async dispatch() { - // reset the promise for the dispatcher - DefaultClient.dispatching.reset(); - - do { - // ensure that this is OK to start working - await this.isStarted; - - // pick items up off the queue and run then one at a time until the queue is empty - const [promise, task] = DefaultClient.queue.shift() ?? []; - if (is.promise(promise)) { - try { - promise.resolve(task ? await task() : undefined); - } catch (e) { - console.log(e); - promise.reject(e); - } - } - } while (DefaultClient.queue.length); - - // unblock anything that is waiting for the dispatcher to empty - this.dispatching.resolve(); + return this.languageClient.ready; } private static async withLspCancellationHandling(task: () => Promise, token: vscode.CancellationToken): Promise { @@ -2651,7 +2554,7 @@ export class DefaultClient implements Client { * listen for notifications from the language server. */ private registerNotifications(): void { - console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); + console.assert(this.languageClient.isInitialized, "This method must not be called until the language client is initialized."); this.languageClient.onNotification(ReloadWindowNotification, () => void util.promptForReloadWindowDueToSettingsChange()); this.languageClient.onNotification(UpdateTrustedCompilersNotification, (e) => void this.addTrustedCompiler(e.compilerPath)); @@ -2760,7 +2663,7 @@ export class DefaultClient implements Client { * listen for file created/deleted events under the ${workspaceFolder} folder */ private registerFileWatcher(): void { - console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); + console.assert(this.languageClient.isInitialized, "This method must not be called until the language client is initialized."); if (this.rootFolder) { // WARNING: The default limit on Linux is 8k, so for big directories, this can cause file watching to fail. @@ -3121,18 +3024,16 @@ export class DefaultClient implements Client { switchHeaderSourceFileName: fileName, workspaceFolderUri: rootUri.toString() }; - return this.enqueue(async () => { - // Don't use withLspCancellationHandling() or withCancellation() here. If the switch target is already known, - // the caller should still be able to use it even if the progress notification was just cancelled. - try { - return await this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token); - } catch (e: any) { - if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { - throw new vscode.CancellationError(); - } - throw e; + // Don't use withLspCancellationHandling() or withCancellation() here. If the switch target is already known, + // the caller should still be able to use it even if the progress notification was just cancelled. + try { + return await this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token); + } catch (e: any) { + if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { + throw new vscode.CancellationError(); } - }); + throw e; + } } public async requestCompiler(newCompilerPath?: string): Promise { @@ -3212,7 +3113,6 @@ export class DefaultClient implements Client { * send notifications to the language server to restart IntelliSense for the selected file. */ public async restartIntelliSenseForFile(document: vscode.TextDocument): Promise { - await this.ready; return this.languageClient.sendNotification(RestartIntelliSenseForFileNotification, this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document)).catch(logAndReturn.undefined); } @@ -3229,7 +3129,6 @@ export class DefaultClient implements Client { } public async resetDatabase(): Promise { - await this.ready; return this.languageClient.sendNotification(ResetDatabaseNotification); } @@ -3241,29 +3140,24 @@ export class DefaultClient implements Client { } public async pauseParsing(): Promise { - await this.ready; return this.languageClient.sendNotification(PauseParsingNotification); } public async resumeParsing(): Promise { - await this.ready; return this.languageClient.sendNotification(ResumeParsingNotification); } public async PauseCodeAnalysis(): Promise { - await this.ready; this.model.isCodeAnalysisPaused.Value = true; return this.languageClient.sendNotification(PauseCodeAnalysisNotification); } public async ResumeCodeAnalysis(): Promise { - await this.ready; this.model.isCodeAnalysisPaused.Value = false; return this.languageClient.sendNotification(ResumeCodeAnalysisNotification); } public async CancelCodeAnalysis(): Promise { - await this.ready; return this.languageClient.sendNotification(CancelCodeAnalysisNotification); } @@ -3398,7 +3292,6 @@ export class DefaultClient implements Client { currentConfiguration: index, workspaceFolderUri: this.RootUri?.toString() }; - await this.ready; await this.languageClient.sendNotification(ChangeSelectedSettingNotification, params); let configName: string = ""; @@ -3414,7 +3307,6 @@ export class DefaultClient implements Client { uri: vscode.Uri.file(path).toString(), workspaceFolderUri: this.RootUri?.toString() }; - await this.ready; return this.languageClient.sendNotification(ChangeCompileCommandsNotification, params); } @@ -3603,7 +3495,6 @@ export class DefaultClient implements Client { const params: WorkspaceFolderParams = { workspaceFolderUri: this.RootUri?.toString() }; - await this.ready; return this.languageClient.sendNotification(ClearCustomConfigurationsNotification, params); } @@ -3612,7 +3503,6 @@ export class DefaultClient implements Client { const params: WorkspaceFolderParams = { workspaceFolderUri: this.RootUri?.toString() }; - await this.ready; return this.languageClient.sendNotification(ClearCustomBrowseConfigurationNotification, params); } @@ -3699,7 +3589,6 @@ export class DefaultClient implements Client { position: editor.selection.active, next: next }; - await this.ready; const response: Position | undefined = await this.languageClient.sendRequest(GoToDirectiveInGroupRequest, params); if (response) { const p: vscode.Position = new vscode.Position(response.line, response.character); @@ -3732,7 +3621,6 @@ export class DefaultClient implements Client { isCodeAction: codeActionArguments !== undefined, isCursorAboveSignatureLine: codeActionArguments?.isCursorAboveSignatureLine }; - await this.ready; const currentFileVersion: number | undefined = openFileVersions.get(params.uri); if (currentFileVersion === undefined) { return; @@ -3778,23 +3666,19 @@ export class DefaultClient implements Client { } } - public async handleRunCodeAnalysisOnActiveFile(): Promise { - await this.ready; + public handleRunCodeAnalysisOnActiveFile(): Promise { return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ActiveFile }); } - public async handleRunCodeAnalysisOnOpenFiles(): Promise { - await this.ready; + public handleRunCodeAnalysisOnOpenFiles(): Promise { return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.OpenFiles }); } - public async handleRunCodeAnalysisOnAllFiles(): Promise { - await this.ready; + public handleRunCodeAnalysisOnAllFiles(): Promise { return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.AllFiles }); } public async handleRemoveAllCodeAnalysisProblems(): Promise { - await this.ready; if (removeAllCodeAnalysisProblems()) { return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ClearSquiggles }); } @@ -3825,8 +3709,6 @@ export class DefaultClient implements Client { } public async handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - await this.ready; - // A deep copy is needed because the call to identifiers.splice below can // remove elements in identifiersAndUris[...].identifiers. const identifiersAndUrisCopy: CodeAnalysisDiagnosticIdentifiersAndUri[] = []; @@ -4308,7 +4190,7 @@ export class DefaultClient implements Client { public onInterval(): void { // These events can be discarded until the language client is ready. // Don't queue them up with this.notifyWhenLanguageClientReady calls. - if (this.innerLanguageClient !== undefined && this.configuration !== undefined) { + if (this.languageClient.isInitialized && this.configuration !== undefined) { void this.languageClient.sendNotification(IntervalTimerNotification).catch(logAndReturn.undefined); this.configuration.checkCppProperties(); this.configuration.checkCompileCommands(); @@ -4354,8 +4236,6 @@ export class DefaultClient implements Client { } public async handleReferencesIcon(): Promise { - await this.ready; - workspaceReferences.UpdateProgressUICounter(this.model.referencesCommandMode.Value); // If the search is find all references, preview partial results. @@ -4489,9 +4369,6 @@ class NullClient implements Client { readonly ready: Promise = Promise.resolve(); - async enqueue(task: () => Promise) { - return task(); - } public get InitializingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } public get IndexingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } public get ParsingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } diff --git a/Extension/src/LanguageServer/clientCollection.ts b/Extension/src/LanguageServer/clientCollection.ts index 54528a827..390afc2ff 100644 --- a/Extension/src/LanguageServer/clientCollection.ts +++ b/Extension/src/LanguageServer/clientCollection.ts @@ -121,7 +121,6 @@ export class ClientCollection { const client: cpptools.Client = pair[1]; const newClient: cpptools.Client = this.createClient(client.RootFolder, true); - await newClient.ready; for (const document of client.TrackedDocuments.values()) { this.transferOwnership(document, client); await newClient.sendDidOpen(document); diff --git a/Extension/src/LanguageServer/codeAnalysis.ts b/Extension/src/LanguageServer/codeAnalysis.ts index 3e9bcec3e..8a71a3d00 100644 --- a/Extension/src/LanguageServer/codeAnalysis.ts +++ b/Extension/src/LanguageServer/codeAnalysis.ts @@ -4,10 +4,11 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; import * as vscode from 'vscode'; -import { LanguageClient, NotificationType, Range } from 'vscode-languageclient/node'; +import { NotificationType, Range } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import { Location, WorkspaceEdit } from './commonTypes'; import { CppSourceStr } from './extension'; +import { LanguageClient } from './languageClient'; import { LocalizeStringParams, getLocalizedString } from './localization'; import { CppSettings } from './settings'; import { makeVscodeLocation, makeVscodeRange, makeVscodeTextEdits, rangeEquals } from './utils'; diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 1f1d58477..0f45e1ba8 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -185,11 +185,11 @@ export async function activate(): Promise { disposables.push(vscode.workspace.onDidOpenTextDocument(onDidOpenTextDocument)); disposables.push(vscode.workspace.onDidChangeConfiguration(onDidChangeSettings)); - disposables.push(vscode.window.onDidChangeTextEditorVisibleRanges((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorVisibleRanges(e)))); - disposables.push(vscode.window.onDidChangeActiveTextEditor((e) => clients.ActiveClient.enqueue(async () => onDidChangeActiveTextEditor(e)))); + disposables.push(vscode.window.onDidChangeTextEditorVisibleRanges(e => onDidChangeTextEditorVisibleRanges(e))); + disposables.push(vscode.window.onDidChangeActiveTextEditor(e => onDidChangeActiveTextEditor(e))); ui.didChangeActiveEditor(); // Handle already active documents (for non-cpp files that we don't register didOpen). - disposables.push(vscode.window.onDidChangeTextEditorSelection((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorSelection(e)))); - disposables.push(vscode.window.onDidChangeVisibleTextEditors((e) => clients.ActiveClient.enqueue(async () => onDidChangeVisibleTextEditors(e)))); + disposables.push(vscode.window.onDidChangeTextEditorSelection(e => onDidChangeTextEditorSelection(e))); + disposables.push(vscode.window.onDidChangeVisibleTextEditors(e => onDidChangeVisibleTextEditors(e))); updateLanguageConfigurations(); reportMacCrashes(); @@ -563,12 +563,10 @@ async function selectClient(): Promise { } async function onResetDatabase(): Promise { - await clients.ActiveClient.ready; return clients.ActiveClient.resetDatabase(); } async function onRescanCompilers(sender?: any): Promise { - await clients.ActiveClient.ready; return clients.ActiveClient.rescanCompilers(sender); } @@ -577,7 +575,6 @@ async function onAddMissingInclude(): Promise { } async function selectIntelliSenseConfiguration(sender?: any): Promise { - await clients.ActiveClient.ready; return clients.ActiveClient.promptSelectIntelliSenseConfiguration(sender); } @@ -875,7 +872,6 @@ async function onFindAllReferences(uri: vscode.Uri, position: vscode.Position, t return undefined; } - await client.ready; const result = await sendFindAllReferencesRequest(client, uri, position, token ?? CancellationToken.None); return result?.locations; } @@ -890,7 +886,6 @@ async function onGoToDefinition(uri: vscode.Uri, position: vscode.Position, toke return undefined; } - await client.ready; return sendGoToDefinitionRequest(client, uri, position, token ?? CancellationToken.None); } @@ -904,7 +899,6 @@ async function onPrepareCallHierarchy(uri: vscode.Uri, position: vscode.Position return undefined; } - await client.ready; return sendPrepareCallHierarchyRequest(client, uri, position, token ?? CancellationToken.None); } @@ -918,7 +912,6 @@ async function onCallHierarchyCallsTo(item: vscode.CallHierarchyItem, token?: vs return undefined; } - await client.ready; return sendCallHierarchyCallsToRequest(client, item, token ?? CancellationToken.None); } @@ -932,7 +925,6 @@ async function onCallHierarchyCallsFrom(item: vscode.CallHierarchyItem, token?: return undefined; } - await client.ready; return sendCallHierarchyCallsFromRequest(client, item, token ?? CancellationToken.None); } diff --git a/Extension/src/LanguageServer/languageClient.ts b/Extension/src/LanguageServer/languageClient.ts new file mode 100644 index 000000000..9ac4db49e --- /dev/null +++ b/Extension/src/LanguageServer/languageClient.ts @@ -0,0 +1,90 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { Disposable } from 'vscode'; +import { CancellationToken, NotificationHandler, NotificationType, RequestType } from 'vscode-languageclient'; +import * as rpc from 'vscode-languageclient/node'; +import { ManualSignal } from '../Utility/Async/manualSignal'; + +export class LanguageClient { + private _rpcClient?: rpc.LanguageClient; + private readonly _started = new ManualSignal(true); + + /** + * Returns a promise that waits for the RPC connection to be ready. + */ + public get ready(): Promise { + return this._started; + } + + /** + * Set the initialization state of the underlying RPC client. + * This is used to indicate that the underlying RPC client has been initialized and is ready to send requests. + * If resetting the RPC client, set this to false. + */ + public set isStarted(value: boolean) { + if (value) { + this._started.resolve(); + } else { + this._started.reset(); + } + } + + /** + * Returns true if the underlying RPC client has been initialized. + * Used in cases where a quick check of the initialization state is desired instead of waiting on the `ready` promise. + */ + public get isInitialized(): boolean { + return !!this._rpcClient; + } + + /** + * Set the underlying RPC client. + * This should only be called once during initialization. + */ + public setLanguageClient(languageClient?: rpc.LanguageClient): void { + this._rpcClient = languageClient; + } + + /** + * Validate and return the underlying RPC client. + * Strips away the `undefined` type from the RPC client. + */ + private get rpcClient(): rpc.LanguageClient { + if (!this._rpcClient) { + throw new Error("Attempting to use languageClient before initialized"); + } + + return this._rpcClient; + } + + public get protocol2CodeConverter(): rpc.LanguageClient["protocol2CodeConverter"] { + return this.rpcClient.protocol2CodeConverter; + } + + public get code2ProtocolConverter(): rpc.LanguageClient["code2ProtocolConverter"] { + return this.rpcClient.code2ProtocolConverter; + } + + public async sendRequest(type: RequestType, params: P, token?: CancellationToken): Promise { + await this.ready; + return this.rpcClient.sendRequest(type, params, token); + } + + public async sendNotification

(type: NotificationType

, params?: P): Promise { + await this.ready; + return this.rpcClient.sendNotification(type, params); + } + + public onNotification

(type: NotificationType

, handler: NotificationHandler

): Disposable { + // These messages come from cpptools, and therefore it is not needed to await the ready signal. + return this.rpcClient.onNotification(type, handler); + } + + public stop(): Promise { + return this._rpcClient?.stop() ?? Promise.resolve(); + } +} diff --git a/Extension/src/LanguageServer/protocolFilter.ts b/Extension/src/LanguageServer/protocolFilter.ts index 5a95fc5a5..0ef586c9b 100644 --- a/Extension/src/LanguageServer/protocolFilter.ts +++ b/Extension/src/LanguageServer/protocolFilter.ts @@ -45,10 +45,8 @@ export function createProtocolFilter(): Middleware { // client.takeOwnership() will call client.TrackedDocuments.add() again, but that's ok. It's a Set. client.takeOwnership(document); void sendMessage(document); - client.ready.then(() => { - const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document)); - client.onDidChangeVisibleTextEditors(cppEditors).catch(logAndReturn.undefined); - }).catch(logAndReturn.undefined); + const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document)); + void client.onDidChangeVisibleTextEditors(cppEditors).catch(logAndReturn.undefined); } } },