From 4308b3d90308ed858f3252c1cba1b589348bacf0 Mon Sep 17 00:00:00 2001 From: Hanxing Yang Date: Thu, 26 Dec 2024 15:05:29 +0800 Subject: [PATCH] get definition by textDocument/documentLink (#1174) --- .../editor/code-editor/code-editor.ts | 69 +++++++------------ .../components/editor/code-editor/common.ts | 6 ++ .../editor/code-editor/lsp/index.ts | 39 +++++++++++ .../editor/code-editor/lsp/spxls/methods.ts | 17 ++++- .../ui/resource-reference/index.ts | 8 +-- 5 files changed, 87 insertions(+), 52 deletions(-) diff --git a/spx-gui/src/components/editor/code-editor/code-editor.ts b/spx-gui/src/components/editor/code-editor/code-editor.ts index 4e83bf98..bffad80b 100644 --- a/spx-gui/src/components/editor/code-editor/code-editor.ts +++ b/spx-gui/src/components/editor/code-editor/code-editor.ts @@ -13,11 +13,9 @@ import { type DiagnosticsContext, type IDiagnosticsProvider, type IResourceReferencesProvider, - type ResourceReference, type ResourceReferencesContext, builtInCommandCopilotExplain, ChatExplainKind, - type ChatExplainTargetCodeSegment, builtInCommandCopilotReview, builtInCommandGoToDefinition, type HoverContext, @@ -44,9 +42,9 @@ import { type Range, type TextDocumentIdentifier, type ResourceIdentifier, + type ResourceReference, fromLSPTextEdit, textDocumentId2ResourceModelId, - parseDefinitionId, type Position, type Selection, type CommandArgs, @@ -55,7 +53,6 @@ import { } from './common' import * as spxDocumentationItems from './document-base/spx' import * as gopDocumentationItems from './document-base/gop' -import { isDocumentLinkForResourceReference } from './lsp/spxls/methods' import { TextDocument, createTextDocument } from './text-document' import { type Monaco } from './monaco' @@ -67,7 +64,7 @@ const allItems = Object.values({ class ResourceReferencesProvider extends Emitter<{ - didChangeResourceReferences: [] + didChangeResourceReferences: [] // TODO }> implements IResourceReferencesProvider { @@ -75,20 +72,7 @@ class ResourceReferencesProvider super() } async provideResourceReferences(ctx: ResourceReferencesContext): Promise { - const result = await this.lspClient.textDocumentDocumentLink({ - textDocument: ctx.textDocument.id - }) - if (result == null) return [] - const rrs: ResourceReference[] = [] - for (const documentLink of result) { - if (!isDocumentLinkForResourceReference(documentLink)) continue - rrs.push({ - kind: documentLink.data.kind, - range: fromLSPRange(documentLink.range), - resource: { uri: documentLink.target } - }) - } - return rrs + return this.lspClient.getResourceReferences(ctx.textDocument.id) } } @@ -160,13 +144,10 @@ class HoverProvider implements IHoverProvider { private documentBase: DocumentBase ) {} - private async getExplainAction(lspHover: lsp.Hover) { + private async getExplainAction(textDocument: TextDocumentIdentifier, position: Position) { let definition: DefinitionDocumentationItem | null = null - if (!lsp.MarkupContent.is(lspHover.contents)) return null - // TODO: get definition ID from LS `textDocument/documentLink` - const matched = lspHover.contents.value.match(/def-id="([^"]+)"/) - if (matched == null) return null - const defId = parseDefinitionId(matched[1]) + const defId = await this.lspClient.getDefinition(textDocument, position) + if (defId == null) return null definition = await this.documentBase.getDocumentation(defId) if (definition == null) return null return { @@ -236,7 +217,7 @@ class HoverProvider implements IHoverProvider { let range: Range | undefined = undefined if (lspHover.range != null) range = fromLSPRange(lspHover.range) const maybeActions = await Promise.all([ - this.getExplainAction(lspHover), + this.getExplainAction(ctx.textDocument.id, position), this.getGoToDefinitionAction(position, lspParams), this.getRenameAction(ctx, position, lspParams) ]) @@ -246,25 +227,25 @@ class HoverProvider implements IHoverProvider { } class ContextMenuProvider implements IContextMenuProvider { - constructor(private lspClient: SpxLSPClient) {} - - private getExplainMenuItemForPosition({ textDocument }: ContextMenuContext, position: Position) { - const word = textDocument.getWordAtPosition(position) - if (word == null) return null - const wordStart = { ...position, column: word.startColumn } - const wordEnd = { ...position, column: word.endColumn } - const explainTarget: ChatExplainTargetCodeSegment = { - kind: ChatExplainKind.CodeSegment, - codeSegment: { - // TODO: use definition info from LS and explain definition instead of code-segment - textDocument: textDocument.id, - range: { start: wordStart, end: wordEnd }, - content: word.word - } - } + constructor( + private lspClient: SpxLSPClient, + private documentBase: DocumentBase + ) {} + + private async getExplainMenuItemForPosition({ textDocument }: ContextMenuContext, position: Position) { + const defId = await this.lspClient.getDefinition(textDocument.id, position) + if (defId == null) return null + const definition = await this.documentBase.getDocumentation(defId) + if (definition == null) return null return { command: builtInCommandCopilotExplain, - arguments: [explainTarget] satisfies CommandArgs + arguments: [ + { + kind: ChatExplainKind.Definition, + overview: definition.overview, + definition: definition.definition + } + ] satisfies CommandArgs } } @@ -474,7 +455,7 @@ export class CodeEditor extends Disposable { } }) - ui.registerContextMenuProvider(new ContextMenuProvider(this.lspClient)) + ui.registerContextMenuProvider(new ContextMenuProvider(lspClient, documentBase)) ui.registerCopilot(copilot) ui.registerDiagnosticsProvider(this.diagnosticsProvider) ui.registerHoverProvider(this.hoverProvider) diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts index f37c2b67..accabca7 100644 --- a/spx-gui/src/components/editor/code-editor/common.ts +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -54,6 +54,12 @@ export type ResourceIdentifier = { uri: ResourceURI } +export type ResourceReference = { + kind: ResourceReferenceKind + range: Range + resource: ResourceIdentifier +} + export type TextDocumentIdentifier = { /** * URI of the text document. Examples: diff --git a/spx-gui/src/components/editor/code-editor/lsp/index.ts b/spx-gui/src/components/editor/code-editor/lsp/index.ts index 345ab58f..f6c74619 100644 --- a/spx-gui/src/components/editor/code-editor/lsp/index.ts +++ b/spx-gui/src/components/editor/code-editor/lsp/index.ts @@ -7,9 +7,18 @@ import { toText } from '@/models/common/file' import type { Project } from '@/models/project' import wasmExecScriptUrl from '@/assets/wasm_exec.js?url' import spxlsWasmUrl from '@/assets/spxls.wasm?url' +import { + fromLSPRange, + type DefinitionIdentifier, + type Position, + type ResourceReference, + type TextDocumentIdentifier, + containsPosition +} from '../common' import { Spxlc } from './spxls/client' import type { Files as SpxlsFiles } from './spxls' import { spxGetDefinitions, spxRenameResources } from './spxls/commands' +import { isDocumentLinkForResourceReference, parseDocumentLinkForDefinition } from './spxls/methods' function loadScript(url: string) { return new Promise((resolve, reject) => { @@ -142,4 +151,34 @@ export class SpxLSPClient extends Disposable { const spxlc = await this.prepareRequest() return spxlc.request(lsp.DocumentFormattingRequest.method, params) } + + // Higher-level APIs + + async getResourceReferences(textDocument: TextDocumentIdentifier): Promise { + const documentLinks = await this.textDocumentDocumentLink({ textDocument }) + if (documentLinks == null) return [] + const rrs: ResourceReference[] = [] + for (const documentLink of documentLinks) { + if (!isDocumentLinkForResourceReference(documentLink)) continue + rrs.push({ + kind: documentLink.data.kind, + range: fromLSPRange(documentLink.range), + resource: { uri: documentLink.target } + }) + } + return rrs + } + + async getDefinition(textDocument: TextDocumentIdentifier, position: Position): Promise { + const documentLinks = await this.textDocumentDocumentLink({ textDocument }) + if (documentLinks == null) return null + for (const documentLink of documentLinks) { + const definition = parseDocumentLinkForDefinition(documentLink) + if (definition == null) continue + const range = fromLSPRange(documentLink.range) + if (!containsPosition(range, position)) continue + return definition + } + return null + } } diff --git a/spx-gui/src/components/editor/code-editor/lsp/spxls/methods.ts b/spx-gui/src/components/editor/code-editor/lsp/spxls/methods.ts index 2e3d2547..01eeed89 100644 --- a/spx-gui/src/components/editor/code-editor/lsp/spxls/methods.ts +++ b/spx-gui/src/components/editor/code-editor/lsp/spxls/methods.ts @@ -1,5 +1,5 @@ import type * as lsp from 'vscode-languageserver-protocol' -import { isResourceUri, type ResourceReferenceKind } from '../../common' +import { isResourceUri, parseDefinitionId, type DefinitionIdentifier, type ResourceReferenceKind } from '../../common' export type DocumentLinkForResourceReference = { range: lsp.Range @@ -13,3 +13,18 @@ export type DocumentLinkForResourceReference = { export function isDocumentLinkForResourceReference(link: lsp.DocumentLink): link is DocumentLinkForResourceReference { return link.target != null && isResourceUri(link.target) } + +export type DocumentLinkForDefinition = { + range: lsp.Range + /** Definition identifier string */ + target: string +} + +export function parseDocumentLinkForDefinition(link: lsp.DocumentLink): DefinitionIdentifier | null { + if (link.target == null) return null + try { + return parseDefinitionId(link.target) + } catch { + return null + } +} diff --git a/spx-gui/src/components/editor/code-editor/ui/resource-reference/index.ts b/spx-gui/src/components/editor/code-editor/ui/resource-reference/index.ts index a828f2ea..f0d6e1fe 100644 --- a/spx-gui/src/components/editor/code-editor/ui/resource-reference/index.ts +++ b/spx-gui/src/components/editor/code-editor/ui/resource-reference/index.ts @@ -1,6 +1,6 @@ import { computed, shallowRef, watch } from 'vue' import Emitter from '@/utils/emitter' -import { ResourceReferenceKind, type BaseContext, type Range, type ResourceIdentifier } from '../../common' +import { ResourceReferenceKind, type BaseContext, type ResourceReference } from '../../common' import type { TextDocument } from '../../text-document' import type { monaco } from '../../monaco' import { toMonacoPosition, toMonacoRange } from '../common' @@ -11,12 +11,6 @@ import { createResourceSelector } from './selector' export type ResourceReferencesContext = BaseContext -export type ResourceReference = { - kind: ResourceReferenceKind - range: Range - resource: ResourceIdentifier -} - export type InternalResourceReference = ResourceReference & { id: string }