Skip to content

Commit

Permalink
feat: add call hierarchy
Browse files Browse the repository at this point in the history
  • Loading branch information
ayame113 committed Jul 17, 2021
1 parent f027725 commit 6840575
Show file tree
Hide file tree
Showing 6 changed files with 560 additions and 30 deletions.
59 changes: 30 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,36 @@ This npm package can be used by Atom package authors wanting to integrate LSP-co

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;

| Capability | Atom interface |
| ------------------------------- | --------------------------- |
| window/showMessage | Notifications package |
| window/showMessageRequest | Notifications package |
| window/logMessage | Atom-IDE console |
| telemetry/event | Ignored |
| workspace/didChangeWatchedFiles | Atom file watch API |
| textDocument/publishDiagnostics | Linter v2 push/indie |
| textDocument/completion | AutoComplete+ |
| completionItem/resolve | AutoComplete+ (Atom 1.24+) |
| textDocument/hover | Atom-IDE data tips |
| textDocument/signatureHelp | Atom-IDE signature help |
| textDocument/definition | Atom-IDE definitions |
| textDocument/findReferences | Atom-IDE findReferences |
| textDocument/documentHighlight | Atom-IDE code highlights |
| textDocument/documentSymbol | Atom-IDE outline view |
| workspace/symbol | TBD |
| textDocument/codeAction | Atom-IDE code actions |
| textDocument/codeLens | TBD |
| textDocument/formatting | Format File command |
| textDocument/rangeFormatting | Format Selection command |
| textDocument/onTypeFormatting | Atom-IDE on type formatting |
| textDocument/onSaveFormatting | Atom-IDE on save formatting |
| textDocument/rename | TBD |
| textDocument/didChange | Send on save |
| textDocument/didOpen | Send on open |
| textDocument/didSave | Send after save |
| textDocument/willSave | Send before save |
| textDocument/didClose | Send on close |
| Capability | Atom interface |
| --------------------------------- | --------------------------- |
| window/showMessage | Notifications package |
| window/showMessageRequest | Notifications package |
| window/logMessage | Atom-IDE console |
| telemetry/event | Ignored |
| workspace/didChangeWatchedFiles | Atom file watch API |
| textDocument/publishDiagnostics | Linter v2 push/indie |
| textDocument/completion | AutoComplete+ |
| completionItem/resolve | AutoComplete+ (Atom 1.24+) |
| textDocument/hover | Atom-IDE data tips |
| textDocument/signatureHelp | Atom-IDE signature help |
| textDocument/definition | Atom-IDE definitions |
| textDocument/findReferences | Atom-IDE findReferences |
| textDocument/documentHighlight | Atom-IDE code highlights |
| textDocument/documentSymbol | Atom-IDE outline view |
| workspace/symbol | TBD |
| textDocument/codeAction | Atom-IDE code actions |
| textDocument/codeLens | TBD |
| textDocument/formatting | Format File command |
| textDocument/rangeFormatting | Format Selection command |
| textDocument/onTypeFormatting | Atom-IDE on type formatting |
| textDocument/onSaveFormatting | Atom-IDE on save formatting |
| textDocument/prepareCallHierarchy | Atom-IDE outline view |
| textDocument/rename | TBD |
| textDocument/didChange | Send on save |
| textDocument/didOpen | Send on open |
| textDocument/didSave | Send after save |
| textDocument/willSave | Send before save |
| textDocument/didClose | Send on close |

## Developing packages

Expand Down
145 changes: 145 additions & 0 deletions lib/adapters/call-hierarchy-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type * as atomIde from "atom-ide-base"
import Convert from "../convert"
import * as Utils from "../utils"
import { SymbolTag } from "../languageclient"
import type { LanguageClientConnection, ServerCapabilities, CallHierarchyItem } from "../languageclient"
import type { CancellationTokenSource } from "vscode-jsonrpc"
import type { Point, TextEditor } from "atom"

import OutlineViewAdapter from "./outline-view-adapter"

/** Public: Adapts the documentSymbolProvider of the language server to the Outline View supplied by Atom IDE UI. */
export default class CallHierarchyAdapter {
private _cancellationTokens: WeakMap<LanguageClientConnection, CancellationTokenSource> = new WeakMap()

/**
* Public: Determine whether this adapter can be used to adapt a language server based on the serverCapabilities
* matrix containing a callHierarchyProvider.
*
* @param serverCapabilities The {ServerCapabilities} of the language server to consider.
* @returns A {Boolean} indicating adapter can adapt the server based on the given serverCapabilities.
*/
public static canAdapt(serverCapabilities: ServerCapabilities): boolean {
return !!serverCapabilities.callHierarchyProvider
}

/** Corresponds to lsp's CallHierarchyPrepareRequest */

/**
* Public: Obtain the relationship between calling and called functions hierarchically. Corresponds to lsp's
* CallHierarchyPrepareRequest.
*
* @param connection A {LanguageClientConnection} to the language server that provides highlights.
* @param editor The Atom {TextEditor} containing the text associated with the calling.
* @param position The Atom {Point} associated with the calling.
* @param type The hierarchy type either incoming or outgoing.
* @returns A {Promise} of an {CallHierarchy}.
*/
async getCallHierarchy<T extends atomIde.CallHierarchyType>(
connection: LanguageClientConnection,
editor: TextEditor,
point: Point,
type: T
): Promise<atomIde.CallHierarchy<T>> {
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (cancellationToken) =>
connection.prepareCallHierarchy(
{
textDocument: Convert.editorToTextDocumentIdentifier(editor),
position: Convert.pointToPosition(point),
},
cancellationToken
)
)
return <CallHierarchyForAdapter<T>>{
type,
data: results?.map(convertCallHierarchyItem) ?? [],
itemAt(n: number) {
if (type === "incoming") {
return <Promise<atomIde.CallHierarchy<T>>>this.adapter.getIncoming(this.connection, this.data[n].rawData)
} else {
return <Promise<atomIde.CallHierarchy<T>>>this.adapter.getOutgoing(this.connection, this.data[n].rawData)
}
},
connection,
adapter: this,
}
}
/** Corresponds to lsp's CallHierarchyIncomingCallsRequest. */
async getIncoming(
connection: LanguageClientConnection,
item: CallHierarchyItem
): Promise<atomIde.CallHierarchy<"incoming">> {
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (_cancellationToken) =>
connection.callHierarchyIncomingCalls({ item })
)
return <CallHierarchyForAdapter<"incoming">>{
type: "incoming",
data: results?.map?.((l) => convertCallHierarchyItem(l.from)) || [],
itemAt(n: number) {
return this.adapter.getIncoming(this.connection, this.data[n].rawData)
},
connection,
adapter: this,
}
}
/** Corresponds to lsp's CallHierarchyOutgoingCallsRequest. */
async getOutgoing(
connection: LanguageClientConnection,
item: CallHierarchyItem
): Promise<atomIde.CallHierarchy<"outgoing">> {
const results = await Utils.doWithCancellationToken(connection, this._cancellationTokens, (_cancellationToken) =>
connection.callHierarchyOutgoingCalls({ item })
)
return <CallHierarchyForAdapter<"outgoing">>{
type: "outgoing",
data: results?.map((l) => convertCallHierarchyItem(l.to)) || [],
itemAt(n: number) {
return this.adapter.getOutgoing(this.connection, this.data[n].rawData)
},
connection,
adapter: this,
}
}
}

function convertCallHierarchyItem(rawData: CallHierarchyItem): CallHierarchyItemForAdapter {
return {
path: Convert.uriToPath(rawData.uri),
name: rawData.name,
icon: OutlineViewAdapter.symbolKindToEntityKind(rawData.kind) ?? undefined,
tags: rawData.tags
? [
...rawData.tags.reduce((set, tag) => {
// filter out null and remove duplicates
const entity = symbolTagToEntityKind(tag)
return entity == null ? set : set.add(entity)
}, new Set<atomIde.SymbolTagKind>()),
]
: [],
detail: rawData.detail,
range: Convert.lsRangeToAtomRange(rawData.range),
selectionRange: Convert.lsRangeToAtomRange(rawData.selectionRange),
rawData,
}
}

function symbolTagToEntityKind(symbol: number): atomIde.SymbolTagKind | null {
switch (symbol) {
case SymbolTag.Deprecated:
return "deprecated"
default:
return null
}
}

/** Extend CallHierarchy to include properties used inside the adapter */
interface CallHierarchyForAdapter<T extends atomIde.CallHierarchyType> extends atomIde.CallHierarchy<T> {
data: CallHierarchyItemForAdapter[]
adapter: CallHierarchyAdapter
connection: LanguageClientConnection
}

/** Extend CallHierarchyItem to include properties used inside the adapter */
interface CallHierarchyItemForAdapter extends atomIde.CallHierarchyItem {
rawData: CallHierarchyItem
}
41 changes: 40 additions & 1 deletion lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as linter from "atom/linter"
import Convert from "./convert.js"
import ApplyEditAdapter from "./adapters/apply-edit-adapter"
import AutocompleteAdapter, { grammarScopeToAutoCompleteSelector } from "./adapters/autocomplete-adapter"
import CallHierarchyAdapter from "./adapters/call-hierarchy-adapter"
import CodeActionAdapter from "./adapters/code-action-adapter"
import CodeFormatAdapter from "./adapters/code-format-adapter"
import CodeHighlightAdapter from "./adapters/code-highlight-adapter"
Expand Down Expand Up @@ -75,6 +76,7 @@ export default class AutoLanguageClient {

// Shared adapters that can take the RPC connection as required
protected autoComplete?: AutocompleteAdapter
protected callHierarchy?: CallHierarchyAdapter
protected datatip?: DatatipAdapter
protected definitions?: DefinitionAdapter
protected findReferences?: FindReferencesAdapter
Expand Down Expand Up @@ -247,13 +249,15 @@ export default class AutoLanguageClient {
codeDescriptionSupport: true,
dataSupport: true,
},
callHierarchy: {
dynamicRegistration: false,
},
implementation: undefined,
typeDefinition: undefined,
colorProvider: undefined,
foldingRange: undefined,
selectionRange: undefined,
linkedEditingRange: undefined,
callHierarchy: undefined,
semanticTokens: undefined,
},
general: {
Expand Down Expand Up @@ -745,6 +749,41 @@ export default class AutoLanguageClient {
return this.outlineView.getOutline(server.connection, editor)
}

// Call Hierarchy View via LS callHierarchy---------------------------------
public provideCallHierarchy(): atomIde.CallHierarchyProvider {
return {
name: this.name,
grammarScopes: this.getGrammarScopes(),
priority: 1,
getIncomingCallHierarchy: this.getIncomingCallHierarchy.bind(this),
getOutgoingCallHierarchy: this.getOutgoingCallHierarchy.bind(this),
}
}

protected async getIncomingCallHierarchy(
editor: TextEditor,
point: Point
): Promise<atomIde.CallHierarchy<"incoming"> | null> {
const server = await this._serverManager.getServer(editor)
if (server == null || !CallHierarchyAdapter.canAdapt(server.capabilities)) {
return null
}
this.callHierarchy = this.callHierarchy || new CallHierarchyAdapter()
return this.callHierarchy.getCallHierarchy(server.connection, editor, point, "incoming")
}

protected async getOutgoingCallHierarchy(
editor: TextEditor,
point: Point
): Promise<atomIde.CallHierarchy<"outgoing"> | null> {
const server = await this._serverManager.getServer(editor)
if (server == null || !CallHierarchyAdapter.canAdapt(server.capabilities)) {
return null
}
this.callHierarchy = this.callHierarchy || new CallHierarchyAdapter()
return this.callHierarchy.getCallHierarchy(server.connection, editor, point, "outgoing")
}

// Linter push v2 API via LS publishDiagnostics
public consumeLinterV2(registerIndie: (params: { name: string }) => linter.IndieDelegate): void {
this._linterDelegate = registerIndie({ name: this.name })
Expand Down
45 changes: 45 additions & 0 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,51 @@ export class LanguageClientConnection extends EventEmitter {
return this._sendRequest(lsp.ExecuteCommandRequest.type, params)
}

/**
* Public: Send a `textDocument/prepareCallHierarchy` request.
*
* @param params The {CallHierarchyIncomingCallsParams} that containing {textDocument} and {position} associated with
* the calling.
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
* @returns A {Promise} containing an {Array} of {CallHierarchyItem}s that corresponding to the request.
*/
public prepareCallHierarchy(
params: lsp.CallHierarchyPrepareParams,
_cancellationToken?: jsonrpc.CancellationToken
): Promise<lsp.CallHierarchyItem[] | null> {
return this._sendRequest(lsp.CallHierarchyPrepareRequest.type, params)
}

/**
* Public: Send a `callHierarchy/incomingCalls` request.
*
* @param params The {CallHierarchyIncomingCallsParams} that identifies {CallHierarchyItem} to get incoming calls.
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
* @returns A {Promise} containing an {Array} of {CallHierarchyIncomingCall}s for the function that called by the
* function given to the parameter.
*/
public callHierarchyIncomingCalls(
params: lsp.CallHierarchyIncomingCallsParams,
_cancellationToken?: jsonrpc.CancellationToken
): Promise<lsp.CallHierarchyIncomingCall[] | null> {
return this._sendRequest(lsp.CallHierarchyIncomingCallsRequest.type, params)
}

/**
* Public: Send a `callHierarchy/outgoingCalls` request.
*
* @param params The {CallHierarchyOutgoingCallsParams} that identifies {CallHierarchyItem} to get outgoing calls.
* @param cancellationToken The {CancellationToken} that is used to cancel this request if necessary.
* @returns A {Promise} containing an {Array} of {CallHierarchyIncomingCall}s for the function that calls the function
* given to the parameter.
*/
public callHierarchyOutgoingCalls(
params: lsp.CallHierarchyOutgoingCallsParams,
_cancellationToken?: jsonrpc.CancellationToken
): Promise<lsp.CallHierarchyOutgoingCall[] | null> {
return this._sendRequest(lsp.CallHierarchyOutgoingCallsRequest.type, params)
}

private _onRequest<T extends Extract<keyof KnownRequests, string>>(
type: { method: T },
callback: RequestCallback<T>
Expand Down
Loading

0 comments on commit 6840575

Please sign in to comment.