diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index ba353a858..1c4ea50da 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -804,9 +804,9 @@ export interface Client { getShowConfigureIntelliSenseButton(): boolean; setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; - getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise; + getIncludes(maxDepth: number): Promise; getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise; - getProjectContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise; + getProjectContext(uri: vscode.Uri): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -2228,11 +2228,11 @@ export class DefaultClient implements Client { await this.languageClient.sendNotification(DidOpenNotification, params); } - public async getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { + public async getIncludes(maxDepth: number): Promise { const params: GetIncludesParams = { maxDepth: maxDepth }; await this.ready; - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(IncludesRequest, params, token), token); + return DefaultClient.withoutLspCancellationHandling( + () => this.languageClient.sendRequest(IncludesRequest, params)); } public async getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise { @@ -2242,11 +2242,11 @@ export class DefaultClient implements Client { () => this.languageClient.sendRequest(CppContextRequest, params, token), token); } - public async getProjectContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise { + public async getProjectContext(uri: vscode.Uri): Promise { const params: TextDocumentIdentifier = { uri: uri.toString() }; - await withCancellation(this.ready, token); - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(ProjectContextRequest, params, token), token); + await this.ready; + return DefaultClient.withoutLspCancellationHandling( + () => this.languageClient.sendRequest(ProjectContextRequest, params)); } /** @@ -2348,6 +2348,27 @@ export class DefaultClient implements Client { return result; } + // Copilot completions will cancel the current request if it times out (showing the user completion results without context info), + // but the results can still be used for future requests (due to caching) so it's better to return results instead of cancelling. + private static async withoutLspCancellationHandling(task: () => Promise): Promise { + let result: T; + + try { + result = await task(); + } catch (e: any) { + if (e instanceof ResponseError && e.code === ServerCancelled) { + // This can occur if the user switches files and makes edits fast enough. + // It doesn't seem like a very important scenario, so cancelling seems fine, even though + // not cancelling would allow the value to be cached if the user switches back to that file. + throw new vscode.CancellationError(); + } else { + throw e; + } + } + + return result; + } + private callTaskWithTimeout(task: () => Thenable, ms: number, cancelToken?: vscode.CancellationTokenSource): Promise { let timer: NodeJS.Timeout; @@ -4151,7 +4172,7 @@ class NullClient implements Client { getShowConfigureIntelliSenseButton(): boolean { return false; } setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } - getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as GetIncludesResult); } + getIncludes(maxDepth: number): Promise { return Promise.resolve({} as GetIncludesResult); } getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } - getProjectContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise { return Promise.resolve({} as ProjectContextResult); } + getProjectContext(uri: vscode.Uri): Promise { return Promise.resolve({} as ProjectContextResult); } } diff --git a/Extension/src/LanguageServer/copilotProviders.ts b/Extension/src/LanguageServer/copilotProviders.ts index 2433e1695..653fad1d5 100644 --- a/Extension/src/LanguageServer/copilotProviders.ts +++ b/Extension/src/LanguageServer/copilotProviders.ts @@ -38,14 +38,14 @@ export async function registerRelatedFilesProvider(): Promise { for (const languageId of ['c', 'cpp', 'cuda-cpp']) { api.registerRelatedFilesProvider( { extensionId: util.extensionContext.extension.id, languageId }, - async (uri: vscode.Uri, context: { flags: Record }, token: vscode.CancellationToken) => { + async (uri: vscode.Uri, context: { flags: Record }) => { const start = performance.now(); const telemetryProperties: Record = {}; const telemetryMetrics: Record = {}; try { - const getIncludesHandler = async () => (await getIncludesWithCancellation(1, token))?.includedFiles.map(file => vscode.Uri.file(file)) ?? []; + const getIncludesHandler = async () => (await getIncludes(1))?.includedFiles.map(file => vscode.Uri.file(file)) ?? []; const getTraitsHandler = async () => { - const projectContext = await getProjectContext(uri, context, token); + const projectContext = await getProjectContext(uri, context); if (!projectContext) { return undefined; @@ -154,9 +154,9 @@ export async function registerRelatedFilesProvider(): Promise { } } -async function getIncludesWithCancellation(maxDepth: number, token: vscode.CancellationToken): Promise { +async function getIncludes(maxDepth: number): Promise { const activeClient = getActiveClient(); - const includes = await activeClient.getIncludes(maxDepth, token); + const includes = await activeClient.getIncludes(maxDepth); const wksFolder = activeClient.RootUri?.toString(); if (!wksFolder) { diff --git a/Extension/src/LanguageServer/lmTool.ts b/Extension/src/LanguageServer/lmTool.ts index 746ff3829..75ca8b994 100644 --- a/Extension/src/LanguageServer/lmTool.ts +++ b/Extension/src/LanguageServer/lmTool.ts @@ -127,11 +127,11 @@ function filterCompilerArguments(compiler: string, compilerArguments: string[], return result; } -export async function getProjectContext(uri: vscode.Uri, context: { flags: Record }, token: vscode.CancellationToken): Promise { +export async function getProjectContext(uri: vscode.Uri, context: { flags: Record }): Promise { const telemetryProperties: Record = {}; const telemetryMetrics: Record = {}; try { - const projectContext = await checkDuration(async () => await getClients()?.ActiveClient?.getProjectContext(uri, token) ?? undefined); + const projectContext = await checkDuration(async () => await getClients()?.ActiveClient?.getProjectContext(uri) ?? undefined); telemetryMetrics["duration"] = projectContext.duration; if (!projectContext.result) { return undefined; diff --git a/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts b/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts index f11c048e1..afcf90366 100644 --- a/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts +++ b/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts @@ -200,7 +200,7 @@ describe('CppConfigurationLanguageModelTool Tests', () => { } }); - const result = await getProjectContext(mockTextDocumentStub.uri, context, new vscode.CancellationTokenSource().token); + const result = await getProjectContext(mockTextDocumentStub.uri, context); ok(result, 'result should not be undefined'); ok(result.language === 'C++'); @@ -364,7 +364,7 @@ describe('CppConfigurationLanguageModelTool Tests', () => { } }); - const result = await getProjectContext(mockTextDocumentStub.uri, { flags: {} }, new vscode.CancellationTokenSource().token); + const result = await getProjectContext(mockTextDocumentStub.uri, { flags: {} }); ok(telemetryStub.calledOnce, 'Telemetry should be called once'); ok(telemetryStub.calledWithMatch('ProjectContext', sinon.match({