Skip to content

Commit 6840575

Browse files
committed
feat: add call hierarchy
1 parent f027725 commit 6840575

File tree

6 files changed

+560
-30
lines changed

6 files changed

+560
-30
lines changed

README.md

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,36 @@ This npm package can be used by Atom package authors wanting to integrate LSP-co
2525

2626
The language server protocol consists of a number of capabilities. Some of these already have a counterpoint we can connect up to today while others do not. The following table shows each capability in v2 and how it is exposed via Atom;
2727

28-
| Capability | Atom interface |
29-
| ------------------------------- | --------------------------- |
30-
| window/showMessage | Notifications package |
31-
| window/showMessageRequest | Notifications package |
32-
| window/logMessage | Atom-IDE console |
33-
| telemetry/event | Ignored |
34-
| workspace/didChangeWatchedFiles | Atom file watch API |
35-
| textDocument/publishDiagnostics | Linter v2 push/indie |
36-
| textDocument/completion | AutoComplete+ |
37-
| completionItem/resolve | AutoComplete+ (Atom 1.24+) |
38-
| textDocument/hover | Atom-IDE data tips |
39-
| textDocument/signatureHelp | Atom-IDE signature help |
40-
| textDocument/definition | Atom-IDE definitions |
41-
| textDocument/findReferences | Atom-IDE findReferences |
42-
| textDocument/documentHighlight | Atom-IDE code highlights |
43-
| textDocument/documentSymbol | Atom-IDE outline view |
44-
| workspace/symbol | TBD |
45-
| textDocument/codeAction | Atom-IDE code actions |
46-
| textDocument/codeLens | TBD |
47-
| textDocument/formatting | Format File command |
48-
| textDocument/rangeFormatting | Format Selection command |
49-
| textDocument/onTypeFormatting | Atom-IDE on type formatting |
50-
| textDocument/onSaveFormatting | Atom-IDE on save formatting |
51-
| textDocument/rename | TBD |
52-
| textDocument/didChange | Send on save |
53-
| textDocument/didOpen | Send on open |
54-
| textDocument/didSave | Send after save |
55-
| textDocument/willSave | Send before save |
56-
| textDocument/didClose | Send on close |
28+
| Capability | Atom interface |
29+
| --------------------------------- | --------------------------- |
30+
| window/showMessage | Notifications package |
31+
| window/showMessageRequest | Notifications package |
32+
| window/logMessage | Atom-IDE console |
33+
| telemetry/event | Ignored |
34+
| workspace/didChangeWatchedFiles | Atom file watch API |
35+
| textDocument/publishDiagnostics | Linter v2 push/indie |
36+
| textDocument/completion | AutoComplete+ |
37+
| completionItem/resolve | AutoComplete+ (Atom 1.24+) |
38+
| textDocument/hover | Atom-IDE data tips |
39+
| textDocument/signatureHelp | Atom-IDE signature help |
40+
| textDocument/definition | Atom-IDE definitions |
41+
| textDocument/findReferences | Atom-IDE findReferences |
42+
| textDocument/documentHighlight | Atom-IDE code highlights |
43+
| textDocument/documentSymbol | Atom-IDE outline view |
44+
| workspace/symbol | TBD |
45+
| textDocument/codeAction | Atom-IDE code actions |
46+
| textDocument/codeLens | TBD |
47+
| textDocument/formatting | Format File command |
48+
| textDocument/rangeFormatting | Format Selection command |
49+
| textDocument/onTypeFormatting | Atom-IDE on type formatting |
50+
| textDocument/onSaveFormatting | Atom-IDE on save formatting |
51+
| textDocument/prepareCallHierarchy | Atom-IDE outline view |
52+
| textDocument/rename | TBD |
53+
| textDocument/didChange | Send on save |
54+
| textDocument/didOpen | Send on open |
55+
| textDocument/didSave | Send after save |
56+
| textDocument/willSave | Send before save |
57+
| textDocument/didClose | Send on close |
5758

5859
## Developing packages
5960

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type * as atomIde from "atom-ide-base"
2+
import Convert from "../convert"
3+
import * as Utils from "../utils"
4+
import { SymbolTag } from "../languageclient"
5+
import type { LanguageClientConnection, ServerCapabilities, CallHierarchyItem } from "../languageclient"
6+
import type { CancellationTokenSource } from "vscode-jsonrpc"
7+
import type { Point, TextEditor } from "atom"
8+
9+
import OutlineViewAdapter from "./outline-view-adapter"
10+
11+
/** Public: Adapts the documentSymbolProvider of the language server to the Outline View supplied by Atom IDE UI. */
12+
export default class CallHierarchyAdapter {
13+
private _cancellationTokens: WeakMap<LanguageClientConnection, CancellationTokenSource> = new WeakMap()
14+
15+
/**
16+
* Public: Determine whether this adapter can be used to adapt a language server based on the serverCapabilities
17+
* matrix containing a callHierarchyProvider.
18+
*
19+
* @param serverCapabilities The {ServerCapabilities} of the language server to consider.
20+
* @returns A {Boolean} indicating adapter can adapt the server based on the given serverCapabilities.
21+
*/
22+
public static canAdapt(serverCapabilities: ServerCapabilities): boolean {
23+
return !!serverCapabilities.callHierarchyProvider
24+
}
25+
26+
/** Corresponds to lsp's CallHierarchyPrepareRequest */
27+
28+
/**
29+
* Public: Obtain the relationship between calling and called functions hierarchically. Corresponds to lsp's
30+
* CallHierarchyPrepareRequest.
31+
*
32+
* @param connection A {LanguageClientConnection} to the language server that provides highlights.
33+
* @param editor The Atom {TextEditor} containing the text associated with the calling.
34+
* @param position The Atom {Point} associated with the calling.
35+
* @param type The hierarchy type either incoming or outgoing.
36+
* @returns A {Promise} of an {CallHierarchy}.
37+
*/
38+
async getCallHierarchy<T extends atomIde.CallHierarchyType>(
39+
connection: LanguageClientConnection,
40+
editor: TextEditor,
41+
point: Point,
42+
type: T
43+
): Promise<atomIde.CallHierarchy<T>> {
44+
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (cancellationToken) =>
45+
connection.prepareCallHierarchy(
46+
{
47+
textDocument: Convert.editorToTextDocumentIdentifier(editor),
48+
position: Convert.pointToPosition(point),
49+
},
50+
cancellationToken
51+
)
52+
)
53+
return <CallHierarchyForAdapter<T>>{
54+
type,
55+
data: results?.map(convertCallHierarchyItem) ?? [],
56+
itemAt(n: number) {
57+
if (type === "incoming") {
58+
return <Promise<atomIde.CallHierarchy<T>>>this.adapter.getIncoming(this.connection, this.data[n].rawData)
59+
} else {
60+
return <Promise<atomIde.CallHierarchy<T>>>this.adapter.getOutgoing(this.connection, this.data[n].rawData)
61+
}
62+
},
63+
connection,
64+
adapter: this,
65+
}
66+
}
67+
/** Corresponds to lsp's CallHierarchyIncomingCallsRequest. */
68+
async getIncoming(
69+
connection: LanguageClientConnection,
70+
item: CallHierarchyItem
71+
): Promise<atomIde.CallHierarchy<"incoming">> {
72+
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (_cancellationToken) =>
73+
connection.callHierarchyIncomingCalls({ item })
74+
)
75+
return <CallHierarchyForAdapter<"incoming">>{
76+
type: "incoming",
77+
data: results?.map?.((l) => convertCallHierarchyItem(l.from)) || [],
78+
itemAt(n: number) {
79+
return this.adapter.getIncoming(this.connection, this.data[n].rawData)
80+
},
81+
connection,
82+
adapter: this,
83+
}
84+
}
85+
/** Corresponds to lsp's CallHierarchyOutgoingCallsRequest. */
86+
async getOutgoing(
87+
connection: LanguageClientConnection,
88+
item: CallHierarchyItem
89+
): Promise<atomIde.CallHierarchy<"outgoing">> {
90+
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (_cancellationToken) =>
91+
connection.callHierarchyOutgoingCalls({ item })
92+
)
93+
return <CallHierarchyForAdapter<"outgoing">>{
94+
type: "outgoing",
95+
data: results?.map((l) => convertCallHierarchyItem(l.to)) || [],
96+
itemAt(n: number) {
97+
return this.adapter.getOutgoing(this.connection, this.data[n].rawData)
98+
},
99+
connection,
100+
adapter: this,
101+
}
102+
}
103+
}
104+
105+
function convertCallHierarchyItem(rawData: CallHierarchyItem): CallHierarchyItemForAdapter {
106+
return {
107+
path: Convert.uriToPath(rawData.uri),
108+
name: rawData.name,
109+
icon: OutlineViewAdapter.symbolKindToEntityKind(rawData.kind) ?? undefined,
110+
tags: rawData.tags
111+
? [
112+
...rawData.tags.reduce((set, tag) => {
113+
// filter out null and remove duplicates
114+
const entity = symbolTagToEntityKind(tag)
115+
return entity == null ? set : set.add(entity)
116+
}, new Set<atomIde.SymbolTagKind>()),
117+
]
118+
: [],
119+
detail: rawData.detail,
120+
range: Convert.lsRangeToAtomRange(rawData.range),
121+
selectionRange: Convert.lsRangeToAtomRange(rawData.selectionRange),
122+
rawData,
123+
}
124+
}
125+
126+
function symbolTagToEntityKind(symbol: number): atomIde.SymbolTagKind | null {
127+
switch (symbol) {
128+
case SymbolTag.Deprecated:
129+
return "deprecated"
130+
default:
131+
return null
132+
}
133+
}
134+
135+
/** Extend CallHierarchy to include properties used inside the adapter */
136+
interface CallHierarchyForAdapter<T extends atomIde.CallHierarchyType> extends atomIde.CallHierarchy<T> {
137+
data: CallHierarchyItemForAdapter[]
138+
adapter: CallHierarchyAdapter
139+
connection: LanguageClientConnection
140+
}
141+
142+
/** Extend CallHierarchyItem to include properties used inside the adapter */
143+
interface CallHierarchyItemForAdapter extends atomIde.CallHierarchyItem {
144+
rawData: CallHierarchyItem
145+
}

lib/auto-languageclient.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as linter from "atom/linter"
88
import Convert from "./convert.js"
99
import ApplyEditAdapter from "./adapters/apply-edit-adapter"
1010
import AutocompleteAdapter, { grammarScopeToAutoCompleteSelector } from "./adapters/autocomplete-adapter"
11+
import CallHierarchyAdapter from "./adapters/call-hierarchy-adapter"
1112
import CodeActionAdapter from "./adapters/code-action-adapter"
1213
import CodeFormatAdapter from "./adapters/code-format-adapter"
1314
import CodeHighlightAdapter from "./adapters/code-highlight-adapter"
@@ -75,6 +76,7 @@ export default class AutoLanguageClient {
7576

7677
// Shared adapters that can take the RPC connection as required
7778
protected autoComplete?: AutocompleteAdapter
79+
protected callHierarchy?: CallHierarchyAdapter
7880
protected datatip?: DatatipAdapter
7981
protected definitions?: DefinitionAdapter
8082
protected findReferences?: FindReferencesAdapter
@@ -247,13 +249,15 @@ export default class AutoLanguageClient {
247249
codeDescriptionSupport: true,
248250
dataSupport: true,
249251
},
252+
callHierarchy: {
253+
dynamicRegistration: false,
254+
},
250255
implementation: undefined,
251256
typeDefinition: undefined,
252257
colorProvider: undefined,
253258
foldingRange: undefined,
254259
selectionRange: undefined,
255260
linkedEditingRange: undefined,
256-
callHierarchy: undefined,
257261
semanticTokens: undefined,
258262
},
259263
general: {
@@ -745,6 +749,41 @@ export default class AutoLanguageClient {
745749
return this.outlineView.getOutline(server.connection, editor)
746750
}
747751

752+
// Call Hierarchy View via LS callHierarchy---------------------------------
753+
public provideCallHierarchy(): atomIde.CallHierarchyProvider {
754+
return {
755+
name: this.name,
756+
grammarScopes: this.getGrammarScopes(),
757+
priority: 1,
758+
getIncomingCallHierarchy: this.getIncomingCallHierarchy.bind(this),
759+
getOutgoingCallHierarchy: this.getOutgoingCallHierarchy.bind(this),
760+
}
761+
}
762+
763+
protected async getIncomingCallHierarchy(
764+
editor: TextEditor,
765+
point: Point
766+
): Promise<atomIde.CallHierarchy<"incoming"> | null> {
767+
const server = await this._serverManager.getServer(editor)
768+
if (server == null || !CallHierarchyAdapter.canAdapt(server.capabilities)) {
769+
return null
770+
}
771+
this.callHierarchy = this.callHierarchy || new CallHierarchyAdapter()
772+
return this.callHierarchy.getCallHierarchy(server.connection, editor, point, "incoming")
773+
}
774+
775+
protected async getOutgoingCallHierarchy(
776+
editor: TextEditor,
777+
point: Point
778+
): Promise<atomIde.CallHierarchy<"outgoing"> | null> {
779+
const server = await this._serverManager.getServer(editor)
780+
if (server == null || !CallHierarchyAdapter.canAdapt(server.capabilities)) {
781+
return null
782+
}
783+
this.callHierarchy = this.callHierarchy || new CallHierarchyAdapter()
784+
return this.callHierarchy.getCallHierarchy(server.connection, editor, point, "outgoing")
785+
}
786+
748787
// Linter push v2 API via LS publishDiagnostics
749788
public consumeLinterV2(registerIndie: (params: { name: string }) => linter.IndieDelegate): void {
750789
this._linterDelegate = registerIndie({ name: this.name })

lib/languageclient.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,51 @@ export class LanguageClientConnection extends EventEmitter {
533533
return this._sendRequest(lsp.ExecuteCommandRequest.type, params)
534534
}
535535

536+
/**
537+
* Public: Send a `textDocument/prepareCallHierarchy` request.
538+
*
539+
* @param params The {CallHierarchyIncomingCallsParams} that containing {textDocument} and {position} associated with
540+
* the calling.
541+
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
542+
* @returns A {Promise} containing an {Array} of {CallHierarchyItem}s that corresponding to the request.
543+
*/
544+
public prepareCallHierarchy(
545+
params: lsp.CallHierarchyPrepareParams,
546+
_cancellationToken?: jsonrpc.CancellationToken
547+
): Promise<lsp.CallHierarchyItem[] | null> {
548+
return this._sendRequest(lsp.CallHierarchyPrepareRequest.type, params)
549+
}
550+
551+
/**
552+
* Public: Send a `callHierarchy/incomingCalls` request.
553+
*
554+
* @param params The {CallHierarchyIncomingCallsParams} that identifies {CallHierarchyItem} to get incoming calls.
555+
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
556+
* @returns A {Promise} containing an {Array} of {CallHierarchyIncomingCall}s for the function that called by the
557+
* function given to the parameter.
558+
*/
559+
public callHierarchyIncomingCalls(
560+
params: lsp.CallHierarchyIncomingCallsParams,
561+
_cancellationToken?: jsonrpc.CancellationToken
562+
): Promise<lsp.CallHierarchyIncomingCall[] | null> {
563+
return this._sendRequest(lsp.CallHierarchyIncomingCallsRequest.type, params)
564+
}
565+
566+
/**
567+
* Public: Send a `callHierarchy/outgoingCalls` request.
568+
*
569+
* @param params The {CallHierarchyOutgoingCallsParams} that identifies {CallHierarchyItem} to get outgoing calls.
570+
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
571+
* @returns A {Promise} containing an {Array} of {CallHierarchyIncomingCall}s for the function that calls the function
572+
* given to the parameter.
573+
*/
574+
public callHierarchyOutgoingCalls(
575+
params: lsp.CallHierarchyOutgoingCallsParams,
576+
_cancellationToken?: jsonrpc.CancellationToken
577+
): Promise<lsp.CallHierarchyOutgoingCall[] | null> {
578+
return this._sendRequest(lsp.CallHierarchyOutgoingCallsRequest.type, params)
579+
}
580+
536581
private _onRequest<T extends Extract<keyof KnownRequests, string>>(
537582
type: { method: T },
538583
callback: RequestCallback<T>

0 commit comments

Comments
 (0)