From 8f05015161c451c179ef111582755e9e1e519b9e Mon Sep 17 00:00:00 2001 From: zhanba Date: Fri, 15 Mar 2024 16:53:41 +0800 Subject: [PATCH] feat: go to definition --- .vscode/settings.json | 12 +-- .../src/libro-e2-editor.ts | 4 +- .../libro-core/src/cell/libro-cell-model.ts | 7 +- packages/libro-core/src/libro-model.ts | 3 + packages/libro-core/src/libro-protocol.ts | 3 + .../src/cell/jupyter-code-cell-view.tsx | 7 +- .../libro-jupyter/src/libro-jupyter-model.ts | 14 +--- packages/libro-lab/src/lab-app.ts | 2 +- packages/libro-language-client/package.json | 1 + .../src/common/codeConverter.ts | 76 +++++++++++++++++ .../src/common/vscodeAdaptor/convertor.ts | 9 +- .../common/vscodeAdaptor/monacoLanguages.ts | 20 ++++- packages/libro-language-client/src/index.ts | 1 + .../src/libro-language-client-contribution.ts | 83 +++++++++++++++++-- packages/libro-language-client/src/util.ts | 43 ++++++++++ 15 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 packages/libro-language-client/src/util.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index dc8219284..6acd7c7d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,28 +43,28 @@ "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[typescript]": { "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[javascriptreact]": { "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[javascript]": { "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[json]": { @@ -89,14 +89,14 @@ "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.stylelint": true + "source.fixAll.stylelint": "explicit" } }, "[less]": { "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.stylelint": true + "source.fixAll.stylelint": "explicit" } }, "[css][less]": { diff --git a/packages/libro-cofine-editor/src/libro-e2-editor.ts b/packages/libro-cofine-editor/src/libro-e2-editor.ts index 99dae81c3..7d3431a7b 100644 --- a/packages/libro-cofine-editor/src/libro-e2-editor.ts +++ b/packages/libro-cofine-editor/src/libro-e2-editor.ts @@ -889,8 +889,8 @@ export class LibroE2Editor implements IEditor { }; setCursorPosition = (position: IPosition) => { this.monacoEditor?.setPosition({ - column: position.column, - lineNumber: position.line, + column: position.column + 1, + lineNumber: position.line + 1, }); }; getSelection = () => { diff --git a/packages/libro-core/src/cell/libro-cell-model.ts b/packages/libro-core/src/cell/libro-cell-model.ts index 4cba2b420..150c6963e 100644 --- a/packages/libro-core/src/cell/libro-cell-model.ts +++ b/packages/libro-core/src/cell/libro-cell-model.ts @@ -1,7 +1,7 @@ import { Model } from '@difizen/libro-code-editor'; import type { ICell } from '@difizen/libro-common'; import { concatMultilineString } from '@difizen/libro-common'; -import { DisposableCollection } from '@difizen/mana-app'; +import { DisposableCollection, watch } from '@difizen/mana-app'; import { prop, inject, postConstruct, transient } from '@difizen/mana-app'; import type { DefaultDecodedFormatter } from '../formatter/index.js'; @@ -34,6 +34,8 @@ export class LibroCellModel extends Model implements CellModel { @prop() trusted: boolean; + version = 0; + constructor(@inject(CellOptions) options: CellOptions) { super({ id: options.cell.id as string, @@ -68,6 +70,9 @@ export class LibroCellModel extends Model implements CellModel { this.libroFormatType, formatValue, ); + watch(this, 'value', () => { + this.version++; + }); } get source(): string { diff --git a/packages/libro-core/src/libro-model.ts b/packages/libro-core/src/libro-model.ts index 5d6859401..11f1c77a0 100644 --- a/packages/libro-core/src/libro-model.ts +++ b/packages/libro-core/src/libro-model.ts @@ -78,6 +78,8 @@ export class LibroModel implements NotebookModel, DndListModel { id: string; + version = 0; + /** * The shared notebook model. */ @@ -298,6 +300,7 @@ export class LibroModel implements NotebookModel, DndListModel { } onChange() { + this.version++; this.dirty = true; this.onChangedEmitter.fire(true); } diff --git a/packages/libro-core/src/libro-protocol.ts b/packages/libro-core/src/libro-protocol.ts index 01157bde4..091dd52b6 100644 --- a/packages/libro-core/src/libro-protocol.ts +++ b/packages/libro-core/src/libro-protocol.ts @@ -71,6 +71,7 @@ export interface ICellContentChange { } export interface BaseNotebookModel { id: string; + version: number; /** * The dirty state of the model. * #### Notes @@ -322,6 +323,8 @@ export interface CellModel extends IModel, Disposable { */ id: string; + version: number; + source: string; /** diff --git a/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx b/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx index 928ce5297..5269a0327 100644 --- a/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx +++ b/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx @@ -7,7 +7,7 @@ import type { TooltipProviderOption, } from '@difizen/libro-code-editor'; import { KernelError } from '@difizen/libro-kernel'; -import { LibroCellURIScheme } from '@difizen/libro-language-client'; +import { getCellURI } from '@difizen/libro-language-client'; import { transient, URI } from '@difizen/mana-app'; import { view, ViewInstance } from '@difizen/mana-app'; import { getOrigin, useInject } from '@difizen/mana-app'; @@ -55,9 +55,8 @@ export class JupyterCodeCellView extends LibroCodeCellView { protected override getEditorOption(): CodeEditorViewOptions { const options = super.getEditorOption(); - let uri = new URI(this.parent.model.filePath); - uri = URI.withScheme(uri, LibroCellURIScheme); - uri = URI.withQuery(uri, `cellid=${this.model.id}`); + const uri = getCellURI(this.parent.model, this.model); + return { ...options, uuid: uri.toString(), diff --git a/packages/libro-jupyter/src/libro-jupyter-model.ts b/packages/libro-jupyter/src/libro-jupyter-model.ts index 1c2ea8d65..d9da505c7 100644 --- a/packages/libro-jupyter/src/libro-jupyter-model.ts +++ b/packages/libro-jupyter/src/libro-jupyter-model.ts @@ -2,13 +2,12 @@ import type { VirtualizedManager } from '@difizen/libro-core'; import { LibroModel, VirtualizedManagerHelper } from '@difizen/libro-core'; import { ContentsManager, - ExecutableNotebookModel, isDisplayDataMsg, LibroKernelConnectionManager, ServerConnection, ServerManager, } from '@difizen/libro-kernel'; -import type { IKernelConnection } from '@difizen/libro-kernel'; +import type { IKernelConnection, ExecutableNotebookModel } from '@difizen/libro-kernel'; import type { IContentsCheckpointModel, IContentsModel } from '@difizen/libro-kernel'; import { getOrigin, ModalService, prop } from '@difizen/mana-app'; import { Deferred } from '@difizen/mana-app'; @@ -26,17 +25,6 @@ import { getDefaultKernel } from './utils/index.js'; type IModel = IContentsModel; @transient() export class LibroJupyterModel extends LibroModel implements ExecutableNotebookModel { - static is = (arg: Record | undefined): arg is LibroJupyterModel => { - return ( - !!arg && - ExecutableNotebookModel.is(arg) && - 'kernelConnection' in arg && - typeof (arg as any).kernelConnection === 'object' && - 'lspEnabled' in arg && - typeof (arg as any).lspEnabled === 'boolean' - ); - }; - protected libroFileService: LibroFileService; protected virtualizedManager: VirtualizedManager; diff --git a/packages/libro-lab/src/lab-app.ts b/packages/libro-lab/src/lab-app.ts index d25e84451..9bb9730aa 100644 --- a/packages/libro-lab/src/lab-app.ts +++ b/packages/libro-lab/src/lab-app.ts @@ -33,7 +33,7 @@ export class LibroLabApp implements ApplicationContribution { @inject(LayoutService) layoutService: LayoutService; async onStart() { - localStorage.setItem(ShouldPreventStoreViewKey, 'false'); + localStorage.setItem(ShouldPreventStoreViewKey, 'true'); this.configurationService.set( LibroJupyterConfiguration['OpenSlot'], LibroLabLayoutSlots.content, diff --git a/packages/libro-language-client/package.json b/packages/libro-language-client/package.json index 025c20dd1..35667d445 100644 --- a/packages/libro-language-client/package.json +++ b/packages/libro-language-client/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@difizen/libro-core": "^0.1.23", + "@difizen/libro-code-editor": "^0.1.23", "@difizen/libro-kernel": "^0.1.23", "@difizen/libro-common": "^0.1.23", "@difizen/libro-lsp": "^0.1.23", diff --git a/packages/libro-language-client/src/common/codeConverter.ts b/packages/libro-language-client/src/common/codeConverter.ts index 7ded1b3c7..58ba11b60 100644 --- a/packages/libro-language-client/src/common/codeConverter.ts +++ b/packages/libro-language-client/src/common/codeConverter.ts @@ -4,6 +4,7 @@ * ------------------------------------------------------------------------------------------ */ import * as proto from '@difizen/vscode-languageserver-protocol'; +import type { LocationLink } from 'vscode'; import type { InlineCompletionContext, InlayHint, @@ -46,6 +47,8 @@ import type { TextEdit, Uri, SymbolInformation as VSymbolInformation, + Definition, + DefinitionLink, } from 'vscode'; import ProtocolCallHierarchyItem from './protocolCallHierarchyItem.js'; @@ -172,11 +175,25 @@ export interface Converter { asRanges(values: readonly Range[]): proto.Range[]; + asDefinitionResult(item: Definition): proto.Definition; + asDefinitionResult(item: DefinitionLink[]): proto.Definition; + asDefinitionResult(item: undefined | null): undefined; + asDefinitionResult( + item: Definition | DefinitionLink[] | undefined | null, + ): proto.Definition | undefined; + asDefinitionResult( + item: Definition | DefinitionLink[] | undefined | null, + ): proto.Definition | undefined; + asLocation(value: null): null; asLocation(value: undefined): undefined; asLocation(value: Location): proto.Location; asLocation(value: Location | undefined | null): proto.Location | undefined | null; + asLocationLink(item: undefined | null): undefined; + asLocationLink(item: LocationLink): proto.LocationLink; + asLocationLink(item: LocationLink | undefined | null): proto.LocationLink | undefined; + asDiagnosticSeverity(value: DiagnosticSeverity): number; asDiagnosticTag(value: DiagnosticTag): number | undefined; @@ -623,6 +640,63 @@ export function createConverter(uriConverter?: URIConverter): Converter { return values.map(asRange as (item: Range) => proto.Range); } + function asLocationLink(item: undefined | null): undefined; + function asLocationLink(item: LocationLink): proto.LocationLink; + function asLocationLink( + item: LocationLink | undefined | null, + ): proto.LocationLink | undefined { + if (!item) { + return undefined; + } + const result: proto.LocationLink = { + targetUri: item.targetUri.toString(), + targetRange: asRange(item.targetSelectionRange)!, // See issue: https://github.com/Microsoft/vscode/issues/58649 + originSelectionRange: asRange(item.originSelectionRange)!, + targetSelectionRange: asRange(item.targetSelectionRange)!, + }; + if (!result.targetSelectionRange) { + throw new Error(`targetSelectionRange must not be undefined or null`); + } + return result; + } + + // Function to check if an object is a LocationLink + function isLocationLink(object: any): object is LocationLink { + return ( + object !== undefined && + 'targetUri' in object && + 'targetRange' in object && + 'targetSelectionRange' in object + ); + } + + function asDefinitionResult(item: Definition): proto.Definition; + function asDefinitionResult(item: DefinitionLink[]): proto.Definition; + function asDefinitionResult(item: undefined | null): undefined; + function asDefinitionResult( + item: Definition | DefinitionLink[] | undefined | null, + ): proto.Definition | proto.DefinitionLink[] | undefined; + function asDefinitionResult( + item: Definition | DefinitionLink[] | undefined | null, + ): proto.Definition | proto.DefinitionLink[] | undefined { + if (!item) { + return undefined; + } + if (Array.isArray(item)) { + if (item.length === 0) { + return undefined; + } else if (isLocationLink(item[0])) { + const links: LocationLink[] = item as unknown as LocationLink[]; + return links.map((location) => asLocationLink(location)); + } else { + const locations: Location[] = item as Location[]; + return locations.map((location) => asLocation(location)); + } + } else { + return asLocation(item); + } + } + function asLocation(value: Location): proto.Location; function asLocation(value: undefined): undefined; function asLocation(value: null): null; @@ -1405,5 +1479,7 @@ export function createConverter(uriConverter?: URIConverter): Converter { asWorkspaceSymbol, asInlineCompletionParams, asInlineCompletionContext, + asDefinitionResult, + asLocationLink, }; } diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/convertor.ts b/packages/libro-language-client/src/common/vscodeAdaptor/convertor.ts index f4dbd8f56..0a245489e 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/convertor.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/convertor.ts @@ -1,4 +1,5 @@ import type { CellView, LibroView } from '@difizen/libro-core'; +import { ExecutableNotebookModel } from '@difizen/libro-kernel'; import type { NotebookCell, NotebookDocument, NotebookRange } from 'vscode'; import { NotebookDocumentSyncFeature } from '../notebook.js'; @@ -9,14 +10,14 @@ import { EndOfLine, NotebookCellKind, Uri } from './vscodeAdaptor.js'; export const l2c = { asNotebookDocument(libroView: LibroView): NotebookDocument { const model = libroView.model as any; - if (model.filePath === undefined) { - throw new Error('no filePath: invalid libro jupyter model'); + if (!ExecutableNotebookModel.is(model)) { + throw new Error('invalid libro jupyter model'); } const filePath = model.filePath as string; return { uri: Uri.parse(filePath), notebookType: 'jupyter', - version: 0, + version: libroView.model.version, isDirty: libroView.model.dirty, isUntitled: false, isClosed: false, @@ -49,7 +50,7 @@ export const l2c = { fileName: filePath, isUntitled: false, languageId: 'python', - version: 0, + version: cell.model.version, isDirty: false, isClosed: false, save: unsupported, diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts b/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts index 4704d7f9d..63babacae 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts @@ -115,9 +115,25 @@ export class MonacoLanguages implements IMonacoLanguages { selector: DocumentSelector, provider: DefinitionProvider, ): Disposable { + return monaco.languages.registerDefinitionProvider( + selector, + this.createDefinitionProvider(provider), + ); + } + protected createDefinitionProvider( + provider: DefinitionProvider, + ): monaco.languages.DefinitionProvider { return { - dispose: () => { - return; + provideDefinition: async (model, position, token) => { + const params = this.m2p.asTextDocumentPositionParams(model, position); + const result = await provider.provideDefinition( + { uri: model.uri } as any, + this.p2c.asPosition(params.position), + token, + ); + return ( + result && this.p2m.asDefinitionResult(this.c2p.asDefinitionResult(result)) + ); }, }; } diff --git a/packages/libro-language-client/src/index.ts b/packages/libro-language-client/src/index.ts index b3640c338..87265ebf9 100644 --- a/packages/libro-language-client/src/index.ts +++ b/packages/libro-language-client/src/index.ts @@ -3,3 +3,4 @@ export * from './libro-language-client.js'; export * from './module.js'; export * from './common/api.js'; export * from './constants.js'; +export * from './util.js'; diff --git a/packages/libro-language-client/src/libro-language-client-contribution.ts b/packages/libro-language-client/src/libro-language-client-contribution.ts index 41936e63d..0afb48b4b 100644 --- a/packages/libro-language-client/src/libro-language-client-contribution.ts +++ b/packages/libro-language-client/src/libro-language-client-contribution.ts @@ -1,9 +1,15 @@ +import { EditorCellView, LibroService } from '@difizen/libro-core'; +import { ExecutableNotebookModel } from '@difizen/libro-kernel'; import { ApplicationContribution, inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; +import { URI } from 'vscode-uri'; import { CloseAction, ErrorAction } from './common/api.js'; import { LSPEnv } from './common/vscodeAdaptor/lspEnv.js'; +import { ILibroWorkspace } from './common/vscodeAdaptor/services.js'; import { workspace } from './common/vscodeAdaptor/vscodeAdaptor.js'; import { LibroLanguageClientManager } from './libro-language-client-manager.js'; +import { getCellURI, toEditorRange, toMonacoPosition } from './util.js'; @singleton({ contrib: [ApplicationContribution] }) export class LibroLanguageClientContribution implements ApplicationContribution { @@ -13,9 +19,13 @@ export class LibroLanguageClientContribution implements ApplicationContribution @inject(LSPEnv) protected readonly lspEnv: LSPEnv; + @inject(LibroService) + protected readonly libroService: LibroService; + async onViewStart() { // not block this.startLanguageClients(); + this.setupEditorOpener(); } async startLanguageClients() { @@ -34,11 +44,13 @@ export class LibroLanguageClientContribution implements ApplicationContribution closed: () => ({ action: CloseAction.DoNotRestart }), }, // pyright requires a workspace folder to be present, otherwise it will not work - // workspaceFolder: { - // index: 0, - // name: 'workspace', - // uri: URI.parse('/examples'), // abs path - // }, + workspaceFolder: { + index: 0, + name: 'workspace', + uri: URI.parse( + '/Users/ryannz/projj/github.com/difizen/libro-server/examples', + ), // abs path + }, synchronize: { fileEvents: [workspace.createFileSystemWatcher('**', false)], }, @@ -46,4 +58,65 @@ export class LibroLanguageClientContribution implements ApplicationContribution }); } } + + setupEditorOpener() { + monaco.editor.registerEditorOpener({ + openCodeEditor: (source, resource, selectionOrPosition) => { + // simulate openening a new browser tab for our own type (open definition of alert) + const model = monaco.editor.getModel(resource); + if (model?.id === source.getModel()?.id) { + return true; + } + const libroView = Array.from(this.libroService.getViewCache().values()).find( + (item) => { + return ( + ExecutableNotebookModel.is(item.model) && + URI.parse(item.model.filePath).path === resource.path + ); + }, + ); + + if (!libroView) { + return false; + } + + const cell = libroView.model.cells.find((item) => { + return ( + ExecutableNotebookModel.is(libroView.model) && + getCellURI(libroView.model, item.model).toString() === + decodeURIComponent(resource.toString()) + ); + }); + + if (EditorCellView.is(cell)) { + libroView.selectCell(cell); + cell.editor?.focus(); + let line = 0; + if (monaco.Range.isIRange(selectionOrPosition)) { + cell.editor?.revealSelection(toEditorRange(selectionOrPosition)); + cell.editor?.setCursorPosition(toEditorRange(selectionOrPosition).start); + line = toEditorRange(selectionOrPosition).start.line; + } else { + cell.editor?.setCursorPosition(toMonacoPosition(selectionOrPosition)); + line = toMonacoPosition(selectionOrPosition).line; + } + libroView.model.scrollToView(cell, (line ?? 0) * 20); + return false; + } + + // alternatively set model directly in the editor if you have your own tab/navigation implementation + // const model = monaco.editor.getModel(resource); + // editor.setModel(model); + // if (monaco.Range.isIRange(selectionOrPosition)) { + // editor.revealRangeInCenterIfOutsideViewport(selectionOrPosition); + // editor.setSelection(selectionOrPosition); + // } else { + // editor.revealPositionInCenterIfOutsideViewport(selectionOrPosition); + // editor.setPosition(selectionOrPosition); + // } + + return false; + }, + }); + } } diff --git a/packages/libro-language-client/src/util.ts b/packages/libro-language-client/src/util.ts new file mode 100644 index 000000000..a27a3b657 --- /dev/null +++ b/packages/libro-language-client/src/util.ts @@ -0,0 +1,43 @@ +import type { IPosition, IRange } from '@difizen/libro-code-editor'; +import type { CellModel } from '@difizen/libro-core'; +import type { ExecutableNotebookModel } from '@difizen/libro-kernel'; +import { URI } from '@difizen/mana-app'; +import type * as monaco from '@difizen/monaco-editor-core'; + +import { LibroCellURIScheme } from './constants.js'; + +export const getCellURI = ( + libroModel: ExecutableNotebookModel, + cellModel: CellModel, +): URI => { + let uri = new URI(libroModel.filePath); + uri = URI.withScheme(uri, LibroCellURIScheme); + uri = URI.withQuery(uri, `cellid=${cellModel.id}`); + return uri; +}; + +export const toEditorRange = (range: monaco.IRange): IRange => { + return { + start: { + line: range.startLineNumber - 1, + column: range.startColumn - 1, + }, + end: { + line: range.endLineNumber - 1, + column: range.endColumn - 1, + }, + }; +}; + +export const toMonacoPosition = (position: monaco.IPosition | undefined): IPosition => { + if (!position) { + return { + column: 0, + line: 0, + }; + } + return { + column: position?.column - 1, + line: position?.lineNumber - 1, + }; +};