Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions src/lsptoolshost/projectContext/projectContextFeature.ts
Original file line number Diff line number Diff line change
@@ -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<RoslynProtocol.ProjectContextRegistrationOptions> {
private readonly _client: LanguageClient;
private readonly _registrations: Map<string, RegistrationData<RoslynProtocol.ProjectContextRegistrationOptions>>;

constructor(client: LanguageClient) {
this._client = client;
this._registrations = new Map();
this.registrationType = new ProtocolNotificationType0<RoslynProtocol.ProjectContextRegistrationOptions>(
RoslynProtocol.ProjectContextRefreshNotification.method
);
}
fillInitializeParams?: ((params: InitializeParams) => void) | undefined;
preInitialize?:
| ((capabilities: ServerCapabilities<any>, documentSelector: DocumentSelector | undefined) => void)
| undefined;
registrationType: RegistrationType<RoslynProtocol.ProjectContextRegistrationOptions>;
register(data: RegistrationData<RoslynProtocol.ProjectContextRegistrationOptions>): 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<VDocumentSelector> {
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 };
}
}
56 changes: 26 additions & 30 deletions src/lsptoolshost/projectContext/projectContextService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -42,6 +53,10 @@ export class ProjectContextService {
}
});

_languageClient.onNotification(ProjectContextRefreshNotification.type, async () => {
await this.refresh();
});

vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh());
}

Expand All @@ -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;
}

Expand All @@ -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(
Expand Down
10 changes: 9 additions & 1 deletion src/lsptoolshost/server/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -106,6 +107,7 @@ export class RoslynLanguageServer {
private _projectFiles: vscode.Uri[] = new Array<vscode.Uri>();

public readonly _onAutoInsertFeature: OnAutoInsertFeature;
public readonly _projectContextFeature: ProjectContextFeature;

public readonly _buildDiagnosticService: BuildDiagnosticsService;
public readonly _projectContextService: ProjectContextService;
Expand Down Expand Up @@ -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();

Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/lsptoolshost/server/roslynProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export interface OnAutoInsertResponseItem {
command?: Command;
}

export type ProjectContextRegistrationOptions = TextDocumentRegistrationOptions;

/**
* OnAutoInsert options.
*/
Expand Down Expand Up @@ -272,6 +274,12 @@ export namespace VSGetProjectContextsRequest {
export const type = new RequestType<VSGetProjectContextParams, VSProjectContextList, void>(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;
Expand Down
Loading