Skip to content

Commit a055128

Browse files
committed
Add silent document symbol and call hierarchy commands
Introduce internal commands for document symbols and call hierarchy so silent callers can query cpptools without triggering the interactive provider side effects used by the VS Code UI. Extract the shared request/translation logic from the document symbol and call hierarchy providers so the existing provider implementations and the new command handlers reuse the same server request paths, cancellation handling, and result mapping. Keep the current interactive behavior intact for user-invoked provider flows, including reference view state management and call hierarchy telemetry, while exposing side-effect-free entry points for concurrent programmatic callers.
1 parent 42b8b6c commit a055128

File tree

3 files changed

+297
-175
lines changed

3 files changed

+297
-175
lines changed

Extension/src/LanguageServer/Providers/callHierarchyProvider.ts

Lines changed: 170 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,157 @@ const CallHierarchyCallsToRequest: RequestType<CallHierarchyParams, CallHierarch
8888
const CallHierarchyCallsFromRequest: RequestType<CallHierarchyParams, CallHierarchyCallsItemResult, void> =
8989
new RequestType<CallHierarchyParams, CallHierarchyCallsItemResult, void>('cpptools/callHierarchyCallsFrom');
9090

91+
interface CallHierarchyRequestResult<T> {
92+
result?: T;
93+
cancelled: boolean;
94+
}
95+
96+
function makeVscodeCallHierarchyItem(client: DefaultClient, item: CallHierarchyItem): vscode.CallHierarchyItem {
97+
const containerDetail: string = (item.detail !== "") ? `${item.detail} - ` : "";
98+
const itemUri: vscode.Uri = vscode.Uri.file(item.file);
99+
100+
// Get file detail
101+
const isInWorkspace: boolean = client.RootUri !== undefined &&
102+
itemUri.fsPath.startsWith(client.RootUri?.fsPath);
103+
const dirPath: string = isInWorkspace ?
104+
path.relative(client.RootPath, path.dirname(item.file)) : path.dirname(item.file);
105+
const fileDetail: string = dirPath.length === 0 ?
106+
`${path.basename(item.file)}` : `${path.basename(item.file)} (${dirPath})`;
107+
108+
return new vscode.CallHierarchyItem(
109+
item.kind,
110+
item.name,
111+
containerDetail + fileDetail,
112+
itemUri,
113+
makeVscodeRange(item.range),
114+
makeVscodeRange(item.selectionRange));
115+
}
116+
117+
function createIncomingCalls(client: DefaultClient, calls: CallHierarchyCallsItem[]): vscode.CallHierarchyIncomingCall[] {
118+
const result: vscode.CallHierarchyIncomingCall[] = [];
119+
120+
for (const call of calls) {
121+
const item: vscode.CallHierarchyItem = makeVscodeCallHierarchyItem(client, call.item);
122+
const ranges: vscode.Range[] = [];
123+
call.fromRanges.forEach(r => {
124+
ranges.push(makeVscodeRange(r));
125+
});
126+
127+
const incomingCall: vscode.CallHierarchyIncomingCall =
128+
new vscode.CallHierarchyIncomingCall(item, ranges);
129+
result.push(incomingCall);
130+
}
131+
132+
return result;
133+
}
134+
135+
function createOutgoingCalls(client: DefaultClient, calls: CallHierarchyCallsItem[]): vscode.CallHierarchyOutgoingCall[] {
136+
const result: vscode.CallHierarchyOutgoingCall[] = [];
137+
138+
for (const call of calls) {
139+
const item: vscode.CallHierarchyItem = makeVscodeCallHierarchyItem(client, call.item);
140+
const ranges: vscode.Range[] = [];
141+
call.fromRanges.forEach(r => {
142+
ranges.push(makeVscodeRange(r));
143+
});
144+
145+
const outgoingCall: vscode.CallHierarchyOutgoingCall =
146+
new vscode.CallHierarchyOutgoingCall(item, ranges);
147+
result.push(outgoingCall);
148+
}
149+
150+
return result;
151+
}
152+
153+
export async function sendPrepareCallHierarchyRequest(client: DefaultClient, document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<CallHierarchyRequestResult<vscode.CallHierarchyItem>> {
154+
const range: vscode.Range | undefined = document.getWordRangeAtPosition(position);
155+
if (range === undefined) {
156+
return { cancelled: false };
157+
}
158+
159+
await client.ready;
160+
161+
const params: CallHierarchyParams = {
162+
textDocument: { uri: document.uri.toString() },
163+
position: Position.create(position.line, position.character)
164+
};
165+
166+
let response: CallHierarchyItemResult;
167+
try {
168+
response = await client.languageClient.sendRequest(CallHierarchyItemRequest, params, token);
169+
} catch (e: any) {
170+
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
171+
return { cancelled: true };
172+
}
173+
throw e;
174+
}
175+
176+
if (token.isCancellationRequested) {
177+
return { cancelled: true };
178+
}
179+
180+
return {
181+
cancelled: false,
182+
result: response.item ? makeVscodeCallHierarchyItem(client, response.item) : undefined
183+
};
184+
}
185+
186+
export async function sendCallHierarchyIncomingCallsRequest(client: DefaultClient, item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<CallHierarchyRequestResult<vscode.CallHierarchyIncomingCall[]>> {
187+
await client.ready;
188+
189+
const params: CallHierarchyParams = {
190+
textDocument: { uri: item.uri.toString() },
191+
position: Position.create(item.selectionRange.start.line, item.selectionRange.start.character)
192+
};
193+
194+
let response: CallHierarchyCallsItemResult;
195+
try {
196+
response = await client.languageClient.sendRequest(CallHierarchyCallsToRequest, params, token);
197+
} catch (e: any) {
198+
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
199+
return { cancelled: true };
200+
}
201+
throw e;
202+
}
203+
204+
if (token.isCancellationRequested) {
205+
return { cancelled: true };
206+
}
207+
208+
return {
209+
cancelled: false,
210+
result: response.calls.length !== 0 ? createIncomingCalls(client, response.calls) : undefined
211+
};
212+
}
213+
214+
export async function sendCallHierarchyOutgoingCallsRequest(client: DefaultClient, item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<CallHierarchyRequestResult<vscode.CallHierarchyOutgoingCall[]>> {
215+
await client.ready;
216+
217+
const params: CallHierarchyParams = {
218+
textDocument: { uri: item.uri.toString() },
219+
position: Position.create(item.selectionRange.start.line, item.selectionRange.start.character)
220+
};
221+
222+
let response: CallHierarchyCallsItemResult;
223+
try {
224+
response = await client.languageClient.sendRequest(CallHierarchyCallsFromRequest, params, token);
225+
} catch (e: any) {
226+
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
227+
return { cancelled: true };
228+
}
229+
throw e;
230+
}
231+
232+
if (token.isCancellationRequested) {
233+
return { cancelled: true };
234+
}
235+
236+
return {
237+
cancelled: false,
238+
result: response.calls.length !== 0 ? createOutgoingCalls(client, response.calls) : undefined
239+
};
240+
}
241+
91242
export class CallHierarchyProvider implements vscode.CallHierarchyProvider {
92243
// Indicates whether a request is from an entry root node (e.g. top function in the call tree).
93244
private isEntryRootNodeTelemetry: boolean = false;
@@ -98,16 +249,9 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider {
98249
}
99250

100251
public async prepareCallHierarchy(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CallHierarchyItem | undefined> {
101-
await this.client.ready;
102-
103252
workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest);
104253
workspaceReferences.clearViews();
105254

106-
const range: vscode.Range | undefined = document.getWordRangeAtPosition(position);
107-
if (range === undefined) {
108-
return undefined;
109-
}
110-
111255
// Listen to a cancellation for this request. When this request is cancelled,
112256
// use a local cancellation source to explicitly cancel a token.
113257
const cancelSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();
@@ -118,37 +262,26 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider {
118262
cancelSource.cancel();
119263
});
120264

121-
const params: CallHierarchyParams = {
122-
textDocument: { uri: document.uri.toString() },
123-
position: Position.create(position.line, position.character)
124-
};
125-
let response: CallHierarchyItemResult;
265+
let result: CallHierarchyRequestResult<vscode.CallHierarchyItem>;
126266
try {
127-
response = await this.client.languageClient.sendRequest(CallHierarchyItemRequest, params, cancelSource.token);
128-
} catch (e: any) {
129-
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
130-
return undefined;
131-
}
132-
throw e;
133-
}
134-
finally {
267+
result = await sendPrepareCallHierarchyRequest(this.client, document, position, cancelSource.token);
268+
} finally {
135269
cancellationTokenListener.dispose();
136270
requestCanceledListener.dispose();
137271
}
138272

139-
if (cancelSource.token.isCancellationRequested) {
273+
if (cancelSource.token.isCancellationRequested || result.cancelled) {
140274
throw new vscode.CancellationError();
141275
}
142-
if (response.item === undefined) {
276+
if (result.result === undefined) {
143277
return undefined;
144278
}
145279

146280
this.isEntryRootNodeTelemetry = true;
147-
return this.makeVscodeCallHierarchyItem(response.item);
281+
return result.result;
148282
}
149283

150284
public async provideCallHierarchyIncomingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyIncomingCall[] | undefined> {
151-
await this.client.ready;
152285
workspaceReferences.cancelCurrentReferenceRequest(CancellationSender.NewRequest);
153286

154287
const CallHierarchyCallsToEvent: string = "CallHierarchyCallsTo";
@@ -171,40 +304,30 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider {
171304
});
172305

173306
// Send the request to the language server.
174-
let result: vscode.CallHierarchyIncomingCall[] | undefined;
175-
const params: CallHierarchyParams = {
176-
textDocument: { uri: item.uri.toString() },
177-
position: Position.create(item.selectionRange.start.line, item.selectionRange.start.character)
178-
};
179-
let response: CallHierarchyCallsItemResult | undefined;
180-
let cancelled: boolean = false;
307+
let result: CallHierarchyRequestResult<vscode.CallHierarchyIncomingCall[]>;
181308
try {
182-
response = await this.client.languageClient.sendRequest(CallHierarchyCallsToRequest, params, cancelSource.token);
183-
} catch (e: any) {
184-
cancelled = e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled);
185-
if (!cancelled) {
186-
throw e;
187-
}
309+
result = await sendCallHierarchyIncomingCallsRequest(this.client, item, cancelSource.token);
310+
} finally {
311+
cancellationTokenListener.dispose();
312+
requestCanceledListener.dispose();
188313
}
314+
189315
// Reset anything that can be cleared before processing the result.
190316
const progressBarDuration: number | undefined = workspaceReferences.getCallHierarchyProgressBarDuration();
191317
workspaceReferences.resetProgressBar();
192318
workspaceReferences.resetReferences();
193-
cancellationTokenListener.dispose();
194-
requestCanceledListener.dispose();
195319

196320
// Process the result.
197-
if (cancelSource.token.isCancellationRequested || cancelled || requestCanceled !== undefined) {
321+
if (cancelSource.token.isCancellationRequested || result.cancelled || requestCanceled !== undefined) {
198322
const requestStatus: CallHierarchyRequestStatus = requestCanceled === CancellationSender.User ?
199323
CallHierarchyRequestStatus.CanceledByUser : CallHierarchyRequestStatus.Canceled;
200324
this.logTelemetry(CallHierarchyCallsToEvent, requestStatus, progressBarDuration);
201325
throw new vscode.CancellationError();
202-
} else if (response && response.calls.length !== 0) {
203-
result = this.createIncomingCalls(response.calls);
204326
}
205327

328+
const incomingCalls: vscode.CallHierarchyIncomingCall[] | undefined = result.result;
206329
this.logTelemetry(CallHierarchyCallsToEvent, CallHierarchyRequestStatus.Succeeded, progressBarDuration);
207-
return result;
330+
return incomingCalls;
208331
}
209332

210333
public async provideCallHierarchyOutgoingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyOutgoingCall[] | undefined> {
@@ -214,89 +337,15 @@ export class CallHierarchyProvider implements vscode.CallHierarchyProvider {
214337
return undefined;
215338
}
216339

217-
await this.client.ready;
218-
219-
let result: vscode.CallHierarchyOutgoingCall[] | undefined;
220-
const params: CallHierarchyParams = {
221-
textDocument: { uri: item.uri.toString() },
222-
position: Position.create(item.selectionRange.start.line, item.selectionRange.start.character)
223-
};
224-
let response: CallHierarchyCallsItemResult | undefined;
225-
let cancelled: boolean = false;
226-
try {
227-
response = await this.client.languageClient.sendRequest(CallHierarchyCallsFromRequest, params, token);
228-
} catch (e: any) {
229-
cancelled = e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled);
230-
if (!cancelled) {
231-
throw e;
232-
}
233-
}
234-
if (token.isCancellationRequested || cancelled) {
340+
const result: CallHierarchyRequestResult<vscode.CallHierarchyOutgoingCall[]> =
341+
await sendCallHierarchyOutgoingCallsRequest(this.client, item, token);
342+
if (token.isCancellationRequested || result.cancelled) {
235343
this.logTelemetry(CallHierarchyCallsFromEvent, CallHierarchyRequestStatus.Canceled);
236344
throw new vscode.CancellationError();
237-
} else if (response && response.calls.length !== 0) {
238-
result = this.createOutgoingCalls(response.calls);
239345
}
240346

241347
this.logTelemetry(CallHierarchyCallsFromEvent, CallHierarchyRequestStatus.Succeeded);
242-
return result;
243-
}
244-
245-
private makeVscodeCallHierarchyItem(item: CallHierarchyItem): vscode.CallHierarchyItem {
246-
const containerDetail: string = (item.detail !== "") ? `${item.detail} - ` : "";
247-
const itemUri: vscode.Uri = vscode.Uri.file(item.file);
248-
249-
// Get file detail
250-
const isInWorkspace: boolean = this.client.RootUri !== undefined &&
251-
itemUri.fsPath.startsWith(this.client.RootUri?.fsPath);
252-
const dirPath: string = isInWorkspace ?
253-
path.relative(this.client.RootPath, path.dirname(item.file)) : path.dirname(item.file);
254-
const fileDetail: string = dirPath.length === 0 ?
255-
`${path.basename(item.file)}` : `${path.basename(item.file)} (${dirPath})`;
256-
257-
return new vscode.CallHierarchyItem(
258-
item.kind,
259-
item.name,
260-
containerDetail + fileDetail,
261-
itemUri,
262-
makeVscodeRange(item.range),
263-
makeVscodeRange(item.selectionRange));
264-
}
265-
266-
private createIncomingCalls(calls: CallHierarchyCallsItem[]): vscode.CallHierarchyIncomingCall[] {
267-
const result: vscode.CallHierarchyIncomingCall[] = [];
268-
269-
for (const call of calls) {
270-
const item: vscode.CallHierarchyItem = this.makeVscodeCallHierarchyItem(call.item);
271-
const ranges: vscode.Range[] = [];
272-
call.fromRanges.forEach(r => {
273-
ranges.push(makeVscodeRange(r));
274-
});
275-
276-
const incomingCall: vscode.CallHierarchyIncomingCall =
277-
new vscode.CallHierarchyIncomingCall(item, ranges);
278-
result.push(incomingCall);
279-
}
280-
281-
return result;
282-
}
283-
284-
private createOutgoingCalls(calls: CallHierarchyCallsItem[]): vscode.CallHierarchyOutgoingCall[] {
285-
const result: vscode.CallHierarchyOutgoingCall[] = [];
286-
287-
for (const call of calls) {
288-
const item: vscode.CallHierarchyItem = this.makeVscodeCallHierarchyItem(call.item);
289-
const ranges: vscode.Range[] = [];
290-
call.fromRanges.forEach(r => {
291-
ranges.push(makeVscodeRange(r));
292-
});
293-
294-
const outgoingCall: vscode.CallHierarchyOutgoingCall =
295-
new vscode.CallHierarchyOutgoingCall(item, ranges);
296-
result.push(outgoingCall);
297-
}
298-
299-
return result;
348+
return result.result;
300349
}
301350

302351
private logTelemetry(eventName: string, requestStatus: CallHierarchyRequestStatus, progressBarDuration?: number): void {

0 commit comments

Comments
 (0)