Skip to content

Commit 015208e

Browse files
authored
Merge pull request #172 from ayame113/add-call-hierarchy
2 parents e260db3 + 48b62a5 commit 015208e

File tree

6 files changed

+679
-30
lines changed

6 files changed

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

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 * as 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?: typeof 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 ?? 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 ?? 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)