Skip to content

Commit 01efb1a

Browse files
sherryyshibobbrow
authored andcommitted
Allow other VS Code extensions to provide custom intellisense configurations. (#1804)
1 parent c9473b2 commit 01efb1a

File tree

6 files changed

+306
-32
lines changed

6 files changed

+306
-32
lines changed

Extension/src/LanguageServer/client.ts

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { DataBinding } from './dataBinding';
2222
import minimatch = require("minimatch");
2323
import * as logger from '../logger';
2424
import { updateLanguageConfigurations } from './extension';
25+
import { CustomConfigurationProvider, SourceFileConfigurationItem } from '../api';
26+
import { CancellationTokenSource } from 'vscode';
2527
import { SettingsTracker, getTracker } from './settingsTracker';
2628

2729
let ui: UI;
@@ -86,6 +88,10 @@ interface DecorationRangesPair {
8688
ranges: vscode.Range[];
8789
}
8890

91+
interface CustomConfigurationParams {
92+
configurationItems: SourceFileConfigurationItem[];
93+
}
94+
8995
// Requests
9096
const NavigationListRequest: RequestType<TextDocumentIdentifier, string, void, void> = new RequestType<TextDocumentIdentifier, string, void, void>('cpptools/requestNavigationList');
9197
const GoToDeclarationRequest: RequestType<void, void, void, void> = new RequestType<void, void, void, void>('cpptools/goToDeclaration');
@@ -105,6 +111,7 @@ const ChangeFolderSettingsNotification: NotificationType<FolderSettingsParams, v
105111
const ChangeCompileCommandsNotification: NotificationType<FileChangedParams, void> = new NotificationType<FileChangedParams, void>('cpptools/didChangeCompileCommands');
106112
const ChangeSelectedSettingNotification: NotificationType<FolderSelectedSettingParams, void> = new NotificationType<FolderSelectedSettingParams, void>('cpptools/didChangeSelectedSetting');
107113
const IntervalTimerNotification: NotificationType<void, void> = new NotificationType<void, void>('cpptools/onIntervalTimer');
114+
const CustomConfigurationNotification: NotificationType<CustomConfigurationParams, void> = new NotificationType<CustomConfigurationParams, void>('cpptools/didChangeCustomConfiguration');
108115

109116
// Notifications from the server
110117
const ReloadWindowNotification: NotificationType<void, void> = new NotificationType<void, void>('cpptools/reloadWindow');
@@ -138,13 +145,19 @@ export interface Client {
138145
TrackedDocuments: Set<vscode.TextDocument>;
139146
onDidChangeSettings(): void;
140147
onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]): void;
148+
onDidChangeCustomConfigurations(provider: CustomConfigurationProvider): void;
141149
takeOwnership(document: vscode.TextDocument): void;
150+
runBlockingTask<T>(task: Thenable<T>): Thenable<T>;
151+
runBlockingThenableWithTimeout(thenable: () => Thenable<any>, ms: number, tokenSource?: CancellationTokenSource): Thenable<any>;
152+
requestWhenReady(request: () => Thenable<any>): Thenable<any>;
153+
notifyWhenReady(notify: () => void): void;
142154
requestGoToDeclaration(): Thenable<void>;
143155
requestSwitchHeaderSource(rootPath: string, fileName: string): Thenable<string>;
144156
requestNavigationList(document: vscode.TextDocument): Thenable<string>;
145157
activeDocumentChanged(document: vscode.TextDocument): void;
146158
activate(): void;
147159
selectionChanged(selection: vscode.Position): void;
160+
sendCustomConfigurations(configs: SourceFileConfigurationItem[]): void;
148161
resetDatabase(): void;
149162
deactivate(): void;
150163
pauseParsing(): void;
@@ -215,12 +228,14 @@ class DefaultClient implements Client {
215228
}
216229

217230
/**
218-
* All public methods on this class must be guarded by the "onReady" promise. Requests and notifications received before the client is
219-
* ready are executed after this promise is resolved.
231+
* All public methods on this class must be guarded by the "pendingTask" promise. Requests and notifications received before the task is
232+
* complete are executed after this promise is resolved.
220233
* @see requestWhenReady<T>(request)
221234
* @see notifyWhenReady(notify)
222235
*/
223-
private onReadyPromise: Thenable<void>;
236+
237+
private pendingTask: Thenable<any>;
238+
private pendingRequests: number = 0;
224239

225240
constructor(allClients: ClientCollection, workspaceFolder?: vscode.WorkspaceFolder) {
226241
try {
@@ -232,7 +247,7 @@ class DefaultClient implements Client {
232247
ui = getUI();
233248
ui.bind(this);
234249

235-
this.onReadyPromise = languageClient.onReady().then(() => {
250+
this.runBlockingTask(languageClient.onReady()).then(() => {
236251
this.configuration = new configs.CppProperties(this.RootUri);
237252
this.configuration.ConfigurationsChanged((e) => this.onConfigurationsChanged(e));
238253
this.configuration.SelectionChanged((e) => this.onSelectedConfigurationChanged(e));
@@ -385,6 +400,23 @@ class DefaultClient implements Client {
385400
}
386401
}
387402

403+
public onDidChangeCustomConfigurations(provider: CustomConfigurationProvider): void {
404+
let documentUris: vscode.Uri[] = [];
405+
this.trackedDocuments.forEach(document => documentUris.push(document.uri));
406+
407+
let tokenSource: CancellationTokenSource = new CancellationTokenSource();
408+
409+
if (documentUris.length === 0) {
410+
return;
411+
}
412+
413+
this.runBlockingThenableWithTimeout(() => {
414+
return provider.provideConfigurations(documentUris, tokenSource.token);
415+
}, 1000, tokenSource).then((configs: SourceFileConfigurationItem[]) => {
416+
this.sendCustomConfigurations(configs);
417+
});
418+
}
419+
388420
/**
389421
* Take ownership of a document that was previously serviced by another client.
390422
* This process involves sending a textDocument/didOpen message to the server so
@@ -405,24 +437,75 @@ class DefaultClient implements Client {
405437
}
406438

407439
/*************************************************************************************
408-
* wait until the language client is ready for use before attempting to send messages
440+
* wait until the all pendingTasks are complete (e.g. language client is ready for use)
441+
* before attempting to send messages
409442
*************************************************************************************/
443+
public runBlockingTask(task: Thenable<any>): Thenable<any> {
444+
if (this.pendingTask) {
445+
return this.requestWhenReady(() => { return task; });
446+
} else {
447+
this.pendingTask = task;
448+
return task.then((result) => {
449+
this.pendingTask = undefined;
450+
return result;
451+
}, (error) => {
452+
this.pendingTask = undefined;
453+
throw error;
454+
});
455+
}
456+
}
410457

411-
private requestWhenReady<T>(request: () => Thenable<T>): Thenable<T> {
412-
if (this.languageClient) {
458+
public runBlockingThenableWithTimeout(thenable: () => Thenable<any>, ms: number, tokenSource?: CancellationTokenSource): Thenable<any> {
459+
let timer: NodeJS.Timer;
460+
// Create a promise that rejects in <ms> milliseconds
461+
let timeout: Promise<any> = new Promise((resolve, reject) => {
462+
timer = setTimeout(() => {
463+
clearTimeout(timer);
464+
if (tokenSource) {
465+
tokenSource.cancel();
466+
}
467+
reject("Timed out in " + ms + "ms.");
468+
}, ms);
469+
});
470+
471+
// Returns a race between our timeout and the passed in promise
472+
return this.runBlockingTask(Promise.race([thenable(), timeout]).then((result: any) => {
473+
clearTimeout(timer);
474+
return result;
475+
}, (error: any) => {
476+
throw error;
477+
}));
478+
}
479+
480+
public requestWhenReady(request: () => Thenable<any>): Thenable<any> {
481+
if (this.pendingTask === undefined) {
413482
return request();
414-
} else if (this.isSupported && this.onReadyPromise) {
415-
return this.onReadyPromise.then(() => request());
483+
} else if (this.isSupported && this.pendingTask) {
484+
this.pendingRequests++;
485+
return this.pendingTask.then(() => {
486+
this.pendingRequests--;
487+
if (this.pendingRequests === 0) {
488+
this.pendingTask = undefined;
489+
}
490+
return request();
491+
});
416492
} else {
417-
return Promise.reject<T>("Unsupported client");
493+
return Promise.reject("Unsupported client");
418494
}
419495
}
420496

421-
private notifyWhenReady(notify: () => void): void {
422-
if (this.languageClient) {
497+
public notifyWhenReady(notify: () => void): void {
498+
if (this.pendingTask === undefined) {
423499
notify();
424-
} else if (this.isSupported && this.onReadyPromise) {
425-
this.onReadyPromise.then(() => notify());
500+
} else if (this.isSupported && this.pendingTask) {
501+
this.pendingRequests++;
502+
this.pendingTask.then(() => {
503+
this.pendingRequests--;
504+
if (this.pendingRequests === 0) {
505+
this.pendingTask = undefined;
506+
}
507+
notify();
508+
});
426509
}
427510
}
428511

@@ -763,6 +846,13 @@ class DefaultClient implements Client {
763846
this.notifyWhenReady(() => this.languageClient.sendNotification(ChangeCompileCommandsNotification, params));
764847
}
765848

849+
public sendCustomConfigurations(configs: SourceFileConfigurationItem[]): void {
850+
let params: CustomConfigurationParams = {
851+
configurationItems: configs
852+
};
853+
this.notifyWhenReady(() => this.languageClient.sendNotification(CustomConfigurationNotification, params));
854+
}
855+
766856
/*********************************************
767857
* command handlers
768858
*********************************************/
@@ -853,7 +943,13 @@ class NullClient implements Client {
853943
TrackedDocuments = new Set<vscode.TextDocument>();
854944
onDidChangeSettings(): void {}
855945
onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]): void {}
946+
onDidChangeCustomConfigurations(provider: CustomConfigurationProvider): void {}
856947
takeOwnership(document: vscode.TextDocument): void {}
948+
runBlockingTask<T>(task: Thenable<T>): Thenable<T> { return; }
949+
runBlockingThenableWithTimeout(thenable: () => Thenable<any>, ms: number, tokenSource?: CancellationTokenSource): Thenable<any> { return; }
950+
requestWhenReady(request: () => Thenable<any>): Thenable<any> { return; }
951+
notifyWhenReady(notify: () => void): void {}
952+
sendCustomConfigurations(configs: SourceFileConfigurationItem[]): void {}
857953
requestGoToDeclaration(): Thenable<void> { return Promise.resolve(); }
858954
requestSwitchHeaderSource(rootPath: string, fileName: string): Thenable<string> { return Promise.resolve(""); }
859955
requestNavigationList(document: vscode.TextDocument): Thenable<string> { return Promise.resolve(""); }

Extension/src/LanguageServer/extension.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import * as path from 'path';
88
import * as vscode from 'vscode';
9+
import { CancellationTokenSource } from "vscode-jsonrpc";
910
import * as fs from 'fs';
1011
import * as util from '../common';
1112
import * as telemetry from '../telemetry';
@@ -15,6 +16,7 @@ import { ClientCollection } from './clientCollection';
1516
import { CppSettings } from './settings';
1617
import { PersistentWorkspaceState } from './persistentState';
1718
import { getLanguageConfig } from './languageConfig';
19+
import { CustomConfigurationProvider, SourceFileConfigurationItem } from '../api';
1820
import * as os from 'os';
1921

2022
let prevCrashFile: string;
@@ -27,6 +29,7 @@ let intervalTimer: NodeJS.Timer;
2729
let realActivationOccurred: boolean = false;
2830
let tempCommands: vscode.Disposable[] = [];
2931
let activatedPreviously: PersistentWorkspaceState<boolean>;
32+
let customConfigurationProviders: CustomConfigurationProvider[] = [];
3033

3134
/**
3235
* activate: set up the extension for language services
@@ -41,7 +44,7 @@ export function activate(activationEventOccurred: boolean): void {
4144
activatedPreviously.Value = false;
4245
realActivation();
4346
}
44-
47+
4548
registerCommands();
4649
tempCommands.push(vscode.workspace.onDidOpenTextDocument(d => onDidOpenTextDocument(d)));
4750

@@ -74,6 +77,37 @@ export function activate(activationEventOccurred: boolean): void {
7477
}
7578
}
7679

80+
export function registerCustomConfigurationProvider(provider: CustomConfigurationProvider): void {
81+
customConfigurationProviders.push(provider);
82+
83+
// Request for configurations from the provider only if realActivationOccurred.
84+
// Otherwise, the request will be sent when realActivation is called.
85+
if (realActivationOccurred) {
86+
onDidChangeCustomConfiguration(provider);
87+
}
88+
}
89+
90+
export async function provideCustomConfiguration(document: vscode.TextDocument, client: Client): Promise<void> {
91+
let tokenSource: CancellationTokenSource = new CancellationTokenSource();
92+
return client.runBlockingThenableWithTimeout(async () => {
93+
// Loop through registered providers until one is able to service the current document
94+
for (let i: number = 0; i < customConfigurationProviders.length; i++) {
95+
if (await customConfigurationProviders[i].canProvideConfiguration(document.uri)) {
96+
return customConfigurationProviders[i].provideConfigurations([document.uri]);
97+
}
98+
}
99+
return Promise.reject("No providers found for " + document.uri);
100+
}, 1000, tokenSource).then((configs: SourceFileConfigurationItem[]) => {
101+
if (configs !== null && configs.length > 0) {
102+
client.sendCustomConfigurations(configs);
103+
}
104+
});
105+
}
106+
107+
export function onDidChangeCustomConfiguration(customConfigurationProvider: CustomConfigurationProvider): void {
108+
clients.forEach((client: Client) => client.onDidChangeCustomConfigurations(customConfigurationProvider));
109+
}
110+
77111
function onDidOpenTextDocument(document: vscode.TextDocument): void {
78112
if (document.languageId === "c" || document.languageId === "cpp") {
79113
onActivationEvent();
@@ -108,6 +142,10 @@ function realActivation(): void {
108142
onDidChangeActiveTextEditor(vscode.window.activeTextEditor);
109143
}
110144

145+
// There may have already been registered CustomConfigurationProviders.
146+
// Request for configurations from those providers.
147+
customConfigurationProviders.forEach(provider => onDidChangeCustomConfiguration(provider));
148+
111149
disposables.push(vscode.workspace.onDidChangeConfiguration(onDidChangeSettings));
112150
disposables.push(vscode.workspace.onDidSaveTextDocument(onDidSaveTextDocument));
113151
disposables.push(vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor));
@@ -172,7 +210,7 @@ function onDidChangeTextEditorSelection(event: vscode.TextEditorSelectionChangeE
172210
if (!event.textEditor || !vscode.window.activeTextEditor || event.textEditor.document.uri !== vscode.window.activeTextEditor.document.uri ||
173211
(event.textEditor.document.languageId !== "cpp" && event.textEditor.document.languageId !== "c")) {
174212
return;
175-
}
213+
}
176214

177215
if (activeDocument !== event.textEditor.document.uri.toString()) {
178216
// For some strange (buggy?) reason we don't reliably get onDidChangeActiveTextEditor callbacks.
@@ -304,7 +342,7 @@ function selectClient(): Thenable<Client> {
304342
function onResetDatabase(): void {
305343
onActivationEvent();
306344
/* need to notify the affected client(s) */
307-
selectClient().then(client => client.resetDatabase(), rejected => {});
345+
selectClient().then(client => client.resetDatabase(), rejected => { });
308346
}
309347

310348
function onSelectConfiguration(): void {
@@ -323,7 +361,7 @@ function onEditConfiguration(): void {
323361
if (!isFolderOpen()) {
324362
vscode.window.showInformationMessage('Open a folder first to edit configurations');
325363
} else {
326-
selectClient().then(client => client.handleConfigurationEditCommand(), rejected => {});
364+
selectClient().then(client => client.handleConfigurationEditCommand(), rejected => { });
327365
}
328366
}
329367

@@ -406,17 +444,17 @@ function onShowReleaseNotes(): void {
406444

407445
function onPauseParsing(): void {
408446
onActivationEvent();
409-
selectClient().then(client => client.pauseParsing(), rejected => {});
447+
selectClient().then(client => client.pauseParsing(), rejected => { });
410448
}
411449

412450
function onResumeParsing(): void {
413451
onActivationEvent();
414-
selectClient().then(client => client.resumeParsing(), rejected => {});
452+
selectClient().then(client => client.resumeParsing(), rejected => { });
415453
}
416454

417455
function onShowParsingCommands(): void {
418456
onActivationEvent();
419-
selectClient().then(client => client.handleShowParsingCommands(), rejected => {});
457+
selectClient().then(client => client.handleShowParsingCommands(), rejected => { });
420458
}
421459

422460
function onTakeSurvey(): void {

Extension/src/LanguageServer/protocolFilter.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,35 @@
77
import { Middleware } from 'vscode-languageclient';
88
import { ClientCollection } from './clientCollection';
99
import { Client } from './client';
10+
import { provideCustomConfiguration } from './extension';
1011

1112
export function createProtocolFilter(me: Client, clients: ClientCollection): Middleware {
1213
// Disabling lint for invoke handlers
1314
/* tslint:disable */
14-
let defaultHandler: (data: any, callback: (data: any) => void) => void = (data, callback: (data) => void) => { if (clients.ActiveClient === me) { callback(data); } };
15-
let invoke1 = (a, callback: (a) => any) => { if (clients.ActiveClient === me) { return callback(a); } return null; };
16-
let invoke2 = (a, b, callback: (a, b) => any) => { if (clients.ActiveClient === me) { return callback(a, b); } return null; };
17-
let invoke3 = (a, b, c, callback: (a, b, c) => any) => { if (clients.ActiveClient === me) { return callback(a, b, c); } return null; };
18-
let invoke4 = (a, b, c, d, callback: (a, b, c, d) => any) => { if (clients.ActiveClient === me) { return callback(a, b, c, d); } return null; };
19-
let invoke5 = (a, b, c, d, e, callback: (a, b, c, d, e) => any) => { if (clients.ActiveClient === me) { return callback(a, b, c, d, e); } return null; };
15+
let defaultHandler: (data: any, callback: (data: any) => void) => void = (data, callback: (data) => void) => { if (clients.ActiveClient === me) {me.notifyWhenReady(() => callback(data));}};
16+
let invoke1 = (a, callback: (a) => any) => { if (clients.ActiveClient === me) { return me.requestWhenReady(callback(a)); } return null; };
17+
let invoke2 = (a, b, callback: (a, b) => any) => { if (clients.ActiveClient === me) { return me.requestWhenReady(callback(a, b)); } return null; };
18+
let invoke3 = (a, b, c, callback: (a, b, c) => any) => { if (clients.ActiveClient === me) { return me.requestWhenReady(callback(a, b, c)); } return null; };
19+
let invoke4 = (a, b, c, d, callback: (a, b, c, d) => any) => { if (clients.ActiveClient === me) { return me.requestWhenReady(callback(a, b, c, d)); } return null; };
20+
let invoke5 = (a, b, c, d, e, callback: (a, b, c, d, e) => any) => { if (clients.ActiveClient === me) { return me.requestWhenReady(callback(a, b, c, d, e)); } return null; };
2021
/* tslint:enable */
2122

2223
return {
2324
didOpen: (document, sendMessage) => {
2425
if (clients.checkOwnership(me, document)) {
2526
me.TrackedDocuments.add(document);
26-
sendMessage(document);
27+
provideCustomConfiguration(document, me).then(() => {
28+
sendMessage(document);
29+
}, () => {
30+
sendMessage(document);
31+
});
2732
}
2833
},
2934
didChange: defaultHandler,
3035
willSave: defaultHandler,
3136
willSaveWaitUntil: (event, sendMessage) => {
3237
if (clients.ActiveClient === me) {
33-
return sendMessage(event);
38+
return me.requestWhenReady(() => sendMessage(event));
3439
}
3540
return Promise.resolve([]);
3641
},
@@ -39,7 +44,7 @@ export function createProtocolFilter(me: Client, clients: ClientCollection): Mid
3944
if (clients.ActiveClient === me) {
4045
console.assert(me.TrackedDocuments.has(document));
4146
me.TrackedDocuments.delete(document);
42-
sendMessage(document);
47+
me.notifyWhenReady(() => sendMessage(document));
4348
}
4449
},
4550

0 commit comments

Comments
 (0)