From 6662cb6dea31a673a55bd982b1ebd3e904697ecc Mon Sep 17 00:00:00 2001 From: zhanba Date: Wed, 24 Jan 2024 16:49:06 +0800 Subject: [PATCH] feat: support file sync & support format code --- .../libro-code-cell/src/code-cell-model.ts | 6 +- .../libro-code-cell/src/code-cell-view.tsx | 28 ++++-- .../src/code-editor-protocol.ts | 23 +++++ packages/libro-codemirror/src/editor.ts | 10 ++- .../src/editor-contribution.ts | 7 +- .../src/libro-e2-editor.ts | 81 +++++++++++------ packages/libro-common/src/index.ts | 1 + packages/libro-common/src/mime.ts | 12 +++ .../libro-core/src/cell/libro-cell-view.tsx | 4 +- .../src/command/document-commands.ts | 11 ++- .../src/command/libro-command-contribution.ts | 20 ++++- packages/libro-core/src/libro-model.ts | 22 +++-- packages/libro-core/src/libro-protocol.ts | 31 ++++++- packages/libro-core/src/libro-service.ts | 32 ++++++- packages/libro-core/src/libro-view.tsx | 10 ++- .../libro-core/src/toolbar/libro-toolbar.tsx | 13 +++ .../src/file/navigatable-view.tsx | 2 +- .../src/common/protocolConverter.ts | 1 + .../vscodeAdaptor/diagnosticCollection.ts | 62 +------------ .../src/common/vscodeAdaptor/fileWatcher.ts | 38 ++++++++ .../common/vscodeAdaptor/libroWorkspace.ts | 47 +++++++--- .../common/vscodeAdaptor/monacoLanguages.ts | 87 +++++++++++++++---- .../src/common/vscodeAdaptor/services.ts | 4 - .../src/common/vscodeAdaptor/typings.d.ts | 10 +++ .../src/libro-language-client-contribution.ts | 30 ++++++- .../src/libro-language-client-manager.ts | 34 ++------ .../src/adapters/notebook-adapter.ts | 5 +- .../libro-search/src/libro-search-view.tsx | 3 + 28 files changed, 445 insertions(+), 189 deletions(-) create mode 100644 packages/libro-common/src/mime.ts create mode 100644 packages/libro-language-client/src/common/vscodeAdaptor/fileWatcher.ts create mode 100644 packages/libro-language-client/src/common/vscodeAdaptor/typings.d.ts diff --git a/packages/libro-code-cell/src/code-cell-model.ts b/packages/libro-code-cell/src/code-cell-model.ts index 460668bf7..9186341a0 100644 --- a/packages/libro-code-cell/src/code-cell-model.ts +++ b/packages/libro-code-cell/src/code-cell-model.ts @@ -1,5 +1,5 @@ -import type { ExecutionCount } from '@difizen/libro-common'; -import type { ICodeCell } from '@difizen/libro-common'; +import { MIME } from '@difizen/libro-common'; +import type { ICodeCell, ExecutionCount } from '@difizen/libro-common'; import type { ExecutableCellModel } from '@difizen/libro-core'; import { CellOptions, LibroCellModel } from '@difizen/libro-core'; import type { Event as ManaEvent } from '@difizen/mana-app'; @@ -39,7 +39,7 @@ export class LibroCodeCellModel extends LibroCellModel implements ExecutableCell this.executing = false; this.msgChangeEmitter = new Emitter(); this.executeCount = (options.cell as ICodeCell).execution_count || null; - this.mimeType = 'text/x-python'; + this.mimeType = MIME.python; this.hasOutputHidden = false; this.hasOutputsScrolled = false; this.viewManager = viewManager; diff --git a/packages/libro-code-cell/src/code-cell-view.tsx b/packages/libro-code-cell/src/code-cell-view.tsx index f6be17c9a..edcb6f275 100644 --- a/packages/libro-code-cell/src/code-cell-view.tsx +++ b/packages/libro-code-cell/src/code-cell-view.tsx @@ -19,6 +19,8 @@ import { LirboContextKey, } from '@difizen/libro-core'; import type { ViewSize } from '@difizen/mana-app'; +import { Disposable } from '@difizen/mana-app'; +import { DisposableCollection } from '@difizen/mana-app'; import { getOrigin, inject, @@ -100,6 +102,7 @@ const CodeEditorViewComponent = forwardRef( @transient() @view('code-editor-cell-view') export class LibroCodeCellView extends LibroExecutableCellView { + protected toDisposeOnEditor = new DisposableCollection(); @inject(LirboContextKey) protected readonly lirboContextKey: LirboContextKey; override view = CodeEditorViewComponent; @@ -256,19 +259,30 @@ export class LibroCodeCellView extends LibroExecutableCellView { if (e.status === 'ready') { this.editor = this.editorView!.editor; this.afterEditorReady(); + } else if (e.status === 'disposed') { + this.toDisposeOnEditor.dispose(); } }); } protected async afterEditorReady() { - watch(this.parent.model, 'readOnly', () => { - this.editorView?.editor?.setOption( - 'readOnly', - getOrigin(this.parent.model.readOnly), - ); - }); - this.editorView?.onModalChange((val) => (this.hasModal = val)); this.focusEditor(); + this.toDisposeOnEditor.push( + watch(this.parent.model, 'readOnly', () => { + this.editorView?.editor?.setOption( + 'readOnly', + getOrigin(this.parent.model.readOnly), + ); + }), + ); + this.toDisposeOnEditor.push( + this.editorView?.onModalChange((val) => (this.hasModal = val)) ?? Disposable.NONE, + ); + this.toDisposeOnEditor.push( + this.editor?.onModelContentChanged?.((e) => { + this.parent.model.onCellContentChange({ cell: this, changes: e }); + }) ?? Disposable.NONE, + ); } protected focusEditor() { diff --git a/packages/libro-code-editor/src/code-editor-protocol.ts b/packages/libro-code-editor/src/code-editor-protocol.ts index d01f62948..26c49a7ce 100644 --- a/packages/libro-code-editor/src/code-editor-protocol.ts +++ b/packages/libro-code-editor/src/code-editor-protocol.ts @@ -394,6 +394,10 @@ export interface IEditor extends ISelectionOwner, Disposable { dispose: () => void; getState: () => EditorState; + + format: () => void; + + onModelContentChanged?: Event; } export type EditorTheme = Record; @@ -656,3 +660,22 @@ export interface CodeEditorContribution { stateFactory?: EditorStateFactory; defaultConfig: IEditorConfig; } + +export interface IModelContentChange { + /** + * The range that got replaced. + */ + readonly range: IRange; + /** + * The offset of the range that got replaced. + */ + readonly rangeOffset: number; + /** + * The length of the range that got replaced. + */ + readonly rangeLength: number; + /** + * The new text for the range. + */ + readonly text: string; +} diff --git a/packages/libro-codemirror/src/editor.ts b/packages/libro-codemirror/src/editor.ts index 6ec5c9441..7ab633327 100644 --- a/packages/libro-codemirror/src/editor.ts +++ b/packages/libro-codemirror/src/editor.ts @@ -33,7 +33,11 @@ import type { KeydownHandler, SearchMatch, } from '@difizen/libro-code-editor'; -import { findFirstArrayIndex, removeAllWhereFromArray } from '@difizen/libro-common'; +import { + findFirstArrayIndex, + MIME, + removeAllWhereFromArray, +} from '@difizen/libro-common'; import type { LSPProvider } from '@difizen/libro-lsp'; import { Deferred, Disposable, Emitter } from '@difizen/mana-app'; import { getOrigin, watch } from '@difizen/mana-app'; @@ -93,7 +97,7 @@ const DOWN_ARROW = 40; export const codeMirrorDefaultConfig: Required = { ...defaultConfig, mode: 'null', - mimetype: 'text/x-python', + mimetype: MIME.python, theme: { light: 'jupyter', dark: 'jupyter', hc: 'jupyter' }, smartIndent: true, electricChars: true, @@ -875,6 +879,8 @@ export class CodeMirrorEditor implements IEditor { command(this.editor); } + format: () => void; + /** * Handle keydown events from the editor. */ diff --git a/packages/libro-cofine-editor/src/editor-contribution.ts b/packages/libro-cofine-editor/src/editor-contribution.ts index fb7e1ff81..83c91ce82 100644 --- a/packages/libro-cofine-editor/src/editor-contribution.ts +++ b/packages/libro-cofine-editor/src/editor-contribution.ts @@ -1,5 +1,6 @@ import type { CodeEditorFactory, EditorStateFactory } from '@difizen/libro-code-editor'; import { CodeEditorContribution } from '@difizen/libro-code-editor'; +import { MIME } from '@difizen/libro-common'; import { inject, singleton } from '@difizen/mana-app'; import { LanguageSpecRegistry } from './language-specs.js'; @@ -32,11 +33,7 @@ export class LibroE2EditorContribution implements CodeEditorContribution { }; canHandle(mime: string): number { - const mimes = [ - 'application/vnd.libro.sql+json', - 'text/x-python', - 'application/vnd.libro.prompt+json', - ]; + const mimes = [MIME.odpssql, MIME.python, MIME.prompt]; if (mimes.includes(mime)) { return 50 + 1; } diff --git a/packages/libro-cofine-editor/src/libro-e2-editor.ts b/packages/libro-cofine-editor/src/libro-e2-editor.ts index 1c04050c5..9468d6747 100644 --- a/packages/libro-cofine-editor/src/libro-e2-editor.ts +++ b/packages/libro-cofine-editor/src/libro-e2-editor.ts @@ -8,6 +8,7 @@ import type { IEditorConfig, IEditorOptions, IModel, + IModelContentChange, IPosition, IRange, SearchMatch, @@ -16,6 +17,7 @@ import type { import { defaultConfig } from '@difizen/libro-code-editor'; import type { E2Editor } from '@difizen/libro-cofine-editor-core'; import { EditorProvider, MonacoEnvironment } from '@difizen/libro-cofine-editor-core'; +import { MIME } from '@difizen/libro-common'; import { NotebookCommands } from '@difizen/libro-core'; import type { LSPProvider } from '@difizen/libro-lsp'; import { @@ -28,7 +30,7 @@ import { watch, } from '@difizen/mana-app'; import { Disposable, DisposableCollection, Emitter } from '@difizen/mana-app'; -import { editor, Selection } from '@difizen/monaco-editor-core'; +import { editor, KeyCode, Selection } from '@difizen/monaco-editor-core'; import 'resize-observer-polyfill'; import { v4 } from 'uuid'; @@ -228,7 +230,7 @@ export const libroE2DefaultConfig: Required = { }, scrollBarHeight: 12, mode: 'null', - mimetype: 'text/x-python', + mimetype: MIME.python, smartIndent: true, electricChars: true, keyMap: 'default', @@ -371,6 +373,9 @@ export class LibroE2Editor implements IEditor { return this.monacoEditor?.getModel()?.getLineCount() || 0; } + protected onModelContentChangedEmitter = new Emitter(); + onModelContentChanged = this.onModelContentChangedEmitter.event; + lspProvider?: LSPProvider; completionProvider?: CompletionProvider; @@ -458,7 +463,7 @@ export class LibroE2Editor implements IEditor { * */ // overflowWidgetsDomNode: document.getElementById('monaco-editor-overflow-widgets-root')!, - // fixedOverflowWidgets: true, + fixedOverflowWidgets: true, suggest: { snippetsPreventQuickSuggestions: false }, autoClosingQuotes: editorConfig.autoClosingBrackets ? 'always' : 'never', autoDetectHighContrast: false, @@ -519,15 +524,24 @@ export class LibroE2Editor implements IEditor { */ language: this.languageSpec.language, theme: this.theme, - model, + model: getOrigin(model), }; this._editor = editorPorvider.create(host, options); this.toDispose.push( - this.monacoEditor?.onDidChangeModelContent(() => { + this.monacoEditor?.onDidChangeModelContent((e) => { const value = this.monacoEditor?.getValue(); this.model.value = value ?? ''; - // this.updateEditorSize(); + this.onModelContentChangedEmitter.fire( + e.changes.map((item) => { + return { + range: this.toEditorRange(item.range), + rangeLength: item.rangeLength, + rangeOffset: item.rangeOffset, + text: item.text, + }; + }), + ); }) ?? Disposable.NONE, ); this.toDispose.push( @@ -556,11 +570,6 @@ export class LibroE2Editor implements IEditor { // this.monacoEditor._themeService, // this.monacoEditor._themeService.getColorTheme(), // ); - - // setTimeout(() => { - // this.monacoEditor?.trigger('editor', 'editor.action.formatDocument'); - // console.log('trigger format'); - // }, 5000); } protected inspectResize() { @@ -607,22 +616,23 @@ export class LibroE2Editor implements IEditor { */ protected handleCommand(commandRegistry: CommandRegistry) { // need monaco 0.34 - // editor.addKeybindingRules([ - // { - // // disable show command center - // keybinding: KeyCode.F1, - // command: null, - // }, - // { - // // disable show error command - // keybinding: KeyCode.F8, - // command: null, - // }, - // { - // // disable toggle debugger breakpoint - // keybinding: KeyCode.F9, - // command: null, - // }, + editor.addKeybindingRules([ + { + // disable show command center + keybinding: KeyCode.F1, + command: null, + }, + { + // disable show error command + keybinding: KeyCode.F8, + command: null, + }, + { + // disable toggle debugger breakpoint + keybinding: KeyCode.F9, + command: null, + }, + ]); this.monacoEditor?.addCommand( 9, () => { @@ -812,6 +822,19 @@ export class LibroE2Editor implements IEditor { return monacoSelection; } + protected toEditorRange(range: monaco.IRange): IRange { + return { + start: { + line: range.startLineNumber - 1, + column: range.startColumn - 1, + }, + end: { + line: range.endLineNumber - 1, + column: range.endColumn - 1, + }, + }; + } + getSelectionValue = (range?: IRange | undefined) => { const selection = range ?? this.getSelection(); return this.monacoEditor @@ -955,6 +978,10 @@ export class LibroE2Editor implements IEditor { } }; + format = () => { + this.monacoEditor?.trigger('libro-format', 'editor.action.formatDocument', ''); + }; + protected _isDisposed = false; /** * Tests whether the editor is disposed. diff --git a/packages/libro-common/src/index.ts b/packages/libro-common/src/index.ts index bff3b3cd0..a3d8c5458 100644 --- a/packages/libro-common/src/index.ts +++ b/packages/libro-common/src/index.ts @@ -9,3 +9,4 @@ export * from './polling/index.js'; export * from './display-wrapper.js'; export * from './protocol/index.js'; export * from './dom.js'; +export * from './mime.js'; diff --git a/packages/libro-common/src/mime.ts b/packages/libro-common/src/mime.ts new file mode 100644 index 000000000..c71427d72 --- /dev/null +++ b/packages/libro-common/src/mime.ts @@ -0,0 +1,12 @@ +export const MIME = { + text: 'text/plain', + binary: 'application/octet-stream', + unknown: 'application/unknown', + markdown: 'text/markdown', + latex: 'text/latex', + uriList: 'text/uri-list', + json: 'application/json', + python: 'text/x-python', + prompt: 'application/vnd.libro.prompt+json', + odpssql: 'application/vnd.libro.sql+json', +} as const; diff --git a/packages/libro-core/src/cell/libro-cell-view.tsx b/packages/libro-core/src/cell/libro-cell-view.tsx index 09b085308..e9120f11e 100644 --- a/packages/libro-core/src/cell/libro-cell-view.tsx +++ b/packages/libro-core/src/cell/libro-cell-view.tsx @@ -71,13 +71,13 @@ export class LibroCellView extends BaseView implements CellView { this.toDispose.push( watch(this.model, 'value', () => { this.parent.model.onChange?.(); - this.parent.model.onSourceChange?.(); + this.parent.model.onSourceChange?.([this]); }), ); this.toDispose.push( watch(this.model, 'type', () => { this.parent.model.onChange?.(); - this.parent.model.onSourceChange?.(); + this.parent.model.onSourceChange?.([this]); }), ); if (ExecutableCellModel.is(this.model)) { diff --git a/packages/libro-core/src/command/document-commands.ts b/packages/libro-core/src/command/document-commands.ts index 0b682ac61..89d0a88b0 100644 --- a/packages/libro-core/src/command/document-commands.ts +++ b/packages/libro-core/src/command/document-commands.ts @@ -1,4 +1,8 @@ -import { SaveOutlined, SettingOutlined } from '@ant-design/icons'; +import { + FormatPainterOutlined, + SaveOutlined, + SettingOutlined, +} from '@ant-design/icons'; import type { Command } from '@difizen/mana-app'; export const DocumentCommands: Record = { @@ -13,4 +17,9 @@ export const DocumentCommands: Record = icon: SettingOutlined, label: 'Setting', }, + FormatCell: { + id: 'document.notebook.format_cell', + icon: FormatPainterOutlined, + label: 'format cell code', + }, }; diff --git a/packages/libro-core/src/command/libro-command-contribution.ts b/packages/libro-core/src/command/libro-command-contribution.ts index 14c144c02..e0a7f79a0 100644 --- a/packages/libro-core/src/command/libro-command-contribution.ts +++ b/packages/libro-core/src/command/libro-command-contribution.ts @@ -1,3 +1,4 @@ +import { MIME } from '@difizen/libro-common'; import type { CommandRegistry } from '@difizen/mana-app'; import { inject, @@ -8,7 +9,7 @@ import { import { equals } from '@difizen/mana-app'; import { v4 } from 'uuid'; -import { LibroCellView, ExecutableCellModel } from '../cell/index.js'; +import { LibroCellView, ExecutableCellModel, EditorCellView } from '../cell/index.js'; import type { LibroEditorCellView } from '../cell/index.js'; import { LirboContextKey } from '../libro-context-key.js'; import type { CellView, NotebookView } from '../libro-protocol.js'; @@ -1101,6 +1102,23 @@ export class LibroCommandContribution implements CommandContribution { return !libro?.model.readOnly && path === LibroToolbarArea.HeaderRight; }, }); + this.libroCommand.registerLibroCommand(command, DocumentCommands['FormatCell'], { + execute: async (cell) => { + if (EditorCellView.is(cell) && cell.editor?.model.mimeType === MIME.python) { + cell.editor?.format(); + } + }, + isVisible: (cell, libro, path) => { + if (!libro || !(libro instanceof LibroView)) { + return false; + } + return ( + !libro?.model.readOnly && + EditorCellView.is(cell) && + path === LibroToolbarArea.CellRight + ); + }, + }); this.libroCommand.registerLibroCommand( command, NotebookCommands['UndoCellAction'], diff --git a/packages/libro-core/src/libro-model.ts b/packages/libro-core/src/libro-model.ts index a24d549e3..4ba0c1005 100644 --- a/packages/libro-core/src/libro-model.ts +++ b/packages/libro-core/src/libro-model.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/unified-signatures */ +import type { IModelContentChange } from '@difizen/libro-code-editor'; import type { ICodeCell, INotebookContent, @@ -26,6 +27,7 @@ import type { ScrollParams, CellView, MouseMode, + ICellContentChange, } from './libro-protocol.js'; import { EnterEditModeWhenAddCell } from './libro-setting.js'; import { VirtualizedManagerHelper } from './virtualized-manager-helper.js'; @@ -60,9 +62,9 @@ export class LibroModel implements NotebookModel, DndListModel { return this.onCommandModeChangedEmitter.event; } - protected onContentChangedEmitter: Emitter = new Emitter(); - get onContentChanged() { - return this.onContentChangedEmitter.event; + protected onChangedEmitter: Emitter = new Emitter(); + get onChanged() { + return this.onChangedEmitter.event; } protected onSourceChangedEmitter: Emitter = new Emitter(); @@ -70,6 +72,15 @@ export class LibroModel implements NotebookModel, DndListModel { return this.onSourceChangedEmitter.event; } + protected onCellContentChangedEmitter: Emitter = new Emitter(); + get onCellContentChanged() { + return this.onCellContentChangedEmitter.event; + } + + onCellContentChange(change: ICellContentChange) { + this.onCellContentChangedEmitter.fire(change); + } + id: string; /** @@ -291,12 +302,9 @@ export class LibroModel implements NotebookModel, DndListModel { }); } - /** - * cell list change or cell content change - */ onChange() { this.dirty = true; - this.onContentChangedEmitter.fire(true); + this.onChangedEmitter.fire(true); } onSourceChange() { diff --git a/packages/libro-core/src/libro-protocol.ts b/packages/libro-core/src/libro-protocol.ts index 18a0417c3..4da4bec63 100644 --- a/packages/libro-core/src/libro-protocol.ts +++ b/packages/libro-core/src/libro-protocol.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/unified-signatures */ -import type { IModel } from '@difizen/libro-code-editor'; +import type { IModel, IModelContentChange } from '@difizen/libro-code-editor'; import type { CellType, ICell, @@ -64,6 +64,11 @@ export interface ScrollParams { } export type NotebookModel = BaseNotebookModel & DndListModel; + +export interface ICellContentChange { + cell: CellView; + changes: IModelContentChange[]; +} export interface BaseNotebookModel { id: string; /** @@ -98,12 +103,26 @@ export interface BaseNotebookModel { onCommandModeChanged: Event; - onContentChanged: Event; + /** + * all changes + */ + onChanged: Event; + /** + * cell content & type + */ onSourceChanged: Event; + /** + * cell create & delete + */ onCellViewChanged: Event; + /** + * cell content change detail + */ + onCellContentChanged: Event; + getCells: () => CellView[]; /** @@ -171,9 +190,15 @@ export interface BaseNotebookModel { enterEditMode?: () => void; + /** + * all changes: cell value\cell type\cell output\ cell executecount\ cell or notebook metadata\cell create & delete + * @returns + */ onChange?: () => void; - onSourceChange?: () => void; + onSourceChange?: (cells: CellView[]) => void; + + onCellContentChange: (changes: ICellContentChange) => void; interrupt?: () => void; diff --git a/packages/libro-core/src/libro-service.ts b/packages/libro-core/src/libro-service.ts index 51ceaefc5..614656d2b 100644 --- a/packages/libro-core/src/libro-service.ts +++ b/packages/libro-core/src/libro-service.ts @@ -1,10 +1,16 @@ +import type { IModelContentChange } from '@difizen/libro-code-editor'; import type { Disposable } from '@difizen/mana-app'; import { DisposableCollection, Emitter } from '@difizen/mana-app'; import { ThemeService, ViewManager } from '@difizen/mana-app'; import { inject, singleton } from '@difizen/mana-app'; import { prop } from '@difizen/mana-app'; -import type { CellView, NotebookOption, NotebookView } from './libro-protocol.js'; +import type { + CellView, + ICellContentChange, + NotebookOption, + NotebookView, +} from './libro-protocol.js'; import { notebookViewFactoryId, ModelFactory, @@ -79,6 +85,22 @@ export class LibroService implements NotebookService, Disposable { get onNotebookViewClosed() { return this.onNotebookViewClosedEmitter.event; } + protected onNotebookCellCreatedEmitter: Emitter = new Emitter(); + get onNotebookCellCreated() { + return this.onNotebookCellCreatedEmitter.event; + } + protected onNotebookCellSavedEmitter: Emitter = new Emitter(); + get onNotebookCellSaved() { + return this.onNotebookCellSavedEmitter.event; + } + protected onNotebookCellChangedEmitter: Emitter = new Emitter(); + get onNotebookCellChanged() { + return this.onNotebookCellChangedEmitter.event; + } + protected onNotebookCellDeletedEmitter: Emitter = new Emitter(); + get onNotebookCellDeleted() { + return this.onNotebookCellDeletedEmitter.event; + } get focus(): NotebookView | undefined { return this._focus; @@ -133,6 +155,11 @@ export class LibroService implements NotebookService, Disposable { this.onNotebookViewSavedEmitter.fire(notebookView); }), ); + this.toDispose.push( + notebookView.model.onCellContentChanged((e) => { + this.onNotebookCellChangedEmitter.fire(e); + }), + ); this.toDispose.push( notebookView.model.onCellViewChanged((e) => { const changes: NotebookViewChange = { @@ -150,6 +177,7 @@ export class LibroService implements NotebookService, Disposable { removedCells: e.delete.cells, addedCells: [], }); + this.onNotebookCellDeletedEmitter.fire(e.delete.cells); } if (e.insert) { @@ -161,11 +189,13 @@ export class LibroService implements NotebookService, Disposable { removedCells: [], addedCells: e.insert.cells, }); + this.onNotebookCellCreatedEmitter.fire(e.insert.cells); } this.onNotebookViewChangedEmitter.fire(changes); }), ); + return notebookViewPromise; } diff --git a/packages/libro-core/src/libro-view.tsx b/packages/libro-core/src/libro-view.tsx index 99a34f662..db7ff4d4b 100644 --- a/packages/libro-core/src/libro-view.tsx +++ b/packages/libro-core/src/libro-view.tsx @@ -1,4 +1,5 @@ import { ToTopOutlined } from '@ant-design/icons'; +import type { IModelContentChange } from '@difizen/libro-code-editor'; import { concatMultilineString, copy2clipboard, @@ -281,6 +282,10 @@ export class LibroView extends BaseView implements NotebookView { get onCellCreate() { return this.onCellCreateEmitter.event; } + protected onCellDeleteEmitter: Emitter = new Emitter(); + get onCellDelete() { + return this.onCellDeleteEmitter.event; + } onBlurEmitter: Emitter = new Emitter(); get onBlur() { @@ -318,6 +323,10 @@ export class LibroView extends BaseView implements NotebookView { get onSave() { return this.onSaveEmitter.event; } + onCellContentChangedEmitter: Emitter = new Emitter(); + get onCellContentChanged() { + return this.onCellContentChangedEmitter.event; + } runCellEmitter: Emitter = new Emitter(); get onRunCell() { @@ -411,7 +420,6 @@ export class LibroView extends BaseView implements NotebookView { this.toDispose.push( watch(this.model, 'cells', () => { this.model.onChange?.(); - this.model.onSourceChange?.(); }), ); this.initializedDefer.resolve(); diff --git a/packages/libro-core/src/toolbar/libro-toolbar.tsx b/packages/libro-core/src/toolbar/libro-toolbar.tsx index b970c7575..2faed85ca 100644 --- a/packages/libro-core/src/toolbar/libro-toolbar.tsx +++ b/packages/libro-core/src/toolbar/libro-toolbar.tsx @@ -94,6 +94,19 @@ export class LibroToolbarContribution implements ToolbarContribution { group: ['group2'], order: 'c-all', }); + registry.registerItem({ + id: DocumentCommands['FormatCell'].id, + command: DocumentCommands['FormatCell'].id, + tooltip: ( +
+ {l10n.t('格式化代码')} + Shift+Option+F +
+ ), + + group: ['sidetoolbar1'], + order: 'd', + }); registry.registerItem({ id: NotebookCommands['MoveCellUp'].id, command: NotebookCommands['MoveCellUp'].id, diff --git a/packages/libro-jupyter/src/file/navigatable-view.tsx b/packages/libro-jupyter/src/file/navigatable-view.tsx index 55a803e39..28d3ae8d6 100644 --- a/packages/libro-jupyter/src/file/navigatable-view.tsx +++ b/packages/libro-jupyter/src/file/navigatable-view.tsx @@ -141,7 +141,7 @@ export class LibroNavigatableView return; } this.libroView = libroView; - this.libroView.model.onContentChanged(() => { + this.libroView.model.onChanged(() => { this.dirty = true; this.dirtyEmitter.fire(); if (this.autoSave === 'on') { diff --git a/packages/libro-language-client/src/common/protocolConverter.ts b/packages/libro-language-client/src/common/protocolConverter.ts index 075259bb5..943b94a1f 100644 --- a/packages/libro-language-client/src/common/protocolConverter.ts +++ b/packages/libro-language-client/src/common/protocolConverter.ts @@ -4,6 +4,7 @@ * ------------------------------------------------------------------------------------------ */ // import * as code from 'vscode'; +import type { TextDocumentPositionParams } from '@difizen/vscode-languageserver-protocol'; import * as ls from '@difizen/vscode-languageserver-protocol'; import { NotebookCellTextDocumentFilter, diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/diagnosticCollection.ts b/packages/libro-language-client/src/common/vscodeAdaptor/diagnosticCollection.ts index 01059d340..36c9d2927 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/diagnosticCollection.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/diagnosticCollection.ts @@ -224,6 +224,7 @@ export class LibroDiagnosticCollection implements DiagnosticCollection { updateModelMarkers(uri: Uri, markers: editor.IMarkerData[]): void { const model = monaco.editor.getModel(uri); if (model) { + monaco.editor.setModelMarkers(model, this.name, []); monaco.editor.setModelMarkers(model, this.name, markers); } } @@ -236,64 +237,3 @@ export class LibroDiagnosticCollection implements DiagnosticCollection { } } } - -export class MonacoModelDiagnostics implements Disposable { - readonly uri: monaco.Uri; - protected _markers: monaco.editor.IMarkerData[] = []; - protected _diagnostics: Diagnostic[] = []; - protected readonly toDispose = new DisposableCollection(); - - constructor( - protected readonly _monaco: typeof monaco, - uri: string, - diagnostics: Diagnostic[], - readonly owner: string, - protected readonly p2m: ProtocolToMonacoConverter, - protected readonly c2p: c2p.Converter, - ) { - this.uri = this._monaco.Uri.parse(uri); - this.diagnostics = diagnostics; - this.toDispose.push( - this._monaco.editor.onDidCreateModel((model) => this.doUpdateModelMarkers(model)), - ); - } - - set diagnostics(diagnostics: Diagnostic[]) { - this._diagnostics = diagnostics; - this.c2p - .asDiagnostics(diagnostics) - .then((diag) => { - this._markers = this.p2m.asDiagnostics(diag); - this.updateModelMarkers(); - return; - }) - .catch(() => { - // - }); - } - - get diagnostics(): Diagnostic[] { - return this._diagnostics; - } - - get markers(): ReadonlyArray { - return this._markers; - } - - dispose(): void { - this._markers = []; - this.updateModelMarkers(); - this.toDispose.dispose(); - } - - updateModelMarkers(): void { - const model = this._monaco.editor.getModel(this.uri); - this.doUpdateModelMarkers(model ? model : undefined); - } - - protected doUpdateModelMarkers(model: monaco.editor.IModel | undefined): void { - if (model && this.uri.toString() === model.uri.toString()) { - this._monaco.editor.setModelMarkers(model, this.owner, this._markers); - } - } -} diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/fileWatcher.ts b/packages/libro-language-client/src/common/vscodeAdaptor/fileWatcher.ts new file mode 100644 index 000000000..c283d4e9c --- /dev/null +++ b/packages/libro-language-client/src/common/vscodeAdaptor/fileWatcher.ts @@ -0,0 +1,38 @@ +import type { NotebookView } from '@difizen/libro-core'; +import { EditorCellView } from '@difizen/libro-core'; +import { LibroService } from '@difizen/libro-core'; +import { inject, singleton } from '@difizen/mana-app'; +import type { Event, FileSystemWatcher, GlobPattern, Uri } from 'vscode'; + +import { EventEmitter } from './vscodeAdaptor.js'; + +@singleton() +export class LibroFileWatcher implements FileSystemWatcher { + @inject(LibroService) protected readonly libroService: LibroService; + + create( + globPattern: GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, + ) { + this.globPattern = globPattern; + this.ignoreCreateEvents = ignoreCreateEvents ?? true; + this.ignoreChangeEvents = ignoreChangeEvents ?? true; + this.ignoreDeleteEvents = ignoreDeleteEvents ?? true; + } + + globPattern: GlobPattern; + ignoreCreateEvents: boolean; + ignoreChangeEvents: boolean; + ignoreDeleteEvents: boolean; + protected onDidCreateEmitter = new EventEmitter(); + onDidCreate: Event = this.onDidCreateEmitter.event; + protected onDidChangeEmitter = new EventEmitter(); + onDidChange: Event = this.onDidChangeEmitter.event; + protected onDidDeleteEmitter = new EventEmitter(); + onDidDelete: Event = this.onDidDeleteEmitter.event; + dispose() { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/libroWorkspace.ts b/packages/libro-language-client/src/common/vscodeAdaptor/libroWorkspace.ts index 3d23783e7..2cbd5eb2e 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/libroWorkspace.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/libroWorkspace.ts @@ -1,5 +1,6 @@ -import { LibroService } from '@difizen/libro-core'; -import { inject, noop, singleton } from '@difizen/mana-app'; +import type { NotebookView } from '@difizen/libro-core'; +import { EditorCellView, LibroService } from '@difizen/libro-core'; +import { Disposable, inject, noop, singleton } from '@difizen/mana-app'; import type { NotebookDocument, WorkspaceFolder, @@ -26,6 +27,7 @@ import type { } from 'vscode'; import { l2c } from './convertor.js'; +import { Range } from './extHostTypes.js'; import { ILibroWorkspace } from './services.js'; import { unsupported } from './util.js'; @@ -115,12 +117,12 @@ export class LibroWorkspace implements ILibroWorkspace { return disposable; }; - onDidOpenTextDocument: Event = () => { - return { - dispose: () => { - return; - }, - }; + onDidOpenTextDocument: Event = (listener) => { + return this.libroService.onNotebookCellCreated((cells) => { + cells.forEach((cell) => { + listener(l2c.asNotebookCell(cell).document); + }); + }); }; onDidSaveTextDocument: Event = () => { return { @@ -143,12 +145,29 @@ export class LibroWorkspace implements ILibroWorkspace { }, }; }; - onDidChangeTextDocument: Event = () => { - return { - dispose: () => { - return; - }, - }; + onDidChangeTextDocument: Event = (listener, thisArgs) => { + return ( + this.libroService.onNotebookCellChanged((e) => { + listener.call(thisArgs, { + document: l2c.asNotebookCell(e.cell).document, + contentChanges: e.changes.map((change) => { + return { + range: new Range( + change.range.start.line, + change.range.start.column, + change.range.end.line, + change.range.end.column, + ), + rangeLength: change.rangeLength, + rangeOffset: change.rangeOffset, + text: change.text, + }; + }), + reason: undefined, + }); + }), + thisArgs + ); }; onDidCreateFiles: Event = () => { return { diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts b/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts index add23b20f..de132a37f 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/monacoLanguages.ts @@ -1,5 +1,6 @@ import { singleton } from '@difizen/mana-app'; import * as monaco from '@difizen/monaco-editor-core'; +import { score } from '@difizen/monaco-editor-core/esm/vs/editor/common/languageSelector.js'; import type { DiagnosticCollection, TextDocument, @@ -21,7 +22,6 @@ import type { DocumentRangeSemanticTokensProvider, DocumentSymbolProvider, DocumentSymbolProviderMetadata, - EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, @@ -42,18 +42,40 @@ import type { InlayHintsProvider, } from 'vscode'; +import * as c2p from '../codeConverter.js'; +import * as p2c from '../protocolConverter.js'; + import { LibroDiagnosticCollection } from './diagnosticCollection.js'; +import { + MonacoToProtocolConverter, + ProtocolToMonacoConverter, +} from './monaco-converter.js'; import { IMonacoLanguages } from './services.js'; @singleton({ token: IMonacoLanguages }) export class MonacoLanguages implements IMonacoLanguages { + protected readonly c2p: c2p.Converter = c2p.createConverter(); + protected readonly p2c: p2c.Converter = p2c.createConverter(undefined, true, true); + protected readonly p2m: ProtocolToMonacoConverter = new ProtocolToMonacoConverter( + monaco, + ); + protected readonly m2p: MonacoToProtocolConverter = new MonacoToProtocolConverter( + monaco, + ); createDiagnosticCollection(name?: string): DiagnosticCollection { return new LibroDiagnosticCollection(); } match(selector: DocumentSelector, document: TextDocument): number { // const notebook = extHostDocuments.getDocumentData(document.uri)?.notebook; - // return score(typeConverters.LanguageSelector.from(selector), document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); - return 0; + // return score( + // typeConverters.LanguageSelector.from(selector), + // document.uri, + // document.languageId, + // true, + // notebook?.uri, + // notebook?.notebookType, + // ); + return 1; } registerCodeActionsProvider( @@ -121,22 +143,36 @@ export class MonacoLanguages implements IMonacoLanguages { selector: DocumentSelector, provider: HoverProvider, ): Disposable { - return { - dispose: () => { - return; - }, - }; + return monaco.languages.registerHoverProvider( + selector, + this.createHoverProvider(provider), + ); } - registerEvaluatableExpressionProvider( - selector: DocumentSelector, - provider: EvaluatableExpressionProvider, - ): Disposable { + + protected createHoverProvider( + provider: HoverProvider, + ): monaco.languages.HoverProvider { return { - dispose: () => { - return; + provideHover: async (model, position, token) => { + const params = this.m2p.asTextDocumentPositionParams(model, position); + + const hover = await provider.provideHover( + { uri: model.uri } as any, + this.p2c.asPosition(params.position), + token, + ); + + if (!hover || !hover.range) { + return; + } + return { + contents: hover?.contents, + range: this.p2m.asRange(this.c2p.asRange(hover.range)), + }; }, }; } + registerInlineValuesProvider( selector: DocumentSelector, provider: InlineValuesProvider, @@ -209,12 +245,31 @@ export class MonacoLanguages implements IMonacoLanguages { selector: DocumentSelector, provider: DocumentFormattingEditProvider, ): Disposable { + const documentFormattingEditProvider = + this.createDocumentFormattingEditProvider(provider); + return monaco.languages.registerDocumentFormattingEditProvider( + selector, + documentFormattingEditProvider, + ); + } + + protected createDocumentFormattingEditProvider( + provider: DocumentFormattingEditProvider, + ): monaco.languages.DocumentFormattingEditProvider { return { - dispose: () => { - return; + provideDocumentFormattingEdits: async (model, options, token) => { + const params = this.m2p.asDocumentFormattingParams(model, options); + const result = await provider.provideDocumentFormattingEdits( + { uri: model.uri } as any, + params.options, + token, + ); + + return result && this.p2m.asTextEdits(result); }, }; } + registerDocumentRangeFormattingEditProvider( selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/services.ts b/packages/libro-language-client/src/common/vscodeAdaptor/services.ts index 523f477ae..1639b6b7c 100644 --- a/packages/libro-language-client/src/common/vscodeAdaptor/services.ts +++ b/packages/libro-language-client/src/common/vscodeAdaptor/services.ts @@ -177,10 +177,6 @@ export interface IMonacoLanguages { selector: DocumentSelector, provider: HoverProvider, ): Disposable; - registerEvaluatableExpressionProvider( - selector: DocumentSelector, - provider: EvaluatableExpressionProvider, - ): Disposable; registerInlineValuesProvider( selector: DocumentSelector, provider: InlineValuesProvider, diff --git a/packages/libro-language-client/src/common/vscodeAdaptor/typings.d.ts b/packages/libro-language-client/src/common/vscodeAdaptor/typings.d.ts new file mode 100644 index 000000000..b640c9b8e --- /dev/null +++ b/packages/libro-language-client/src/common/vscodeAdaptor/typings.d.ts @@ -0,0 +1,10 @@ +declare module '@difizen/monaco-editor-core/esm/vs/editor/common/languageSelector.js' { + function score( + selector: LanguageSelector | undefined, + candidateUri: URI, + candidateLanguage: string, + candidateIsSynchronized: boolean, + candidateNotebookUri: URI | undefined, + candidateNotebookType: string | undefined, + ): number; +} 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 4692d05a5..8c3585c11 100644 --- a/packages/libro-language-client/src/libro-language-client-contribution.ts +++ b/packages/libro-language-client/src/libro-language-client-contribution.ts @@ -4,9 +4,11 @@ import type { ILanguageServerManager } from '@difizen/libro-lsp'; import { ILanguageServerManagerFactory } from '@difizen/libro-lsp'; import { ApplicationContribution, inject, singleton } from '@difizen/mana-app'; -import { createLibroLanguageServices } from './common/vscodeAdaptor/vscodeAdaptor.js'; - -// import { createLibroLanguageServices } from './common/vscodeAdaptor/vscodeAdaptor.js'; +import { CloseAction, ErrorAction } from './common/api.js'; +import { + createLibroLanguageServices, + Uri, +} from './common/vscodeAdaptor/vscodeAdaptor.js'; import { LibroLanguageClientManager } from './libro-language-client-manager.js'; @singleton({ contrib: [ApplicationContribution] }) @@ -32,7 +34,27 @@ export class LibroLanguageClientContribution implements ApplicationContribution }); for (const serverId of serverIds) { const serverUrl = this.serverUri(serverId); - await this.libroLanguageClientManager.getOrCreateLanguageClient(serverUrl); + await this.libroLanguageClientManager.getOrCreateLanguageClient(serverUrl, { + name: `${serverId} Language Client`, + clientOptions: { + // use a language id as a document selector + documentSelector: [{ language: 'python' }], + // disable the default error handler + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + 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('/Users/ryannz/projj/github.com/difizen/libro/examples'), + }, + // synchronize: { + // fileEvents: [workspace.createFileSystemWatcher('**')], + // }, + }, + }); } } } diff --git a/packages/libro-language-client/src/libro-language-client-manager.ts b/packages/libro-language-client/src/libro-language-client-manager.ts index 3e3169d9b..bcd31e6d0 100644 --- a/packages/libro-language-client/src/libro-language-client-manager.ts +++ b/packages/libro-language-client/src/libro-language-client-manager.ts @@ -1,7 +1,6 @@ import { singleton } from '@difizen/mana-app'; -import { CloseAction, ErrorAction } from './common/client.js'; -import { Uri } from './common/vscodeAdaptor/vscodeAdaptor.js'; +import type { LibroLanguageClientOptions } from './libro-language-client.js'; import { LibroLanguageClient } from './libro-language-client.js'; interface IConnectionData { @@ -13,36 +12,19 @@ interface IConnectionData { export class LibroLanguageClientManager { protected clientMap = new Map(); - async getOrCreateLanguageClient(url: string) { + async getOrCreateLanguageClient(url: string, options: LibroLanguageClientOptions) { if (this.clientMap.has(url)) { return this.clientMap.get(url); } - const client = await this.createLanguageClient(url); + const client = await this.createLanguageClient(url, options); this.clientMap.set(url, client); return client; } - protected createLanguageClient = (url: string) => { - return LibroLanguageClient.createWebSocketLanguageClient(url, { - name: 'Pyright Language Client', - clientOptions: { - // use a language id as a document selector - documentSelector: [{ notebook: '*', language: 'python' }], - // disable the default error handler - errorHandler: { - error: () => ({ action: ErrorAction.Continue }), - 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('/Users/ryannz/projj/github.com/difizen/libro/examples'), - }, - // synchronize: { - // fileEvents: [workspace.createFileSystemWatcher('**')], - // }, - }, - }); + protected createLanguageClient = ( + url: string, + options: LibroLanguageClientOptions, + ) => { + return LibroLanguageClient.createWebSocketLanguageClient(url, options); }; } diff --git a/packages/libro-lsp/src/adapters/notebook-adapter.ts b/packages/libro-lsp/src/adapters/notebook-adapter.ts index 1bf8fdd7a..3b90055ac 100644 --- a/packages/libro-lsp/src/adapters/notebook-adapter.ts +++ b/packages/libro-lsp/src/adapters/notebook-adapter.ts @@ -4,6 +4,7 @@ // Distributed under the terms of the Modified BSD License. import type * as nbformat from '@difizen/libro-common'; +import { MIME } from '@difizen/libro-common'; import type { CellView, CellViewChange, @@ -118,9 +119,7 @@ export class NotebookAdapter extends WidgetLSPAdapter { this._editorToCell.clear(); return this.editor.model.cells - .filter( - (item) => EditorCellView.is(item) && item.model.mimeType === 'text/x-python', - ) + .filter((item) => EditorCellView.is(item) && item.model.mimeType === MIME.python) .map((cell) => { return { ceEditor: this.getCellEditor(cell)!, diff --git a/packages/libro-search/src/libro-search-view.tsx b/packages/libro-search/src/libro-search-view.tsx index 285489ed9..4c0e0b0c7 100644 --- a/packages/libro-search/src/libro-search-view.tsx +++ b/packages/libro-search/src/libro-search-view.tsx @@ -259,6 +259,9 @@ export class LibroSearchView extends BaseView { this.toDispose.push( this.libro.model.onSourceChanged(() => this.onCellsChanged()), ); + this.toDispose.push( + this.libro.model.onCellViewChanged(() => this.onCellsChanged()), + ); } };