diff --git a/src/lsptoolshost/projectContext/projectContextFeature.ts b/src/lsptoolshost/projectContext/projectContextFeature.ts new file mode 100644 index 000000000..523910b32 --- /dev/null +++ b/src/lsptoolshost/projectContext/projectContextFeature.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + languages as Languages, + workspace as Workspace, + DocumentSelector as VDocumentSelector, + TextDocument, +} from 'vscode'; + +import { DynamicFeature, FeatureState, LanguageClient, RegistrationData, ensure } from 'vscode-languageclient/node'; + +import { + ClientCapabilities, + DocumentSelector, + InitializeParams, + ProtocolNotificationType0, + RegistrationType, + ServerCapabilities, + TextDocumentRegistrationOptions, +} from 'vscode-languageserver-protocol'; + +import * as RoslynProtocol from '../server/roslynProtocol'; +import { randomUUID } from 'crypto'; + +export class ProjectContextFeature implements DynamicFeature { + private readonly _client: LanguageClient; + private readonly _registrations: Map>; + + constructor(client: LanguageClient) { + this._client = client; + this._registrations = new Map(); + this.registrationType = new ProtocolNotificationType0( + RoslynProtocol.ProjectContextRefreshNotification.method + ); + } + fillInitializeParams?: ((params: InitializeParams) => void) | undefined; + preInitialize?: + | ((capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined) => void) + | undefined; + registrationType: RegistrationType; + register(data: RegistrationData): void { + if (!data.registerOptions.documentSelector) { + return; + } + this._registrations.set(data.id, data); + } + unregister(id: string): void { + const registration = this._registrations.get(id); + if (registration !== undefined) { + this._registrations.delete(id); + } + } + clear(): void { + this._registrations.clear(); + } + + public getState(): FeatureState { + const selectors = this.getDocumentSelectors(); + + let count = 0; + for (const selector of selectors) { + count++; + for (const document of Workspace.textDocuments) { + if (Languages.match(selector, document) > 0) { + return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true }; + } + } + } + const registrations = count > 0; + return { kind: 'document', id: this.registrationType.method, registrations, matches: false }; + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + const workspaceCapabilities: any = ensure(capabilities, 'workspace')!; + if (workspaceCapabilities['_vs_projectContext'] === undefined) { + workspaceCapabilities['_vs_projectContext'] = {} as any; + } + const projectContext = workspaceCapabilities['_vs_projectContext']; + projectContext.refreshSupport = true; + } + + public initialize(_capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { + const capabilities: any = _capabilities; + const options = this.getRegistrationOptions(documentSelector, capabilities._vs_projectContext); + if (!options) { + return; + } + this.register({ + id: randomUUID(), + registerOptions: options, + }); + } + + public getOptions(textDocument: TextDocument): RoslynProtocol.ProjectContextRegistrationOptions | undefined { + for (const registration of this._registrations.values()) { + const selector = registration.registerOptions.documentSelector; + if ( + selector !== null && + Languages.match(this._client.protocol2CodeConverter.asDocumentSelector(selector), textDocument) > 0 + ) { + return registration.registerOptions; + } + } + return undefined; + } + + private *getDocumentSelectors(): IterableIterator { + for (const registration of this._registrations.values()) { + const selector = registration.registerOptions.documentSelector; + if (selector === null) { + continue; + } + yield this._client.protocol2CodeConverter.asDocumentSelector(selector); + } + } + + private getRegistrationOptions( + documentSelector: DocumentSelector | undefined, + capability: undefined | TextDocumentRegistrationOptions + ): RoslynProtocol.ProjectContextRegistrationOptions | undefined { + if (!documentSelector || !capability) { + return undefined; + } + return { documentSelector }; + } +} diff --git a/src/lsptoolshost/projectContext/projectContextService.ts b/src/lsptoolshost/projectContext/projectContextService.ts index e22f2550e..51cf97714 100644 --- a/src/lsptoolshost/projectContext/projectContextService.ts +++ b/src/lsptoolshost/projectContext/projectContextService.ts @@ -5,10 +5,16 @@ import * as vscode from 'vscode'; import { RoslynLanguageServer } from '../server/roslynLanguageServer'; -import { VSGetProjectContextsRequest, VSProjectContext, VSProjectContextList } from '../server/roslynProtocol'; +import { + ProjectContextRefreshNotification, + VSGetProjectContextsRequest, + VSProjectContext, + VSProjectContextList, +} from '../server/roslynProtocol'; import { TextDocumentIdentifier } from 'vscode-languageserver-protocol'; import { UriConverter } from '../utils/uriConverter'; import { LanguageServerEvents, ServerState } from '../server/languageServerEvents'; +import { RoslynLanguageClient } from '../server/roslynLanguageClient'; export interface ProjectContextChangeEvent { languageId: string; @@ -17,7 +23,8 @@ export interface ProjectContextChangeEvent { isVerified: boolean; } -const VerificationDelay = 2 * 1000; +// We want to verify the project context is in a stable state before warning the user about miscellaneous files. +const VerificationDelay = 5 * 1000; let _verifyTimeout: NodeJS.Timeout | undefined; let _documentUriToVerify: vscode.Uri | undefined; @@ -32,7 +39,11 @@ export class ProjectContextService { _vs_is_miscellaneous: false, }; - constructor(private _languageServer: RoslynLanguageServer, _languageServerEvents: LanguageServerEvents) { + constructor( + private _languageServer: RoslynLanguageServer, + _languageClient: RoslynLanguageClient, + _languageServerEvents: LanguageServerEvents + ) { _languageServerEvents.onServerStateChange(async (e) => { // When the project initialization is complete, open files // could move from the miscellaneous workspace context into @@ -42,6 +53,10 @@ export class ProjectContextService { } }); + _languageClient.onNotification(ProjectContextRefreshNotification.type, async () => { + await this.refresh(); + }); + vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh()); } @@ -62,22 +77,15 @@ export class ProjectContextService { const uri = textEditor!.document.uri; - // Whether we have refreshed the active document's project context. - let isVerifyPass = false; - + // We verify a project context is stable by waiting for a period of time + // without any changes before sending a verified event. Changing active document + // or receiving a new project context refresh notification cancels any pending verification. if (_verifyTimeout) { - // If we have changed active document then do not verify the previous one. clearTimeout(_verifyTimeout); _verifyTimeout = undefined; } if (_documentUriToVerify) { - if (uri.toString() === _documentUriToVerify.toString()) { - // We have rerequested project contexts for the active document - // and we can now notify if the document isn't part of the workspace. - isVerifyPass = true; - } - _documentUriToVerify = undefined; } @@ -93,24 +101,12 @@ export class ProjectContextService { } const context = contextList._vs_projectContexts[contextList._vs_defaultIndex]; - const isVerified = !context._vs_is_miscellaneous || isVerifyPass; - this._contextChangeEmitter.fire({ languageId, uri, context, isVerified }); - - if (context._vs_is_miscellaneous && !isVerifyPass) { - // Request the active project context be refreshed but delay the request to give - // time for the project system to update with new files. - _verifyTimeout = setTimeout(() => { - _verifyTimeout = undefined; - _documentUriToVerify = uri; - - // Trigger a refresh, but don't block on refresh completing. - this.refresh().catch((e) => { - throw new Error(`Error refreshing project context status ${e}`); - }); - }, VerificationDelay); + this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: false }); - return; - } + // If we do not recieve a refresh even within the timout period, send a verified event. + _verifyTimeout = setTimeout(() => { + this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: true }); + }, VerificationDelay); } private async getProjectContexts( diff --git a/src/lsptoolshost/server/roslynLanguageServer.ts b/src/lsptoolshost/server/roslynLanguageServer.ts index 98109be50..b1180d804 100644 --- a/src/lsptoolshost/server/roslynLanguageServer.ts +++ b/src/lsptoolshost/server/roslynLanguageServer.ts @@ -69,6 +69,7 @@ import { getProfilingEnvVars } from '../profiling/profiling'; import { isString } from '../utils/isString'; import { getServerPath } from '../activate'; import { UriConverter } from '../utils/uriConverter'; +import { ProjectContextFeature } from '../projectContext/projectContextFeature'; // Flag indicating if C# Devkit was installed the last time we activated. // Used to determine if we need to restart the server on extension changes. @@ -106,6 +107,7 @@ export class RoslynLanguageServer { private _projectFiles: vscode.Uri[] = new Array(); public readonly _onAutoInsertFeature: OnAutoInsertFeature; + public readonly _projectContextFeature: ProjectContextFeature; public readonly _buildDiagnosticService: BuildDiagnosticsService; public readonly _projectContextService: ProjectContextService; @@ -134,7 +136,7 @@ export class RoslynLanguageServer { this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild); this.registerDocumentOpenForDiagnostics(); - this._projectContextService = new ProjectContextService(this, this._languageServerEvents); + this._projectContextService = new ProjectContextService(this, this._languageClient, this._languageServerEvents); this.registerDebuggerAttach(); @@ -143,6 +145,7 @@ export class RoslynLanguageServer { registerOnAutoInsert(this, this._languageClient); this._onAutoInsertFeature = new OnAutoInsertFeature(this._languageClient); + this._projectContextFeature = new ProjectContextFeature(this._languageClient); } public get state(): ServerState { @@ -335,6 +338,7 @@ export class RoslynLanguageServer { ); client.registerFeature(server._onAutoInsertFeature); + client.registerFeature(server._projectContextFeature); // Start the client. This will also launch the server process. await client.start(); @@ -604,6 +608,10 @@ export class RoslynLanguageServer { return this._onAutoInsertFeature; } + public getProjectContextFeature(): ProjectContextFeature | undefined { + return this._projectContextFeature; + } + private static async startServer( platformInfo: PlatformInformation, hostExecutableResolver: IHostExecutableResolver, diff --git a/src/lsptoolshost/server/roslynProtocol.ts b/src/lsptoolshost/server/roslynProtocol.ts index f51b7cf94..2f10dd349 100644 --- a/src/lsptoolshost/server/roslynProtocol.ts +++ b/src/lsptoolshost/server/roslynProtocol.ts @@ -99,6 +99,8 @@ export interface OnAutoInsertResponseItem { command?: Command; } +export type ProjectContextRegistrationOptions = TextDocumentRegistrationOptions; + /** * OnAutoInsert options. */ @@ -272,6 +274,12 @@ export namespace VSGetProjectContextsRequest { export const type = new RequestType(method); } +export namespace ProjectContextRefreshNotification { + export const method = 'workspace/projectContext/_vs_refresh'; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new NotificationType(method); +} + export namespace ProjectInitializationCompleteNotification { export const method = 'workspace/projectInitializationComplete'; export const messageDirection: MessageDirection = MessageDirection.serverToClient;