diff --git a/packages/libro-codemirror-code-cell/.eslintrc.mjs b/packages/libro-code-cell/.eslintrc.mjs similarity index 100% rename from packages/libro-codemirror-code-cell/.eslintrc.mjs rename to packages/libro-code-cell/.eslintrc.mjs diff --git a/packages/libro-code-cell/.fatherrc.ts b/packages/libro-code-cell/.fatherrc.ts new file mode 100644 index 00000000..a6745d8a --- /dev/null +++ b/packages/libro-code-cell/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types', { allowDeclareFields: true }], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-codemirror-code-cell/CHANGELOG.md b/packages/libro-code-cell/CHANGELOG.md similarity index 100% rename from packages/libro-codemirror-code-cell/CHANGELOG.md rename to packages/libro-code-cell/CHANGELOG.md diff --git a/packages/libro-codemirror-code-cell/README.md b/packages/libro-code-cell/README.md similarity index 100% rename from packages/libro-codemirror-code-cell/README.md rename to packages/libro-code-cell/README.md diff --git a/packages/libro-codemirror-raw-cell/babel.config.json b/packages/libro-code-cell/babel.config.json similarity index 100% rename from packages/libro-codemirror-raw-cell/babel.config.json rename to packages/libro-code-cell/babel.config.json diff --git a/packages/libro-codemirror-code-cell/jest.config.mjs b/packages/libro-code-cell/jest.config.mjs similarity index 100% rename from packages/libro-codemirror-code-cell/jest.config.mjs rename to packages/libro-code-cell/jest.config.mjs diff --git a/packages/libro-codemirror-raw-cell/package.json b/packages/libro-code-cell/package.json similarity index 94% rename from packages/libro-codemirror-raw-cell/package.json rename to packages/libro-code-cell/package.json index 5484a1c1..7c782947 100644 --- a/packages/libro-codemirror-raw-cell/package.json +++ b/packages/libro-code-cell/package.json @@ -1,5 +1,5 @@ { - "name": "@difizen/libro-codemirror-raw-cell", + "name": "@difizen/libro-code-cell", "version": "0.1.0", "description": "", "keywords": [ @@ -46,7 +46,6 @@ }, "dependencies": { "@difizen/libro-code-editor": "^0.1.0", - "@difizen/libro-codemirror": "^0.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-core": "^0.1.0", "@difizen/mana-app": "latest" diff --git a/packages/libro-codemirror-code-cell/src/code-cell-contribution.ts b/packages/libro-code-cell/src/code-cell-contribution.ts similarity index 81% rename from packages/libro-codemirror-code-cell/src/code-cell-contribution.ts rename to packages/libro-code-cell/src/code-cell-contribution.ts index 74432cd8..ddf401b6 100644 --- a/packages/libro-codemirror-code-cell/src/code-cell-contribution.ts +++ b/packages/libro-code-cell/src/code-cell-contribution.ts @@ -1,6 +1,6 @@ -import { inject, singleton } from '@difizen/mana-app'; -import type { CellMeta, CellModel, CellOptions } from '@difizen/libro-core'; import { CellModelContribution, CellViewContribution } from '@difizen/libro-core'; +import type { CellMeta, CellModel, CellOptions } from '@difizen/libro-core'; +import { inject, singleton } from '@difizen/mana-app'; import { CodeCellModelFactory } from './code-cell-protocol.js'; import { LibroCodeCellView } from './code-cell-view.js'; @@ -9,12 +9,7 @@ import { LibroCodeCellView } from './code-cell-view.js'; export class CodeEditorCellContribution implements CellModelContribution, CellViewContribution { - protected libroCellModelFactory: CodeCellModelFactory; - constructor( - @inject(CodeCellModelFactory) libroCellModelFactory: CodeCellModelFactory, - ) { - this.libroCellModelFactory = libroCellModelFactory; - } + @inject(CodeCellModelFactory) libroCellModelFactory: CodeCellModelFactory; cellMeta: CellMeta = { type: 'code', diff --git a/packages/libro-codemirror-code-cell/src/code-cell-model.ts b/packages/libro-code-cell/src/code-cell-model.ts similarity index 90% rename from packages/libro-codemirror-code-cell/src/code-cell-model.ts rename to packages/libro-code-cell/src/code-cell-model.ts index 7d41c1fa..460668bf 100644 --- a/packages/libro-codemirror-code-cell/src/code-cell-model.ts +++ b/packages/libro-code-cell/src/code-cell-model.ts @@ -1,10 +1,10 @@ -import type { ICodeCell } from '@difizen/libro-common'; import type { ExecutionCount } from '@difizen/libro-common'; -import { LibroCellModel, CellOptions } from '@difizen/libro-core'; +import type { ICodeCell } 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'; +import { inject, prop, transient, ViewManager } from '@difizen/mana-app'; import { Emitter } from '@difizen/mana-app'; -import { prop, ViewManager, inject, transient } from '@difizen/mana-app'; /** * 基础的可执行代码的cell, 带有执行能力 @@ -20,6 +20,8 @@ export class LibroCodeCellModel extends LibroCellModel implements ExecutableCell @prop() hasOutputsScrolled: boolean; + declare libroFormatType: string; + viewManager: ViewManager; // Emitter Msg @@ -48,7 +50,7 @@ export class LibroCodeCellModel extends LibroCellModel implements ExecutableCell return { id: this.id, cell_type: this.type, - source: this.value, + source: this.source, metadata: this.metadata, execution_count: this.executeCount, // outputs: this.outputs, diff --git a/packages/libro-codemirror-code-cell/src/code-cell-protocol.ts b/packages/libro-code-cell/src/code-cell-protocol.ts similarity index 100% rename from packages/libro-codemirror-code-cell/src/code-cell-protocol.ts rename to packages/libro-code-cell/src/code-cell-protocol.ts diff --git a/packages/libro-codemirror-code-cell/src/code-cell-view.tsx b/packages/libro-code-cell/src/code-cell-view.tsx similarity index 50% rename from packages/libro-codemirror-code-cell/src/code-cell-view.tsx rename to packages/libro-code-cell/src/code-cell-view.tsx index 90ebe63a..511fee3f 100644 --- a/packages/libro-codemirror-code-cell/src/code-cell-view.tsx +++ b/packages/libro-code-cell/src/code-cell-view.tsx @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-parameter-properties */ /* eslint-disable @typescript-eslint/parameter-properties */ -import { CodeEditorView } from '@difizen/libro-code-editor'; -import type { CodeEditorViewOptions, IRange } from '@difizen/libro-code-editor'; -import { CodeMirrorEditor, codeMirrorEditorFactory } from '@difizen/libro-codemirror'; +import type { CodeEditorViewOptions, CodeEditorView } from '@difizen/libro-code-editor'; +import { CodeEditorManager } from '@difizen/libro-code-editor'; import type { ICodeCell, IOutput } from '@difizen/libro-common'; import { isOutput } from '@difizen/libro-common'; import type { @@ -12,39 +11,73 @@ import type { } from '@difizen/libro-core'; import { CellService, + EditorStatus, LibroExecutableCellView, LibroOutputArea, + VirtualizedManager, } from '@difizen/libro-core'; import { Deferred } from '@difizen/mana-app'; -import { getOrigin, prop, useInject, watch } from '@difizen/mana-app'; -import { inject, transient } from '@difizen/mana-app'; import { + getOrigin, + inject, + prop, + transient, + useInject, view, ViewInstance, ViewManager, ViewOption, ViewRender, + watch, } from '@difizen/mana-app'; -import { forwardRef, memo } from 'react'; -import React, { useEffect } from 'react'; +import type { ViewSize } from '@difizen/mana-app'; +import React, { useEffect, useRef } from 'react'; import type { LibroCodeCellModel } from './code-cell-model.js'; +function countLines(inputString: string) { + const lines = inputString.split('\n'); + return lines.length; +} + const CellEditor: React.FC = () => { const instance = useInject(ViewInstance); + const virtualizedManager = useInject(VirtualizedManager); + const editorRef = useRef(null); + useEffect(() => { if (instance.editorView?.editor) { instance.editor = getOrigin(instance.editorView?.editor); } }, [instance, instance.editorView?.editor]); - return <>{instance.editorView && }; + + if (virtualizedManager.isVirtualized) { + instance.renderEditorIntoVirtualized = true; + const editorAreaHeight = instance.calcEditorAreaHeight(); + if (instance.setEditorHost) { + instance.setEditorHost(editorRef); + } + + return ( +
+ ); + } else { + return <>{instance.editorView && }; + } }; -export const CellEditorMemo = memo(CellEditor); +export const CellEditorMemo = React.memo(CellEditor); -const CodeEditorViewComponent = forwardRef( - function CodeEditorViewComponent(_props, ref) { +const CodeEditorViewComponent = React.forwardRef( + function CodeEditorViewComponent(props, ref) { const instance = useInject(ViewInstance); + return (
= new Deferred(); + + get editorReady() { + return this.editorViewReadyDeferred.promise; + } + protected outputAreaDeferred = new Deferred(); get outputAreaReady() { return this.outputAreaDeferred.promise; } - protected editorViewReadyDeferred: Deferred = new Deferred(); + override onViewResize(size: ViewSize) { + if (size.height) { + this.editorAreaHeight = size.height; + } + } - get editorReady() { - return this.editorViewReadyDeferred.promise; + calcEditorAreaHeight() { + if ( + this.editorStatus === EditorStatus.NOTLOADED || + this.editorStatus === EditorStatus.LOADING + ) { + const codeHeight = countLines(this.model.value) * 20; + const editorPadding = 12 + 18; + + const scrollbarHeight = 12; + + // TODO: 滚动条有条件显示 + + const editorAreaHeight = codeHeight + editorPadding + scrollbarHeight; + + this.editorAreaHeight = editorAreaHeight; + } + + // 编辑器已经加载的情况下cell高度都由对它的高度监听得到。 + return this.editorAreaHeight; } constructor( @inject(ViewOption) options: CellViewOptions, @inject(CellService) cellService: CellService, @inject(ViewManager) viewManager: ViewManager, + @inject(CodeEditorManager) codeEditorManager: CodeEditorManager, ) { super(options, cellService); this.options = options; this.viewManager = viewManager; + this.codeEditorManager = codeEditorManager; this.outputs = options.cell?.outputs as IOutput[]; this.className = this.className + ' code'; @@ -109,9 +187,7 @@ export class LibroCodeCellView extends LibroExecutableCellView { this.outputWatch(); return; }) - .catch(() => { - // - }); + .catch(console.error); } override outputWatch() { @@ -138,38 +214,53 @@ export class LibroCodeCellView extends LibroExecutableCellView { } } - createEditor() { + setEditorHost(ref: any) { + const editorHostId = this.parent.id + this.id; + + this.codeEditorManager.setEditorHostRef(editorHostId, ref); + } + + protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { - factory: (editorOption) => - codeMirrorEditorFactory({ - ...editorOption, - config: { - ...editorOption.config, - ...{ readOnly: this.parent.model.readOnly }, - }, - }), + editorHostId: this.parent.id + this.id, model: this.model, config: { readOnly: this.parent.model.readOnly, editable: !this.parent.model.readOnly, + placeholder: '请输入代码', }, }; - this.viewManager - .getOrCreateView(CodeEditorView, option) - .then((editorView) => { - this.editorView = editorView; - this.editorViewReadyDeferred.resolve(); - watch(this.parent.model, 'readOnly', () => { - this.editorView?.editor?.setOption('readOnly', this.parent.model.readOnly); - if (this.editorView?.editor instanceof CodeMirrorEditor) { - this.editorView?.editor.setOption('placeholder', '请输入代码'); - } - }); - return; - }) - .catch(() => { - // - }); + return option; + } + + protected async createEditor() { + const option = this.getEditorOption(); + + this.editorStatus = EditorStatus.LOADING; + + // 防止虚拟滚动中编辑器被频繁创建 + if (this.editorView) { + this.editorStatus = EditorStatus.LOADED; + return; + } + + const editorView = await this.codeEditorManager.getOrCreateEditorView(option); + + this.editorView = editorView; + this.editorViewReadyDeferred.resolve(); + this.editorStatus = EditorStatus.LOADED; + + await this.afterEditorReady(); + } + + 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)); } override shouldEnterEditorMode(e: React.FocusEvent) { @@ -190,29 +281,26 @@ export class LibroCodeCellView extends LibroExecutableCellView { } if (!this.editorView) { this.editorReady - .then(async () => { - await this.editorView?.editorReady; - this.editorView?.editor?.setOption('styleActiveLine', true); - this.editorView?.editor?.setOption('highlightActiveLineGutter', true); - if (this.editorView?.editor?.hasFocus()) { + .then(() => { + this.editorView?.editorReady.then(() => { + this.editorView?.editor.setOption('styleActiveLine', true); + this.editorView?.editor.setOption('highlightActiveLineGutter', true); + if (this.editorView?.editor.hasFocus()) { + return; + } + this.editorView?.editor.focus(); return; - } - this.editorView?.editor?.focus(); + }); return; }) - .catch(() => { - // - }); + .catch(console.error); } else { - if (!this.editorView?.editor) { - return; - } - this.editorView.editor.setOption('styleActiveLine', true); - this.editorView.editor.setOption('highlightActiveLineGutter', true); - if (this.editorView.editor.hasFocus()) { + this.editorView?.editor.setOption('styleActiveLine', true); + this.editorView?.editor.setOption('highlightActiveLineGutter', true); + if (this.editorView?.editor.hasFocus()) { return; } - this.editorView.editor.focus(); + this.editorView?.editor.focus(); } } else { if (this.container?.current?.parentElement?.contains(document.activeElement)) { @@ -223,18 +311,7 @@ export class LibroCodeCellView extends LibroExecutableCellView { }; override clearExecution = () => { - (this.model as LibroCodeCellModel).clearExecution(); + this.model.clearExecution(); this.outputArea.clear(); }; - - override getSelections = (): [] => { - return this.editor?.getSelections() as []; - }; - - override getSelectionsOffsetAt = (selection: IRange) => { - const isSelect = selection; - const start = this.editor?.getOffsetAt(isSelect.start) ?? 0; - const end = this.editor?.getOffsetAt(isSelect.end) ?? 0; - return { start: start, end: end }; - }; } diff --git a/packages/libro-codemirror-code-cell/src/index.ts b/packages/libro-code-cell/src/index.ts similarity index 100% rename from packages/libro-codemirror-code-cell/src/index.ts rename to packages/libro-code-cell/src/index.ts diff --git a/packages/libro-codemirror-code-cell/src/module.ts b/packages/libro-code-cell/src/module.ts similarity index 75% rename from packages/libro-codemirror-code-cell/src/module.ts rename to packages/libro-code-cell/src/module.ts index 5ade3fc5..e5b1634c 100644 --- a/packages/libro-codemirror-code-cell/src/module.ts +++ b/packages/libro-code-cell/src/module.ts @@ -1,16 +1,14 @@ -import { ManaModule } from '@difizen/mana-app'; +import { CodeEditorModule } from '@difizen/libro-code-editor'; import { CellOptions } from '@difizen/libro-core'; +import { ManaModule } from '@difizen/mana-app'; import { CodeEditorCellContribution } from './code-cell-contribution.js'; import { LibroCodeCellModel } from './code-cell-model.js'; import { CodeCellModelFactory } from './code-cell-protocol.js'; import { LibroCodeCellView } from './code-cell-view.js'; -export const CodeCellModule = ManaModule.create().register( - CodeEditorCellContribution, - LibroCodeCellView, - LibroCodeCellModel, - { +export const CodeCellModule = ManaModule.create() + .register(CodeEditorCellContribution, LibroCodeCellView, LibroCodeCellModel, { token: CodeCellModelFactory, useFactory: (ctx) => { return (options: CellOptions) => { @@ -23,5 +21,5 @@ export const CodeCellModule = ManaModule.create().register( return model; }; }, - }, -); + }) + .dependOn(CodeEditorModule); diff --git a/packages/libro-codemirror-code-cell/tsconfig.json b/packages/libro-code-cell/tsconfig.json similarity index 100% rename from packages/libro-codemirror-code-cell/tsconfig.json rename to packages/libro-code-cell/tsconfig.json diff --git a/packages/libro-code-editor/package.json b/packages/libro-code-editor/package.json index d79cb702..e258e994 100644 --- a/packages/libro-code-editor/package.json +++ b/packages/libro-code-editor/package.json @@ -46,10 +46,9 @@ }, "dependencies": { "@difizen/mana-app": "latest", + "@difizen/mana-l10n": "latest", "@difizen/libro-common": "^0.1.0", - "@difizen/libro-shared-model": "^0.1.0", - "uuid": "^9.0.0", - "vscode-languageserver-protocol": "^3.17.0" + "uuid": "^9.0.0" }, "peerDependencies": { "react": "^18.2.0" diff --git a/packages/libro-code-editor/src/code-editor-info-manager.ts b/packages/libro-code-editor/src/code-editor-info-manager.ts new file mode 100644 index 00000000..c1ced090 --- /dev/null +++ b/packages/libro-code-editor/src/code-editor-info-manager.ts @@ -0,0 +1,25 @@ +import { singleton } from '@difizen/mana-app'; + +@singleton() +export class CodeEditorInfoManager { + editorHostRefMap: Map; + + constructor() { + this.editorHostRefMap = new Map(); + } + + setEditorHostRef(id: string, ref: any) { + if (!this.editorHostRefMap) { + this.editorHostRefMap = new Map(); + } + + this.editorHostRefMap.set(id, ref); + } + + getEditorHostRef(id: string) { + if (!this.editorHostRefMap) { + return undefined; + } + return this.editorHostRefMap.get(id); + } +} diff --git a/packages/libro-code-editor/src/code-editor-manager.ts b/packages/libro-code-editor/src/code-editor-manager.ts new file mode 100644 index 00000000..306186d1 --- /dev/null +++ b/packages/libro-code-editor/src/code-editor-manager.ts @@ -0,0 +1,72 @@ +import type { Contribution } from '@difizen/mana-app'; +import { + Priority, + ViewManager, + contrib, + inject, + singleton, + Syringe, +} from '@difizen/mana-app'; + +import { CodeEditorInfoManager } from './code-editor-info-manager.js'; +import type { IModel } from './code-editor-model.js'; +import type { IEditor, IEditorOptions } from './code-editor-protocol.js'; +import type { CodeEditorViewOptions } from './code-editor-view.js'; +import { CodeEditorView } from './code-editor-view.js'; + +/** + * A factory used to create a code editor. + */ +export type CodeEditorFactory = (options: IEditorOptions) => IEditor; + +export const CodeEditorContribution = Syringe.defineToken('CodeEditorContribution'); +export interface CodeEditorContribution { + canHandle(mime: string): number; + factory: CodeEditorFactory; +} + +@singleton() +export class CodeEditorManager { + protected readonly codeEditorProvider: Contribution.Provider; + protected readonly viewManager: ViewManager; + protected codeEditorInfoManager: CodeEditorInfoManager; + + constructor( + @contrib(CodeEditorContribution) + codeEditorProvider: Contribution.Provider, + @inject(ViewManager) viewManager: ViewManager, + @inject(CodeEditorInfoManager) codeEditorInfoManager: CodeEditorInfoManager, + ) { + this.codeEditorProvider = codeEditorProvider; + this.viewManager = viewManager; + this.codeEditorInfoManager = codeEditorInfoManager; + } + + setEditorHostRef(id: string, ref: any) { + this.codeEditorInfoManager.setEditorHostRef(id, ref); + } + + protected findCodeEditorProvider(model: IModel) { + const prioritized = Priority.sortSync( + this.codeEditorProvider.getContributions(), + (contribution) => contribution.canHandle(model.mimeType), + ); + const sorted = prioritized.map((c) => c.value); + return sorted[0]; + } + + async getOrCreateEditorView(option: CodeEditorViewOptions): Promise { + const factory = this.findCodeEditorProvider(option.model)?.factory; + if (!factory) { + throw new Error(`no code editor found for mimetype: ${option.model.mimeType}`); + } + const editorView = await this.viewManager.getOrCreateView< + CodeEditorView, + CodeEditorViewOptions + >(CodeEditorView, { + factory, + ...option, + }); + return editorView; + } +} diff --git a/packages/libro-code-editor/src/model.ts b/packages/libro-code-editor/src/code-editor-model.ts similarity index 92% rename from packages/libro-code-editor/src/model.ts rename to packages/libro-code-editor/src/code-editor-model.ts index dbc4a8b1..f342406d 100644 --- a/packages/libro-code-editor/src/model.ts +++ b/packages/libro-code-editor/src/code-editor-model.ts @@ -1,11 +1,9 @@ import type { CellType } from '@difizen/libro-common'; import type { Disposable, Event } from '@difizen/mana-app'; -import { prop } from '@difizen/mana-app'; -import { Emitter } from '@difizen/mana-app'; -import { transient } from '@difizen/mana-app'; +import { prop, transient, Emitter } from '@difizen/mana-app'; import { v4 } from 'uuid'; -import type { ITextSelection } from './code-editor.js'; +import type { ITextSelection } from './code-editor-protocol.js'; export interface IModelOptions { /** @@ -53,6 +51,7 @@ export interface IModel extends Disposable { @transient() export class Model implements IModel { id: string; + /** * The text stored in the model. */ @@ -84,6 +83,7 @@ export class Model implements IModel { // this.sharedModel = models.createStandaloneCell(this.type, options.id) as models.ISharedText; // this.sharedModel.changed.connect(this._onSharedModelChanged, this); this.id = options?.id ?? v4(); + this.value = options?.value ?? ''; this.mimeType = options?.mimeType ?? 'text/plain'; this.selections = []; diff --git a/packages/libro-code-editor/src/code-editor.ts b/packages/libro-code-editor/src/code-editor-protocol.ts similarity index 71% rename from packages/libro-code-editor/src/code-editor.ts rename to packages/libro-code-editor/src/code-editor-protocol.ts index 4f662184..1c5dc9ee 100644 --- a/packages/libro-code-editor/src/code-editor.ts +++ b/packages/libro-code-editor/src/code-editor-protocol.ts @@ -1,38 +1,7 @@ import type { JSONObject } from '@difizen/libro-common'; -import type { Disposable, Event } from '@difizen/mana-app'; -import type { - DidChangeConfigurationParams, - ServerCapabilities, -} from 'vscode-languageserver-protocol'; +import type { Disposable, Event, ThemeType } from '@difizen/mana-app'; -import type { IModel } from './model.js'; - -/** - * Code editor accessor. - */ -export interface ILSPEditor { - /** - * CodeEditor getter. - * - * It will return `null` if the editor is not yet instantiated; - * e.g. to support windowed notebook. - */ - getEditor(): IEditor | null; - - /** - * Promise getter that resolved when the editor is instantiated. - */ - ready(): Promise; - - /** - * Reveal the code editor in viewport. - * - * ### Notes - * The promise will resolve when the editor is instantiated and in - * the viewport. - */ - reveal(): Promise; -} +import type { IModel } from './code-editor-model.js'; /** * A zero-based position in the editor. @@ -148,6 +117,11 @@ export interface ISelectionOwner { */ uuid: string; + /** + * Return selection value, if no range, return primary position value + */ + getSelectionValue: (range?: IRange) => string | undefined; + /** * Returns the primary position of the cursor, never `null`. */ @@ -194,6 +168,23 @@ export interface ISelectionOwner { * document. */ setSelections: (selections: IRange[]) => void; + + /** + * Replaces selection with the given text. + */ + replaceSelection: (text: string, range: IRange) => void; + + /** + * Replaces selection with the given text. + */ + replaceSelections: (edits: { text: string; range: IRange }[]) => void; + + /** + * highlight search matches + * @param matches + * @param currentIndex + */ + highlightMatches: (matches: SearchMatch[], currentIndex: number | undefined) => void; } /** @@ -289,15 +280,15 @@ export interface IEditor extends ISelectionOwner, Disposable { */ getOffsetAt: (position: IPosition) => number; - // /** - // * Find a position for the given offset. - // * - // * @param offset - The offset of interest. - // * - // * @returns The position at the offset, clamped to the extent of the - // * editor contents. - // */ - // getPositionAt: (offset: number) => IPosition | undefined; + /** + * Find a position for the given offset. + * + * @param offset - The offset of interest. + * + * @returns The position at the offset, clamped to the extent of the + * editor contents. + */ + getPositionAt: (offset: number) => IPosition | undefined; /** * Undo one edit (if any undo events are stored). @@ -334,6 +325,38 @@ export interface IEditor extends ISelectionOwner, Disposable { */ resizeToFit: () => void; + // /** + // * Add a keydown handler to the editor. + // * + // * @param handler - A keydown handler. + // * + // * @returns A disposable that can be used to remove the handler. + // */ + // addKeydownHandler: (handler: KeydownHandler) => Disposable; + + // /** + // * Reveals the given position in the editor. + // * + // * @param position - The desired position to reveal. + // */ + // revealPosition: (position: IPosition) => void; + + /** + * Reveals the given selection in the editor. + * + * @param position - The desired selection to reveal. + */ + revealSelection: (selection: IRange) => void; + + // /** + // * Get the window coordinates given a cursor position. + // * + // * @param position - The desired position. + // * + // * @returns The coordinates of the position. + // */ + // getCoordinateForPosition: (position: IPosition) => ICoordinate; + /** * Get the cursor position given window coordinates. * @@ -344,18 +367,37 @@ export interface IEditor extends ISelectionOwner, Disposable { */ getPositionForCoordinate: (coordinate: ICoordinate) => IPosition | null; + // /** + // * Get a list of tokens for the current editor text content. + // */ + // getTokens: () => IToken[]; + + // /** + // * Get the token at a given editor position. + // */ + // getTokenAt: (offset: number) => IToken; + + // /** + // * Get the token a the cursor position. + // */ + // getTokenAtCursor: () => IToken; + + // /** + // * Inserts a new line at the cursor position and indents it. + // */ + // newIndentedLine: () => void; + onModalChange: Event; } -/** - * A factory used to create a code editor. - */ -export type CodeEditorFactory = (options: IEditorOptions) => IEditor; +export type EditorTheme = Record; /** * The configuration options for an editor. */ export interface IEditorConfig { + value: string; + theme: EditorTheme; /** * Half-period in milliseconds used for cursor blinking. * By setting this to zero, blinking can be disabled. @@ -454,22 +496,30 @@ export interface IEditorConfig { highlightActiveLineGutter?: boolean; placeholder?: HTMLElement | string; + + lspEnabled: boolean; } /** * The default configuration options for an editor. */ export const defaultConfig: IEditorConfig = { + value: '', + theme: { + light: 'light', + dark: 'dark', + hc: 'hc-mana', + }, // Order matters as gutters will be sorted by the configuration order autoClosingBrackets: true, cursorBlinkRate: 530, fontFamily: null, - fontSize: null, + fontSize: 13, handlePaste: true, insertSpaces: true, lineHeight: null, lineNumbers: true, - lineWrap: 'on', + lineWrap: 'off', matchBrackets: true, readOnly: false, editable: true, @@ -477,10 +527,11 @@ export const defaultConfig: IEditorConfig = { rulers: [], showTrailingSpace: false, wordWrapColumn: 80, - codeFolding: false, + codeFolding: true, foldGutter: true, styleActiveLine: false, highlightActiveLineGutter: false, + lspEnabled: true, }; export type TooltipProviderOption = { cursorPosition: number }; @@ -498,134 +549,6 @@ export type CompletionProvider = ( option: CompletionProviderOption, ) => Promise; -export type LSPProviderResult = { - virtualDocument: IVirtualDocument; - lspConnection: ILspConnection; - editor: ILSPEditor; -}; - -export type LSPProvider = () => Promise; - -export interface Position { - /** - * Line number - */ - line: number; - - /** - * Position of character in line - */ - ch: number; -} - -/** - * is_* attributes are there only to enforce strict interface type checking - */ -export interface ISourcePosition extends Position { - isSource: true; -} - -export interface IEditorPosition extends Position { - isEditor: true; -} - -export interface IVirtualPosition extends Position { - isVirtual: true; -} - -export interface IRootPosition extends ISourcePosition { - isRoot: true; -} -export interface IVirtualDocument { - uri: string; - - /** - * Get the corresponding editor of the virtual line. - */ - getEditorAtVirtualLine: (pos: IVirtualPosition) => ILSPEditor; - transformEditorToVirtual: ( - editor: ILSPEditor, - position: IEditorPosition, - ) => IVirtualPosition | null; - ttransformVirtualToEditor: ( - virtualPosition: IVirtualPosition, - ) => IEditorPosition | null; -} - -export interface IDocumentInfo { - /** - * URI of the virtual document. - */ - uri: string; - - /** - * Version of the virtual document. - */ - version: number; - - /** - * Text content of the document. - */ - text: string; - - /** - * Language id of the document. - */ - languageId: string; -} -export interface ILspConnection { - /** - * Is the language server is connected? - */ - isConnected: boolean; - /** - * Is the language server is initialized? - */ - isInitialized: boolean; - - /** - * Is the language server is connected and initialized? - */ - isReady: boolean; - - /** - * Initialize a connection over a web socket that speaks the LSP protocol - */ - connect(socket: WebSocket): void; - - /** - * Close the connection - */ - close(): void; - - // This should support every method from https://microsoft.github.io/language-server-protocol/specification - /** - * The initialize request tells the server which options the client supports - */ - sendInitialize(): void; - /** - * Inform the server that the document was opened - */ - sendOpen(documentInfo: IDocumentInfo): void; - - /** - * Sends the full text of the document to the server - */ - sendChange(documentInfo: IDocumentInfo): void; - - /** - * Send save notification to the server. - */ - sendSaved(documentInfo: IDocumentInfo): void; - - /** - * Send configuration change to the server. - */ - sendConfigurationChange(settings: DidChangeConfigurationParams): void; - - provides(provider: keyof ServerCapabilities): boolean; -} - /** * The options used to initialize an editor. */ @@ -643,17 +566,17 @@ export interface IEditorOptions { /** * The desired uuid for the editor. */ - uuid?: string | undefined; + uuid?: string; /** * The default selection style for the editor. */ - selectionStyle?: Partial | undefined; + selectionStyle?: Partial; /** * The configuration options for the editor. */ - config?: Partial | undefined; + config?: Partial; // /** // * The configuration options for the editor. @@ -661,7 +584,21 @@ export interface IEditorOptions { // translator?: ITranslator; // - tooltipProvider?: TooltipProvider | undefined; - completionProvider?: CompletionProvider | undefined; - lspProvider?: LSPProvider | undefined; + tooltipProvider?: TooltipProvider; + completionProvider?: CompletionProvider; +} + +/** + * Base search match interface + */ +export interface SearchMatch { + /** + * Text of the exact match itself + */ + readonly text: string; + + /** + * Start location of the match (in a text, this is the column) + */ + position: number; } diff --git a/packages/libro-code-editor/src/code-editor-settings.ts b/packages/libro-code-editor/src/code-editor-settings.ts new file mode 100644 index 00000000..4e24be33 --- /dev/null +++ b/packages/libro-code-editor/src/code-editor-settings.ts @@ -0,0 +1,208 @@ +import { + inject, + singleton, + ApplicationContribution, + DisposableCollection, + Emitter, + ConfigurationContribution, + ConfigurationService, +} from '@difizen/mana-app'; +import type { + Disposable, + ConfigurationNode, + ConfigurationStorage, +} from '@difizen/mana-app'; +import { l10n } from '@difizen/mana-l10n'; + +import type { IEditorConfig } from './code-editor-protocol.js'; +import { defaultConfig } from './code-editor-protocol.js'; + +declare global { + interface ObjectConstructor { + typedKeys(obj: T): (keyof T)[]; + } +} +Object.typedKeys = Object.keys as any; + +const LibroUserSettingStorage: ConfigurationStorage = { + id: '__libro.user.storage__', + priority: 100, +}; + +const FontSize: ConfigurationNode = { + id: 'libro.user.codeeditor.fontsize', + description: l10n.t('代码编辑区域字体大小'), + title: l10n.t('代码字号'), + type: 'inputnumber', + defaultValue: defaultConfig.fontSize ?? 13, + schema: { + type: 'number', + }, + storage: LibroUserSettingStorage, +}; + +const LineHeight: ConfigurationNode = { + id: 'libro.user.codeeditor.lineheight', + description: l10n.t('代码编辑区域字体行高'), + title: l10n.t('代码行高'), + type: 'inputnumber', + defaultValue: defaultConfig.lineHeight ?? 20, + schema: { + type: 'number', + }, + storage: LibroUserSettingStorage, +}; + +const TabSize: ConfigurationNode = { + id: 'libro.user.codeeditor.tabsize', + description: l10n.t('tab转换为几个空格大小'), + title: l10n.t('tab大小'), + type: 'inputnumber', + defaultValue: defaultConfig.tabSize ?? 4, + schema: { + type: 'number', + }, + storage: LibroUserSettingStorage, +}; + +const InsertSpaces: ConfigurationNode = { + id: 'libro.user.codeeditor.insertspaces', + description: l10n.t('输入tab是否转换为空格'), + title: l10n.t('tab转空格'), + type: 'checkbox', + defaultValue: defaultConfig.insertSpaces, + schema: { + type: 'boolean', + }, + storage: LibroUserSettingStorage, +}; + +const LineWarp: ConfigurationNode<'wordWrapColumn' | 'off' | 'on' | 'bounded'> = { + id: 'libro.user.codeeditor.linewarp', + description: l10n.t(`自动换行策略: + - "off", lines will never wrap. + - "on", lines will wrap at the viewport border. + - "wordWrapColumn", lines will wrap at 'wordWrapColumn'. + - "bounded", lines will wrap at minimum between viewport width and wordWrapColumn.`), + title: l10n.t('自动换行'), + type: 'select', + defaultValue: defaultConfig.lineWrap, + schema: { + type: 'string', + enum: ['off', 'on', 'wordWrapColumn', 'bounded'], + }, + storage: LibroUserSettingStorage, +}; + +const WordWrapColumn: ConfigurationNode = { + id: 'libro.user.codeeditor.wordWrapColumn', + description: l10n.t('开启自动换行后,自动换行的列数'), + title: l10n.t('自动换行列数'), + type: 'inputnumber', + defaultValue: defaultConfig.wordWrapColumn, + schema: { + type: 'number', + }, + storage: LibroUserSettingStorage, +}; + +const LSPEnabled: ConfigurationNode = { + id: 'libro.user.codeeditor.lspenabled', + description: l10n.t( + '开启语言服务后,编辑器能提供更多辅助编码能力,包括:自动提示、代码诊断、hover提示、格式化、代码跳转、重命名等等(需要使用带有lsp服务的容器, 详情请咨询 @沧浪)', + ), + title: l10n.t('开启语言服务'), + type: 'checkbox', + defaultValue: defaultConfig.lspEnabled, + schema: { + type: 'boolean', + }, + storage: LibroUserSettingStorage, +}; + +export const CodeEditorSetting: { + [key in keyof IEditorConfig]?: ConfigurationNode; +} = { + fontSize: FontSize, + tabSize: TabSize, + insertSpaces: InsertSpaces, + lineHeight: LineHeight, + lineWrap: LineWarp, + wordWrapColumn: WordWrapColumn, + lspEnabled: LSPEnabled, +}; + +@singleton({ contrib: [ConfigurationContribution, ApplicationContribution] }) +export class CodeEditorSettings + implements ConfigurationContribution, ApplicationContribution, Disposable +{ + protected readonly configurationService: ConfigurationService; + + protected codeEditorSettingsChangeEmitter = new Emitter<{ + key: keyof IEditorConfig; + value: any; + }>(); + + onCodeEditorSettingsChange = this.codeEditorSettingsChangeEmitter.event; + + protected toDispose = new DisposableCollection(); + + constructor( + @inject(ConfigurationService) + configurationService: ConfigurationService, + ) { + this.configurationService = configurationService; + } + registerConfigurations() { + return [ + FontSize, + TabSize, + InsertSpaces, + LineHeight, + LineWarp, + WordWrapColumn, + LSPEnabled, + ]; + } + + onStart() { + this.handleEditorSettingsChange(); + } + + async fetchEditorSettings(): Promise> { + const result: Partial = {}; + Object.typedKeys(CodeEditorSetting).forEach(async (key) => { + result[key] = await this.configurationService.get(CodeEditorSetting[key]!); + }); + return result; + } + + protected handleEditorSettingsChange() { + this.toDispose.push( + this.configurationService.onConfigurationValueChange((e) => { + // const ids = Object.values(CodeEditorSetting).map(item => item.id); + const match = Object.entries(CodeEditorSetting).find( + (item) => item[1].id === e.key, + ); + if (match) { + this.codeEditorSettingsChangeEmitter.fire({ + key: match[0] as any, + value: e.value, + }); + } + }), + ); + } + + protected isDisposed = false; + get disposed() { + return this.isDisposed; + } + dispose() { + if (this.disposed) { + return; + } + this.toDispose.dispose(); + this.isDisposed = true; + } +} diff --git a/packages/libro-code-editor/src/code-editor-view.tsx b/packages/libro-code-editor/src/code-editor-view.tsx index c189b266..8ef1ef2d 100644 --- a/packages/libro-code-editor/src/code-editor-view.tsx +++ b/packages/libro-code-editor/src/code-editor-view.tsx @@ -1,23 +1,34 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + import { getOrigin, prop } from '@difizen/mana-app'; -import { BaseView, view, ViewOption } from '@difizen/mana-app'; -import { inject, transient } from '@difizen/mana-app'; -import { Deferred, Emitter } from '@difizen/mana-app'; +import { + inject, + transient, + Deferred, + Emitter, + BaseView, + ThemeService, + view, + ViewOption, +} from '@difizen/mana-app'; import { forwardRef, memo } from 'react'; -import type { IEditor } from './code-editor.js'; +import { CodeEditorInfoManager } from './code-editor-info-manager.js'; +import type { CodeEditorFactory } from './code-editor-manager.js'; +import type { IModel } from './code-editor-model.js'; import type { - CodeEditorFactory, + CompletionProvider, ICoordinate, IEditorConfig, IEditorSelectionStyle, - CompletionProvider, TooltipProvider, - LSPProvider, -} from './code-editor.js'; -import type { IModel } from './model.js'; +} from './code-editor-protocol.js'; +import type { IEditor } from './code-editor-protocol.js'; +import { CodeEditorSettings } from './code-editor-settings.js'; export const CodeEditorRender = memo( - forwardRef((_props, ref) => { + forwardRef((props, ref) => { return
; }), ); @@ -49,6 +60,11 @@ const leadingWhitespaceRe = /^\s+$/; @transient() @view('code-editor-view') export class CodeEditorView extends BaseView { + @inject(ThemeService) protected readonly themeService: ThemeService; + @inject(CodeEditorSettings) protected readonly codeEditorSettings: CodeEditorSettings; + + codeEditorInfoManager: CodeEditorInfoManager; + override view = CodeEditorRender; protected classlist: string[] = []; @@ -57,6 +73,8 @@ export class CodeEditorView extends BaseView { protected modalChangeEmitter = new Emitter(); + protected editorHostRef: any; + get onModalChange() { return this.modalChangeEmitter.event; } @@ -65,7 +83,7 @@ export class CodeEditorView extends BaseView { * Get the editor wrapped by the widget. */ @prop() - editor: IEditor | undefined; + editor: IEditor; protected editorReadyDeferred: Deferred = new Deferred(); get editorReady() { return this.editorReadyDeferred.promise; @@ -73,23 +91,36 @@ export class CodeEditorView extends BaseView { /** * Construct a new code editor widget. */ - constructor(@inject(ViewOption) options: CodeEditorViewOptions) { + constructor( + @inject(ViewOption) options: CodeEditorViewOptions, + @inject(CodeEditorInfoManager) codeEditorInfoManager: CodeEditorInfoManager, + ) { super(); this.options = options; + this.codeEditorInfoManager = codeEditorInfoManager; } - override onViewMount() { - const node = this.container?.current; - if (node) { + override async onViewMount() { + const settings = await this.codeEditorSettings.fetchEditorSettings(); + + const editorHostId = this.options.editorHostId; + const editorHostRef = editorHostId + ? this.codeEditorInfoManager.getEditorHostRef(editorHostId) + : undefined; + + this.editorHostRef = + editorHostRef && editorHostRef.current ? editorHostRef : this.container; + + if (this.editorHostRef.current && this.options.factory) { this.editor = this.options.factory({ - host: node, + ...this.options, + host: this.editorHostRef.current, model: this.options.model, uuid: this.options.uuid, - config: this.options.config, + config: { ...this.options.config, ...settings }, selectionStyle: this.options.selectionStyle, tooltipProvider: this.options.tooltipProvider, completionProvider: this.options.completionProvider, - lspProvider: this.options.lspProvider, }); this.editorReadyDeferred.resolve(); this.editor.onModalChange((val) => this.modalChangeEmitter.fire(val)); @@ -99,30 +130,48 @@ export class CodeEditorView extends BaseView { getOrigin(this.editor).focus(); } - node.addEventListener('focus', this.onViewActive); - node.addEventListener('dragenter', this._evtDragEnter); - node.addEventListener('dragleave', this._evtDragLeave); - node.addEventListener('dragover', this._evtDragOver); - node.addEventListener('drop', this._evtDrop); + this.editorHostRef.current.addEventListener('focus', this.onViewActive); + this.editorHostRef.current.addEventListener('dragenter', this._evtDragEnter); + this.editorHostRef.current.addEventListener('dragleave', this._evtDragLeave); + this.editorHostRef.current.addEventListener('dragover', this._evtDragOver); + this.editorHostRef.current.addEventListener('drop', this._evtDrop); + + this.toDispose.push( + this.codeEditorSettings.onCodeEditorSettingsChange((e) => { + this.editor.setOption(e.key, e.value); + }), + ); } } + removeChildNodes = (parent: any) => { + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + }; + override onViewUnmount() { - const node = this.container?.current; + const node = this.editorHostRef?.current; if (node) { node.removeEventListener('focus', this.onViewActive); node.removeEventListener('dragenter', this._evtDragEnter); node.removeEventListener('dragleave', this._evtDragLeave); node.removeEventListener('dragover', this._evtDragOver); node.removeEventListener('drop', this._evtDrop); + + this.removeChildNodes(node); } } + override onViewResize() { + this.editor?.resizeToFit(); + } + /** * Get the model used by the widget. */ - get model(): IModel | undefined { - return this.editor?.model; + get model(): IModel { + return this.editor.model; } /** @@ -133,11 +182,11 @@ export class CodeEditorView extends BaseView { return; } super.dispose(); - this.editor?.dispose(); + this.editor.dispose(); } protected onViewActive = (): void => { - this.editor?.focus(); + this.editor.focus(); }; /** @@ -145,7 +194,7 @@ export class CodeEditorView extends BaseView { */ protected onResize(): void { if (this.isVisible) { - this.editor?.resizeToFit(); + this.editor.resizeToFit(); } } @@ -164,9 +213,6 @@ export class CodeEditorView extends BaseView { * Handle a change in model selections. */ protected _onSelectionsChanged(): void { - if (!this.editor) { - return; - } const { start, end } = this.editor.getSelection(); if (start.column !== end.column || start.line !== end.line) { @@ -191,9 +237,6 @@ export class CodeEditorView extends BaseView { * Handle the `'lm-dragenter'` event for the widget. */ protected _evtDragEnter = (event: DragEvent): void => { - if (!this.editor) { - return; - } if (this.editor.getOption('readOnly') === true) { return; } @@ -210,9 +253,6 @@ export class CodeEditorView extends BaseView { * Handle the `'lm-dragleave'` event for the widget. */ protected _evtDragLeave = (event: DragEvent): void => { - if (!this.editor) { - return; - } this.removeClass(DROP_TARGET_CLASS); if (this.editor.getOption('readOnly') === true) { return; @@ -229,9 +269,6 @@ export class CodeEditorView extends BaseView { * Handle the `'lm-dragover'` event for the widget. */ protected _evtDragOver = (event: DragEvent): void => { - if (!this.editor) { - return; - } this.removeClass(DROP_TARGET_CLASS); if (this.editor.getOption('readOnly') === true) { return; @@ -250,9 +287,6 @@ export class CodeEditorView extends BaseView { * Handle the `'lm-drop'` event for the widget. */ protected _evtDrop = (event: DragEvent): void => { - if (!this.editor) { - return; - } if (this.editor.getOption('readOnly') === true) { return; } @@ -292,7 +326,7 @@ export class CodeEditorView extends BaseView { /** * The options used to initialize a code editor widget. */ -export interface CodeEditorViewOptions { +export interface CodeEditorViewOptions { /** * A code editor factory. * @@ -300,7 +334,12 @@ export interface CodeEditorViewOptions { * The widget needs a factory and a model instead of a `CodeEditor.IEditor` * object because it needs to provide its own node as the host. */ - factory: CodeEditorFactory; + factory?: CodeEditorFactory; + + /** + * where to mount the editor + */ + editorHostId?: string; /** * The model used to initialize the code editor. @@ -315,7 +354,7 @@ export interface CodeEditorViewOptions { /** * The configuration options for the editor. */ - config?: Partial; + config?: Partial; /** * The default selection style for the editor. @@ -324,9 +363,10 @@ export interface CodeEditorViewOptions { tooltipProvider?: TooltipProvider; completionProvider?: CompletionProvider; - lspProvider?: LSPProvider; autoFocus?: boolean; + + [key: string]: any; } /** diff --git a/packages/libro-code-editor/src/completer/completer-protocol.ts b/packages/libro-code-editor/src/completer/completer-protocol.ts deleted file mode 100644 index 172d23c9..00000000 --- a/packages/libro-code-editor/src/completer/completer-protocol.ts +++ /dev/null @@ -1,259 +0,0 @@ -import type { SourceChange } from '@difizen/libro-shared-model'; -import { Syringe } from '@difizen/mana-app'; -import type { View } from '@difizen/mana-app'; -import type { Event } from '@difizen/mana-app'; -import type React from 'react'; - -import type { IEditor } from '../code-editor.js'; - -/** - * The context which will be passed to the `fetch` function - * of a provider. - */ -export interface CompletionContext { - /** - * The widget (notebook, console, code editor) which invoked - * the completer - */ - widget: View; - - /** - * The current editor. - */ - editor?: IEditor | null; - - /** - * The session extracted from widget for convenience. - */ - // session?: ISessionConnection | null; -} - -/** - * An object describing a completion option injection into text. - */ -export interface IPatch { - /** - * The start of the range to be patched. - */ - start: number; - - /** - * The end of the range to be patched. - */ - end: number; - - /** - * The value to be patched in. - */ - value: string; -} - -/** - * Completion item object based off of LSP CompletionItem. - * Compared to the old kernel completions interface, this enhances the completions UI to support: - * - differentiation between inserted text and user facing text - * - documentation for each completion item to be displayed adjacently - * - deprecation styling - * - custom icons - * and other potential new features. - */ -export interface ICompletionItem { - /** - * User facing completion. - * If insertText is not set, this will be inserted. - */ - label: string; - - /** - * Completion to be inserted. - */ - insertText?: string; - - /** - * Type of this completion item. - */ - type?: string; - - /** - * LabIcon object for icon to be rendered with completion type. - */ - icon?: React.ReactNode; - - /** - * A human-readable string with additional information - * about this item, like type or symbol information. - */ - documentation?: string; - - /** - * Indicates if the item is deprecated. - */ - deprecated?: boolean; - - resolve?: (patch?: IPatch) => Promise; -} - -/** - * A reply to a completion items fetch request. - */ -export interface ICompletionItemsReply { - /** - * The starting index for the substring being replaced by completion. - */ - start: number; - /** - * The end index for the substring being replaced by completion. - */ - end: number; - /** - * A list of completion items. default to CompletionHandler.ICompletionItems - */ - items: T[]; -} - -/** - * The details of a completion request. - */ -export interface IRequest { - /** - * The cursor offset position within the text being completed. - */ - offset: number; - - /** - * The text being completed. - */ - text: string; -} - -export const CompletionProvider = new Syringe.DefinedToken('CompletionProvider'); - -/** - * The interface to implement a completer provider. - */ -export interface CompletionProvider { - /** - * Unique identifier of the provider - */ - readonly identifier: string; - - /** - * Renderer for provider's completions (optional). - */ - // readonly renderer?: Completer.IRenderer | null; - - /** - * Is completion provider applicable to specified context? - * @param request - the completion request text and details - * @param context - additional information about context of completion request - */ - isApplicable: (context: CompletionContext) => Promise; - - /** - * Fetch completion requests. - * - * @param request - the completion request text and details - * @param context - additional information about context of completion request - */ - fetch: ( - request: IRequest, - context: CompletionContext, - ) => Promise>; - - /** - * This method is called to customize the model of a completer widget. - * If it is not provided, the default model will be used. - * - * @param context - additional information about context of completion request - * @returns The completer model - */ - modelFactory?: (context: CompletionContext) => Promise>; - - /** - * Given an incomplete (unresolved) completion item, resolve it by adding - * all missing details, such as lazy-fetched documentation. - * - * @param completion - the completion item to resolve - * @param context - The context of the completer - * @param patch - The text which will be injected if the completion item is - * selected. - */ - resolve?: ( - completionItem: T, - context: CompletionContext, - patch?: IPatch | null, - ) => Promise; - - /** - * If users enable `autoCompletion` in setting, this method is - * called on text changed event of `CodeMirror` to check if the - * completion items should be shown. - * - * @param completerIsVisible - Current visibility status of the - * completer widget0 - * @param changed - changed text. - */ - shouldShowContinuousHint?: ( - completerIsVisible: boolean, - changed: SourceChange, - ) => boolean; -} - -export interface ICompletionProviderManager { - /** - * Register a completer provider with the manager. - * - * @param {CompletionProvider} provider - the provider to be registered. - */ - registerProvider: (provider: CompletionProvider) => void; - - /** - * Invoke the completer in the widget with provided id. - * - * @param {string} id - the id of notebook panel, console panel or code editor. - */ - invoke: (id: string) => void; - - /** - * Activate `select` command in the widget with provided id. - * - * @param {string} id - the id of notebook panel, console panel or code editor. - */ - select: (id: string) => void; - - /** - * Update completer handler of a widget with new context. - * - * @param newCompleterContext - The completion context. - */ - updateCompleter: (newCompleterContext: CompletionContext) => Promise; - - /** - * Signal emitted when active providers list is changed. - */ - activeProvidersChanged: Event; -} - -export interface IConnectorProxy { - /** - * Fetch response from multiple providers, If a provider can not return - * the response for a completer request before timeout, - * the result of this provider will be ignore. - * - * @param {CompletionHandler.IRequest} request - The completion request. - */ - fetch: (request: IRequest) => Promise<(ICompletionItemsReply | null)[]>; - - /** - * Check if completer should make request to fetch completion responses - * on user typing. If the provider with highest rank does not have - * `shouldShowContinuousHint` method, a default one will be used. - * - * @param completerIsVisible - The visible status of completer widget. - * @param changed - CodeMirror changed argument. - */ - shouldShowContinuousHint: ( - completerIsVisible: boolean, - changed: SourceChange, - ) => boolean; -} diff --git a/packages/libro-code-editor/src/index.spec.ts b/packages/libro-code-editor/src/index.spec.ts deleted file mode 100644 index 902dbfe6..00000000 --- a/packages/libro-code-editor/src/index.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import 'reflect-metadata'; -import assert from 'assert'; - -import { CodeEditorView, CodeEditorModule, defaultMimeType } from './index.js'; - -describe('libro-code-editor', () => { - it('#import', () => { - assert(CodeEditorView); - assert(CodeEditorModule); - assert(defaultMimeType); - }); -}); diff --git a/packages/libro-code-editor/src/index.ts b/packages/libro-code-editor/src/index.ts index 8b779f6f..44715b6f 100644 --- a/packages/libro-code-editor/src/index.ts +++ b/packages/libro-code-editor/src/index.ts @@ -1,5 +1,7 @@ -export * from './code-editor.js'; -export * from './mimetype.js'; +export * from './code-editor-manager.js'; +export * from './code-editor-model.js'; +export * from './code-editor-protocol.js'; +export * from './code-editor-settings.js'; export * from './code-editor-view.js'; +export * from './mimetype.js'; export * from './module.js'; -export * from './model.js'; diff --git a/packages/libro-code-editor/src/mimetype.ts b/packages/libro-code-editor/src/mimetype.ts index 8514bb36..8a3a3690 100644 --- a/packages/libro-code-editor/src/mimetype.ts +++ b/packages/libro-code-editor/src/mimetype.ts @@ -1,3 +1,6 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + import type { ILanguageInfoMetadata } from '@difizen/libro-common'; /** diff --git a/packages/libro-code-editor/src/module.ts b/packages/libro-code-editor/src/module.ts index bb56dcb9..2ee8a8d3 100644 --- a/packages/libro-code-editor/src/module.ts +++ b/packages/libro-code-editor/src/module.ts @@ -1,6 +1,17 @@ import { ManaModule } from '@difizen/mana-app'; +import { CodeEditorInfoManager } from './code-editor-info-manager.js'; +import { CodeEditorContribution, CodeEditorManager } from './code-editor-manager.js'; +import { Model } from './code-editor-model.js'; +import { CodeEditorSettings } from './code-editor-settings.js'; import { CodeEditorView } from './code-editor-view.js'; -import { Model } from './model.js'; -export const CodeEditorModule = ManaModule.create().register(CodeEditorView, Model); +export const CodeEditorModule = ManaModule.create() + .register( + CodeEditorInfoManager, + CodeEditorView, + CodeEditorManager, + Model, + CodeEditorSettings, + ) + .contribution(CodeEditorContribution); diff --git a/packages/libro-codemirror-code-cell/src/index.spec.ts b/packages/libro-codemirror-code-cell/src/index.spec.ts deleted file mode 100644 index 4014a4d3..00000000 --- a/packages/libro-codemirror-code-cell/src/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'assert'; - -import { LibroCodeCellView, CodeCellModelFactory } from './index.js'; -import 'reflect-metadata'; - -describe('libro-codemirror-code-cell', () => { - it('#import', () => { - assert(LibroCodeCellView); - assert(CodeCellModelFactory); - }); -}); diff --git a/packages/libro-codemirror-markdown-cell/src/index.spec.ts b/packages/libro-codemirror-markdown-cell/src/index.spec.ts deleted file mode 100644 index 8ba3b7db..00000000 --- a/packages/libro-codemirror-markdown-cell/src/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'assert'; - -import { MarkdownCell, MarkdownCellView } from './index.js'; -import 'reflect-metadata'; - -describe('libro-codemirror-markdown-cell', () => { - it('#import', () => { - assert(MarkdownCell); - assert(MarkdownCellView); - }); -}); diff --git a/packages/libro-codemirror-raw-cell/src/index.spec.ts b/packages/libro-codemirror-raw-cell/src/index.spec.ts deleted file mode 100644 index 9f6bbce7..00000000 --- a/packages/libro-codemirror-raw-cell/src/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'assert'; - -import { LibroRawCellView, LibroRawCellModel } from './index.js'; -import 'reflect-metadata'; - -describe('libro-codemirror-raw-cell', () => { - it('#import', () => { - assert(LibroRawCellView); - assert(LibroRawCellModel); - }); -}); diff --git a/packages/libro-codemirror/package.json b/packages/libro-codemirror/package.json index 986e04cb..c514121c 100644 --- a/packages/libro-codemirror/package.json +++ b/packages/libro-codemirror/package.json @@ -45,7 +45,6 @@ "lint:tsc": "tsc --noEmit" }, "dependencies": { - "@difizen/mana-app": "latest", "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.1.0", "@codemirror/lang-javascript": "^6.0.2", @@ -61,8 +60,11 @@ "@difizen/libro-code-editor": "^0.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-rendermime": "^0.1.0", + "@difizen/libro-lsp": "^0.1.0", + "@difizen/mana-app": "latest", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.1.4", + "highlight.js": "^11.9.0", "markdown-it": "^13.0.1", "uuid": "^9.0.0", "vscode-languageserver-protocol": "^3.17.0" diff --git a/packages/libro-codemirror/src/auto-complete/filter.ts b/packages/libro-codemirror/src/auto-complete/filter.ts index 5828c399..27a362ae 100644 --- a/packages/libro-codemirror/src/auto-complete/filter.ts +++ b/packages/libro-codemirror/src/auto-complete/filter.ts @@ -63,13 +63,11 @@ export class FuzzyMatcher { // at the start if (chars.length === 1) { const first = codePointAt(word, 0); - if (first === chars[0]) { - return [0, 0, codePointSize(first)]; - } - if (first === folded[0]) { - return [Penalty.CaseFold, 0, codePointSize(first)]; - } - return null; + return first === chars[0] + ? [0, 0, codePointSize(first)] + : first === folded[0] + ? [Penalty.CaseFold, 0, codePointSize(first)] + : null; } const direct = word.indexOf(this.pattern); if (direct === 0) { diff --git a/packages/libro-codemirror/src/auto-complete/index.ts b/packages/libro-codemirror/src/auto-complete/index.ts index bb1b0b48..6eaac4a6 100644 --- a/packages/libro-codemirror/src/auto-complete/index.ts +++ b/packages/libro-codemirror/src/auto-complete/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ import type { Extension, EditorState, StateEffect } from '@codemirror/state'; import { Prec } from '@codemirror/state'; import type { KeyBinding } from '@codemirror/view'; @@ -65,13 +66,11 @@ const completionKeymapExt = Prec.highest( /// returns `null`. export function completionStatus(state: EditorState): null | 'active' | 'pending' { const cState = state.field(completionState, false); - if (cState && cState.active.some((a) => a.state === State.Pending)) { - return 'pending'; - } - if (cState && cState.active.some((a) => a.state !== State.Inactive)) { - return 'active'; - } - return null; + return cState && cState.active.some((a) => a.state === State.Pending) + ? 'pending' + : cState && cState.active.some((a) => a.state !== State.Inactive) + ? 'active' + : null; } const completionArrayCache: WeakMap = diff --git a/packages/libro-codemirror/src/auto-complete/snippet.ts b/packages/libro-codemirror/src/auto-complete/snippet.ts index a7be46e9..e536c221 100644 --- a/packages/libro-codemirror/src/auto-complete/snippet.ts +++ b/packages/libro-codemirror/src/auto-complete/snippet.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable prefer-const */ + import { indentUnit } from '@codemirror/language'; import type { ChangeDesc, @@ -93,8 +94,13 @@ class Snippet { name = m[2] || m[3] || '', found = -1; for (let i = 0; i < fields.length; i++) { - const sameName = name ? fields[i].name === name : false; - if (seq !== null ? fields[i].seq === seq : sameName) { + if ( + seq !== null + ? fields[i].seq === seq + : name + ? fields[i].name === name + : false + ) { found = i; } } diff --git a/packages/libro-codemirror/src/auto-complete/state.ts b/packages/libro-codemirror/src/auto-complete/state.ts index f27318a5..304ce6c4 100644 --- a/packages/libro-codemirror/src/auto-complete/state.ts +++ b/packages/libro-codemirror/src/auto-complete/state.ts @@ -49,7 +49,7 @@ function sortOptions(active: readonly ActiveSource[], state: EditorState) { let match; for (const option of a.result.options) { if ((match = matcher.match(option.label))) { - if (option.boost !== undefined) { + if (option.boost) { match[0] += option.boost; } options.push(new Option(option, a, match)); @@ -68,9 +68,7 @@ function sortOptions(active: readonly ActiveSource[], state: EditorState) { !prev || prev.label !== opt.completion.label || prev.detail !== opt.completion.detail || - (prev.type !== null && - opt.completion.type !== null && - prev.type !== opt.completion.type) || + (prev.type && opt.completion.type && prev.type !== opt.completion.type) || prev.apply !== opt.completion.apply ) { result.push(opt); @@ -191,16 +189,15 @@ export class CompletionState { ) { active = this.active; } - let open; - if ( + + let open = tr.selection || active.some((a) => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) || !sameResults(active, this.active) - ) { - open = CompletionDialog.build(active, state, this.id, this.open, conf); - } else { - open = this.open && tr.docChanged ? this.open.map(tr.changes) : this.open; - } + ? CompletionDialog.build(active, state, this.id, this.open, conf) + : this.open && tr.docChanged + ? this.open.map(tr.changes) + : this.open; if ( !open && active.every((a) => a.state !== State.Pending) && @@ -277,13 +274,11 @@ export const enum State { } export function getUserEvent(tr: Transaction): 'input' | 'delete' | null { - if (tr.isUserEvent('input.type')) { - return 'input'; - } - if (tr.isUserEvent('delete.backward')) { - return 'delete'; - } - return null; + return tr.isUserEvent('input.type') + ? 'input' + : tr.isUserEvent('delete.backward') + ? 'delete' + : null; } export class ActiveSource { diff --git a/packages/libro-codemirror/src/auto-complete/view.ts b/packages/libro-codemirror/src/auto-complete/view.ts index faef2c29..0f69c19c 100644 --- a/packages/libro-codemirror/src/auto-complete/view.ts +++ b/packages/libro-codemirror/src/auto-complete/view.ts @@ -51,16 +51,12 @@ export function moveCompletionSelection( ); } const { length } = cState.open.options; - let selected; - if (cState.open.selected > -1) { - selected = cState.open.selected + step * (forward ? 1 : -1); - } else { - if (forward) { - selected = 0; - } else { - selected = length - 1; - } - } + let selected = + cState.open.selected > -1 + ? cState.open.selected + step * (forward ? 1 : -1) + : forward + ? 0 + : length - 1; if (selected < 0) { selected = by === 'page' ? 0 : length - 1; } else if (selected >= length) { @@ -238,9 +234,7 @@ export const completionPlugin = ViewPlugin.fromClass( return undefined; }, ) - .catch(() => { - // - }); + .catch(console.error); } scheduleAccept() { diff --git a/packages/libro-codemirror/src/completion.ts b/packages/libro-codemirror/src/completion.ts index 8f3956ed..5f0f6953 100644 --- a/packages/libro-codemirror/src/completion.ts +++ b/packages/libro-codemirror/src/completion.ts @@ -19,7 +19,7 @@ export const kernelCompletions: EditorCompletion = try { result = await Promise.any([ provider({ cursorPosition: context.pos }), - new Promise((_resolve, reject) => { + new Promise((resolve, reject) => { setTimeout(() => { reject(`request time out in ${timeout}ms`); }, timeout); @@ -45,7 +45,7 @@ export const kernelCompletions: EditorCompletion = return { label: item['text'] as string, type: item['type'] === '' ? undefined : (item['type'] as string), - } as Completion; + }; }); } else { items = result.matches.map((item) => { diff --git a/packages/libro-codemirror/src/config.ts b/packages/libro-codemirror/src/config.ts index 7f595ff5..cc6b7373 100644 --- a/packages/libro-codemirror/src/config.ts +++ b/packages/libro-codemirror/src/config.ts @@ -1,4 +1,4 @@ -import { defaultKeymap, historyKeymap, history } from '@codemirror/commands'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { pythonLanguage } from '@codemirror/lang-python'; import { bracketMatching, @@ -17,23 +17,23 @@ import { Compartment, EditorState, StateEffect } from '@codemirror/state'; import type { KeyBinding } from '@codemirror/view'; import { crosshairCursor, - rectangularSelection, - dropCursor, drawSelection, - highlightSpecialChars, - highlightActiveLineGutter, + dropCursor, EditorView, highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, keymap, lineNumbers, placeholder, + rectangularSelection, } from '@codemirror/view'; import type { IEditorConfig } from '@difizen/libro-code-editor'; import { + autocompletion, closeBrackets, closeBracketsKeymap, - autocompletion, completionKeymap, } from './auto-complete/index.js'; import { kernelCompletions } from './completion.js'; @@ -41,6 +41,9 @@ import type { IOptions } from './editor.js'; import { hyperLink } from './hyperlink.js'; import { indentationMarkers } from './indentation-markers/index.js'; import { FoldIcon, UnFoldIcon } from './libro-icon.js'; +import { lspPythonCompletion } from './lsp/completion.js'; +import { formatKeymap } from './lsp/format.js'; +import { lspLint, lspTooltip } from './lsp/index.js'; import { ensure } from './mode.js'; import { getTheme, defaultTheme } from './theme.js'; import { tabTooltip, tooltipKeymap } from './tooltip.js'; @@ -59,12 +62,6 @@ export interface CodeMirrorConfig extends IEditorConfig { */ mimetype?: string; - /** - * The theme to style the editor with. see editortheme.ts for an example - * of how to design a theme for CodeMirror 6. - */ - theme?: string; - // FIXME-TRANS: Handle theme localizable names // themeDisplayName?: string @@ -257,7 +254,7 @@ class FacetWrapper extends ExtensionBuilder { return this._facet.of(value); } - private _facet: Facet; + protected _facet: Facet; } /** * Extension builder that provides an extension depending @@ -274,8 +271,8 @@ class ConditionalExtension extends ExtensionBuilder { return value ? this._truthy : this._falsy; } - private _truthy: Extension; - private _falsy: Extension; + protected _truthy: Extension; + protected _falsy: Extension; } /** @@ -293,8 +290,8 @@ class GenConditionalExtension extends ExtensionBuilder { return this._builder.of(this._fn(value)); } - private _fn: (a: T) => boolean; - private _builder: ConditionalExtension; + protected _fn: (a: T) => boolean; + protected _builder: ConditionalExtension; } /** @@ -325,8 +322,8 @@ class ConfigurableBuilder implements IConfigurableBuilder { ); } - private _compartment: Compartment; - private _builder: IExtensionBuilder; + protected _compartment: Compartment; + protected _builder: IExtensionBuilder; } /* @@ -348,7 +345,7 @@ class ThemeBuilder implements IConfigurableBuilder { return this._compartment.reconfigure(getTheme(v)); } - private _compartment: Compartment; + protected _compartment: Compartment; } /* @@ -370,7 +367,7 @@ class PlaceHolderBuilder implements IConfigurableBuilder { return this._compartment.reconfigure(placeholder(v)); } - private _compartment: Compartment; + protected _compartment: Compartment; } /** @@ -508,17 +505,31 @@ export class EditorConfiguration { 'jupyterKernelCompletion', createConditionalBuilder( pythonLanguage.data.of({ - autocomplete: kernelCompletions(options['completionProvider']), + autocomplete: kernelCompletions(options.completionProvider), }), ), ], [ 'jupyterKernelTooltip', - createConditionalBuilder(tabTooltip(options['tooltipProvider'])), + createConditionalBuilder(tabTooltip(options.tooltipProvider)), ], ['indentationMarkers', createConditionalBuilder(indentationMarkers())], ['hyperLink', createConditionalBuilder(hyperLink)], ['placeholder', createPlaceHolderBuilder()], + [ + 'lspTooltip', + createConditionalBuilder(lspTooltip({ lspProvider: options.lspProvider })), + ], + [ + 'lspLint', + createConditionalBuilder(lspLint({ lspProvider: options.lspProvider })), + ], + [ + 'lspCompletion', + createConditionalBuilder( + lspPythonCompletion({ lspProvider: options.lspProvider }), + ), + ], ]); this._themeOverloaderSpec = { '&': {}, '.cm-line': {} }; this._themeOverloader = new Compartment(); @@ -607,6 +618,7 @@ export class EditorConfiguration { ...completionKeymap, ...lintKeymap, ...tooltipKeymap, + ...formatKeymap, ]; const keymapExt = builder!.of( @@ -635,7 +647,7 @@ export class EditorConfiguration { return extensions; } - private updateThemeOverload( + protected updateThemeOverload( config: Partial | Record, ): Extension { const { fontFamily, fontSize, lineHeight, lineWrap, wordWrapColumn } = config; @@ -679,11 +691,11 @@ export class EditorConfiguration { return EditorView.theme(this._themeOverloaderSpec); } - private get(key: string): IConfigurableBuilder | undefined { + protected get(key: string): IConfigurableBuilder | undefined { return this._configurableBuilderMap.get(key); } - private _configurableBuilderMap: Map; - private _themeOverloaderSpec: Record>; - private _themeOverloader: Compartment; + protected _configurableBuilderMap: Map; + protected _themeOverloaderSpec: Record>; + protected _themeOverloader: Compartment; } diff --git a/packages/libro-codemirror/src/editor-contribution.ts b/packages/libro-codemirror/src/editor-contribution.ts new file mode 100644 index 00000000..04576795 --- /dev/null +++ b/packages/libro-codemirror/src/editor-contribution.ts @@ -0,0 +1,15 @@ +import type { CodeEditorFactory } from '@difizen/libro-code-editor'; +import { CodeEditorContribution } from '@difizen/libro-code-editor'; +import { singleton } from '@difizen/mana-app'; + +import { codeMirrorEditorFactory } from './factory.js'; + +@singleton({ contrib: [CodeEditorContribution] }) +export class CodeMirrorEditorContribution implements CodeEditorContribution { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + canHandle(mime: string): number { + // default editor + return 50; + } + factory: CodeEditorFactory = codeMirrorEditorFactory; +} diff --git a/packages/libro-codemirror/src/editor.ts b/packages/libro-codemirror/src/editor.ts index 1c3c6076..f72db898 100644 --- a/packages/libro-codemirror/src/editor.ts +++ b/packages/libro-codemirror/src/editor.ts @@ -4,34 +4,36 @@ import type { ChangeSet, Extension, Range, - StateEffectType, StateCommand, + StateEffectType, Text, } from '@codemirror/state'; import { - Prec, - EditorState, EditorSelection, + EditorState, + Prec, StateEffect, StateField, } from '@codemirror/state'; -import { Decoration, EditorView } from '@codemirror/view'; import type { Command, DecorationSet, ViewUpdate } from '@codemirror/view'; +import { Decoration, EditorView } from '@codemirror/view'; +import { defaultConfig, defaultSelectionStyle } from '@difizen/libro-code-editor'; import type { + ICoordinate, IEditor, IEditorConfig, - IEditorSelectionStyle, - KeydownHandler, IEditorOptions, + IEditorSelectionStyle, IModel, IPosition, IRange, - ICoordinate, ITextSelection, IToken, + KeydownHandler, + SearchMatch, } from '@difizen/libro-code-editor'; -import { defaultSelectionStyle, defaultConfig } from '@difizen/libro-code-editor'; import { findFirstArrayIndex, removeAllWhereFromArray } from '@difizen/libro-common'; +import type { LSPProvider } from '@difizen/libro-lsp'; import { Disposable, Emitter } from '@difizen/mana-app'; import { getOrigin, watch } from '@difizen/mana-app'; import type { SyntaxNodeRef } from '@lezer/common'; @@ -90,7 +92,7 @@ export const codeMirrorDefaultConfig: Required = { ...defaultConfig, mode: 'null', mimetype: 'text/x-python', - theme: 'jupyter', + theme: { light: 'jupyter', dark: 'jupyter', hc: 'jupyter' }, smartIndent: true, electricChars: true, keyMap: 'default', @@ -108,7 +110,6 @@ export const codeMirrorDefaultConfig: Required = { styleSelectedText: true, selectionPointer: false, handlePaste: true, - lineWrap: 'off', // highlightActiveLineGutter: false, @@ -135,12 +136,21 @@ export const codeMirrorDefaultConfig: Required = { }; export class CodeMirrorEditor implements IEditor { + // highlight + protected highlightEffect: StateEffectType<{ + matches: SearchMatch[]; + currentIndex: number | undefined; + }>; + protected highlightMark: Decoration; + protected selectedMatchMark: Decoration; + protected highlightField: StateField; + /** * Construct a CodeMirror editor. */ constructor(options: IOptions) { this._editorConfig = new EditorConfiguration(options); - const host = (this.host = options['host']); + const host = (this.host = options.host); host.classList.add(EDITOR_CLASS); host.classList.add('jp-Editor'); @@ -148,7 +158,7 @@ export class CodeMirrorEditor implements IEditor { host.addEventListener('blur', this, true); host.addEventListener('scroll', this, true); - this._uuid = options['uuid'] || v4(); + this._uuid = options.uuid || v4(); // State and effects for handling the selection marks this._addMark = StateEffect.define(); @@ -186,20 +196,84 @@ export class CodeMirrorEditor implements IEditor { provide: (f) => EditorView.decorations.from(f), }); + // handle highlight + this.highlightEffect = StateEffect.define<{ + matches: SearchMatch[]; + currentIndex: number | undefined; + }>({ + map: (value, mapping) => ({ + matches: value.matches.map((v) => ({ + text: v.text, + position: mapping.mapPos(v.position), + })), + currentIndex: value.currentIndex, + }), + }); + this.highlightMark = Decoration.mark({ class: 'cm-searchMatch' }); + this.selectedMatchMark = Decoration.mark({ + class: 'cm-searchMatch cm-searchMatch-selected libro-selectedtext', + }); + this.highlightField = StateField.define({ + create: () => { + return Decoration.none; + }, + update: (highlights, transaction) => { + // eslint-disable-next-line no-param-reassign + highlights = highlights.map(transaction.changes); + for (const ef of transaction.effects) { + if (ef.is(this.highlightEffect)) { + const e = ef as StateEffect<{ + matches: SearchMatch[]; + currentIndex: number | undefined; + }>; + if (e.value.matches.length) { + // eslint-disable-next-line no-param-reassign + highlights = highlights.update({ + add: e.value.matches.map((m, index) => { + if (index === e.value.currentIndex) { + return this.selectedMatchMark.range( + m.position, + m.position + m.text.length, + ); + } + return this.highlightMark.range( + m.position, + m.position + m.text.length, + ); + }), + filter: (from, to) => { + return ( + !e.value.matches.some( + (m) => m.position >= from && m.position + m.text.length <= to, + ) || from === to + ); + }, + }); + } else { + // eslint-disable-next-line no-param-reassign + highlights = Decoration.none; + } + } + } + return highlights; + }, + provide: (f) => EditorView.decorations.from(f), + }); + // Handle selection style. - const style = options['selectionStyle'] || {}; + const style = options.selectionStyle || {}; this._selectionStyle = { ...defaultSelectionStyle, ...(style as IEditorSelectionStyle), }; - const model = (this._model = options['model']); + const model = (this._model = options.model); const config = options.config || {}; const fullConfig = (this._config = { ...codeMirrorDefaultConfig, ...config, - mimetype: options['model'].mimeType, + mimetype: options.model.mimeType, }); // this._initializeEditorBinding(); @@ -207,16 +281,13 @@ export class CodeMirrorEditor implements IEditor { // Extension for handling DOM events const domEventHandlers = EditorView.domEventHandlers({ keydown: (event: KeyboardEvent) => { - const index = findFirstArrayIndex( - this._keydownHandlers, - (handler: KeydownHandler) => { - if (handler(this, event) === true) { - event.preventDefault(); - return true; - } - return false; - }, - ); + const index = findFirstArrayIndex(this._keydownHandlers, (handler) => { + if (handler(this, event) === true) { + event.preventDefault(); + return true; + } + return false; + }); if (index === -1) { return this.onKeydown(event); } @@ -261,10 +332,6 @@ export class CodeMirrorEditor implements IEditor { watch(model, 'mimeType', this._onMimeTypeChanged); } - handleTooltipChange = (val: boolean) => { - this.modalChangeEmitter.fire(val); - }; - /** * Initialize the editor binding. */ @@ -277,9 +344,7 @@ export class CodeMirrorEditor implements IEditor { // }; // } - save: () => void = () => { - // - }; + save: () => void; /** * A signal emitted when either the top or bottom edge is requested. */ @@ -360,7 +425,7 @@ export class CodeMirrorEditor implements IEditor { /** * Tests whether the editor is disposed. */ - get isDisposed(): boolean { + get disposed(): boolean { return this._isDisposed; } @@ -368,7 +433,7 @@ export class CodeMirrorEditor implements IEditor { * Dispose of the resources held by the widget. */ dispose(): void { - if (this.isDisposed) { + if (this.disposed) { return; } this._isDisposed = true; @@ -394,7 +459,7 @@ export class CodeMirrorEditor implements IEditor { // Don't bother setting the option if it is already the same. if (this._config[option] !== value) { this._config[option] = value; - this._editorConfig.reconfigureExtension(this._editor, option as string, value); + this._editorConfig.reconfigureExtension(this._editor, option, value); } if (option === 'readOnly') { @@ -537,6 +602,16 @@ export class CodeMirrorEditor implements IEditor { return this.state.sliceDoc(fromOffset, toOffset); } + getSelectionValue(range?: IRange) { + const fromOffset = range + ? this.getOffsetAt(range.start) + : this.editor.state.selection.main.from; + const toOffset = range + ? this.getOffsetAt(range.end) + : this.editor.state.selection.main.to; + return this.state.sliceDoc(fromOffset, toOffset); + } + /** * Add a keydown handler to the editor. * @@ -547,10 +622,7 @@ export class CodeMirrorEditor implements IEditor { addKeydownHandler(handler: KeydownHandler): Disposable { this._keydownHandlers.push(handler); return Disposable.create(() => { - removeAllWhereFromArray( - this._keydownHandlers, - (val: KeydownHandler) => val === handler, - ); + removeAllWhereFromArray(this._keydownHandlers, (val) => val === handler); }); } @@ -682,10 +754,41 @@ export class CodeMirrorEditor implements IEditor { * * @param text The text to be inserted. */ - replaceSelection(text: string): void { - this.editor.dispatch(this.state.replaceSelection(text)); + replaceSelection(text: string, range: IRange): void { + this.editor.dispatch({ + changes: { + from: this.getOffsetAt(range.start), + to: this.getOffsetAt(range.end), + insert: text, + }, + }); + } + + replaceSelections(edits: { text: string; range: IRange }[]): void { + // const trans = this.state.replaceSelection(text); + this.editor.dispatch({ + changes: edits.map((item) => ({ + from: this.getOffsetAt(item.range.start), + to: this.getOffsetAt(item.range.end), + insert: item.text, + })), + }); } + highlightMatches(matches: SearchMatch[], currentIndex: number | undefined) { + const effects: StateEffect[] = [ + this.highlightEffect.of({ matches: matches, currentIndex: currentIndex }), + ]; + if (!this.state.field(this.highlightField, false)) { + effects.push(StateEffect.appendConfig.of([this.highlightField])); + } + this.editor.dispatch({ effects }); + } + + handleTooltipChange = (val: boolean) => { + this.modalChangeEmitter.fire(val); + }; + /** * Get a list of tokens for the current editor text content. */ @@ -806,10 +909,36 @@ export class CodeMirrorEditor implements IEditor { }); } + /** + * Handles a selections change. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected _onSelectionsChanged(args: ITextSelection[]): void { + // const uuid = args.key; + // if (uuid !== this.uuid) { + // this._cleanSelections(uuid); + // if (args.type !== 'remove' && args.newValue) { + // this._markSelections(uuid, args.newValue); + // } + // } + } + + /** + * Clean selections for the given uuid. + */ + protected _cleanSelections(uuid: string) { + this.editor.dispatch({ + effects: this._removeMark.of({ + uuid: uuid, + decorations: this._selectionMarkers[uuid], + }), + }); + } + protected _buildMarkDecoration( uuid: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _selections: ISelectionText[], + selections: ISelectionText[], ) { const decorations: Range[] = []; @@ -886,6 +1015,20 @@ export class CodeMirrorEditor implements IEditor { // }); // } + /** + * Marks selections. + */ + protected _markSelections(uuid: string, selections: ITextSelection[]) { + const sel = selections.map((selection) => ({ + from: this.getOffsetAt(selection.start), + to: this.getOffsetAt(selection.end), + style: selection.style, + })); + this.editor.dispatch({ + effects: this._addMark.of({ uuid: uuid, selections: sel }), + }); + } + /** * Handles a cursor activity event. */ @@ -943,7 +1086,6 @@ export class CodeMirrorEditor implements IEditor { if (update.docChanged) { this._lastChange = update.changes; } - this.model.value = update.state.doc.toJSON().join('\n'); } /** @@ -1013,13 +1155,47 @@ export class CodeMirrorEditor implements IEditor { this._caretHover = null; } } + /** + * Check for an out of sync editor. + */ + protected _checkSync(): void { + const change = this._lastChange; + if (!change) { + return; + } + this._lastChange = null; + const doc = this.doc; + if (doc.toString() === this._model.value) { + return; + } + + // void showDialog({ + // title: this._trans.__('Code Editor out of Sync'), + // body: this._trans.__( + // 'Please open your browser JavaScript console for bug report instructions', + // ), + // }); + console.warn( + 'If you are able and willing to publicly share the text or code in your editor, you can help us debug the "Code Editor out of Sync" message by pasting the following to the public issue at https://github.com/jupyterlab/jupyterlab/issues/2951. Please note that the data below includes the text/code in your editor.', + ); + console.warn( + JSON.stringify({ + model: this._model.value, + view: doc.toString(), + selections: this.getSelections(), + cursor: this.getCursorPosition(), + lineSep: this.state.facet(EditorState.lineSeparator), + change, + }), + ); + } protected _model: IModel; protected _editor: EditorView; protected _selectionMarkers: Record[]> = {}; - protected _caretHover: HTMLElement | null = null; + protected _caretHover: HTMLElement | null; protected _config: IConfig; - protected _hoverTimeout: number | undefined = undefined; - protected _hoverId: string | undefined = undefined; + protected _hoverTimeout: number; + protected _hoverId: string; protected _keydownHandlers = new Array(); protected _selectionStyle: IEditorSelectionStyle; protected _uuid = ''; @@ -1036,7 +1212,7 @@ export class CodeMirrorEditor implements IEditor { export type IConfig = CodeMirrorConfig; export interface IOptions extends IEditorOptions { - [x: string]: any; + lspProvider?: LSPProvider; /** * The configuration options for the editor. */ diff --git a/packages/libro-codemirror/src/hyperlink.ts b/packages/libro-codemirror/src/hyperlink.ts index a89e738c..e78c1f4a 100644 --- a/packages/libro-codemirror/src/hyperlink.ts +++ b/packages/libro-codemirror/src/hyperlink.ts @@ -12,7 +12,7 @@ export interface HyperLinkState { } class HyperLink extends WidgetType { - private readonly state: HyperLinkState; + protected readonly state: HyperLinkState; constructor(state: HyperLinkState) { super(); this.state = state; diff --git a/packages/libro-codemirror/src/indentation-markers/index.ts b/packages/libro-codemirror/src/indentation-markers/index.ts index f75b8ba9..b853490c 100644 --- a/packages/libro-codemirror/src/indentation-markers/index.ts +++ b/packages/libro-codemirror/src/indentation-markers/index.ts @@ -117,8 +117,8 @@ class IndentMarkersClass implements PluginValue { view: EditorView; decorations!: DecorationSet; - private unitWidth: number; - private currentLineNumber: number; + protected unitWidth: number; + protected currentLineNumber: number; constructor(view: EditorView) { this.view = view; @@ -149,7 +149,7 @@ class IndentMarkersClass implements PluginValue { } } - private generate(state: EditorState) { + protected generate(state: EditorState) { const builder = new RangeSetBuilder(); const lines = getVisibleLines(this.view, state); diff --git a/packages/libro-codemirror/src/indentation-markers/map.ts b/packages/libro-codemirror/src/indentation-markers/map.ts index f3c6e965..b053ab26 100644 --- a/packages/libro-codemirror/src/indentation-markers/map.ts +++ b/packages/libro-codemirror/src/indentation-markers/map.ts @@ -22,19 +22,19 @@ export interface IndentEntry { */ export class IndentationMap { /** The {@link EditorState} indentation is derived from. */ - private state: EditorState; + protected state: EditorState; /** The set of lines that are used as an entrypoint. */ - private lines: Set; + protected lines: Set; /** The internal mapping of line numbers to {@link IndentEntry} objects. */ - private map: Map; + protected map: Map; /** The width of the editor's indent unit. */ - private unitWidth: number; + protected unitWidth: number; /** The type of indentation to use (terminate at end of scope vs last non-empty line in scope) */ - private markerType: 'fullScope' | 'codeOnly'; + protected markerType: 'fullScope' | 'codeOnly'; /** * @param lines - The set of lines to get the indentation map for. @@ -96,7 +96,7 @@ export class IndentationMap { * @param col - The visual beginning whitespace width of the line. * @param level - The indentation level of the line. */ - private set(line: Line, col: number, level: number) { + protected set(line: Line, col: number, level: number) { const empty = !line.text.trim().length; const entry: IndentEntry = { line, col, level, empty }; this.map.set(entry.line.number, entry); @@ -109,7 +109,7 @@ export class IndentationMap { * * @param line - The {@link Line} to add to the map. */ - private add(line: Line) { + protected add(line: Line) { if (this.has(line)) { return this.get(line); } @@ -165,7 +165,7 @@ export class IndentationMap { * @param from - The {@link Line} to start from. * @param dir - The direction to search in. Either `1` or `-1`. */ - private closestNonEmpty(from: Line, dir: -1 | 1) { + protected closestNonEmpty(from: Line, dir: -1 | 1) { let lineNo = from.number + dir; while (dir === -1 ? lineNo >= 1 : lineNo <= this.state.doc.lines) { @@ -205,7 +205,7 @@ export class IndentationMap { * Finds the state's active block (via the current selection) and sets all * the active indent level for the lines in the block. */ - private findAndSetActiveLines() { + protected findAndSetActiveLines() { const currentLine = getCurrentLine(this.state); if (!this.has(currentLine)) { diff --git a/packages/libro-codemirror/src/index.ts b/packages/libro-codemirror/src/index.ts index 2619ff25..b424f3a0 100644 --- a/packages/libro-codemirror/src/index.ts +++ b/packages/libro-codemirror/src/index.ts @@ -4,8 +4,11 @@ import './style/variables.css'; export * from './config.js'; export * from './editor.js'; +export * from './lsp/index.js'; export * from './mode.js'; -export * from './theme.js'; +export * from './module.js'; export * from './factory.js'; export * from './monitor.js'; +export * from './theme.js'; + export * from './auto-complete/index.js'; diff --git a/packages/libro-codemirror/src/libro-icon.ts b/packages/libro-codemirror/src/libro-icon.ts deleted file mode 100644 index f2e347f3..00000000 --- a/packages/libro-codemirror/src/libro-icon.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const FoldIcon = - '1.通用/2.Icon图标/Line/Down'; -export const UnFoldIcon = - '1.通用/2.Icon图标/Line/Down收起'; diff --git a/packages/libro-codemirror/src/libro-icon.tsx b/packages/libro-codemirror/src/libro-icon.tsx new file mode 100644 index 00000000..559b7957 --- /dev/null +++ b/packages/libro-codemirror/src/libro-icon.tsx @@ -0,0 +1,4 @@ +export const FoldIcon = + '1.通用/2.Icon图标/Line/Down'; +export const UnFoldIcon = + '1.通用/2.Icon图标/Line/Down收起'; diff --git a/packages/libro-codemirror/src/lsp/completion.ts b/packages/libro-codemirror/src/lsp/completion.ts new file mode 100644 index 00000000..60972831 --- /dev/null +++ b/packages/libro-codemirror/src/lsp/completion.ts @@ -0,0 +1,171 @@ +import { pythonLanguage } from '@codemirror/lang-python'; +import { CompletionItemKind, CompletionTriggerKind } from '@difizen/libro-lsp'; + +import type { Completion, CompletionSource } from '../auto-complete/index.js'; + +import type { CMLSPExtension } from './protocol.js'; +import { offsetToPos, renderMarkupContent } from './util.js'; + +export type CompletionItemDetailReolve = ( + completion: Completion, +) => Node | null | Promise; + +const CompletionItemKindMap = Object.fromEntries( + Object.entries(CompletionItemKind).map(([key, value]) => [value, key]), +) as Record; + +function toSet(chars: Set) { + let preamble = ''; + let flat = Array.from(chars).join(''); + const words = /\w/.test(flat); + if (words) { + preamble += '\\w'; + flat = flat.replace(/\w/g, ''); + } + return `[${preamble}${flat.replace(/[^\w\s]/g, '\\$&')}]`; +} + +function prefixMatch(options: Completion[]) { + const first = new Set(); + const rest = new Set(); + + for (const { apply } of options) { + const [initial, ...restStr] = apply as string; + first.add(initial); + for (const char of restStr) { + rest.add(char); + } + } + + const source = toSet(first) + toSet(rest) + '*$'; + return [new RegExp('^' + source), new RegExp(source)]; +} + +export const lspPythonCompletion: CMLSPExtension = ({ lspProvider }) => { + const completionSource: CompletionSource = async (context) => { + /** + * 只在显式的使用tab触发时调用kernel completion + * 只在只在隐式的输入时触发时调用lsp completion + */ + if (!lspProvider || context.explicit === true) { + return null; + } + + const { virtualDocument: doc, lspConnection, editor } = await lspProvider(); + + const { state } = context; + let { pos } = context; + + if (!lspConnection.isReady || !lspConnection.provides('completionProvider')) { + return null; + } + + const { line, character } = offsetToPos(state.doc, pos); + + const rootPos = doc.transformFromEditorToRoot(editor, { + line, + ch: character, + isEditor: true, + }); + + if (!rootPos) { + return null; + } + + const virtualPos = doc.virtualPositionAtDocument(rootPos); + + const result = await lspConnection.clientRequests[ + 'textDocument/completion' + ].request({ + position: { line: virtualPos.line, character: virtualPos.ch }, + textDocument: { + uri: doc.documentInfo.uri, + }, + context: { + triggerKind: CompletionTriggerKind.Invoked, + }, + }); + + if (!result) { + return null; + } + + const items = 'items' in result ? result.items : result; + + let options = items.map((item) => { + const { detail, label, kind, textEdit, documentation, sortText, filterText } = + item; + const completion: Completion & { + filterText: string; + sortText?: string; + apply: string; + } = { + label, + detail, + apply: textEdit?.newText ?? label, + type: kind && CompletionItemKindMap[kind].toLowerCase(), + sortText: sortText ?? label, + filterText: filterText ?? label, + }; + if (documentation) { + const resolver: CompletionItemDetailReolve = async () => { + return renderMarkupContent(documentation); + }; + completion.info = resolver; + } else { + const resolver: CompletionItemDetailReolve = async () => { + const itemResult = + await lspConnection.clientRequests['completionItem/resolve'].request(item); + return itemResult.documentation + ? renderMarkupContent(itemResult.documentation) + : null; + }; + + completion.info = resolver; + } + return completion; + }); + + const [, match] = prefixMatch(options); + const token = context.matchBefore(match); + + // TODO: sort 方法需要进一步改进 + if (token) { + pos = token.from; + const word = token.text.toLowerCase(); + if (/^\w+$/.test(word)) { + options = options + .filter(({ filterText }) => filterText.toLowerCase().startsWith(word)) + .sort( + ({ apply: a, sortText: sortTexta }, { apply: b, sortText: sortTextb }) => { + switch (true) { + case sortTexta !== undefined && sortTextb !== undefined: + return sortTexta!.localeCompare(sortTextb!); + case a.startsWith(token.text) && !b.startsWith(token.text): + return -1; + case !a.startsWith(token.text) && b.startsWith(token.text): + return 1; + } + return 0; + }, + ); + } + } else { + options = options.sort(({ sortText: sortTexta }, { sortText: sortTextb }) => { + switch (true) { + case sortTexta !== undefined && sortTextb !== undefined: + return sortTexta!.localeCompare(sortTextb!); + } + return 0; + }); + } + + return { + from: pos, + options, + }; + }; + return pythonLanguage.data.of({ + autocomplete: completionSource, + }); +}; diff --git a/packages/libro-codemirror/src/lsp/format.ts b/packages/libro-codemirror/src/lsp/format.ts new file mode 100644 index 00000000..c7793dbb --- /dev/null +++ b/packages/libro-codemirror/src/lsp/format.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/no-parameter-properties */ +/* eslint-disable @typescript-eslint/parameter-properties */ +import type { TransactionSpec } from '@codemirror/state'; +import { StateEffect } from '@codemirror/state'; +import type { + Command, + EditorView, + KeyBinding, + PluginValue, + ViewUpdate, +} from '@codemirror/view'; +import { ViewPlugin } from '@codemirror/view'; + +import { insertCompletionText } from '../auto-complete/index.js'; + +import type { CMLSPExtension, LSPExtensionOptions } from './protocol.js'; +import { offsetToPos, posToOffset } from './util.js'; + +export const startFormatEffect = StateEffect.define(); + +export const formatCell: Command = (view: EditorView) => { + view.dispatch({ effects: startFormatEffect.of(true) }); + return true; +}; + +export const formatKeymap: readonly KeyBinding[] = [{ key: 'Alt-f', run: formatCell }]; + +class FormatPlugin implements PluginValue { + constructor( + readonly view: EditorView, + readonly options: LSPExtensionOptions, + ) {} + + update(update: ViewUpdate) { + for (const tr of update.transactions) { + for (const effect of tr.effects) { + if (effect.is(startFormatEffect)) { + this.doFormat(); + } + } + } + } + + async doFormat() { + const lspProvider = await this.options.lspProvider?.(); + if (!lspProvider) { + return; + } + // const { state } = this.view; + // const currentLine = state.doc.lineAt(state.selection.main.head).number; + + const { editor, virtualDocument, lspConnection } = lspProvider; + const virtualStartPos = virtualDocument.transformEditorToVirtual(editor, { + line: 0, + ch: 0, + isEditor: true, + }); + + const end = offsetToPos(this.view.state.doc, this.view.state.doc.length); + + const virtualEndPos = virtualDocument.transformEditorToVirtual(editor, { + line: end.line, + ch: end.character, + isEditor: true, + }); + + if (!virtualStartPos || !virtualEndPos) { + return; + } + + lspConnection.clientRequests['textDocument/rangeFormatting'] + .request({ + textDocument: { uri: virtualDocument.uri }, + range: { + start: { line: virtualStartPos.line, character: virtualStartPos.ch }, + end: { line: virtualEndPos.line, character: virtualEndPos.ch }, + }, + options: { + tabSize: this.view.state.tabSize, + insertSpaces: true, + }, + }) + .then((result) => { + if (result && result?.length) { + const items = result; + const transaction: TransactionSpec[] = []; + items.forEach((item) => { + const defaultNewLine = { + line: end.line + 1, + ch: 0, + }; + const editorStart = + virtualDocument.transformVirtualToEditor({ + line: item.range.start.line, + ch: item.range.start.character, + isVirtual: true, + }) ?? defaultNewLine; + const editorEnd = + virtualDocument.transformVirtualToEditor({ + line: item.range.end.line, + ch: item.range.end.character, + isVirtual: true, + }) ?? defaultNewLine; + + if (!editorStart || !editorEnd) { + return; + } + const from = posToOffset(this.view.state.doc, { + line: editorStart.line, + character: editorStart.ch, + }); + const to = posToOffset(this.view.state.doc, { + line: editorEnd.line, + character: editorEnd.ch, + }); + // FIXME: 需要处理新增行的情况,目前在virtualdocument无法处理 + // console.log('format', item.range, editorStart, editorEnd, from, to); + if (from !== undefined && to !== undefined) { + const trans = insertCompletionText( + this.view.state, + item.newText, + from, + to, + ); + transaction.push(trans); + } + }); + // console.log(transaction, 'format trans'); + + this.view.dispatch(...transaction); + } + return; + }) + .catch(console.error); + } + + destroy() { + // + } +} + +export const lspFormat: CMLSPExtension = (options) => { + return [ViewPlugin.define((view) => new FormatPlugin(view, options))]; +}; diff --git a/packages/libro-codemirror/src/lsp/index.ts b/packages/libro-codemirror/src/lsp/index.ts new file mode 100644 index 00000000..8ba5beb2 --- /dev/null +++ b/packages/libro-codemirror/src/lsp/index.ts @@ -0,0 +1,6 @@ +export * from './lint.js'; +export * from './protocol.js'; +export * from './tooltip.js'; +export * from './util.js'; +export * from './completion.js'; +export * from './format.js'; diff --git a/packages/libro-codemirror/src/lsp/lint.ts b/packages/libro-codemirror/src/lsp/lint.ts new file mode 100644 index 00000000..a3bdb4ed --- /dev/null +++ b/packages/libro-codemirror/src/lsp/lint.ts @@ -0,0 +1,122 @@ +import type { Diagnostic } from '@codemirror/lint'; +import { setDiagnostics } from '@codemirror/lint'; +import type { PluginValue, EditorView } from '@codemirror/view'; +import { ViewPlugin } from '@codemirror/view'; +import { DiagnosticSeverity } from '@difizen/libro-lsp'; + +import type { CMLSPExtension, LSPExtensionOptions } from './protocol.js'; +import { posToOffset } from './util.js'; + +class LintPlugin implements PluginValue { + constructor( + readonly view: EditorView, + readonly options: LSPExtensionOptions, + ) { + this.processDiagnostic(); + } + + processDiagnostic() { + if (!this.options.lspProvider) { + return; + } + this.options + .lspProvider() + .then(({ lspConnection, virtualDocument, editor }) => { + lspConnection.serverNotifications['textDocument/publishDiagnostics'].event( + (e) => { + const diagnostics = e.diagnostics + .map(({ range, message, severity = DiagnosticSeverity.Information }) => { + const currentEditor = virtualDocument.getEditorAtVirtualLine({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + + // the diagnostic range must be in current editor + if (editor !== currentEditor) { + return; + } + + const editorStart = virtualDocument.transformVirtualToEditor({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + + let offset: number | undefined; + if (editorStart) { + offset = posToOffset(this.view.state.doc, { + line: editorStart.line, + character: editorStart.ch, + })!; + } + + const editorEnd = virtualDocument.transformVirtualToEditor({ + line: range.end.line, + ch: range.end.character, + isVirtual: true, + }); + + let end: number | undefined; + if (editorEnd) { + end = posToOffset(this.view.state.doc, { + line: editorEnd.line, + character: editorEnd.ch, + }); + } + return { + from: offset, + to: end, + severity: ( + { + [DiagnosticSeverity.Error]: 'error', + [DiagnosticSeverity.Warning]: 'warning', + [DiagnosticSeverity.Information]: 'info', + [DiagnosticSeverity.Hint]: 'info', + } as const + )[severity], + message, + } as Diagnostic; + }) + .filter(isDiagnostic) + .sort((a, b) => { + switch (true) { + case a.from < b.from: + return -1; + case a.from > b.from: + return 1; + } + return 0; + }); + + this.view.dispatch(setDiagnostics(this.view.state, diagnostics)); + }, + ); + return; + }) + .catch(console.error); + } + + update() { + // + } + + destroy() { + // + } +} + +export const lspLint: CMLSPExtension = (options) => { + return [ViewPlugin.define((view) => new LintPlugin(view, options))]; +}; + +function isDiagnostic(item: any): item is Diagnostic { + return ( + item !== undefined && + item !== null && + item.from !== null && + item.to !== null && + item.from !== undefined && + item.to !== undefined + ); +} diff --git a/packages/libro-codemirror/src/lsp/protocol.ts b/packages/libro-codemirror/src/lsp/protocol.ts new file mode 100644 index 00000000..ab05a33e --- /dev/null +++ b/packages/libro-codemirror/src/lsp/protocol.ts @@ -0,0 +1,8 @@ +import type { Extension } from '@codemirror/state'; +import type { LSPProvider } from '@difizen/libro-lsp'; + +export interface LSPExtensionOptions { + lspProvider?: LSPProvider; +} + +export type CMLSPExtension = (option: LSPExtensionOptions) => Extension; diff --git a/packages/libro-codemirror/src/lsp/tooltip.ts b/packages/libro-codemirror/src/lsp/tooltip.ts new file mode 100644 index 00000000..25175981 --- /dev/null +++ b/packages/libro-codemirror/src/lsp/tooltip.ts @@ -0,0 +1,76 @@ +import { hoverTooltip } from '@codemirror/view'; + +import type { CMLSPExtension } from './protocol.js'; +import { offsetToPos, posToOffset, renderMarkupContent } from './util.js'; + +export const lspTooltip: CMLSPExtension = (options) => { + return hoverTooltip(async (view, pos) => { + if (!options.lspProvider) { + return null; + } + + const { + lspConnection: connection, + virtualDocument: doc, + editor, + } = await options.lspProvider(); + + if (!connection.isReady || !connection.provides('hoverProvider')) { + return null; + } + + const { line, character } = offsetToPos(view.state.doc, pos); + + const virtualPos = doc.transformEditorToVirtual(editor, { + line, + ch: character, + isEditor: true, + }); + + if (!virtualPos) { + return null; + } + + const result = await connection.clientRequests['textDocument/hover'].request({ + position: { line: virtualPos.line, character: virtualPos.ch }, + textDocument: { + uri: doc.documentInfo.uri, + }, + }); + if (!result) { + return null; + } + const { contents, range } = result; + let offset = posToOffset(view.state.doc, { line, character })!; + let end; + if (range) { + const editorStart = doc.transformVirtualToEditor({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + + if (editorStart) { + offset = posToOffset(view.state.doc, { + line: editorStart.line, + character: editorStart.ch, + })!; + } + const editorEnd = doc.transformVirtualToEditor({ + line: range.end.line, + ch: range.end.character, + isVirtual: true, + }); + if (editorEnd) { + end = posToOffset(view.state.doc, { + line: editorEnd.line, + character: editorEnd.ch, + }); + } + } + + const dom = renderMarkupContent(contents); + + return { pos: offset, end, create: () => ({ dom }), above: false }; + }); +}; diff --git a/packages/libro-codemirror/src/lsp/util.ts b/packages/libro-codemirror/src/lsp/util.ts new file mode 100644 index 00000000..1362e32f --- /dev/null +++ b/packages/libro-codemirror/src/lsp/util.ts @@ -0,0 +1,69 @@ +import type { Text } from '@codemirror/state'; +import hljs from 'highlight.js'; +import MarkdownIt from 'markdown-it'; +import type * as lsp from 'vscode-languageserver-protocol'; +import 'highlight.js/styles/github.css'; + +export function posToOffset(doc: Text, pos: { line: number; character: number }) { + if (pos.line >= doc.lines) { + return; + } + const offset = doc.line(pos.line + 1).from + pos.character; + if (offset > doc.length) { + return; + } + return offset; +} + +export function offsetToPos(doc: Text, offset: number) { + const line = doc.lineAt(offset); + return { + line: line.number - 1, + character: offset - line.from, + }; +} + +export function formatContents( + contents: lsp.MarkupContent | lsp.MarkedString | lsp.MarkedString[], +): string { + if (Array.isArray(contents)) { + return contents.map((c) => formatContents(c) + '\n\n').join(''); + } else if (typeof contents === 'string') { + return contents; + } else { + return contents.value; + } +} + +export const renderMarkdownContent = (val: string) => { + const render = new MarkdownIt({ + html: true, + linkify: true, + breaks: true, + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + const hl = hljs.highlight(lang, str).value; + return hl; + } catch (__) { + // + } + } + + return ''; // use external default escaping + }, + }); + + return render.render(val); +}; + +export const renderMarkupContent = ( + contents: lsp.MarkupContent | lsp.MarkedString | lsp.MarkedString[], +) => { + const dom = document.createElement('div'); + dom.classList.add('documentation'); + + const res = renderMarkdownContent(formatContents(contents)); + dom.innerHTML = res; + return dom; +}; diff --git a/packages/libro-codemirror/src/mode.ts b/packages/libro-codemirror/src/mode.ts index 46b92cfc..ee0d0b6b 100644 --- a/packages/libro-codemirror/src/mode.ts +++ b/packages/libro-codemirror/src/mode.ts @@ -1,6 +1,6 @@ import { markdown } from '@codemirror/lang-markdown'; -import { LanguageDescription } from '@codemirror/language'; import type { LanguageSupport } from '@codemirror/language'; +import { LanguageDescription } from '@codemirror/language'; import { defaultMimeType } from '@difizen/libro-code-editor'; import { PathExt } from '@difizen/libro-common'; import { highlightTree } from '@lezer/highlight'; diff --git a/packages/libro-codemirror/src/module.ts b/packages/libro-codemirror/src/module.ts new file mode 100644 index 00000000..f76fa597 --- /dev/null +++ b/packages/libro-codemirror/src/module.ts @@ -0,0 +1,8 @@ +import { CodeEditorModule } from '@difizen/libro-code-editor'; +import { ManaModule } from '@difizen/mana-app'; + +import { CodeMirrorEditorContribution } from './editor-contribution.js'; + +export const CodeMirrorEditorModule = ManaModule.create() + .register(CodeMirrorEditorContribution) + .dependOn(CodeEditorModule); diff --git a/packages/libro-codemirror/src/theme.ts b/packages/libro-codemirror/src/theme.ts index e4b20b87..0b08c30f 100644 --- a/packages/libro-codemirror/src/theme.ts +++ b/packages/libro-codemirror/src/theme.ts @@ -14,7 +14,7 @@ export const jupyterEditorTheme = EditorView.theme({ * these things differently. */ '&': { - background: 'var(--jp-layout-color0)', + background: 'var(--mana-libro-input-background)', color: 'var(--mana-libro-text-default-color)', }, @@ -88,9 +88,9 @@ export const jupyterEditorTheme = EditorView.theme({ lineHeight: '20px', }, - // '.cm-editor': { - // paddingTop: '24px', - // }, + '.cm-editor': { + background: 'var(--mana-libro-input-background)', + }, '.cm-searchMatch': { backgroundColor: 'var(--jp-search-unselected-match-background-color)', diff --git a/packages/libro-codemirror/src/tooltip.ts b/packages/libro-codemirror/src/tooltip.ts index 454ac665..f9dc4cb3 100644 --- a/packages/libro-codemirror/src/tooltip.ts +++ b/packages/libro-codemirror/src/tooltip.ts @@ -43,7 +43,7 @@ const tooltipField = StateField.define({ return null; }, - update(_tooltips, tr) { + update(tooltips, tr) { const { effects } = tr; for (const effect of effects) { if (effect.is(closeTooltipEffect)) { @@ -119,9 +119,7 @@ class TooltipPlugin implements PluginValue { }); return undefined; }) - .catch(() => { - // - }); + .catch(console.error); } } }); diff --git a/packages/libro-codemirror-markdown-cell/.eslintrc.mjs b/packages/libro-cofine-editor-contribution/.eslintrc.mjs similarity index 100% rename from packages/libro-codemirror-markdown-cell/.eslintrc.mjs rename to packages/libro-cofine-editor-contribution/.eslintrc.mjs diff --git a/packages/libro-codemirror-code-cell/.fatherrc.ts b/packages/libro-cofine-editor-contribution/.fatherrc.ts similarity index 100% rename from packages/libro-codemirror-code-cell/.fatherrc.ts rename to packages/libro-cofine-editor-contribution/.fatherrc.ts diff --git a/packages/libro-codemirror-markdown-cell/CHANGELOG.md b/packages/libro-cofine-editor-contribution/CHANGELOG.md similarity index 100% rename from packages/libro-codemirror-markdown-cell/CHANGELOG.md rename to packages/libro-cofine-editor-contribution/CHANGELOG.md diff --git a/packages/libro-codemirror-markdown-cell/README.md b/packages/libro-cofine-editor-contribution/README.md similarity index 100% rename from packages/libro-codemirror-markdown-cell/README.md rename to packages/libro-cofine-editor-contribution/README.md diff --git a/packages/libro-codemirror-code-cell/babel.config.json b/packages/libro-cofine-editor-contribution/babel.config.json similarity index 100% rename from packages/libro-codemirror-code-cell/babel.config.json rename to packages/libro-cofine-editor-contribution/babel.config.json diff --git a/packages/libro-codemirror-markdown-cell/jest.config.mjs b/packages/libro-cofine-editor-contribution/jest.config.mjs similarity index 100% rename from packages/libro-codemirror-markdown-cell/jest.config.mjs rename to packages/libro-cofine-editor-contribution/jest.config.mjs diff --git a/packages/libro-cofine-editor-contribution/package.json b/packages/libro-cofine-editor-contribution/package.json new file mode 100644 index 00000000..a9cc129c --- /dev/null +++ b/packages/libro-cofine-editor-contribution/package.json @@ -0,0 +1,54 @@ +{ + "name": "@difizen/libro-cofine-editor-contribution", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook", + "monaco" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/mana-app": "latest" + }, + "devDependencies": { + "@difizen/monaco-editor-core": "latest" + } +} diff --git a/packages/libro-cofine-editor-contribution/src/editor-options-registry.ts b/packages/libro-cofine-editor-contribution/src/editor-options-registry.ts new file mode 100644 index 00000000..352faf65 --- /dev/null +++ b/packages/libro-cofine-editor-contribution/src/editor-options-registry.ts @@ -0,0 +1,15 @@ +import { singleton } from '@difizen/mana-app'; +import type monaco from '@difizen/monaco-editor-core'; + +@singleton() +export class EditorOptionsRegistry { + protected optionsMap = new Map(); + + set(uri: monaco.Uri, data: any) { + this.optionsMap.set(uri, data); + } + + get(uri: monaco.Uri) { + return this.optionsMap.get(uri); + } +} diff --git a/packages/libro-cofine-editor-contribution/src/index.ts b/packages/libro-cofine-editor-contribution/src/index.ts new file mode 100644 index 00000000..c030ed1e --- /dev/null +++ b/packages/libro-cofine-editor-contribution/src/index.ts @@ -0,0 +1,26 @@ +import type { Disposable } from '@difizen/mana-app'; +import { Syringe } from '@difizen/mana-app'; +import type monaco from '@difizen/monaco-editor-core'; + +export const EditorHandlerContribution = Syringe.defineToken( + 'LanguageWorkerContribution', +); + +export interface EditorHandlerContribution extends Disposable { + beforeCreate: (coreMonaco: typeof monaco) => void; + afterCreate: ( + editor: monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, + coreMonaco: typeof monaco, + ) => void; + canHandle: (language: string) => boolean; +} +export { EditorOptionsRegistry } from './editor-options-registry.js'; +export { + LanguageWorkerContribution, + LanguageWorkerRegistry, +} from './language-worker-registry.js'; +export { + LazyLoaderRegistry, + LazyLoaderRegistryContribution, +} from './lazy-loader-registry.js'; +export { LanguageOptionsRegistry } from './options-registry.js'; diff --git a/packages/libro-cofine-editor-contribution/src/language-worker-registry.ts b/packages/libro-cofine-editor-contribution/src/language-worker-registry.ts new file mode 100644 index 00000000..10b09d54 --- /dev/null +++ b/packages/libro-cofine-editor-contribution/src/language-worker-registry.ts @@ -0,0 +1,64 @@ +import type { Contribution } from '@difizen/mana-app'; +import { contrib, singleton, Syringe } from '@difizen/mana-app'; + +export const LanguageWorkerContribution = Syringe.defineToken( + 'LanguageWorkerContribution', +); +export interface LanguageWorkerContribution { + registerLanguageWorkers: (registry: LanguageWorkerRegistry) => void; +} + +export interface LanguageWorkerConfig { + language: string; + priority?: number; + getWorkerUrl: (language: string, moduleId?: string) => string; +} + +@singleton() +export class LanguageWorkerRegistry { + protected configs: LanguageWorkerConfig[] = []; + protected readonly provider: Contribution.Provider; + constructor( + @contrib(LanguageWorkerContribution) + provider: Contribution.Provider, + ) { + this.provider = provider; + this.provider.getContributions().forEach((contribution) => { + contribution.registerLanguageWorkers(this); + }); + } + + registerWorker(config: LanguageWorkerConfig) { + const conf = { + ...config, + language: config.language.toUpperCase(), + }; + if (this.configs.find((c) => c.language === conf.language)) { + console.warn( + `Language ${conf.language} has already registed, this will overwrite the previous configuration`, + ); + } + this.configs = [...this.configs, conf].sort((a, b) => { + const aPriority = a.priority || 100; + const bPriority = b.priority || 100; + return bPriority - aPriority; + }); + } + + getLanguageWorker(language: string, moduleId?: string): string | undefined { + this.configs = []; + this.provider.getContributions({ cache: false }).forEach((contribution) => { + contribution.registerLanguageWorkers(this); + }); + const config = this.getLanguageConfig(language); + if (config) { + return config.getWorkerUrl(language, moduleId); + } + return undefined; + } + getLanguageConfig(language: string): LanguageWorkerConfig | undefined { + return this.configs.find( + (item) => item.language.toUpperCase() === language.toUpperCase(), + ); + } +} diff --git a/packages/libro-cofine-editor-contribution/src/lazy-loader-registry.ts b/packages/libro-cofine-editor-contribution/src/lazy-loader-registry.ts new file mode 100644 index 00000000..38fd0ffa --- /dev/null +++ b/packages/libro-cofine-editor-contribution/src/lazy-loader-registry.ts @@ -0,0 +1,29 @@ +import type { Contribution } from '@difizen/mana-app'; +import { contrib, singleton, Syringe } from '@difizen/mana-app'; + +export const LazyLoaderRegistryContribution = Syringe.defineToken( + 'LazyLoaderRegistryContribution', +); +export interface LazyLoaderRegistryContribution extends Disposable { + handleLazyLoder: () => void; + isLazyLoader?: boolean; +} + +@singleton() +export class LazyLoaderRegistry { + provider: Contribution.Provider; + constructor( + @contrib(LazyLoaderRegistryContribution) + provider: Contribution.Provider, + ) { + this.provider = provider; + } + handleLazyLoder() { + this.provider.getContributions({ cache: false }).forEach((c) => { + if (c.isLazyLoader) { + return; + } + c.handleLazyLoder(); + }); + } +} diff --git a/packages/libro-cofine-editor-contribution/src/options-registry.ts b/packages/libro-cofine-editor-contribution/src/options-registry.ts new file mode 100644 index 00000000..a882fb76 --- /dev/null +++ b/packages/libro-cofine-editor-contribution/src/options-registry.ts @@ -0,0 +1,14 @@ +import { singleton } from '@difizen/mana-app'; + +@singleton() +export class LanguageOptionsRegistry { + protected optionsMap: Map = new Map(); + + registerOptions(key: string, data: any) { + this.optionsMap.set(key, data); + } + + getOptions(key: string): any { + return this.optionsMap.get(key); + } +} diff --git a/packages/libro-cofine-editor-contribution/tsconfig.json b/packages/libro-cofine-editor-contribution/tsconfig.json new file mode 100644 index 00000000..a18e7b25 --- /dev/null +++ b/packages/libro-cofine-editor-contribution/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src", "typings"] +} diff --git a/packages/libro-codemirror-raw-cell/.eslintrc.mjs b/packages/libro-cofine-editor-core/.eslintrc.mjs similarity index 100% rename from packages/libro-codemirror-raw-cell/.eslintrc.mjs rename to packages/libro-cofine-editor-core/.eslintrc.mjs diff --git a/packages/libro-codemirror-markdown-cell/.fatherrc.ts b/packages/libro-cofine-editor-core/.fatherrc.ts similarity index 100% rename from packages/libro-codemirror-markdown-cell/.fatherrc.ts rename to packages/libro-cofine-editor-core/.fatherrc.ts diff --git a/packages/libro-cofine-editor-core/CHANGELOG.md b/packages/libro-cofine-editor-core/CHANGELOG.md new file mode 100644 index 00000000..0991cbc5 --- /dev/null +++ b/packages/libro-cofine-editor-core/CHANGELOG.md @@ -0,0 +1,31 @@ +# @difizen/libro-codemirror-markdown-cell + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version +- Updated dependencies [127cb35] +- Updated dependencies + - @difizen/libro-code-editor@0.1.0 + - @difizen/libro-codemirror@0.1.0 + - @difizen/libro-common@0.1.0 + - @difizen/libro-core@0.1.0 + - @difizen/libro-markdown@0.1.0 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version +- Updated dependencies + - @difizen/libro-code-editor@0.0.2-alpha.0 + - @difizen/libro-codemirror@0.0.2-alpha.0 + - @difizen/libro-common@0.0.2-alpha.0 + - @difizen/libro-core@0.0.2-alpha.0 + - @difizen/libro-markdown@0.0.2-alpha.0 diff --git a/packages/libro-codemirror-raw-cell/README.md b/packages/libro-cofine-editor-core/README.md similarity index 100% rename from packages/libro-codemirror-raw-cell/README.md rename to packages/libro-cofine-editor-core/README.md diff --git a/packages/libro-codemirror-markdown-cell/babel.config.json b/packages/libro-cofine-editor-core/babel.config.json similarity index 100% rename from packages/libro-codemirror-markdown-cell/babel.config.json rename to packages/libro-cofine-editor-core/babel.config.json diff --git a/packages/libro-codemirror-raw-cell/jest.config.mjs b/packages/libro-cofine-editor-core/jest.config.mjs similarity index 100% rename from packages/libro-codemirror-raw-cell/jest.config.mjs rename to packages/libro-cofine-editor-core/jest.config.mjs diff --git a/packages/libro-cofine-editor-core/package.json b/packages/libro-cofine-editor-core/package.json new file mode 100644 index 00000000..4ce4a0a7 --- /dev/null +++ b/packages/libro-cofine-editor-core/package.json @@ -0,0 +1,59 @@ +{ + "name": "@difizen/libro-cofine-editor-core", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook", + "monaco" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "build:worker": "webpack --config ./scripts/webpack.worker.js", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/mana-app": "latest", + "@difizen/libro-cofine-editor-contribution": "^0.1.0", + "debug": "^4.3.2", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@types/debug": "^4.1.6", + "@difizen/monaco-editor-core": "latest" + } +} diff --git a/packages/libro-cofine-editor-core/src/default-worker-contribution.ts b/packages/libro-cofine-editor-core/src/default-worker-contribution.ts new file mode 100644 index 00000000..2d6e1a3d --- /dev/null +++ b/packages/libro-cofine-editor-core/src/default-worker-contribution.ts @@ -0,0 +1,25 @@ +import type { LanguageWorkerRegistry } from '@difizen/libro-cofine-editor-contribution'; +import { LanguageWorkerContribution } from '@difizen/libro-cofine-editor-contribution'; +import { inject, singleton } from '@difizen/mana-app'; + +import pkg from '../package.json'; + +import { MonacoLoaderConfig } from './monaco-loader.js'; + +@singleton({ contrib: LanguageWorkerContribution }) +export class DefaultWorkerContribution implements LanguageWorkerContribution { + config: MonacoLoaderConfig; + constructor(@inject(MonacoLoaderConfig) config: MonacoLoaderConfig) { + this.config = config; + } + registerLanguageWorkers(registry: LanguageWorkerRegistry) { + registry.registerWorker({ + language: 'editorWorkerService', + getWorkerUrl: () => { + return `data:text/javascript;charset=utf-8,${encodeURIComponent( + `importScripts('https://g.alipay.com/@difizen/libro-cofine-editor-core@${pkg.version}/dist/worker/editor.worker.min.js')`, + )}`; + }, + }); + } +} diff --git a/packages/libro-cofine-editor-core/src/e2-editor.ts b/packages/libro-cofine-editor-core/src/e2-editor.ts new file mode 100644 index 00000000..da917fa7 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/e2-editor.ts @@ -0,0 +1,167 @@ +import { DisposableCollection } from '@difizen/mana-app'; +import { inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { EditorHanlerRegistry } from './editor-handler-registry.js'; +import type { LazyCallbackType, Options } from './editor-provider.js'; +import { MonacoEnvironment } from './monaco-environment.js'; +import { ThemeRegistry } from './theme-registry.js'; + +export const EditorNode = Symbol('EditorNode'); +export const MonacoOptions = Symbol('MonacoOptions'); +export const LazyCallback = Symbol('LazyCallback'); +export const IsDiff = Symbol('IsDiff'); +/** + * E2 Editor + */ + +@singleton() +export class E2Editor< + T extends monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, +> { + codeEditor!: T; + model!: monaco.editor.ITextModel; + modified!: monaco.editor.ITextModel; + original!: monaco.editor.ITextModel; + language?: string; + protected toDispose: DisposableCollection = new DisposableCollection(); + node: HTMLElement; + options: Options; + editorHandlerRegistry: EditorHanlerRegistry; + themeRegistry: ThemeRegistry; + isDiff: boolean; + lazyCallback?: LazyCallbackType; + constructor( + @inject(EditorNode) node: HTMLElement, + @inject(MonacoOptions) options: Options, + @inject(EditorHanlerRegistry) editorHandlerRegistry: EditorHanlerRegistry, + @inject(ThemeRegistry) themeRegistry: ThemeRegistry, + @inject(IsDiff) isDiff: boolean, + @inject(LazyCallback) lazycallback?: LazyCallbackType, + ) { + this.node = node; + this.options = options; + this.editorHandlerRegistry = editorHandlerRegistry; + this.themeRegistry = themeRegistry; + this.isDiff = isDiff; + this.lazyCallback = lazycallback; + + if (MonacoEnvironment.lazy) { + // 资源懒加载场景 + if (!isDiff) { + // 先不设置方言 + const allLanguage = monaco.languages.getLanguages(); + const isRegister = allLanguage.some((item) => item.id === options.language); + this.model = monaco.editor.createModel( + options.value || '', + isRegister ? options.language : '', + ); + (this as E2Editor).codeEditor = + monaco.editor.create(node, { ...options, model: this.model }); + this.handleEditorLanguageFeatureBefore(options.language); + MonacoEnvironment.initModule() + .then(() => { + // 设置方言和主题 + if (!isRegister) { + const codeEditor = (this as E2Editor) + .codeEditor; + // 为了触发onCreate事件 + codeEditor.setModel( + monaco.editor.createModel( + codeEditor.getValue() || '', + options.language, + ), + ); + } + this.language = options.language; + monaco.editor.setTheme(options.theme || ''); + // 调用回调函数 + this.handleEditorLanguageFeatureAfter(); + if (this.lazyCallback) { + this.lazyCallback( + (this as E2Editor).codeEditor, + ); + } + return; + }) + .catch(console.error); + } else { + this.modified = monaco.editor.createModel(options.modified || ''); + this.original = monaco.editor.createModel(options.original || ''); + (this as E2Editor).codeEditor = + monaco.editor.createDiffEditor(node, { ...options }); + (this as E2Editor).codeEditor.setModel({ + original: this.original, + modified: this.modified, + }); + this.handleEditorLanguageFeatureBefore(options.language); + MonacoEnvironment.initModule() + .then(() => { + monaco.editor.setModelLanguage(this.modified, options.language!); + monaco.editor.setModelLanguage(this.original, options.language!); + this.language = this.original.getLanguageId(); + + monaco.editor.setTheme(options.theme || ''); + // 调用回调函数 + this.handleEditorLanguageFeatureAfter(); + if (this.lazyCallback) { + this.lazyCallback( + (this as E2Editor).codeEditor, + ); + } + return; + }) + .catch(console.error); + } + } else { + // create前钩子函数调用 + this.handleEditorLanguageFeatureBefore(options.language); + + if (!isDiff) { + this.model = monaco.editor.createModel(options.value || '', options.language); + (this as E2Editor).codeEditor = + monaco.editor.create(node, { ...options, model: this.model }); + this.toDispose.push( + this.model.onDidChangeLanguage((e) => { + this.language = e.newLanguage; + }), + ); + this.language = this.model.getLanguageId(); + } else { + this.modified = monaco.editor.createModel( + options.modified || '', + options.language, + ); + this.original = monaco.editor.createModel( + options.original || '', + options.language, + ); + (this as E2Editor).codeEditor = + monaco.editor.createDiffEditor(node, { ...options }); + (this as E2Editor).codeEditor.setModel({ + original: this.original, + modified: this.modified, + }); + this.toDispose.push( + this.modified.onDidChangeLanguage((e) => { + this.language = e.newLanguage; + }), + ); + this.language = this.modified.getLanguageId(); + } + this.handleEditorLanguageFeatureAfter(); + } + } + + protected handleEditorLanguageFeatureBefore(language: string | undefined) { + if (language) { + this.editorHandlerRegistry.handleBefore(language); + } + } + + protected handleEditorLanguageFeatureAfter() { + if (this.language) { + this.editorHandlerRegistry.handleAfter(this.language, this.codeEditor); + } + } +} diff --git a/packages/libro-cofine-editor-core/src/ediotor.worker.ts b/packages/libro-cofine-editor-core/src/ediotor.worker.ts new file mode 100644 index 00000000..25306e20 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/ediotor.worker.ts @@ -0,0 +1,25 @@ +import { SimpleWorkerServer } from '@difizen/monaco-editor-core/esm/vs/base/common/worker/simpleWorker.js'; +import { EditorSimpleWorker } from '@difizen/monaco-editor-core/esm/vs/editor/common/services/editorSimpleWorker.js'; + +let initialized = false; +export function initialize(foreignModule: any) { + if (initialized) { + return; + } + initialized = true; + const simpleWorker = new SimpleWorkerServer( + (msg: string) => { + globalThis.postMessage(msg); + }, + (host: string) => new EditorSimpleWorker(host, foreignModule), + ); + globalThis.onmessage = (e) => { + simpleWorker.onmessage(e.data); + }; +} +globalThis.onmessage = (e) => { + // Ignore first message in this case and initialize if not yet initialized + if (!initialized) { + initialize(null); + } +}; diff --git a/packages/libro-cofine-editor-core/src/editor-handler-registry.ts b/packages/libro-cofine-editor-core/src/editor-handler-registry.ts new file mode 100644 index 00000000..0f80b5c6 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/editor-handler-registry.ts @@ -0,0 +1,47 @@ +import { EditorHandlerContribution } from '@difizen/libro-cofine-editor-contribution'; +import type { Contribution } from '@difizen/mana-app'; +import { contrib, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +@singleton() +export class EditorHanlerRegistry { + contributions: EditorHandlerContribution[]; + effected: EditorHandlerContribution[] = []; + provider: Contribution.Provider; + constructor( + @contrib(EditorHandlerContribution) + provider: Contribution.Provider, + ) { + this.provider = provider; + this.contributions = provider.getContributions(); + } + + handleAfter( + languege: string, + editor: monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, + ): void { + this.effected.forEach((contribution) => contribution.dispose()); + this.effected = []; + this.contributions = this.provider.getContributions({ cache: false }); + const canHanleList = this.contributions.filter((handler) => + handler.canHandle(languege), + ); + canHanleList.forEach((contribution) => { + contribution.afterCreate(editor, monaco); + this.effected.push(contribution); + }); + } + + handleBefore(languege: string): void { + this.contributions = this.provider.getContributions({ cache: false }); + this.effected.forEach((contribution) => contribution.dispose()); + this.effected = []; + const canHanleList = this.contributions.filter((handler) => + handler.canHandle(languege), + ); + canHanleList.forEach((contribution) => { + contribution.beforeCreate(monaco); + this.effected.push(contribution); + }); + } +} diff --git a/packages/libro-cofine-editor-core/src/editor-provider.ts b/packages/libro-cofine-editor-core/src/editor-provider.ts new file mode 100644 index 00000000..db29c089 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/editor-provider.ts @@ -0,0 +1,27 @@ +import type monaco from '@difizen/monaco-editor-core'; + +import type { E2Editor } from './e2-editor.js'; + +export const EditorProvider = Symbol('EditorProvider'); +export type Options = monaco.editor.IStandaloneEditorConstructionOptions & + monaco.editor.IDiffEditorConstructionOptions & { + modified?: string; + original?: string; + }; + +export interface EditorProvider { + create: ( + node: HTMLElement, + options: Options, + callback?: LazyCallbackType, + ) => E2Editor; + createDiff: ( + node: HTMLElement, + options: Options, + callback?: LazyCallbackType, + ) => E2Editor; +} + +export type LazyCallbackType = ( + editor: monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, +) => void; diff --git a/packages/libro-cofine-editor-core/src/global.d.ts b/packages/libro-cofine-editor-core/src/global.d.ts new file mode 100644 index 00000000..1d145726 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/global.d.ts @@ -0,0 +1,7 @@ +declare module '@difizen/monaco-editor-core/esm/vs/base/common/worker/simpleWorker.js'; +declare module '@difizen/monaco-editor-core/esm/vs/editor/common/services/editorSimpleWorker.js'; +declare module '*.json'; +interface Window { + _Monaco: any; + MonacoEnvironment: any; +} diff --git a/packages/libro-cofine-editor-core/src/index.ts b/packages/libro-cofine-editor-core/src/index.ts new file mode 100644 index 00000000..872e52fb --- /dev/null +++ b/packages/libro-cofine-editor-core/src/index.ts @@ -0,0 +1,46 @@ +import type monaco from '@difizen/monaco-editor-core'; + +import type { E2Editor } from './e2-editor.js'; +import { EditorProvider } from './editor-provider.js'; +import { MonacoEnvironment } from './monaco-environment.js'; + +export { + EditorHandlerContribution, + LanguageOptionsRegistry, + LanguageWorkerContribution, + LanguageWorkerRegistry, +} from '@difizen/libro-cofine-editor-contribution'; +export type { E2Editor } from './e2-editor.js'; +export { EditorHanlerRegistry } from './editor-handler-registry.js'; +export { EditorProvider } from './editor-provider.js'; +export { InitializeContribution } from './initialize-provider.js'; +export { MonacoEnvironment } from './monaco-environment.js'; +export { MonacoLoaderConfig } from './monaco-loader.js'; +export { + SnippetSuggestContribution, + SnippetSuggestRegistry, +} from './snippets-suggest-registry.js'; +export { + MixedThemeRegistry, + ThemeContribution, + ThemeRegistry, +} from './theme-registry.js'; +export type { + MixedTheme, + ITextmateThemeSetting, + IRawThemeSetting, + IRawTheme, +} from './theme-registry.js'; +export class Editor { + editor: E2Editor; + codeEditor: monaco.editor.IStandaloneCodeEditor; + constructor( + node: HTMLElement, + options: monaco.editor.IStandaloneEditorConstructionOptions = {}, + ) { + const editorProvider = + MonacoEnvironment.container.get(EditorProvider); + this.editor = editorProvider.create(node, options); + this.codeEditor = this.editor.codeEditor; + } +} diff --git a/packages/libro-cofine-editor-core/src/initialize-provider.ts b/packages/libro-cofine-editor-core/src/initialize-provider.ts new file mode 100644 index 00000000..e38cab3b --- /dev/null +++ b/packages/libro-cofine-editor-core/src/initialize-provider.ts @@ -0,0 +1,54 @@ +import type { Contribution } from '@difizen/mana-app'; +import { + contrib, + DefaultContributionProvider, + singleton, + Syringe, +} from '@difizen/mana-app'; +import type monaco from '@difizen/monaco-editor-core'; + +export const InitializeContribution = Syringe.defineToken('InitializeContribution'); +export interface InitializeContribution { + initialized?: boolean; + onInitialize?: (coreMonaco: typeof monaco) => void | Promise; + awaysInitialized?: boolean; +} +@singleton() +export class InitializeProvider { + provider: Contribution.Provider; + constructor( + @contrib(InitializeContribution) + provider: Contribution.Provider, + ) { + this.provider = provider; + } + + async initialize(coreMonaco: typeof monaco): Promise { + const contributions = this.provider.getContributions({ cache: false }); + for (const contribution of contributions) { + if (contribution.onInitialize) { + try { + if (!contribution.initialized) { + const inited = contribution.onInitialize(coreMonaco); + if (inited) { + // eslint-disable-next-line no-await-in-loop + await inited; + } + if (!contribution.awaysInitialized) { + contribution.initialized = true; + } + } + } catch (error) { + // noop + } + } + } + } +} + +export class InitializeContributionProvider extends DefaultContributionProvider { + override getContributions() { + this.services = undefined; + return super.getContributions(); + } +} diff --git a/packages/libro-cofine-editor-core/src/mana-export.ts b/packages/libro-cofine-editor-core/src/mana-export.ts new file mode 100644 index 00000000..e2fa105a --- /dev/null +++ b/packages/libro-cofine-editor-core/src/mana-export.ts @@ -0,0 +1,8 @@ +import type { Container } from '@difizen/mana-app'; + +import { MonacoEnvironment } from './monaco-environment.js'; + +export default async (container: Container): Promise => { + MonacoEnvironment.setContainer(container); + await MonacoEnvironment.init(); +}; diff --git a/packages/libro-cofine-editor-core/src/monaco-compability.esm.ts b/packages/libro-cofine-editor-core/src/monaco-compability.esm.ts new file mode 100644 index 00000000..83f7bf09 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/monaco-compability.esm.ts @@ -0,0 +1,4 @@ +import * as monaco from '@difizen/monaco-editor-core'; + +export default monaco; +export const { Emitter, MarkerSeverity, Range, Uri, editor, languages } = monaco || {}; diff --git a/packages/libro-cofine-editor-core/src/monaco-environment.ts b/packages/libro-cofine-editor-core/src/monaco-environment.ts new file mode 100644 index 00000000..9c96e745 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/monaco-environment.ts @@ -0,0 +1,115 @@ +import { + LanguageWorkerRegistry, + LazyLoaderRegistry, +} from '@difizen/libro-cofine-editor-contribution'; +import type { Syringe } from '@difizen/mana-app'; +import { Deferred } from '@difizen/mana-app'; +import { GlobalContainer } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { InitializeProvider } from './initialize-provider.js'; +import { + DefaultLoaderConfig, + MonacoLoader, + MonacoLoaderConfig, +} from './monaco-loader.js'; +import BaseModule from './monaco-module.js'; + +export const MonacoEnvironmentBound = 'MonacoEnvironmentBound'; + +export type ModuleLoader = (container: Syringe.Container) => void; +type InitType = { + lazy: boolean; + cdnUrl?: string; +}; +export class MonacoEnvironment { + static container: Syringe.Container; + static loaders: ModuleLoader[] = []; + static preLoaders: ModuleLoader[] = []; + static monaco: typeof monaco; + protected static loader?: MonacoLoader; + protected static moduleLoadStart = false; + protected static moduleInitDeferred: Deferred = new Deferred(); + protected static baseModule?: Syringe.Module; + static lazy = false; + + static setContainer = (container: Syringe.Container) => { + MonacoEnvironment.container = container; + if (!container.isBound(MonacoEnvironmentBound)) { + container.register(MonacoEnvironmentBound, { useValue: true }); + MonacoEnvironment.container.register(MonacoLoaderConfig, { + useValue: DefaultLoaderConfig, + }); + container.register(MonacoLoader); + } + }; + static async load(cdnUrl?: string) { + if (!MonacoEnvironment.container) { + MonacoEnvironment.setContainer(GlobalContainer); + } + if (!MonacoEnvironment.loader) { + MonacoEnvironment.loader = MonacoEnvironment.container.get(MonacoLoader); + } + await MonacoEnvironment.loader.load(cdnUrl); + } + static async init( + { lazy, cdnUrl }: InitType | undefined = { lazy: false, cdnUrl: '' }, + ): Promise { + if (!MonacoEnvironment.monaco) { + await MonacoEnvironment.load(cdnUrl!); + } + window.MonacoEnvironment = { + getWorkerUrl: (moduleId: string, label: string) => { + const workerRegistry = MonacoEnvironment.container.get(LanguageWorkerRegistry); + return workerRegistry.getLanguageWorker(label, moduleId); + }, + }; + try { + if (!MonacoEnvironment.baseModule) { + MonacoEnvironment.baseModule = BaseModule; + MonacoEnvironment.container.load(MonacoEnvironment.baseModule); + } + MonacoEnvironment.lazy = lazy; + if (!lazy) { + await MonacoEnvironment.initModule(); + } else { + for (const loader of MonacoEnvironment.preLoaders) { + await loader(MonacoEnvironment.container); + } + const initialize = MonacoEnvironment.container.get(InitializeProvider); + await initialize.initialize(monaco); + } + } catch (ex) { + console.error(ex); + } + } + + static async initModule() { + if (!MonacoEnvironment.loaders.length) { + return; + } + for (const loader of MonacoEnvironment.loaders) { + await loader(MonacoEnvironment.container); + } + MonacoEnvironment.loaders = []; + try { + const initialize = MonacoEnvironment.container.get(InitializeProvider); + await initialize.initialize(monaco); + if (MonacoEnvironment.lazy) { + const layzInit = MonacoEnvironment.container.get(LazyLoaderRegistry); + await layzInit.handleLazyLoder(); + } + } catch (e) { + console.error(e); + } + } + + static async loadModule(loader: ModuleLoader) { + MonacoEnvironment.loaders.push(loader); + } + + // 提前加载(懒加载模式下需要提前加载的模块) + static async preLoadModule(loader: ModuleLoader) { + MonacoEnvironment.preLoaders.push(loader); + } +} diff --git a/packages/libro-cofine-editor-core/src/monaco-loader.ts b/packages/libro-cofine-editor-core/src/monaco-loader.ts new file mode 100644 index 00000000..b4901c09 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/monaco-loader.ts @@ -0,0 +1,101 @@ +import { inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +export const MonacoLoaderConfig = Symbol('MonacoLoaderConfig'); +export interface MonacoLoaderConfig { + requireConfig: { + paths: { + vs: string; + [key: string]: string; + }; + }; +} + +export const DefaultLoaderConfig: MonacoLoaderConfig = { + requireConfig: { + paths: { + vs: 'https://g.alipay.com/@alipay/monaco-editor-core@0.0.0-beta.13/min/vs', + }, + }, +}; + +@singleton() +export class MonacoLoader { + loaded = false; + protected start = false; + protected vsRequire: any; + protected readonly config; + constructor(@inject(MonacoLoaderConfig) config: MonacoLoaderConfig) { + this.config = config; + } + cdnUrl = ''; + + async load(cdnUrl?: string) { + if (cdnUrl) { + this.cdnUrl = cdnUrl; + } + if (!this.start) { + this.start = true; + await this.doLoad(); + } + } + protected async doLoad(): Promise { + if (!this.loaded) { + await this.initMonaco(); + } + } + // 初始化,将monaco挂在到window上 + initMonaco() { + return new Promise((resolve, reject) => { + if (this.cdnUrl) { + const beforeExist = document.getElementById('e2-monaco-id'); + if (beforeExist) { + if (window._Monaco) { + const global: any = window; + global.monaco = window._Monaco; + this.loaded = true; + resolve(true); + return; + } + beforeExist.onload = () => { + const global: any = window; + global.monaco = window._Monaco; + this.loaded = true; + resolve(true); + }; + return; + } + const script = document.createElement('script'); + script.id = 'e2-monaco-id'; + script.src = this.cdnUrl; + // script.src = + // 'https://gw.alipayobjects.com/os/dataphin-fe/monaco_editor_core_1701689088670.js'; + document.head.appendChild(script); + script.onload = () => { + const global: any = window; + global.monaco = window._Monaco; + this.loaded = true; + resolve(true); + }; + script.onerror = () => { + document.head.removeChild(script); + // 如果第一次失败的话,就再次加载monaco + const script2 = document.createElement('script'); + script2.src = this.cdnUrl; + document.head.appendChild(script2); + script2.onload = () => { + const global: any = window; + global.monaco = window._Monaco; + this.loaded = true; + resolve(true); + }; + }; + } else { + const global: any = window; + global.monaco = monaco; + this.loaded = true; + resolve(true); + } + }); + } +} diff --git a/packages/libro-cofine-editor-core/src/monaco-module.ts b/packages/libro-cofine-editor-core/src/monaco-module.ts new file mode 100644 index 00000000..84cb0256 --- /dev/null +++ b/packages/libro-cofine-editor-core/src/monaco-module.ts @@ -0,0 +1,101 @@ +import { + EditorHandlerContribution, + EditorOptionsRegistry, + LanguageOptionsRegistry, + LanguageWorkerContribution, + LanguageWorkerRegistry, + LazyLoaderRegistry, + LazyLoaderRegistryContribution, +} from '@difizen/libro-cofine-editor-contribution'; +import { Contribution, Module, Syringe } from '@difizen/mana-app'; + +import 'reflect-metadata'; +import { DefaultWorkerContribution } from './default-worker-contribution.js'; +import { + E2Editor, + EditorNode, + IsDiff, + LazyCallback, + MonacoOptions, +} from './e2-editor.js'; +import { EditorHanlerRegistry } from './editor-handler-registry.js'; +import type { Options } from './editor-provider.js'; +import { EditorProvider } from './editor-provider.js'; +import { + InitializeContribution, + InitializeContributionProvider, + InitializeProvider, +} from './initialize-provider.js'; +import { + SnippetSuggestContribution, + SnippetSuggestRegistry, +} from './snippets-suggest-registry.js'; +import { + MixedThemeRegistry, + ThemeContribution, + ThemeRegistry, +} from './theme-registry.js'; + +export const MonacoModule = Module() + .register( + { + token: { token: Contribution.Provider, named: InitializeContribution }, + useDynamic: (ctx) => + new InitializeContributionProvider(InitializeContribution, ctx.container), + lifecycle: Syringe.Lifecycle.singleton, + }, + InitializeProvider, + LanguageWorkerRegistry, + LanguageOptionsRegistry, + EditorOptionsRegistry, + DefaultWorkerContribution, + SnippetSuggestRegistry, + LazyLoaderRegistry, + { + token: MixedThemeRegistry, + useValue: {}, + }, + ThemeRegistry, + { + token: EditorProvider, + lifecycle: Syringe.Lifecycle.singleton, + useDynamic: (ctx) => { + return { + create: (node: HTMLElement, options: Options, lazyCallback?: () => void) => { + const child = ctx.container.createChild(); + child.register(EditorNode, { useValue: node }); + child.register(MonacoOptions, { useValue: options }); + child.register(LazyCallback, { useValue: lazyCallback }); + child.register({ token: IsDiff, useValue: false }); + child.register(EditorHanlerRegistry); + child.register(E2Editor); + return child.get(E2Editor); + }, + createDiff: ( + node: HTMLElement, + options: Options, + lazyCallback?: () => void, + ) => { + const child = ctx.container.createChild(); + child.register(EditorNode, { useValue: node }); + child.register(MonacoOptions, { useValue: options }); + child.register(LazyCallback, { useValue: lazyCallback }); + child.register({ token: IsDiff, useValue: true }); + child.register(EditorHanlerRegistry); + child.register(E2Editor); + return child.get(E2Editor); + }, + }; + }, + }, + ) + // 语言 worker 扩展点 + .contribution( + LanguageWorkerContribution, + EditorHandlerContribution, + ThemeContribution, + SnippetSuggestContribution, + LazyLoaderRegistryContribution, + ); + +export default MonacoModule; diff --git a/packages/libro-cofine-editor-core/src/snippets-suggest-registry.ts b/packages/libro-cofine-editor-core/src/snippets-suggest-registry.ts new file mode 100644 index 00000000..135b281f --- /dev/null +++ b/packages/libro-cofine-editor-core/src/snippets-suggest-registry.ts @@ -0,0 +1,101 @@ +import type { Contribution } from '@difizen/mana-app'; +import { contrib, singleton, Syringe } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { InitializeContribution } from './initialize-provider.js'; + +export interface SnippetLoadOptions { + language?: string | string[]; + source: string; +} + +export type JsonSerializedSnippets = Record; +export interface JsonSerializedSnippet { + body: string[]; + scope: string; + prefix: string; + description: string; +} + +export interface SnippetSuggestContribution { + registerSnippetSuggest: (registry: SnippetSuggestRegistry) => void; + _initRegisterSnippetSuggest?: boolean; +} + +export const SnippetSuggestContribution = Syringe.defineToken( + 'SnippetSuggestContribution', +); + +@singleton({ contrib: InitializeContribution }) +export class SnippetSuggestRegistry implements InitializeContribution { + registerCompletion = false; + awaysInitialized = true; + onInitialize() { + this.snippetsSuggestContrbutions + .getContributions({ cache: false }) + .forEach((contribution) => { + if (contribution._initRegisterSnippetSuggest) { + return; + } + contribution.registerSnippetSuggest(this); + contribution._initRegisterSnippetSuggest = true; + }); + } + protected languageSnippets: Record = {}; + + snippetsSuggestContrbutions: Contribution.Provider; + constructor( + @contrib(SnippetSuggestContribution) + snippetsSuggestContrbutions: Contribution.Provider, + ) { + this.snippetsSuggestContrbutions = snippetsSuggestContrbutions; + } + + async provideCompletionItems( + model: monaco.editor.ITextModel, + ): Promise { + const _language = model.getLanguageId(); + const _snippets: JsonSerializedSnippet[] = this.languageSnippets[_language] || []; + const suggestions = _snippets.map((it) => { + return { + label: it.prefix, + insertText: it.body.join('\n'), + documentation: it.description, + detail: 'snippet', + insertTextRules: 4, + kind: 27, + range: undefined as unknown as monaco.IRange, + }; + }); + return { suggestions }; + } + + fromJSON( + snippets: JsonSerializedSnippets | undefined, + { language }: SnippetLoadOptions, + ) { + if (!language || !language?.length || !snippets) { + return; + } + const _languages = typeof language === 'string' ? [language] : language; + _languages.forEach((it) => { + if (!this.languageSnippets[it]) { + this.languageSnippets[it] = []; + } + this.languageSnippets[it].push( + ...(Object.values(snippets) as JsonSerializedSnippet[]), + ); + }); + // 采集数据 + if (this.registerCompletion) { + return; + } + this.registerCompletion = true; + monaco.languages.registerCompletionItemProvider( + { pattern: '**' }, + { + provideCompletionItems: this.provideCompletionItems.bind(this), + }, + ); + } +} diff --git a/packages/libro-cofine-editor-core/src/theme-registry.ts b/packages/libro-cofine-editor-core/src/theme-registry.ts new file mode 100644 index 00000000..8b74887e --- /dev/null +++ b/packages/libro-cofine-editor-core/src/theme-registry.ts @@ -0,0 +1,116 @@ +import type { Contribution } from '@difizen/mana-app'; +import { contrib, inject, singleton, Syringe } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { InitializeContribution } from './initialize-provider.js'; + +export const ThemeContribution = Syringe.defineToken('ThemeContribution'); + +export interface ITextmateThemeSetting { + colors: Record; + tokenColors: IRawThemeSetting[]; + include?: string; + name: string; + givenName?: string; + base?: monaco.editor.BuiltinTheme; + givenBase?: string; +} + +export interface ThemeContribution { + registerItem: (registry: ThemeRegistry) => void; + _registerFinish?: boolean; +} + +/** + * A single theme setting. + */ +export interface IRawThemeSetting { + readonly name?: string; + readonly scope?: string | string[]; + readonly settings: { + readonly fontStyle?: string; + readonly foreground?: string; + readonly background?: string; + }; +} + +/** + * A TextMate theme. + */ +export interface IRawTheme { + readonly name?: string; + readonly settings: IRawThemeSetting[]; +} + +export interface MixedTheme extends IRawTheme, monaco.editor.IStandaloneThemeData {} + +export interface MixedThemeRegistry { + registerThemes: ( + themeOptions: Record, + setTheme: (name: string, data: monaco.editor.IStandaloneThemeData) => void, + ) => void; +} +export const MixedThemeRegistry = Symbol('MixedThemeRegistry'); +@singleton({ contrib: InitializeContribution }) +export class ThemeRegistry implements InitializeContribution { + contributions: ThemeContribution[]; + themeOptions: Record = {}; + themes: Map = new Map(); + awaysInitialized = true; + + onInitialize() { + this.provider.getContributions({ cache: false }).forEach((item) => { + if (item._registerFinish) { + return; + } + item.registerItem(this); + item._registerFinish = true; + }); + if (this.mixedThemeEnable) { + this.mixedThemeRegistry.registerThemes( + this.themeOptions, + this.setTheme.bind(this), + ); + } + } + protected readonly provider: Contribution.Provider; + protected readonly mixedThemeRegistry: MixedThemeRegistry; + constructor( + @contrib(ThemeContribution) provider: Contribution.Provider, + @inject(MixedThemeRegistry) mixedThemeRegistry: MixedThemeRegistry, + ) { + this.provider = provider; + this.mixedThemeRegistry = mixedThemeRegistry; + this.contributions = this.provider.getContributions(); + } + + get mixedThemeEnable(): boolean { + return !!(this.mixedThemeRegistry && this.mixedThemeRegistry.registerThemes); + } + + setTheme(name: string, data: monaco.editor.IStandaloneThemeData): void { + try { + this.themes.set(name, data); + // monaco auto refreshes a theme with new data + monaco.editor.defineTheme(name, data); + } catch (ex) { + console.error(ex); + } + } + + registerMonacoTheme( + setting: monaco.editor.IStandaloneThemeData, + name: string, + monacoBase?: monaco.editor.BuiltinTheme, + ): void { + this.setTheme(name, { ...setting, base: monacoBase || setting.base || 'vs' }); + } + registerMixedTheme( + setting: ITextmateThemeSetting, + givenName: string, + givenBase?: monaco.editor.BuiltinTheme, + ): void { + const name = givenName || setting.name; + this.themeOptions[givenName] = { ...setting, givenName, name, base: givenBase }; + } +} diff --git a/packages/libro-cofine-editor-core/tsconfig.json b/packages/libro-cofine-editor-core/tsconfig.json new file mode 100644 index 00000000..b76b3969 --- /dev/null +++ b/packages/libro-cofine-editor-core/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src", "typings", "package.json"] +} diff --git a/packages/libro-search-codemirror-cell/.eslintrc.mjs b/packages/libro-cofine-editor/.eslintrc.mjs similarity index 100% rename from packages/libro-search-codemirror-cell/.eslintrc.mjs rename to packages/libro-cofine-editor/.eslintrc.mjs diff --git a/packages/libro-search-codemirror-cell/.fatherrc.ts b/packages/libro-cofine-editor/.fatherrc.ts similarity index 100% rename from packages/libro-search-codemirror-cell/.fatherrc.ts rename to packages/libro-cofine-editor/.fatherrc.ts diff --git a/packages/libro-cofine-editor/CHANGELOG.md b/packages/libro-cofine-editor/CHANGELOG.md new file mode 100644 index 00000000..0991cbc5 --- /dev/null +++ b/packages/libro-cofine-editor/CHANGELOG.md @@ -0,0 +1,31 @@ +# @difizen/libro-codemirror-markdown-cell + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version +- Updated dependencies [127cb35] +- Updated dependencies + - @difizen/libro-code-editor@0.1.0 + - @difizen/libro-codemirror@0.1.0 + - @difizen/libro-common@0.1.0 + - @difizen/libro-core@0.1.0 + - @difizen/libro-markdown@0.1.0 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version +- Updated dependencies + - @difizen/libro-code-editor@0.0.2-alpha.0 + - @difizen/libro-codemirror@0.0.2-alpha.0 + - @difizen/libro-common@0.0.2-alpha.0 + - @difizen/libro-core@0.0.2-alpha.0 + - @difizen/libro-markdown@0.0.2-alpha.0 diff --git a/packages/libro-search-codemirror-cell/README.md b/packages/libro-cofine-editor/README.md similarity index 100% rename from packages/libro-search-codemirror-cell/README.md rename to packages/libro-cofine-editor/README.md diff --git a/packages/libro-search-codemirror-cell/babel.config.json b/packages/libro-cofine-editor/babel.config.json similarity index 100% rename from packages/libro-search-codemirror-cell/babel.config.json rename to packages/libro-cofine-editor/babel.config.json diff --git a/packages/libro-search-codemirror-cell/jest.config.mjs b/packages/libro-cofine-editor/jest.config.mjs similarity index 100% rename from packages/libro-search-codemirror-cell/jest.config.mjs rename to packages/libro-cofine-editor/jest.config.mjs diff --git a/packages/libro-cofine-editor/package.json b/packages/libro-cofine-editor/package.json new file mode 100644 index 00000000..0a3229fd --- /dev/null +++ b/packages/libro-cofine-editor/package.json @@ -0,0 +1,67 @@ +{ + "name": "@difizen/libro-cofine-editor", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/monaco-editor-core": "latest", + "@difizen/libro-cofine-editor-core": "^0.1.0", + "@difizen/libro-cofine-textmate": "^0.1.0", + "@difizen/libro-cofine-language-python": "^0.1.0", + "@difizen/libro-lsp": "^0.1.0", + "@difizen/libro-code-editor": "^0.1.0", + "@difizen/libro-common": "^0.1.0", + "@difizen/libro-core": "^0.1.0", + "@difizen/mana-app": "latest", + "vscode-languageserver-protocol": "^3.17.4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "react": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.25", + "@types/uuid": "^9.0.2" + } +} diff --git a/packages/libro-cofine-editor/src/editor-contribution.ts b/packages/libro-cofine-editor/src/editor-contribution.ts new file mode 100644 index 00000000..1d0071f5 --- /dev/null +++ b/packages/libro-cofine-editor/src/editor-contribution.ts @@ -0,0 +1,28 @@ +import type { CodeEditorFactory } from '@difizen/libro-code-editor'; +import { CodeEditorContribution } from '@difizen/libro-code-editor'; +import { inject, singleton } from '@difizen/mana-app'; + +import { LibroE2EditorFactory } from './libro-e2-editor.js'; + +@singleton({ contrib: [CodeEditorContribution] }) +export class LibroE2EditorContribution implements CodeEditorContribution { + factory: CodeEditorFactory; + + constructor( + @inject(LibroE2EditorFactory) libroE2EditorFactory: LibroE2EditorFactory, + ) { + this.factory = libroE2EditorFactory; + } + + canHandle(mime: string): number { + const mimes = [ + 'application/vnd.libro.sql+json', + 'text/x-python', + 'application/vnd.libro.prompt+json', + ]; + if (mimes.includes(mime)) { + return 50 + 1; + } + return 0; + } +} diff --git a/packages/libro-cofine-editor/src/index.less b/packages/libro-cofine-editor/src/index.less new file mode 100644 index 00000000..ce599dcc --- /dev/null +++ b/packages/libro-cofine-editor/src/index.less @@ -0,0 +1,32 @@ +.libro-e2-editor-container { + padding: 12px 0 18px; + background: var(--mana-libro-input-background) !important; + border-radius: 4px; +} + +.libro-e2-editor { + background: var(--mana-libro-input-background) !important; + + .monaco-editor-background, + .margin { + background: var(--mana-libro-input-background) !important; + } + + .current-line-margin, + .current-line-both { + background: var(--mana-libro-editor-activeline-color) !important; + border: unset !important; + } + + .line-numbers { + margin-left: 8px; + color: var(--mana-libro-editor-gutter-number-color) !important; + font-weight: 400; + font-size: 13px; + // font-family: Menlo-Regular, consolas, 'DejaVu Sans Mono', monospace !important; + } + + .cursors-layer .cursor { + background-color: var(--mana-libro-editor-cursor-color); + } +} diff --git a/packages/libro-cofine-editor/src/index.ts b/packages/libro-cofine-editor/src/index.ts new file mode 100644 index 00000000..22b50d75 --- /dev/null +++ b/packages/libro-cofine-editor/src/index.ts @@ -0,0 +1,6 @@ +export * from './editor-contribution.js'; +export * from './language-specs.js'; +export * from './libro-e2-editor.js'; +export * from './libro-e2-preload.js'; +export * from './libro-sql-dataphin-api.js'; +export * from './module.js'; diff --git a/packages/libro-cofine-editor/src/language-specs.ts b/packages/libro-cofine-editor/src/language-specs.ts new file mode 100644 index 00000000..0fedc947 --- /dev/null +++ b/packages/libro-cofine-editor/src/language-specs.ts @@ -0,0 +1,105 @@ +import type { Contribution } from '@difizen/mana-app'; +import { ApplicationContribution } from '@difizen/mana-app'; +import { contrib, inject, singleton, Syringe } from '@difizen/mana-app'; + +import type { LibroE2Editor, LibroE2EditorConfig } from './libro-e2-editor.js'; +import { LibroDataphinRequestAPI } from './libro-sql-dataphin-api.js'; + +export const LanguageSpecContribution = Syringe.defineToken('LanguageSpecContribution'); +export interface LanguageSpecContribution { + registerLanguageSpec: (register: LanguageSpecRegistry) => void; +} +export interface LanguageSpec { + name: string; + language: string; + mime: string; + ext: string[]; + loadModule?: (container: Syringe.Container) => Promise; + beforeEditorInit?: () => Promise; + editorConfig?: Partial; + afterEditorInit?: (editor: LibroE2Editor) => Promise; +} + +@singleton({ contrib: [ApplicationContribution] }) +export class LanguageSpecRegistry implements ApplicationContribution { + get languageSpecs(): LanguageSpec[] { + return this.languageSpecsData; + } + protected languageSpecsData: LanguageSpec[] = []; + + protected readonly languageSpecProvider: Contribution.Provider; + + constructor( + @contrib(LanguageSpecContribution) + languageSpecProvider: Contribution.Provider, + ) { + this.languageSpecProvider = languageSpecProvider; + } + + initialize() { + this.languageSpecProvider.getContributions().forEach((item) => { + item.registerLanguageSpec(this); + }); + } + + registerLanguageSpec(spec: LanguageSpec) { + const index = this.languageSpecsData.findIndex( + (item) => item.language === spec.language, + ); + if (index >= 0) { + this.languageSpecsData.splice(index, 1, spec); + } else { + this.languageSpecsData.push(spec); + } + } + + hasLanguage(spec: LanguageSpec) { + return ( + this.languageSpecsData.findIndex((item) => item.language === spec.language) > 0 + ); + } +} + +@singleton({ contrib: [LanguageSpecContribution] }) +export class LibroLanguageSpecs implements LanguageSpecContribution { + @inject(LibroDataphinRequestAPI) + protected readonly dataphinAPI: LibroDataphinRequestAPI; + + registerLanguageSpec = (register: LanguageSpecRegistry) => { + register.registerLanguageSpec({ + name: 'Python', + language: 'python', + ext: ['.py'], + mime: 'text/x-python', + // load: async container => { + // const textmate = await import('@difizen/libro-cofine-textmate'); + // container.load(textmate.TextmateModule); + // const module = await import('@difizen/libro-cofine-language-python'); + // container.load(module.default); + // }, + editorConfig: {}, + }); + register.registerLanguageSpec({ + name: 'SQL', + mime: 'application/vnd.libro.sql+json', + language: 'sql-odps', + ext: ['.sql'], + editorConfig: {}, + beforeEditorInit: async () => { + // + }, + }); + register.registerLanguageSpec({ + name: 'Markdown', + language: 'markdown', + mime: 'text/x-markdown', + ext: ['md', 'markdown', 'mkd'], + }); + register.registerLanguageSpec({ + name: 'Prompt', + language: 'markdown', + mime: 'application/vnd.libro.prompt+json', + ext: ['md', 'markdown', 'mkd'], + }); + }; +} diff --git a/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts b/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts new file mode 100644 index 00000000..faf3aaf1 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts @@ -0,0 +1,214 @@ +import { CompletionTriggerKind } from '@difizen/libro-lsp'; +import { languages } from '@difizen/monaco-editor-core'; +import type monaco from '@difizen/monaco-editor-core'; +import * as lsp from 'vscode-languageserver-protocol'; +import { InsertReplaceEdit } from 'vscode-languageserver-protocol'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; +import { CompletionItemKind } from './type-concerters.js'; + +export class CompletionProvider + extends LangaugeFeatureProvider + implements monaco.languages.CompletionItemProvider +{ + triggerCharacters: ['.', '[', '"', "'"]; + + public provideCompletionItems = async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): Promise => { + const result = await this.provideCompletionItemsFromLSPServer(model, position); + return result; + }; + + protected provideCompletionItemsFromKernel = async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): Promise => { + const editor = this.getEditorByModel(model); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + + const reply = await editor.completionProvider?.({ + cursorPosition: editor.getOffsetAt({ + column: position.column, + line: position.lineNumber, + }), + }); + + const start = reply?.cursor_start + ? editor.getPositionAt(reply?.cursor_start) + : editor.getCursorPosition(); + const end = reply?.cursor_end + ? editor.getPositionAt(reply?.cursor_end) + : editor.getCursorPosition(); + + const suggestion: languages.CompletionItem[] = (reply?.matches ?? []).map( + (match) => { + return { + label: match, + kind: languages.CompletionItemKind.Text, + insertText: match, + range: { + startColumn: start?.column, + startLineNumber: start?.line, + endColumn: end?.column, + endLineNumber: end?.line, + }, + } as languages.CompletionItem; + }, + ); + + return { + suggestions: suggestion as any, + }; + }; + + protected provideCompletionItemsFromLSPServer = async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): Promise => { + const provider = await this.getProvider(model); + if (!provider) { + return; + } + const editor = this.getEditorByModel(model); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + + const { lspConnection, editor: docEditor, virtualDocument: doc } = provider; + + if (!lspConnection.isReady || !lspConnection.provides('completionProvider')) { + return; + } + + const virtualPos = doc.transformEditorToVirtual(docEditor, { + line: position.lineNumber - 1, + ch: position.column, + isEditor: true, + }); + + if (!virtualPos) { + return; + } + + const result = await lspConnection.clientRequests[ + 'textDocument/completion' + ].request({ + position: { line: virtualPos.line, character: virtualPos.ch }, + textDocument: { + uri: doc.documentInfo.uri, + }, + context: { + triggerKind: CompletionTriggerKind.Invoked, + }, + }); + + const items = 'items' in result ? result.items : result; + + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: position.column, + }; + + const suggestions: monaco.languages.CompletionItem[] = items.map( + (item: lsp.CompletionItem) => { + return this.transformCompletion(item, range); + }, + ); + + if (Array.isArray(result)) { + return { suggestions }; + } + + return { + incomplete: result.isIncomplete, + suggestions, + }; + }; + + resolveCompletionItem = async ( + item: monaco.languages.CompletionItem, + token: monaco.CancellationToken, + ): Promise => { + const original = (item as any).__original as lsp.CompletionItem | undefined; + if (!original) { + return; + } + const itemResult = + await this.lspConnection.clientRequests['completionItem/resolve'].request( + original, + ); + if (token.isCancellationRequested) { + return; + } + const resolve = this.transformCompletion(itemResult, item.range); + return resolve; + }; + + transformCompletion( + item: lsp.CompletionItem, + range: monaco.IRange | monaco.languages.CompletionItemRanges, + ): monaco.languages.CompletionItem { + const converted: monaco.languages.CompletionItem = { + ...item, + label: item.label, + sortText: item.sortText, + filterText: item.filterText, + insertText: item.insertText ? item.insertText : item.label, + kind: CompletionItemKind.from(item.kind ?? lsp.CompletionItemKind.Property), + detail: item.detail, + documentation: item.documentation, + command: item.command + ? { + id: item.command.command, + title: item.command.title, + arguments: item.command?.arguments, + } + : undefined, + range, + additionalTextEdits: undefined, + }; + + // FIXME: text edit 生成的range有可能在其他的editor上,这里似乎无法处理 + if (item.textEdit) { + converted.insertText = item.textEdit.newText; + if (InsertReplaceEdit.is(item.textEdit)) { + converted.range = { + insert: convertRange(item.textEdit.insert), + replace: convertRange(item.textEdit.replace), + }; + } else { + converted.range = convertRange(item.textEdit.range); + } + } + + if (item.additionalTextEdits) { + converted.additionalTextEdits = item.additionalTextEdits.map((edit) => { + return { + range: convertRange(edit.range), + text: edit.newText, + }; + }); + } + + // Stash a few additional pieces of information. + (converted as any).__original = item; + + return converted; + } +} + +function convertRange(range: lsp.Range): monaco.IRange { + return { + startLineNumber: range.start.line + 1, + startColumn: range.start.character + 1, + endLineNumber: range.end.line + 1, + endColumn: range.end.character + 1, + }; +} diff --git a/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts b/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts new file mode 100644 index 00000000..3282900b --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts @@ -0,0 +1,148 @@ +import type { LibroService } from '@difizen/libro-core'; +import { EditorCellView } from '@difizen/libro-core'; +import type { LSPConnection, VirtualDocument } from '@difizen/libro-lsp'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { LibroE2Editor } from '../../libro-e2-editor.js'; +import { MonacoRange, MonacoUri } from '../../types.js'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; + +export enum MarkerSeverity { + Hint = 1, + Info = 2, + Warning = 4, + Error = 8, +} + +const vererityMap = { + 1: MarkerSeverity.Error, + 2: MarkerSeverity.Warning, + 3: MarkerSeverity.Info, + 4: MarkerSeverity.Hint, +}; + +export class DiagnosticProvider extends LangaugeFeatureProvider { + constructor( + libroService: LibroService, + lspConnection: LSPConnection, + virtualDocument: VirtualDocument, + ) { + super(libroService, lspConnection, virtualDocument); + this.processDiagnostic(); + } + + protected diagnosticList: { + model: monaco.editor.ITextModel; + markers: monaco.editor.IMarkerData[]; + }[] = []; + + protected addDiagnostic( + model: monaco.editor.ITextModel, + marker: monaco.editor.IMarkerData, + ) { + if ( + !this.diagnosticList.some((d) => d.model.uri.toString() === model.uri.toString()) + ) { + this.diagnosticList.push({ + model, + markers: [marker], + }); + } else { + this.diagnosticList + .find((d) => d.model.uri.toString() === model.uri.toString()) + ?.markers.push(marker); + } + } + + protected clearDiagnostic() { + this.libroService.active?.model.cells.forEach((item) => { + if (EditorCellView.is(item)) { + if (item.editor instanceof LibroE2Editor) { + const model = item.editor.monacoEditor?.getModel(); + if (model) { + monaco.editor.setModelMarkers(model, 'libro-e2', []); + } + } + } + }); + } + + protected displayDiagnostic() { + this.clearDiagnostic(); + this.diagnosticList.forEach((d) => { + monaco.editor.setModelMarkers(d.model, 'libro-e2', d.markers); + }); + } + + async processDiagnostic() { + this.lspConnection.serverNotifications['textDocument/publishDiagnostics'].event( + (e) => { + this.diagnosticList = []; + e.diagnostics.forEach((diagnostic) => { + const { range } = diagnostic; + // the diagnostic range must be in current editor + const editor = this.getEditorFromLSPPosition(range); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + const model = editor?.monacoEditor?.getModel(); + if (!model) { + return; + } + + const editorStart = this.virtualDocument.transformVirtualToEditor({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + + const editorEnd = this.virtualDocument.transformVirtualToEditor({ + line: range.end.line, + ch: range.end.character, + isVirtual: true, + }); + + if (!editorStart || !editorEnd) { + return; + } + + const markerRange = new MonacoRange( + editorStart.line + 1, + editorStart.ch, + editorEnd.line + 1, + editorEnd.ch, + ); + + const marker: monaco.editor.IMarkerData = { + source: diagnostic.source, + tags: diagnostic.tags, + message: diagnostic.message, + code: String(diagnostic.code), + severity: diagnostic.severity + ? vererityMap[diagnostic.severity] + : monaco.MarkerSeverity.Info, + relatedInformation: diagnostic.relatedInformation?.map((item) => { + return { + message: item.message, + resource: MonacoUri.parse(item.location.uri), + startLineNumber: markerRange.startLineNumber, + startColumn: markerRange.startColumn, + endLineNumber: markerRange.endLineNumber, + endColumn: markerRange.endColumn, + }; + }), + startLineNumber: editorStart.line + 1, + startColumn: editorStart.ch + 1, + endLineNumber: editorEnd.line + 1, + endColumn: editorEnd.ch + 1, + }; + + this.addDiagnostic(model, marker); + }); + + this.displayDiagnostic(); + }, + ); + } +} diff --git a/packages/libro-cofine-editor/src/language/lsp/format-provider.ts b/packages/libro-cofine-editor/src/language/lsp/format-provider.ts new file mode 100644 index 00000000..fa607dd9 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/format-provider.ts @@ -0,0 +1,87 @@ +import type monaco from '@difizen/monaco-editor-core'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; + +export class FormatProvider + extends LangaugeFeatureProvider + implements + monaco.languages.DocumentFormattingEditProvider, + monaco.languages.DocumentRangeFormattingEditProvider +{ + displayName = 'libro-e2-format'; + + provideDocumentRangeFormattingEdits = async ( + model: monaco.editor.ITextModel, + range: monaco.Range, + options: monaco.languages.FormattingOptions, + token: monaco.CancellationToken, + ): Promise => { + return this.formatDocumentByRange(model, range, options, token); + }; + provideDocumentFormattingEdits = async ( + model: monaco.editor.ITextModel, + options: monaco.languages.FormattingOptions, + token: monaco.CancellationToken, + ): Promise => { + return this.formatDocumentByRange(model, model.getFullModelRange(), options, token); + }; + + formatDocumentByRange = async ( + model: monaco.editor.ITextModel, + range: monaco.Range, + options: monaco.languages.FormattingOptions, + token: monaco.CancellationToken, + ): Promise => { + const provider = await this.getProvider(model); + if (!provider) { + return []; + } + + const { virtualDocument: doc } = provider; + const result = await this.lspConnection.clientRequests[ + 'textDocument/rangeFormatting' + ].request({ + // TODO: range transform + // TODO: pyright-extend supoport range format + range: { + start: { + line: range.startLineNumber - 1, + character: range.startColumn - 1, + }, + end: { + line: range.endLineNumber - 1, + character: range.endColumn - 1, + }, + }, + textDocument: { + uri: doc.documentInfo.uri, + }, + options: { + insertSpaces: options.insertSpaces, + tabSize: options.tabSize, + }, + }); + + if (token.isCancellationRequested) { + return []; + } + + if (!result) { + return []; + } + + const edits: monaco.languages.TextEdit[] = result.map((item) => { + return { + range: { + startColumn: item.range.start.character + 1, + startLineNumber: item.range.start.line + 1, + endColumn: item.range.end.character + 1, + endLineNumber: item.range.end.line + 1, + }, + text: item.newText, + }; + }); + + return edits; + }; +} diff --git a/packages/libro-cofine-editor/src/language/lsp/hover-provider.ts b/packages/libro-cofine-editor/src/language/lsp/hover-provider.ts new file mode 100644 index 00000000..6d110b2e --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/hover-provider.ts @@ -0,0 +1,98 @@ +import type monaco from '@difizen/monaco-editor-core'; +import type * as lsp from 'vscode-languageserver-protocol'; + +import { MonacoRange } from '../../types.js'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; + +export class HoverProvider + extends LangaugeFeatureProvider + implements monaco.languages.HoverProvider +{ + provideHover = async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken, + ): Promise => { + const editor = this.getEditorByModel(model); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + const provider = await this.getProvider(model); + if (!provider) { + return; + } + + const { lspConnection, editor: docEditor, virtualDocument: doc } = provider; + + const virtualPos = doc.transformEditorToVirtual(docEditor, { + line: position.lineNumber - 1, // lsp is zero based, monaco is one based + ch: position.column, + isEditor: true, + }); + + if (!virtualPos) { + return; + } + + const result = await lspConnection.clientRequests['textDocument/hover'].request({ + position: { line: virtualPos.line, character: virtualPos.ch }, + textDocument: { + uri: doc.documentInfo.uri, + }, + }); + + if (!result) { + return; + } + + const { contents, range } = result; + + const resultContents = [{ value: formatContents(contents) }]; + + let resultRange: monaco.Range | undefined; + + if (range) { + const editorStart = doc.transformVirtualToEditor({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + + const editorEnd = doc.transformVirtualToEditor({ + line: range.end.line, + ch: range.end.character, + isVirtual: true, + }); + if (editorStart && editorEnd) { + resultRange = new MonacoRange( + editorStart.line + 1, + editorStart.ch + 1, + editorEnd.line + 1, + editorEnd.ch + 1, + ); + } + } + + if (token.isCancellationRequested) { + return; + } + + return { + contents: resultContents, + range: resultRange, + }; + }; +} + +export function formatContents( + contents: lsp.MarkupContent | lsp.MarkedString | lsp.MarkedString[], +): string { + if (Array.isArray(contents)) { + return contents.map((c) => formatContents(c) + '\n\n').join(''); + } else if (typeof contents === 'string') { + return contents; + } else { + return contents.value; + } +} diff --git a/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts b/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts new file mode 100644 index 00000000..9fd6b3f3 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts @@ -0,0 +1,69 @@ +import type { LibroService } from '@difizen/libro-core'; +import { EditorCellView } from '@difizen/libro-core'; +import type { LSPProviderResult } from '@difizen/libro-lsp'; +import type { LSPConnection, VirtualDocument } from '@difizen/libro-lsp'; +import type monaco from '@difizen/monaco-editor-core'; +import type * as lsp from 'vscode-languageserver-protocol'; + +import { LibroE2Editor } from '../../libro-e2-editor.js'; + +export class LangaugeFeatureProvider { + protected libroService: LibroService; + protected lspProvider?: LSPProviderResult; + protected lspConnection: LSPConnection; + virtualDocument: VirtualDocument; + constructor( + libroService: LibroService, + lspConnection: LSPConnection, + virtualDocument: VirtualDocument, + ) { + this.libroService = libroService; + this.lspConnection = lspConnection; + this.virtualDocument = virtualDocument; + } + + protected getEditorByModel(model: monaco.editor.ITextModel) { + const cells = this.libroService.active?.model.cells; + if (!cells) { + return; + } + const cell = cells.find((item) => { + const editorUuid = model.uri.path.split('.')[0]; + if (!EditorCellView.is(item)) { + return false; + } + const e2editor = item.editor; + if (!(e2editor instanceof LibroE2Editor)) { + return false; + } + return editorUuid === e2editor.uuid; + }); + + return (cell as EditorCellView).editor as LibroE2Editor | undefined; + } + + protected getEditorFromLSPPosition(range: lsp.Range) { + const currentEditor = this.virtualDocument.getEditorAtVirtualLine({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + const editor = currentEditor.getEditor(); + if (editor instanceof LibroE2Editor) { + return editor; + } + return; + } + + protected async getProvider(model: monaco.editor.ITextModel) { + const editor = this.getEditorByModel(model); + + if (!editor) { + return; + } + + const provider = await editor.lspProvider?.(); + this.lspProvider = provider; + return provider; + } +} diff --git a/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts b/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts new file mode 100644 index 00000000..e0e0f992 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts @@ -0,0 +1,147 @@ +import { + EditorHandlerContribution, + LanguageOptionsRegistry, +} from '@difizen/libro-cofine-editor-core'; +import { LibroService } from '@difizen/libro-core'; +import type { LSPConnection } from '@difizen/libro-lsp'; +import { ILSPDocumentConnectionManager } from '@difizen/libro-lsp'; +import { inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import { LibroE2URIScheme } from '../../libro-e2-editor.js'; + +import { CompletionProvider } from './completion-provider.js'; +import { DiagnosticProvider } from './diagnostic-provider.js'; +import { HoverProvider } from './hover-provider.js'; +import { SignatureHelpProvider } from './signature-help-provider.js'; + +@singleton({ + contrib: [EditorHandlerContribution], +}) +export class LSPContribution implements EditorHandlerContribution { + protected readonly optionsResgistry: LanguageOptionsRegistry; + + protected readonly libroService: LibroService; + + protected readonly lspDocumentConnectionManager: ILSPDocumentConnectionManager; + + constructor( + @inject(LanguageOptionsRegistry) optionsResgistry: LanguageOptionsRegistry, + @inject(LibroService) libroService: LibroService, + @inject(ILSPDocumentConnectionManager) + lspDocumentConnectionManager: ILSPDocumentConnectionManager, + ) { + this.optionsResgistry = optionsResgistry; + this.libroService = libroService; + this.lspDocumentConnectionManager = lspDocumentConnectionManager; + } + + protected lspLangs = ['python']; + + beforeCreate() { + // + } + afterCreate(editor: any) { + this.registerLSPFeature(editor as monaco.editor.IStandaloneCodeEditor); + } + canHandle(language: string) { + return this.lspLangs.includes(language); + } + + getLanguageSelector( + model: monaco.editor.ITextModel, + ): monaco.languages.LanguageFilter { + return { + scheme: LibroE2URIScheme, + pattern: model.uri.path, + }; + } + + async getVirtualDocument() { + const libroView = this.libroService.active; + if (!libroView) { + return; + } + await this.lspDocumentConnectionManager.ready; + const adapter = this.lspDocumentConnectionManager.adapters.get(libroView.model.id); + if (!adapter) { + throw new Error('no adapter'); + } + + await adapter.ready; + + // Get the associated virtual document of the opened document + const virtualDocument = adapter.virtualDocument; + return virtualDocument; + } + + async getLSPConnection() { + const virtualDocument = await this.getVirtualDocument(); + if (!virtualDocument) { + throw new Error('no virtualDocument'); + } + + // Get the LSP connection of the virtual document. + const lspConnection = this.lspDocumentConnectionManager.connections.get( + virtualDocument.uri, + ) as LSPConnection; + + return lspConnection; + } + + registerLSPFeature(editor: monaco.editor.IStandaloneCodeEditor) { + const model = editor.getModel(); + if (!model) { + return; + } + + Promise.all([this.getVirtualDocument(), this.getLSPConnection()]) + .then(([virtualDocument, lspConnection]) => { + if (!lspConnection || !virtualDocument) { + return; + } + monaco.languages.registerCompletionItemProvider( + this.getLanguageSelector(model), + new CompletionProvider(this.libroService, lspConnection, virtualDocument), + ); + monaco.languages.registerHoverProvider( + this.getLanguageSelector(model), + new HoverProvider(this.libroService, lspConnection, virtualDocument), + ); + new DiagnosticProvider(this.libroService, lspConnection, virtualDocument); + monaco.languages.registerSignatureHelpProvider( + this.getLanguageSelector(model), + new SignatureHelpProvider(this.libroService, lspConnection, virtualDocument), + ); + // const formatProvider = new FormatProvider( + // this.libroService, + // lspConnection, + // virtualDocument, + // ); + // monaco.languages.registerDocumentFormattingEditProvider( + // this.getLanguageSelector(model), + // formatProvider, + // ); + // monaco.languages.registerDocumentRangeFormattingEditProvider( + // this.getLanguageSelector(model), + // formatProvider, + // ); + return; + }) + .catch(console.error); + + // // SignatureHelp + // monaco.languages.registerSignatureHelpProvider(id, new SignatureHelpProvider(this._worker)); + + // // 定义跳转; + // monaco.languages.registerDefinitionProvider(id, new DefinitionAdapter(this._worker)); + } + + protected isDisposed = false; + get disposed() { + return this.isDisposed; + } + dispose() { + this.isDisposed = false; + } +} diff --git a/packages/libro-cofine-editor/src/language/lsp/module.ts b/packages/libro-cofine-editor/src/language/lsp/module.ts new file mode 100644 index 00000000..ba93012d --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/module.ts @@ -0,0 +1,5 @@ +import { Module } from '@difizen/mana-app'; + +import { LSPContribution } from './lsp-contribution.js'; + +export const LSPFeatureModule = Module().register(LSPContribution); diff --git a/packages/libro-cofine-editor/src/language/lsp/semantic-highlight-provider.ts b/packages/libro-cofine-editor/src/language/lsp/semantic-highlight-provider.ts new file mode 100644 index 00000000..e55e6551 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/semantic-highlight-provider.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type monaco from '@difizen/monaco-editor-core'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; + +export class SemanticHighlightProvider + extends LangaugeFeatureProvider + implements monaco.languages.DocumentSemanticTokensProvider +{ + onDidChange?: monaco.IEvent | undefined; + getLegend(): monaco.languages.SemanticTokensLegend { + throw new Error('Method not implemented.'); + } + provideDocumentSemanticTokens( + model: monaco.editor.ITextModel, + lastResultId: string | null, + token: monaco.CancellationToken, + ): monaco.languages.ProviderResult< + monaco.languages.SemanticTokens | monaco.languages.SemanticTokensEdits + > { + throw new Error('Method not implemented.'); + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/libro-cofine-editor/src/language/lsp/signature-help-provider.ts b/packages/libro-cofine-editor/src/language/lsp/signature-help-provider.ts new file mode 100644 index 00000000..e6c1d006 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/signature-help-provider.ts @@ -0,0 +1,65 @@ +import type monaco from '@difizen/monaco-editor-core'; + +import { LangaugeFeatureProvider } from './language-feature-provider.js'; + +export class SignatureHelpProvider + extends LangaugeFeatureProvider + implements monaco.languages.SignatureHelpProvider +{ + signatureHelpTriggerCharacters: ['(', ',', ')']; + signatureHelpRetriggerCharacters?: readonly string[] | undefined; + provideSignatureHelp = async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken, + context: monaco.languages.SignatureHelpContext, + ): Promise => { + const editor = this.getEditorByModel(model); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + const provider = await this.getProvider(model); + if (!provider) { + return; + } + + const { lspConnection, editor: docEditor, virtualDocument: doc } = provider; + + const virtualPos = doc.transformEditorToVirtual(docEditor, { + line: position.lineNumber - 1, // lsp is zero based, monaco is one based + ch: position.column - 1, + isEditor: true, + }); + + if (!virtualPos) { + return; + } + + const sigInfo = await lspConnection.clientRequests[ + 'textDocument/signatureHelp' + ].request({ + position: { line: virtualPos.line, character: virtualPos.ch }, + textDocument: { + uri: doc.documentInfo.uri, + }, + }); + + return { + value: { + signatures: sigInfo.signatures.map((sig: any) => { + return { + label: sig.label, + documentation: sig.documentation, + parameters: sig.parameters ?? [], + activeParameter: sig.activeParameter, + }; + }), + activeSignature: sigInfo.activeSignature ?? 0, + activeParameter: sigInfo.activeParameter ?? 0, + }, + dispose: () => { + // + }, + }; + }; +} diff --git a/packages/libro-cofine-editor/src/language/lsp/type-concerters.ts b/packages/libro-cofine-editor/src/language/lsp/type-concerters.ts new file mode 100644 index 00000000..8e00f506 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/lsp/type-concerters.ts @@ -0,0 +1,72 @@ +import { languages } from '@difizen/monaco-editor-core'; +import * as lsp from 'vscode-languageserver-protocol'; + +const _from = new Map([ + [lsp.CompletionItemKind.Method, languages.CompletionItemKind.Method], + [lsp.CompletionItemKind.Function, languages.CompletionItemKind.Function], + [lsp.CompletionItemKind.Constructor, languages.CompletionItemKind.Constructor], + [lsp.CompletionItemKind.Field, languages.CompletionItemKind.Field], + [lsp.CompletionItemKind.Variable, languages.CompletionItemKind.Variable], + [lsp.CompletionItemKind.Class, languages.CompletionItemKind.Class], + [lsp.CompletionItemKind.Interface, languages.CompletionItemKind.Interface], + [lsp.CompletionItemKind.Struct, languages.CompletionItemKind.Struct], + [lsp.CompletionItemKind.Module, languages.CompletionItemKind.Module], + [lsp.CompletionItemKind.Property, languages.CompletionItemKind.Property], + [lsp.CompletionItemKind.Unit, languages.CompletionItemKind.Unit], + [lsp.CompletionItemKind.Value, languages.CompletionItemKind.Value], + [lsp.CompletionItemKind.Constant, languages.CompletionItemKind.Constant], + [lsp.CompletionItemKind.Enum, languages.CompletionItemKind.Enum], + [lsp.CompletionItemKind.EnumMember, languages.CompletionItemKind.EnumMember], + [lsp.CompletionItemKind.Keyword, languages.CompletionItemKind.Keyword], + [lsp.CompletionItemKind.Snippet, languages.CompletionItemKind.Snippet], + [lsp.CompletionItemKind.Text, languages.CompletionItemKind.Text], + [lsp.CompletionItemKind.Color, languages.CompletionItemKind.Color], + [lsp.CompletionItemKind.File, languages.CompletionItemKind.File], + [lsp.CompletionItemKind.Reference, languages.CompletionItemKind.Reference], + [lsp.CompletionItemKind.Folder, languages.CompletionItemKind.Folder], + [lsp.CompletionItemKind.Event, languages.CompletionItemKind.Event], + [lsp.CompletionItemKind.Operator, languages.CompletionItemKind.Operator], + [lsp.CompletionItemKind.TypeParameter, languages.CompletionItemKind.TypeParameter], + // [lsp.CompletionItemKind.Issue, languages.CompletionItemKind.Issue], + // [lsp.CompletionItemKind.User, languages.CompletionItemKind.User], +]); + +const _to = new Map([ + [languages.CompletionItemKind.Method, lsp.CompletionItemKind.Method], + [languages.CompletionItemKind.Function, lsp.CompletionItemKind.Function], + [languages.CompletionItemKind.Constructor, lsp.CompletionItemKind.Constructor], + [languages.CompletionItemKind.Field, lsp.CompletionItemKind.Field], + [languages.CompletionItemKind.Variable, lsp.CompletionItemKind.Variable], + [languages.CompletionItemKind.Class, lsp.CompletionItemKind.Class], + [languages.CompletionItemKind.Interface, lsp.CompletionItemKind.Interface], + [languages.CompletionItemKind.Struct, lsp.CompletionItemKind.Struct], + [languages.CompletionItemKind.Module, lsp.CompletionItemKind.Module], + [languages.CompletionItemKind.Property, lsp.CompletionItemKind.Property], + [languages.CompletionItemKind.Unit, lsp.CompletionItemKind.Unit], + [languages.CompletionItemKind.Value, lsp.CompletionItemKind.Value], + [languages.CompletionItemKind.Constant, lsp.CompletionItemKind.Constant], + [languages.CompletionItemKind.Enum, lsp.CompletionItemKind.Enum], + [languages.CompletionItemKind.EnumMember, lsp.CompletionItemKind.EnumMember], + [languages.CompletionItemKind.Keyword, lsp.CompletionItemKind.Keyword], + [languages.CompletionItemKind.Snippet, lsp.CompletionItemKind.Snippet], + [languages.CompletionItemKind.Text, lsp.CompletionItemKind.Text], + [languages.CompletionItemKind.Color, lsp.CompletionItemKind.Color], + [languages.CompletionItemKind.File, lsp.CompletionItemKind.File], + [languages.CompletionItemKind.Reference, lsp.CompletionItemKind.Reference], + [languages.CompletionItemKind.Folder, lsp.CompletionItemKind.Folder], + [languages.CompletionItemKind.Event, lsp.CompletionItemKind.Event], + [languages.CompletionItemKind.Operator, lsp.CompletionItemKind.Operator], + [languages.CompletionItemKind.TypeParameter, lsp.CompletionItemKind.TypeParameter], + // [languages.CompletionItemKind.User, lsp.CompletionItemKind.User], + // [languages.CompletionItemKind.Issue, lsp.CompletionItemKind.Issue], +]); + +export class CompletionItemKind { + static from(kind: lsp.CompletionItemKind): languages.CompletionItemKind { + return _from.get(kind) ?? languages.CompletionItemKind.Property; + } + + static to(kind: languages.CompletionItemKind): lsp.CompletionItemKind { + return _to.get(kind) ?? lsp.CompletionItemKind.Property; + } +} diff --git a/packages/libro-cofine-editor/src/language/python/data/MagicPython.tmLanguage.json b/packages/libro-cofine-editor/src/language/python/data/MagicPython.tmLanguage.json new file mode 100644 index 00000000..475e7569 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/python/data/MagicPython.tmLanguage.json @@ -0,0 +1,4213 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/MagicStack/MagicPython/blob/master/grammars/MagicPython.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/MagicStack/MagicPython/commit/7d0f2b22a5ad8fccbd7341bc7b7a715169283044", + "name": "MagicPython", + "scopeName": "source.python", + "patterns": [ + { + "include": "#statement" + }, + { + "include": "#expression" + } + ], + "repository": { + "impossible": { + "comment": "This is a special rule that should be used where no match is desired. It is not a good idea to match something like '1{0}' because in some cases that can result in infinite loops in token generation. So the rule instead matches and impossible expression to allow a match to fail and move to the next token.", + "match": "$.^" + }, + "statement": { + "patterns": [ + { + "include": "#import" + }, + { + "include": "#class-declaration" + }, + { + "include": "#function-declaration" + }, + { + "include": "#generator" + }, + { + "include": "#statement-keyword" + }, + { + "include": "#assignment-operator" + }, + { + "include": "#decorator" + }, + { + "include": "#docstring-statement" + }, + { + "include": "#semicolon" + } + ] + }, + "semicolon": { + "patterns": [ + { + "name": "invalid.deprecated.semicolon.python", + "match": "\\;$" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.python", + "contentName": "meta.typehint.comment.python", + "begin": "(?x)\n (?:\n \\# \\s* (type:)\n \\s*+ (?# we want `\\s*+` which is possessive quantifier since\n we do not actually want to backtrack when matching\n whitespace here)\n (?! $ | \\#)\n )\n", + "end": "(?:$|(?=\\#))", + "beginCaptures": { + "0": { + "name": "meta.typehint.comment.python" + }, + "1": { + "name": "comment.typehint.directive.notation.python" + } + }, + "patterns": [ + { + "name": "comment.typehint.ignore.notation.python", + "match": "(?x)\n \\G ignore\n (?= \\s* (?: $ | \\#))\n" + }, + { + "name": "comment.typehint.type.notation.python", + "match": "(?x)\n (?))" + }, + { + "name": "comment.typehint.variable.notation.python", + "match": "([[:alpha:]_]\\w*)" + } + ] + }, + { + "include": "#comments-base" + } + ] + }, + "docstring-statement": { + "begin": "^(?=\\s*[rR]?(\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\"))", + "comment": "the string either terminates correctly or by the beginning of a new line (this is for single line docstrings that aren't terminated) AND it's not followed by another docstring", + "end": "((?<=\\1)|^)(?!\\s*[rR]?(\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\"))", + "patterns": [ + { + "include": "#docstring" + } + ] + }, + "docstring": { + "patterns": [ + { + "name": "string.quoted.docstring.multi.python", + "begin": "(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\1)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + }, + { + "include": "#docstring-guts-unicode" + } + ] + }, + { + "name": "string.quoted.docstring.raw.multi.python", + "begin": "([rR])(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#string-consume-escape" + }, + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + } + ] + }, + { + "name": "string.quoted.docstring.single.python", + "begin": "(\\'|\\\")", + "end": "(\\1)|(\\n)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + }, + { + "include": "#docstring-guts-unicode" + } + ] + }, + { + "name": "string.quoted.docstring.raw.single.python", + "begin": "([rR])(\\'|\\\")", + "end": "(\\2)|(\\n)", + "beginCaptures": { + "1": { + "name": "storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#string-consume-escape" + }, + { + "include": "#codetags" + } + ] + } + ] + }, + "docstring-guts-unicode": { + "patterns": [ + { + "include": "#escape-sequence-unicode" + }, + { + "include": "#escape-sequence" + }, + { + "include": "#string-line-continuation" + } + ] + }, + "docstring-prompt": { + "match": "(?x)\n (?:\n (?:^|\\G) \\s* (?# '\\G' is necessary for ST)\n ((?:>>>|\\.\\.\\.) \\s) (?=\\s*\\S)\n )\n", + "captures": { + "1": { + "name": "keyword.control.flow.python" + } + } + }, + "statement-keyword": { + "patterns": [ + { + "name": "storage.type.function.python", + "match": "\\b((async\\s+)?\\s*def)\\b" + }, + { + "name": "keyword.control.flow.python", + "comment": "if `as` is eventually followed by `:` or line continuation\nit's probably control flow like:\n with foo as bar, \\\n Foo as Bar:\n try:\n do_stuff()\n except Exception as e:\n pass\n", + "match": "\\b(?>= | //= | \\*\\*=\n | \\+= | -= | /= | @=\n | \\*= | %= | ~= | \\^= | &= | \\|=\n | =(?!=)\n" + }, + "operator": { + "match": "(?x)\n \\b(?> | & | \\| | \\^ | ~) (?# 3)\n\n | (\\*\\* | \\* | \\+ | - | % | // | / | @) (?# 4)\n\n | (!= | == | >= | <= | < | >) (?# 5)\n\n | (:=) (?# 6)\n", + "captures": { + "1": { + "name": "keyword.operator.logical.python" + }, + "2": { + "name": "keyword.control.flow.python" + }, + "3": { + "name": "keyword.operator.bitwise.python" + }, + "4": { + "name": "keyword.operator.arithmetic.python" + }, + "5": { + "name": "keyword.operator.comparison.python" + }, + "6": { + "name": "keyword.operator.assignment.python" + } + } + }, + "punctuation": { + "patterns": [ + { + "name": "punctuation.separator.colon.python", + "match": ":" + }, + { + "name": "punctuation.separator.element.python", + "match": "," + } + ] + }, + "literal": { + "patterns": [ + { + "name": "constant.language.python", + "match": "\\b(True|False|None|NotImplemented|Ellipsis)\\b" + }, + { + "include": "#number" + } + ] + }, + "number": { + "name": "constant.numeric.python", + "patterns": [ + { + "include": "#number-float" + }, + { + "include": "#number-dec" + }, + { + "include": "#number-hex" + }, + { + "include": "#number-oct" + }, + { + "include": "#number-bin" + }, + { + "include": "#number-long" + }, + { + "name": "invalid.illegal.name.python", + "match": "\\b[0-9]+\\w+" + } + ] + }, + "number-float": { + "name": "constant.numeric.float.python", + "match": "(?x)\n (?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )?\n })\n )\n", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "3": { + "name": "storage.type.format.python" + }, + "4": { + "name": "storage.type.format.python" + } + } + }, + { + "name": "meta.format.brace.python", + "match": "(?x)\n (\n {\n \\w* (\\.[[:alpha:]_]\\w* | \\[[^\\]'\"]+\\])*\n (![rsa])?\n (:)\n [^'\"{}\\n]* (?:\n \\{ [^'\"}\\n]*? \\} [^'\"{}\\n]*\n )*\n }\n )\n", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "3": { + "name": "storage.type.format.python" + }, + "4": { + "name": "storage.type.format.python" + } + } + } + ] + }, + "fstring-formatting": { + "patterns": [ + { + "include": "#fstring-formatting-braces" + }, + { + "include": "#fstring-formatting-singe-brace" + } + ] + }, + "fstring-formatting-singe-brace": { + "name": "invalid.illegal.brace.python", + "match": "(}(?!}))" + }, + "import": { + "comment": "Import statements used to correctly mark `from`, `import`, and `as`\n", + "patterns": [ + { + "begin": "\\b(?)", + "end": "(?=:)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.annotation.result.python" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "item-access": { + "patterns": [ + { + "name": "meta.item-access.python", + "begin": "(?x)\n \\b(?=\n [[:alpha:]_]\\w* \\s* \\[\n )\n", + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#item-name" + }, + { + "include": "#item-index" + }, + { + "include": "#expression" + } + ] + } + ] + }, + "item-name": { + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#special-names" + }, + { + "name": "meta.indexed-name.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "item-index": { + "begin": "(\\[)", + "end": "(?=\\])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + } + }, + "contentName": "meta.item-access.arguments.python", + "patterns": [ + { + "name": "punctuation.separator.slice.python", + "match": ":" + }, + { + "include": "#expression" + } + ] + }, + "decorator": { + "name": "meta.function.decorator.python", + "begin": "(?x)\n ^\\s*\n ((@)) \\s* (?=[[:alpha:]_]\\w*)\n", + "end": "(?x)\n ( \\) )\n # trailing whitespace and comments are legal\n (?: (.*?) (?=\\s*(?:\\#|$)) )\n | (?=\\n|\\#)\n", + "beginCaptures": { + "1": { + "name": "entity.name.function.decorator.python" + }, + "2": { + "name": "punctuation.definition.decorator.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "invalid.illegal.decorator.python" + } + }, + "patterns": [ + { + "include": "#decorator-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "decorator-name": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#illegal-object-name" + }, + { + "name": "entity.name.function.decorator.python", + "match": "(?x)\n ([[:alpha:]_]\\w*) | (\\.)\n", + "captures": { + "2": { + "name": "punctuation.separator.period.python" + } + } + }, + { + "include": "#line-continuation" + }, + { + "name": "invalid.illegal.decorator.python", + "match": "(?x)\n \\s* ([^([:alpha:]\\s_\\.#\\\\] .*?) (?=\\#|$)\n", + "captures": { + "1": { + "name": "invalid.illegal.decorator.python" + } + } + } + ] + }, + "call-wrapper-inheritance": { + "comment": "same as a function call, but in inheritance context", + "name": "meta.function-call.python", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#inheritance-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "inheritance-name": { + "patterns": [ + { + "include": "#lambda-incomplete" + }, + { + "include": "#builtin-possible-callables" + }, + { + "include": "#inheritance-identifier" + } + ] + }, + "function-call": { + "name": "meta.function-call.python", + "comment": "Regular function call of the type \"name(args)\"", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#function-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "function-name": { + "patterns": [ + { + "include": "#builtin-possible-callables" + }, + { + "comment": "Some color schemas support meta.function-call.generic scope", + "name": "meta.function-call.generic.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "function-arguments": { + "begin": "(\\()", + "end": "(?=\\))(?!\\)\\s*\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + } + }, + "contentName": "meta.function-call.arguments.python", + "patterns": [ + { + "name": "punctuation.separator.arguments.python", + "match": "(,)" + }, + { + "match": "(?x)\n (?:(?<=[,(])|^) \\s* (\\*{1,2})\n", + "captures": { + "1": { + "name": "keyword.operator.unpacking.arguments.python" + } + } + }, + { + "include": "#lambda-incomplete" + }, + { + "include": "#illegal-names" + }, + { + "match": "\\b([[:alpha:]_]\\w*)\\s*(=)(?!=)", + "captures": { + "1": { + "name": "variable.parameter.function-call.python" + }, + "2": { + "name": "keyword.operator.assignment.python" + } + } + }, + { + "name": "keyword.operator.assignment.python", + "match": "=(?!=)" + }, + { + "include": "#expression" + }, + { + "match": "\\s*(\\))\\s*(\\()", + "captures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "punctuation.definition.arguments.begin.python" + } + } + } + ] + }, + "builtin-callables": { + "patterns": [ + { + "include": "#illegal-names" + }, + { + "include": "#illegal-object-name" + }, + { + "include": "#builtin-exceptions" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#builtin-types" + } + ] + }, + "builtin-possible-callables": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#magic-names" + } + ] + }, + "builtin-exceptions": { + "name": "support.type.exception.python", + "match": "(?x) (?" + }, + "regexp-base-expression": { + "patterns": [ + { + "include": "#regexp-quantifier" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fregexp-base-expression": { + "patterns": [ + { + "include": "#fregexp-quantifier" + }, + { + "include": "#fstring-formatting-braces" + }, + { + "match": "\\{.*?\\}" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fstring-formatting-braces": { + "patterns": [ + { + "comment": "empty braces are illegal", + "match": "({)(\\s*?)(})", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "2": { + "name": "invalid.illegal.brace.python" + }, + "3": { + "name": "constant.character.format.placeholder.other.python" + } + } + }, + { + "name": "constant.character.escape.python", + "match": "({{|}})" + } + ] + }, + "regexp-base-common": { + "patterns": [ + { + "name": "support.other.match.any.regexp", + "match": "\\." + }, + { + "name": "support.other.match.begin.regexp", + "match": "\\^" + }, + { + "name": "support.other.match.end.regexp", + "match": "\\$" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[+*?]\\??" + }, + { + "name": "keyword.operator.disjunction.regexp", + "match": "\\|" + }, + { + "include": "#regexp-escape-sequence" + } + ] + }, + "regexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\n" + }, + "fregexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{\\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\\}\n" + }, + "regexp-backreference-number": { + "name": "meta.backreference.regexp", + "match": "(\\\\[1-9]\\d?)", + "captures": { + "1": { + "name": "entity.name.tag.backreference.regexp" + } + } + }, + "regexp-backreference": { + "name": "meta.backreference.named.regexp", + "match": "(?x)\n (\\() (\\?P= \\w+(?:\\s+[[:alnum:]]+)?) (\\))\n", + "captures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.backreference.regexp" + }, + "3": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.end.regexp" + } + } + }, + "regexp-flags": { + "name": "storage.modifier.flag.regexp", + "match": "\\(\\?[aiLmsux]+\\)" + }, + "regexp-escape-special": { + "name": "support.other.escape.special.regexp", + "match": "\\\\([AbBdDsSwWZ])" + }, + "regexp-escape-character": { + "name": "constant.character.escape.regexp", + "match": "(?x)\n \\\\ (\n x[0-9A-Fa-f]{2}\n | 0[0-7]{1,2}\n | [0-7]{3}\n )\n" + }, + "regexp-escape-unicode": { + "name": "constant.character.unicode.regexp", + "match": "(?x)\n \\\\ (\n u[0-9A-Fa-f]{4}\n | U[0-9A-Fa-f]{8}\n )\n" + }, + "regexp-escape-catchall": { + "name": "constant.character.escape.regexp", + "match": "\\\\(.|\\n)" + }, + "regexp-escape-sequence": { + "patterns": [ + { + "include": "#regexp-escape-special" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-backreference-number" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "regexp-charecter-set-escapes": { + "patterns": [ + { + "name": "constant.character.escape.regexp", + "match": "\\\\[abfnrtv\\\\]" + }, + { + "include": "#regexp-escape-special" + }, + { + "name": "constant.character.escape.regexp", + "match": "\\\\([0-7]{1,3})" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "codetags": { + "match": "(?:\\b(NOTE|XXX|HACK|FIXME|BUG|TODO)\\b)", + "captures": { + "1": { + "name": "keyword.codetag.notation.python" + } + } + }, + "comments-base": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($)", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-single-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?='''))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-double-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?=\"\"\"))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-one-regexp-expression": { + "patterns": [ + { + "include": "#regexp-base-expression" + }, + { + "include": "#single-one-regexp-character-set" + }, + { + "include": "#single-one-regexp-comments" + }, + { + "include": "#regexp-flags" + }, + { + "include": "#single-one-regexp-named-group" + }, + { + "include": "#regexp-backreference" + }, + { + "include": "#single-one-regexp-lookahead" + }, + { + "include": "#single-one-regexp-lookahead-negative" + }, + { + "include": "#single-one-regexp-lookbehind" + }, + { + "include": "#single-one-regexp-lookbehind-negative" + }, + { + "include": "#single-one-regexp-conditional" + }, + { + "include": "#single-one-regexp-parentheses-non-capturing" + }, + { + "include": "#single-one-regexp-parentheses" + } + ] + }, + "single-one-regexp-character-set": { + "patterns": [ + { + "match": "(?x)\n \\[ \\^? \\] (?! .*?\\])\n" + }, + { + "name": "meta.character.set.regexp", + "begin": "(\\[)(\\^)?(\\])?", + "end": "(\\]|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?)\n", + "end": "(\\)|(?=\"))|((?=(?)\n", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "double-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-single-tail" + } + ] + }, + "fstring-terminator-single-tail": { + "begin": "((?:=?)(?:![rsa])?)(:)(?=.*?{)", + "end": "(?=})|(?=\\n)", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-single-brace" + }, + { + "include": "#fstring-single-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + }, + "fstring-fnorm-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[fF])([bBuU])?('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "2": { + "name": "invalid.illegal.prefix.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.interpolated.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-normf-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[bBuU])([fF])('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "invalid.illegal.prefix.python" + }, + "2": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-raw-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b(?:[rR][fF]|[fF][rR]))('''|\"\"\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.raw.multi.python storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python string.quoted.raw.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.raw.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-raw-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-raw-multi-core" + } + ] + }, + "fstring-multi-core": { + "name": "string.interpolated.python string.quoted.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-raw-multi-core": { + "name": "string.interpolated.python string.quoted.raw.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-multi-brace": { + "comment": "value interpolation using { ... }", + "begin": "(\\{)", + "end": "(?x)\n (\\})\n", + "beginCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "endCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "patterns": [ + { + "include": "#fstring-terminator-multi" + }, + { + "include": "#f-expression" + } + ] + }, + "fstring-terminator-multi": { + "patterns": [ + { + "name": "storage.type.format.python", + "match": "(=(![rsa])?)(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(=?![rsa])(?=})" + }, + { + "match": "(?x)\n ( (?: =?) (?: ![rsa])? )\n ( : \\w? [<>=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-multi-tail" + } + ] + }, + "fstring-terminator-multi-tail": { + "begin": "((?:=?)(?:![rsa])?)(:)(?=.*?{)", + "end": "(?=})", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + } + } +} diff --git a/packages/libro-cofine-editor/src/language/python/data/MagicRegExp.tmLanguage.json b/packages/libro-cofine-editor/src/language/python/data/MagicRegExp.tmLanguage.json new file mode 100644 index 00000000..bc9d737f --- /dev/null +++ b/packages/libro-cofine-editor/src/language/python/data/MagicRegExp.tmLanguage.json @@ -0,0 +1,497 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/MagicStack/MagicPython/blob/master/grammars/MagicRegExp.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/MagicStack/MagicPython/commit/c9b3409deb69acec31bbf7913830e93a046b30cc", + "name": "MagicRegExp", + "scopeName": "source.regexp.python", + "patterns": [ + { + "include": "#regexp-expression" + } + ], + "repository": { + "regexp-base-expression": { + "patterns": [ + { + "include": "#regexp-quantifier" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fregexp-base-expression": { + "patterns": [ + { + "include": "#fregexp-quantifier" + }, + { + "include": "#fstring-formatting-braces" + }, + { + "match": "\\{.*?\\}" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fstring-formatting-braces": { + "patterns": [ + { + "comment": "empty braces are illegal", + "match": "({)(\\s*?)(})", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "2": { + "name": "invalid.illegal.brace.python" + }, + "3": { + "name": "constant.character.format.placeholder.other.python" + } + } + }, + { + "name": "constant.character.escape.python", + "match": "({{|}})" + } + ] + }, + "regexp-base-common": { + "patterns": [ + { + "name": "support.other.match.any.regexp", + "match": "\\." + }, + { + "name": "support.other.match.begin.regexp", + "match": "\\^" + }, + { + "name": "support.other.match.end.regexp", + "match": "\\$" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[+*?]\\??" + }, + { + "name": "keyword.operator.disjunction.regexp", + "match": "\\|" + }, + { + "include": "#regexp-escape-sequence" + } + ] + }, + "regexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\n" + }, + "fregexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{\\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\\}\n" + }, + "regexp-backreference-number": { + "name": "meta.backreference.regexp", + "match": "(\\\\[1-9]\\d?)", + "captures": { + "1": { + "name": "entity.name.tag.backreference.regexp" + } + } + }, + "regexp-backreference": { + "name": "meta.backreference.named.regexp", + "match": "(?x)\n (\\() (\\?P= \\w+(?:\\s+[[:alnum:]]+)?) (\\))\n", + "captures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.backreference.regexp" + }, + "3": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.end.regexp" + } + } + }, + "regexp-flags": { + "name": "storage.modifier.flag.regexp", + "match": "\\(\\?[aiLmsux]+\\)" + }, + "regexp-escape-special": { + "name": "support.other.escape.special.regexp", + "match": "\\\\([AbBdDsSwWZ])" + }, + "regexp-escape-character": { + "name": "constant.character.escape.regexp", + "match": "(?x)\n \\\\ (\n x[0-9A-Fa-f]{2}\n | 0[0-7]{1,2}\n | [0-7]{3}\n )\n" + }, + "regexp-escape-unicode": { + "name": "constant.character.unicode.regexp", + "match": "(?x)\n \\\\ (\n u[0-9A-Fa-f]{4}\n | U[0-9A-Fa-f]{8}\n )\n" + }, + "regexp-escape-catchall": { + "name": "constant.character.escape.regexp", + "match": "\\\\(.|\\n)" + }, + "regexp-escape-sequence": { + "patterns": [ + { + "include": "#regexp-escape-special" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-backreference-number" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "regexp-charecter-set-escapes": { + "patterns": [ + { + "name": "constant.character.escape.regexp", + "match": "\\\\[abfnrtv\\\\]" + }, + { + "include": "#regexp-escape-special" + }, + { + "name": "constant.character.escape.regexp", + "match": "\\\\([0-7]{1,3})" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "codetags": { + "match": "(?:\\b(NOTE|XXX|HACK|FIXME|BUG|TODO)\\b)", + "captures": { + "1": { + "name": "keyword.codetag.notation.python" + } + } + }, + "regexp-expression": { + "patterns": [ + { + "include": "#regexp-base-expression" + }, + { + "include": "#regexp-character-set" + }, + { + "include": "#regexp-comments" + }, + { + "include": "#regexp-flags" + }, + { + "include": "#regexp-named-group" + }, + { + "include": "#regexp-backreference" + }, + { + "include": "#regexp-lookahead" + }, + { + "include": "#regexp-lookahead-negative" + }, + { + "include": "#regexp-lookbehind" + }, + { + "include": "#regexp-lookbehind-negative" + }, + { + "include": "#regexp-conditional" + }, + { + "include": "#regexp-parentheses-non-capturing" + }, + { + "include": "#regexp-parentheses" + } + ] + }, + "regexp-character-set": { + "patterns": [ + { + "match": "(?x)\n \\[ \\^? \\] (?! .*?\\])\n" + }, + { + "name": "meta.character.set.regexp", + "begin": "(\\[)(\\^)?(\\])?", + "end": "(\\])", + "beginCaptures": { + "1": { + "name": "punctuation.character.set.begin.regexp constant.other.set.regexp" + }, + "2": { + "name": "keyword.operator.negation.regexp" + }, + "3": { + "name": "constant.character.set.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.character.set.end.regexp constant.other.set.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-charecter-set-escapes" + }, + { + "name": "constant.character.set.regexp", + "match": "[^\\n]" + } + ] + } + ] + }, + "regexp-named-group": { + "name": "meta.named.regexp", + "begin": "(?x)\n (\\() (\\?P <\\w+(?:\\s+[[:alnum:]]+)?>)\n", + "end": "(\\))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookbehind-negative": { + "begin": "(\\()\\? = { + [BuiltinFunctions.__import__]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module\n\nImport a module. Because this function is meant for use by the Python\ninterpreter and not for general use, it is better to use\nimportlib.import_module() to programmatically import a module.\n\nThe globals argument is only used to determine the context;\nthey are not modified.  The locals argument is unused.  The fromlist\nshould be a list of names to emulate ``from name import ...'', or an\nempty list to emulate ``import name''.\nWhen importing a module from a package, note that __import__('A.B', ...)\nreturns package A when fromlist is empty, but its submodule B when\nfromlist is not empty.  The level argument is used to determine whether to\nperform absolute or relative imports: 0 is absolute, while a positive number\nis the number of parent directories to search relative to the current module.", + hover: [ + { + language: 'python', + value: + '__import__(name: Text, globals: Optional[Mapping[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., fromlist: Sequence[str]=..., level: int=...) -> Any', + }, + ], + }, + [BuiltinFunctions.abs]: { + completionKind: languages.CompletionItemKind.Function, + documentation: 'Return the absolute value of the argument.', + hover: [{ language: 'python', value: 'abs(n: SupportsAbs[_T], /) -> _T' }], + }, + [BuiltinFunctions.all]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return True if bool(x) is True for all values x in the iterable.\n\nIf the iterable is empty, return True.', + hover: [{ language: 'python', value: 'all(i: Iterable[object], /) -> bool' }], + }, + [BuiltinFunctions.any]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return True if bool(x) is True for any x in the iterable.\n\nIf the iterable is empty, return False.', + hover: [{ language: 'python', value: 'any(i: Iterable[object], /) -> bool' }], + }, + [BuiltinFunctions.ascii]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return an ASCII-only representation of an object.\n\nAs repr(), return a string containing a printable representation of an\nobject, but escape the non-ASCII characters in the string returned by\nrepr() using \\x, \\u or \\U escapes. This generates a string similar\nto that returned by repr() in Python 2.', + hover: [{ language: 'python', value: 'ascii(o: object, /) -> str' }], + }, + [BuiltinFunctions.bin]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the binary representation of an integer.\n\n>>> bin(2796202)\n'0b1010101010101010101010'", + hover: [ + { + language: 'python', + value: 'bin(number: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.bool]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'bool(x) -> bool\n\nReturns True when the argument x is true, False otherwise.\nThe builtins True and False are the only two instances of the class bool.\nThe class bool is a subclass of the class int, and cannot be subclassed.', + hover: [{ language: 'python', value: 'bool(o: object=...)' }], + }, + [BuiltinFunctions.bytearray]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'bytearray(iterable_of_ints) -> bytearray\nbytearray(string, encoding[, errors]) -> bytearray\nbytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer\nbytearray(int) -> bytes array of size given by the parameter initialized with null bytes\nbytearray() -> empty bytes array\n\nConstruct a mutable bytearray object from:\n  - an iterable yielding integers in range(256)\n  - a text string encoded using the specified encoding\n  - a bytes or a buffer object\n  - any object implementing the buffer API.\n  - an integer', + hover: [{ language: 'python', value: 'bytearray()' }], + }, + [BuiltinFunctions.bytes]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'bytes(iterable_of_ints) -> bytes\nbytes(string, encoding[, errors]) -> bytes\nbytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer\nbytes(int) -> bytes object of size given by the parameter initialized with null bytes\nbytes() -> empty bytes object\n\nConstruct an immutable array of bytes from:\n  - an iterable yielding integers in range(256)\n  - a text string encoded using the specified encoding\n  - any object implementing the buffer API.\n  - an integer', + hover: [{ language: 'python', value: 'bytes(ints: Iterable[int])' }], + }, + [BuiltinFunctions.callable]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return whether the object is callable (i.e., some kind of function).\n\nNote that classes are callable, as are instances of classes with a\n__call__() method.', + hover: [ + { + language: 'python', + value: 'callable(o: object, /) -> bool', + }, + ], + }, + [BuiltinFunctions.chr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff.', + hover: [ + { + language: 'python', + value: 'chr(code: int, /) -> str', + }, + ], + }, + [BuiltinFunctions.classmethod]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'classmethod(function) -> method\n\nConvert a function to be a class method.\n\nA class method receives the class as implicit first argument,\njust like an instance method receives the instance.\nTo declare a class method, use this idiom:\n\n  class C:\n      @classmethod\n      def f(cls, arg1, arg2, ...):\n          ...\n\nIt can be called either on the class (e.g. C.f()) or on an instance\n(e.g. C().f()).  The instance is ignored except for its class.\nIf a class method is called for a derived class, the derived class\nobject is passed as the implied first argument.\n\nClass methods are different than C++ or Java static methods.\nIf you want those, see the staticmethod builtin.', + hover: [ + { + language: 'python', + value: 'classmethod(f: Callable[..., Any])', + }, + ], + }, + [BuiltinFunctions.compile]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Compile source into a code object that can be executed by exec() or eval().\n\nThe source code may represent a Python module, statement or expression.\nThe filename will be used for run-time error messages.\nThe mode must be 'exec' to compile a module, 'single' to compile a\nsingle (interactive) statement, or 'eval' to compile an expression.\nThe flags argument, if present, controls which future statements influence\nthe compilation of the code.\nThe dont_inherit argument, if true, stops the compilation inheriting\nthe effects of any future statements in effect in the code calling\ncompile; if absent or false these statements do influence the compilation,\nin addition to any features explicitly specified.", + hover: [ + { + language: 'python', + value: + 'compile(source: Union[str, bytes, mod, AST], filename: Union[str, bytes], mode: str, flags: int=..., dont_inherit: int=..., optimize: int=...) -> Any', + }, + ], + }, + [BuiltinFunctions.complex]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'Create a complex number from a real part and an optional imaginary part.\n\nThis is equivalent to (real + imag*1j) where imag defaults to 0.', + hover: [ + { + language: 'python', + value: 'complex(real: float=..., imag: float=...)', + }, + ], + }, + [BuiltinFunctions.delattr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Deletes the named attribute from the given object.\n\ndelattr(x, 'y') is equivalent to ``del x.y''", + hover: [ + { + language: 'python', + value: 'delattr(o: Any, name: Text, /) -> None', + }, + ], + }, + [BuiltinFunctions.dict]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n    (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n    d = {}\n    for k, v in iterable:\n        d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n    in the keyword argument list.  For example:  dict(one=1, two=2)", + hover: [ + { + language: 'python', + value: 'dict(**kwargs: _VT)', + }, + ], + }, + [BuiltinFunctions.dir]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "dir([object]) -> list of strings\n\nIf called without an argument, return the names in the current scope.\nElse, return an alphabetized list of names comprising (some of) the attributes\nof the given object, and of attributes reachable from it.\nIf the object supplies a method named __dir__, it will be used; otherwise\nthe default dir() logic is used and returns:\n  for a module object: the module's attributes.\n  for a class object:  its attributes, and recursively the attributes\n    of its bases.\n  for any other object: its attributes, its class's attributes, and\n    recursively the attributes of its class's base classes.", + hover: [ + { + language: 'python', + value: 'dir(o: object=..., /) -> List[str]', + }, + ], + }, + [BuiltinFunctions.divmod]: { + completionKind: languages.CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: 'divmod(a: _N2, b: _N2, /) -> Tuple[_N2, _N2]', + }, + ], + documentation: 'Return the tuple (x//y, x%y).  Invariant: div*y + mod == x.', + }, + [BuiltinFunctions.enumerate]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return an enumerate object.\n\n  iterable\n    an object supporting iteration\n\nThe enumerate object yields pairs containing a count (from start, which\ndefaults to zero) and a value yielded by the iterable argument.\n\nenumerate is useful for obtaining an indexed list:\n    (0, seq[0]), (1, seq[1]), (2, seq[2]), ...', + hover: [ + { + language: 'python', + value: 'enumerate(iterable: Iterable[_T], start: int=...)', + }, + ], + }, + [BuiltinFunctions.eval]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Evaluate the given source in the context of globals and locals.\n\nThe source may be a string representing a Python expression\nor a code object as returned by compile().\nThe globals must be a dictionary and locals can be any mapping,\ndefaulting to the current globals and locals.\nIf only globals is given, locals defaults to it.', + hover: [ + { + language: 'python', + value: + 'eval(source: Union[Text, bytes, CodeType], globals: Optional[Dict[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.exec]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Execute the given source in the context of globals and locals.\n\nThe source may be a string representing one or more Python statements\nor a code object as returned by compile().\nThe globals must be a dictionary and locals can be any mapping,\ndefaulting to the current globals and locals.\nIf only globals is given, locals defaults to it.', + hover: [ + { + language: 'python', + value: + 'exec(object: Union[str, bytes, CodeType], globals: Optional[Dict[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.filter]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'filter(function or None, iterable) --> filter object\n\nReturn an iterator yielding those items of iterable for which function(item)\nis true. If function is None, return the items that are true.', + hover: [ + { + language: 'python', + value: + 'filter(function: None, iterable: Iterable[Optional[_T]], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.float]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'Convert a string or number to a floating point number, if possible.', + hover: [ + { + language: 'python', + value: + 'float(x: Union[SupportsFloat, _SupportsIndex, Text, bytes, bytearray]=...)', + }, + ], + }, + [BuiltinFunctions.format]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return value.__format__(format_spec)\n\nformat_spec defaults to the empty string.\nSee the Format Specification Mini-Language section of help('FORMATTING') for\ndetails.", + hover: [ + { + language: 'python', + value: 'format(o: object, format_spec: str=..., /) -> str', + }, + ], + }, + [BuiltinFunctions.frozenset]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'frozenset() -> empty frozenset object\nfrozenset(iterable) -> frozenset object\n\nBuild an immutable unordered collection of unique elements.', + hover: [ + { + language: 'python', + value: 'frozenset(iterable: Iterable[_T]=...)', + }, + ], + }, + + [BuiltinFunctions.getattr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "getattr(object, name[, default]) -> value\n\nGet a named attribute from an object; getattr(x, 'y') is equivalent to x.y.\nWhen a default argument is given, it is returned when the attribute doesn't\nexist; without it, an exception is raised in that case.", + hover: [ + { + language: 'python', + value: 'getattr(o: Any, /, name: Text, default: Any=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.globals]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the dictionary containing the current scope's global variables.\n\nNOTE: Updates to this dictionary *will* affect name lookups in the current\nglobal scope and vice-versa.", + hover: [ + { + language: 'python', + value: 'globals() -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.hasattr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return whether the object has an attribute with the given name.\n\nThis is done by calling getattr(obj, name) and catching AttributeError.', + hover: [ + { + language: 'python', + value: 'hasattr(o: Any, name: Text, /) -> bool', + }, + ], + }, + [BuiltinFunctions.hash]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return whether the object has an attribute with the given name.\n\nThis is done by calling getattr(obj, name) and catching AttributeError.', + hover: [ + { + language: 'python', + value: 'hasattr(o: Any, name: Text, /) -> bool', + }, + ], + }, + + [BuiltinFunctions.help]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Define the builtin 'help'.\n\nThis is a wrapper around pydoc.help that provides a helpful message\nwhen 'help' is typed at the Python interactive prompt.\n\nCalling help() at the Python prompt starts an interactive help session.\nCalling help(thing) prints help for the python object 'thing'.", + hover: [ + { + language: 'python', + value: 'help(*args: Any, **kwds: Any) -> None', + }, + ], + }, + [BuiltinFunctions.hex]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the hexadecimal representation of an integer.\n\n>>> hex(12648430)\n'0xc0ffee'", + hover: [ + { + language: 'python', + value: 'hex(i: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.id]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the identity of an object.\n\nThis is guaranteed to be unique among simultaneously existing objects.\n(CPython uses the object's memory address.)", + hover: [ + { + language: 'python', + value: 'id(o: object, /) -> int', + }, + ], + }, + [BuiltinFunctions.input]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Read a string from standard input.  The trailing newline is stripped.\n\nThe prompt string, if given, is printed to standard output without a\ntrailing newline before reading input.\n\nIf the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.\nOn *nix systems, readline is used if available.', + hover: [ + { + language: 'python', + value: 'input(prompt: Any=..., /) -> str', + }, + ], + }, + [BuiltinFunctions.int]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + "int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given.  If x is a number, return x.__int__().  For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base.  The literal can be preceded by '+' or '-' and be surrounded\nby whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4", + hover: [ + { + language: 'python', + value: 'int(x: Union[Text, bytes, SupportsInt, _SupportsIndex]=...)', + }, + ], + }, + [BuiltinFunctions.isinstance]: { + completionKind: languages.CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: + 'isinstance(o: object, t: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], /) -> bool', + }, + ], + documentation: + 'Return whether an object is an instance of a class or of a subclass thereof.\n\nA tuple, as in ``isinstance(x, (A, B, ...))``, may be given as the target to\ncheck against. This is equivalent to ``isinstance(x, A) or isinstance(x, B)\nor ...`` etc.', + }, + [BuiltinFunctions.issubclass]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return whether 'cls' is a derived from another class or is the same class.\n\nA tuple, as in ``issubclass(x, (A, B, ...))``, may be given as the target to\ncheck against. This is equivalent to ``issubclass(x, A) or issubclass(x, B)\nor ...`` etc.", + hover: [ + { + language: 'python', + value: + 'issubclass(cls: type, classinfo: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], /) -> bool', + }, + ], + }, + [BuiltinFunctions.iter]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'iter(iterable) -> iterator\niter(callable, sentinel) -> iterator\n\nGet an iterator from an object.  In the first form, the argument must\nsupply its own iterator, or be a sequence.\nIn the second form, the callable is called until it returns the sentinel.', + hover: [ + { + language: 'python', + value: 'iter(iterable: Iterable[_T], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.len]: { + completionKind: languages.CompletionItemKind.Function, + documentation: 'Return the number of items in a container.', + hover: [ + { + language: 'python', + value: 'len(o: Sized, /) -> int', + }, + ], + }, + [BuiltinFunctions.list]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.', + hover: [ + { + language: 'python', + value: 'list()', + }, + ], + }, + + [BuiltinFunctions.locals]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return a dictionary containing the current scope's local variables.\n\nNOTE: Whether or not updates to this dictionary will affect name lookups in\nthe local scope and vice-versa is *implementation dependent* and not\ncovered by any backwards compatibility guarantees.", + hover: [ + { + language: 'python', + value: 'locals() -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.map]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'map(func, *iterables) --> map object\n\nMake an iterator that computes the function using arguments from\neach of the iterables.  Stops when the shortest iterable is exhausted.', + hover: [ + { + language: 'python', + value: + 'map(func: Callable[[_T1], _S], iter1: Iterable[_T1], /) -> Iterator[_S]', + }, + ], + }, + [BuiltinFunctions.max]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'max(iterable, *[, default=obj, key=func]) -> value\nmax(arg1, arg2, *args, *[, key=func]) -> value\n\nWith a single iterable argument, return its biggest item. The\ndefault keyword-only argument specifies an object to return if\nthe provided iterable is empty.\nWith two or more arguments, return the largest argument.', + hover: [ + { + language: 'python', + value: + 'max(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], Any]=...) -> _T', + }, + ], + }, + [BuiltinFunctions.memoryview]: { + completionKind: languages.CompletionItemKind.Function, + documentation: 'Create a new memoryview object which references the given object.', + hover: [ + { + language: 'python', + value: 'memoryview(obj: Union[bytes, bytearray, memoryview])', + }, + ], + }, + [BuiltinFunctions.min]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'min(iterable, *[, default=obj, key=func]) -> value\nmin(arg1, arg2, *args, *[, key=func]) -> value\n\nWith a single iterable argument, return its smallest item. The\ndefault keyword-only argument specifies an object to return if\nthe provided iterable is empty.\nWith two or more arguments, return the smallest argument.', + hover: [ + { + language: 'python', + value: + 'min(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], Any]=...) -> _T', + }, + ], + }, + [BuiltinFunctions.next]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'next(iterator[, default])\n\nReturn the next item from the iterator. If default is given and the iterator\nis exhausted, it is returned instead of raising StopIteration.', + hover: [ + { + language: 'python', + value: 'next(i: Iterator[_T], /) -> _T', + }, + ], + }, + [BuiltinFunctions.object]: { + completionKind: languages.CompletionItemKind.Class, + documentation: 'The most base type', + hover: [ + { + language: 'python', + value: 'object()', + }, + ], + }, + [BuiltinFunctions.oct]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the octal representation of an integer.\n\n>>> oct(342391)\n'0o1234567'", + hover: [ + { + language: 'python', + value: 'oct(i: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.open]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Open file and return a stream.  Raise OSError upon failure.\n\nfile is either a text or byte string giving the name (and the path\nif the file isn't in the current working directory) of the file to\nbe opened or an integer file descriptor of the file to be\nwrapped. (If a file descriptor is given, it is closed when the\nreturned I/O object is closed, unless closefd is set to False.)\n\nmode is an optional string that specifies the mode in which the file\nis opened. It defaults to 'r' which means open for reading in text\nmode.  Other common values are 'w' for writing (truncating the file if\nit already exists), 'x' for creating and writing to a new file, and\n'a' for appending (which on some Unix systems, means that all writes\nappend to the end of the file regardless of the current seek position).\nIn text mode, if encoding is not specified the encoding used is platform\ndependent: locale.getpreferredencoding(False) is called to get the\ncurrent locale encoding. (For reading and writing raw bytes use binary\nmode and leave encoding unspecified.) The available modes are:\n\n========= ===============================================================\nCharacter Meaning\n--------- ---------------------------------------------------------------\n'r'       open for reading (default)\n'w'       open for writing, truncating the file first\n'x'       create a new file and open it for writing\n'a'       open for writing, appending to the end of the file if it exists\n'b'       binary mode\n't'       text mode (default)\n'+'       open a disk file for updating (reading and writing)\n'U'       universal newline mode (deprecated)\n========= ===============================================================\n\nThe default mode is 'rt' (open for reading text). For binary random\naccess, the mode 'w+b' opens and truncates the file to 0 bytes, while\n'r+b' opens the file without truncation. The 'x' mode implies 'w' and\nraises an `FileExistsError` if the file already exists.\n\nPython distinguishes between files opened in binary and text modes,\neven when the underlying operating system doesn't. Files opened in\nbinary mode (appending 'b' to the mode argument) return contents as\nbytes objects without any decoding. In text mode (the default, or when\n't' is appended to the mode argument), the contents of the file are\nreturned as strings, the bytes having been first decoded using a\nplatform-dependent encoding or using the specified encoding if given.\n\n'U' mode is deprecated and will raise an exception in future versions\nof Python.  It has no effect in Python 3.  Use newline to control\nuniversal newlines mode.\n\nbuffering is an optional integer used to set the buffering policy.\nPass 0 to switch buffering off (only allowed in binary mode), 1 to select\nline buffering (only usable in text mode), and an integer > 1 to indicate\nthe size of a fixed-size chunk buffer.  When no buffering argument is\ngiven, the default buffering policy works as follows:\n\n* Binary files are buffered in fixed-size chunks; the size of the buffer\n  is chosen using a heuristic trying to determine the underlying device's\n  \"block size\" and falling back on `io.DEFAULT_BUFFER_SIZE`.\n  On many systems, the buffer will typically be 4096 or 8192 bytes long.\n\n* \"Interactive\" text files (files for which isatty() returns True)\n  use line buffering.  Other text files use the policy described above\n  for binary files.\n\nencoding is the name of the encoding used to decode or encode the\nfile. This should only be used in text mode. The default encoding is\nplatform dependent, but any encoding supported by Python can be\npassed.  See the codecs module for the list of supported encodings.\n\nerrors is an optional string that specifies how encoding errors are to\nbe handled---this argument should not be used in binary mode. Pass\n'strict' to raise a ValueError exception if there is an encoding error\n(the default of None has the same effect), or pass 'ignore' to ignore\nerrors. (Note that ignoring encoding errors can lead to data loss.)\nSee the documentation for codecs.register or run 'help(codecs.Codec)'\nfor a list of the permitted encoding error strings.\n\nnewline controls how universal newlines works (it only applies to text\nmode). It can be None, '', '\n', '\r', and '\r\n'.  It works as\nfollows:\n\n* On input, if newline is None, universal newlines mode is\n  enabled. Lines in the input can end in '\n', '\r', or '\r\n', and\n  these are translated into '\n' before being returned to the\n  caller. If it is '', universal newline mode is enabled, but line\n  endings are returned to the caller untranslated. If it has any of\n  the other legal values, input lines are only terminated by the given\n  string, and the line ending is returned to the caller untranslated.\n\n* On output, if newline is None, any '\n' characters written are\n  translated to the system default line separator, os.linesep. If\n  newline is '' or '\n', no translation takes place. If newline is any\n  of the other legal values, any '\n' characters written are translated\n  to the given string.\n\nIf closefd is False, the underlying file descriptor will be kept open\nwhen the file is closed. This does not work when a file name is given\nand must be True in that case.\n\nA custom opener can be used by passing a callable as *opener*. The\nunderlying file descriptor for the file object is then obtained by\ncalling *opener* with (*file*, *flags*). *opener* must return an open\nfile descriptor (passing os.open as *opener* results in functionality\nsimilar to passing None).\n\nopen() returns a file object whose type depends on the mode, and\nthrough which the standard file operations such as reading and writing\nare performed. When open() is used to open a file in a text mode ('w',\n'r', 'wt', 'rt', etc.), it returns a TextIOWrapper. When used to open\na file in a binary mode, the returned class varies: in read binary\nmode, it returns a BufferedReader; in write binary and append binary\nmodes, it returns a BufferedWriter, and in read/write mode, it returns\na BufferedRandom.\n\nIt is also possible to use a string or bytearray as a file for both\nreading and writing. For strings StringIO can be used like a file\nopened in a text mode, and for bytes a BytesIO can be used like a file\nopened in a binary mode.", + hover: [ + { + language: 'python', + value: + 'open(file: Union[str, bytes, int], mode: str=..., buffering: int=..., encoding: Optional[str]=..., errors: Optional[str]=..., newline: Optional[str]=..., closefd: bool=..., opener: Optional[Callable[[str, int], int]]=...) -> IO[Any]', + }, + ], + }, + [BuiltinFunctions.ord]: { + completionKind: languages.CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: 'ord(c: Union[Text, bytes], /) -> int', + }, + ], + documentation: 'Return the Unicode code point for a one-character string.', + }, + [BuiltinFunctions.pow]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Equivalent to x**y (with two arguments) or x**y % z (with three arguments)\n\nSome types, such as ints, are able to use a more efficient algorithm when\ninvoked using the three argument form.', + hover: [ + { + language: 'python', + value: 'pow(x: int, y: int, /) -> Any', + }, + ], + }, + [BuiltinFunctions.print]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)\n\nPrints the values to a stream, or to sys.stdout by default.\nOptional keyword arguments:\nfile:  a file-like object (stream); defaults to the current sys.stdout.\nsep:   string inserted between values, default a space.\nend:   string appended after the last value, default a newline.\nflush: whether to forcibly flush the stream.", + hover: [ + { + language: 'python', + value: + 'print(*values: object, sep: Optional[Text]=..., end: Optional[Text]=..., file: Optional[_Writer]=..., flush: bool=...) -> None', + }, + ], + }, + [BuiltinFunctions.property]: { + completionKind: languages.CompletionItemKind.Class, + documentation: `Property attribute.\n\n  fget\n    function to be used for getting an attribute value\n  fset\n    function to be used for setting an attribute value\n  fdel\n    function to be used for del'ing an attribute\n  doc\n    docstring\n\nTypical use is to define a managed attribute x:\n\nclass C(object):\n    def getx(self): return self._x\n    def setx(self, value): self._x = value\n    def delx(self): del self._x\n    x = property(getx, setx, delx, "I'm the 'x' property.")\n\nDecorators make defining new properties or modifying existing ones easy:\n\nclass C(object):\n    @property\n    def x(self):\n        "I am the 'x' property."\n        return self._x\n    @x.setter\n    def x(self, value):\n        self._x = value\n    @x.deleter\n    def x(self):\n        del self._x`, + hover: [ + { + language: 'python', + value: + 'property(fget: Optional[Callable[[Any], Any]]=..., fset: Optional[Callable[[Any, Any], None]]=..., fdel: Optional[Callable[[Any], None]]=..., doc: Optional[str]=...)', + }, + ], + }, + [BuiltinFunctions.range]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'range(stop) -> range object\nrange(start, stop[, step]) -> range object\n\nReturn an object that produces a sequence of integers from start (inclusive)\nto stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.\nstart defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.\nThese are exactly the valid indices for a list of 4 elements.\nWhen step is given, it specifies the increment (or decrement).', + hover: [ + { + language: 'python', + value: 'range(stop: int)', + }, + ], + }, + [BuiltinFunctions.repr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return the canonical string representation of the object.\n\nFor many object types, including most builtins, eval(repr(obj)) == obj.', + hover: [ + { + language: 'python', + value: 'repr(o: object, /) -> str', + }, + ], + }, + [BuiltinFunctions.reversed]: { + completionKind: languages.CompletionItemKind.Function, + documentation: 'Return a reverse iterator over the values of the given sequence.', + hover: [ + { + language: 'python', + value: 'reversed(object: Sequence[_T], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.round]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Round a number to a given precision in decimal digits.\n\nThe return value is an integer if ndigits is omitted or None.  Otherwise\nthe return value has the same type as the number.  ndigits may be negative.', + hover: [ + { + language: 'python', + value: 'round(number: float) -> int', + }, + ], + }, + [BuiltinFunctions.set]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'set() -> new empty set object\nset(iterable) -> new set object\n\nBuild an unordered collection of unique elements.', + hover: [ + { + language: 'python', + value: 'set(iterable: Iterable[_T]=...)', + }, + ], + }, + [BuiltinFunctions.setattr]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Sets the named attribute on the given object to the specified value.\n\nsetattr(x, 'y', v) is equivalent to ``x.y = v''", + hover: [ + { + language: 'python', + value: 'setattr(object: Any, name: Text, value: Any, /) -> None', + }, + ], + }, + [BuiltinFunctions.slice]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'slice(stop)\nslice(start, stop[, step])\n\nCreate a slice object.  This is used for extended slicing (e.g. a[0:10:2]).', + hover: [ + { + language: 'python', + value: 'slice(stop: Any)', + }, + ], + }, + [BuiltinFunctions.sorted]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'Return a new list containing all items from the iterable in ascending order.\n\nA custom key function can be supplied to customize the sort order, and the\nreverse flag can be set to request the result in descending order.', + hover: [ + { + language: 'python', + value: + 'sorted(iterable: Iterable[_T], /, *, key: Optional[Callable[[_T], Any]]=..., reverse: bool=...) -> List[_T]', + }, + ], + }, + [BuiltinFunctions.staticmethod]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'staticmethod(function) -> method\n\nConvert a function to be a static method.\n\nA static method does not receive an implicit first argument.\nTo declare a static method, use this idiom:\n\n     class C:\n         @staticmethod\n         def f(arg1, arg2, ...):\n             ...\n\nIt can be called either on the class (e.g. C.f()) or on an instance\n(e.g. C().f()).  The instance is ignored except for its class.\n\nStatic methods in Python are similar to those found in Java or C++.\nFor a more advanced concept, see the classmethod builtin.', + hover: [ + { + language: 'python', + value: 'staticmethod(f: Callable[..., Any])', + }, + ], + }, + [BuiltinFunctions.str]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + "str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", + hover: [ + { + language: 'python', + value: 'str(o: object=...)', + }, + ], + }, + [BuiltinFunctions.sum]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + "Return the sum of a 'start' value (default: 0) plus an iterable of numbers\n\nWhen the iterable is empty, return the start value.\nThis function is intended specifically for use with numeric values and may\nreject non-numeric types.", + hover: [ + { + language: 'python', + value: 'sum(iterable: Iterable[_T], /) -> Union[_T, int]', + }, + ], + }, + [BuiltinFunctions.super]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + 'super() -> same as super(__class__, )\nsuper(type) -> unbound super object\nsuper(type, obj) -> bound super object; requires isinstance(obj, type)\nsuper(type, type2) -> bound super object; requires issubclass(type2, type)\nTypical use to call a cooperative superclass method:\nclass C(B):\n    def meth(self, arg):\n        super().meth(arg)\nThis works for class methods too:\nclass C(B):\n    @classmethod\n    def cmeth(cls, arg):\n        super().cmeth(arg)', + hover: [ + { + language: 'python', + value: 'super(t: Any, obj: Any)', + }, + ], + }, + [BuiltinFunctions.tuple]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + "Built-in immutable sequence.\n\nIf no argument is given, the constructor returns an empty tuple.\nIf iterable is specified the tuple is initialized from iterable's items.\n\nIf the argument is a tuple, the return value is the same object.", + hover: [ + { + language: 'python', + value: 'tuple(iterable: Iterable[_T_co]=...)', + }, + ], + }, + [BuiltinFunctions.type]: { + completionKind: languages.CompletionItemKind.Class, + documentation: + "type(object_or_name, bases, dict)\ntype(object) -> the object's type\ntype(name, bases, dict) -> a new type", + hover: [ + { + language: 'python', + value: 'type(o: object)', + }, + ], + }, + [BuiltinFunctions.vars]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'vars([object]) -> dictionary\n\nWithout arguments, equivalent to locals().\nWith an argument, equivalent to object.__dict__.', + hover: [ + { + language: 'python', + value: 'vars(object: Any=..., /) -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.zip]: { + completionKind: languages.CompletionItemKind.Function, + documentation: + 'zip(*iterables) --> zip object\n\nReturn a zip object whose .__next__() method returns a tuple where\nthe i-th element comes from the i-th iterable argument.  The .__next__()\nmethod continues until the shortest iterable in the argument sequence\nis exhausted and then it raises StopIteration.', + hover: [ + { + language: 'python', + value: 'zip(iter1: Iterable[_T1], /) -> Iterator[Tuple[_T1]]', + }, + ], + }, +}; diff --git a/packages/libro-cofine-editor/src/language/python/python-language-feature.ts b/packages/libro-cofine-editor/src/language/python/python-language-feature.ts new file mode 100644 index 00000000..2c095125 --- /dev/null +++ b/packages/libro-cofine-editor/src/language/python/python-language-feature.ts @@ -0,0 +1,163 @@ +import type { SnippetSuggestRegistry } from '@difizen/libro-cofine-editor-core'; +import { + InitializeContribution, + LanguageOptionsRegistry, + SnippetSuggestContribution, +} from '@difizen/libro-cofine-editor-core'; +import type { + GrammarDefinition, + TextmateRegistry, +} from '@difizen/libro-cofine-textmate'; +import { LanguageGrammarDefinitionContribution } from '@difizen/libro-cofine-textmate'; +import { inject, singleton } from '@difizen/mana-app'; +import { languages } from '@difizen/monaco-editor-core'; +import * as monaco from '@difizen/monaco-editor-core'; + +import platformGrammar from './data/MagicPython.tmLanguage.json'; +import cGrammar from './data/MagicRegExp.tmLanguage.json'; +import snippetsJson from './data/snippets/python.snippets.json'; + +export interface PythonLanguageOption { + lspHost: { + host: string; + path: string; + }; +} + +export function isPythonLanguageOption(data: object): data is PythonLanguageOption { + return data && typeof data === 'object' && 'lspHost' in data; +} + +let langRegisted = false; +let grammerRegisted = false; + +@singleton({ + contrib: [ + LanguageGrammarDefinitionContribution, + SnippetSuggestContribution, + InitializeContribution, + ], +}) +export class PythonContribution + implements + LanguageGrammarDefinitionContribution, + SnippetSuggestContribution, + InitializeContribution +{ + protected readonly optionsResgistry: LanguageOptionsRegistry; + + readonly id = 'python'; + readonly config: languages.LanguageConfiguration = { + comments: { + lineComment: '#', + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '[', close: ']' }, + { open: '{', close: '}' }, + { open: '(', close: ')' }, + { open: "'", close: "'", notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + folding: { + markers: { + start: new RegExp('^\\s*#pragma\\s+region\\b'), + end: new RegExp('^\\s*#pragma\\s+endregion\\b'), + }, + }, + onEnterRules: [ + { + beforeText: + /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\s*$/, + action: { indentAction: languages.IndentAction.Indent }, + }, + ], + }; + + constructor( + @inject(LanguageOptionsRegistry) optionsResgistry: LanguageOptionsRegistry, + ) { + this.optionsResgistry = optionsResgistry; + } + + onInitialize() { + monaco.languages.register({ + id: this.id, + extensions: [ + '.py', + '.rpy', + '.pyw', + '.cpy', + '.gyp', + '.gypi', + '.snakefile', + '.smk', + ], + aliases: ['Python', 'py'], + firstLine: '^#!\\s*/.*\\bpython[0-9.-]*\\b', + }); + monaco.languages.onLanguage(this.id, () => { + this.registerLanguageFeature(); + }); + } + + protected registerLanguageFeature() { + if (langRegisted) { + return; + } + langRegisted = true; + monaco.languages.setLanguageConfiguration(this.id, this.config); + } + + registerSnippetSuggest(registry: SnippetSuggestRegistry) { + registry.fromJSON(snippetsJson as any, { + language: [this.id], + source: 'Python Language', + }); + } + + registerTextmateLanguage(registry: TextmateRegistry): void { + if (grammerRegisted) { + return; + } + grammerRegisted = true; + + registry.registerTextmateGrammarScope('source.python', { + async getGrammarDefinition(): Promise { + return { + format: 'json', + content: platformGrammar, + }; + }, + }); + + registry.registerTextmateGrammarScope('source.regexp.python', { + async getGrammarDefinition(): Promise { + return { + format: 'json', + content: cGrammar, + }; + }, + }); + registry.mapLanguageIdToTextmateGrammar(this.id, 'source.python'); + } + + protected isDisposed = false; + get disposed() { + return this.isDisposed; + } + dispose() { + this.isDisposed = false; + } +} diff --git a/packages/libro-cofine-editor/src/libro-e2-editor.ts b/packages/libro-cofine-editor/src/libro-e2-editor.ts new file mode 100644 index 00000000..889bbc79 --- /dev/null +++ b/packages/libro-cofine-editor/src/libro-e2-editor.ts @@ -0,0 +1,900 @@ +import type { + CodeEditorFactory, + CompletionProvider, + ICoordinate, + IEditor, + IEditorConfig, + IEditorOptions, + IModel, + IPosition, + IRange, + SearchMatch, + TooltipProvider, +} from '@difizen/libro-code-editor'; +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 { NotebookCommands } from '@difizen/libro-core'; +import type { LSPProvider } from '@difizen/libro-lsp'; +import { + CommandRegistry, + inject, + ThemeService, + transient, + watch, +} from '@difizen/mana-app'; +import { Disposable, DisposableCollection, Emitter } from '@difizen/mana-app'; +import { editor, Selection } from '@difizen/monaco-editor-core'; +import { v4 } from 'uuid'; + +import './index.less'; +import { LanguageSpecRegistry } from './language-specs.js'; +import { PlaceholderContentWidget } from './placeholder.js'; +import type { MonacoEditorOptions, MonacoEditorType, MonacoMatch } from './types.js'; +import { MonacoRange, MonacoUri } from './types.js'; + +export interface LibroE2EditorConfig extends IEditorConfig { + /** + * The mode to use. + */ + mode?: string; + + /** + * content mimetype + */ + mimetype?: string; + + // FIXME-TRANS: Handle theme localizable names + // themeDisplayName?: string + + /** + * Whether to use the context-sensitive indentation that the mode provides + * (or just indent the same as the line before). + */ + smartIndent?: boolean; + + /** + * Configures whether the editor should re-indent the current line when a + * character is typed that might change its proper indentation + * (only works if the mode supports indentation). + */ + electricChars?: boolean; + + /** + * Configures the keymap to use. The default is "default", which is the + * only keymap defined in codemirror.js itself. + * Extra keymaps are found in the CodeMirror keymap directory. + */ + keyMap?: string; + + /** + * Can be used to specify extra keybindings for the editor, alongside the + * ones defined by keyMap. Should be either null, or a valid keymap value. + */ + // extraKeys?: KeyBinding[] | null; + + /** + * Can be used to add extra gutters (beyond or instead of the line number + * gutter). + * Should be an array of CSS class names, each of which defines a width + * (and optionally a background), + * and which will be used to draw the background of the gutters. + * May include the CodeMirror-linenumbers class, in order to explicitly + * set the position of the line number gutter + * (it will default to be to the right of all other gutters). + * These class names are the keys passed to setGutterMarker. + */ + gutters?: string[]; + + /** + * Determines whether the gutter scrolls along with the content + * horizontally (false) + * or whether it stays fixed during horizontal scrolling (true, + * the default). + */ + fixedGutter?: boolean; + + /** + * Whether the cursor should be drawn when a selection is active. + */ + showCursorWhenSelecting?: boolean; + + /** + * When fixedGutter is on, and there is a horizontal scrollbar, by default + * the gutter will be visible to the left of this scrollbar. If this + * option is set to true, it will be covered by an element with class + * CodeMirror-gutter-filler. + */ + coverGutterNextToScrollbar?: boolean; + + /** + * Controls whether drag-and-drop is enabled. + */ + dragDrop?: boolean; + + /** + * Explicitly set the line separator for the editor. + * By default (value null), the document will be split on CRLFs as well as + * lone CRs and LFs, and a single LF will be used as line separator in all + * output (such as getValue). When a specific string is given, lines will + * only be split on that string, and output will, by default, use that + * same separator. + */ + lineSeparator?: string | null; + + /** + * Chooses a scrollbar implementation. The default is "native", showing + * native scrollbars. The core library also provides the "null" style, + * which completely hides the scrollbars. Addons can implement additional + * scrollbar models. + */ + scrollbarStyle?: string; + + /** + * When enabled, which is the default, doing copy or cut when there is no + * selection will copy or cut the whole lines that have cursors on them. + */ + lineWiseCopyCut?: boolean; + + /** + * Whether to scroll past the end of the buffer. + */ + scrollPastEnd?: boolean; + + /** + * Whether to give the wrapper of the line that contains the cursor the class + * cm-activeLine. + */ + styleActiveLine?: boolean; + + /** + * Whether to causes the selected text to be marked with the CSS class + * CodeMirror-selectedtext. Useful to change the colour of the selection + * (in addition to the background). + */ + styleSelectedText?: boolean; + + /** + * Defines the mouse cursor appearance when hovering over the selection. + * It can be set to a string, like "pointer", or to true, + * in which case the "default" (arrow) cursor will be used. + */ + selectionPointer?: boolean | string; + + // + highlightActiveLineGutter?: boolean; + highlightSpecialChars?: boolean; + history?: boolean; + drawSelection?: boolean; + dropCursor?: boolean; + allowMultipleSelections?: boolean; + autocompletion?: boolean; + rectangularSelection?: boolean; + crosshairCursor?: boolean; + highlightSelectionMatches?: boolean; + foldGutter?: boolean; + syntaxHighlighting?: boolean; + /** + * 是否从kernel获取completion + */ + jupyterKernelCompletion?: boolean; + /** + * 是否从kernel获取tooltip + */ + jupyterKernelTooltip?: boolean; + indentationMarkers?: boolean; + hyperLink?: boolean; + /** + * 是否开启tab触发completion和tooltip + */ + tabEditorFunction?: boolean; + + lspCompletion?: boolean; + + lspTooltip?: boolean; + + lspLint?: boolean; + + placeholder?: HTMLElement | string; +} + +export const LibroE2EditorOptions = Symbol('LibroE2EditorOptions'); + +export interface LibroE2EditorOptions extends IEditorOptions { + lspProvider?: LSPProvider; + + /** + * The configuration options for the editor. + */ + config?: Partial; +} + +export const libroE2DefaultConfig: Required = { + ...defaultConfig, + theme: { + dark: 'libro-dark', + light: 'libro-light', + hc: 'e2-hc', + }, + mode: 'null', + mimetype: 'text/x-python', + smartIndent: true, + electricChars: true, + keyMap: 'default', + // extraKeys: null, + gutters: [], + fixedGutter: true, + showCursorWhenSelecting: false, + coverGutterNextToScrollbar: false, + dragDrop: true, + lineSeparator: null, + scrollbarStyle: 'native', + lineWiseCopyCut: true, + scrollPastEnd: false, + styleActiveLine: false, + styleSelectedText: true, + selectionPointer: false, + handlePaste: true, + lineWrap: 'off', + lspEnabled: true, + + // + highlightActiveLineGutter: false, + highlightSpecialChars: true, + history: true, + drawSelection: true, + dropCursor: true, + allowMultipleSelections: true, + autocompletion: true, + rectangularSelection: true, + crosshairCursor: true, + highlightSelectionMatches: true, + foldGutter: true, + syntaxHighlighting: true, + jupyterKernelCompletion: true, + indentationMarkers: true, + hyperLink: true, + jupyterKernelTooltip: true, + tabEditorFunction: true, + lspCompletion: true, + lspTooltip: true, + lspLint: true, + placeholder: '', +}; + +export const LibroE2EditorFactory = Symbol('LibroE2EditorFactory'); +export type LibroE2EditorFactory = CodeEditorFactory; + +export const E2EditorClassname = 'libro-e2-editor'; + +export const LibroE2URIScheme = 'libro-e2'; + +@transient() +export class LibroE2Editor implements IEditor { + protected readonly themeService: ThemeService; + protected readonly languageSpecRegistry: LanguageSpecRegistry; + protected readonly commandRegistry: CommandRegistry; + + protected defaultLineHeight = 20; + + protected toDispose = new DisposableCollection(); + + /** + * The DOM node that hosts the editor. + */ + readonly host: HTMLElement; + /** + * The DOM node that hosts the monaco editor. + */ + readonly editorHost: HTMLElement; + + protected placeholder: PlaceholderContentWidget; + + protected oldDeltaDecorations: string[] = []; + + protected _config: LibroE2EditorConfig; + + protected _uuid = ''; + /** + * The uuid of this editor; + */ + get uuid(): string { + return this._uuid; + } + set uuid(value: string) { + this._uuid = value; + } + + protected _model: IModel; + /** + * Returns a model for this editor. + */ + get model(): IModel { + return this._model; + } + + protected _editor?: E2Editor; + get editor(): E2Editor | undefined { + return this?._editor; + } + + get monacoEditor(): MonacoEditorType | undefined { + return this?._editor?.codeEditor; + } + + get lineCount(): number { + return this.monacoEditor?.getModel()?.getLineCount() || 0; + } + + lspProvider?: LSPProvider; + + completionProvider?: CompletionProvider; + + tooltipProvider?: TooltipProvider; + + protected isLayouting = false; + constructor( + @inject(LibroE2EditorOptions) options: LibroE2EditorOptions, + @inject(LanguageSpecRegistry) languageSpecRegistry: LanguageSpecRegistry, + @inject(ThemeService) themeService: ThemeService, + @inject(CommandRegistry) commandRegistry: CommandRegistry, + ) { + this.languageSpecRegistry = languageSpecRegistry; + this.themeService = themeService; + this.commandRegistry = commandRegistry; + this.host = options.host; + this.host.classList.add('libro-e2-editor-container'); + this._uuid = options.uuid || v4(); + + this._model = options.model; + + const config = options.config || {}; + const fullConfig = (this._config = { + ...libroE2DefaultConfig, + ...config, + mimetype: options.model.mimeType, + }); + + this.completionProvider = options.completionProvider; + this.tooltipProvider = options.tooltipProvider; + this.lspProvider = options.lspProvider; + + this.editorHost = document.createElement('div'); + this.host.append(this.editorHost); + + this.createEditor(this.editorHost, fullConfig); + + this.onMimeTypeChanged(); + this.onCursorActivity(); + + this.toDispose.push(watch(this._model, 'mimeType', this.onMimeTypeChanged)); + // this.toDispose.push(watch(this._model, 'source', this.onValueChange)); + this.toDispose.push(watch(this._model, 'selections', this.onSelectionChange)); + this.toDispose.push(this.themeService.onDidColorThemeChange(this.onThemeChange)); + } + + get languageSpec() { + return this.languageSpecRegistry.languageSpecs.find( + (item) => item.mime === this.model.mimeType, + ); + } + + get theme(): string { + const themetype = this.themeService.getActiveTheme().type; + return this._config.theme[themetype]; + } + + protected toMonacoOptions( + editorConfig: Partial, + ): MonacoEditorOptions { + return { + minimap: { + enabled: false, + }, + lineHeight: editorConfig.lineHeight ?? this.defaultLineHeight, + fontSize: editorConfig.fontSize ?? 13, + lineNumbers: editorConfig.lineNumbers ? 'on' : 'off', + folding: editorConfig.codeFolding, + wordWrap: editorConfig.lineWrap, + lineDecorationsWidth: 15, + lineNumbersMinChars: 3, + suggestSelection: 'first', + wordBasedSuggestions: false, + scrollBeyondLastLine: false, + /** + * 使用该选项可以让modal widget出现在正确的范围,而不是被遮挡,解决z-index问题,但是会导致hover组件之类的无法被选中 + * 根据 https://github.com/microsoft/monaco-editor/issues/2156,0.34.x 版本修复了这个问题 + * TODO: 当前0.31.1 无法开启此选项,升级 E2 3.x 可以解决(monaco 0.39) + * + * ```html + *
+ * ``` + * + */ + // overflowWidgetsDomNode: document.getElementById('monaco-editor-overflow-widgets-root')!, + // fixedOverflowWidgets: true, + suggest: { snippetsPreventQuickSuggestions: false }, + autoClosingQuotes: editorConfig.autoClosingBrackets ? 'always' : 'never', + autoDetectHighContrast: false, + scrollbar: { + alwaysConsumeMouseWheel: false, + verticalScrollbarSize: 0, + }, + extraEditorClassName: E2EditorClassname, + renderLineHighlight: 'all', + renderLineHighlightOnlyWhenFocus: true, + readOnly: editorConfig.readOnly, + cursorWidth: 1, + tabSize: editorConfig.tabSize, + insertSpaces: editorConfig.insertSpaces, + matchBrackets: editorConfig.matchBrackets ? 'always' : 'never', + rulers: editorConfig.rulers, + wordWrapColumn: editorConfig.wordWrapColumn, + 'semanticHighlighting.enabled': true, + }; + } + + protected async createEditor(host: HTMLElement, config: LibroE2EditorConfig) { + if (!this.languageSpec) { + return; + } + const editorConfig: LibroE2EditorConfig = { + ...config, + ...this.languageSpec.editorConfig, + }; + this._config = editorConfig; + // await this.languageSpec.loadModule() + await MonacoEnvironment.init(); + await this.languageSpec?.beforeEditorInit?.(); + const editorPorvider = + MonacoEnvironment.container.get(EditorProvider); + + const uri = MonacoUri.from({ + scheme: LibroE2URIScheme, + path: `${this.uuid}${this.languageSpec.ext[0]}`, + }); + + const options: MonacoEditorOptions = { + ...this.toMonacoOptions(editorConfig), + /** + * language ia an uri: + */ + language: this.languageSpec.language, + uri, + theme: this.theme, + value: this.model.value, + }; + + const e2Editor = editorPorvider.create(host, options); + this._editor = e2Editor; + this.toDispose.push( + this.monacoEditor?.onDidChangeModelContent(() => { + const value = this.monacoEditor?.getValue(); + this.model.value = value ?? ''; + this.updateEditorSize(); + }) ?? Disposable.NONE, + ); + this.toDispose.push( + this.monacoEditor?.onDidContentSizeChange(() => { + this.updateEditorSize(); + }) ?? Disposable.NONE, + ); + this.toDispose.push( + this.monacoEditor?.onDidBlurEditorText(() => { + this.blur(); + }) ?? Disposable.NONE, + ); + + this.updateEditorSize(); + this.inspectResize(); + this.handleCommand(this.commandRegistry); + await this.languageSpec?.afterEditorInit?.(this); + this.placeholder = new PlaceholderContentWidget( + config.placeholder, + this.monacoEditor!, + ); + + // console.log( + // 'editor._themeService.getColorTheme()', + // this.monacoEditor._themeService, + // this.monacoEditor._themeService.getColorTheme(), + // ); + + // setTimeout(() => { + // this.monacoEditor?.trigger('editor', 'editor.action.formatDocument'); + // console.log('trigger format'); + // }, 5000); + } + + protected inspectResize() { + // TODO + } + + protected getEditorNode() { + return Array.from( + this.host.getElementsByClassName(E2EditorClassname), + )[0] as HTMLDivElement; + } + + protected updateEditorSize() { + const contentHeight = this.monacoEditor?.getContentHeight() ?? 20; + this.host.style.height = `${contentHeight + 30}px`; + try { + this.isLayouting = true; + this.monacoEditor?.layout({ + width: this.host.offsetWidth, + height: contentHeight, + }); + } finally { + this.isLayouting = false; + } + } + + /** + * 解决e2与libro快捷键冲突 + * @param commandRegistry + */ + 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, + // }, + this.monacoEditor?.addCommand( + 9, + () => { + commandRegistry.executeCommand(NotebookCommands['EnterCommandMode'].id); + }, + '!editorHasSelection && !editorHasSelection && !editorHasMultipleSelections', + ); + this.monacoEditor?.addCommand( + 2048 | 3, + () => { + commandRegistry.executeCommand(NotebookCommands['RunCell'].id); + }, + '!findWidgetVisible && !referenceSearchVisible', + ); + this.monacoEditor?.addCommand( + 1024 | 3, + () => { + commandRegistry.executeCommand(NotebookCommands['RunCellAndSelectNext'].id); + }, + '!findInputFocussed', + ); + this.monacoEditor?.addCommand( + 512 | 3, + () => { + commandRegistry.executeCommand(NotebookCommands['RunCellAndInsertBelow'].id); + }, + '!findWidgetVisible', + ); + + this.monacoEditor?.addCommand( + 2048 | 1024 | 83, + () => { + commandRegistry.executeCommand(NotebookCommands['SplitCellAntCursor'].id); + }, + // '!findWidgetVisible', + ); + + this.monacoEditor?.addCommand( + 2048 | 36, + () => { + commandRegistry.executeCommand('libro-search:toggle'); + }, + // '!findWidgetVisible', + ); + } + + protected onValueChange() { + // this.editor?.codeEditor.setValue(this.model.value); + } + + protected onSelectionChange() { + this.setSelections(this.model.selections); + } + + protected onThemeChange = () => { + this.monacoEditor?.updateOptions({ theme: this.theme }); + }; + + /** + * Handles a mime type change. + * 切换语言 + * cell 切换没走这里 + */ + protected onMimeTypeChanged(): void { + const model = this.monacoEditor?.getModel(); + if (this.languageSpec && model) { + editor.setModelLanguage(model, this.languageSpec.language); + } + } + + /** + * Handles a cursor activity event. + */ + protected onCursorActivity(): void { + // Only add selections if the editor has focus. This avoids unwanted + // triggering of cursor activity due to collaborator actions. + if (this.hasFocus()) { + // const selections = this.getSelections(); + // this.model.selections = selections; + } + } + + getOption(option: K) { + return this._config[option]; + } + + /** + * + * @param option + * @param value + */ + setOption = ( + option: K, + value: LibroE2EditorConfig[K], + ) => { + if (value === null || value === undefined) { + return; + } + + if (option === 'theme') { + this._config.theme = value as NonNullable; + this.monacoEditor?.updateOptions({ + theme: this.theme, + }); + } + + if (option === 'placeholder') { + this._config.placeholder = value as NonNullable< + LibroE2EditorConfig['placeholder'] + >; + this.placeholder.update(value as NonNullable); + } + + if (option === 'lspEnabled') { + this._config.lspEnabled = value as NonNullable; + } + + const sizeKeys = [ + 'fontFamily', + 'fontSize', + 'lineHeight', + 'wordWrapColumn', + 'lineWrap', + ]; + const monacoOptionkeys = sizeKeys.concat(['readOnly', 'insertSpaces', 'tabSize']); + + if (monacoOptionkeys.includes(option)) { + this._config = { ...this._config, [option]: value }; + + this.monacoEditor?.updateOptions(this.toMonacoOptions(this._config)); + } + + if (sizeKeys.includes(option)) { + this.updateEditorSize(); + } + }; + + getLine = (line: number) => { + return this.monacoEditor?.getModel()?.getLineContent(line); + }; + getOffsetAt = (position: IPosition) => { + return ( + this.monacoEditor + ?.getModel() + ?.getOffsetAt({ lineNumber: position.line, column: position.column }) || 0 + ); + }; + undo = () => { + this.monacoEditor?.trigger('source', 'undo', {}); + }; + + redo = () => { + this.monacoEditor?.trigger('source', 'redo', {}); + }; + focus = () => { + this.monacoEditor?.focus(); + }; + hasFocus = () => { + return this.monacoEditor?.hasWidgetFocus() ?? false; + }; + blur = () => { + document.documentElement.focus(); + }; + resizeToFit = () => { + this.monacoEditor?.layout(); + }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getPositionForCoordinate = (coordinate: ICoordinate) => { + return null; + }; + + protected modalChangeEmitter = new Emitter(); + get onModalChange() { + return this.modalChangeEmitter.event; + } + + protected toMonacoRange(range: IRange) { + const selection = range ?? this.getSelection(); + const monacoSelection = { + startLineNumber: selection.start.line || 1, + startColumn: selection.start.column || 1, + endLineNumber: selection.end.line || 1, + endColumn: selection.end.column || 1, + }; + return monacoSelection; + } + + getSelectionValue = (range?: IRange | undefined) => { + const selection = range ?? this.getSelection(); + return this.monacoEditor + ?.getModel() + ?.getValueInRange(this.toMonacoRange(selection)); + }; + + getPositionAt = (offset: number): IPosition | undefined => { + const position = this.monacoEditor?.getModel()?.getPositionAt(offset); + return position ? { line: position.lineNumber, column: position.column } : position; + }; + + protected toMonacoMatch(match: SearchMatch): MonacoMatch { + const start = this.getPositionAt(match.position); + const end = this.getPositionAt(match.position + match.text.length); + return { + range: new MonacoRange( + start?.line ?? 1, + start?.column ?? 1, + end?.line ?? 1, + end?.column ?? 1, + ), + matches: [match.text], + _findMatchBrand: undefined, + }; + } + + replaceSelection = (text: string, range: IRange) => { + this.monacoEditor?.executeEdits(undefined, [ + { + range: this.toMonacoRange(range), + text, + }, + ]); + this.monacoEditor?.pushUndoStop(); + }; + + replaceSelections = (edits: { text: string; range: IRange }[]) => { + this.monacoEditor?.executeEdits( + undefined, + edits.map((item) => ({ range: this.toMonacoRange(item.range), text: item.text })), + ); + this.monacoEditor?.pushUndoStop(); + }; + + getCursorPosition = () => { + const position: IPosition = { + line: this.monacoEditor?.getPosition()?.lineNumber || 1, + column: this.monacoEditor?.getPosition()?.column || 1, + }; + + return position; + }; + setCursorPosition = (position: IPosition) => { + this.monacoEditor?.setPosition({ + column: position.column, + lineNumber: position.line, + }); + }; + getSelection = () => { + const selection = { + start: { + line: this.monacoEditor?.getSelection()?.startLineNumber || 1, + column: this.monacoEditor?.getSelection()?.startColumn || 1, + } as IPosition, + end: { + line: this.monacoEditor?.getSelection()?.endLineNumber || 1, + column: this.monacoEditor?.getSelection()?.endColumn || 1, + } as IPosition, + }; + return selection; + }; + setSelection = (selection: IRange) => { + this.monacoEditor?.setSelection(this.toMonacoRange(selection)); + }; + getSelections = () => { + const selections: IRange[] = []; + this.monacoEditor?.getSelections()?.map((selection: any) => + selections.push({ + start: { + line: selection.startLineNumber || 1, + column: selection.startColumn || 1, + } as IPosition, + end: { + line: selection.endLineNumber || 1, + column: selection.endColumn || 1, + } as IPosition, + }), + ); + return selections; + }; + setSelections = (selections: IRange[]) => { + this.monacoEditor?.setSelections( + selections.map( + (item) => + new Selection( + item.start.line, + item.start.column, + item.end.line, + item.end.column, + ), + ), + ); + }; + + revealSelection = (selection: IRange) => { + this.monacoEditor?.revealRange(this.toMonacoRange(selection)); + }; + highlightMatches = (matches: SearchMatch[], currentIndex: number | undefined) => { + let currentMatch: SearchMatch | undefined; + const _matches = matches + .map((item, index) => { + if (index === currentIndex) { + currentMatch = item; + return { + range: item, + options: { + className: `currentFindMatch`, // 当前高亮 + }, + }; + } + return { + range: item, + options: { + className: `findMatch`, // 匹配高亮 + }, + }; + }) + .map((item) => ({ + ...item, + range: this.toMonacoMatch(item.range).range, + })); + this.oldDeltaDecorations = + this.monacoEditor?.deltaDecorations(this.oldDeltaDecorations, _matches) || []; + if (currentMatch) { + const start = this.getPositionAt(currentMatch.position); + const end = this.getPositionAt(currentMatch.position + currentMatch.text.length); + if (start && end) { + this.revealSelection({ end, start }); + } + } + }; + + protected _isDisposed = false; + /** + * Tests whether the editor is disposed. + */ + get disposed(): boolean { + return this._isDisposed; + } + dispose() { + if (this.disposed) { + return; + } + this.placeholder.dispose(); + this.toDispose.dispose(); + this._isDisposed = true; + } +} diff --git a/packages/libro-cofine-editor/src/libro-e2-preload.ts b/packages/libro-cofine-editor/src/libro-e2-preload.ts new file mode 100644 index 00000000..96e050b8 --- /dev/null +++ b/packages/libro-cofine-editor/src/libro-e2-preload.ts @@ -0,0 +1,25 @@ +import { MonacoEnvironment } from '@difizen/libro-cofine-editor-core'; +import type { Syringe } from '@difizen/mana-app'; +import { Deferred } from '@difizen/mana-app'; + +import { LSPFeatureModule } from './language/lsp/module.js'; +import { PythonLanguageFeature } from './language/python/module.js'; +import { LibroE2ThemeModule } from './theme/module.js'; + +export const E2LoadedDeferred = new Deferred(); + +export const loadE2 = async (libroContainer: Syringe.Container) => { + // libro and e2 share same container! + MonacoEnvironment.setContainer(libroContainer); + await MonacoEnvironment.loadModule(async (container) => { + const textmate = await import('@difizen/libro-cofine-textmate'); + container.load(textmate.TextmateModule); + // const module = await import('@difizen/libro-cofine-language-python'); + // container.load(module.default); + container.load(PythonLanguageFeature); + container.load(LibroE2ThemeModule); + container.load(LSPFeatureModule); + }); + await MonacoEnvironment.init(); + E2LoadedDeferred.resolve(); +}; diff --git a/packages/libro-cofine-editor/src/libro-sql-dataphin-api.ts b/packages/libro-cofine-editor/src/libro-sql-dataphin-api.ts new file mode 100644 index 00000000..921caf8b --- /dev/null +++ b/packages/libro-cofine-editor/src/libro-sql-dataphin-api.ts @@ -0,0 +1,20 @@ +import { singleton } from '@difizen/mana-app'; + +@singleton() +export class LibroDataphinRequestAPI { + queryTables = () => { + // + }; + queryFunctions = () => { + // + }; + queryDefinitionUri = () => { + // + }; + queryHover = () => { + // + }; + queryTableDetail = () => { + // + }; +} diff --git a/packages/libro-cofine-editor/src/module.ts b/packages/libro-cofine-editor/src/module.ts new file mode 100644 index 00000000..e570d042 --- /dev/null +++ b/packages/libro-cofine-editor/src/module.ts @@ -0,0 +1,44 @@ +import type { IEditorOptions } from '@difizen/libro-code-editor'; +import { CodeEditorModule } from '@difizen/libro-code-editor'; +import { ManaModule } from '@difizen/mana-app'; + +import { LibroE2EditorContribution } from './editor-contribution.js'; +import { + LanguageSpecContribution, + LanguageSpecRegistry, + LibroLanguageSpecs, +} from './language-specs.js'; +import { + LibroE2Editor, + LibroE2EditorFactory, + LibroE2EditorOptions, +} from './libro-e2-editor.js'; +import { loadE2 } from './libro-e2-preload.js'; +import { LibroDataphinRequestAPI } from './libro-sql-dataphin-api.js'; + +export const LibroE2EditorModule = ManaModule.create() + .register( + LibroE2EditorContribution, + LibroE2Editor, + LanguageSpecRegistry, + LibroLanguageSpecs, + LibroDataphinRequestAPI, + { + token: LibroE2EditorFactory, + useFactory: (ctx) => { + return (options: IEditorOptions) => { + const child = ctx.container.createChild(); + child.register({ token: LibroE2EditorOptions, useValue: options }); + return child.get(LibroE2Editor); + }; + }, + }, + ) + .contribution(LanguageSpecContribution) + .dependOn(CodeEditorModule) + .preload(async (ctx) => { + await loadE2(ctx.container); + if ('function' === typeof (window as any).define && (window as any).define.amd) { + delete (window as any).define.amd; + } + }); diff --git a/packages/libro-cofine-editor/src/placeholder.ts b/packages/libro-cofine-editor/src/placeholder.ts new file mode 100644 index 00000000..520590b4 --- /dev/null +++ b/packages/libro-cofine-editor/src/placeholder.ts @@ -0,0 +1,100 @@ +import type { Disposable } from '@difizen/mana-app'; +import { DisposableCollection } from '@difizen/mana-app'; +import { editor } from '@difizen/monaco-editor-core'; + +import type { MonacoEditorType } from './types.js'; + +export type PlaceHolderContent = string | HTMLElement | undefined; + +/** + * Represents an placeholder renderer for monaco editor + * Roughly based on https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts + */ +export class PlaceholderContentWidget implements Disposable { + static ID = 'editor.widget.placeholderHint'; + + protected placeholder: PlaceHolderContent; + + protected editor: MonacoEditorType; + + protected domNode: HTMLDivElement; + + protected toDispose: DisposableCollection = new DisposableCollection(); + + constructor(placeholder: PlaceHolderContent, monacoEditor: MonacoEditorType) { + this.placeholder = placeholder; + this.editor = monacoEditor; + // register a listener for editor code changes + this.editor.onDidChangeModelContent(() => this.onDidChangeModelContent()); + // ensure that on initial load the placeholder is shown + this.onDidChangeModelContent(); + } + + onDidChangeModelContent() { + if (this.editor.getValue() === '') { + this.editor.addContentWidget(this); + } else { + this.editor.removeContentWidget(this); + } + } + + update(placeholder: PlaceHolderContent) { + if (this.disposed) { + return; + } + this.placeholder = placeholder; + this.onDidChangeModelContent(); + } + + getId() { + return PlaceholderContentWidget.ID; + } + + getDomNode() { + if (!this.domNode) { + this.domNode = document.createElement('div'); + this.domNode.style.width = 'max-content'; + this.domNode.style.pointerEvents = 'none'; + this.domNode.style.opacity = '60%'; + this.domNode.addEventListener('click', () => { + this.editor.focus(); + }); + + const content = + typeof this.placeholder === 'string' + ? document.createTextNode(this.placeholder) + : this.placeholder; + + if (content) { + this.domNode.appendChild(content); + } + this.domNode.style.fontStyle = 'italic'; + if (typeof content === 'string') { + this.domNode.setAttribute('aria-label', 'placeholder ' + content); + } else { + this.domNode.setAttribute('aria-hidden', 'true'); + } + this.editor.applyFontInfo(this.domNode); + } + + return this.domNode; + } + + getPosition() { + return { + position: { lineNumber: 1, column: 1 }, + preference: [editor.ContentWidgetPositionPreference.EXACT], + }; + } + + disposed = false; + + dispose() { + if (this.disposed) { + return; + } + this.toDispose.dispose(); + this.editor.removeContentWidget(this); + this.disposed = true; + } +} diff --git a/packages/libro-cofine-editor/src/theme/data/jupyter_dark.json b/packages/libro-cofine-editor/src/theme/data/jupyter_dark.json new file mode 100644 index 00000000..0352c081 --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/jupyter_dark.json @@ -0,0 +1,406 @@ +{ + "name": "Jupyter Theme Dark", + "type": "dark", + "includes": "dark-plus", + "colors": { + "menu.background": "#1e202b", + "menubar.selectionBackground": "#1e202b", + + "sideBar.background": "#1e202b", + "sideBarSectionHeader.background": "#252631", + "sideBar.border": "#18181d", + + "editor.background": "#1e202b", + "editor.foreground": "#dbd9d9", + + "notebook.cellEditorBackground": "#23232e", + + "statusBar.background": "#0d5641", + "statusBar.noFolderBackground": "#309825", + "statusBar.debuggingBackground": "#c24d27", + "statusBarItem.remoteBackground": "#063e3f", + + "editor.lineHighlightBorder": "#292b3b", + "editor.selectionBackground": "#4a4e72", + + "activityBar.background": "#21222d", + "widget.shadow": "#21222d" + }, + "tokenColors": [ + { + "name": "Comments", + "scope": ["comment", "punctuation.definition.comment"], + "settings": { + "foreground": "#6a8b93", + "fontStyle": "italic" + } + }, + { + "name": "Comments: Preprocessor", + "scope": "comment.block.preprocessor", + "settings": { + "fontStyle": "", + "foreground": "#AAAAAA" + } + }, + { + "name": "Comments: Documentation", + "scope": ["comment.documentation", "comment.block.documentation"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Invalid - Illegal", + "scope": "invalid.illegal", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Operators", + "scope": "keyword.operator", + "settings": { + "foreground": "#AD3EFE" + } + }, + { + "name": "Keywords", + "scope": ["keyword", "storage"], + "settings": { + "foreground": "#1bae1b", + "fontStyle": "bold" + } + }, + { + "name": "Defines", + "scope": ["support.type"], + "settings": { + "foreground": "#008100" + } + }, + { + "name": "Language Constants", + "scope": ["support.constant", "variable.language"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Booleans", + "scope": ["constant.language"], + "settings": { + "fontStyle": "bold", + "foreground": "#0aa361" + } + }, + { + "name": "Variables", + "scope": ["variable", "support.variable"], + "settings": { + "foreground": "#da2bdd" + } + }, + { + "name": "Functions", + "scope": ["entity.name.function", "support.function"], + "settings": { + "foreground": "#41adec" + } + }, + { + "name": "Classes", + "scope": ["entity.name.type", "entity.other.inherited-class", "support.class"], + "settings": { + "foreground": "#6868f3" + } + }, + { + "name": "Exceptions", + "scope": "entity.name.exception", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Sections", + "scope": "entity.name.section", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Parameter", + "scope": ["variable.parameter"], + "settings": { + "foreground": "#72b6fb" + } + }, + { + "name": "Numbers, Characters", + "scope": ["constant.numeric", "constant.character", "constant"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Strings", + "scope": [ + "string", + "punctuation.definition.string.begin", + "punctuation.definition.string.end" + ], + "settings": { + "foreground": "#C44445" + } + }, + { + "name": "Strings: Escape Sequences", + "scope": ["constant.character.escape"], + "settings": { + "foreground": "#da5239" + } + }, + { + "name": "Strings: Regular Expressions", + "scope": "string.regexp", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Strings: Symbols", + "scope": "constant.other.symbol", + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Punctuation", + "scope": "punctuation", + "settings": { + "foreground": "#dfd5d5" + } + }, + { + "name": "HTML: Doctype Declaration", + "scope": [ + "meta.tag.sgml.doctype", + "meta.tag.sgml.doctype string", + "meta.tag.sgml.doctype entity.name.tag", + "meta.tag.sgml punctuation.definition.tag.html" + ], + "settings": { + "foreground": "#AAAAAA" + } + }, + { + "name": "HTML: Tags", + "scope": [ + "meta.tag", + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Tag Names", + "scope": "entity.name.tag", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "HTML: Attribute Names", + "scope": [ + "meta.tag entity.other.attribute-name", + "entity.other.attribute-name.html" + ], + "settings": { + "fontStyle": "italic", + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Entities", + "scope": ["constant.character.entity", "punctuation.definition.entity"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Selectors", + "scope": [ + "meta.selector", + "meta.selector entity", + "meta.selector entity punctuation", + "entity.name.tag.css" + ], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "CSS: Property Names", + "scope": ["meta.property-name", "support.type.property-name"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Property Values", + "scope": [ + "meta.property-value", + "meta.property-value constant.other", + "support.constant.property-value" + ], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "CSS: Important Keyword", + "scope": "keyword.other.important", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Changed", + "scope": "markup.changed", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Deletion", + "scope": "markup.deleted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Markup: Error", + "scope": "markup.error", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Insertion", + "scope": "markup.inserted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Link", + "scope": "meta.link", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup: Output", + "scope": ["markup.output", "markup.raw"], + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Prompt", + "scope": "markup.prompt", + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Heading", + "scope": "markup.heading", + "settings": { + "foreground": "#AA3731" + } + }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Traceback", + "scope": "markup.traceback", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Underline", + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "name": "Markup Quote", + "scope": "markup.quote", + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Markup Lists", + "scope": "markup.list", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup Styling", + "scope": ["markup.bold", "markup.italic"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Markup Inline", + "scope": "markup.inline.raw", + "settings": { + "fontStyle": "", + "foreground": "#AB6526" + } + }, + { + "name": "Extra: Diff Range", + "scope": ["meta.diff.range", "meta.diff.index", "meta.separator"], + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff From", + "scope": "meta.diff.header.from-file", + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff To", + "scope": "meta.diff.header.to-file", + "settings": { + "foreground": "#434343" + } + } + ] +} diff --git a/packages/libro-cofine-editor/src/theme/data/jupyter_hc_dark.json b/packages/libro-cofine-editor/src/theme/data/jupyter_hc_dark.json new file mode 100644 index 00000000..9761dde7 --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/jupyter_hc_dark.json @@ -0,0 +1,385 @@ +{ + "name": "Jupyter Theme HC Dark", + "type": "dark", + "colors": { + "editor.background": "#1b1b1b", + "editor.foreground": "#f8f8f8" + }, + "tokenColors": [ + { + "name": "Comments", + "scope": ["comment", "punctuation.definition.comment"], + "settings": { + "foreground": "#a0b9bf", + "fontStyle": "italic" + } + }, + { + "name": "Comments: Preprocessor", + "scope": "comment.block.preprocessor", + "settings": { + "fontStyle": "", + "foreground": "#AAAAAA" + } + }, + { + "name": "Comments: Documentation", + "scope": ["comment.documentation", "comment.block.documentation"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Invalid - Illegal", + "scope": "invalid.illegal", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Operators", + "scope": "keyword.operator", + "settings": { + "foreground": "#AD3EFE" + } + }, + { + "name": "Keywords", + "scope": ["keyword", "storage"], + "settings": { + "foreground": "#6ce76c", + "fontStyle": "bold" + } + }, + { + "name": "Defines", + "scope": ["support.type"], + "settings": { + "foreground": "#008100" + } + }, + { + "name": "Language Constants", + "scope": ["support.constant", "variable.language"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Booleans", + "scope": ["constant.language"], + "settings": { + "fontStyle": "bold", + "foreground": "#0aa361" + } + }, + { + "name": "Variables", + "scope": ["variable", "support.variable"], + "settings": { + "foreground": "#df6be1" + } + }, + { + "name": "Functions", + "scope": ["entity.name.function", "support.function"], + "settings": { + "foreground": "#92ccee" + } + }, + { + "name": "Classes", + "scope": ["entity.name.type", "entity.other.inherited-class", "support.class"], + "settings": { + "foreground": "#c2c2ff" + } + }, + { + "name": "Exceptions", + "scope": "entity.name.exception", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Sections", + "scope": "entity.name.section", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Parameter", + "scope": ["variable.parameter"], + "settings": { + "foreground": "#72b6fb" + } + }, + { + "name": "Numbers, Characters", + "scope": ["constant.numeric", "constant.character", "constant"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Strings", + "scope": [ + "string", + "punctuation.definition.string.begin", + "punctuation.definition.string.end" + ], + "settings": { + "foreground": "#C44445" + } + }, + { + "name": "Strings: Escape Sequences", + "scope": ["constant.character.escape"], + "settings": { + "foreground": "#da5239" + } + }, + { + "name": "Strings: Regular Expressions", + "scope": "string.regexp", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Strings: Symbols", + "scope": "constant.other.symbol", + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Punctuation", + "scope": "punctuation", + "settings": { + "foreground": "#dfd5d5" + } + }, + { + "name": "HTML: Doctype Declaration", + "scope": [ + "meta.tag.sgml.doctype", + "meta.tag.sgml.doctype string", + "meta.tag.sgml.doctype entity.name.tag", + "meta.tag.sgml punctuation.definition.tag.html" + ], + "settings": { + "foreground": "#AAAAAA" + } + }, + { + "name": "HTML: Tags", + "scope": [ + "meta.tag", + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Tag Names", + "scope": "entity.name.tag", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "HTML: Attribute Names", + "scope": [ + "meta.tag entity.other.attribute-name", + "entity.other.attribute-name.html" + ], + "settings": { + "fontStyle": "italic", + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Entities", + "scope": ["constant.character.entity", "punctuation.definition.entity"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Selectors", + "scope": [ + "meta.selector", + "meta.selector entity", + "meta.selector entity punctuation", + "entity.name.tag.css" + ], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "CSS: Property Names", + "scope": ["meta.property-name", "support.type.property-name"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Property Values", + "scope": [ + "meta.property-value", + "meta.property-value constant.other", + "support.constant.property-value" + ], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "CSS: Important Keyword", + "scope": "keyword.other.important", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Changed", + "scope": "markup.changed", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Deletion", + "scope": "markup.deleted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Markup: Error", + "scope": "markup.error", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Insertion", + "scope": "markup.inserted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Link", + "scope": "meta.link", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup: Output", + "scope": ["markup.output", "markup.raw"], + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Prompt", + "scope": "markup.prompt", + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Heading", + "scope": "markup.heading", + "settings": { + "foreground": "#AA3731" + } + }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Traceback", + "scope": "markup.traceback", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Underline", + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "name": "Markup Quote", + "scope": "markup.quote", + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Markup Lists", + "scope": "markup.list", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup Styling", + "scope": ["markup.bold", "markup.italic"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Markup Inline", + "scope": "markup.inline.raw", + "settings": { + "fontStyle": "", + "foreground": "#AB6526" + } + }, + { + "name": "Extra: Diff Range", + "scope": ["meta.diff.range", "meta.diff.index", "meta.separator"], + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff From", + "scope": "meta.diff.header.from-file", + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff To", + "scope": "meta.diff.header.to-file", + "settings": { + "foreground": "#434343" + } + } + ] +} diff --git a/packages/libro-cofine-editor/src/theme/data/jupyter_hc_light.json b/packages/libro-cofine-editor/src/theme/data/jupyter_hc_light.json new file mode 100644 index 00000000..8615c8cd --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/jupyter_hc_light.json @@ -0,0 +1,386 @@ +{ + "name": "Jupyter Theme HC Light", + "$schema": "vscode://schemas/color-theme", + "type": "hcLight", + "colors": { + "editor.foreground": "#1b1b1b", + "editor.background": "#f8f8f8" + }, + "tokenColors": [ + { + "name": "Comments", + "scope": ["comment", "punctuation.definition.comment"], + "settings": { + "foreground": "#6a8b93", + "fontStyle": "italic" + } + }, + { + "name": "Comments: Preprocessor", + "scope": "comment.block.preprocessor", + "settings": { + "fontStyle": "", + "foreground": "#AAAAAA" + } + }, + { + "name": "Comments: Documentation", + "scope": ["comment.documentation", "comment.block.documentation"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Invalid - Illegal", + "scope": "invalid.illegal", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Operators", + "scope": "keyword.operator", + "settings": { + "foreground": "#AD3EFE" + } + }, + { + "name": "Keywords", + "scope": ["keyword", "storage"], + "settings": { + "foreground": "#008100", + "fontStyle": "bold" + } + }, + { + "name": "Defines", + "scope": ["support.type"], + "settings": { + "foreground": "#008100" + } + }, + { + "name": "Language Constants", + "scope": ["support.constant", "variable.language"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Booleans", + "scope": ["constant.language"], + "settings": { + "fontStyle": "bold", + "foreground": "#008100" + } + }, + { + "name": "Variables", + "scope": ["variable", "support.variable"], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Functions", + "scope": ["entity.name.function", "support.function"], + "settings": { + "foreground": "#68482d" + } + }, + { + "name": "Classes", + "scope": ["entity.name.type", "entity.other.inherited-class", "support.class"], + "settings": { + "foreground": "#0000ff" + } + }, + { + "name": "Exceptions", + "scope": "entity.name.exception", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Sections", + "scope": "entity.name.section", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Parameter", + "scope": ["variable.parameter"], + "settings": { + "foreground": "#396694" + } + }, + { + "name": "Numbers, Characters", + "scope": ["constant.numeric", "constant.character", "constant"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Strings", + "scope": [ + "string", + "punctuation.definition.string.begin", + "punctuation.definition.string.end" + ], + "settings": { + "foreground": "#C44445" + } + }, + { + "name": "Strings: Escape Sequences", + "scope": ["constant.character.escape"], + "settings": { + "foreground": "#da5239" + } + }, + { + "name": "Strings: Regular Expressions", + "scope": "string.regexp", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Strings: Symbols", + "scope": "constant.other.symbol", + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Punctuation", + "scope": "punctuation", + "settings": { + "foreground": "#252525" + } + }, + { + "name": "HTML: Doctype Declaration", + "scope": [ + "meta.tag.sgml.doctype", + "meta.tag.sgml.doctype string", + "meta.tag.sgml.doctype entity.name.tag", + "meta.tag.sgml punctuation.definition.tag.html" + ], + "settings": { + "foreground": "#AAAAAA" + } + }, + { + "name": "HTML: Tags", + "scope": [ + "meta.tag", + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Tag Names", + "scope": "entity.name.tag", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "HTML: Attribute Names", + "scope": [ + "meta.tag entity.other.attribute-name", + "entity.other.attribute-name.html" + ], + "settings": { + "fontStyle": "italic", + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Entities", + "scope": ["constant.character.entity", "punctuation.definition.entity"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Selectors", + "scope": [ + "meta.selector", + "meta.selector entity", + "meta.selector entity punctuation", + "entity.name.tag.css" + ], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "CSS: Property Names", + "scope": ["meta.property-name", "support.type.property-name"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Property Values", + "scope": [ + "meta.property-value", + "meta.property-value constant.other", + "support.constant.property-value" + ], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "CSS: Important Keyword", + "scope": "keyword.other.important", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Changed", + "scope": "markup.changed", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Deletion", + "scope": "markup.deleted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Markup: Error", + "scope": "markup.error", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Insertion", + "scope": "markup.inserted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Link", + "scope": "meta.link", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup: Output", + "scope": ["markup.output", "markup.raw"], + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Prompt", + "scope": "markup.prompt", + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Heading", + "scope": "markup.heading", + "settings": { + "foreground": "#AA3731" + } + }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Traceback", + "scope": "markup.traceback", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Underline", + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "name": "Markup Quote", + "scope": "markup.quote", + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Markup Lists", + "scope": "markup.list", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup Styling", + "scope": ["markup.bold", "markup.italic"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Markup Inline", + "scope": "markup.inline.raw", + "settings": { + "fontStyle": "", + "foreground": "#AB6526" + } + }, + { + "name": "Extra: Diff Range", + "scope": ["meta.diff.range", "meta.diff.index", "meta.separator"], + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff From", + "scope": "meta.diff.header.from-file", + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff To", + "scope": "meta.diff.header.to-file", + "settings": { + "foreground": "#434343" + } + } + ] +} diff --git a/packages/libro-cofine-editor/src/theme/data/jupyter_light.json b/packages/libro-cofine-editor/src/theme/data/jupyter_light.json new file mode 100644 index 00000000..8d20d542 --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/jupyter_light.json @@ -0,0 +1,386 @@ +{ + "name": "Jupyter Theme Light", + "type": "light", + "includes": "light-plus", + "colors": { + "editor.background": "#f5f5f5", + "editor.foreground": "#333333" + }, + "tokenColors": [ + { + "name": "Comments", + "scope": ["comment", "punctuation.definition.comment"], + "settings": { + "foreground": "#6a8b93", + "fontStyle": "italic" + } + }, + { + "name": "Comments: Preprocessor", + "scope": "comment.block.preprocessor", + "settings": { + "fontStyle": "", + "foreground": "#AAAAAA" + } + }, + { + "name": "Comments: Documentation", + "scope": ["comment.documentation", "comment.block.documentation"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Invalid - Illegal", + "scope": "invalid.illegal", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Operators", + "scope": "keyword.operator", + "settings": { + "foreground": "#AD3EFE" + } + }, + { + "name": "Keywords", + "scope": ["keyword", "storage"], + "settings": { + "foreground": "#008100", + "fontStyle": "bold" + } + }, + { + "name": "Defines", + "scope": ["support.type"], + "settings": { + "foreground": "#008100" + } + }, + { + "name": "Language Constants", + "scope": ["support.constant", "variable.language"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Booleans", + "scope": ["constant.language"], + "settings": { + "fontStyle": "bold", + "foreground": "#008100" + } + }, + { + "name": "Variables", + "scope": ["variable", "support.variable"], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Functions", + "scope": ["entity.name.function", "support.function"], + "settings": { + "foreground": "#68482d" + } + }, + { + "name": "Classes", + "scope": ["entity.name.type", "entity.other.inherited-class", "support.class"], + "settings": { + "foreground": "#0000ff" + } + }, + { + "name": "Exceptions", + "scope": "entity.name.exception", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Sections", + "scope": "entity.name.section", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Parameter", + "scope": ["variable.parameter"], + "settings": { + "foreground": "#396694" + } + }, + { + "name": "Numbers, Characters", + "scope": ["constant.numeric", "constant.character", "constant"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Strings", + "scope": [ + "string", + "punctuation.definition.string.begin", + "punctuation.definition.string.end" + ], + "settings": { + "foreground": "#C44445" + } + }, + { + "name": "Strings: Escape Sequences", + "scope": ["constant.character.escape"], + "settings": { + "foreground": "#da5239" + } + }, + { + "name": "Strings: Regular Expressions", + "scope": "string.regexp", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Strings: Symbols", + "scope": "constant.other.symbol", + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "Punctuation", + "scope": "punctuation", + "settings": { + "foreground": "#252525" + } + }, + { + "name": "HTML: Doctype Declaration", + "scope": [ + "meta.tag.sgml.doctype", + "meta.tag.sgml.doctype string", + "meta.tag.sgml.doctype entity.name.tag", + "meta.tag.sgml punctuation.definition.tag.html" + ], + "settings": { + "foreground": "#AAAAAA" + } + }, + { + "name": "HTML: Tags", + "scope": [ + "meta.tag", + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Tag Names", + "scope": "entity.name.tag", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "HTML: Attribute Names", + "scope": [ + "meta.tag entity.other.attribute-name", + "entity.other.attribute-name.html" + ], + "settings": { + "fontStyle": "italic", + "foreground": "#91B3E0" + } + }, + { + "name": "HTML: Entities", + "scope": ["constant.character.entity", "punctuation.definition.entity"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Selectors", + "scope": [ + "meta.selector", + "meta.selector entity", + "meta.selector entity punctuation", + "entity.name.tag.css" + ], + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "CSS: Property Names", + "scope": ["meta.property-name", "support.type.property-name"], + "settings": { + "foreground": "#AB6526" + } + }, + { + "name": "CSS: Property Values", + "scope": [ + "meta.property-value", + "meta.property-value constant.other", + "support.constant.property-value" + ], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "CSS: Important Keyword", + "scope": "keyword.other.important", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Changed", + "scope": "markup.changed", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Deletion", + "scope": "markup.deleted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Markup: Error", + "scope": "markup.error", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Insertion", + "scope": "markup.inserted", + "settings": { + "foreground": "#000000" + } + }, + { + "name": "Markup: Link", + "scope": "meta.link", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup: Output", + "scope": ["markup.output", "markup.raw"], + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Prompt", + "scope": "markup.prompt", + "settings": { + "foreground": "#777777" + } + }, + { + "name": "Markup: Heading", + "scope": "markup.heading", + "settings": { + "foreground": "#AA3731" + } + }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Traceback", + "scope": "markup.traceback", + "settings": { + "foreground": "#660000" + } + }, + { + "name": "Markup: Underline", + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "name": "Markup Quote", + "scope": "markup.quote", + "settings": { + "foreground": "#7A3E9D" + } + }, + { + "name": "Markup Lists", + "scope": "markup.list", + "settings": { + "foreground": "#4B83CD" + } + }, + { + "name": "Markup Styling", + "scope": ["markup.bold", "markup.italic"], + "settings": { + "foreground": "#448C27" + } + }, + { + "name": "Markup Inline", + "scope": "markup.inline.raw", + "settings": { + "fontStyle": "", + "foreground": "#AB6526" + } + }, + { + "name": "Extra: Diff Range", + "scope": ["meta.diff.range", "meta.diff.index", "meta.separator"], + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff From", + "scope": "meta.diff.header.from-file", + "settings": { + "foreground": "#434343" + } + }, + { + "name": "Extra: Diff To", + "scope": "meta.diff.header.to-file", + "settings": { + "foreground": "#434343" + } + } + ] +} diff --git a/packages/libro-cofine-editor/src/theme/data/libro_dark.json b/packages/libro-cofine-editor/src/theme/data/libro_dark.json new file mode 100644 index 00000000..822f0219 --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/libro_dark.json @@ -0,0 +1,186 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "libro dark", + "include": "jupyter-dark", + "colors": { + "editor.background": "#19191b", + "editor.lineHighlightBackground": "#353D53", + "editorCursor.foreground": "#E9E9EC", + "editorIndentGuide.background": "#353D53", + "editor.selectionBackground": "#2858D8", + "editorBracketMatch.background": "#2858D8", + "editor.selectionHighlightBackground": "#223979", + "editor.findMatchBackground": "#FFC600" + }, + "tokenColors": [ + { + "scope": "variable", + "settings": { + "foreground": "#e3e4e6" + } + }, + { + "scope": "storage.type.function", + "settings": { + "foreground": "#109b67", + "fontStyle": "bold" + } + }, + { + "scope": "support.function.builtin.python", + "settings": { + "foreground": "#098658" + } + }, + { + "scope": "comment.line.number-sign.python", + "settings": { + "foreground": "#6A8B93", + "fontStyle": "italic" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#109b67", + "fontStyle": "bold" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#e3e4e6" + } + }, + { + "scope": "meta.member.access", + "settings": { + "foreground": "#5da4ea" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#109b67", + "fontStyle": "bold" + } + }, + { + "scope": "constant.numeric", + "settings": { + "foreground": "#95cd77" + } + }, + { + "scope": "string.quoted", + "settings": { + "foreground": "#ff5b48" + } + }, + { + "scope": "entity.name.function", + "settings": { + "foreground": "#187dff" + } + }, + { + "name": "SQL Scope", + "scope": ["keywords", "customKeywords"], + "settings": { + "foreground": "#46AAFF" + } + }, + { + "name": "SQL Scope", + "scope": ["comment.sql-odps"], + "settings": { + "foreground": "#787D8C" + } + }, + { + "name": "SQL Scope", + "scope": ["builtinFunctions", "function"], + "settings": { + "foreground": "#FD85F4" + } + }, + { + "name": "SQL Scope", + "scope": ["operator"], + "settings": { + "foreground": "#C8D3EC" + } + }, + { + "name": "SQL Scope", + "scope": ["string"], + "settings": { + "foreground": "#E5B200" + } + }, + { + "name": "SQL Scope", + "scope": ["number"], + "settings": { + "foreground": "#B9DCA6" + } + }, + { + "name": "SQL Scope", + "scope": ["variable"], + "settings": { + "foreground": "#B8DBA6" + } + }, + { + "name": "Log Scope", + "scope": ["log-info"], + "settings": { + "foreground": "#008800" + } + }, + { + "name": "Log Scope", + "scope": ["log-alert"], + "settings": { + "foreground": "#EABA19" + } + }, + { + "name": "Log Scope", + "scope": ["log-date"], + "settings": { + "foreground": "#E9E9EC" + } + }, + { + "name": "Log Scope", + "scope": ["log-error"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-links"], + "settings": { + "foreground": "#1292FF" + } + }, + { + "name": "Log Scope", + "scope": ["log-failed"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-success"], + "settings": { + "foreground": "#E9E9EC" + } + } + ], + "semanticHighlighting": true +} diff --git a/packages/libro-cofine-editor/src/theme/data/libro_light.json b/packages/libro-cofine-editor/src/theme/data/libro_light.json new file mode 100644 index 00000000..b38f9d61 --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/data/libro_light.json @@ -0,0 +1,170 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "libro light", + "include": "jupyter-light", + "colors": { + "activityBar.background": "#ececec", + "activityBar.activeBorder": "#000000", + "activityBar.foreground": "#000000" + }, + "tokenColors": [ + { + "scope": "storage.type.function", + "settings": { + "foreground": "#098658", + "fontStyle": "bold" + } + }, + { + "scope": "support.function.builtin.python", + "settings": { + "foreground": "#098658" + } + }, + { + "scope": "comment.line.number-sign.python", + "settings": { + "foreground": "#6A8B93", + "fontStyle": "italic" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#098658", + "fontStyle": "bold" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#c700c7" + } + }, + { + "scope": "meta.member.access", + "settings": { + "foreground": "#2060a0" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#098658", + "fontStyle": "bold" + } + }, + { + "scope": "string.quoted", + "settings": { + "foreground": "#c03030" + } + }, + { + "scope": "entity.name.function", + "settings": { + "foreground": "#003cff" + } + }, + + { + "name": "SQL Scope", + "scope": ["keywords", "customKeywords"], + "settings": { + "foreground": "#236fd9" + } + }, + { + "name": "SQL Scope", + "scope": ["comment"], + "settings": { + "foreground": "#999999" + } + }, + { + "name": "SQL Scope", + "scope": ["builtinFunctions", "function"], + "settings": { + "foreground": "#CB3BC1" + } + }, + { + "name": "SQL Scope", + "scope": ["operator"], + "settings": { + "foreground": "#232226" + } + }, + { + "name": "SQL Scope", + "scope": ["string"], + "settings": { + "foreground": "#D45E00" + } + }, + { + "name": "SQL Scope", + "scope": ["number"], + "settings": { + "foreground": "#2E7F01" + } + }, + { + "name": "SQL Scope", + "scope": ["variable"], + "settings": { + "foreground": "#B8DBA6" + } + }, + { + "name": "Log Scope", + "scope": ["log-info"], + "settings": { + "foreground": "#008800" + } + }, + { + "name": "Log Scope", + "scope": ["log-alert"], + "settings": { + "foreground": "#DE9504" + } + }, + { + "name": "Log Scope", + "scope": ["log-date"], + "settings": { + "foreground": "#171617" + } + }, + { + "name": "Log Scope", + "scope": ["log-error"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-links"], + "settings": { + "foreground": "#1292FF" + } + }, + { + "name": "Log Scope", + "scope": ["log-failed"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-success"], + "settings": { + "foreground": "#171617" + } + } + ], + "semanticHighlighting": true +} diff --git a/packages/libro-cofine-editor/src/theme/libro-python-theme-contribution.ts b/packages/libro-cofine-editor/src/theme/libro-python-theme-contribution.ts new file mode 100644 index 00000000..d6a77e6e --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/libro-python-theme-contribution.ts @@ -0,0 +1,36 @@ +/* eslint-disable global-require */ +import type { ThemeRegistry } from '@difizen/libro-cofine-editor-core'; +import { ThemeContribution } from '@difizen/libro-cofine-editor-core'; +import { singleton } from '@difizen/mana-app'; + +import jupyterDark from './data/jupyter_dark.json'; +import jupyterHCDark from './data/jupyter_hc_dark.json'; +import jupyterHCLight from './data/jupyter_hc_light.json'; +import jupyterLight from './data/jupyter_light.json'; +import libroDark from './data/libro_dark.json'; +import libroLight from './data/libro_light.json'; + +@singleton({ contrib: ThemeContribution }) +export class LibroPythonThemeContribution implements ThemeContribution { + registerItem(registry: ThemeRegistry): void { + if (!registry.mixedThemeEnable) { + console.warn('cannot register textmate themes'); + return; + } + + // theme名称必须满足 ^[a-z0-9\-]+$, 必须提供base theme + // jupyter theme from https://github.com/sam-the-programmer/vscode-jupyter-theme + registry.registerMixedTheme(jupyterLight, 'jupyter-light', 'vs'); + registry.registerMixedTheme(jupyterDark, 'jupyter-dark', 'vs-dark'); + registry.registerMixedTheme(jupyterHCLight, 'jupyter-hc-light', 'hc-black'); + registry.registerMixedTheme(jupyterHCDark, 'jupyter-hc-dark', 'hc-black'); + + /** + * libro theme based on jupyter theme, 支持python; + * 同时兼容sql和log, 如果有其他语言需要支持,需要在主题中指定对应的token和颜色; + * monaco不同编辑器实例无法使用不同的主题,所有的编辑器实例共享一个主题,后创建的编辑器会覆盖更新全局的主题,所以libro所有e2编辑器必须使用同一个主题! + */ + registry.registerMixedTheme(libroLight, 'libro-light', 'vs'); + registry.registerMixedTheme(libroDark, 'libro-dark', 'vs-dark'); + } +} diff --git a/packages/libro-cofine-editor/src/theme/module.ts b/packages/libro-cofine-editor/src/theme/module.ts new file mode 100644 index 00000000..cdc233ca --- /dev/null +++ b/packages/libro-cofine-editor/src/theme/module.ts @@ -0,0 +1,5 @@ +import { Module } from '@difizen/mana-app'; + +import { LibroPythonThemeContribution } from './libro-python-theme-contribution.js'; + +export const LibroE2ThemeModule = Module().register(LibroPythonThemeContribution); diff --git a/packages/libro-cofine-editor/src/types.ts b/packages/libro-cofine-editor/src/types.ts new file mode 100644 index 00000000..e37405e5 --- /dev/null +++ b/packages/libro-cofine-editor/src/types.ts @@ -0,0 +1,11 @@ +import { Range, Uri } from '@difizen/monaco-editor-core'; +import type monaco from '@difizen/monaco-editor-core'; + +export type MonacoEditorType = monaco.editor.IStandaloneCodeEditor; +export type MonacoEditorOptions = monaco.editor.IStandaloneEditorConstructionOptions & { + uri?: monaco.Uri; +}; +export type MonacoMatch = monaco.editor.FindMatch; + +export const MonacoRange = Range; +export const MonacoUri = Uri; diff --git a/packages/libro-cofine-editor/tsconfig.json b/packages/libro-cofine-editor/tsconfig.json new file mode 100644 index 00000000..a18e7b25 --- /dev/null +++ b/packages/libro-cofine-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src", "typings"] +} diff --git a/packages/libro-cofine-language-python/.eslintrc.mjs b/packages/libro-cofine-language-python/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-cofine-language-python/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-cofine-language-python/.fatherrc.ts b/packages/libro-cofine-language-python/.fatherrc.ts new file mode 100644 index 00000000..d7186780 --- /dev/null +++ b/packages/libro-cofine-language-python/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types'], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-cofine-language-python/CHANGELOG.md b/packages/libro-cofine-language-python/CHANGELOG.md new file mode 100644 index 00000000..0991cbc5 --- /dev/null +++ b/packages/libro-cofine-language-python/CHANGELOG.md @@ -0,0 +1,31 @@ +# @difizen/libro-codemirror-markdown-cell + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version +- Updated dependencies [127cb35] +- Updated dependencies + - @difizen/libro-code-editor@0.1.0 + - @difizen/libro-codemirror@0.1.0 + - @difizen/libro-common@0.1.0 + - @difizen/libro-core@0.1.0 + - @difizen/libro-markdown@0.1.0 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version +- Updated dependencies + - @difizen/libro-code-editor@0.0.2-alpha.0 + - @difizen/libro-codemirror@0.0.2-alpha.0 + - @difizen/libro-common@0.0.2-alpha.0 + - @difizen/libro-core@0.0.2-alpha.0 + - @difizen/libro-markdown@0.0.2-alpha.0 diff --git a/packages/libro-cofine-language-python/README.md b/packages/libro-cofine-language-python/README.md new file mode 100644 index 00000000..555f09e7 --- /dev/null +++ b/packages/libro-cofine-language-python/README.md @@ -0,0 +1,3 @@ +# libro-notebook + +cell diff --git a/packages/libro-cofine-language-python/babel.config.json b/packages/libro-cofine-language-python/babel.config.json new file mode 100644 index 00000000..51a623c5 --- /dev/null +++ b/packages/libro-cofine-language-python/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + "@babel/plugin-transform-flow-strip-types", + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-cofine-language-python/jest.config.mjs b/packages/libro-cofine-language-python/jest.config.mjs new file mode 100644 index 00000000..dd07ec10 --- /dev/null +++ b/packages/libro-cofine-language-python/jest.config.mjs @@ -0,0 +1,3 @@ +import configs from '../../jest.config.mjs'; + +export default { ...configs }; diff --git a/packages/libro-cofine-language-python/package.json b/packages/libro-cofine-language-python/package.json new file mode 100644 index 00000000..b69259cd --- /dev/null +++ b/packages/libro-cofine-language-python/package.json @@ -0,0 +1,57 @@ +{ + "name": "@difizen/libro-cofine-language-python", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook", + "monaco" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/libro-cofine-editor-core": "^0.1.0", + "@difizen/libro-cofine-textmate": "^0.1.0", + "@difizen/mana-app": "latest", + "reflect-metadata": "0.1.13" + }, + "devDependencies": { + "@difizen/monaco-editor-core": "latest" + } +} diff --git a/packages/libro-cofine-language-python/src/data/MagicPython.tmLanguage.json b/packages/libro-cofine-language-python/src/data/MagicPython.tmLanguage.json new file mode 100644 index 00000000..d175c37e --- /dev/null +++ b/packages/libro-cofine-language-python/src/data/MagicPython.tmLanguage.json @@ -0,0 +1,5232 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/MagicStack/MagicPython/blob/master/grammars/MagicPython.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/MagicStack/MagicPython/commit/b453f26ed856c9b16a053517c41207e3a72cc7d5", + "name": "MagicPython", + "scopeName": "source.python", + "patterns": [ + { + "include": "#statement" + }, + { + "include": "#expression" + } + ], + "repository": { + "impossible": { + "comment": "This is a special rule that should be used where no match is desired. It is not a good idea to match something like '1{0}' because in some cases that can result in infinite loops in token generation. So the rule instead matches and impossible expression to allow a match to fail and move to the next token.", + "match": "$.^" + }, + "statement": { + "patterns": [ + { + "include": "#import" + }, + { + "include": "#class-declaration" + }, + { + "include": "#function-declaration" + }, + { + "include": "#statement-keyword" + }, + { + "include": "#assignment-operator" + }, + { + "include": "#decorator" + }, + { + "include": "#docstring-statement" + }, + { + "include": "#semicolon" + } + ] + }, + "semicolon": { + "patterns": [ + { + "name": "invalid.deprecated.semicolon.python", + "match": "\\;$" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.python", + "contentName": "meta.typehint.comment.python", + "begin": "(?x)\n (?:\n \\# \\s* (type:)\n \\s*+ (?# we want `\\s*+` which is possessive quantifier since\n we do not actually want to backtrack when matching\n whitespace here)\n (?! $ | \\#)\n )\n", + "end": "(?:$|(?=\\#))", + "beginCaptures": { + "0": { + "name": "meta.typehint.comment.python" + }, + "1": { + "name": "comment.typehint.directive.notation.python" + } + }, + "patterns": [ + { + "name": "comment.typehint.ignore.notation.python", + "match": "(?x)\n \\G ignore\n (?= \\s* (?: $ | \\#))\n" + }, + { + "name": "comment.typehint.type.notation.python", + "match": "(?x)\n (?))" + }, + { + "name": "comment.typehint.variable.notation.python", + "match": "([[:alpha:]_]\\w*)" + } + ] + }, + { + "include": "#comments-base" + } + ] + }, + "docstring-statement": { + "begin": "^(?=\\s*[rR]?(\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\"))", + "end": "(?<=\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\")", + "patterns": [ + { + "include": "#docstring" + } + ] + }, + "docstring": { + "patterns": [ + { + "name": "string.quoted.docstring.multi.python", + "begin": "(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\1)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + }, + { + "include": "#docstring-guts-unicode" + } + ] + }, + { + "name": "string.quoted.docstring.raw.multi.python", + "begin": "([rR])(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#string-consume-escape" + }, + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + } + ] + }, + { + "name": "string.quoted.docstring.single.python", + "begin": "(\\'|\\\")", + "end": "(\\1)|((?>>|\\.\\.\\.) \\s) (?=\\s*\\S)\n )\n", + "captures": { + "1": { + "name": "keyword.control.flow.python" + } + } + }, + "statement-keyword": { + "patterns": [ + { + "name": "storage.type.function.python", + "match": "\\b((async\\s+)?\\s*def)\\b" + }, + { + "name": "keyword.control.flow.python", + "match": "(?x)\n \\b(?>= | //= | \\*\\*=\n | \\+= | -= | /= | @=\n | \\*= | %= | ~= | \\^= | &= | \\|=\n | =(?!=)\n" + }, + "operator": { + "match": "(?x)\n \\b(?> | & | \\| | \\^ | ~) (?# 3)\n\n | (\\*\\* | \\* | \\+ | - | % | // | / | @) (?# 4)\n\n | (!= | == | >= | <= | < | >) (?# 5)\n", + "captures": { + "1": { + "name": "keyword.operator.logical.python" + }, + "2": { + "name": "keyword.control.flow.python" + }, + "3": { + "name": "keyword.operator.bitwise.python" + }, + "4": { + "name": "keyword.operator.arithmetic.python" + }, + "5": { + "name": "keyword.operator.comparison.python" + } + } + }, + "punctuation": { + "patterns": [ + { + "name": "punctuation.separator.colon.python", + "match": ":" + }, + { + "name": "punctuation.separator.element.python", + "match": "," + } + ] + }, + "literal": { + "patterns": [ + { + "name": "constant.language.python", + "match": "\\b(True|False|None|NotImplemented|Ellipsis)\\b" + }, + { + "include": "#number" + } + ] + }, + "number": { + "name": "constant.numeric.python", + "patterns": [ + { + "include": "#number-float" + }, + { + "include": "#number-dec" + }, + { + "include": "#number-hex" + }, + { + "include": "#number-oct" + }, + { + "include": "#number-bin" + }, + { + "include": "#number-long" + }, + { + "name": "invalid.illegal.name.python", + "match": "\\b[0-9]+\\w+" + } + ] + }, + "number-float": { + "name": "constant.numeric.float.python", + "match": "(?x)\n (?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )?\n })\n )\n", + "captures": { + "2": { + "name": "storage.type.format.python" + }, + "3": { + "name": "storage.type.format.python" + } + } + }, + { + "name": "constant.character.format.placeholder.other.python", + "begin": "(?x)\n \\{\n \\w*? (\\.[[:alpha:]_]\\w*? | \\[[^\\]'\"]+\\])*?\n (![rsa])?\n (:)\n (?=[^'\"}\\n]*\\})\n", + "end": "\\}", + "beginCaptures": { + "2": { + "name": "storage.type.format.python" + }, + "3": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "match": "(?x) \\{ [^'\"}\\n]*? \\} (?=.*?\\})\n" + } + ] + } + ] + }, + "fstring-formatting": { + "patterns": [ + { + "include": "#fstring-formatting-braces" + }, + { + "include": "#fstring-formatting-singe-brace" + } + ] + }, + "fstring-formatting-singe-brace": { + "name": "invalid.illegal.brace.python", + "match": "(}(?!}))" + }, + "import": { + "comment": "Import statements\n", + "patterns": [ + { + "match": "(?x)\n \\s* \\b(from)\\b \\s*(\\.+)\\s* (import)?\n", + "captures": { + "1": { + "name": "keyword.control.import.python" + }, + "2": { + "name": "punctuation.separator.period.python" + }, + "3": { + "name": "keyword.control.import.python" + } + } + }, + { + "name": "keyword.control.import.python", + "match": "\\b(?)", + "end": "(?=:)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.annotation.result.python" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "item-access": { + "patterns": [ + { + "name": "meta.item-access.python", + "begin": "(?x)\n \\b(?=\n [[:alpha:]_]\\w* \\s* \\[\n )\n", + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#item-name" + }, + { + "include": "#item-index" + }, + { + "include": "#expression" + } + ] + } + ] + }, + "item-name": { + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#special-names" + }, + { + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "item-index": { + "begin": "(\\[)", + "end": "(?=\\])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + } + }, + "contentName": "meta.item-access.arguments.python", + "patterns": [ + { + "name": "punctuation.separator.slice.python", + "match": ":" + }, + { + "include": "#expression" + } + ] + }, + "decorator": { + "name": "meta.function.decorator.python", + "begin": "(?x)\n ^\\s*\n (@) \\s* (?=[[:alpha:]_]\\w*)\n", + "end": "(?x)\n ( \\) )\n # trailing whitespace and comments are legal\n (?: (.*?) (?=\\s*(?:\\#|$)) )\n | (?=\\n|\\#)\n", + "beginCaptures": { + "1": { + "name": "entity.name.function.decorator.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "invalid.illegal.decorator.python" + } + }, + "patterns": [ + { + "include": "#decorator-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "decorator-name": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#illegal-object-name" + }, + { + "name": "entity.name.function.decorator.python", + "match": "(?x)\n ([[:alpha:]_]\\w*) | \\.\n" + }, + { + "include": "#line-continuation" + }, + { + "name": "invalid.illegal.decorator.python", + "match": "(?x)\n \\s* ([^([:alpha:]\\s_\\.#\\\\] .*?) (?=\\#|$)\n", + "captures": { + "1": { + "name": "invalid.illegal.decorator.python" + } + } + } + ] + }, + "call-wrapper-inheritance": { + "comment": "same as a function call, but in inheritance context", + "name": "meta.function-call.python", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#inheritance-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "inheritance-name": { + "patterns": [ + { + "include": "#lambda-incomplete" + }, + { + "include": "#builtin-possible-callables" + }, + { + "include": "#inheritance-identifier" + } + ] + }, + "function-call": { + "name": "meta.function-call.python", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#function-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "function-name": { + "patterns": [ + { + "include": "#builtin-possible-callables" + }, + { + "comment": "Some color schemas support meta.function-call.generic scope", + "name": "meta.function-call.generic.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "function-arguments": { + "begin": "(?x)\n (?:\n (\\()\n (?:\\s*(\\*\\*|\\*))?\n )\n", + "end": "(?=\\))(?!\\)\\s*\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + }, + "2": { + "name": "keyword.operator.unpacking.arguments.python" + } + }, + "contentName": "meta.function-call.arguments.python", + "patterns": [ + { + "match": "(?x)\n (?:\n (,)\n (?:\\s*(\\*\\*|\\*))?\n )\n", + "captures": { + "1": { + "name": "punctuation.separator.arguments.python" + }, + "2": { + "name": "keyword.operator.unpacking.arguments.python" + } + } + }, + { + "include": "#lambda-incomplete" + }, + { + "include": "#illegal-names" + }, + { + "match": "\\b([[:alpha:]_]\\w*)\\s*(=)(?!=)", + "captures": { + "1": { + "name": "variable.parameter.function-call.python" + }, + "2": { + "name": "keyword.operator.assignment.python" + } + } + }, + { + "name": "keyword.operator.assignment.python", + "match": "=(?!=)" + }, + { + "include": "#expression" + }, + { + "match": "\\s*(\\))\\s*(\\()", + "captures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "punctuation.definition.arguments.begin.python" + } + } + } + ] + }, + "builtin-callables": { + "patterns": [ + { + "include": "#illegal-names" + }, + { + "include": "#illegal-object-name" + }, + { + "include": "#builtin-exceptions" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#builtin-types" + } + ] + }, + "builtin-possible-callables": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#magic-names" + } + ] + }, + "builtin-exceptions": { + "name": "support.type.exception.python", + "match": "(?x) (?" + }, + "regexp-base-expression": { + "patterns": [ + { + "include": "#regexp-quantifier" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fregexp-base-expression": { + "patterns": [ + { + "include": "#fregexp-quantifier" + }, + { + "include": "#fstring-formatting-braces" + }, + { + "match": "\\{.*?\\}" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fstring-formatting-braces": { + "patterns": [ + { + "comment": "empty braces are illegal", + "match": "({)(\\s*?)(})", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "2": { + "name": "invalid.illegal.brace.python" + }, + "3": { + "name": "constant.character.format.placeholder.other.python" + } + } + }, + { + "name": "constant.character.escape.python", + "match": "({{|}})" + } + ] + }, + "regexp-base-common": { + "patterns": [ + { + "name": "support.other.match.any.regexp", + "match": "\\." + }, + { + "name": "support.other.match.begin.regexp", + "match": "\\^" + }, + { + "name": "support.other.match.end.regexp", + "match": "\\$" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[+*?]\\??" + }, + { + "name": "keyword.operator.disjunction.regexp", + "match": "\\|" + }, + { + "include": "#regexp-escape-sequence" + } + ] + }, + "regexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\n" + }, + "fregexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{\\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\\}\n" + }, + "regexp-backreference-number": { + "name": "meta.backreference.regexp", + "match": "(\\\\[1-9]\\d?)", + "captures": { + "1": { + "name": "entity.name.tag.backreference.regexp" + } + } + }, + "regexp-backreference": { + "name": "meta.backreference.named.regexp", + "match": "(?x)\n (\\() (\\?P= \\w+(?:\\s+[[:alnum:]]+)?) (\\))\n", + "captures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.backreference.regexp" + }, + "3": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.end.regexp" + } + } + }, + "regexp-flags": { + "name": "storage.modifier.flag.regexp", + "match": "\\(\\?[aiLmsux]+\\)" + }, + "regexp-escape-special": { + "name": "support.other.escape.special.regexp", + "match": "\\\\([AbBdDsSwWZ])" + }, + "regexp-escape-character": { + "name": "constant.character.escape.regexp", + "match": "(?x)\n \\\\ (\n x[0-9A-Fa-f]{2}\n | 0[0-7]{1,2}\n | [0-7]{3}\n )\n" + }, + "regexp-escape-unicode": { + "name": "constant.character.unicode.regexp", + "match": "(?x)\n \\\\ (\n u[0-9A-Fa-f]{4}\n | U[0-9A-Fa-f]{8}\n )\n" + }, + "regexp-escape-catchall": { + "name": "constant.character.escape.regexp", + "match": "\\\\(.|\\n)" + }, + "regexp-escape-sequence": { + "patterns": [ + { + "include": "#regexp-escape-special" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-backreference-number" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "regexp-charecter-set-escapes": { + "patterns": [ + { + "name": "constant.character.escape.regexp", + "match": "\\\\[abfnrtv\\\\]" + }, + { + "include": "#regexp-escape-special" + }, + { + "name": "constant.character.escape.regexp", + "match": "\\\\([0-7]{1,3})" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "codetags": { + "match": "(?:\\b(NOTE|XXX|HACK|FIXME|BUG|TODO)\\b)", + "captures": { + "1": { + "name": "keyword.codetag.notation.python" + } + } + }, + "comments-base": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($)", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-single-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?='''))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-double-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?=\"\"\"))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-one-regexp-expression": { + "patterns": [ + { + "include": "#regexp-base-expression" + }, + { + "include": "#single-one-regexp-character-set" + }, + { + "include": "#single-one-regexp-comments" + }, + { + "include": "#regexp-flags" + }, + { + "include": "#single-one-regexp-named-group" + }, + { + "include": "#regexp-backreference" + }, + { + "include": "#single-one-regexp-lookahead" + }, + { + "include": "#single-one-regexp-lookahead-negative" + }, + { + "include": "#single-one-regexp-lookbehind" + }, + { + "include": "#single-one-regexp-lookbehind-negative" + }, + { + "include": "#single-one-regexp-conditional" + }, + { + "include": "#single-one-regexp-parentheses-non-capturing" + }, + { + "include": "#single-one-regexp-parentheses" + } + ] + }, + "single-one-regexp-character-set": { + "patterns": [ + { + "match": "(?x)\n \\[ \\^? \\] (?! .*?\\])\n" + }, + { + "name": "meta.character.set.regexp", + "begin": "(\\[)(\\^)?(\\])?", + "end": "(\\]|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?)\n", + "end": "(\\)|(?=\"))|((?=(?)\n", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "double-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?)\n", + "end": "(\\)|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-fregexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-fregexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-fregexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-fregexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-fregexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-fregexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-fregexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-fregexp-lookbehind-negative": { + "begin": "(\\()\\?)\n", + "end": "(\\)|(?=\"))|((?=(?)\n", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-fregexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-fregexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-fregexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-fregexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-fregexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-fregexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-fregexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-fregexp-lookbehind-negative": { + "begin": "(\\()\\?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-single-tail" + } + ] + }, + "fstring-terminator-single-tail": { + "begin": "(![rsa])?(:)(?=.*?{)", + "end": "(?=})|(?=\\n)", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-single-brace" + }, + { + "include": "#fstring-single-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + }, + "fstring-fnorm-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[fF])([bBuU])?('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "2": { + "name": "invalid.illegal.prefix.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.interpolated.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-normf-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[bBuU])([fF])('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "invalid.illegal.prefix.python" + }, + "2": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-raw-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b(?:[R][fF]|[fF][R]))('''|\"\"\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.raw.multi.python storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python string.quoted.raw.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.raw.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-raw-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-raw-multi-core" + } + ] + }, + "fstring-multi-core": { + "name": "string.interpolated.python string.quoted.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-raw-multi-core": { + "name": "string.interpolated.python string.quoted.raw.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-multi-brace": { + "comment": "value interpolation using { ... }", + "begin": "(\\{)", + "end": "(?x)\n (\\})\n", + "beginCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "endCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "patterns": [ + { + "include": "#fstring-terminator-multi" + }, + { + "include": "#f-expression" + } + ] + }, + "fstring-terminator-multi": { + "patterns": [ + { + "name": "storage.type.format.python", + "match": "(![rsa])(?=})" + }, + { + "match": "(?x)\n (![rsa])?\n ( : \\w? [<>=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-multi-tail" + } + ] + }, + "fstring-terminator-multi-tail": { + "begin": "(![rsa])?(:)(?=.*?{)", + "end": "(?=})", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + } + } +} diff --git a/packages/libro-cofine-language-python/src/data/MagicRegExp.tmLanguage.json b/packages/libro-cofine-language-python/src/data/MagicRegExp.tmLanguage.json new file mode 100644 index 00000000..e9f3d7b1 --- /dev/null +++ b/packages/libro-cofine-language-python/src/data/MagicRegExp.tmLanguage.json @@ -0,0 +1,497 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/MagicStack/MagicPython/blob/master/grammars/MagicRegExp.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/MagicStack/MagicPython/commit/361a4964a559481330764a447e7bab88d4f1b01b", + "name": "MagicRegExp", + "scopeName": "source.regexp.python", + "patterns": [ + { + "include": "#regexp-expression" + } + ], + "repository": { + "regexp-base-expression": { + "patterns": [ + { + "include": "#regexp-quantifier" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fregexp-base-expression": { + "patterns": [ + { + "include": "#fregexp-quantifier" + }, + { + "include": "#fstring-formatting-braces" + }, + { + "match": "\\{.*?\\}" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fstring-formatting-braces": { + "patterns": [ + { + "comment": "empty braces are illegal", + "match": "({)(\\s*?)(})", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "2": { + "name": "invalid.illegal.brace.python" + }, + "3": { + "name": "constant.character.format.placeholder.other.python" + } + } + }, + { + "name": "constant.character.escape.python", + "match": "({{|}})" + } + ] + }, + "regexp-base-common": { + "patterns": [ + { + "name": "support.other.match.any.regexp", + "match": "\\." + }, + { + "name": "support.other.match.begin.regexp", + "match": "\\^" + }, + { + "name": "support.other.match.end.regexp", + "match": "\\$" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[+*?]\\??" + }, + { + "name": "keyword.operator.disjunction.regexp", + "match": "\\|" + }, + { + "include": "#regexp-escape-sequence" + } + ] + }, + "regexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\n" + }, + "fregexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{\\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\\}\n" + }, + "regexp-backreference-number": { + "name": "meta.backreference.regexp", + "match": "(\\\\[1-9]\\d?)", + "captures": { + "1": { + "name": "entity.name.tag.backreference.regexp" + } + } + }, + "regexp-backreference": { + "name": "meta.backreference.named.regexp", + "match": "(?x)\n (\\() (\\?P= \\w+(?:\\s+[[:alnum:]]+)?) (\\))\n", + "captures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.backreference.regexp" + }, + "3": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.end.regexp" + } + } + }, + "regexp-flags": { + "name": "storage.modifier.flag.regexp", + "match": "\\(\\?[aiLmsux]+\\)" + }, + "regexp-escape-special": { + "name": "support.other.escape.special.regexp", + "match": "\\\\([AbBdDsSwWZ])" + }, + "regexp-escape-character": { + "name": "constant.character.escape.regexp", + "match": "(?x)\n \\\\ (\n x[0-9A-Fa-f]{2}\n | 0[0-7]{1,2}\n | [0-7]{3}\n )\n" + }, + "regexp-escape-unicode": { + "name": "constant.character.unicode.regexp", + "match": "(?x)\n \\\\ (\n u[0-9A-Fa-f]{4}\n | U[0-9A-Fa-f]{8}\n )\n" + }, + "regexp-escape-catchall": { + "name": "constant.character.escape.regexp", + "match": "\\\\(.|\\n)" + }, + "regexp-escape-sequence": { + "patterns": [ + { + "include": "#regexp-escape-special" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-backreference-number" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "regexp-charecter-set-escapes": { + "patterns": [ + { + "name": "constant.character.escape.regexp", + "match": "\\\\[abfnrtv\\\\]" + }, + { + "include": "#regexp-escape-special" + }, + { + "name": "constant.character.escape.regexp", + "match": "\\\\([0-7]{1,3})" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "codetags": { + "match": "(?:\\b(NOTE|XXX|HACK|FIXME|BUG|TODO)\\b)", + "captures": { + "1": { + "name": "keyword.codetag.notation.python" + } + } + }, + "regexp-expression": { + "patterns": [ + { + "include": "#regexp-base-expression" + }, + { + "include": "#regexp-character-set" + }, + { + "include": "#regexp-comments" + }, + { + "include": "#regexp-flags" + }, + { + "include": "#regexp-named-group" + }, + { + "include": "#regexp-backreference" + }, + { + "include": "#regexp-lookahead" + }, + { + "include": "#regexp-lookahead-negative" + }, + { + "include": "#regexp-lookbehind" + }, + { + "include": "#regexp-lookbehind-negative" + }, + { + "include": "#regexp-conditional" + }, + { + "include": "#regexp-parentheses-non-capturing" + }, + { + "include": "#regexp-parentheses" + } + ] + }, + "regexp-character-set": { + "patterns": [ + { + "match": "(?x)\n \\[ \\^? \\] (?! .*?\\])\n" + }, + { + "name": "meta.character.set.regexp", + "begin": "(\\[)(\\^)?(\\])?", + "end": "(\\])", + "beginCaptures": { + "1": { + "name": "punctuation.character.set.begin.regexp constant.other.set.regexp" + }, + "2": { + "name": "keyword.operator.negation.regexp" + }, + "3": { + "name": "constant.character.set.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.character.set.end.regexp constant.other.set.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-charecter-set-escapes" + }, + { + "name": "constant.character.set.regexp", + "match": "[^\\n]" + } + ] + } + ] + }, + "regexp-named-group": { + "name": "meta.named.regexp", + "begin": "(?x)\n (\\() (\\?P <\\w+(?:\\s+[[:alnum:]]+)?>)\n", + "end": "(\\))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#regexp-expression" + } + ] + }, + "regexp-lookbehind-negative": { + "begin": "(\\()\\? = { + [BuiltinFunctions.__import__]: { + completionKind: CompletionItemKind.Function, + documentation: + "__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module\n\nImport a module. Because this function is meant for use by the Python\ninterpreter and not for general use, it is better to use\nimportlib.import_module() to programmatically import a module.\n\nThe globals argument is only used to determine the context;\nthey are not modified.  The locals argument is unused.  The fromlist\nshould be a list of names to emulate ``from name import ...'', or an\nempty list to emulate ``import name''.\nWhen importing a module from a package, note that __import__('A.B', ...)\nreturns package A when fromlist is empty, but its submodule B when\nfromlist is not empty.  The level argument is used to determine whether to\nperform absolute or relative imports: 0 is absolute, while a positive number\nis the number of parent directories to search relative to the current module.", + hover: [ + { + language: 'python', + value: + '__import__(name: Text, globals: Optional[Mapping[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., fromlist: Sequence[str]=..., level: int=...) -> Any', + }, + ], + }, + [BuiltinFunctions.abs]: { + completionKind: CompletionItemKind.Function, + documentation: 'Return the absolute value of the argument.', + hover: [{ language: 'python', value: 'abs(n: SupportsAbs[_T], /) -> _T' }], + }, + [BuiltinFunctions.all]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return True if bool(x) is True for all values x in the iterable.\n\nIf the iterable is empty, return True.', + hover: [{ language: 'python', value: 'all(i: Iterable[object], /) -> bool' }], + }, + [BuiltinFunctions.any]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return True if bool(x) is True for any x in the iterable.\n\nIf the iterable is empty, return False.', + hover: [{ language: 'python', value: 'any(i: Iterable[object], /) -> bool' }], + }, + [BuiltinFunctions.ascii]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return an ASCII-only representation of an object.\n\nAs repr(), return a string containing a printable representation of an\nobject, but escape the non-ASCII characters in the string returned by\nrepr() using \\x, \\u or \\U escapes. This generates a string similar\nto that returned by repr() in Python 2.', + hover: [{ language: 'python', value: 'ascii(o: object, /) -> str' }], + }, + [BuiltinFunctions.bin]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the binary representation of an integer.\n\n>>> bin(2796202)\n'0b1010101010101010101010'", + hover: [ + { + language: 'python', + value: 'bin(number: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.bool]: { + completionKind: CompletionItemKind.Class, + documentation: + 'bool(x) -> bool\n\nReturns True when the argument x is true, False otherwise.\nThe builtins True and False are the only two instances of the class bool.\nThe class bool is a subclass of the class int, and cannot be subclassed.', + hover: [{ language: 'python', value: 'bool(o: object=...)' }], + }, + [BuiltinFunctions.bytearray]: { + completionKind: CompletionItemKind.Class, + documentation: + 'bytearray(iterable_of_ints) -> bytearray\nbytearray(string, encoding[, errors]) -> bytearray\nbytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer\nbytearray(int) -> bytes array of size given by the parameter initialized with null bytes\nbytearray() -> empty bytes array\n\nConstruct a mutable bytearray object from:\n  - an iterable yielding integers in range(256)\n  - a text string encoded using the specified encoding\n  - a bytes or a buffer object\n  - any object implementing the buffer API.\n  - an integer', + hover: [{ language: 'python', value: 'bytearray()' }], + }, + [BuiltinFunctions.bytes]: { + completionKind: CompletionItemKind.Class, + documentation: + 'bytes(iterable_of_ints) -> bytes\nbytes(string, encoding[, errors]) -> bytes\nbytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer\nbytes(int) -> bytes object of size given by the parameter initialized with null bytes\nbytes() -> empty bytes object\n\nConstruct an immutable array of bytes from:\n  - an iterable yielding integers in range(256)\n  - a text string encoded using the specified encoding\n  - any object implementing the buffer API.\n  - an integer', + hover: [{ language: 'python', value: 'bytes(ints: Iterable[int])' }], + }, + [BuiltinFunctions.callable]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return whether the object is callable (i.e., some kind of function).\n\nNote that classes are callable, as are instances of classes with a\n__call__() method.', + hover: [ + { + language: 'python', + value: 'callable(o: object, /) -> bool', + }, + ], + }, + [BuiltinFunctions.chr]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff.', + hover: [ + { + language: 'python', + value: 'chr(code: int, /) -> str', + }, + ], + }, + [BuiltinFunctions.classmethod]: { + completionKind: CompletionItemKind.Class, + documentation: + 'classmethod(function) -> method\n\nConvert a function to be a class method.\n\nA class method receives the class as implicit first argument,\njust like an instance method receives the instance.\nTo declare a class method, use this idiom:\n\n  class C:\n      @classmethod\n      def f(cls, arg1, arg2, ...):\n          ...\n\nIt can be called either on the class (e.g. C.f()) or on an instance\n(e.g. C().f()).  The instance is ignored except for its class.\nIf a class method is called for a derived class, the derived class\nobject is passed as the implied first argument.\n\nClass methods are different than C++ or Java static methods.\nIf you want those, see the staticmethod builtin.', + hover: [ + { + language: 'python', + value: 'classmethod(f: Callable[..., Any])', + }, + ], + }, + [BuiltinFunctions.compile]: { + completionKind: CompletionItemKind.Function, + documentation: + "Compile source into a code object that can be executed by exec() or eval().\n\nThe source code may represent a Python module, statement or expression.\nThe filename will be used for run-time error messages.\nThe mode must be 'exec' to compile a module, 'single' to compile a\nsingle (interactive) statement, or 'eval' to compile an expression.\nThe flags argument, if present, controls which future statements influence\nthe compilation of the code.\nThe dont_inherit argument, if true, stops the compilation inheriting\nthe effects of any future statements in effect in the code calling\ncompile; if absent or false these statements do influence the compilation,\nin addition to any features explicitly specified.", + hover: [ + { + language: 'python', + value: + 'compile(source: Union[str, bytes, mod, AST], filename: Union[str, bytes], mode: str, flags: int=..., dont_inherit: int=..., optimize: int=...) -> Any', + }, + ], + }, + [BuiltinFunctions.complex]: { + completionKind: CompletionItemKind.Class, + documentation: + 'Create a complex number from a real part and an optional imaginary part.\n\nThis is equivalent to (real + imag*1j) where imag defaults to 0.', + hover: [ + { + language: 'python', + value: 'complex(real: float=..., imag: float=...)', + }, + ], + }, + [BuiltinFunctions.delattr]: { + completionKind: CompletionItemKind.Function, + documentation: + "Deletes the named attribute from the given object.\n\ndelattr(x, 'y') is equivalent to ``del x.y''", + hover: [ + { + language: 'python', + value: 'delattr(o: Any, name: Text, /) -> None', + }, + ], + }, + [BuiltinFunctions.dict]: { + completionKind: CompletionItemKind.Class, + documentation: + "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n    (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n    d = {}\n    for k, v in iterable:\n        d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n    in the keyword argument list.  For example:  dict(one=1, two=2)", + hover: [ + { + language: 'python', + value: 'dict(**kwargs: _VT)', + }, + ], + }, + [BuiltinFunctions.dir]: { + completionKind: CompletionItemKind.Function, + documentation: + "dir([object]) -> list of strings\n\nIf called without an argument, return the names in the current scope.\nElse, return an alphabetized list of names comprising (some of) the attributes\nof the given object, and of attributes reachable from it.\nIf the object supplies a method named __dir__, it will be used; otherwise\nthe default dir() logic is used and returns:\n  for a module object: the module's attributes.\n  for a class object:  its attributes, and recursively the attributes\n    of its bases.\n  for any other object: its attributes, its class's attributes, and\n    recursively the attributes of its class's base classes.", + hover: [ + { + language: 'python', + value: 'dir(o: object=..., /) -> List[str]', + }, + ], + }, + [BuiltinFunctions.divmod]: { + completionKind: CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: 'divmod(a: _N2, b: _N2, /) -> Tuple[_N2, _N2]', + }, + ], + documentation: 'Return the tuple (x//y, x%y).  Invariant: div*y + mod == x.', + }, + [BuiltinFunctions.enumerate]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return an enumerate object.\n\n  iterable\n    an object supporting iteration\n\nThe enumerate object yields pairs containing a count (from start, which\ndefaults to zero) and a value yielded by the iterable argument.\n\nenumerate is useful for obtaining an indexed list:\n    (0, seq[0]), (1, seq[1]), (2, seq[2]), ...', + hover: [ + { + language: 'python', + value: 'enumerate(iterable: Iterable[_T], start: int=...)', + }, + ], + }, + [BuiltinFunctions.eval]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Evaluate the given source in the context of globals and locals.\n\nThe source may be a string representing a Python expression\nor a code object as returned by compile().\nThe globals must be a dictionary and locals can be any mapping,\ndefaulting to the current globals and locals.\nIf only globals is given, locals defaults to it.', + hover: [ + { + language: 'python', + value: + 'eval(source: Union[Text, bytes, CodeType], globals: Optional[Dict[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.exec]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Execute the given source in the context of globals and locals.\n\nThe source may be a string representing one or more Python statements\nor a code object as returned by compile().\nThe globals must be a dictionary and locals can be any mapping,\ndefaulting to the current globals and locals.\nIf only globals is given, locals defaults to it.', + hover: [ + { + language: 'python', + value: + 'exec(object: Union[str, bytes, CodeType], globals: Optional[Dict[str, Any]]=..., locals: Optional[Mapping[str, Any]]=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.filter]: { + completionKind: CompletionItemKind.Function, + documentation: + 'filter(function or None, iterable) --> filter object\n\nReturn an iterator yielding those items of iterable for which function(item)\nis true. If function is None, return the items that are true.', + hover: [ + { + language: 'python', + value: + 'filter(function: None, iterable: Iterable[Optional[_T]], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.float]: { + completionKind: CompletionItemKind.Class, + documentation: + 'Convert a string or number to a floating point number, if possible.', + hover: [ + { + language: 'python', + value: + 'float(x: Union[SupportsFloat, _SupportsIndex, Text, bytes, bytearray]=...)', + }, + ], + }, + [BuiltinFunctions.format]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return value.__format__(format_spec)\n\nformat_spec defaults to the empty string.\nSee the Format Specification Mini-Language section of help('FORMATTING') for\ndetails.", + hover: [ + { + language: 'python', + value: 'format(o: object, format_spec: str=..., /) -> str', + }, + ], + }, + [BuiltinFunctions.frozenset]: { + completionKind: CompletionItemKind.Class, + documentation: + 'frozenset() -> empty frozenset object\nfrozenset(iterable) -> frozenset object\n\nBuild an immutable unordered collection of unique elements.', + hover: [ + { + language: 'python', + value: 'frozenset(iterable: Iterable[_T]=...)', + }, + ], + }, + + [BuiltinFunctions.getattr]: { + completionKind: CompletionItemKind.Function, + documentation: + "getattr(object, name[, default]) -> value\n\nGet a named attribute from an object; getattr(x, 'y') is equivalent to x.y.\nWhen a default argument is given, it is returned when the attribute doesn't\nexist; without it, an exception is raised in that case.", + hover: [ + { + language: 'python', + value: 'getattr(o: Any, /, name: Text, default: Any=..., /) -> Any', + }, + ], + }, + [BuiltinFunctions.globals]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the dictionary containing the current scope's global variables.\n\nNOTE: Updates to this dictionary *will* affect name lookups in the current\nglobal scope and vice-versa.", + hover: [ + { + language: 'python', + value: 'globals() -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.hasattr]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return whether the object has an attribute with the given name.\n\nThis is done by calling getattr(obj, name) and catching AttributeError.', + hover: [ + { + language: 'python', + value: 'hasattr(o: Any, name: Text, /) -> bool', + }, + ], + }, + [BuiltinFunctions.hash]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return whether the object has an attribute with the given name.\n\nThis is done by calling getattr(obj, name) and catching AttributeError.', + hover: [ + { + language: 'python', + value: 'hasattr(o: Any, name: Text, /) -> bool', + }, + ], + }, + + [BuiltinFunctions.help]: { + completionKind: CompletionItemKind.Function, + documentation: + "Define the builtin 'help'.\n\nThis is a wrapper around pydoc.help that provides a helpful message\nwhen 'help' is typed at the Python interactive prompt.\n\nCalling help() at the Python prompt starts an interactive help session.\nCalling help(thing) prints help for the python object 'thing'.", + hover: [ + { + language: 'python', + value: 'help(*args: Any, **kwds: Any) -> None', + }, + ], + }, + [BuiltinFunctions.hex]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the hexadecimal representation of an integer.\n\n>>> hex(12648430)\n'0xc0ffee'", + hover: [ + { + language: 'python', + value: 'hex(i: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.id]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the identity of an object.\n\nThis is guaranteed to be unique among simultaneously existing objects.\n(CPython uses the object's memory address.)", + hover: [ + { + language: 'python', + value: 'id(o: object, /) -> int', + }, + ], + }, + [BuiltinFunctions.input]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Read a string from standard input.  The trailing newline is stripped.\n\nThe prompt string, if given, is printed to standard output without a\ntrailing newline before reading input.\n\nIf the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.\nOn *nix systems, readline is used if available.', + hover: [ + { + language: 'python', + value: 'input(prompt: Any=..., /) -> str', + }, + ], + }, + [BuiltinFunctions.int]: { + completionKind: CompletionItemKind.Class, + documentation: + "int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given.  If x is a number, return x.__int__().  For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base.  The literal can be preceded by '+' or '-' and be surrounded\nby whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4", + hover: [ + { + language: 'python', + value: 'int(x: Union[Text, bytes, SupportsInt, _SupportsIndex]=...)', + }, + ], + }, + [BuiltinFunctions.isinstance]: { + completionKind: CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: + 'isinstance(o: object, t: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], /) -> bool', + }, + ], + documentation: + 'Return whether an object is an instance of a class or of a subclass thereof.\n\nA tuple, as in ``isinstance(x, (A, B, ...))``, may be given as the target to\ncheck against. This is equivalent to ``isinstance(x, A) or isinstance(x, B)\nor ...`` etc.', + }, + [BuiltinFunctions.issubclass]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return whether 'cls' is a derived from another class or is the same class.\n\nA tuple, as in ``issubclass(x, (A, B, ...))``, may be given as the target to\ncheck against. This is equivalent to ``issubclass(x, A) or issubclass(x, B)\nor ...`` etc.", + hover: [ + { + language: 'python', + value: + 'issubclass(cls: type, classinfo: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], /) -> bool', + }, + ], + }, + [BuiltinFunctions.iter]: { + completionKind: CompletionItemKind.Function, + documentation: + 'iter(iterable) -> iterator\niter(callable, sentinel) -> iterator\n\nGet an iterator from an object.  In the first form, the argument must\nsupply its own iterator, or be a sequence.\nIn the second form, the callable is called until it returns the sentinel.', + hover: [ + { + language: 'python', + value: 'iter(iterable: Iterable[_T], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.len]: { + completionKind: CompletionItemKind.Function, + documentation: 'Return the number of items in a container.', + hover: [ + { + language: 'python', + value: 'len(o: Sized, /) -> int', + }, + ], + }, + [BuiltinFunctions.list]: { + completionKind: CompletionItemKind.Class, + documentation: + 'Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.', + hover: [ + { + language: 'python', + value: 'list()', + }, + ], + }, + + [BuiltinFunctions.locals]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return a dictionary containing the current scope's local variables.\n\nNOTE: Whether or not updates to this dictionary will affect name lookups in\nthe local scope and vice-versa is *implementation dependent* and not\ncovered by any backwards compatibility guarantees.", + hover: [ + { + language: 'python', + value: 'locals() -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.map]: { + completionKind: CompletionItemKind.Function, + documentation: + 'map(func, *iterables) --> map object\n\nMake an iterator that computes the function using arguments from\neach of the iterables.  Stops when the shortest iterable is exhausted.', + hover: [ + { + language: 'python', + value: + 'map(func: Callable[[_T1], _S], iter1: Iterable[_T1], /) -> Iterator[_S]', + }, + ], + }, + [BuiltinFunctions.max]: { + completionKind: CompletionItemKind.Function, + documentation: + 'max(iterable, *[, default=obj, key=func]) -> value\nmax(arg1, arg2, *args, *[, key=func]) -> value\n\nWith a single iterable argument, return its biggest item. The\ndefault keyword-only argument specifies an object to return if\nthe provided iterable is empty.\nWith two or more arguments, return the largest argument.', + hover: [ + { + language: 'python', + value: + 'max(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], Any]=...) -> _T', + }, + ], + }, + [BuiltinFunctions.memoryview]: { + completionKind: CompletionItemKind.Function, + documentation: 'Create a new memoryview object which references the given object.', + hover: [ + { + language: 'python', + value: 'memoryview(obj: Union[bytes, bytearray, memoryview])', + }, + ], + }, + [BuiltinFunctions.min]: { + completionKind: CompletionItemKind.Function, + documentation: + 'min(iterable, *[, default=obj, key=func]) -> value\nmin(arg1, arg2, *args, *[, key=func]) -> value\n\nWith a single iterable argument, return its smallest item. The\ndefault keyword-only argument specifies an object to return if\nthe provided iterable is empty.\nWith two or more arguments, return the smallest argument.', + hover: [ + { + language: 'python', + value: + 'min(arg1: _T, arg2: _T, /, *_args: _T, key: Callable[[_T], Any]=...) -> _T', + }, + ], + }, + [BuiltinFunctions.next]: { + completionKind: CompletionItemKind.Function, + documentation: + 'next(iterator[, default])\n\nReturn the next item from the iterator. If default is given and the iterator\nis exhausted, it is returned instead of raising StopIteration.', + hover: [ + { + language: 'python', + value: 'next(i: Iterator[_T], /) -> _T', + }, + ], + }, + [BuiltinFunctions.object]: { + completionKind: CompletionItemKind.Class, + documentation: 'The most base type', + hover: [ + { + language: 'python', + value: 'object()', + }, + ], + }, + [BuiltinFunctions.oct]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the octal representation of an integer.\n\n>>> oct(342391)\n'0o1234567'", + hover: [ + { + language: 'python', + value: 'oct(i: Union[int, _SupportsIndex], /) -> str', + }, + ], + }, + [BuiltinFunctions.open]: { + completionKind: CompletionItemKind.Function, + documentation: + "Open file and return a stream.  Raise OSError upon failure.\n\nfile is either a text or byte string giving the name (and the path\nif the file isn't in the current working directory) of the file to\nbe opened or an integer file descriptor of the file to be\nwrapped. (If a file descriptor is given, it is closed when the\nreturned I/O object is closed, unless closefd is set to False.)\n\nmode is an optional string that specifies the mode in which the file\nis opened. It defaults to 'r' which means open for reading in text\nmode.  Other common values are 'w' for writing (truncating the file if\nit already exists), 'x' for creating and writing to a new file, and\n'a' for appending (which on some Unix systems, means that all writes\nappend to the end of the file regardless of the current seek position).\nIn text mode, if encoding is not specified the encoding used is platform\ndependent: locale.getpreferredencoding(False) is called to get the\ncurrent locale encoding. (For reading and writing raw bytes use binary\nmode and leave encoding unspecified.) The available modes are:\n\n========= ===============================================================\nCharacter Meaning\n--------- ---------------------------------------------------------------\n'r'       open for reading (default)\n'w'       open for writing, truncating the file first\n'x'       create a new file and open it for writing\n'a'       open for writing, appending to the end of the file if it exists\n'b'       binary mode\n't'       text mode (default)\n'+'       open a disk file for updating (reading and writing)\n'U'       universal newline mode (deprecated)\n========= ===============================================================\n\nThe default mode is 'rt' (open for reading text). For binary random\naccess, the mode 'w+b' opens and truncates the file to 0 bytes, while\n'r+b' opens the file without truncation. The 'x' mode implies 'w' and\nraises an `FileExistsError` if the file already exists.\n\nPython distinguishes between files opened in binary and text modes,\neven when the underlying operating system doesn't. Files opened in\nbinary mode (appending 'b' to the mode argument) return contents as\nbytes objects without any decoding. In text mode (the default, or when\n't' is appended to the mode argument), the contents of the file are\nreturned as strings, the bytes having been first decoded using a\nplatform-dependent encoding or using the specified encoding if given.\n\n'U' mode is deprecated and will raise an exception in future versions\nof Python.  It has no effect in Python 3.  Use newline to control\nuniversal newlines mode.\n\nbuffering is an optional integer used to set the buffering policy.\nPass 0 to switch buffering off (only allowed in binary mode), 1 to select\nline buffering (only usable in text mode), and an integer > 1 to indicate\nthe size of a fixed-size chunk buffer.  When no buffering argument is\ngiven, the default buffering policy works as follows:\n\n* Binary files are buffered in fixed-size chunks; the size of the buffer\n  is chosen using a heuristic trying to determine the underlying device's\n  \"block size\" and falling back on `io.DEFAULT_BUFFER_SIZE`.\n  On many systems, the buffer will typically be 4096 or 8192 bytes long.\n\n* \"Interactive\" text files (files for which isatty() returns True)\n  use line buffering.  Other text files use the policy described above\n  for binary files.\n\nencoding is the name of the encoding used to decode or encode the\nfile. This should only be used in text mode. The default encoding is\nplatform dependent, but any encoding supported by Python can be\npassed.  See the codecs module for the list of supported encodings.\n\nerrors is an optional string that specifies how encoding errors are to\nbe handled---this argument should not be used in binary mode. Pass\n'strict' to raise a ValueError exception if there is an encoding error\n(the default of None has the same effect), or pass 'ignore' to ignore\nerrors. (Note that ignoring encoding errors can lead to data loss.)\nSee the documentation for codecs.register or run 'help(codecs.Codec)'\nfor a list of the permitted encoding error strings.\n\nnewline controls how universal newlines works (it only applies to text\nmode). It can be None, '', '\n', '\r', and '\r\n'.  It works as\nfollows:\n\n* On input, if newline is None, universal newlines mode is\n  enabled. Lines in the input can end in '\n', '\r', or '\r\n', and\n  these are translated into '\n' before being returned to the\n  caller. If it is '', universal newline mode is enabled, but line\n  endings are returned to the caller untranslated. If it has any of\n  the other legal values, input lines are only terminated by the given\n  string, and the line ending is returned to the caller untranslated.\n\n* On output, if newline is None, any '\n' characters written are\n  translated to the system default line separator, os.linesep. If\n  newline is '' or '\n', no translation takes place. If newline is any\n  of the other legal values, any '\n' characters written are translated\n  to the given string.\n\nIf closefd is False, the underlying file descriptor will be kept open\nwhen the file is closed. This does not work when a file name is given\nand must be True in that case.\n\nA custom opener can be used by passing a callable as *opener*. The\nunderlying file descriptor for the file object is then obtained by\ncalling *opener* with (*file*, *flags*). *opener* must return an open\nfile descriptor (passing os.open as *opener* results in functionality\nsimilar to passing None).\n\nopen() returns a file object whose type depends on the mode, and\nthrough which the standard file operations such as reading and writing\nare performed. When open() is used to open a file in a text mode ('w',\n'r', 'wt', 'rt', etc.), it returns a TextIOWrapper. When used to open\na file in a binary mode, the returned class varies: in read binary\nmode, it returns a BufferedReader; in write binary and append binary\nmodes, it returns a BufferedWriter, and in read/write mode, it returns\na BufferedRandom.\n\nIt is also possible to use a string or bytearray as a file for both\nreading and writing. For strings StringIO can be used like a file\nopened in a text mode, and for bytes a BytesIO can be used like a file\nopened in a binary mode.", + hover: [ + { + language: 'python', + value: + 'open(file: Union[str, bytes, int], mode: str=..., buffering: int=..., encoding: Optional[str]=..., errors: Optional[str]=..., newline: Optional[str]=..., closefd: bool=..., opener: Optional[Callable[[str, int], int]]=...) -> IO[Any]', + }, + ], + }, + [BuiltinFunctions.ord]: { + completionKind: CompletionItemKind.Function, + hover: [ + { + language: 'python', + value: 'ord(c: Union[Text, bytes], /) -> int', + }, + ], + documentation: 'Return the Unicode code point for a one-character string.', + }, + [BuiltinFunctions.pow]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Equivalent to x**y (with two arguments) or x**y % z (with three arguments)\n\nSome types, such as ints, are able to use a more efficient algorithm when\ninvoked using the three argument form.', + hover: [ + { + language: 'python', + value: 'pow(x: int, y: int, /) -> Any', + }, + ], + }, + [BuiltinFunctions.print]: { + completionKind: CompletionItemKind.Function, + documentation: + "print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)\n\nPrints the values to a stream, or to sys.stdout by default.\nOptional keyword arguments:\nfile:  a file-like object (stream); defaults to the current sys.stdout.\nsep:   string inserted between values, default a space.\nend:   string appended after the last value, default a newline.\nflush: whether to forcibly flush the stream.", + hover: [ + { + language: 'python', + value: + 'print(*values: object, sep: Optional[Text]=..., end: Optional[Text]=..., file: Optional[_Writer]=..., flush: bool=...) -> None', + }, + ], + }, + [BuiltinFunctions.property]: { + completionKind: CompletionItemKind.Class, + documentation: `Property attribute.\n\n  fget\n    function to be used for getting an attribute value\n  fset\n    function to be used for setting an attribute value\n  fdel\n    function to be used for del'ing an attribute\n  doc\n    docstring\n\nTypical use is to define a managed attribute x:\n\nclass C(object):\n    def getx(self): return self._x\n    def setx(self, value): self._x = value\n    def delx(self): del self._x\n    x = property(getx, setx, delx, "I'm the 'x' property.")\n\nDecorators make defining new properties or modifying existing ones easy:\n\nclass C(object):\n    @property\n    def x(self):\n        "I am the 'x' property."\n        return self._x\n    @x.setter\n    def x(self, value):\n        self._x = value\n    @x.deleter\n    def x(self):\n        del self._x`, + hover: [ + { + language: 'python', + value: + 'property(fget: Optional[Callable[[Any], Any]]=..., fset: Optional[Callable[[Any, Any], None]]=..., fdel: Optional[Callable[[Any], None]]=..., doc: Optional[str]=...)', + }, + ], + }, + [BuiltinFunctions.range]: { + completionKind: CompletionItemKind.Function, + documentation: + 'range(stop) -> range object\nrange(start, stop[, step]) -> range object\n\nReturn an object that produces a sequence of integers from start (inclusive)\nto stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.\nstart defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.\nThese are exactly the valid indices for a list of 4 elements.\nWhen step is given, it specifies the increment (or decrement).', + hover: [ + { + language: 'python', + value: 'range(stop: int)', + }, + ], + }, + [BuiltinFunctions.repr]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return the canonical string representation of the object.\n\nFor many object types, including most builtins, eval(repr(obj)) == obj.', + hover: [ + { + language: 'python', + value: 'repr(o: object, /) -> str', + }, + ], + }, + [BuiltinFunctions.reversed]: { + completionKind: CompletionItemKind.Function, + documentation: 'Return a reverse iterator over the values of the given sequence.', + hover: [ + { + language: 'python', + value: 'reversed(object: Sequence[_T], /) -> Iterator[_T]', + }, + ], + }, + [BuiltinFunctions.round]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Round a number to a given precision in decimal digits.\n\nThe return value is an integer if ndigits is omitted or None.  Otherwise\nthe return value has the same type as the number.  ndigits may be negative.', + hover: [ + { + language: 'python', + value: 'round(number: float) -> int', + }, + ], + }, + [BuiltinFunctions.set]: { + completionKind: CompletionItemKind.Class, + documentation: + 'set() -> new empty set object\nset(iterable) -> new set object\n\nBuild an unordered collection of unique elements.', + hover: [ + { + language: 'python', + value: 'set(iterable: Iterable[_T]=...)', + }, + ], + }, + [BuiltinFunctions.setattr]: { + completionKind: CompletionItemKind.Function, + documentation: + "Sets the named attribute on the given object to the specified value.\n\nsetattr(x, 'y', v) is equivalent to ``x.y = v''", + hover: [ + { + language: 'python', + value: 'setattr(object: Any, name: Text, value: Any, /) -> None', + }, + ], + }, + [BuiltinFunctions.slice]: { + completionKind: CompletionItemKind.Class, + documentation: + 'slice(stop)\nslice(start, stop[, step])\n\nCreate a slice object.  This is used for extended slicing (e.g. a[0:10:2]).', + hover: [ + { + language: 'python', + value: 'slice(stop: Any)', + }, + ], + }, + [BuiltinFunctions.sorted]: { + completionKind: CompletionItemKind.Function, + documentation: + 'Return a new list containing all items from the iterable in ascending order.\n\nA custom key function can be supplied to customize the sort order, and the\nreverse flag can be set to request the result in descending order.', + hover: [ + { + language: 'python', + value: + 'sorted(iterable: Iterable[_T], /, *, key: Optional[Callable[[_T], Any]]=..., reverse: bool=...) -> List[_T]', + }, + ], + }, + [BuiltinFunctions.staticmethod]: { + completionKind: CompletionItemKind.Class, + documentation: + 'staticmethod(function) -> method\n\nConvert a function to be a static method.\n\nA static method does not receive an implicit first argument.\nTo declare a static method, use this idiom:\n\n     class C:\n         @staticmethod\n         def f(arg1, arg2, ...):\n             ...\n\nIt can be called either on the class (e.g. C.f()) or on an instance\n(e.g. C().f()).  The instance is ignored except for its class.\n\nStatic methods in Python are similar to those found in Java or C++.\nFor a more advanced concept, see the classmethod builtin.', + hover: [ + { + language: 'python', + value: 'staticmethod(f: Callable[..., Any])', + }, + ], + }, + [BuiltinFunctions.str]: { + completionKind: CompletionItemKind.Class, + documentation: + "str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", + hover: [ + { + language: 'python', + value: 'str(o: object=...)', + }, + ], + }, + [BuiltinFunctions.sum]: { + completionKind: CompletionItemKind.Function, + documentation: + "Return the sum of a 'start' value (default: 0) plus an iterable of numbers\n\nWhen the iterable is empty, return the start value.\nThis function is intended specifically for use with numeric values and may\nreject non-numeric types.", + hover: [ + { + language: 'python', + value: 'sum(iterable: Iterable[_T], /) -> Union[_T, int]', + }, + ], + }, + [BuiltinFunctions.super]: { + completionKind: CompletionItemKind.Class, + documentation: + 'super() -> same as super(__class__, )\nsuper(type) -> unbound super object\nsuper(type, obj) -> bound super object; requires isinstance(obj, type)\nsuper(type, type2) -> bound super object; requires issubclass(type2, type)\nTypical use to call a cooperative superclass method:\nclass C(B):\n    def meth(self, arg):\n        super().meth(arg)\nThis works for class methods too:\nclass C(B):\n    @classmethod\n    def cmeth(cls, arg):\n        super().cmeth(arg)', + hover: [ + { + language: 'python', + value: 'super(t: Any, obj: Any)', + }, + ], + }, + [BuiltinFunctions.tuple]: { + completionKind: CompletionItemKind.Class, + documentation: + "Built-in immutable sequence.\n\nIf no argument is given, the constructor returns an empty tuple.\nIf iterable is specified the tuple is initialized from iterable's items.\n\nIf the argument is a tuple, the return value is the same object.", + hover: [ + { + language: 'python', + value: 'tuple(iterable: Iterable[_T_co]=...)', + }, + ], + }, + [BuiltinFunctions.type]: { + completionKind: CompletionItemKind.Class, + documentation: + "type(object_or_name, bases, dict)\ntype(object) -> the object's type\ntype(name, bases, dict) -> a new type", + hover: [ + { + language: 'python', + value: 'type(o: object)', + }, + ], + }, + [BuiltinFunctions.vars]: { + completionKind: CompletionItemKind.Function, + documentation: + 'vars([object]) -> dictionary\n\nWithout arguments, equivalent to locals().\nWith an argument, equivalent to object.__dict__.', + hover: [ + { + language: 'python', + value: 'vars(object: Any=..., /) -> Dict[str, Any]', + }, + ], + }, + [BuiltinFunctions.zip]: { + completionKind: CompletionItemKind.Function, + documentation: + 'zip(*iterables) --> zip object\n\nReturn a zip object whose .__next__() method returns a tuple where\nthe i-th element comes from the i-th iterable argument.  The .__next__()\nmethod continues until the shortest iterable in the argument sequence\nis exhausted and then it raises StopIteration.', + hover: [ + { + language: 'python', + value: 'zip(iter1: Iterable[_T1], /) -> Iterator[Tuple[_T1]]', + }, + ], + }, +}; diff --git a/packages/libro-cofine-language-python/src/python-language-feature.ts b/packages/libro-cofine-language-python/src/python-language-feature.ts new file mode 100644 index 00000000..0506863f --- /dev/null +++ b/packages/libro-cofine-language-python/src/python-language-feature.ts @@ -0,0 +1,208 @@ +/* eslint-disable global-require */ + +import { + LanguageOptionsRegistry, + SnippetSuggestContribution, +} from '@difizen/libro-cofine-editor-core'; +import type { SnippetSuggestRegistry } from '@difizen/libro-cofine-editor-core'; +import type { + GrammarDefinition, + TextmateRegistry, +} from '@difizen/libro-cofine-textmate'; +import { LanguageGrammarDefinitionContribution } from '@difizen/libro-cofine-textmate'; +// import { connectLanguageClient } from '@difizen/libro-cofine-editor-lsp'; +// import type { LSPClientContribution, LSPClientRegistry } from '@difizen/libro-cofine-editor-lsp'; +import { inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import platformGrammar from './data/MagicPython.tmLanguage.json'; +import cGrammar from './data/MagicRegExp.tmLanguage.json'; +import snippetsJson from './data/snippets/python.snippets.json'; +import type { BuiltinFunctions } from './python-builtin.js'; +import { + asMarkdownString, + BuiltinFunctionList, + BuiltinFunctionOptions, +} from './python-builtin.js'; + +export interface PythonLanguageOption { + lspHost: { + host: string; + path: string; + }; +} + +export function isPythonLanguageOption(data: object): data is PythonLanguageOption { + return data && typeof data === 'object' && 'lspHost' in data; +} +let providerRegistered = false; +@singleton({ + contrib: [LanguageGrammarDefinitionContribution, SnippetSuggestContribution], +}) +// LSPClientContribution, +export class PythonContribution + implements LanguageGrammarDefinitionContribution, SnippetSuggestContribution +{ + protected readonly optionsResgistry: LanguageOptionsRegistry; + constructor( + @inject(LanguageOptionsRegistry) optionsResgistry: LanguageOptionsRegistry, + ) { + this.optionsResgistry = optionsResgistry; + } + readonly id = 'python'; + readonly config: monaco.languages.LanguageConfiguration = { + comments: { + lineComment: '#', + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '[', close: ']' }, + { open: '{', close: '}' }, + { open: '(', close: ')' }, + { open: "'", close: "'", notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + folding: { + markers: { + start: new RegExp('^\\s*#pragma\\s+region\\b'), + end: new RegExp('^\\s*#pragma\\s+endregion\\b'), + }, + }, + onEnterRules: [ + { + beforeText: + /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\s*$/, + action: { indentAction: monaco.languages.IndentAction.Indent }, + }, + ], + }; + + registerSnippetSuggest(registry: SnippetSuggestRegistry) { + registry.fromJSON(snippetsJson as any, { + language: [this.id], + source: 'Python Language', + }); + } + + registerTextmateLanguage(registry: TextmateRegistry): void { + monaco.languages.register({ + id: this.id, + extensions: [ + '.py', + '.rpy', + '.pyw', + '.cpy', + '.gyp', + '.gypi', + '.snakefile', + '.smk', + ], + aliases: ['Python', 'py'], + firstLine: '^#!\\s*/.*\\bpython[0-9.-]*\\b', + }); + if (!providerRegistered) { + monaco.languages.registerCompletionItemProvider(this.id, this); + monaco.languages.registerHoverProvider(this.id, this); + providerRegistered = true; + } + monaco.languages.setLanguageConfiguration(this.id, this.config); + registry.registerTextmateGrammarScope('source.python', { + async getGrammarDefinition(): Promise { + return { + format: 'json', + content: platformGrammar, + }; + }, + }); + + registry.registerTextmateGrammarScope('source.regexp.python', { + async getGrammarDefinition(): Promise { + return { + format: 'json', + content: cGrammar, + }; + }, + }); + registry.mapLanguageIdToTextmateGrammar(this.id, 'source.python'); + } + async provideCompletionItems( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): Promise { + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + if (word.word.match(/[\w]*$/)) { + const matchedNames = BuiltinFunctionList.filter((name) => + name.startsWith(word.word), + ) as BuiltinFunctions[]; + const suggestions = matchedNames.map((item) => { + return { + label: item, + kind: BuiltinFunctionOptions[item].completionKind, + documentation: BuiltinFunctionOptions[item].documentation, + detail: BuiltinFunctionOptions[item].documentation, + insertText: item, + range, + }; + }); + return { suggestions }; + } + + return { + suggestions: [], + }; + } + async provideHover( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): Promise { + let word = model.getWordUntilPosition(position); + let nextWord = word; + let currentPosition = position; + let i = 0; // 防止死循环,设置偏移上限, 内置函数最长 11 字符 + while (i < 15 && word.word && nextWord.word.startsWith(word.word)) { + word = nextWord; + currentPosition = currentPosition.delta(0, 1); + nextWord = model.getWordUntilPosition(currentPosition); + i += 1; + } + if (BuiltinFunctionList.includes(word.word)) { + const contents: monaco.IMarkdownString[] = []; + const option = BuiltinFunctionOptions[word.word as BuiltinFunctions]; + if (option.hover) { + contents.push(...option.hover.map((item) => asMarkdownString(item))); + } + if (option.documentation) { + contents.push({ + value: option.documentation, + }); + } + return { + range: new monaco.Range( + position.lineNumber, + word.startColumn, + position.lineNumber, + word.endColumn, + ), + contents, + }; + } + return undefined; + } +} diff --git a/packages/libro-cofine-language-python/tsconfig.json b/packages/libro-cofine-language-python/tsconfig.json new file mode 100644 index 00000000..a18e7b25 --- /dev/null +++ b/packages/libro-cofine-language-python/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src", "typings"] +} diff --git a/packages/libro-cofine-textmate/.eslintrc.mjs b/packages/libro-cofine-textmate/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-cofine-textmate/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-cofine-textmate/.fatherrc.ts b/packages/libro-cofine-textmate/.fatherrc.ts new file mode 100644 index 00000000..d7186780 --- /dev/null +++ b/packages/libro-cofine-textmate/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types'], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-cofine-textmate/CHANGELOG.md b/packages/libro-cofine-textmate/CHANGELOG.md new file mode 100644 index 00000000..0991cbc5 --- /dev/null +++ b/packages/libro-cofine-textmate/CHANGELOG.md @@ -0,0 +1,31 @@ +# @difizen/libro-codemirror-markdown-cell + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version +- Updated dependencies [127cb35] +- Updated dependencies + - @difizen/libro-code-editor@0.1.0 + - @difizen/libro-codemirror@0.1.0 + - @difizen/libro-common@0.1.0 + - @difizen/libro-core@0.1.0 + - @difizen/libro-markdown@0.1.0 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version +- Updated dependencies + - @difizen/libro-code-editor@0.0.2-alpha.0 + - @difizen/libro-codemirror@0.0.2-alpha.0 + - @difizen/libro-common@0.0.2-alpha.0 + - @difizen/libro-core@0.0.2-alpha.0 + - @difizen/libro-markdown@0.0.2-alpha.0 diff --git a/packages/libro-cofine-textmate/README.md b/packages/libro-cofine-textmate/README.md new file mode 100644 index 00000000..555f09e7 --- /dev/null +++ b/packages/libro-cofine-textmate/README.md @@ -0,0 +1,3 @@ +# libro-notebook + +cell diff --git a/packages/libro-cofine-textmate/babel.config.json b/packages/libro-cofine-textmate/babel.config.json new file mode 100644 index 00000000..51a623c5 --- /dev/null +++ b/packages/libro-cofine-textmate/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + "@babel/plugin-transform-flow-strip-types", + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-cofine-textmate/jest.config.mjs b/packages/libro-cofine-textmate/jest.config.mjs new file mode 100644 index 00000000..dd07ec10 --- /dev/null +++ b/packages/libro-cofine-textmate/jest.config.mjs @@ -0,0 +1,3 @@ +import configs from '../../jest.config.mjs'; + +export default { ...configs }; diff --git a/packages/libro-cofine-textmate/package.json b/packages/libro-cofine-textmate/package.json new file mode 100644 index 00000000..6978d88c --- /dev/null +++ b/packages/libro-cofine-textmate/package.json @@ -0,0 +1,59 @@ +{ + "name": "@difizen/libro-cofine-textmate", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook", + "monaco" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/libro-cofine-editor-core": "^0.1.0", + "@difizen/mana-app": "latest", + "fast-plist": "^0.1.2", + "vscode-oniguruma": "^1.5.1", + "vscode-textmate": "^5.4.0" + }, + "devDependencies": { + "@types/debug": "^4.1.6", + "@difizen/monaco-editor-core": "latest" + } +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_defaults.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_defaults.json new file mode 100644 index 00000000..3bd13560 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_defaults.json @@ -0,0 +1,23 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark Default Colors", + "colors": { + "editor.background": "#1E1E1E", + "editor.foreground": "#D4D4D4", + "editor.inactiveSelectionBackground": "#3A3D41", + "editorIndentGuide.background": "#404040", + "editorIndentGuide.activeBackground": "#707070", + "editor.selectionHighlightBackground": "#ADD6FF26", + "list.dropBackground": "#383B3D", + "activityBarBadge.background": "#007ACC", + "sideBarTitle.foreground": "#BBBBBB", + "input.placeholderForeground": "#A6A6A6", + "settings.textInputBackground": "#292929", + "settings.numberInputBackground": "#292929", + "menu.background": "#252526", + "menu.foreground": "#CCCCCC", + "statusBarItem.remoteForeground": "#FFF", + "statusBarItem.remoteBackground": "#16825D" + }, + "tokenColors": [] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_editor.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_editor.json new file mode 100644 index 00000000..917eadef --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_editor.json @@ -0,0 +1,116 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark (e2-editor)", + "include": "dark_plus.json", + "colors": { + "editor.background": "#1F2740", + "editor.lineHighlightBackground": "#353D53", + "editorCursor.foreground": "#E9E9EC", + "editorIndentGuide.background": "#353D53", + "editor.selectionBackground": "#2858D8", + "editorBracketMatch.background": "#2858D8", + "editor.selectionHighlightBackground": "#223979", + "editor.findMatchBackground": "#FFC600" + }, + "tokenColors": [ + { + "name": "Default Scope", + "scope": [""], + "settings": { + "foreground": "#ffffff", + "background": "#353D53" + } + }, + { + "name": "Default Scope", + "scope": ["keywords", "customKeywords"], + "settings": { + "foreground": "#46AAFF" + } + }, + { + "name": "Default Scope", + "scope": ["comment"], + "settings": { + "foreground": "#787D8C" + } + }, + { + "name": "Default Scope", + "scope": ["builtinFunctions", "function"], + "settings": { + "foreground": "#FD85F4" + } + }, + { + "name": "Default Scope", + "scope": ["operator"], + "settings": { + "foreground": "#C8D3EC" + } + }, + { + "name": "Default Scope", + "scope": ["string"], + "settings": { + "foreground": "#E5B200" + } + }, + { + "name": "Default Scope", + "scope": ["number"], + "settings": { + "foreground": "#B9DCA6" + } + }, + { + "name": "Log Scope", + "scope": ["log-info"], + "settings": { + "foreground": "#008800" + } + }, + { + "name": "Log Scope", + "scope": ["log-alert"], + "settings": { + "foreground": "#EABA19" + } + }, + { + "name": "Log Scope", + "scope": ["log-date"], + "settings": { + "foreground": "#E9E9EC" + } + }, + { + "name": "Log Scope", + "scope": ["log-error"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-links"], + "settings": { + "foreground": "#1292FF" + } + }, + { + "name": "Log Scope", + "scope": ["log-failed"], + "settings": { + "foreground": "#FF4B61" + } + }, + { + "name": "Log Scope", + "scope": ["log-success"], + "settings": { + "foreground": "#E9E9EC" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_plus.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_plus.json new file mode 100644 index 00000000..1af7773b --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_plus.json @@ -0,0 +1,180 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark+ (default dark)", + "include": "dark_vs.json", + "colors": {}, + "tokenColors": [ + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars", + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" + ], + "settings": { + "foreground": "#DCDCAA" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "meta.return-type", + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.other.attribute", + "entity.name.scope-resolution", + "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", + "storage.type.error.go", + "storage.type.rune.go", + "storage.type.cs", + "storage.type.generic.cs", + "storage.type.modifier.cs", + "storage.type.variable.cs", + "storage.type.annotation.java", + "storage.type.generic.java", + "storage.type.java", + "storage.type.object.array.java", + "storage.type.primitive.array.java", + "storage.type.primitive.java", + "storage.type.token.java", + "storage.type.groovy", + "storage.type.annotation.groovy", + "storage.type.parameters.groovy", + "storage.type.generic.groovy", + "storage.type.object.array.groovy", + "storage.type.primitive.array.groovy", + "storage.type.primitive.groovy" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Types declaration and references, TS grammar specific", + "scope": [ + "meta.type.cast.expr", + "meta.type.new.expr", + "support.constant.math", + "support.constant.dom", + "support.constant.json", + "entity.other.inherited-class" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Control flow / Special keywords", + "scope": [ + "keyword.control", + "source.cpp keyword.operator.new", + "keyword.operator.delete", + "keyword.other.using", + "keyword.other.operator", + "entity.name.operator" + ], + "settings": { + "foreground": "#C586C0" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "Object keys, TS grammar specific", + "scope": ["meta.object-literal.key"], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "CSS property value", + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#CE9178" + } + }, + { + "name": "Regular expression groups", + "scope": [ + "punctuation.definition.group.regexp", + "punctuation.definition.group.assertion.regexp", + "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", + "keyword.operator.negation.regexp", + "support.other.parenthesis.regexp" + ], + "settings": { + "foreground": "#CE9178" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#d16969" + } + }, + { + "scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"], + "settings": { + "foreground": "#DCDCAA" + } + }, + { + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "constant.character", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "constant.character.escape", + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "entity.name.label", + "settings": { + "foreground": "#C8C8C8" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_vs.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_vs.json new file mode 100644 index 00000000..120201b8 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/dark_vs.json @@ -0,0 +1,358 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark (Visual Studio)", + "include": "dark_defaults.json", + "colors": {}, + "tokenColors": [ + { + "scope": ["meta.embedded", "source.groovy.embedded"], + "settings": { + "foreground": "#D4D4D4" + } + }, + { + "scope": "emphasis", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "strong", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "header", + "settings": { + "foreground": "#000080" + } + }, + { + "scope": "comment", + "settings": { + "foreground": "#6A9955" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "constant.numeric", + "entity.name.operator.custom-literal.number", + "variable.other.enummember", + "keyword.operator.plus.exponent", + "keyword.operator.minus.exponent" + ], + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "constant.regexp", + "settings": { + "foreground": "#646695" + } + }, + { + "scope": "entity.name.tag", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "entity.name.tag.css", + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": [ + "entity.other.attribute-name.class.css", + "entity.other.attribute-name.class.mixin.css", + "entity.other.attribute-name.id.css", + "entity.other.attribute-name.parent-selector.css", + "entity.other.attribute-name.pseudo-class.css", + "entity.other.attribute-name.pseudo-element.css", + "source.css.less entity.other.attribute-name.id", + "entity.other.attribute-name.attribute.scss", + "entity.other.attribute-name.scss" + ], + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "invalid", + "settings": { + "foreground": "#f44747" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold", + "foreground": "#569cd6" + } + }, + { + "scope": "markup.heading", + "settings": { + "fontStyle": "bold", + "foreground": "#569cd6" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "markup.inserted", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "markup.deleted", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "markup.changed", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "punctuation.definition.quote.begin.markdown", + "settings": { + "foreground": "#6A9955" + } + }, + { + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "#6796e6" + } + }, + { + "scope": "markup.inline.raw", + "settings": { + "foreground": "#ce9178" + } + }, + { + "name": "brackets of XML/HTML tags", + "scope": "punctuation.definition.tag", + "settings": { + "foreground": "#808080" + } + }, + { + "scope": ["meta.preprocessor", "entity.name.function.preprocessor"], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "meta.preprocessor.string", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "meta.preprocessor.numeric", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "meta.structure.dictionary.key.python", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage.type", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": ["storage.modifier", "keyword.operator.noexcept"], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "string", + "entity.name.operator.custom-literal.string", + "meta.embedded.assembly" + ], + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.tag", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.value", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.regexp", + "settings": { + "foreground": "#d16969" + } + }, + { + "name": "String interpolation", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end", + "punctuation.section.embedded" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "name": "Reset JavaScript string interpolation expression", + "scope": ["meta.template.expression"], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": [ + "support.type.vendored.property-name", + "support.type.property-name", + "variable.css", + "variable.scss", + "variable.other.less", + "source.coffee.embedded" + ], + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": [ + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.alignof", + "keyword.operator.typeid", + "keyword.operator.alignas", + "keyword.operator.instanceof", + "keyword.operator.logical.python", + "keyword.operator.wordlike" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.other.unit", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": [ + "punctuation.section.embedded.begin.php", + "punctuation.section.embedded.end.php" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "support.function.git-rebase", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": "constant.sha.git-rebase", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "name": "coloring of the Java import and package identifiers", + "scope": [ + "storage.modifier.import.java", + "variable.language.wildcard.java", + "storage.modifier.package.java" + ], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "name": "this.self", + "scope": "variable.language", + "settings": { + "foreground": "#569cd6" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black.json new file mode 100644 index 00000000..c21ba9e7 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black.json @@ -0,0 +1,120 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark High Contrast", + "include": "hc_black_defaults.json", + "colors": { + "selection.background": "#008000", + "editor.selectionBackground": "#FFFFFF", + "statusBarItem.remoteBackground": "#00000000" + }, + "tokenColors": [ + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars", + "source.powershell variable.other.member" + ], + "settings": { + "foreground": "#DCDCAA" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "meta.return-type", + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.name.scope-resolution", + "entity.name.class", + "storage.type.cs", + "storage.type.generic.cs", + "storage.type.modifier.cs", + "storage.type.variable.cs", + "storage.type.annotation.java", + "storage.type.generic.java", + "storage.type.java", + "storage.type.object.array.java", + "storage.type.primitive.array.java", + "storage.type.primitive.java", + "storage.type.token.java", + "storage.type.groovy", + "storage.type.annotation.groovy", + "storage.type.parameters.groovy", + "storage.type.generic.groovy", + "storage.type.object.array.groovy", + "storage.type.primitive.array.groovy", + "storage.type.primitive.groovy" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Types declaration and references, TS grammar specific", + "scope": [ + "meta.type.cast.expr", + "meta.type.new.expr", + "support.constant.math", + "support.constant.dom", + "support.constant.json", + "entity.other.inherited-class" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Control flow / Special keywords", + "scope": [ + "keyword.control", + "source.cpp keyword.operator.new", + "source.cpp keyword.operator.delete", + "keyword.other.using", + "keyword.other.operator" + ], + "settings": { + "foreground": "#C586C0" + } + }, + { + "name": "Variable and parameter name", + "scope": ["variable", "meta.definition.variable.name", "support.variable"], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "Object keys, TS grammar specific", + "scope": ["meta.object-literal.key"], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "CSS property value", + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#CE9178" + } + }, + { + "name": "HC Search Editor context line override", + "scope": "meta.resultLinePrefix.contextLinePrefix.search", + "settings": { + "foreground": "#CBEDCB" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black_defaults.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black_defaults.json new file mode 100644 index 00000000..e42ab756 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_black_defaults.json @@ -0,0 +1,335 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "High Contrast Default Colors", + "tokenColors": [], + "colors": { + "editor.background": "#000000", + "editor.foreground": "#FFFFFF", + "editorIndentGuide.background": "#FFFFFF", + "editorIndentGuide.activeBackground": "#FFFFFF", + "statusBarItem.remoteBackground": "#00000000", + "sideBarTitle.foreground": "#FFFFFF" + }, + "settings": [ + { + "settings": { + "foreground": "#FFFFFF", + "background": "#000000" + } + }, + { + "scope": ["meta.embedded", "source.groovy.embedded"], + "settings": { + "foreground": "#FFFFFF", + "background": "#000000" + } + }, + { + "scope": "emphasis", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "strong", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#000080" + } + }, + { + "scope": "comment", + "settings": { + "foreground": "#7ca668" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "constant.numeric", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "constant.regexp", + "settings": { + "foreground": "#b46695" + } + }, + { + "scope": "constant.character", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "entity.name.tag", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "entity.name.tag.css", + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": [ + "entity.other.attribute-name.class.css", + "entity.other.attribute-name.class.mixin.css", + "entity.other.attribute-name.id.css", + "entity.other.attribute-name.parent-selector.css", + "entity.other.attribute-name.pseudo-class.css", + "entity.other.attribute-name.pseudo-element.css", + + "source.css.less entity.other.attribute-name.id", + + "entity.other.attribute-name.attribute.scss", + "entity.other.attribute-name.scss" + ], + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "invalid", + "settings": { + "foreground": "#f44747" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "markup.heading", + "settings": { + "foreground": "#6796e6" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "markup.inserted", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "markup.deleted", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "markup.changed", + "settings": { + "foreground": "#569cd6" + } + }, + { + "name": "brackets of XML/HTML tags", + "scope": ["punctuation.definition.tag"], + "settings": { + "foreground": "#808080" + } + }, + { + "scope": "meta.preprocessor", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "meta.preprocessor.string", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "meta.preprocessor.numeric", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "meta.structure.dictionary.key.python", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": "storage", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage.type", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage.modifier", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "string", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.tag", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.value", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.regexp", + "settings": { + "foreground": "#d16969" + } + }, + { + "name": "String interpolation", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end", + "punctuation.section.embedded" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "name": "Reset JavaScript string interpolation expression", + "scope": ["meta.template.expression"], + "settings": { + "foreground": "#ffffff" + } + }, + { + "scope": [ + "support.type.vendored.property-name", + "support.type.property-name", + "variable.css", + "variable.scss", + "variable.other.less", + "source.coffee.embedded" + ], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": [ + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.logical.python" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.other.unit", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "support.function.git-rebase", + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": "constant.sha.git-rebase", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "name": "coloring of the Java import and package identifiers", + "scope": [ + "storage.modifier.import.java", + "variable.language.wildcard.java", + "storage.modifier.package.java" + ], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "name": "coloring of the TS this", + "scope": "variable.language.this", + "settings": { + "foreground": "#569cd6" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_editor.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_editor.json new file mode 100644 index 00000000..4c9c1dcd --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/hc_editor.json @@ -0,0 +1,7 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Dark High Contrast (e2-editor)", + "colors": {}, + "tokenColors": [], + "include": "hc_black.json" +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_defaults.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_defaults.json new file mode 100644 index 00000000..69d2378c --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_defaults.json @@ -0,0 +1,22 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Light Default Colors", + "tokenColors": [], + "colors": { + "editor.background": "#FFFFFF", + "editor.foreground": "#000000", + "editor.inactiveSelectionBackground": "#E5EBF1", + "editorIndentGuide.background": "#D3D3D3", + "editorIndentGuide.activeBackground": "#939393", + "editor.selectionHighlightBackground": "#ADD6FF80", + "editorSuggestWidget.background": "#F3F3F3", + "activityBarBadge.background": "#007ACC", + "sideBarTitle.foreground": "#6F6F6F", + "list.hoverBackground": "#E8E8E8", + "input.placeholderForeground": "#767676", + "settings.textInputBorder": "#CECECE", + "settings.numberInputBorder": "#CECECE", + "statusBarItem.remoteForeground": "#FFF", + "statusBarItem.remoteBackground": "#16825D" + } +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_editor.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_editor.json new file mode 100644 index 00000000..fb47c58a --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_editor.json @@ -0,0 +1,11 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Light (e2-editor)", + "include": "light_plus.json", + "tokenColors": [], + "colors": { + "activityBar.background": "#ececec", + "activityBar.activeBorder": "#000000", + "activityBar.foreground": "#000000" + } +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_plus.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_plus.json new file mode 100644 index 00000000..acd88640 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_plus.json @@ -0,0 +1,180 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Light+ (default light)", + "include": "light_vs.json", + "colors": {}, + "tokenColors": [ + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars", + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" + ], + "settings": { + "foreground": "#795E26" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "meta.return-type", + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.other.attribute", + "entity.name.scope-resolution", + "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", + "storage.type.error.go", + "storage.type.rune.go", + "storage.type.cs", + "storage.type.generic.cs", + "storage.type.modifier.cs", + "storage.type.variable.cs", + "storage.type.annotation.java", + "storage.type.generic.java", + "storage.type.java", + "storage.type.object.array.java", + "storage.type.primitive.array.java", + "storage.type.primitive.java", + "storage.type.token.java", + "storage.type.groovy", + "storage.type.annotation.groovy", + "storage.type.parameters.groovy", + "storage.type.generic.groovy", + "storage.type.object.array.groovy", + "storage.type.primitive.array.groovy", + "storage.type.primitive.groovy" + ], + "settings": { + "foreground": "#267f99" + } + }, + { + "name": "Types declaration and references, TS grammar specific", + "scope": [ + "meta.type.cast.expr", + "meta.type.new.expr", + "support.constant.math", + "support.constant.dom", + "support.constant.json", + "entity.other.inherited-class" + ], + "settings": { + "foreground": "#267f99" + } + }, + { + "name": "Control flow / Special keywords", + "scope": [ + "keyword.control", + "source.cpp keyword.operator.new", + "source.cpp keyword.operator.delete", + "keyword.other.using", + "keyword.other.operator", + "entity.name.operator" + ], + "settings": { + "foreground": "#AF00DB" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#001080" + } + }, + { + "name": "Object keys, TS grammar specific", + "scope": ["meta.object-literal.key"], + "settings": { + "foreground": "#001080" + } + }, + { + "name": "CSS property value", + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#0451a5" + } + }, + { + "name": "Regular expression groups", + "scope": [ + "punctuation.definition.group.regexp", + "punctuation.definition.group.assertion.regexp", + "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", + "keyword.operator.negation.regexp", + "support.other.parenthesis.regexp" + ], + "settings": { + "foreground": "#d16969" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#811f3f" + } + }, + { + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "#000000" + } + }, + { + "scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"], + "settings": { + "foreground": "#ff0000" + } + }, + { + "scope": "constant.character", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "constant.character.escape", + "settings": { + "foreground": "#ff0000" + } + }, + { + "scope": "entity.name.label", + "settings": { + "foreground": "#000000" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_vs.json b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_vs.json new file mode 100644 index 00000000..6f0d1558 --- /dev/null +++ b/packages/libro-cofine-textmate/src/data/monaco-themes/vscode/light_vs.json @@ -0,0 +1,380 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "Light (Visual Studio)", + "include": "light_defaults.json", + "colors": {}, + "tokenColors": [ + { + "scope": ["meta.embedded", "source.groovy.embedded"], + "settings": { + "foreground": "#000000ff" + } + }, + { + "scope": "emphasis", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "strong", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#000080" + } + }, + { + "scope": "comment", + "settings": { + "foreground": "#008000" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": [ + "constant.numeric", + "entity.name.operator.custom-literal.number", + "variable.other.enummember", + "keyword.operator.plus.exponent", + "keyword.operator.minus.exponent" + ], + "settings": { + "foreground": "#09885a" + } + }, + { + "scope": "constant.regexp", + "settings": { + "foreground": "#811f3f" + } + }, + { + "name": "css tags in selectors, xml tags", + "scope": "entity.name.tag", + "settings": { + "foreground": "#800000" + } + }, + { + "scope": "entity.name.selector", + "settings": { + "foreground": "#800000" + } + }, + { + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "#ff0000" + } + }, + { + "scope": [ + "entity.other.attribute-name.class.css", + "entity.other.attribute-name.class.mixin.css", + "entity.other.attribute-name.id.css", + "entity.other.attribute-name.parent-selector.css", + "entity.other.attribute-name.pseudo-class.css", + "entity.other.attribute-name.pseudo-element.css", + "source.css.less entity.other.attribute-name.id", + "entity.other.attribute-name.attribute.scss", + "entity.other.attribute-name.scss" + ], + "settings": { + "foreground": "#800000" + } + }, + { + "scope": "invalid", + "settings": { + "foreground": "#cd3131" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold", + "foreground": "#000080" + } + }, + { + "scope": "markup.heading", + "settings": { + "fontStyle": "bold", + "foreground": "#800000" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "markup.inserted", + "settings": { + "foreground": "#09885a" + } + }, + { + "scope": "markup.deleted", + "settings": { + "foreground": "#a31515" + } + }, + { + "scope": "markup.changed", + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": [ + "punctuation.definition.quote.begin.markdown", + "punctuation.definition.list.begin.markdown" + ], + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": "markup.inline.raw", + "settings": { + "foreground": "#800000" + } + }, + { + "name": "brackets of XML/HTML tags", + "scope": "punctuation.definition.tag", + "settings": { + "foreground": "#800000" + } + }, + { + "scope": ["meta.preprocessor", "entity.name.function.preprocessor"], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "meta.preprocessor.string", + "settings": { + "foreground": "#a31515" + } + }, + { + "scope": "meta.preprocessor.numeric", + "settings": { + "foreground": "#09885a" + } + }, + { + "scope": "meta.structure.dictionary.key.python", + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": "storage", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "storage.type", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": ["storage.modifier", "keyword.operator.noexcept"], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": [ + "string", + "entity.name.operator.custom-literal.string", + "meta.embedded.assembly" + ], + "settings": { + "foreground": "#a31515" + } + }, + { + "scope": [ + "string.comment.buffered.block.pug", + "string.quoted.pug", + "string.interpolated.pug", + "string.unquoted.plain.in.yaml", + "string.unquoted.plain.out.yaml", + "string.unquoted.block.yaml", + "string.quoted.single.yaml", + "string.quoted.double.xml", + "string.quoted.single.xml", + "string.unquoted.cdata.xml", + "string.quoted.double.html", + "string.quoted.single.html", + "string.unquoted.html", + "string.quoted.single.handlebars", + "string.quoted.double.handlebars" + ], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "string.regexp", + "settings": { + "foreground": "#811f3f" + } + }, + { + "name": "String interpolation", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end", + "punctuation.section.embedded" + ], + "settings": { + "foreground": "#0000ff" + } + }, + { + "name": "Reset JavaScript string interpolation expression", + "scope": ["meta.template.expression"], + "settings": { + "foreground": "#000000" + } + }, + { + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": [ + "support.type.vendored.property-name", + "support.type.property-name", + "variable.css", + "variable.scss", + "variable.other.less", + "source.coffee.embedded" + ], + "settings": { + "foreground": "#ff0000" + } + }, + { + "scope": ["support.type.property-name.json"], + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#000000" + } + }, + { + "scope": [ + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.alignof", + "keyword.operator.typeid", + "keyword.operator.alignas", + "keyword.operator.instanceof", + "keyword.operator.logical.python", + "keyword.operator.wordlike" + ], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": "keyword.other.unit", + "settings": { + "foreground": "#09885a" + } + }, + { + "scope": [ + "punctuation.section.embedded.begin.php", + "punctuation.section.embedded.end.php" + ], + "settings": { + "foreground": "#800000" + } + }, + { + "scope": "support.function.git-rebase", + "settings": { + "foreground": "#0451a5" + } + }, + { + "scope": "constant.sha.git-rebase", + "settings": { + "foreground": "#09885a" + } + }, + { + "name": "coloring of the Java import and package identifiers", + "scope": [ + "storage.modifier.import.java", + "variable.language.wildcard.java", + "storage.modifier.package.java" + ], + "settings": { + "foreground": "#000000" + } + }, + { + "name": "this.self", + "scope": "variable.language", + "settings": { + "foreground": "#0000ff" + } + } + ] +} diff --git a/packages/libro-cofine-textmate/src/global.d.ts b/packages/libro-cofine-textmate/src/global.d.ts new file mode 100644 index 00000000..d9e254f9 --- /dev/null +++ b/packages/libro-cofine-textmate/src/global.d.ts @@ -0,0 +1,7 @@ +declare module '@difizen/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +declare module '@difizen/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; +declare module '@difizen/monaco-editor-core/esm/vs/editor/common/languages/language'; +declare module '@difizen/monaco-editor-core/esm/vs/editor/common/languages'; +declare module '@difizen/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneLanguages'; +declare module '*.json'; +declare module 'vscode-oniguruma/release/onig.wasm'; diff --git a/packages/libro-cofine-textmate/src/index.ts b/packages/libro-cofine-textmate/src/index.ts new file mode 100644 index 00000000..095c56e8 --- /dev/null +++ b/packages/libro-cofine-textmate/src/index.ts @@ -0,0 +1,8 @@ +import { TextmateModule } from './monaco-module.js'; + +export * from './monaco-module.js'; +export * from './monaco-textmate-service.js'; +export * from './textmate-contribution.js'; +export * from './textmate-registry.js'; + +export default TextmateModule; diff --git a/packages/libro-cofine-textmate/src/monaco-grammar-registry.ts b/packages/libro-cofine-textmate/src/monaco-grammar-registry.ts new file mode 100644 index 00000000..e72a1a58 --- /dev/null +++ b/packages/libro-cofine-textmate/src/monaco-grammar-registry.ts @@ -0,0 +1,84 @@ +import { inject, singleton } from '@difizen/mana-app'; +import type { IOnigLib, IRawGrammar, IRawTheme } from 'vscode-textmate'; +import { Registry, parseRawGrammar } from 'vscode-textmate'; + +import { TextmateRegistry } from './textmate-registry.js'; + +export const isBasicWasmSupported = typeof (window as any).WebAssembly !== 'undefined'; +export const OnigurumaPromise = Symbol('OnigasmPromise'); +export type OnigurumaPromise = Promise; + +@singleton() +export class MonacoGrammarRegistry { + public registry?: Registry; + protected readonly textmateRegistry: TextmateRegistry; + protected readonly onigasmPromise: OnigurumaPromise; + constructor( + @inject(TextmateRegistry) textmateRegistry: TextmateRegistry, + @inject(OnigurumaPromise) onigasmPromise: OnigurumaPromise, + ) { + this.textmateRegistry = textmateRegistry; + this.onigasmPromise = onigasmPromise; + } + + getRegistry(theme: IRawTheme): Registry { + return new Registry({ + onigLib: this.onigasmPromise, + theme, + loadGrammar: async (scopeName: string) => { + const provider = this.textmateRegistry.getProvider(scopeName); + if (provider) { + const definition = await provider.getGrammarDefinition(); + let rawGrammar: IRawGrammar; + if (typeof definition.content === 'string') { + rawGrammar = parseRawGrammar( + definition.content, + definition.format === 'json' ? 'grammar.json' : 'grammar.plist', + ); + } else { + rawGrammar = definition.content as unknown as IRawGrammar; + } + return rawGrammar; + } + return undefined; + }, + getInjections: (scopeName: string) => { + const provider = this.textmateRegistry.getProvider(scopeName); + if (provider && provider.getInjections) { + return provider.getInjections(scopeName); + } + return []; + }, + }); + } + setupRegistry(theme: IRawTheme): void { + this.registry = new Registry({ + onigLib: this.onigasmPromise, + theme, + loadGrammar: async (scopeName: string) => { + const provider = this.textmateRegistry.getProvider(scopeName); + if (provider) { + const definition = await provider.getGrammarDefinition(); + let rawGrammar: IRawGrammar; + if (typeof definition.content === 'string') { + rawGrammar = parseRawGrammar( + definition.content, + definition.format === 'json' ? 'grammar.json' : 'grammar.plist', + ); + } else { + rawGrammar = definition.content as unknown as IRawGrammar; + } + return rawGrammar; + } + return undefined; + }, + getInjections: (scopeName: string) => { + const provider = this.textmateRegistry.getProvider(scopeName); + if (provider && provider.getInjections) { + return provider.getInjections(scopeName); + } + return []; + }, + }); + } +} diff --git a/packages/libro-cofine-textmate/src/monaco-module.ts b/packages/libro-cofine-textmate/src/monaco-module.ts new file mode 100644 index 00000000..793bb6e4 --- /dev/null +++ b/packages/libro-cofine-textmate/src/monaco-module.ts @@ -0,0 +1,73 @@ +/* eslint-disable func-names */ +/* eslint-disable global-require */ + +import { Module } from '@difizen/mana-app'; +import * as oniguruma from 'vscode-oniguruma'; +import * as onig from 'vscode-oniguruma/release/onig.wasm'; + +import { + isBasicWasmSupported, + MonacoGrammarRegistry, + OnigurumaPromise, +} from './monaco-grammar-registry.js'; +import { MonacoTextmateService } from './monaco-textmate-service.js'; +import { MonacoThemeRegistry } from './monaco-theme-registry.js'; +import { LanguageGrammarDefinitionContribution } from './textmate-contribution.js'; +import { TextmateRegistry } from './textmate-registry.js'; +import { TextmateThemeContribution } from './textmate-theme-contribution.js'; + +console.warn(onig); + +export function fetchOniguruma(): Promise { + return new Promise((resolve, reject) => { + const onigurumaPath = onig; // webpack doing its magic here + const request = new XMLHttpRequest(); + + request.onreadystatechange = function (): void { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('Could not fetch onigasm')); + } + } + }; + let onigurumaUrl = onigurumaPath; + if (typeof onigurumaPath !== 'string' && onigurumaPath.default) { + onigurumaUrl = onigurumaPath.default; + } + request.open('GET', onigurumaUrl, true); + request.responseType = 'arraybuffer'; + request.send(); + }); +} + +const vscodeOnigurumaLib = fetchOniguruma().then((buffer) => + oniguruma.loadWASM(buffer).then(() => { + return { + createOnigScanner(patterns: string[]) { + return new oniguruma.OnigScanner(patterns); + }, + createOnigString(s: string) { + return new oniguruma.OnigString(s); + }, + }; + }), +); + +export const TextmateModule = Module() + .register( + OnigurumaPromise, + { + useValue: isBasicWasmSupported + ? vscodeOnigurumaLib + : Promise.reject(new Error('wasm not supported')), + }, + MonacoTextmateService, + TextmateRegistry, + MonacoThemeRegistry, + TextmateThemeContribution, + MonacoGrammarRegistry, + ) + .contribution(LanguageGrammarDefinitionContribution); +export default TextmateModule; diff --git a/packages/libro-cofine-textmate/src/monaco-textmate-service.ts b/packages/libro-cofine-textmate/src/monaco-textmate-service.ts new file mode 100644 index 00000000..0f7420e1 --- /dev/null +++ b/packages/libro-cofine-textmate/src/monaco-textmate-service.ts @@ -0,0 +1,222 @@ +import { EditorHandlerContribution } from '@difizen/libro-cofine-editor-core'; +import type { Contribution } from '@difizen/mana-app'; +import { Disposable, DisposableCollection } from '@difizen/mana-app'; +import { contrib, inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; +import { TokenizationRegistry } from '@difizen/monaco-editor-core/esm/vs/editor/common/languages'; +import { ILanguageService } from '@difizen/monaco-editor-core/esm/vs/editor/common/languages/language'; +import { TokenizationSupportAdapter } from '@difizen/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneLanguages'; +import { StandaloneServices } from '@difizen/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { IStandaloneThemeService } from '@difizen/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; + +import { + isBasicWasmSupported, + MonacoGrammarRegistry, + OnigurumaPromise, +} from './monaco-grammar-registry.js'; +import { MonacoThemeRegistry } from './monaco-theme-registry.js'; +import { + getEncodedLanguageId, + LanguageGrammarDefinitionContribution, +} from './textmate-contribution.js'; +import { TextmateRegistry } from './textmate-registry.js'; +import type { TokenizerOption } from './textmate-tokenizer.js'; +import { createTextmateTokenizer } from './textmate-tokenizer.js'; + +@singleton({ contrib: EditorHandlerContribution }) +export class MonacoTextmateService implements EditorHandlerContribution { + protected readonly tokenizerOption: TokenizerOption = { + lineLimit: 400, + }; + + protected readonly _activatedLanguages = new Set(); + protected readonly grammarProviders: Contribution.Provider; + protected readonly textmateRegistry: TextmateRegistry; + protected readonly grammarRegistry: MonacoGrammarRegistry; + protected readonly onigasmPromise: OnigurumaPromise; + protected readonly monacoThemeRegistry: MonacoThemeRegistry; + constructor( + @contrib(LanguageGrammarDefinitionContribution) + grammarProviders: Contribution.Provider, + @inject(TextmateRegistry) textmateRegistry: TextmateRegistry, + @inject(MonacoGrammarRegistry) grammarRegistry: MonacoGrammarRegistry, + @inject(OnigurumaPromise) onigasmPromise: OnigurumaPromise, + @inject(MonacoThemeRegistry) monacoThemeRegistry: MonacoThemeRegistry, + ) { + this.grammarProviders = grammarProviders; + this.textmateRegistry = textmateRegistry; + this.grammarRegistry = grammarRegistry; + this.onigasmPromise = onigasmPromise; + this.monacoThemeRegistry = monacoThemeRegistry; + } + beforeCreate() { + this.initialize(); + } + afterCreate() { + // + } + canHandle() { + return true; + } + dispose() { + // + } + + initialize(): void { + if (!isBasicWasmSupported) { + console.warn('Textmate support deactivated because WebAssembly is not detected.'); + return; + } + + for (const grammarProvider of this.grammarProviders.getContributions({ + cache: false, + })) { + try { + if (grammarProvider._finishRegisterTextmateLanguage) { + return; + } + grammarProvider.registerTextmateLanguage(this.textmateRegistry); + grammarProvider._finishRegisterTextmateLanguage = true; + } catch (err) { + console.error(err); + } + } + const theme = this.monacoThemeRegistry.getThemeData(this.currentEditorTheme); + if (!theme) { + return; + } + this.grammarRegistry.setupRegistry(theme); + + for (const id of this.textmateRegistry.languages) { + this.activateLanguage(id); + } + } + + protected readonly toDisposeOnUpdateTheme = new DisposableCollection(); + + protected updateTheme(): void { + this.toDisposeOnUpdateTheme.dispose(); + + const { currentEditorTheme } = this; + document.body.classList.add(currentEditorTheme); + this.toDisposeOnUpdateTheme.push( + Disposable.create(() => document.body.classList.remove(currentEditorTheme)), + ); + + // first update registry to run tokenization with the proper theme + const theme = this.monacoThemeRegistry.getThemeData(currentEditorTheme); + if (this.grammarRegistry.registry && theme) { + this.grammarRegistry.registry.setTheme(theme); + } + + // then trigger tokenization by setting monaco theme + monaco.editor.setTheme(currentEditorTheme); + } + + protected get currentEditorTheme(): string { + return MonacoThemeRegistry.DARK_DEFAULT_THEME; + } + + activateLanguage(language: string): Disposable { + const toDispose = new DisposableCollection( + Disposable.create(() => { + /* mark as not disposed */ + }), + ); + toDispose.push( + this.waitForLanguage(language, () => + this.doActivateLanguage(language, toDispose), + ), + ); + return toDispose; + } + + protected async doActivateLanguage( + languageId: string, + toDispose: DisposableCollection, + ): Promise { + if (this._activatedLanguages.has(languageId)) { + return; + } + this._activatedLanguages.add(languageId); + toDispose.push( + Disposable.create(() => this._activatedLanguages.delete(languageId)), + ); + + const scopeName = this.textmateRegistry.getScope(languageId); + if (!scopeName) { + return; + } + const provider = this.textmateRegistry.getProvider(scopeName); + if (!provider) { + return; + } + + const configuration = this.textmateRegistry.getGrammarConfiguration(languageId); + const initialLanguage = getEncodedLanguageId(languageId); + + await this.onigasmPromise; + if (toDispose.disposed) { + return; + } + if (!this.grammarRegistry.registry) { + return; + } + try { + const grammar = await this.grammarRegistry.registry.loadGrammarWithConfiguration( + scopeName, + initialLanguage, + configuration, + ); + if (toDispose.disposed) { + return; + } + if (!grammar) { + throw new Error( + `no grammar for ${scopeName}, ${initialLanguage}, ${JSON.stringify( + configuration, + )}`, + ); + } + const options = configuration.tokenizerOption + ? configuration.tokenizerOption + : this.tokenizerOption; + const tokenizer = createTextmateTokenizer(grammar, options); + toDispose.push(monaco.languages.setTokensProvider(languageId, tokenizer)); + const support = TokenizationRegistry.get(languageId); + const themeService = StandaloneServices.get(IStandaloneThemeService); + const adapter = new TokenizationSupportAdapter( + themeService, + { + language: languageId, + id: StandaloneServices.get( + ILanguageService, + )._registry.languageIdCodec._languageToLanguageId.get(languageId), + }, + tokenizer, + ); + support!.tokenize = adapter.tokenize.bind(adapter); + } catch (error) { + console.warn('No grammar for this language id', languageId, error); + } + } + + protected waitForLanguage( + language: string, + cb: () => { + // + }, + ): Disposable { + const modeService = StandaloneServices.get(ILanguageService); + for (const modeId of Object.keys(modeService._instantiatedModes || {})) { + const mode = modeService._instantiatedModes[modeId]; + if (mode.getId() === language) { + cb(); + return Disposable.create(() => { + // + }); + } + } + return monaco.languages.onLanguage(language, cb); + } +} diff --git a/packages/libro-cofine-textmate/src/monaco-theme-registry.ts b/packages/libro-cofine-textmate/src/monaco-theme-registry.ts new file mode 100644 index 00000000..fc8e2f85 --- /dev/null +++ b/packages/libro-cofine-textmate/src/monaco-theme-registry.ts @@ -0,0 +1,178 @@ +import type { + MixedTheme, + ITextmateThemeSetting, +} from '@difizen/libro-cofine-editor-core'; +import { MixedThemeRegistry } from '@difizen/libro-cofine-editor-core'; +import type { Color } from '@difizen/mana-app'; +import { inject, singleton } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; +import { StandaloneServices } from '@difizen/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { IStandaloneThemeService } from '@difizen/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme'; + +import { MonacoGrammarRegistry } from './monaco-grammar-registry.js'; + +export interface MixStandaloneTheme { + themeData: MixedTheme; +} + +@singleton({ contrib: MixedThemeRegistry }) +export class MonacoThemeRegistry implements MixedThemeRegistry { + protected readonly grammarRegistry: MonacoGrammarRegistry; + constructor(@inject(MonacoGrammarRegistry) grammarRegistry: MonacoGrammarRegistry) { + this.grammarRegistry = grammarRegistry; + } + protected registedTheme: string[] = []; + getThemeData(): MixedTheme; + getThemeData(name: string): MixedTheme | undefined; + getThemeData(name?: string): MixedTheme | undefined { + const theme = this.doGetTheme(name); + return theme && theme.themeData; + } + + getTheme(): MixStandaloneTheme; + getTheme(name: string): MixStandaloneTheme | undefined; + getTheme(name?: string): MixStandaloneTheme | undefined { + return this.doGetTheme(name); + } + + protected doGetTheme(name: string | undefined): MixStandaloneTheme | undefined { + const standaloneThemeService = StandaloneServices.get(IStandaloneThemeService); + const theme = !name + ? standaloneThemeService.getTheme() + : standaloneThemeService._knownThemes.get(name); + return theme as MixStandaloneTheme | undefined; + } + + setTheme(name: string, data: MixedTheme): void { + // monaco auto refreshes a theme with new data + monaco.editor.defineTheme(name, data); + } + + registerThemes( + themeOptions: Record, + setTheme: (name: string, data: monaco.editor.IStandaloneThemeData) => void, + ): void { + Object.keys(themeOptions).forEach((key) => { + this.doRegisterThemes(themeOptions[key], themeOptions, setTheme); + }); + } + doRegisterThemes( + option: ITextmateThemeSetting, + themeOptions: Record, + setTheme: (name: string, data: monaco.editor.IStandaloneThemeData) => void, + ): MixedTheme { + const result: MixedTheme = { + name: option.name, + base: option.base || 'vs', + inherit: true, + colors: {}, + rules: [], + settings: [], + }; + if (typeof option.include !== 'undefined') { + const parentOption = themeOptions[option.include]; + if (!parentOption || this.registedTheme.includes(parentOption.name)) { + console.error(`Couldn't resolve includes theme ${option.include}.`); + } else { + const parentTheme = this.doRegisterThemes(parentOption, themeOptions, setTheme); + Object.assign(result.colors, parentTheme.colors); + result.rules.push(...parentTheme.rules); + result.settings.push(...parentTheme.settings); + } + } + const { tokenColors } = option; + if (Array.isArray(tokenColors)) { + for (const tokenColor of tokenColors) { + if (tokenColor.scope && tokenColor.settings) { + result.settings.push({ + scope: tokenColor.scope, + settings: { + foreground: this.normalizeColor(tokenColor.settings.foreground), + background: this.normalizeColor(tokenColor.settings.background), + fontStyle: tokenColor.settings.fontStyle, + }, + }); + } + } + } + if (option.colors) { + Object.assign(result.colors, option.colors); + result.encodedTokensColors = Object.keys(result.colors).map( + (key) => result.colors[key], + ); + } + if (option.name && option.base) { + for (const setting of result.settings) { + this.transform(setting, (rule) => result.rules.push(rule)); + } + + // the default rule (scope empty) is always the first rule. Ignore all other default rules. + const defaultTheme = StandaloneServices.get( + IStandaloneThemeService, + )._knownThemes.get(result.base)!; + const foreground = + result.colors['editor.foreground'] || + defaultTheme.getColor('editor.foreground'); + const background = + result.colors['editor.background'] || + defaultTheme.getColor('editor.background'); + result.settings.unshift({ + settings: { + foreground: this.normalizeColor(foreground), + background: this.normalizeColor(background), + }, + }); + + const reg = this.grammarRegistry.getRegistry(result); + reg.setTheme(result); + result.encodedTokensColors = reg.getColorMap(); + // index 0 has to be set to null as it is 'undefined' by default, but monaco code expects it to be null + if (result.encodedTokensColors) { + result.encodedTokensColors[0] = null!; + } + setTheme(option.name, result); + } + return result; + } + + protected transform( + tokenColor: any, + acceptor: (rule: monaco.editor.ITokenThemeRule) => void, + ): void { + if (typeof tokenColor.scope === 'undefined') { + tokenColor.scope = ['']; + } else if (typeof tokenColor.scope === 'string') { + tokenColor.scope = tokenColor.scope + .split(',') + .map((scope: string) => scope.trim()); + } + + for (const scope of tokenColor.scope) { + acceptor({ + ...tokenColor.settings, + token: scope, + }); + } + } + + protected normalizeColor(color: string | Color | undefined): string | undefined { + if (!color) { + return undefined; + } + const normalized = String(color).replace(/^#/, '').slice(0, 6); + if (normalized.length < 6 || !normalized.match(/^[0-9A-Fa-f]{6}$/)) { + // ignoring not normalized colors to avoid breaking token color indexes between monaco and vscode-textmate + console.error( + `Color '${normalized}' is NOT normalized, it must have 6 positions.`, + ); + return undefined; + } + return `#${normalized}`; + } +} + +export namespace MonacoThemeRegistry { + export const DARK_DEFAULT_THEME = 'e2-dark'; + export const LIGHT_DEFAULT_THEME = 'e2-light'; + export const HC_DEFAULT_THEME = 'e2-hc'; +} diff --git a/packages/libro-cofine-textmate/src/monaco-theme-types.ts b/packages/libro-cofine-textmate/src/monaco-theme-types.ts new file mode 100644 index 00000000..a359fc8a --- /dev/null +++ b/packages/libro-cofine-textmate/src/monaco-theme-types.ts @@ -0,0 +1,5 @@ +import type * as monaco from '@difizen/monaco-editor-core'; + +export interface MonacoTheme extends monaco.editor.IStandaloneThemeData { + name: string; +} diff --git a/packages/libro-cofine-textmate/src/textmate-contribution.ts b/packages/libro-cofine-textmate/src/textmate-contribution.ts new file mode 100644 index 00000000..f4eb0890 --- /dev/null +++ b/packages/libro-cofine-textmate/src/textmate-contribution.ts @@ -0,0 +1,15 @@ +import { Syringe } from '@difizen/mana-app'; +import * as monaco from '@difizen/monaco-editor-core'; + +import type { TextmateRegistry } from './textmate-registry.js'; + +export const LanguageGrammarDefinitionContribution = Syringe.defineToken( + 'LanguageGrammarDefinitionContribution', +); +export interface LanguageGrammarDefinitionContribution { + registerTextmateLanguage: (registry: TextmateRegistry) => void; + _finishRegisterTextmateLanguage?: boolean; +} +export function getEncodedLanguageId(languageId: string): number { + return monaco.languages.getEncodedLanguageId(languageId); +} diff --git a/packages/libro-cofine-textmate/src/textmate-registry.ts b/packages/libro-cofine-textmate/src/textmate-registry.ts new file mode 100644 index 00000000..e04f9657 --- /dev/null +++ b/packages/libro-cofine-textmate/src/textmate-registry.ts @@ -0,0 +1,134 @@ +/* eslint-disable no-console */ +/* eslint-disable no-restricted-syntax */ + +import { Disposable } from '@difizen/mana-app'; +import { singleton } from '@difizen/mana-app'; +import type { IGrammarConfiguration } from 'vscode-textmate'; + +import type { TokenizerOption } from './textmate-tokenizer.js'; + +export interface TextmateGrammarConfiguration extends IGrammarConfiguration { + /** + * Optional options to further refine the tokenization of the grammar. + */ + readonly tokenizerOption?: TokenizerOption; +} + +export interface GrammarDefinitionProvider { + getGrammarDefinition: () => Promise; + getInjections?: (scopeName: string) => string[]; +} + +export interface GrammarDefinition { + format: 'json' | 'plist'; + content: Record | string; + location?: string; +} + +@singleton() +export class TextmateRegistry { + protected readonly scopeToProvider = new Map(); + protected readonly languageToConfig = new Map< + string, + TextmateGrammarConfiguration[] + >(); + protected readonly languageIdToScope = new Map(); + + get languages(): IterableIterator { + return this.languageIdToScope.keys(); + } + + registerTextmateGrammarScope( + scope: string, + provider: GrammarDefinitionProvider, + ): Disposable { + const providers = this.scopeToProvider.get(scope) || []; + const existingProvider = providers[0]; + if (existingProvider) { + Promise.all([ + existingProvider.getGrammarDefinition(), + provider.getGrammarDefinition(), + ]) + .then(([a, b]) => { + if (a.location !== b.location || (!a.location && !b.location)) { + console.warn( + `a registered grammar provider for '${scope}' scope is overridden`, + ); + } + return; + }) + .catch(console.error); + } + providers.unshift(provider); + this.scopeToProvider.set(scope, providers); + return Disposable.create(() => { + const index = providers.indexOf(provider); + if (index !== -1) { + providers.splice(index, 1); + } + }); + } + + getProvider(scope: string): GrammarDefinitionProvider | undefined { + const providers = this.scopeToProvider.get(scope); + return providers && providers[0]; + } + + mapLanguageIdToTextmateGrammar(languageId: string, scope: string): Disposable { + const scopes = this.languageIdToScope.get(languageId) || []; + const existingScope = scopes[0]; + if (typeof existingScope === 'string') { + console.warn( + `'${languageId}' language is remapped from '${existingScope}' to '${scope}' scope`, + ); + } + scopes.unshift(scope); + this.languageIdToScope.set(languageId, scopes); + return Disposable.create(() => { + const index = scopes.indexOf(scope); + if (index !== -1) { + scopes.splice(index, 1); + } + }); + } + + getScope(languageId: string): string | undefined { + const scopes = this.languageIdToScope.get(languageId); + return scopes && scopes[0]; + } + + getLanguageId(scope: string): string | undefined { + for (const languageId of this.languageIdToScope.keys()) { + if (this.getScope(languageId) === scope) { + return languageId; + } + } + return undefined; + } + + registerGrammarConfiguration( + languageId: string, + config: TextmateGrammarConfiguration, + ): Disposable { + const configs = this.languageToConfig.get(languageId) || []; + const existingConfig = configs[0]; + if (existingConfig) { + console.warn( + `a registered grammar configuration for '${languageId}' language is overridden`, + ); + } + configs.unshift(config); + this.languageToConfig.set(languageId, configs); + return Disposable.create(() => { + const index = configs.indexOf(config); + if (index !== -1) { + configs.splice(index, 1); + } + }); + } + + getGrammarConfiguration(languageId: string): TextmateGrammarConfiguration { + const configs = this.languageToConfig.get(languageId); + return (configs && configs[0]) || {}; + } +} diff --git a/packages/libro-cofine-textmate/src/textmate-theme-contribution.ts b/packages/libro-cofine-textmate/src/textmate-theme-contribution.ts new file mode 100644 index 00000000..50b5a5b0 --- /dev/null +++ b/packages/libro-cofine-textmate/src/textmate-theme-contribution.ts @@ -0,0 +1,39 @@ +/* eslint-disable global-require */ +import type { ThemeRegistry } from '@difizen/libro-cofine-editor-core'; +import { ThemeContribution } from '@difizen/libro-cofine-editor-core'; +import { singleton } from '@difizen/mana-app'; + +import darkDefault from './data/monaco-themes/vscode/dark_defaults.json'; +import darkEditor from './data/monaco-themes/vscode/dark_editor.json'; +import darkPlus from './data/monaco-themes/vscode/dark_plus.json'; +import darkVS from './data/monaco-themes/vscode/dark_vs.json'; +import HCBlack from './data/monaco-themes/vscode/hc_black.json'; +import HCBlackDefault from './data/monaco-themes/vscode/hc_black_defaults.json'; +import HCEditor from './data/monaco-themes/vscode/hc_editor.json'; +import lightDefault from './data/monaco-themes/vscode/light_defaults.json'; +import lightEditor from './data/monaco-themes/vscode/light_editor.json'; +import lightPlus from './data/monaco-themes/vscode/light_plus.json'; +import lightVS from './data/monaco-themes/vscode/light_vs.json'; + +@singleton({ contrib: ThemeContribution }) +export class TextmateThemeContribution implements ThemeContribution { + registerItem(registry: ThemeRegistry): void { + if (!registry.mixedThemeEnable) { + console.warn('cannot register textmate themes'); + return; + } + registry.registerMixedTheme(darkDefault, 'dark_defaults.json'); + registry.registerMixedTheme(darkVS, 'dark_vs.json'); + registry.registerMixedTheme(darkPlus, 'dark_plus.json'); + registry.registerMixedTheme(darkEditor, 'e2-dark', 'vs-dark'); + + registry.registerMixedTheme(lightDefault, 'light_defaults.json'); + registry.registerMixedTheme(lightVS, 'light_vs.json'); + registry.registerMixedTheme(lightPlus, 'light_plus.json'); + registry.registerMixedTheme(lightEditor, 'e2-light', 'vs'); + + registry.registerMixedTheme(HCBlackDefault, 'hc_black_defaults.json'); + registry.registerMixedTheme(HCBlack, 'hc_black.json'); + registry.registerMixedTheme(HCEditor, 'e2-hc', 'hc-black'); + } +} diff --git a/packages/libro-cofine-textmate/src/textmate-tokenizer.ts b/packages/libro-cofine-textmate/src/textmate-tokenizer.ts new file mode 100644 index 00000000..cf32c87d --- /dev/null +++ b/packages/libro-cofine-textmate/src/textmate-tokenizer.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import type monaco from '@difizen/monaco-editor-core'; +import type { IGrammar, StackElement } from 'vscode-textmate'; +import { INITIAL } from 'vscode-textmate'; + +export class TokenizerState implements monaco.languages.IState { + constructor(public readonly ruleStack: StackElement) {} + + clone(): monaco.languages.IState { + return new TokenizerState(this.ruleStack); + } + + equals(other: monaco.languages.IState): boolean { + return ( + other instanceof TokenizerState && + (other === this || other.ruleStack === this.ruleStack) + ); + } +} + +/** + * Options for the TextMate tokenizer. + */ +export interface TokenizerOption { + /** + * Maximum line length that will be handled by the TextMate tokenizer. If the length of the actual line exceeds this + * limit, the tokenizer terminates and the tokenization of any subsequent lines might be broken. + * + * If the `lineLimit` is not defined, it means, there are no line length limits. Otherwise, it must be a positive + * integer or an error will be thrown. + */ + lineLimit?: number; +} + +export namespace TokenizerOption { + /** + * The default TextMate tokenizer option. + * + * @deprecated Use the current value of `editor.maxTokenizationLineLength` preference instead. + */ + export const DEFAULT: TokenizerOption = { + lineLimit: 400, + }; +} + +export function createTextmateTokenizer( + grammar: IGrammar, + options: TokenizerOption, +): monaco.languages.EncodedTokensProvider & monaco.languages.TokensProvider { + if ( + options.lineLimit !== undefined && + (options.lineLimit <= 0 || !Number.isInteger(options.lineLimit)) + ) { + throw new Error( + `The 'lineLimit' must be a positive integer. It was ${options.lineLimit}.`, + ); + } + return { + getInitialState: () => new TokenizerState(INITIAL), + tokenizeEncoded( + line: string, + state: TokenizerState, + ): monaco.languages.IEncodedLineTokens { + let processedLine = line; + if (options.lineLimit !== undefined && line.length > options.lineLimit) { + // Line is too long to be tokenized + processedLine = line.substr(0, options.lineLimit); + } + const result = grammar.tokenizeLine2(processedLine, state.ruleStack); + return { + endState: new TokenizerState(result.ruleStack), + tokens: result.tokens, + }; + }, + tokenize(line: string, state: TokenizerState): monaco.languages.ILineTokens { + let processedLine = line; + if (options.lineLimit !== undefined && line.length > options.lineLimit) { + // Line is too long to be tokenized + processedLine = line.substr(0, options.lineLimit); + } + const result = grammar.tokenizeLine(processedLine, state.ruleStack); + return { + endState: new TokenizerState(result.ruleStack), + tokens: result.tokens.map((t) => ({ + startIndex: t.startIndex, + scopes: t.scopes.reverse().join(' '), + })), + }; + }, + }; +} diff --git a/packages/libro-cofine-textmate/tsconfig.json b/packages/libro-cofine-textmate/tsconfig.json new file mode 100644 index 00000000..a18e7b25 --- /dev/null +++ b/packages/libro-cofine-textmate/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src", "typings"] +} diff --git a/packages/libro-common/package.json b/packages/libro-common/package.json index a3c9d6cd..36793ae0 100644 --- a/packages/libro-common/package.json +++ b/packages/libro-common/package.json @@ -45,7 +45,7 @@ "lint:tsc": "tsc --noEmit" }, "dependencies": { - "@difizen/mana-common": "latest", + "@difizen/mana-app": "latest", "path-browserify": "^1.0.0", "sanitize-html": "^2.7.2", "url-parse": "^1.5.10" diff --git a/packages/libro-common/src/iter.ts b/packages/libro-common/src/iter.ts index 9496cc70..4c9bd236 100644 --- a/packages/libro-common/src/iter.ts +++ b/packages/libro-common/src/iter.ts @@ -109,8 +109,8 @@ export class ArrayIterator implements IIterator { return this._source[this._index++]; } - private _index = 0; - private _source: ArrayLike; + protected _index = 0; + protected _source: ArrayLike; } /** diff --git a/packages/libro-common/src/polling/poll.ts b/packages/libro-common/src/polling/poll.ts index 605ce426..038eb999 100644 --- a/packages/libro-common/src/polling/poll.ts +++ b/packages/libro-common/src/polling/poll.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Event } from '@difizen/mana-common'; -import { Emitter, Deferred } from '@difizen/mana-common'; +import type { Event } from '@difizen/mana-app'; +import { Emitter, Deferred } from '@difizen/mana-app'; import { deepEqual } from '../json.js'; diff --git a/packages/libro-common/src/polling/protocol.ts b/packages/libro-common/src/polling/protocol.ts index d7a41644..59126906 100644 --- a/packages/libro-common/src/polling/protocol.ts +++ b/packages/libro-common/src/polling/protocol.ts @@ -1,4 +1,4 @@ -import type { Event } from '@difizen/mana-common'; +import type { Event } from '@difizen/mana-app'; /** * A readonly poll that calls an asynchronous function with each tick. diff --git a/packages/libro-common/src/sanitizer.ts b/packages/libro-common/src/sanitizer.ts index 2c5bf459..b5ef7dcf 100644 --- a/packages/libro-common/src/sanitizer.ts +++ b/packages/libro-common/src/sanitizer.ts @@ -8,7 +8,7 @@ class CssProp { /* * Numeric base expressions used to help build more complex regular expressions */ - private static readonly N = { + protected static readonly N = { integer: `[+-]?[0-9]+`, integer_pos: `[+]?[0-9]+`, integer_zero_ff: `([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])`, @@ -21,7 +21,7 @@ class CssProp { /* * Base expressions of common CSS syntax elements */ - private static readonly B = { + protected static readonly B = { angle: `(${CssProp.N.number}(deg|rad|grad|turn)|0)`, frequency: `${CssProp.N.number}(Hz|kHz)`, ident: String.raw`-?([_a-z]|[\xA0-\xFF]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])([_a-z0-9-]|[\xA0-\xFF]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*`, @@ -40,7 +40,7 @@ class CssProp { /* * Atomic (i.e. not dependant on other regular expressions) sub RegEx segments */ - private static readonly A = { + protected static readonly A = { absolute_size: `xx-small|x-small|small|medium|large|x-large|xx-large`, attachment: `scroll|fixed|local`, bg_origin: `border-box|padding-box|content-box`, @@ -62,7 +62,7 @@ class CssProp { /* * Color definition sub expressions */ - private static readonly _COLOR = { + protected static readonly _COLOR = { hex: `\\#(0x)?[0-9a-f]+`, name: `aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|transparent|violet|wheat|white|whitesmoke|yellow|yellowgreen`, rgb: String.raw`rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)`, @@ -72,7 +72,7 @@ class CssProp { /* * Compound (i.e. dependant on other (sub) regular expressions) sub RegEx segments */ - private static readonly _C = { + protected static readonly _C = { alpha: `${CssProp.N.integer_zero_ff}|${CssProp.N.number_zero_one}|${CssProp.B.percentage_zero_hundred}`, alphavalue: CssProp.N.number_zero_one, bg_position: `((${CssProp.B.len_or_perc}|left|center|right|top|bottom)\\s*){1,4}`, @@ -96,29 +96,29 @@ class CssProp { top: `${CssProp.B.length}|auto`, }; - private static readonly _C1 = { + protected static readonly _C1 = { image_list: `image\\(\\s*(${CssProp.B.url})*\\s*(${CssProp.B.url}|${CssProp._C.color})\\s*\\)`, linear_color_stop: `(${CssProp._C.color})(\\s*${CssProp._C.color_stop_length})?`, // eslint-disable-next-line no-useless-escape shadow: `((${CssProp._C.color})\\s+((${CssProp.B.length})\\s*){2,4}(\s+inset)?)|((inset\\s+)?((${CssProp.B.length})\\s*){2,4}\\s*(${CssProp._C.color})?)`, }; - private static readonly _C2 = { + protected static readonly _C2 = { color_stop_list: `((${CssProp._C1.linear_color_stop})(\\s*(${CssProp._C.linear_color_hint}))?\\s*,\\s*)+(${CssProp._C1.linear_color_stop})`, shape: `rect\\(\\s*(${CssProp._C.top})\\s*,\\s*(${CssProp._C.right})\\s*,\\s*(${CssProp._C.bottom})\\s*,\\s*(${CssProp._C.left})\\s*\\)`, }; - private static readonly _C3 = { + protected static readonly _C3 = { linear_gradient: `linear-gradient\\((((${CssProp.B.angle})|to\\s+(${CssProp.A.side_or_corner}))\\s*,\\s*)?\\s*(${CssProp._C2.color_stop_list})\\s*\\)`, radial_gradient: `radial-gradient\\(((((${CssProp.A.ending_shape})|(${CssProp._C.size}))\\s*)*\\s*(at\\s+${CssProp._C.position})?\\s*,\\s*)?\\s*(${CssProp._C2.color_stop_list})\\s*\\)`, }; - private static readonly _C4 = { + protected static readonly _C4 = { image: `${CssProp.B.url}|${CssProp._C3.linear_gradient}|${CssProp._C3.radial_gradient}|${CssProp._C1.image_list}`, bg_image: `(${CssProp.B.url}|${CssProp._C3.linear_gradient}|${CssProp._C3.radial_gradient}|${CssProp._C1.image_list})|none`, }; - private static readonly C = { + protected static readonly C = { ...CssProp._C, ...CssProp._C1, ...CssProp._C2, @@ -129,7 +129,7 @@ class CssProp { /* * Property value regular expressions not dependant on other sub expressions */ - private static readonly AP = { + protected static readonly AP = { border_collapse: `collapse|separate`, box: `normal|none|contents`, box_sizing: `content-box|padding-box|border-box`, @@ -170,7 +170,7 @@ class CssProp { /* * Compound propertiy value regular expressions (i.e. dependant on other sub expressions) */ - private static readonly _CP = { + protected static readonly _CP = { background_attachment: `${CssProp.A.attachment}(,\\s*${CssProp.A.attachment})*`, background_color: CssProp.C.color, background_origin: `${CssProp.A.box}(,\\s*${CssProp.A.box})*`, @@ -268,11 +268,11 @@ class CssProp { min_width: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|auto`, }; - private static readonly _CP1 = { + protected static readonly _CP1 = { font: `(((((${CssProp.AP.font_style}|${CssProp.AP.font_variant}|${CssProp.AP.font_weight})\\s*){1,3})?\\s*(${CssProp._CP.font_size})\\s*(\\/\\s*(${CssProp._CP.line_height}))?\\s+(${CssProp._CP.font_family}))|caption|icon|menu|message-box|small-caption|status-bar)`, }; - private static readonly CP = { ...CssProp._CP, ...CssProp._CP1 }; + protected static readonly CP = { ...CssProp._CP, ...CssProp._CP1 }; // CSS Property value validation regular expressions for use with sanitize-html @@ -456,7 +456,7 @@ export class Sanitizer implements ISanitizer { return sanitize(dirty, { ...this._options, ...(options || {}) }); } - private _options: sanitize.IOptions = { + protected _options: sanitize.IOptions = { // HTML tags that are allowed to be used. Tags were extracted from Google Caja allowedTags: [ 'a', diff --git a/packages/libro-common/src/url.ts b/packages/libro-common/src/url.ts index fe1a099f..3e4d14e3 100644 --- a/packages/libro-common/src/url.ts +++ b/packages/libro-common/src/url.ts @@ -1,4 +1,4 @@ -import { isWeb } from '@difizen/mana-common'; +import { isWeb } from '@difizen/mana-app'; import { posix } from 'path-browserify'; import urlparse from 'url-parse'; diff --git a/packages/libro-core/package.json b/packages/libro-core/package.json index 9dacadb0..10252d40 100644 --- a/packages/libro-core/package.json +++ b/packages/libro-core/package.json @@ -55,6 +55,7 @@ "@difizen/mana-l10n": "latest", "@difizen/mana-react": "latest", "classnames": "^2.3.2", + "dayjs": "^1.11.10", "dnd-core": "^16.0.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/packages/libro-core/src/command/libro-command-contribution.ts b/packages/libro-core/src/command/libro-command-contribution.ts index 59a0548f..4d8087d1 100644 --- a/packages/libro-core/src/command/libro-command-contribution.ts +++ b/packages/libro-core/src/command/libro-command-contribution.ts @@ -47,7 +47,7 @@ export class LibroCommandContribution implements CommandContribution { NotebookCommands['EnterCommandMode'], { execute: () => { - this.libroService.active?.enterCommandMode(true); + return this.libroService.active?.enterCommandMode(true); }, }, ); @@ -169,7 +169,7 @@ export class LibroCommandContribution implements CommandContribution { return false; } if (libro.model.interrupt) { - libro.model.interrupt(); + return libro.model.interrupt(); } }, isEnabled: (cell, libro) => { @@ -203,7 +203,7 @@ export class LibroCommandContribution implements CommandContribution { if (!libro || !(libro instanceof LibroView)) { return false; } - this.modalService.openModal(RestartClearOutputModal, libro); + return this.modalService.openModal(RestartClearOutputModal, libro); }, isVisible: (cell, libro, path) => { if (!libro || !(libro instanceof LibroView)) { @@ -225,7 +225,7 @@ export class LibroCommandContribution implements CommandContribution { if (!libro || !(libro instanceof LibroView)) { return false; } - this.modalService.openModal(ShutdownModal, libro); + return this.modalService.openModal(ShutdownModal, libro); }, isVisible: () => false, // isVisible: (cell, libro, path) => { diff --git a/packages/libro-core/src/components/dnd-cell-item-render.tsx b/packages/libro-core/src/components/dnd-cell-item-render.tsx index 9f8daf35..d42806d3 100644 --- a/packages/libro-core/src/components/dnd-cell-item-render.tsx +++ b/packages/libro-core/src/components/dnd-cell-item-render.tsx @@ -192,9 +192,14 @@ const CellOutput: React.FC<{ cell: CellView }> = forwardRef(function CellOutput( const isExecutingRef = useRef(null); + let executing = false; + if (ExecutableCellModel.is(cell.model)) { + executing = cell.model.executing; + } + useLayoutEffect(() => { - isExecutingRef.current = !!cell.model.executing; - }, [cell.model.executing]); + isExecutingRef.current = !!executing; + }, [executing]); useLayoutEffect(() => { if (!outputRef || !isExecutingRef || !outputRef?.current) { diff --git a/packages/libro-core/src/libro-protocol.ts b/packages/libro-core/src/libro-protocol.ts index b6c76200..f20c9019 100644 --- a/packages/libro-core/src/libro-protocol.ts +++ b/packages/libro-core/src/libro-protocol.ts @@ -126,8 +126,6 @@ export interface BaseNotebookModel { canRedo?: boolean; - mouseMode?: MouseMode; - /** * The metadata associated with the notebook. */ @@ -333,6 +331,7 @@ export const DragAreaKey = Symbol('DragAreaKey'); export type MouseMode = 'multipleSelection' | 'mouseDown' | 'drag'; export interface DndListModel { + mouseMode?: MouseMode; active?: CellView | undefined; hover?: CellView | undefined; selectCell: (cell?: CellView) => void; diff --git a/packages/libro-core/src/libro-view.tsx b/packages/libro-core/src/libro-view.tsx index 8af355c0..ee064554 100644 --- a/packages/libro-core/src/libro-view.tsx +++ b/packages/libro-core/src/libro-view.tsx @@ -553,7 +553,7 @@ export class LibroView extends BaseView implements NotebookView { return false; } - Promise.all( + return Promise.all( cells.map((cell) => { if (ExecutableCellModel.is(cell.model)) { return this.executeCellRun(cell); diff --git a/packages/libro-core/src/settings/setting-editor/configuration-panel-view.tsx b/packages/libro-core/src/settings/setting-editor/configuration-panel-view.tsx index 42838445..882ebd35 100644 --- a/packages/libro-core/src/settings/setting-editor/configuration-panel-view.tsx +++ b/packages/libro-core/src/settings/setting-editor/configuration-panel-view.tsx @@ -10,8 +10,7 @@ import { useConfigurationValue, SchemaValidator, } from '@difizen/mana-app'; -import { l10n } from '@difizen/mana-l10n'; -import { Card, Form } from 'antd'; +import { Form } from 'antd'; import React from 'react'; import './index.less'; @@ -68,17 +67,15 @@ export const DefaultConfigurationViewComponent: React.FC = () => { const viewInstance = useInject(ViewInstance); const configs = viewInstance.configurationNodes; return ( -
-
- -
- {configs?.map((config) => { - return ; - })} - -
-
-
+
+ {configs?.map((config) => { + return ; + })} + ); }; diff --git a/packages/libro-core/src/settings/setting-editor/default-node-render.tsx b/packages/libro-core/src/settings/setting-editor/default-node-render.tsx index 5ca78c5a..f1fe223c 100644 --- a/packages/libro-core/src/settings/setting-editor/default-node-render.tsx +++ b/packages/libro-core/src/settings/setting-editor/default-node-render.tsx @@ -1,55 +1,65 @@ import type { RenderProps } from '@difizen/mana-app'; -import { Input, Checkbox, InputNumber, Switch, Select, DatePicker } from 'antd'; -import moment from 'moment'; -import type { FC } from 'react'; +import { l10n } from '@difizen/mana-l10n'; +import { Checkbox, DatePicker, Input, InputNumber, Select, Switch } from 'antd'; +import dayjs from 'dayjs'; +import React from 'react'; const { Option } = Select; -export const DefaultInput: FC> = ({ value, onChange }) => ( +export const DefaultInput: React.FC> = ({ value, onChange }) => ( onChange(event.target.value)} /> ); -export function DefaultInputNumber({ value, onChange, schema }: RenderProps) { - return ( - onChange(val)} - /> - ); -} +export const DefaultInputNumber: React.FC> = ({ + value, + onChange, + schema, +}) => ( + onChange(val)} + /> +); -export function DefaultCheckbox({ value, onChange }: RenderProps) { +export const DefaultCheckbox: React.FC> = ({ + value, + onChange, +}) => { return ( onChange(event.target.checked)}> - {String(value)} + {value ? l10n.t('开启') : l10n.t('关闭')} ); -} +}; -export function DefaultSwitch({ value, onChange }: RenderProps) { - return onChange(checked)} />; -} -export function DefaultSelect({ value, onChange, schema }: RenderProps) { - return ( - - ); -} +export const DefaultSwitch: React.FC> = ({ value, onChange }) => ( + onChange(checked)} /> +); + +export const DefaultSelect: React.FC> = ({ + value, + onChange, + schema, +}) => ( + +); const dateFormat = 'YYYY/MM/DD'; -export function DefaultDatePicker({ value, onChange }: RenderProps) { - return ( - onChange(dateString)} - /> - ); -} +export const DefaultDatePicker: React.FC> = ({ + value, + onChange, +}) => ( + onChange(dateString)} + /> +); diff --git a/packages/libro-core/src/settings/setting-editor/setting-editor-view.tsx b/packages/libro-core/src/settings/setting-editor/setting-editor-view.tsx index 0b8c7823..24b6a631 100644 --- a/packages/libro-core/src/settings/setting-editor/setting-editor-view.tsx +++ b/packages/libro-core/src/settings/setting-editor/setting-editor-view.tsx @@ -12,18 +12,31 @@ import { ConfigurationRegistry, } from '@difizen/mana-app'; import { SplitPanel } from '@difizen/mana-react'; +import { useEffect, useState } from 'react'; import { ConfigurationPanelView } from './configuration-panel-view.js'; -import { SettingTreeFactoryId, SettingTreeView } from './setting-tree-view.js'; +import { SettingTreeView } from './setting-tree-view.js'; export const SettingEditorComponent: React.FC = () => { const viewInstance = useInject(ViewInstance); + const viewManager = useInject(ViewManager); const PanelView = viewInstance.configurationPanel?.view; + + const [treeView, setTreeView] = useState(); + useEffect(() => { + viewManager + .getOrCreateView(SettingTreeView) + .then((item) => { + setTreeView(item); + return; + }) + .catch(console.error); + }); + return ( - {/* TODO: fix */} - + {treeView && } {PanelView && } diff --git a/packages/libro-core/src/settings/settings-modal.less b/packages/libro-core/src/settings/settings-modal.less new file mode 100644 index 00000000..2df80d47 --- /dev/null +++ b/packages/libro-core/src/settings/settings-modal.less @@ -0,0 +1,3 @@ +.libro-settings-modal { + background-color: #fff !important; +} diff --git a/packages/libro-jupyter/package.json b/packages/libro-jupyter/package.json index ab7df167..54f665c0 100644 --- a/packages/libro-jupyter/package.json +++ b/packages/libro-jupyter/package.json @@ -44,11 +44,10 @@ "lint:tsc": "tsc --noEmit" }, "dependencies": { + "@difizen/libro-cofine-editor": "^0.1.0", "@difizen/libro-code-editor": "^0.1.0", + "@difizen/libro-code-cell": "^0.1.0", "@difizen/libro-codemirror": "^0.1.0", - "@difizen/libro-codemirror-code-cell": "^0.1.0", - "@difizen/libro-codemirror-markdown-cell": "^0.1.0", - "@difizen/libro-codemirror-raw-cell": "^0.1.0", "@difizen/libro-rendermime": "^0.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-core": "^0.1.0", @@ -56,7 +55,10 @@ "@difizen/libro-l10n": "^0.1.0", "@difizen/libro-output": "^0.1.0", "@difizen/libro-search": "^0.1.0", - "@difizen/libro-search-codemirror-cell": "^0.1.0", + "@difizen/libro-search-code-cell": "^0.1.0", + "@difizen/libro-lsp": "^0.1.0", + "@difizen/libro-markdown-cell": "^0.1.0", + "@difizen/libro-raw-cell": "^0.1.0", "@difizen/mana-app": "latest", "@difizen/mana-l10n": "latest", "@ant-design/colors": "^7.0.0", diff --git a/packages/libro-jupyter/src/add-between-cell/add-between-cell.tsx b/packages/libro-jupyter/src/add-between-cell/add-between-cell.tsx index dcdb8a39..3b8b3bd4 100644 --- a/packages/libro-jupyter/src/add-between-cell/add-between-cell.tsx +++ b/packages/libro-jupyter/src/add-between-cell/add-between-cell.tsx @@ -1,5 +1,5 @@ import { DisplayWrapComponent } from '@difizen/libro-common'; -import type { BetweenCellProvider, LibroView } from '@difizen/libro-core'; +import type { BetweenCellProvider, CellOptions, LibroView } from '@difizen/libro-core'; import { CellService } from '@difizen/libro-core'; import { CommandRegistry, useInject, ViewInstance } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; @@ -36,8 +36,13 @@ const AddCellOutlined: React.FC = () => ( // eslint-disable-next-line @typescript-eslint/no-unused-vars export const LibroCommonBetweenCellContent: BetweenCellProvider = forwardRef( - function LibroCommonBetweenCellContent(props, _ref) { - // eslint-disable-next-line react/prop-types + function LibroCommonBetweenCellContent( + props: { + index: number; + addCell: (option: CellOptions, position?: number | undefined) => Promise; + }, + ref, + ) { const { addCell, index } = props; const { cellsMeta } = useInject(CellService); const anchorRef = useRef(null); @@ -62,7 +67,7 @@ export const LibroCommonBetweenCellContent: BetweenCellProvider = forwardRef( clearTimeout(delayRef.current); }; - const openTooltip = (_nextOpen: boolean, delay = 0.5) => { + const openTooltip = (nextOpen: boolean, delay = 0.5) => { clearDelay(); if (delay === 0) { @@ -117,7 +122,7 @@ export const LibroCommonBetweenCellContent: BetweenCellProvider = forwardRef( tabIndex={10} className="libro-add-between-cell-anchor" style={{ - position: 'fixed', + position: 'absolute', top: position.top + 5, left: position.left + 5, width: 1, @@ -153,7 +158,9 @@ export const LibroCommonBetweenCellContent: BetweenCellProvider = forwardRef( closeTooltip(); setGutterVisible(true); setMenuVisible(true); - setPosition({ top: e.clientY, left: e.clientX }); + + // TODO: 位置不准确 + setPosition({ top: e.nativeEvent.offsetY, left: e.nativeEvent.offsetX }); anchorRef.current?.focus(); }} > @@ -189,8 +196,10 @@ export const LibroCommonBetweenCellContent: BetweenCellProvider = forwardRef( }, ); -export const LibroWrappedBetweenCellContent: BetweenCellProvider = (props) => { - // eslint-disable-next-line react/prop-types +export const LibroWrappedBetweenCellContent: BetweenCellProvider = (props: { + index: number; + addCell: (option: CellOptions, position?: number | undefined) => Promise; +}) => { const { index, addCell } = props; const instance = useInject(ViewInstance); return ( diff --git a/packages/libro-jupyter/src/add-between-cell/index.less b/packages/libro-jupyter/src/add-between-cell/index.less index 9a97c575..3e14b3ef 100644 --- a/packages/libro-jupyter/src/add-between-cell/index.less +++ b/packages/libro-jupyter/src/add-between-cell/index.less @@ -101,3 +101,9 @@ outline: none; cursor: pointer; } + +.ReactVirtualized__List { + .libro-add-between-cell-area { + width: calc(100% - 45px); + } +} diff --git a/packages/libro-jupyter/src/cell/jupyter-code-cell-model.ts b/packages/libro-jupyter/src/cell/jupyter-code-cell-model.ts index f282c841..632ebbe1 100644 --- a/packages/libro-jupyter/src/cell/jupyter-code-cell-model.ts +++ b/packages/libro-jupyter/src/cell/jupyter-code-cell-model.ts @@ -1,12 +1,12 @@ -import { LibroCodeCellModel } from '@difizen/libro-codemirror-code-cell'; +import { LibroCodeCellModel } from '@difizen/libro-code-cell'; import type { ICellMetadata } from '@difizen/libro-common'; import { CellOptions } from '@difizen/libro-core'; import { inject, transient } from '@difizen/mana-app'; import { prop, ViewManager } from '@difizen/mana-app'; import type { - ExecutedWithKernelCellModel, CodeCellMetadata, + ExecutedWithKernelCellModel, } from '../libro-jupyter-protocol.js'; @transient() @@ -24,12 +24,14 @@ export class JupyterCodeCellModel @inject(ViewManager) viewManager: ViewManager, ) { super(options, viewManager); - this.metadata = options.cell?.metadata || {}; + this.metadata = { + ...options?.cell?.metadata, + libroFormatter: this.libroFormatType, + }; } override clearExecution = () => { this.executeCount = null; - this.executing = false; this.kernelExecuting = false; this.metadata.execution = {}; }; 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 65ab62d9..c6e20afb 100644 --- a/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx +++ b/packages/libro-jupyter/src/cell/jupyter-code-cell-view.tsx @@ -1,4 +1,5 @@ -import { CodeEditorView } from '@difizen/libro-code-editor'; +import { CellEditorMemo, LibroCodeCellView } from '@difizen/libro-code-cell'; +import { CodeEditorManager } from '@difizen/libro-code-editor'; import type { CodeEditorViewOptions, CompletionProvider, @@ -6,14 +7,14 @@ import type { TooltipProvider, TooltipProviderOption, } from '@difizen/libro-code-editor'; -import { codeMirrorEditorFactory } from '@difizen/libro-codemirror'; -import { CellEditorMemo, LibroCodeCellView } from '@difizen/libro-codemirror-code-cell'; import type { CellViewOptions } from '@difizen/libro-core'; import { CellService } from '@difizen/libro-core'; import { KernelError } from '@difizen/libro-kernel'; +import type { LSPConnection, LSPProvider } from '@difizen/libro-lsp'; +import { ILSPDocumentConnectionManager } from '@difizen/libro-lsp'; import { inject, transient } from '@difizen/mana-app'; import { view, ViewInstance, ViewManager, ViewOption } from '@difizen/mana-app'; -import { getOrigin, useInject, watch } from '@difizen/mana-app'; +import { getOrigin, useInject } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; import { forwardRef } from 'react'; @@ -23,7 +24,7 @@ import type { ExecutionMeta } from '../libro-jupyter-protocol.js'; import type { JupyterCodeCellModel } from './jupyter-code-cell-model.js'; const JupyterCodeCellComponent = forwardRef( - function JupyterCodeCellComponent(_props, ref) { + function JupyterCodeCellComponent(props, ref) { const instance = useInject(ViewInstance); return (
{ this.model.clearExecution(); - this.outputArea.clear(); + Promise.resolve() + .then(() => { + this.outputArea.clear(); + return; + }) + .catch(console.error); }; - override onViewMount() { - const option: CodeEditorViewOptions = { - ...this.options, - factory: (editorOption) => - codeMirrorEditorFactory({ - ...editorOption, - config: { - ...editorOption.config, - ...{ readOnly: this.parent.model.readOnly }, - }, - }), - model: this.model, + protected override getEditorOption(): CodeEditorViewOptions { + const options = super.getEditorOption(); + return { + ...options, tooltipProvider: this.tooltipProvider, completionProvider: this.completionProvider, - lspProvider: undefined, + lspProvider: (this.parent.model as LibroJupyterModel).lspEnabled + ? this.lspProvider + : undefined, }; - this.viewManager - .getOrCreateView(CodeEditorView, option) - .then((editorView) => { - this.editorView = editorView; - this.editorViewReadyDeferred.resolve(); - watch(this.parent.model, 'readOnly', () => { - this.editorView?.editor?.setOption('readOnly', this.parent.model.readOnly); - }); - this.editorView.onModalChange((val) => (this.hasModal = val)); - return; - }) - .catch(() => { - // - }); } + lspProvider: LSPProvider = async () => { + await this.lspDocumentConnectionManager.ready; + const adapter = this.lspDocumentConnectionManager.adapters.get( + this.parent.model.id, + ); + if (!adapter) { + throw new Error('no adapter'); + } + + await adapter.ready; + + const virtualEditor = adapter.getCellEditor(this); + + if (!virtualEditor) { + throw new Error('no virtual editor'); + } + + // Get the associated virtual document of the opened document + const virtualDocument = adapter.virtualDocument; + + if (!virtualDocument) { + throw new Error('no virtualDocument'); + } + + // Get the LSP connection of the virtual document. + const lspConnection = this.lspDocumentConnectionManager.connections.get( + virtualDocument.uri, + ) as LSPConnection; + + return { + virtualDocument, + lspConnection, + editor: virtualEditor, + }; + }; + tooltipProvider: TooltipProvider = async (option: TooltipProviderOption) => { const cellContent = this.model.value; const kernelConnection = getOrigin( @@ -113,7 +141,7 @@ export class JupyterCodeCellView extends LibroCodeCellView { }; completionProvider: CompletionProvider = async (option: CompletionProviderOption) => { - const cellContent = this.model.value; + const cellContent = this.model.source; const kernelConnection = getOrigin( (this.parent.model as LibroJupyterModel).kernelConnection, ); @@ -157,35 +185,37 @@ export class JupyterCodeCellView extends LibroCodeCellView { } const kernelConnection = getOrigin(libroModel.kernelConnection); - const cellContent = this.model.value; + const cellContent = this.model.source; const cellModel = this.model; try { - this.clearExecution(); + // if (this.outputArea instanceof LibroOutputArea) + // this.outputArea.lastOutputContainerHeight = + // this.outputArea.container?.current?.clientHeight; + cellModel.executing = true; const future = kernelConnection.requestExecute({ code: cellContent, }); let startTimeStr = ''; - cellModel.executing = true; - - cellModel.metadata['execution'] = { - 'shell.execute_reply.started': '', - 'shell.execute_reply.end': '', - to_execute: new Date().toISOString(), - } as ExecutionMeta; + this.clearExecution(); // Handle iopub messages future.onIOPub = (msg: any) => { - cellModel.msgChangeEmitter.fire(msg); if (msg.header.msg_type === 'execute_input') { + cellModel.metadata.execution = { + 'shell.execute_reply.started': '', + 'shell.execute_reply.end': '', + to_execute: new Date().toISOString(), + } as ExecutionMeta; cellModel.kernelExecuting = true; startTimeStr = msg.header.date as string; - const meta = cellModel.metadata['execution'] as ExecutionMeta; + const meta = cellModel.metadata.execution as ExecutionMeta; if (meta) { meta['shell.execute_reply.started'] = startTimeStr; } } + cellModel.msgChangeEmitter.fire(msg); }; // Handle the execute reply. future.onReply = (msg: any) => { @@ -199,8 +229,10 @@ export class JupyterCodeCellView extends LibroCodeCellView { startTimeStr = msgPromise.metadata['started'] as string; const endTimeStr = msgPromise.header.date; - cellModel.metadata['execution']['shell.execute_reply.started'] = startTimeStr; - cellModel.metadata['execution']['shell.execute_reply.end'] = endTimeStr; + (cellModel.metadata.execution as ExecutionMeta)['shell.execute_reply.started'] = + startTimeStr; + (cellModel.metadata.execution as ExecutionMeta)['shell.execute_reply.end'] = + endTimeStr; if (!msgPromise) { return true; diff --git a/packages/libro-jupyter/src/command/command-contribution.ts b/packages/libro-jupyter/src/command/command-contribution.ts index 0f397c2c..650a3454 100644 --- a/packages/libro-jupyter/src/command/command-contribution.ts +++ b/packages/libro-jupyter/src/command/command-contribution.ts @@ -30,12 +30,12 @@ export class LibroJupyterCommandContribution implements CommandContribution { command, KernelCommands['ShowKernelStatusAndSelector'], { - execute: async (_cell, libro) => { + execute: async (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return; } }, - isVisible: (_cell, libro, path) => { + isVisible: (cell, libro, path) => { if (!libro || !(libro instanceof LibroView)) { return false; } @@ -45,7 +45,7 @@ export class LibroJupyterCommandContribution implements CommandContribution { path === LibroToolbarArea.HeaderLeft ); }, - isEnabled: (_cell, libro) => { + isEnabled: (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return false; } @@ -79,7 +79,7 @@ export class LibroJupyterCommandContribution implements CommandContribution { } return !!cell; }, - isEnabled: (_cell, libro) => { + isEnabled: (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return false; } @@ -113,7 +113,7 @@ export class LibroJupyterCommandContribution implements CommandContribution { path === LibroToolbarArea.CellRight ); }, - isEnabled: (_cell, libro) => { + isEnabled: (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return false; } @@ -129,7 +129,7 @@ export class LibroJupyterCommandContribution implements CommandContribution { command, NotebookCommands['SelectLastRunCell'], { - execute: async (_cell, libro) => { + execute: async (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return; } @@ -137,13 +137,17 @@ export class LibroJupyterCommandContribution implements CommandContribution { libro.model.findRunningCell(); } }, - isVisible: (_cell, libro, path) => { + isVisible: (cell, libro, path) => { if (!libro || !(libro instanceof LibroView)) { return false; } - return path === LibroToolbarArea.HeaderCenter && !libro.model.readOnly; + return ( + !libro?.model.quickEditMode && + path === LibroToolbarArea.HeaderCenter && + !libro.model.readOnly + ); }, - isEnabled: (_cell, libro) => { + isEnabled: (cell, libro) => { if (!libro || !(libro instanceof LibroView) || libro.model.readOnly) { return false; } diff --git a/packages/libro-jupyter/src/components/cell-execution-tip.tsx b/packages/libro-jupyter/src/components/cell-execution-tip.tsx index 9af4fe57..2482a1cb 100644 --- a/packages/libro-jupyter/src/components/cell-execution-tip.tsx +++ b/packages/libro-jupyter/src/components/cell-execution-tip.tsx @@ -8,6 +8,7 @@ import { import { useObserve } from '@difizen/mana-app'; import classnames from 'classnames'; import moment from 'moment'; +import { useState } from 'react'; import type { JupyterCodeCellModel } from '../cell/jupyter-code-cell-model.js'; import { @@ -21,6 +22,7 @@ import { InfoCircle } from './icons.js'; import './index.less'; export function CellExecutionTip({ cell }: { cell: CellView }) { + const [, setCurrentTime] = useState(); const observableCell = useObserve(cell); if (!ExecutableCellView.is(cell)) { @@ -32,7 +34,8 @@ export function CellExecutionTip({ cell }: { cell: CellView }) { } const isHidden = observableCell.model.hasOutputHidden; - const kernelExecuting = (cell.model as JupyterCodeCellModel).kernelExecuting; + const kernelExecuting = (cell.model as unknown as JupyterCodeCellModel) + .kernelExecuting; const executionInfo = parseExecutionInfoFromModel(cell.model); const output = cell.outputArea.outputs; @@ -73,6 +76,9 @@ export function CellExecutionTip({ cell }: { cell: CellView }) {
); } else if (kernelExecuting) { + setTimeout(() => { + setCurrentTime(Date.now()); + }, 100); return (
diff --git a/packages/libro-jupyter/src/components/cell-input-bottom-blank.tsx b/packages/libro-jupyter/src/components/cell-input-bottom-blank.tsx index 1ef49a66..f13046a1 100644 --- a/packages/libro-jupyter/src/components/cell-input-bottom-blank.tsx +++ b/packages/libro-jupyter/src/components/cell-input-bottom-blank.tsx @@ -1,5 +1,5 @@ -import { LibroExecutableCellView } from '@difizen/libro-core'; import type { CellView } from '@difizen/libro-core'; +import { LibroExecutableCellView, LibroOutputArea } from '@difizen/libro-core'; import { useObserve } from '@difizen/mana-app'; import { isWaitingExecute } from '../utils/index.js'; @@ -7,16 +7,21 @@ import { isWaitingExecute } from '../utils/index.js'; export function CellInputBottomBlank({ cell }: { cell: CellView }) { const observableCell = useObserve(cell) as LibroExecutableCellView; - if (!(cell instanceof LibroExecutableCellView)) { + if ( + !(cell instanceof LibroExecutableCellView) || + !(observableCell.outputArea instanceof LibroOutputArea) + ) { return null; } const outputs = observableCell.outputArea.outputs; - const hasNoneOutput = - (!outputs || outputs.length === 0) && observableCell.model.executeCount; + const hasNoneOutput = !outputs || outputs.length === 0; // 有output时 或者 没有被执行过,不显示input底部的空白 - if (hasNoneOutput || isWaitingExecute(observableCell.model)) { + if ( + hasNoneOutput && + (observableCell.model.executeCount || isWaitingExecute(observableCell.model)) + ) { return
; } diff --git a/packages/libro-jupyter/src/configuration/index.ts b/packages/libro-jupyter/src/configuration/index.ts deleted file mode 100644 index ea2b6261..00000000 --- a/packages/libro-jupyter/src/configuration/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './libro-configuration.js'; -export * from './libro-configuration-contribution.js'; diff --git a/packages/libro-jupyter/src/configuration/libro-configuration-contribution.ts b/packages/libro-jupyter/src/configuration/libro-configuration-contribution.ts deleted file mode 100644 index 45c8b952..00000000 --- a/packages/libro-jupyter/src/configuration/libro-configuration-contribution.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ConfigurationContribution } from '@difizen/mana-app'; -import { singleton } from '@difizen/mana-app'; - -import { LibroJupyterConfiguration } from './libro-configuration.js'; - -@singleton({ contrib: ConfigurationContribution }) -export class LibroConfigurationContribution implements ConfigurationContribution { - registerConfigurations() { - return [LibroJupyterConfiguration['OpenSlot']]; - } -} diff --git a/packages/libro-jupyter/src/configuration/libro-configuration.ts b/packages/libro-jupyter/src/configuration/libro-configuration.ts deleted file mode 100644 index c2c5ca70..00000000 --- a/packages/libro-jupyter/src/configuration/libro-configuration.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ConfigurationNode } from '@difizen/mana-app'; - -export const LibroJupyterConfiguration: Record> = { - OpenSlot: { - id: 'libro.jupyter.open.slot', - description: '文件默认打开位置', - title: '文件默认打开位置', - type: 'checkbox', - defaultValue: 'main', - schema: { - type: 'string', - }, - }, -}; diff --git a/packages/libro-jupyter/src/file/file-command.tsx b/packages/libro-jupyter/src/file/file-command.tsx deleted file mode 100644 index a792680e..00000000 --- a/packages/libro-jupyter/src/file/file-command.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import pathUtil from 'path'; - -import { ReloadOutlined } from '@ant-design/icons'; -import type { - CommandRegistry, - MenuPath, - MenuRegistry, - ToolbarRegistry, -} from '@difizen/mana-app'; -import { ViewManager } from '@difizen/mana-app'; -import { - CommandContribution, - FileStatNode, - FileTreeCommand, - inject, - MenuContribution, - ModalService, - OpenerService, - singleton, - ToolbarContribution, - URI, -} from '@difizen/mana-app'; -import { message, Modal } from 'antd'; - -import { FileCreateModal } from './file-create-modal.js'; -import { FileDirCreateModal } from './file-createdir-modal.js'; -import { FileRenameModal } from './file-rename-modal.js'; -import { JupyterFileService } from './file-service.js'; -import { FileView } from './file-view/index.js'; -import { copy2clipboard } from './utils.js'; - -const FileCommands = { - OPEN_FILE: { - id: 'fileTree.command.openfile', - label: '打开', - }, - COPY: { - id: 'fileTree.command.copy', - label: '复制', - }, - PASTE: { - id: 'fileTree.command.paste', - label: '粘贴', - }, - CUT: { - id: 'fileTree.command.cut', - label: '剪切', - }, - RENAME: { - id: 'fileTree.command.rename', - label: '重命名', - }, - COPY_PATH: { - id: 'fileTree.command.copyPath', - label: '复制路径', - }, - COPY_RELATIVE_PATH: { - id: 'fileTree.command.copyRelativePath', - label: '复制相对路径', - }, - CREATE_FILE: { - id: 'fileTree.command.createfile', - label: '新建文件', - }, - CREATE_DIR: { - id: 'fileTree.command.createdir', - label: '新建文件夹', - }, - REFRESH: { - id: 'fileTree.command.refresh', - label: '刷新', - }, -}; -export const FileTreeContextMenuPath: MenuPath = ['file-tree-context-menu']; - -@singleton({ - contrib: [CommandContribution, MenuContribution, ToolbarContribution], -}) -export class FileCommandContribution - implements CommandContribution, MenuContribution, ToolbarContribution -{ - protected viewManager: ViewManager; - @inject(JupyterFileService) fileService: JupyterFileService; - @inject(ModalService) modalService: ModalService; - @inject(OpenerService) protected openService: OpenerService; - fileView: FileView; - lastAction: 'COPY' | 'CUT'; - lastActionNode: FileStatNode; - - constructor(@inject(ViewManager) viewManager: ViewManager) { - this.viewManager = viewManager; - this.viewManager - .getOrCreateView(FileView) - .then((view) => { - this.fileView = view; - return; - }) - .catch(() => { - // - }); - } - - registerMenus(menu: MenuRegistry) { - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.CREATE_FILE.id, - command: FileCommands.CREATE_FILE.id, - order: 'a', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.CREATE_DIR.id, - command: FileCommands.CREATE_DIR.id, - order: 'a', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.OPEN_FILE.id, - command: FileCommands.OPEN_FILE.id, - order: 'a', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.COPY.id, - command: FileCommands.COPY.id, - order: 'b', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.PASTE.id, - command: FileCommands.PASTE.id, - order: 'c', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.CUT.id, - command: FileCommands.CUT.id, - order: 'd', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.RENAME.id, - command: FileCommands.RENAME.id, - order: 'e', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.COPY_PATH.id, - command: FileCommands.COPY_PATH.id, - order: 'g', - }); - menu.registerMenuAction(FileTreeContextMenuPath, { - id: FileCommands.COPY_RELATIVE_PATH.id, - command: FileCommands.COPY_RELATIVE_PATH.id, - order: 'g', - }); - } - registerCommands(command: CommandRegistry): void { - command.registerCommand(FileCommands.OPEN_FILE, { - execute: (node) => { - try { - if (node.fileStat.isFile) { - this.openService - .getOpener(node.uri) - .then((opener) => { - if (opener) { - opener.open(node.uri, { - viewOptions: { - name: node.fileStat.name, - }, - }); - } - return; - }) - .catch(() => { - throw Error(); - }); - } - } catch { - message.error('文件打开失败'); - } - }, - isVisible: (node) => { - return FileStatNode.is(node) && node.fileStat.isFile; - }, - }); - command.registerHandler(FileTreeCommand.REMOVE.id, { - execute: (node) => { - if (FileStatNode.is(node)) { - const filePath = node.uri.path.toString(); - Modal.confirm({ - title: '确认删除一下文件或文件夹?', - content: filePath, - onOk: async () => { - try { - await this.fileService.delete(node.uri); - } catch { - message.error('删除文件失败!'); - } - this.fileView.model.refresh(); - }, - }); - } - }, - isVisible: (node) => { - return FileStatNode.is(node); - }, - }); - command.registerCommand(FileCommands.COPY, { - execute: (node) => { - this.lastAction = 'COPY'; - this.lastActionNode = node; - }, - isVisible: (node) => { - return FileStatNode.is(node) && node.fileStat.isFile; - }, - }); - command.registerCommand(FileCommands.CUT, { - execute: (node) => { - this.lastAction = 'CUT'; - this.lastActionNode = node; - }, - isVisible: (node) => { - return FileStatNode.is(node) && node.fileStat.isFile; - }, - }); - command.registerCommand(FileCommands.PASTE, { - execute: async (data) => { - try { - if (FileStatNode.is(data)) { - const targetUri = data.fileStat.isDirectory ? data.uri : data.uri.parent; - await this.fileService.copy(this.lastActionNode.uri, targetUri); - } else if (data instanceof FileView) { - const targetPath = '/'; - await this.fileService.copy(this.lastActionNode.uri, new URI(targetPath)); - } - if (this.lastAction === 'CUT') { - await this.fileService.delete(this.lastActionNode.uri); - } - this.fileView.model.refresh(); - return; - } catch { - message.error('粘贴失败!'); - } - }, - isVisible: () => { - return this.lastAction === 'CUT' || this.lastAction === 'COPY'; - }, - }); - command.registerCommand(FileCommands.RENAME, { - execute: async (node) => { - this.modalService.openModal(FileRenameModal, { - resource: node.uri, - fileName: node.uri.path.base, - }); - }, - isVisible: (node) => { - return FileStatNode.is(node); - }, - }); - command.registerCommand(FileCommands.CREATE_FILE, { - execute: async (data) => { - let path = '/workspace'; - if (FileStatNode.is(data)) { - path = data.fileStat.isDirectory - ? data.uri.path.toString() - : data.uri.path.dir.toString(); - } - this.modalService.openModal(FileCreateModal, { - path, - }); - }, - }); - command.registerCommand(FileCommands.CREATE_DIR, { - execute: async (data) => { - let path = '/workspace'; - if (FileStatNode.is(data)) { - path = data.fileStat.isDirectory - ? data.uri.path.toString() - : data.uri.path.dir.toString(); - } - this.modalService.openModal(FileDirCreateModal, { - path, - }); - }, - }); - - command.registerCommand(FileCommands.COPY_PATH, { - execute: async (data) => { - let path = '/workspace'; - if (FileStatNode.is(data)) { - path = data.uri.path.toString(); - } - copy2clipboard(path); - }, - }); - - command.registerCommand(FileCommands.COPY_RELATIVE_PATH, { - execute: async (data) => { - let relative = ''; - if (FileStatNode.is(data)) { - relative = pathUtil.relative('/workspace', data.uri.path.toString()); - } - copy2clipboard(relative); - }, - }); - - command.registerCommand(FileCommands.REFRESH, { - execute: async (view) => { - if (view instanceof FileView) { - view.model.refresh(); - } - }, - isVisible: (view) => { - return view instanceof FileView; - }, - }); - } - - registerToolbarItems(toolbarRegistry: ToolbarRegistry): void { - toolbarRegistry.registerItem({ - id: FileCommands.REFRESH.id, - command: FileCommands.REFRESH.id, - icon: , - tooltip: '刷新', - }); - } -} diff --git a/packages/libro-jupyter/src/file/file-create-modal-contribution.ts b/packages/libro-jupyter/src/file/file-create-modal-contribution.ts deleted file mode 100644 index cb2eb5a7..00000000 --- a/packages/libro-jupyter/src/file/file-create-modal-contribution.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ModalContribution, singleton } from '@difizen/mana-app'; - -import { FileCreateModal } from './file-create-modal.js'; - -@singleton({ contrib: ModalContribution }) -export class FileCreateModalContribution implements ModalContribution { - registerModal() { - return FileCreateModal; - } -} diff --git a/packages/libro-jupyter/src/file/file-create-modal.tsx b/packages/libro-jupyter/src/file/file-create-modal.tsx deleted file mode 100644 index 6732c25a..00000000 --- a/packages/libro-jupyter/src/file/file-create-modal.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { ModalItem, ModalItemProps } from '@difizen/mana-app'; -import { URI, useInject, ViewManager } from '@difizen/mana-app'; -import type { InputRef } from 'antd'; -import { Input, Modal } from 'antd'; -import { useEffect, useRef, useState } from 'react'; - -import { FileView, JupyterFileService } from './index.js'; - -export interface ModalItemType { - path: string; -} - -export const FileCreateModalComponent: React.FC> = ({ - visible, - close, - data, -}: ModalItemProps) => { - const fileService = useInject(JupyterFileService); - const viewManager = useInject(ViewManager); - const [newFileName, setNewFileName] = useState(''); - const [fileView, setFileView] = useState(); - const inputRef = useRef(null); - - useEffect(() => { - viewManager - .getOrCreateView(FileView) - .then((view) => { - setFileView(view); - return; - }) - .catch(() => { - // - }); - inputRef.current?.focus(); - }); - return ( - { - await fileService.newFile(newFileName, new URI(data.path)); - if (fileView) { - fileView.model.refresh(); - } - close(); - }} - keyboard={true} - > - { - setNewFileName(e.target.value); - }} - ref={inputRef} - onKeyDown={async (e) => { - if (e.keyCode === 13) { - await fileService.newFile(newFileName, new URI(data.path)); - if (fileView) { - fileView.model.refresh(); - } - close(); - } - }} - /> - - ); -}; - -export const FileCreateModal: ModalItem = { - id: 'file.create.modal', - component: FileCreateModalComponent, -}; diff --git a/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts b/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts deleted file mode 100644 index 2c4034da..00000000 --- a/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ModalContribution, singleton } from '@difizen/mana-app'; - -import { FileDirCreateModal } from './file-createdir-modal.js'; - -@singleton({ contrib: ModalContribution }) -export class FileCreateDirModalContribution implements ModalContribution { - registerModal() { - return FileDirCreateModal; - } -} diff --git a/packages/libro-jupyter/src/file/file-createdir-modal.tsx b/packages/libro-jupyter/src/file/file-createdir-modal.tsx deleted file mode 100644 index b861622a..00000000 --- a/packages/libro-jupyter/src/file/file-createdir-modal.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { ModalItem, ModalItemProps } from '@difizen/mana-app'; -import { URI } from '@difizen/mana-app'; -import { ViewManager } from '@difizen/mana-app'; -import { useInject } from '@difizen/mana-app'; -import type { InputRef } from 'antd'; -import { Input } from 'antd'; -import { Modal } from 'antd'; -import { useEffect, useRef, useState } from 'react'; - -import { JupyterFileService } from './file-service.js'; -import { FileView } from './file-view/index.js'; - -export interface ModalItemType { - path: string; -} - -export const FileCreateDirModalComponent: React.FC> = ({ - visible, - close, - data, -}: ModalItemProps) => { - const fileService = useInject(JupyterFileService); - const viewManager = useInject(ViewManager); - const inputRef = useRef(null); - const [dirName, setDirName] = useState(''); - const [fileView, setFileView] = useState(); - useEffect(() => { - viewManager - .getOrCreateView(FileView) - .then((view) => { - setFileView(view); - return; - }) - .catch(() => { - // - }); - inputRef.current?.focus(); - }); - return ( - { - await fileService.newFileDir(dirName, new URI(data.path)); - if (fileView) { - fileView.model.refresh(); - } - close(); - }} - keyboard={true} - > - { - setDirName(e.target.value); - }} - ref={inputRef} - onKeyDown={async (e) => { - if (e.keyCode === 13) { - await fileService.newFileDir(dirName, new URI(data.path)); - if (fileView) { - fileView.model.refresh(); - } - close(); - } - }} - /> - - ); -}; - -export const FileDirCreateModal: ModalItem = { - id: 'file.createdir.modal', - component: FileCreateDirModalComponent, -}; diff --git a/packages/libro-jupyter/src/file/file-name-alias.ts b/packages/libro-jupyter/src/file/file-name-alias.ts deleted file mode 100644 index 7b478221..00000000 --- a/packages/libro-jupyter/src/file/file-name-alias.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { URI } from '@difizen/mana-app'; -import { singleton } from '@difizen/mana-app'; - -@singleton() -export class FileNameAlias { - protected aliases = new Map(); - - set(uri: URI, name: string) { - this.aliases.set(uri.path.toString(), name); - } - - get(uri: URI) { - return this.aliases.get(uri.path.toString()); - } -} diff --git a/packages/libro-jupyter/src/file/file-protocol.ts b/packages/libro-jupyter/src/file/file-protocol.ts deleted file mode 100644 index 248ab0e8..00000000 --- a/packages/libro-jupyter/src/file/file-protocol.ts +++ /dev/null @@ -1,24 +0,0 @@ -export enum FileType { - Unknown = 0, - File = 1, - Directory = 2, - SymbolicLink = 64, -} -export type DirItem = [string, FileType]; - -export interface EditorView { - dirty: boolean; -} - -export const EditorView = { - is: (data?: Record): data is EditorView => { - return ( - !!data && - typeof data === 'object' && - 'id' in data && - 'view' in data && - 'dirty' in data && - typeof data['view'] === 'function' - ); - }, -}; diff --git a/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts b/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts deleted file mode 100644 index a58aa549..00000000 --- a/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ModalContribution, singleton } from '@difizen/mana-app'; - -import { FileRenameModal } from './file-rename-modal.js'; - -@singleton({ contrib: ModalContribution }) -export class FileRenameModalContribution implements ModalContribution { - registerModal() { - return FileRenameModal; - } -} diff --git a/packages/libro-jupyter/src/file/file-rename-modal.tsx b/packages/libro-jupyter/src/file/file-rename-modal.tsx deleted file mode 100644 index 6f9ae6d5..00000000 --- a/packages/libro-jupyter/src/file/file-rename-modal.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { ModalItem, ModalItemProps, URI } from '@difizen/mana-app'; -import { useInject, ViewManager } from '@difizen/mana-app'; -import type { InputRef } from 'antd'; -import { Input, Modal } from 'antd'; -import { useEffect, useRef, useState } from 'react'; - -import { JupyterFileService } from './file-service.js'; -import { FileView } from './file-view/index.js'; - -export interface ModalItemType { - resource: URI; - fileName: string; -} - -export const FileRenameModalComponent: React.FC> = ({ - visible, - close, - data, -}: ModalItemProps) => { - const fileService = useInject(JupyterFileService); - const viewManager = useInject(ViewManager); - const [newFileName, setNewFileName] = useState(data.fileName); - const inputRef = useRef(null); - const [fileView, setFileView] = useState(); - useEffect(() => { - viewManager - .getOrCreateView(FileView) - .then((view) => { - setFileView(view); - return; - }) - .catch(() => { - // - }); - inputRef.current?.focus(); - }); - return ( - { - await fileService.rename(data.resource, newFileName); - if (fileView) { - fileView.model.refresh(); - } - close(); - }} - > - { - setNewFileName(e.target.value); - }} - ref={inputRef} - /> - - ); -}; - -export const FileRenameModal: ModalItem = { - id: 'file.rename.modal', - component: FileRenameModalComponent, -}; diff --git a/packages/libro-jupyter/src/file/file-service.ts b/packages/libro-jupyter/src/file/file-service.ts deleted file mode 100644 index b56e7388..00000000 --- a/packages/libro-jupyter/src/file/file-service.ts +++ /dev/null @@ -1,215 +0,0 @@ -import pathUtil from 'path'; - -import type { IContentsModel } from '@difizen/libro-kernel'; -import { ContentsManager } from '@difizen/libro-kernel'; -import type { - CopyFileOptions, - FileStatWithMetadata, - MoveFileOptions, - ResolveFileOptions, -} from '@difizen/mana-app'; -import { FileService, URI, inject, singleton } from '@difizen/mana-app'; -import { message } from 'antd'; - -import { FileNameAlias } from './file-name-alias.js'; -import type { DirItem } from './file-protocol.js'; - -interface FileMeta extends Omit { - resource: string; - children?: FileMeta[]; -} - -interface DirectoryModel extends IContentsModel { - type: 'directory'; - content: IContentsModel[]; -} - -@singleton({ token: FileService }) -export class JupyterFileService extends FileService { - @inject(ContentsManager) protected readonly contentsManager: ContentsManager; - // '/read' - // '/read-dir' - // '/mkdirp' - // '/write' - // '/rename' - // '/copy' - // '/delete' - // '/stat' - // '/access' - // '/emptd-dir' - // '/ensure-file' - // '/ensure-link' - // '/ensure-symlink' - - protected fileNameAlias: FileNameAlias; - - constructor( - @inject(FileNameAlias) - fileNameAlias: FileNameAlias, - ) { - super(); - - this.fileNameAlias = fileNameAlias; - } - - async write(filePath: string, content: string): Promise { - await this.contentsManager.save(filePath, { - content, - }); - return filePath; - } - - async readDir(dirPath: string): Promise { - let children: DirItem[] = []; - const res = await this.contentsManager.get(dirPath, { type: 'directory' }); - if (res && this.isDirectory(res)) { - const content = res.content; - children = content.map((item) => { - return [item.path, this.isDirectory(item) ? 2 : 1]; - }); - } - return children; - } - - async read(filePath: string): Promise { - let content: string | undefined = undefined; - const res = await this.contentsManager.get(filePath); - if (res && !this.isDirectory(res)) { - content = res.content as string; - } - return content; - } - - protected async doResolve(filePath: string): Promise { - let stat: FileMeta | undefined = undefined; - try { - const res = await this.contentsManager.get(filePath); - stat = this.toFileMeta(res); - } catch { - // - } - return stat; - } - - protected isDirectory(model: IContentsModel): model is DirectoryModel { - if (model.type === 'directory') { - return true; - } - return false; - } - - protected toFileMeta(model: IContentsModel): FileMeta { - const isDirectory = model.type === 'directory'; - const isSymbolicLink = model.type === 'symlink'; - let children = undefined; - if (isDirectory) { - const content = model.content as IContentsModel[]; - children = content?.map((item) => this.toFileMeta(item)); - } - const uri = URI.withScheme(new URI(model.path), 'file'); - return { - resource: uri.path.toString(), - etag: uri.path.toString(), - name: uri.displayName, - mtime: new Date(model.last_modified).getTime(), - ctime: new Date(model.created).getTime(), - size: model.size!, - isDirectory, - isSymbolicLink, - isFile: !isSymbolicLink && !isDirectory, - children, - }; - } - - override async copy( - source: URI, - _target: URI, - _options?: CopyFileOptions, - ): Promise { - await this.contentsManager.copy(source.path.toString(), _target.path.toString()); - return this.resolve(source); - } - override async move( - source: URI, - _target: URI, - _options?: MoveFileOptions, - ): Promise { - return this.resolve(source); - } - - toFileStatMeta(meta: FileMeta): FileStatWithMetadata { - const uri = URI.withScheme(new URI(meta.resource), 'file'); - return { - ...meta, - resource: uri, - name: this.fileNameAlias.get(uri) ?? meta.name, - children: meta.children?.map((child) => this.toFileStatMeta(child)), - }; - } - - override async resolve( - resource: URI, - _options?: ResolveFileOptions | undefined, - ): Promise { - const resolved = await this.doResolve(resource.path.toString()); - if (resolved) { - return this.toFileStatMeta(resolved); - } - return { - resource, - name: resource.path.base, - mtime: 0, - ctime: 0, - etag: '', - size: 0, - isFile: false, - isDirectory: false, - isSymbolicLink: false, - }; - } - - async delete( - resource: URI, - _options?: ResolveFileOptions | undefined, - ): Promise { - await this.contentsManager.delete(resource.path.toString()); - return this.resolve(resource); - } - - async rename(resource: URI, newName: string): Promise { - const newPath = pathUtil.join(resource.path.dir.toString(), newName); - await this.contentsManager.rename(resource.path.toString(), newPath); - - return this.resolve(resource); - } - - async newFile(fileName: string, target: URI): Promise { - const targetFileUri = new URI(pathUtil.join(target.path.toString(), fileName)); - if ((await this.resolve(targetFileUri)).isFile) { - message.error('文件名重复'); - return this.resolve(target); - } - const fileNameArr = fileName.split('.'); - const ext = fileNameArr[fileNameArr.length - 1]; - const res = await this.contentsManager.newUntitled({ - path: target.path.toString(), - ext, - }); - await this.rename(new URI(res.path.toString()), fileName); - return this.resolve(target); - } - - async newFileDir(dirName: string, target: URI): Promise { - const targetFileUri = new URI(pathUtil.join(target.path.toString(), dirName)); - if ((await this.resolve(targetFileUri)).isDirectory) { - message.error('文件夹重复'); - return this.resolve(target); - } - const res = await this.contentsManager.newUntitled({ - path: target.path.toString(), - type: 'directory', - }); - await this.rename(new URI(res.path.toString()), dirName); - return this.resolve(target); - } -} diff --git a/packages/libro-jupyter/src/file/file-tree-label-provider.ts b/packages/libro-jupyter/src/file/file-tree-label-provider.ts deleted file mode 100644 index e5d1d0fe..00000000 --- a/packages/libro-jupyter/src/file/file-tree-label-provider.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DidChangeLabelEvent, FileStat, URI } from '@difizen/mana-app'; -import { - FileStatNode, - LabelProvider, - LabelProviderContribution, - TreeLabelProvider, - URIIconReference, - inject, - singleton, -} from '@difizen/mana-app'; - -@singleton({ contrib: LabelProviderContribution }) -export class FileTreeLabelProvider implements LabelProviderContribution { - @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - @inject(TreeLabelProvider) protected readonly treeLabelProvider: TreeLabelProvider; - - protected asURIIconReference(element: FileStat): URI | URIIconReference { - return URIIconReference.create( - element.isDirectory ? 'folder' : 'file', - element.resource, - ); - } - canHandle(element: object): number { - return FileStatNode.is(element) ? this.treeLabelProvider.canHandle(element) + 2 : 0; - } - - getIcon(node: FileStatNode): string { - return this.labelProvider.getIcon(this.asURIIconReference(node.fileStat)); - } - - getName(node: FileStatNode): string { - return node.fileStat.name; - } - - getDescription(node: FileStatNode): string { - return this.labelProvider.getLongName(this.asURIIconReference(node.fileStat)); - } - - affects(node: FileStatNode, event: DidChangeLabelEvent): boolean { - return event.affects(node.fileStat); - } -} diff --git a/packages/libro-jupyter/src/file/file-view/index.less b/packages/libro-jupyter/src/file/file-view/index.less deleted file mode 100644 index 60aac1c9..00000000 --- a/packages/libro-jupyter/src/file/file-view/index.less +++ /dev/null @@ -1,5 +0,0 @@ -.libro-jupyter-file-tree { - &-content { - height: calc(100% - 48px); - } -} diff --git a/packages/libro-jupyter/src/file/file-view/index.tsx b/packages/libro-jupyter/src/file/file-view/index.tsx deleted file mode 100644 index 1a20db0d..00000000 --- a/packages/libro-jupyter/src/file/file-view/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { FolderFilled } from '@ant-design/icons'; -import type { TreeNode, ViewOpenHandler } from '@difizen/mana-app'; -import { FileTreeViewFactory } from '@difizen/mana-app'; -import { - FileStatNode, - FileTree, - FileTreeModel, - FileTreeView, - isOSX, - LabelProvider, - TreeDecoratorService, - TreeProps, - TreeViewDecorator, - TreeViewModule, - CommandRegistry, - ManaModule, - OpenerService, - SelectionService, - view, - inject, - singleton, -} from '@difizen/mana-app'; -import React from 'react'; - -import type { LibroNavigatableView } from '../navigatable-view.js'; - -import './index.less'; - -const FileTreeModule = ManaModule.create() - .register(FileTree, FileTreeModel) - .dependOn(TreeViewModule); - -@singleton() -@view(FileTreeViewFactory, FileTreeModule) -export class FileView extends FileTreeView { - @inject(OpenerService) protected openService: OpenerService; - @inject(CommandRegistry) protected command: CommandRegistry; - override id = FileTreeViewFactory; - override className = 'libro-jupyter-file-tree'; - - constructor( - @inject(TreeProps) props: TreeProps, - @inject(FileTreeModel) model: FileTreeModel, - @inject(TreeViewDecorator) treeViewDecorator: TreeViewDecorator, - @inject(SelectionService) selectionService: SelectionService, - @inject(LabelProvider) labelProvider: LabelProvider, - @inject(TreeDecoratorService) decoratorService: TreeDecoratorService, - ) { - super( - props, - model, - treeViewDecorator, - selectionService, - labelProvider, - decoratorService, - ); - this.title.label = '文件导航'; - this.title.icon = ; - this.toDispose.push(this.model.onOpenNode(this.openNode)); - } - - openNode = async (treeNode: TreeNode) => { - if (FileStatNode.is(treeNode) && !treeNode.fileStat.isDirectory) { - const opener = (await this.openService.getOpener( - treeNode.uri, - )) as ViewOpenHandler; - if (opener) { - opener.open(treeNode.uri, { - viewOptions: { name: treeNode.fileStat.name }, - }); - } - } - }; - - override handleClickEvent( - node: TreeNode | undefined, - event: React.MouseEvent, - ): void { - const modifierKeyCombined: boolean = isOSX - ? event.shiftKey || event.metaKey - : event.shiftKey || event.ctrlKey; - if (!modifierKeyCombined && node) { - if ( - FileStatNode.is(node) && - !node.fileStat.isDirectory && - !node.fileStat.isSymbolicLink - ) { - this.model.openNode(node); - } - } - super.handleClickEvent(node, event); - } -} diff --git a/packages/libro-jupyter/src/file/index.ts b/packages/libro-jupyter/src/file/index.ts deleted file mode 100644 index d012952d..00000000 --- a/packages/libro-jupyter/src/file/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './module.js'; -export * from './file-view/index.js'; -export * from './navigatable-view.js'; -export * from './open-handler-contribution.js'; -export * from './file-service.js'; -export * from './file-protocol.js'; diff --git a/packages/libro-jupyter/src/file/module.ts b/packages/libro-jupyter/src/file/module.ts deleted file mode 100644 index 6f99eea0..00000000 --- a/packages/libro-jupyter/src/file/module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { FileTreeModule, ManaModule } from '@difizen/mana-app'; - -import { FileCommandContribution } from './file-command.js'; -import { FileCreateModalContribution } from './file-create-modal-contribution.js'; -import { FileCreateDirModalContribution } from './file-createdir-modal-contribution.js'; -import { FileNameAlias } from './file-name-alias.js'; -import { FileRenameModalContribution } from './file-rename-modal-contribution.js'; -import { JupyterFileService } from './file-service.js'; -import { FileTreeLabelProvider } from './file-tree-label-provider.js'; -import { FileView } from './file-view/index.js'; -import { LibroNavigatableView } from './navigatable-view.js'; -import { LibroJupyterOpenHandler } from './open-handler-contribution.js'; - -export const LibroJupyterFileModule = ManaModule.create() - .register( - JupyterFileService, - FileView, - FileNameAlias, - FileTreeLabelProvider, - LibroNavigatableView, - LibroJupyterOpenHandler, - FileCommandContribution, - FileCreateModalContribution, - FileCreateDirModalContribution, - FileRenameModalContribution, - ) - .dependOn(FileTreeModule); diff --git a/packages/libro-jupyter/src/file/navigatable-view.tsx b/packages/libro-jupyter/src/file/navigatable-view.tsx deleted file mode 100644 index c2c9be41..00000000 --- a/packages/libro-jupyter/src/file/navigatable-view.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import type { LibroView } from '@difizen/libro-core'; -import { LibroService } from '@difizen/libro-core'; -import type { NavigatableView } from '@difizen/mana-app'; -import { CommandRegistry } from '@difizen/mana-app'; -import { - BaseView, - inject, - LabelProvider, - prop, - transient, - URI as VScodeURI, - URIIconReference, - useInject, - view, - ViewInstance, - ViewOption, - ViewRender, - Deferred, - URI, -} from '@difizen/mana-app'; -import { createRef, forwardRef } from 'react'; - -import type { EditorView } from './file-protocol.js'; - -export const LibroEditorComponent = forwardRef(function LibroEditorComponent() { - const instance = useInject(ViewInstance); - - if (!instance.libroView || !instance.libroView.view) { - return null; - } - - return ; -}); - -export const LibroNavigatableViewFactoryId = 'libro-navigatable-view-factory'; -@transient() -@view(LibroNavigatableViewFactoryId) -export class LibroNavigatableView - extends BaseView - implements NavigatableView, EditorView -{ - @inject(LibroService) protected libroService: LibroService; - - @inject(CommandRegistry) commandRegistry: CommandRegistry; - - override view = LibroEditorComponent; - - codeRef = createRef(); - - @prop() filePath?: string; - - @prop() - dirty: boolean; - - @prop() - libroView?: LibroView; - - protected defer = new Deferred(); - - get ready() { - return this.defer.promise; - } - - constructor( - @inject(ViewOption) options: { path: string }, - @inject(LabelProvider) labelProvider: LabelProvider, - ) { - super(); - this.filePath = options.path; - this.dirty = false; - this.title.caption = options.path; - const uri = new URI(options.path); - const uriRef = URIIconReference.create('file', new VScodeURI(options.path)); - const iconClass = labelProvider.getIcon(uriRef); - this.title.icon =
; - this.title.label = uri.displayName; - } - - override async onViewMount(): Promise { - this.getOrCreateLibroView(); - } - - protected async getOrCreateLibroView() { - const libroView = await this.libroService.getOrCreateView({ - id: this.filePath, - resource: this.filePath, - }); - if (!libroView) { - return; - } - this.libroView = libroView; - this.libroView.model.onContentChanged(() => { - this.dirty = true; - }); - this.libroView.onSave(() => { - this.dirty = false; - }); - await this.libroView.initialized; - this.libroView.focus(); - this.defer.resolve(); - } - - getResourceUri(): URI | undefined { - return new URI(this.filePath); - } - - createMoveToUri(resourceUri: URI): URI | undefined { - this.filePath = resourceUri.path.toString(); - this.getOrCreateLibroView(); - return resourceUri; - } -} diff --git a/packages/libro-jupyter/src/file/open-handler-contribution.ts b/packages/libro-jupyter/src/file/open-handler-contribution.ts deleted file mode 100644 index 6aa51637..00000000 --- a/packages/libro-jupyter/src/file/open-handler-contribution.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { URI, ViewOpenHandlerOptions } from '@difizen/mana-app'; -import { ConfigurationService, inject } from '@difizen/mana-app'; -import { - NavigatableViewOpenHandler, - OpenHandler, - singleton, - Priority, -} from '@difizen/mana-app'; - -import { LibroJupyterConfiguration } from '../configuration/index.js'; - -import type { LibroNavigatableView } from './navigatable-view.js'; -import { LibroNavigatableViewFactoryId } from './navigatable-view.js'; - -@singleton({ contrib: OpenHandler }) -export class LibroJupyterOpenHandler extends NavigatableViewOpenHandler { - @inject(ConfigurationService) protected configurationService: ConfigurationService; - - id = LibroNavigatableViewFactoryId; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - canHandle(uri: URI, _options?: ViewOpenHandlerOptions) { - if (uri.scheme === 'file' && uri.path.ext === '.ipynb') { - return Priority.PRIOR + 1; - } - return Priority.IDLE; - } - - override async open(uri: URI, options: ViewOpenHandlerOptions = {}) { - const { viewOptions, ...extra } = options; - const slot = await this.configurationService.get( - LibroJupyterConfiguration['OpenSlot'], - ); - return super.open(uri, { - slot, - viewOptions: { - path: uri.path.toString(), - ...viewOptions, - }, - reveal: true, - ...extra, - }); - } -} diff --git a/packages/libro-jupyter/src/file/utils.ts b/packages/libro-jupyter/src/file/utils.ts deleted file mode 100644 index e1a93dff..00000000 --- a/packages/libro-jupyter/src/file/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { message } from 'antd'; - -function copyFallback(string: string) { - function handler(event: ClipboardEvent) { - const clipboardData = event.clipboardData || (window as any).clipboardData; - clipboardData.setData('text/plain', string); - event.preventDefault(); - document.removeEventListener('copy', handler, true); - } - - document.addEventListener('copy', handler, true); - const successful = document.execCommand('copy'); - if (successful) { - message.success('复制成功'); - } else { - message.warning('复制失败'); - } -} - -// 复制到剪贴板 -export const copy2clipboard = (string: string) => { - navigator.permissions - .query({ - name: 'clipboard-write' as any, - }) - .then((result) => { - if (result.state === 'granted' || result.state === 'prompt') { - if (window.navigator && window.navigator.clipboard) { - window.navigator.clipboard - .writeText(string) - .then(() => { - message.success('复制成功'); - return; - }) - .catch((err) => { - message.warning('复制失败'); - console.error('Could not copy text: ', err); - }); - } else { - console.warn('navigator is not exist'); - } - } else { - console.warn('浏览器权限不允许复制'); - copyFallback(string); - } - return; - }) - .catch(() => { - // - }); -}; diff --git a/packages/libro-jupyter/src/index.spec.ts b/packages/libro-jupyter/src/index.spec.ts deleted file mode 100644 index 654ba143..00000000 --- a/packages/libro-jupyter/src/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'assert'; - -import { JupyterFileService, LibroJupyterModel } from './index.js'; -import 'reflect-metadata'; - -describe('libro-jupyter', () => { - it('#import', () => { - assert(JupyterFileService); - assert(LibroJupyterModel); - }); -}); diff --git a/packages/libro-jupyter/src/index.ts b/packages/libro-jupyter/src/index.ts index 20e59da3..45b87629 100644 --- a/packages/libro-jupyter/src/index.ts +++ b/packages/libro-jupyter/src/index.ts @@ -1,18 +1,18 @@ +export * from '@difizen/libro-code-cell'; +export * from '@difizen/libro-code-editor'; +export * from '@difizen/libro-common'; export * from '@difizen/libro-core'; +export * from '@difizen/libro-cofine-editor'; export * from '@difizen/libro-kernel'; -export * from '@difizen/libro-common'; +export * from '@difizen/libro-l10n'; +export * from '@difizen/libro-lsp'; +export * from '@difizen/libro-markdown-cell'; export * from '@difizen/libro-output'; -export * from '@difizen/libro-codemirror-markdown-cell'; -export * from '@difizen/libro-codemirror-code-cell'; +export * from '@difizen/libro-raw-cell'; +export * from '@difizen/libro-codemirror'; export * from '@difizen/libro-rendermime'; -export * from '@difizen/libro-l10n'; export * from '@difizen/libro-search'; -export * from '@difizen/libro-search-codemirror-cell'; -export * from '@difizen/libro-code-editor'; -export * from '@difizen/libro-codemirror'; -export * from '@difizen/libro-codemirror-raw-cell'; - -export * from './module.js'; +export * from '@difizen/libro-search-code-cell'; export * from './add-between-cell/index.js'; export * from './cell/index.js'; export * from './command/index.js'; @@ -20,14 +20,12 @@ export * from './components/index.js'; export * from './config/index.js'; export * from './contents/index.js'; export * from './keybind-instructions/index.js'; +export * from './libro-jupyter-file-service.js'; +export * from './libro-jupyter-model.js'; +export * from './libro-jupyter-protocol.js'; +export * from './libro-jupyter-server-launch-manager.js'; +export * from './module.js'; export * from './output/index.js'; export * from './rendermime/index.js'; export * from './theme/index.js'; export * from './toolbar/index.js'; -export * from './libro-jupyter-protocol.js'; -export * from './libro-jupyter-model.js'; -export * from './libro-jupyter-view.js'; -export * from './libro-jupyter-file-service.js'; -export * from './libro-jupyter-server-launch-manager.js'; -export * from './file/index.js'; -export * from './configuration/index.js'; diff --git a/packages/libro-jupyter/src/keybind-instructions/index.less b/packages/libro-jupyter/src/keybind-instructions/index.less index 2504fc23..a58ba776 100644 --- a/packages/libro-jupyter/src/keybind-instructions/index.less +++ b/packages/libro-jupyter/src/keybind-instructions/index.less @@ -86,9 +86,9 @@ } .libro-keybind-instructions-icon { - display: flex; width: 18px; height: 18px; + line-height: 22px; } .libro-edit-mode-keybind-instructions-table { @@ -126,8 +126,8 @@ } .libro-keybind-search-match { - background-color: var(--mana-libro-search-match-background-color); color: rgba(0, 10, 26, 89%); + background-color: var(--mana-libro-search-match-background-color); } .libro-command-mode-keybind-instructions-table, diff --git a/packages/libro-jupyter/src/keybind-instructions/keybind-instructions-contribution.ts b/packages/libro-jupyter/src/keybind-instructions/keybind-instructions-contribution.ts index 04d77f45..d0f9d5bc 100644 --- a/packages/libro-jupyter/src/keybind-instructions/keybind-instructions-contribution.ts +++ b/packages/libro-jupyter/src/keybind-instructions/keybind-instructions-contribution.ts @@ -20,12 +20,12 @@ export class KeybindInstructionsContribution registerCommands(command: CommandRegistry) { this.libroCommand.registerLibroCommand(command, KeybindInstructionsCommand, { - execute: async (_cell, libro) => { + execute: async (cell, libro) => { if (!libro || !(libro instanceof LibroView)) { return; } }, - isVisible: (_cell, _libro, path) => { + isVisible: (cell, libro, path) => { return path === LibroToolbarArea.HeaderRight; }, }); diff --git a/packages/libro-jupyter/src/libro-jupyter-model.ts b/packages/libro-jupyter/src/libro-jupyter-model.ts index 2eb1f6a7..80211483 100644 --- a/packages/libro-jupyter/src/libro-jupyter-model.ts +++ b/packages/libro-jupyter/src/libro-jupyter-model.ts @@ -1,24 +1,21 @@ -import { LibroModel } from '@difizen/libro-core'; -import type { - IContentsModel, - ExecutableNotebookModel, - IContentsCheckpointModel, - IKernelConnection, -} from '@difizen/libro-kernel'; +import { LibroModel, VirtualizedManager } from '@difizen/libro-core'; import { - LibroKernelConnectionManager, - ServerManager, ContentsManager, + ExecutableNotebookModel, + LibroKernelConnectionManager, ServerConnection, + ServerManager, } from '@difizen/libro-kernel'; -import { prop, ModalService, getOrigin } from '@difizen/mana-app'; -import { inject, transient } from '@difizen/mana-app'; +import type { IKernelConnection } 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'; +import { inject, transient } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; import { - LibroFileService, ExecutedWithKernelCellModel, + LibroFileService, } from './libro-jupyter-protocol.js'; import { SaveFileErrorModal } from './toolbar/save-file-error.js'; import { getDefaultKernel } from './utils/index.js'; @@ -26,6 +23,17 @@ 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; get fileService() { @@ -42,13 +50,14 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM kernelConnection?: IKernelConnection; @prop() - lspEnabled = false; + lspEnabled = true; protected kernelConnectionManager: LibroKernelConnectionManager; protected serverManager: ServerManager; protected serverConnection: ServerConnection; protected readonly contentsManager: ContentsManager; protected readonly modalService: ModalService; + protected override virtualizedManager: VirtualizedManager; constructor( @inject(LibroFileService) libroFileService: LibroFileService, @@ -58,6 +67,7 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM @inject(ServerConnection) serverConnection: ServerConnection, @inject(ContentsManager) contentsManager: ContentsManager, @inject(ModalService) modalService: ModalService, + @inject(VirtualizedManager) virtualizedManager: VirtualizedManager, ) { super(); this.kernelSelection = getDefaultKernel(); @@ -68,6 +78,7 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM this.contentsManager = contentsManager; this.modalService = modalService; this.dndAreaNullEnable = true; + this.virtualizedManager = virtualizedManager; } get isKernelIdle() { @@ -154,9 +165,7 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM } return; }) - .catch(() => { - // - }); + .catch(console.error); } override async saveNotebookContent(): Promise { @@ -257,9 +266,7 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM } return; }) - .catch(() => { - // - }); + .catch(console.error); return; } @@ -283,7 +290,12 @@ export class LibroJupyterModel extends LibroModel implements ExecutableNotebookM }); if (runningCellIndex > -1) { this.selectCell(this.cells[runningCellIndex]); - this.scrollToView(this.cells[runningCellIndex]); + + if (this.virtualizedManager.isVirtualized) { + this.scrollToCellView({ cellIndex: runningCellIndex }); + } else { + this.scrollToView(this.cells[runningCellIndex]); + } } } } diff --git a/packages/libro-jupyter/src/libro-jupyter-protocol.ts b/packages/libro-jupyter/src/libro-jupyter-protocol.ts index 11190ff1..723eddce 100644 --- a/packages/libro-jupyter/src/libro-jupyter-protocol.ts +++ b/packages/libro-jupyter/src/libro-jupyter-protocol.ts @@ -52,3 +52,8 @@ export interface LibroFileService { currentFileContents: IContentsModel, ) => Promise; } + +export const ServerLaunchManager = Symbol('ServerLaunchManager'); +export interface ServerLaunchManager { + launch: () => Promise; +} diff --git a/packages/libro-jupyter/src/libro-jupyter-server-launch-manager.ts b/packages/libro-jupyter/src/libro-jupyter-server-launch-manager.ts index dabdeda1..4ad85426 100644 --- a/packages/libro-jupyter/src/libro-jupyter-server-launch-manager.ts +++ b/packages/libro-jupyter/src/libro-jupyter-server-launch-manager.ts @@ -2,8 +2,12 @@ import { ServerManager, ServerConnection } from '@difizen/libro-kernel'; import { inject, singleton } from '@difizen/mana-app'; import { ApplicationContribution } from '@difizen/mana-app'; -@singleton({ contrib: [ApplicationContribution] }) -export class JupyterServerLaunchManager implements ApplicationContribution { +import { ServerLaunchManager } from './libro-jupyter-protocol.js'; + +@singleton({ contrib: [ServerLaunchManager, ApplicationContribution] }) +export class JupyterServerLaunchManager + implements ServerLaunchManager, ApplicationContribution +{ protected serverManager: ServerManager; protected serverConnection: ServerConnection; @@ -23,5 +27,10 @@ export class JupyterServerLaunchManager implements ApplicationContribution { baseUrl: `http://${host}`, wsUrl: `ws://${host}`, }); + this.launch(); + } + + launch() { + return this.serverManager.launch(); } } diff --git a/packages/libro-jupyter/src/libro-jupyter-view.tsx b/packages/libro-jupyter/src/libro-jupyter-view.tsx deleted file mode 100644 index 2dacf0cb..00000000 --- a/packages/libro-jupyter/src/libro-jupyter-view.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { NotebookOption } from '@difizen/libro-core'; -import { CollapseServiceFactory, NotebookService } from '@difizen/libro-core'; -import { LibroView, notebookViewFactoryId } from '@difizen/libro-core'; -import { URI, view, ViewOption } from '@difizen/mana-app'; -import { inject, transient } from '@difizen/mana-app'; - -@transient() -@view(notebookViewFactoryId) -export class LibroJupyterView extends LibroView { - uri: URI; - constructor( - @inject(ViewOption) options: NotebookOption, - @inject(CollapseServiceFactory) collapseServiceFactory: CollapseServiceFactory, - @inject(NotebookService) notebookService: NotebookService, - ) { - super(options, collapseServiceFactory, notebookService); - const uri = new URI(options['resource']); - this.uri = uri; - this.title.label = uri.displayName; - } - get options() { - return this.model.options; - } -} diff --git a/packages/libro-jupyter/src/module.ts b/packages/libro-jupyter/src/module.ts index c01332ba..e92a7205 100644 --- a/packages/libro-jupyter/src/module.ts +++ b/packages/libro-jupyter/src/module.ts @@ -1,27 +1,30 @@ import { + CodeCellModule, LibroCodeCellModel, LibroCodeCellView, - CodeCellModule, -} from '@difizen/libro-codemirror-code-cell'; -import { MarkdownCellModule } from '@difizen/libro-codemirror-markdown-cell'; -import { RawCellModule } from '@difizen/libro-codemirror-raw-cell'; +} from '@difizen/libro-code-cell'; +import { CodeMirrorEditorModule } from '@difizen/libro-codemirror'; +import { LibroE2EditorModule } from '@difizen/libro-cofine-editor'; import { - LibroModule, - LibroToolbarModule, - LibroKeybindRegistry, - LibroModel, - LibroAddCellModule, CellExecutionTimeProvider, CellInputBottonBlankProvider, + LibroAddCellModule, + LibroKeybindRegistry, + LibroModel, + LibroModule, + LibroToolbarModule, } from '@difizen/libro-core'; import { LibroKernelManageModule } from '@difizen/libro-kernel'; +import { LibroLSPModule } from '@difizen/libro-lsp'; +import { MarkdownCellModule } from '@difizen/libro-markdown-cell'; import { DisplayDataOutputModule, ErrorOutputModule, StreamOutputModule, } from '@difizen/libro-output'; +import { RawCellModule } from '@difizen/libro-raw-cell'; import { LibroSearchModule } from '@difizen/libro-search'; -import { SearchCodemirrorCellModule } from '@difizen/libro-search-codemirror-cell'; +import { SearchCodeCellModule } from '@difizen/libro-search-code-cell'; import { ManaModule } from '@difizen/mana-app'; import { LibroBetweenCellModule } from './add-between-cell/index.js'; @@ -32,22 +35,19 @@ import { } from './command/index.js'; import { CellExecutionTip, CellInputBottomBlank } from './components/index.js'; import { ConfigAppContribution } from './config/index.js'; -import { LibroConfigurationContribution } from './configuration/libro-configuration-contribution.js'; import { LibroJupyterContentContribution } from './contents/index.js'; -import { LibroJupyterFileModule } from './file/index.js'; import { KeybindInstructionsModule } from './keybind-instructions/index.js'; import { LibroJupyterFileService } from './libro-jupyter-file-service.js'; import { LibroJupyterModel } from './libro-jupyter-model.js'; import { KernelStatusAndSelectorProvider } from './libro-jupyter-protocol.js'; import { JupyterServerLaunchManager } from './libro-jupyter-server-launch-manager.js'; -import { LibroJupyterView } from './libro-jupyter-view.js'; import { LibroJupyterOutputArea } from './output/index.js'; import { PlotlyModule } from './rendermime/index.js'; import { LibroJupyterColorContribution } from './theme/index.js'; import { + KernelStatusSelector, LibroJupyterToolbarContribution, SaveFileErrorContribution, - KernelStatusSelector, } from './toolbar/index.js'; export const LibroJupyterModule = ManaModule.create() @@ -63,8 +63,6 @@ export const LibroJupyterModule = ManaModule.create() LibroJupyterOutputArea, LibroJupyterColorContribution, JupyterServerLaunchManager, - LibroJupyterView, - LibroConfigurationContribution, { token: CellExecutionTimeProvider, useValue: CellExecutionTip, @@ -95,11 +93,13 @@ export const LibroJupyterModule = ManaModule.create() LibroToolbarModule, LibroKernelManageModule, LibroSearchModule, - SearchCodemirrorCellModule, + SearchCodeCellModule, LibroAddCellModule, + LibroLSPModule, + LibroE2EditorModule, + CodeMirrorEditorModule, // custom module LibroBetweenCellModule, KeybindInstructionsModule, PlotlyModule, - LibroJupyterFileModule, ); diff --git a/packages/libro-jupyter/src/output/libro-jupyter-outputarea.tsx b/packages/libro-jupyter/src/output/libro-jupyter-outputarea.tsx index 62ef1564..ef043790 100644 --- a/packages/libro-jupyter/src/output/libro-jupyter-outputarea.tsx +++ b/packages/libro-jupyter/src/output/libro-jupyter-outputarea.tsx @@ -7,17 +7,19 @@ import type { import { LibroOutputArea } from '@difizen/libro-core'; import { isDisplayDataMsg, - isStreamMsg, isErrorMsg, - isExecuteResultMsg, isExecuteReplyMsg, + isExecuteResultMsg, + isStreamMsg, + isUpdateDisplayDataMsg, } from '@difizen/libro-kernel'; -import { view, inject, transient, ViewOption } from '@difizen/mana-app'; +import { inject, transient, view, ViewOption } from '@difizen/mana-app'; @transient() @view('libro-output-area') export class LibroJupyterOutputArea extends LibroOutputArea { declare cell: LibroExecutableCellView; + protected displayIdMap = new Map(); constructor(@inject(ViewOption) option: IOutputAreaOption) { super(option); @@ -27,6 +29,8 @@ export class LibroJupyterOutputArea extends LibroOutputArea { handleMsg() { const cellModel = this.cell.model as ExecutableCellModel; cellModel.msgChangeEmitter.event((msg) => { + const transientMsg = (msg.content.transient || {}) as nbformat.JSONObject; + const displayId = transientMsg['display_id'] as string; if (msg.header.msg_type !== 'status') { if (msg.header.msg_type === 'execute_input') { cellModel.executeCount = msg.content.execution_count; @@ -43,6 +47,20 @@ export class LibroJupyterOutputArea extends LibroOutputArea { }; this.add(output); } + if (isUpdateDisplayDataMsg(msg)) { + const output = { ...msg.content, output_type: 'display_data' }; + const targets = this.displayIdMap.get(displayId); + if (targets) { + for (const index of targets) { + this.set(index, output); + } + } + } + if (displayId && isDisplayDataMsg(msg)) { + const targets = this.displayIdMap.get(displayId) || []; + targets.push(this.outputs.length); + this.displayIdMap.set(displayId, targets); + } //Handle an execute reply message. if (isExecuteReplyMsg(msg)) { const content = msg.content; @@ -68,4 +86,14 @@ export class LibroJupyterOutputArea extends LibroOutputArea { } }); } + + override dispose(): void { + this.displayIdMap.clear(); + super.dispose(); + } + + override clear(wait?: boolean | undefined): void { + super.clear(wait); + this.displayIdMap.clear(); + } } diff --git a/packages/libro-jupyter/src/rendermime/index.less b/packages/libro-jupyter/src/rendermime/index.less index 9df7a64f..854e8186 100644 --- a/packages/libro-jupyter/src/rendermime/index.less +++ b/packages/libro-jupyter/src/rendermime/index.less @@ -1,12 +1,23 @@ +/* stylelint-disable no-duplicate-selectors */ + /* Base styles */ .libro-plotly-render { width: 100%; height: 100%; padding: 0; - min-height: 360px; overflow: hidden; } +/* Document styles */ +.libro-plotly-render { + overflow: hidden; +} + +/* Output styles */ +.libro-plotly-render { + min-height: 360px; +} + /* Document icon */ .libro-PlotlyIcon { background-image: url('./assets/plotly.svg'); diff --git a/packages/libro-jupyter/src/rendermime/plotly-render.tsx b/packages/libro-jupyter/src/rendermime/plotly-render.tsx index b8ead7e5..6a40e09f 100644 --- a/packages/libro-jupyter/src/rendermime/plotly-render.tsx +++ b/packages/libro-jupyter/src/rendermime/plotly-render.tsx @@ -1,6 +1,6 @@ import type { BaseOutputView } from '@difizen/libro-core'; -import { RenderMimeRegistry } from '@difizen/libro-rendermime'; import type { IRenderMimeRegistry } from '@difizen/libro-rendermime'; +import { RenderMimeRegistry } from '@difizen/libro-rendermime'; import { useInject } from '@difizen/mana-app'; import { useEffect, useRef } from 'react'; import type { FC } from 'react'; diff --git a/packages/libro-jupyter/src/rendermime/plotly-renderers.ts b/packages/libro-jupyter/src/rendermime/plotly-renderers.ts index b8536afc..2fcabe28 100644 --- a/packages/libro-jupyter/src/rendermime/plotly-renderers.ts +++ b/packages/libro-jupyter/src/rendermime/plotly-renderers.ts @@ -73,19 +73,19 @@ export class RenderedPlotly { } } - private hasGraphElement() { + protected hasGraphElement() { // Check for the presence of the .plot-container element that plotly.js // places at the top of the figure structure return this.node.querySelector('.plot-container') !== null; } - private updateImage(png_data: string) { + protected updateImage(png_data: string) { this.hideGraph(); this._img_el.src = 'data:image/png;base64,' + png_data; this.showImage(); } - private hideGraph() { + protected hideGraph() { // Hide the graph if there is one const el = this.node.querySelector('.plot-container'); if (el !== null && el !== undefined) { @@ -93,7 +93,7 @@ export class RenderedPlotly { } } - private showGraph() { + protected showGraph() { // Show the graph if there is one const el = this.node.querySelector('.plot-container'); if (el !== null && el !== undefined) { @@ -101,7 +101,7 @@ export class RenderedPlotly { } } - private hideImage() { + protected hideImage() { // Hide the image element const el = this.node.querySelector('.plot-img'); if (el !== null && el !== undefined) { @@ -109,7 +109,7 @@ export class RenderedPlotly { } } - private showImage() { + protected showImage() { // Show the image element const el = this.node.querySelector('.plot-img'); if (el !== null && el !== undefined) { @@ -117,7 +117,7 @@ export class RenderedPlotly { } } - private createGraph(model: BaseOutputView): Promise { + protected createGraph(model: BaseOutputView): Promise { const { data, layout, frames, config } = model.data[this._mimeType] as | any | IPlotlySpec; @@ -158,21 +158,19 @@ export class RenderedPlotly { } return; }) - .catch(() => { - // - }); + .catch(console.error); } return; }); } - private _mimeType: string; - private _img_el: HTMLImageElement; - private _model: BaseOutputView; - private node: HTMLElement; - private static Plotly: typeof PlotlyType | null = null; - private static _resolveLoadingPlotly: () => void; - private static loadingPlotly = new Promise((resolve) => { + protected _mimeType: string; + protected _img_el: HTMLImageElement; + protected _model: BaseOutputView; + protected node: HTMLElement; + protected static Plotly: typeof PlotlyType | null = null; + protected static _resolveLoadingPlotly: () => void; + protected static loadingPlotly = new Promise((resolve) => { RenderedPlotly._resolveLoadingPlotly = resolve; }); } diff --git a/packages/libro-jupyter/src/toolbar/kernel-selector-dropdown.tsx b/packages/libro-jupyter/src/toolbar/kernel-selector-dropdown.tsx index 8d65e6f2..a9d3b098 100644 --- a/packages/libro-jupyter/src/toolbar/kernel-selector-dropdown.tsx +++ b/packages/libro-jupyter/src/toolbar/kernel-selector-dropdown.tsx @@ -150,9 +150,7 @@ export const KernelSelector: React.FC = () => { (libroView.model as LibroJupyterModel).kernelConnecting = false; return; }) - .catch(() => { - // - }); + .catch(console.error); return; } @@ -173,9 +171,7 @@ export const KernelSelector: React.FC = () => { (libroView.model as LibroJupyterModel).kernelConnecting = false; return; }) - .catch(() => { - // - }); + .catch(console.error); } libroView.model.kernelConnecting = false; diff --git a/packages/libro-jupyter/src/typings/index.d.ts b/packages/libro-jupyter/src/typings/index.d.ts index e14b977a..0d887825 100644 --- a/packages/libro-jupyter/src/typings/index.d.ts +++ b/packages/libro-jupyter/src/typings/index.d.ts @@ -4,6 +4,13 @@ declare module 'plotly.js' { export type Frame = Record; export function addFrames(root: Plotly.Root, frames: Frame[]): Promise; export function animate(root: Plotly.Root): void; + export function react( + root: Plotly.Root, + data: Data[], + layout: Layout, + config: any, + ): Promise; + export function toImage(root: Plotly.Root, options: any): Promise; export type Data = any; export type Layout = any; @@ -26,6 +33,4 @@ declare module 'plotly.js' { layout: Layout; on(event: PlotlyEvent, callback: (update: any) => void): void; } - export function react(node: HTMLElement, data: any, layout: any, config: any): void; - export function toImage(...args: any[]): Promise; } diff --git a/packages/libro-kernel/src/libro-kernel-connection-manager.ts b/packages/libro-kernel/src/libro-kernel-connection-manager.ts index 89cf4390..21216d13 100644 --- a/packages/libro-kernel/src/libro-kernel-connection-manager.ts +++ b/packages/libro-kernel/src/libro-kernel-connection-manager.ts @@ -12,7 +12,7 @@ export class LibroKernelConnectionManager { protected kernelManager: LibroKernelManager; @prop() - private kernelConnectionMap: Map; + protected kernelConnectionMap: Map; constructor( @inject(LibroSessionManager) sessionManager: LibroSessionManager, diff --git a/packages/libro-kernel/src/session/libro-session-manager.ts b/packages/libro-kernel/src/session/libro-session-manager.ts index fc381986..64e0e4dd 100644 --- a/packages/libro-kernel/src/session/libro-session-manager.ts +++ b/packages/libro-kernel/src/session/libro-session-manager.ts @@ -294,7 +294,7 @@ export class LibroSessionManager { /** * Send a PATCH to the server, updating the session path or the kernel. */ - private async _patch( + protected async _patch( body: DeepPartial, sessionId?: string, ): Promise { diff --git a/packages/libro-lsp/.eslintrc.mjs b/packages/libro-lsp/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-lsp/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-lsp/.fatherrc.ts b/packages/libro-lsp/.fatherrc.ts new file mode 100644 index 00000000..d7186780 --- /dev/null +++ b/packages/libro-lsp/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types'], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-lsp/CHANGELOG.md b/packages/libro-lsp/CHANGELOG.md new file mode 100644 index 00000000..5292f322 --- /dev/null +++ b/packages/libro-lsp/CHANGELOG.md @@ -0,0 +1,25 @@ +# @difizen/libro-code-editor + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version +- Updated dependencies [127cb35] +- Updated dependencies + - @difizen/libro-common@0.1.0 + - @difizen/libro-shared-model@0.1.0 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version +- Updated dependencies + - @difizen/libro-common@0.0.2-alpha.0 + - @difizen/libro-shared-model@0.0.2-alpha.0 diff --git a/packages/libro-lsp/README.md b/packages/libro-lsp/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/libro-lsp/babel.config.json b/packages/libro-lsp/babel.config.json new file mode 100644 index 00000000..51a623c5 --- /dev/null +++ b/packages/libro-lsp/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + "@babel/plugin-transform-flow-strip-types", + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-lsp/jest.config.mjs b/packages/libro-lsp/jest.config.mjs new file mode 100644 index 00000000..fa6ffdd2 --- /dev/null +++ b/packages/libro-lsp/jest.config.mjs @@ -0,0 +1,4 @@ +import configs from '../../jest.config.mjs'; + +delete configs.transformIgnorePatterns; +export default { ...configs }; diff --git a/packages/libro-lsp/package.json b/packages/libro-lsp/package.json new file mode 100644 index 00000000..42cae725 --- /dev/null +++ b/packages/libro-lsp/package.json @@ -0,0 +1,67 @@ +{ + "name": "@difizen/libro-lsp", + "version": "0.1.0", + "description": "", + "keywords": [ + "libro", + "notebook" + ], + "repository": "git@github.com:difizen/libro.git", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "typings": "./es/index.d.ts", + "default": "./es/index.js" + }, + "./mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./es/mock": { + "typings": "./es/mock/index.d.ts", + "default": "./es/mock/index.js" + }, + "./package.json": "./package.json" + }, + "main": "es/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", + "files": [ + "es", + "src" + ], + "scripts": { + "setup": "father build", + "build": "father build", + "test": ": Note: lint task is delegated to test:* scripts", + "test:vitest": "vitest run", + "test:jest": "jest", + "coverage": ": Note: lint task is delegated to coverage:* scripts", + "coverage:vitest": "vitest run --coverage", + "coverage:jest": "jest --coverage", + "lint": ": Note: lint task is delegated to lint:* scripts", + "lint:eslint": "eslint src", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@difizen/libro-core": "^0.1.0", + "@difizen/libro-kernel": "^0.1.0", + "@difizen/libro-common": "^0.1.0", + "@difizen/libro-code-editor": "^0.1.0", + "@difizen/mana-app": "latest", + "lodash.mergewith": "^4.6.2", + "uuid": "^9.0.0", + "vscode-jsonrpc": "^6.0.0", + "vscode-languageserver-protocol": "^3.17.0", + "vscode-ws-jsonrpc": "~1.0.2" + }, + "peerDependencies": { + "react": "^18.2.0" + }, + "devDependencies": { + "@types/lodash.mergewith": "^4.6.9", + "@types/react": "^18.2.25", + "@types/uuid": "^9.0.2" + } +} diff --git a/packages/libro-lsp/src/adapters/adapter.ts b/packages/libro-lsp/src/adapters/adapter.ts new file mode 100644 index 00000000..1d13d7cf --- /dev/null +++ b/packages/libro-lsp/src/adapters/adapter.ts @@ -0,0 +1,611 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { NotebookView } from '@difizen/libro-core'; +import { Emitter } from '@difizen/mana-app'; +import type { Disposable, Event, View } from '@difizen/mana-app'; +import type {} from '@difizen/mana-app'; +import mergeWith from 'lodash.mergewith'; + +import type { ClientCapabilities, LanguageIdentifier } from '../lsp.js'; +import type { IVirtualPosition } from '../positioning.js'; +import type { + Document, + IDocumentConnectionData, + ILSPCodeExtractorsManager, + ILSPDocumentConnectionManager, + ILSPFeatureManager, + ISocketConnectionOptions, +} from '../tokens.js'; +import type { VirtualDocument } from '../virtual/document.js'; + +/** + * The values should follow the https://microsoft.github.io/language-server-protocol/specification guidelines + */ +const MIME_TYPE_LANGUAGE_MAP: Record = { + 'text/x-rsrc': 'r', + 'text/x-r-source': 'r', + // currently there are no LSP servers for IPython we are aware of + 'text/x-ipython': 'python', +}; + +export interface IEditorChangedData { + /** + * The CM editor invoking the change event. + */ + editor: Document.IEditor; +} + +export interface IAdapterOptions { + /** + * The LSP document and connection manager instance. + */ + connectionManager: ILSPDocumentConnectionManager; + + /** + * The LSP feature manager instance. + */ + featureManager: ILSPFeatureManager; + + /** + * The LSP foreign code extractor manager. + */ + foreignCodeExtractorsManager: ILSPCodeExtractorsManager; +} + +/** + * Foreign code: low level adapter is not aware of the presence of foreign languages; + * it operates on the virtual document and must not attempt to infer the language dependencies + * as this would make the logic of inspections caching impossible to maintain, thus the WidgetAdapter + * has to handle that, keeping multiple connections and multiple virtual documents. + */ +export abstract class WidgetLSPAdapter implements Disposable { + // note: it could be using namespace/IOptions pattern, + // but I do not know how to make it work with the generic type T + // (other than using 'any' in the IOptions interface) + constructor( + public widget: T, + protected options: IAdapterOptions, + ) { + this._connectionManager = options.connectionManager; + this._isConnected = false; + // set up signal connections + this.widget.onSave(this.onSaveState); + // this.widget.context.saveState.connect(this.onSaveState, this); + // this.connectionManager.closed.connect(this.onConnectionClosed, this); + // this.widget.disposed.connect(this.dispose, this); + } + + /** + * Check if the adapter is disposed + */ + get disposed(): boolean { + return this._isDisposed; + } + /** + * Check if the document contains multiple editors + */ + get hasMultipleEditors(): boolean { + return this.editors.length > 1; + } + /** + * Get the ID of the internal widget. + */ + get widgetId(): string { + return this.widget.id; + } + + /** + * Get the language identifier of the document + */ + get language(): LanguageIdentifier { + // the values should follow https://microsoft.github.io/language-server-protocol/specification guidelines, + // see the table in https://microsoft.github.io/language-server-protocol/specification#textDocumentItem + if (Object.prototype.hasOwnProperty.call(MIME_TYPE_LANGUAGE_MAP, this.mimeType)) { + return MIME_TYPE_LANGUAGE_MAP[this.mimeType]; + } else { + const withoutParameters = this.mimeType.split(';')[0]; + const [type, subtype] = withoutParameters.split('/'); + if (type === 'application' || type === 'text') { + if (subtype.startsWith('x-')) { + return subtype.substring(2); + } else { + return subtype; + } + } else { + return this.mimeType; + } + } + } + + /** + * Signal emitted when the adapter is connected. + */ + get adapterConnected(): Event { + return this._adapterConnected.event; + } + + /** + * Signal emitted when the active editor have changed. + */ + get activeEditorChanged(): Event { + return this._activeEditorChanged.event; + } + + /** + * Signal emitted when the adapter is disposed. + */ + get onDispose(): Event { + return this._disposed.event; + } + + /** + * Signal emitted when the an editor is changed. + */ + get editorAdded(): Event { + return this._editorAdded.event; + } + + /** + * Signal emitted when the an editor is removed. + */ + get editorRemoved(): Event { + return this._editorRemoved.event; + } + + /** + * Get the inner HTMLElement of the document widget. + */ + abstract get wrapperElement(): HTMLElement; + + /** + * Get current path of the document. + */ + abstract get documentPath(): string; + + /** + * Get the mime type of the document. + */ + abstract get mimeType(): string; + + /** + * Get the file extension of the document. + */ + abstract get languageFileExtension(): string | undefined; + + /** + * Get the activated CM editor. + */ + abstract get activeEditor(): Document.IEditor | undefined; + + /** + * Get the list of CM editors in the document, there is only one editor + * in the case of file editor. + */ + abstract get editors(): Document.ICodeBlockOptions[]; + + /** + * Promise that resolves once the adapter is initialized + */ + abstract get ready(): Promise; + + /** + * The virtual document is connected or not + */ + get isConnected(): boolean { + return this._isConnected; + } + + /** + * The LSP document and connection manager instance. + */ + get connectionManager(): ILSPDocumentConnectionManager { + return this._connectionManager; + } + + /** + * Promise that resolves once the document is updated + */ + get updateFinished(): Promise { + return this._updateFinished; + } + + /** + * Internal virtual document of the adapter. + */ + get virtualDocument(): VirtualDocument | null { + return this._virtualDocument; + } + + /** + * Callback on connection closed event. + */ + onConnectionClosed( + _: ILSPDocumentConnectionManager, + { virtualDocument }: IDocumentConnectionData, + ): void { + if (virtualDocument === this.virtualDocument) { + this.dispose(); + } + } + + /** + * Dispose the adapter. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + this.disconnect(); + this._virtualDocument = null; + this._disposed.fire(); + } + + /** + * Disconnect virtual document from the language server. + */ + disconnect(): void { + const uri = this.virtualDocument?.uri; + if (uri) { + this.connectionManager.unregisterDocument(uri); + } + + // pretend that all editors were removed to trigger the disconnection of even handlers + // they will be connected again on new connection + for (const { ceEditor: editor } of this.editors) { + this._editorRemoved.fire({ + editor: editor, + }); + } + + this.virtualDocument?.dispose(); + } + + /** + * Update the virtual document. + */ + updateDocuments(): Promise { + if (this._isDisposed) { + console.warn('Cannot update documents: adapter disposed'); + return Promise.reject('Cannot update documents: adapter disposed'); + } + return this.virtualDocument!.updateManager.updateDocuments(this.editors); + } + + /** + * Callback called on the document changed event. + */ + documentChanged(virtualDocument: VirtualDocument): void { + if (this._isDisposed) { + console.warn('Cannot swap document: adapter disposed'); + return; + } + + // TODO only send the difference, using connection.sendSelectiveChange() + const connection = this.connectionManager.connections.get(virtualDocument.uri); + + if (!connection?.isReady) { + console.warn('Skipping document update signal: connection not ready'); + return; + } + + connection.sendFullTextChange(virtualDocument.value, virtualDocument.documentInfo); + } + + /** + * (re)create virtual document using current path and language + */ + protected abstract createVirtualDocument(): VirtualDocument; + + /** + * Get the index of editor from the cursor position in the virtual + * document. Since there is only one editor, this method always return + * 0 + * + * @param position - the position of cursor in the virtual document. + * @return - index of the virtual editor + */ + abstract getEditorIndexAt(position: IVirtualPosition): number; + + /** + * Get the index of input editor + * + * @param ceEditor - instance of the code editor + */ + abstract getEditorIndex(ceEditor: Document.IEditor): number; + + /** + * Get the index of input editor + * + * @param ceEditor - instance of the code editor + */ + abstract getCellEditor(cell: View): Document.IEditor | undefined; + + /** + * Get the wrapper of input editor. + * + * @param ceEditor + */ + abstract getEditorWrapper(ceEditor: Document.IEditor): HTMLElement | undefined; + + // equivalent to triggering didClose and didOpen, as per syncing specification, + // but also reloads the connection; used during file rename (or when it was moved) + protected reloadConnection(): void { + // ignore premature calls (before the editor was initialized) + if (this.virtualDocument === null) { + return; + } + + // disconnect all existing connections (and dispose adapters) + this.disconnect(); + + // recreate virtual document using current path and language + // as virtual editor assumes it gets the virtual document at init, + // just dispose virtual editor (which disposes virtual document too) + // and re-initialize both virtual editor and document + this.initVirtual(); + + // reconnect + this.connectDocument(this.virtualDocument, true).catch(console.warn); + } + + /** + * Callback on document saved event. + */ + protected onSaveState = (): void => { + // ignore premature calls (before the editor was initialized) + if (this.virtualDocument === null) { + return; + } + + const documentsToSave = [this.virtualDocument]; + + for (const virtualDocument of documentsToSave) { + const connection = this.connectionManager.connections.get(virtualDocument.uri); + if (!connection) { + continue; + } + connection.sendSaved(virtualDocument.documentInfo); + for (const foreign of virtualDocument.foreignDocuments.values()) { + documentsToSave.push(foreign); + } + } + }; + + /** + * Connect the virtual document with the language server. + */ + protected async onConnected(data: IDocumentConnectionData): Promise { + const { virtualDocument } = data; + + this._adapterConnected.fire(data); + this._isConnected = true; + + try { + await this.updateDocuments(); + } catch (reason) { + console.warn('Could not update documents', reason); + return; + } + + // refresh the document on the LSP server + this.documentChanged(virtualDocument); + + data.connection.serverNotifications['$/logTrace'].event((message) => { + console.warn( + data.connection.serverIdentifier, + 'trace', + virtualDocument.uri, + message, + ); + }); + + data.connection.serverNotifications['window/logMessage'].event((message) => { + console.warn(data.connection.serverIdentifier + ': ' + message.message); + }); + + data.connection.serverNotifications['window/showMessage'].event((message) => { + // void showDialog({ + // title: this.trans.__('Message from ') + connection.serverIdentifier, + // body: message.message, + // }); + alert(`Message from ${data.connection.serverIdentifier}: ${message.message}`); + }); + + data.connection.serverRequests['window/showMessageRequest'].setHandler( + async (params) => { + alert(`Message from ${data.connection.serverIdentifier}: ${params.message}`); + return null; + + // const actionItems = params.actions; + // const buttons = actionItems + // ? actionItems.map(action => { + // return createButton({ + // label: action.title, + // }); + // }) + // : [createButton({ label: this.trans.__('Dismiss') })]; + // const result = await showDialog({ + // title: this.trans.__('Message from ') + data.connection.serverIdentifier, + // body: params.message, + // buttons: buttons, + // }); + // const choice = buttons.indexOf(result.button); + // if (choice === -1) { + // return null; + // } + // if (actionItems) { + // return actionItems[choice]; + // } + // return null; + }, + ); + } + + /** + * Opens a connection for the document. The connection may or may + * not be initialized, yet, and depending on when this is called, the client + * may not be fully connected. + * + * @param virtualDocument a VirtualDocument + * @param sendOpen whether to open the document immediately + */ + protected async connectDocument( + virtualDocument: VirtualDocument, + sendOpen = false, + ): Promise { + virtualDocument.foreignDocumentOpened(this.onForeignDocumentOpened, this); + const connectionContext = await this._connect(virtualDocument).catch(console.error); + + if (connectionContext && connectionContext.connection) { + virtualDocument.changed(this.documentChanged, this); + if (sendOpen) { + connectionContext.connection.sendOpenWhenReady(virtualDocument.documentInfo); + } + } + } + + /** + * Create the virtual document using current path and language. + */ + protected initVirtual(): void { + const { model } = this.widget; + this._virtualDocument?.dispose(); + this._virtualDocument = this.createVirtualDocument(); + model.onSourceChanged?.(() => this._onContentChanged()); + } + + /** + * Handler for opening a document contained in a parent document. The assumption + * is that the editor already exists for this, and as such the document + * should be queued for immediate opening. + * + * @param host the VirtualDocument that contains the VirtualDocument in another language + * @param context information about the foreign VirtualDocument + */ + protected async onForeignDocumentOpened( + context: Document.IForeignContext, + ): Promise { + const { foreignDocument } = context; + + await this.connectDocument(foreignDocument, true); + + foreignDocument.foreignDocumentClosed(this._onForeignDocumentClosed, this); + } + + /** + * Signal emitted when the adapter is connected. + */ + protected _adapterConnected = new Emitter(); + + /** + * Signal emitted when the active editor have changed. + */ + protected _activeEditorChanged = new Emitter(); + + /** + * Signal emitted when an editor is changed. + */ + protected _editorAdded = new Emitter(); + + /** + * Signal emitted when an editor is removed. + */ + protected _editorRemoved = new Emitter(); + + /** + * Signal emitted when the adapter is disposed. + */ + protected _disposed = new Emitter(); + + protected _isDisposed = false; + + protected readonly _connectionManager: ILSPDocumentConnectionManager; + + protected _isConnected: boolean; + protected _updateFinished: Promise; + protected _virtualDocument: VirtualDocument | null = null; + + /** + * Callback called when a foreign document is closed, + * the associated signals with this virtual document + * are disconnected. + */ + protected _onForeignDocumentClosed(context: Document.IForeignContext): void { + // const { foreignDocument } = context; + } + + /** + * Detect the capabilities for the document type then + * open the websocket connection with the language server. + */ + protected async _connect(virtualDocument: VirtualDocument) { + const language = virtualDocument.language; + + let capabilities: ClientCapabilities = { + textDocument: { + synchronization: { + dynamicRegistration: true, + willSave: false, + didSave: true, + willSaveWaitUntil: false, + }, + }, + workspace: { + didChangeConfiguration: { + dynamicRegistration: true, + }, + }, + }; + capabilities = mergeWith( + capabilities, + this.options.featureManager.clientCapabilities(), + ); + + const options: ISocketConnectionOptions = { + capabilities, + virtualDocument, + language, + hasLspSupportedFile: virtualDocument.hasLspSupportedFile, + }; + + const connection = await this.connectionManager.connect(options); + + if (connection) { + await this.onConnected({ virtualDocument, connection }); + + return { + connection, + virtualDocument, + }; + } else { + return undefined; + } + } + + /** + * Handle content changes and update all virtual documents after a change. + * + * #### Notes + * Update to the state of a notebook may be done without a notice on the + * CodeMirror level, e.g. when a cell is deleted. Therefore a + * JupyterLab-specific signal is watched instead. + * + * While by not using the change event of CodeMirror editors we lose an easy + * way to send selective (range) updates this can be still implemented by + * comparison of before/after states of the virtual documents, which is + * more resilient and editor-independent. + */ + protected async _onContentChanged() { + // Update the virtual documents. + // Sending the updates to LSP is out of scope here. + const promise = this.updateDocuments(); + if (!promise) { + console.warn('Could not update documents'); + return; + } + this._updateFinished = promise.catch(console.warn); + await this.updateFinished; + } +} diff --git a/packages/libro-lsp/src/adapters/notebook-adapter.ts b/packages/libro-lsp/src/adapters/notebook-adapter.ts new file mode 100644 index 00000000..e236e766 --- /dev/null +++ b/packages/libro-lsp/src/adapters/notebook-adapter.ts @@ -0,0 +1,463 @@ +/* eslint-disable @typescript-eslint/no-parameter-properties */ +/* eslint-disable @typescript-eslint/parameter-properties */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type * as nbformat from '@difizen/libro-common'; +import type { + CellView, + CellViewChange, + LibroModel, + LibroView, +} from '@difizen/libro-core'; +import { EditorCellView } from '@difizen/libro-core'; +import type { ExecutableNotebookModel } from '@difizen/libro-kernel'; +import {} from '@difizen/mana-app'; +import { watch, Deferred } from '@difizen/mana-app'; + +import type { IVirtualPosition } from '../positioning.js'; +import type { Document } from '../tokens.js'; +import { untilReady } from '../utils.js'; +import { VirtualDocument } from '../virtual/document.js'; + +import type { IAdapterOptions } from './adapter.js'; +import { WidgetLSPAdapter } from './adapter.js'; + +type ILanguageInfoMetadata = nbformat.ILanguageInfoMetadata; + +export class NotebookAdapter extends WidgetLSPAdapter { + constructor( + public editorWidget: LibroView, + protected override options: IAdapterOptions, + ) { + super(editorWidget, options); + this._editorToCell = new Map(); + this.editor = editorWidget; + this._cellToEditor = new WeakMap(); + Promise.all([this.notebookModel.kcReady, this.connectionManager.ready]) + .then(async () => { + await this.initOnceReady(); + this._readyDelegate.resolve(); + return; + }) + .catch(console.error); + } + + /** + * The wrapped `Notebook` widget. + */ + readonly editor: LibroView; + + get notebookModel() { + return this.widget.model as ExecutableNotebookModel; + } + + get fileContents() { + return this.notebookModel.currentFileContents; + } + + /** + * Get current path of the document. + */ + get documentPath(): string { + return this.fileContents.path; + } + + /** + * Get the mime type of the document. + */ + get mimeType(): string { + let mimeType: string | string[]; + const languageMetadata = this.language_info(); + if (!languageMetadata || !languageMetadata.mimetype) { + // fallback to the code cell mime type if no kernel in use + mimeType = this.fileContents.mimetype!; + } else { + mimeType = languageMetadata.mimetype; + } + return Array.isArray(mimeType) ? mimeType[0] ?? 'text/plain' : mimeType; + } + + /** + * Get the file extension of the document. + */ + get languageFileExtension(): string | undefined { + const languageMetadata = this.language_info(); + if (!languageMetadata || !languageMetadata.file_extension) { + return; + } + return languageMetadata.file_extension.replace('.', ''); + } + + /** + * Get the inner HTMLElement of the document widget. + */ + get wrapperElement(): HTMLElement { + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + return this.widget.container?.current!; + } + + /** + * Get the list of CM editor with its type in the document, + */ + get editors(): Document.ICodeBlockOptions[] { + if (this.disposed) { + return []; + } + + this._editorToCell.clear(); + + return this.editor.model.cells + .filter( + (item) => EditorCellView.is(item) && item.model.mimeType === 'text/x-python', + ) + .map((cell) => { + return { + ceEditor: this.getCellEditor(cell)!, + type: cell.model.type, + value: cell.model.value, + }; + }); + } + + /** + * Get the activated CM editor. + */ + get activeEditor(): Document.IEditor | undefined { + return this.editor.activeCell + ? this.getCellEditor(this.editor.activeCell) + : undefined; + } + + /** + * Promise that resolves once the adapter is initialized + */ + get ready(): Promise { + return this._readyDelegate.promise; + } + + /** + * Get the index of editor from the cursor position in the virtual + * document. + * + * @param position - the position of cursor in the virtual document. + */ + getEditorIndexAt(position: IVirtualPosition): number { + const cell = this._getCellAt(position); + return this.editor.model.cells.findIndex((otherCell) => { + return cell === otherCell; + }); + } + + /** + * Get the index of input editor + * + * @param ceEditor - instance of the code editor + */ + getEditorIndex(ceEditor: Document.IEditor): number { + const cell = this._editorToCell.get(ceEditor)!; + return this.editor.model.cells.findIndex((otherCell) => { + return cell === otherCell; + }); + } + + /** + * Get the wrapper of input editor. + * + * @param ceEditor - instance of the code editor + */ + getEditorWrapper(ceEditor: Document.IEditor): HTMLElement | undefined { + const cell = this._editorToCell.get(ceEditor)!; + return EditorCellView.is(cell) ? cell.editor?.host : undefined; + } + + /** + * Callback on kernel changed event, it will disconnect the + * document with the language server and then reconnect. + * + * @param _session - Session context of changed kernel + * @param change - Changed data + */ + async onKernelChanged(): Promise { + try { + // note: we need to wait until ready before updating language info + const oldLanguageInfo = this._languageInfo; + await untilReady(this.isReady, -1); + await this._updateLanguageInfo(); + const newLanguageInfo = this._languageInfo; + if ( + oldLanguageInfo?.name !== newLanguageInfo.name || + oldLanguageInfo?.mimetype !== newLanguageInfo?.mimetype || + oldLanguageInfo?.file_extension !== newLanguageInfo?.file_extension + ) { + console.warn(`Changed to ${this._languageInfo.name} kernel, reconnecting`); + this.reloadConnection(); + } else { + console.warn( + 'Keeping old LSP connection as the new kernel uses the same langauge', + ); + } + } catch (err) { + console.warn(err); + // try to reconnect anyway + this.reloadConnection(); + } + } + + /** + * Dispose the widget. + */ + override dispose(): void { + if (this.disposed) { + return; + } + + super.dispose(); + + // editors are needed for the parent dispose() to unbind signals, so they are the last to go + this._editorToCell.clear(); + } + + /** + * Method to check if the notebook context is ready. + */ + isReady(): boolean { + return ( + !this.widget.isDisposed && + // this.notebookModel.currentKernelStatus !== undefined && + this.widget.isVisible && + this.notebookModel.cells.length > 0 && + this.notebookModel.kernelConnection !== null + ); + } + + handleCellSourceChange = async () => { + await this.updateDocuments(); + }; + + /** + * Update the virtual document on cell changing event. + * + * @param cells - Observable list of changed cells + * @param change - Changed data + */ + handleCellChange = async (change: CellViewChange): Promise => { + // const cellsAdded: ICellModel[] = []; + // const cellsRemoved: ICellModel[] = []; + // const type = this._type; + // if (change.type === 'set') { + // // handling of conversions is important, because the editors get re-used and their handlers inherited, + // // so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells. + // const convertedToMarkdownOrRaw = []; + // const convertedToCode = []; + + // if (change.newValues.length === change.oldValues.length) { + // // during conversion the cells should not get deleted nor added + // for (let i = 0; i < change.newValues.length; i++) { + // if (change.oldValues[i].type === type && change.newValues[i].type !== type) { + // convertedToMarkdownOrRaw.push(change.newValues[i]); + // } else if (change.oldValues[i].type !== type && change.newValues[i].type === type) { + // convertedToCode.push(change.newValues[i]); + // } + // } + // cellsAdded = convertedToCode; + // cellsRemoved = convertedToMarkdownOrRaw; + // } + // } else if (change.type == 'add') { + // cellsAdded = change.newValues.filter(cellModel => cellModel.type === type); + // } + // note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion) + // because there is no easy way to get the widget associated with the removed cell(s) - because it is no + // longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary + // as (except for a tiny memory leak) it should not impact the functionality in any way + + // if ( + // cellsRemoved.length || + // cellsAdded.length || + // change.type === 'set' || + // change.type === 'move' || + // change.type === 'remove' + // ) { + // in contrast to the file editor document which can be only changed by the modification of the editor content, + // the notebook document cna also get modified by a change in the number or arrangement of editors themselves; + // for this reason each change has to trigger documents update (so that LSP mirror is in sync). + await this.updateDocuments(); + // } + + for (const cellView of change.insert?.cells ?? []) { + // const cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id); + // if (!cellWidget) { + // console.warn(`Widget for added cell with ID: ${cellModel.id} not found!`); + // continue; + // } + + // Add editor to the mapping if needed + this.getCellEditor(cellView); + } + }; + + /** + * Generate the virtual document associated with the document. + */ + createVirtualDocument(): VirtualDocument { + return new VirtualDocument({ + language: this.language, + foreignCodeExtractors: this.options.foreignCodeExtractorsManager, + path: this.documentPath, + fileExtension: this.languageFileExtension, + // notebooks are continuous, each cell is dependent on the previous one + standalone: false, + // notebooks are not supported by LSP servers + hasLspSupportedFile: false, + }); + } + + /** + * Get the metadata of notebook. + */ + protected language_info(): ILanguageInfoMetadata { + return this._languageInfo; + } + /** + * Initialization function called once the editor and the LSP connection + * manager is ready. This function will create the virtual document and + * connect various signals. + */ + protected initOnceReady = async (): Promise => { + await untilReady(this.isReady.bind(this), -1); + await this._updateLanguageInfo(); + this.initVirtual(); + + // connect the document, but do not open it as the adapter will handle this + // after registering all features + this.connectDocument(this.virtualDocument!, false).catch((error) => { + console.warn(error); + }); + + // this.widget.context.sessionContext.kernelChanged.connect(this.onKernelChanged, this); + watch(this.notebookModel, 'kernelConnection', this.onKernelChanged); + + watch(this.notebookModel, 'active', ({ target }) => + this._activeCellChanged(target), + ); + + this._connectModelSignals(this.widget); + }; + + /** + * Connect the cell changed event to its handler + * + * @param notebook - The notebook that emitted event. + */ + protected _connectModelSignals(notebook: LibroView) { + if (notebook.model === null) { + console.warn( + `Model is missing for notebook ${notebook}, cannot connect cell changed signal!`, + ); + } else { + notebook.model.onSourceChanged(this.handleCellSourceChange); + notebook.model.onCellViewChanged(this.handleCellChange); + } + } + + /** + * Update the stored language info with the one from the notebook. + */ + protected async _updateLanguageInfo(): Promise { + const language_info = (await this.notebookModel.kernelConnection?.info) + ?.language_info; + // const language_info = (await this.widget.context.sessionContext?.session?.kernel?.info) + // ?.language_info; + if (language_info) { + this._languageInfo = language_info; + } else { + throw new Error( + 'Language info update failed (no session, kernel, or info available)', + ); + } + } + + /** + * Handle the cell changed event + * @param notebook - The notebook that emitted event + * @param cell - Changed cell. + */ + protected _activeCellChanged(libroModel: LibroModel | null) { + if (!libroModel || libroModel.active?.model.type !== this._type) { + return; + } + + this._activeEditorChanged.fire({ + editor: this.getCellEditor(libroModel.active)!, + }); + } + + /** + * Get the cell at the cursor position of the virtual document. + * @param pos - Position in the virtual document. + */ + protected _getCellAt(pos: IVirtualPosition): CellView { + const editor = this.virtualDocument!.getEditorAtVirtualLine(pos); + return this._editorToCell.get(editor)!; + } + + /** + * Get the cell editor and add new ones to the mappings. + * + * @param cell Cell widget + * @returns Cell editor accessor + */ + getCellEditor(cell: CellView): Document.IEditor | undefined { + if (!EditorCellView.is(cell)) { + return; + } + if (!this._cellToEditor.has(cell)) { + const editor = Object.freeze({ + getEditor: () => cell.editor!, + ready: async () => { + // await cell.ready; + return cell.editor!; + }, + reveal: async () => { + // await this.editor.scrollToCell(cell); + return cell.editor!; + }, + }); + + this._cellToEditor.set(cell, editor); + this._editorToCell.set(editor, cell); + cell.onDisposed(() => { + this._cellToEditor.delete(cell); + this._editorToCell.delete(editor); + this._editorRemoved.fire({ + editor, + }); + }); + + this._editorAdded.fire({ + editor, + }); + } + + return this._cellToEditor.get(cell)!; + } + + /** + * A map between the editor accessor and the containing cell + */ + protected _editorToCell: Map; + + /** + * Mapping of cell to editor accessor to ensure accessor uniqueness. + */ + protected _cellToEditor: WeakMap; + + /** + * Metadata of the notebook + */ + protected _languageInfo: ILanguageInfoMetadata; + + protected _type: nbformat.CellType = 'code'; + + protected _readyDelegate = new Deferred(); +} diff --git a/packages/libro-lsp/src/adapters/status-message.ts b/packages/libro-lsp/src/adapters/status-message.ts new file mode 100644 index 00000000..5fecb314 --- /dev/null +++ b/packages/libro-lsp/src/adapters/status-message.ts @@ -0,0 +1,93 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { Emitter } from '@difizen/mana-app'; +import type { Disposable } from '@difizen/mana-app'; + +export class StatusMessage implements Disposable { + /** + * Timeout reference used to clear the previous `setTimeout` call. + */ + protected _timer: number | null; + /** + * The text message to be shown on the statusbar + */ + protected _message: string; + + protected _changed = new Emitter(); + + protected _isDisposed = false; + + constructor() { + this._message = ''; + this._timer = null; + } + + /** + * Signal emitted on status changed event. + */ + get changed(): Emitter { + return this._changed; + } + + /** + * Test whether the object is disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose the object. + */ + dispose(): void { + if (this.isDisposed) { + return; + } + this._isDisposed = true; + if (this._timer) { + window.clearTimeout(this._timer); + } + } + + /** + * The text message to be shown on the statusbar. + */ + get message(): string { + return this._message; + } + + /** + * Set the text message and (optionally) the timeout to remove it. + * @param message + * @param timeout - number of ms to until the message is cleaned; + * -1 if the message should stay up indefinitely; + * defaults to 3000ms (3 seconds) + */ + set(message: string, timeout: number = 1000 * 3): void { + this._expireTimer(); + this._message = message; + this._changed.fire(); + if (timeout !== -1) { + this._timer = window.setTimeout(this.clear.bind(this), timeout); + } + } + + /** + * Clear the status message. + */ + clear(): void { + this._message = ''; + this._changed.fire(); + } + + /** + * Clear the previous `setTimeout` call. + */ + protected _expireTimer(): void { + if (this._timer !== null) { + window.clearTimeout(this._timer); + this._timer = null; + } + } +} diff --git a/packages/libro-lsp/src/connection-manager.ts b/packages/libro-lsp/src/connection-manager.ts new file mode 100644 index 00000000..4e716741 --- /dev/null +++ b/packages/libro-lsp/src/connection-manager.ts @@ -0,0 +1,626 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +// import { PageConfig, URL } from '@jupyterlab/coreutils'; +// import type { IDocumentWidget } from '@jupyterlab/docregistry'; + +import { URL } from '@difizen/libro-common'; +import type { NotebookView } from '@difizen/libro-core'; +import { PageConfig } from '@difizen/libro-kernel'; +import type { Event } from '@difizen/mana-app'; +import { Emitter, inject, singleton } from '@difizen/mana-app'; +import type * as protocol from 'vscode-languageserver-protocol'; + +import type { WidgetLSPAdapter } from './adapters/adapter.js'; +import { LSPConnection } from './connection.js'; +import type { ClientCapabilities } from './lsp.js'; +import type { AskServersToSendTraceNotifications } from './plugin.js'; +import type { + Document, + IDocumentConnectionData, + ILanguageServerManager, + ILSPConnection, + ISocketConnectionOptions, + TLanguageServerConfigurations, + TLanguageServerId, + TServerKeys, +} from './tokens.js'; +import { + ILSPDocumentConnectionManager, + ILanguageServerManagerFactory, +} from './tokens.js'; +import { expandDottedPaths, sleep, untilReady } from './utils.js'; +import type { VirtualDocument } from './virtual/document.js'; + +/** + * Each Widget with a document (whether file or a notebook) has the same DocumentConnectionManager + * (see JupyterLabWidgetAdapter). Using id_path instead of uri led to documents being overwritten + * as two identical id_paths could be created for two different notebooks. + */ +@singleton({ token: ILSPDocumentConnectionManager }) +export class DocumentConnectionManager implements ILSPDocumentConnectionManager { + constructor( + @inject(ILanguageServerManagerFactory) + languageServerManagerFactory: ILanguageServerManagerFactory, + ) { + this.connections = new Map(); + this.documents = new Map(); + this.adapters = new Map(); + this._ignoredLanguages = new Set(); + this.languageServerManager = languageServerManagerFactory({}); + Private.setLanguageServerManager(this.languageServerManager); + } + + /** + * Map between the URI of the virtual document and its connection + * to the language server + */ + readonly connections: Map; + + /** + * Map between the path of the document and its adapter + */ + readonly adapters: Map>; + + /** + * Map between the URI of the virtual document and the document itself. + */ + readonly documents: Map; + /** + * The language server manager plugin. + */ + readonly languageServerManager: ILanguageServerManager; + + /** + * Initial configuration for the language servers. + */ + initialConfigurations: TLanguageServerConfigurations; + + /** + * Signal emitted when the manager is initialized. + */ + get initialized(): Event { + return this._initialized.event; + } + + /** + * Signal emitted when the manager is connected to the server + */ + get connected(): Event { + return this._connected.event; + } + + /** + * Connection temporarily lost or could not be fully established; a re-connection will be attempted; + */ + get disconnected(): Event { + return this._disconnected.event; + } + + /** + * Connection was closed permanently and no-reconnection will be attempted, e.g.: + * - there was a serious server error + * - user closed the connection, + * - re-connection attempts exceeded, + */ + get closed(): Event { + return this._closed.event; + } + + /** + * Signal emitted when the document is changed. + */ + get documentsChanged(): Event> { + return this._documentsChanged.event; + } + + /** + * Promise resolved when the language server manager is ready. + */ + get ready(): Promise { + return Private.getLanguageServerManager().ready; + } + + /** + * Helper to connect various virtual document signal with callbacks of + * this class. + * + * @param virtualDocument - virtual document to be connected. + */ + connectDocumentSignals(virtualDocument: VirtualDocument): void { + virtualDocument.foreignDocumentOpened(this.onForeignDocumentOpened, this); + + virtualDocument.foreignDocumentClosed(this.onForeignDocumentClosed, this); + this.documents.set(virtualDocument.uri, virtualDocument); + this._documentsChanged.fire(this.documents); + } + + /** + * Helper to disconnect various virtual document signal with callbacks of + * this class. + * + * @param virtualDocument - virtual document to be disconnected. + */ + disconnectDocumentSignals(virtualDocument: VirtualDocument, emit = true): void { + this.documents.delete(virtualDocument.uri); + for (const foreign of virtualDocument.foreignDocuments.values()) { + this.disconnectDocumentSignals(foreign, false); + } + + if (emit) { + this._documentsChanged.fire(this.documents); + } + } + + /** + * Handle foreign document opened event. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onForeignDocumentOpened(context: Document.IForeignContext): void { + /** no-op */ + } + + /** + * Handle foreign document closed event. + */ + onForeignDocumentClosed(context: Document.IForeignContext): void { + const { foreignDocument } = context; + this.unregisterDocument(foreignDocument.uri, false); + this.disconnectDocumentSignals(foreignDocument); + } + + /** + * Register a widget adapter with this manager + * + * @param path - path to the inner document of the adapter + * @param adapter - the adapter to be registered + */ + registerAdapter(path: string, adapter: WidgetLSPAdapter): void { + this.adapters.set(path, adapter); + adapter.onDispose(() => { + if (adapter.virtualDocument) { + this.documents.delete(adapter.virtualDocument.uri); + } + this.adapters.delete(path); + }); + } + + /** + * Handles the settings that do not require an existing connection + * with a language server (or can influence to which server the + * connection will be created, e.g. `rank`). + * + * This function should be called **before** initialization of servers. + */ + updateConfiguration(allServerSettings: TLanguageServerConfigurations): void { + this.languageServerManager.setConfiguration(allServerSettings); + } + + /** + * Handles the settings that the language servers accept using + * `onDidChangeConfiguration` messages, which should be passed under + * the "serverSettings" keyword in the setting registry. + * Other configuration options are handled by `updateConfiguration` instead. + * + * This function should be called **after** initialization of servers. + */ + updateServerConfigurations(allServerSettings: TLanguageServerConfigurations): void { + let languageServerId: TServerKeys; + + for (languageServerId in allServerSettings) { + if (!Object.prototype.hasOwnProperty.call(allServerSettings, languageServerId)) { + continue; + } + const rawSettings = allServerSettings[languageServerId]!; + + const parsedSettings = expandDottedPaths(rawSettings.configuration || {}); + + const serverSettings: protocol.DidChangeConfigurationParams = { + settings: parsedSettings, + }; + + Private.updateServerConfiguration(languageServerId, serverSettings); + } + } + + /** + * Fired the first time a connection is opened. These _should_ be the only + * invocation of `.on` (once remaining LSPFeature.connection_handlers are made + * singletons). + */ + onNewConnection = (connection: LSPConnection): void => { + const errorSignalSlot = (e: any): void => { + console.error(e); + const error: Error = e.length && e.length >= 1 ? e[0] : new Error(); + if (error.message.indexOf('code = 1005') !== -1) { + console.error(`Connection failed for ${connection}`); + this._forEachDocumentOfConnection(connection, (virtualDocument) => { + console.error('disconnecting ' + virtualDocument.uri); + this._closed.fire({ connection, virtualDocument }); + this._ignoredLanguages.add(virtualDocument.language); + console.error( + `Cancelling further attempts to connect ${virtualDocument.uri} and other documents for this language (no support from the server)`, + ); + }); + } else if (error.message.indexOf('code = 1006') !== -1) { + console.error('Connection closed by the server'); + } else { + console.error('Connection error:', e); + } + }; + connection.errorSignal(errorSignalSlot); + + const serverInitializedSlot = (): void => { + // Initialize using settings stored in the SettingRegistry + this._forEachDocumentOfConnection(connection, (virtualDocument) => { + // TODO: is this still necessary, e.g. for status bar to update responsively? + this._initialized.fire({ connection, virtualDocument }); + }); + this.updateServerConfigurations(this.initialConfigurations); + }; + connection.serverInitialized(serverInitializedSlot); + + const closeSignalSlot = (closedManually: boolean) => { + if (!closedManually) { + console.error('Connection unexpectedly disconnected'); + } else { + console.warn('Connection closed'); + this._forEachDocumentOfConnection(connection, (virtualDocument) => { + this._closed.fire({ connection, virtualDocument }); + }); + } + }; + connection.closeSignal(closeSignalSlot); + }; + + /** + * Retry to connect to the server each `reconnectDelay` seconds + * and for `retrialsLeft` times. + * TODO: presently no longer referenced. A failing connection would close + * the socket, triggering the language server on the other end to exit. + */ + async retryToConnect( + options: ISocketConnectionOptions, + reconnectDelay: number, + retrialsLeft = -1, + ): Promise { + const { virtualDocument } = options; + + if (this._ignoredLanguages.has(virtualDocument.language)) { + return; + } + + let interval = reconnectDelay * 1000; + let success = false; + + while (retrialsLeft !== 0 && !success) { + await this.connect(options) + // eslint-disable-next-line @typescript-eslint/no-loop-func + .then(() => { + success = true; + return; + }) + .catch((e) => { + console.warn(e); + }); + + console.warn('will attempt to re-connect in ' + interval / 1000 + ' seconds'); + await sleep(interval); + + // gradually increase the time delay, up to 5 sec + interval = interval < 5 * 1000 ? interval + 500 : interval; + } + } + + /** + * Disconnect the connection to the language server of the requested + * language. + */ + disconnect(languageId: TLanguageServerId): void { + Private.disconnect(languageId); + } + + /** + * Create a new connection to the language server + * @return A promise of the LSP connection + */ + async connect( + options: ISocketConnectionOptions, + firstTimeoutSeconds = 30, + secondTimeoutMinutes = 5, + ): Promise { + const connection = await this._connectSocket(options); + const { virtualDocument } = options; + if (!connection) { + return; + } + if (!connection.isReady) { + try { + // user feedback hinted that 40 seconds was too short and some users are willing to wait more; + // to make the best of both worlds we first check frequently (6.6 times a second) for the first + // 30 seconds, and show the warning early in case if something is wrong; we then continue retrying + // for another 5 minutes, but only once per second. + await untilReady( + () => connection.isReady, + Math.round((firstTimeoutSeconds * 1000) / 150), + 150, + ); + } catch { + console.warn( + `Connection to ${virtualDocument.uri} timed out after ${firstTimeoutSeconds} seconds, will continue retrying for another ${secondTimeoutMinutes} minutes`, + ); + try { + await untilReady(() => connection.isReady, 60 * secondTimeoutMinutes, 1000); + } catch { + console.warn( + `Connection to ${virtualDocument.uri} timed out again after ${secondTimeoutMinutes} minutes, giving up`, + ); + return; + } + } + } + + this._connected.fire({ connection, virtualDocument }); + + return connection; + } + + /** + * Disconnect the signals of requested virtual document uri. + */ + unregisterDocument(uri: string, emit = true): void { + const connection = this.connections.get(uri); + if (connection) { + this.connections.delete(uri); + const allConnection = new Set(this.connections.values()); + + if (!allConnection.has(connection)) { + this.disconnect(connection.serverIdentifier as TLanguageServerId); + connection.dispose(); + } + if (emit) { + this._documentsChanged.fire(this.documents); + } + } + } + + /** + * Enable or disable the logging feature of the language servers + */ + updateLogging( + logAllCommunication: boolean, + setTrace: AskServersToSendTraceNotifications, + ): void { + for (const connection of this.connections.values()) { + connection.logAllCommunication = logAllCommunication; + if (setTrace !== null) { + connection.clientNotifications['$/setTrace'].fire({ value: setTrace }); + } + } + } + + /** + * Create the LSP connection for requested virtual document. + * + * @return Return the promise of the LSP connection. + */ + + protected async _connectSocket( + options: ISocketConnectionOptions, + ): Promise { + const { language, capabilities, virtualDocument } = options; + + this.connectDocumentSignals(virtualDocument); + + const uris = DocumentConnectionManager.solveUris(virtualDocument, language); + const matchingServers = this.languageServerManager.getMatchingServers({ + language, + }); + + // for now use only the server with the highest rank. + const languageServerId = matchingServers.length === 0 ? null : matchingServers[0]; + + // lazily load 1) the underlying library (1.5mb) and/or 2) a live WebSocket- + // like connection: either already connected or potentially in the process + // of connecting. + if (!uris) { + return; + } + const connection = await Private.connection( + language, + languageServerId!, + uris, + this.onNewConnection, + capabilities, + ); + + // if connecting for the first time, all documents subsequent documents will + // be re-opened and synced + this.connections.set(virtualDocument.uri, connection); + + return connection; + } + + /** + * Helper to apply callback on all documents of a connection. + */ + protected _forEachDocumentOfConnection( + connection: ILSPConnection, + callback: (virtualDocument: VirtualDocument) => void, + ) { + for (const [virtualDocumentUri, currentConnection] of this.connections.entries()) { + if (connection !== currentConnection) { + continue; + } + callback(this.documents.get(virtualDocumentUri)!); + } + } + + protected _initialized = new Emitter(); + + protected _connected = new Emitter(); + + protected _disconnected = new Emitter(); + + protected _closed = new Emitter(); + + protected _documentsChanged = new Emitter< + Map + >(); + + /** + * Set of ignored languages + */ + protected _ignoredLanguages: Set; +} + +export namespace DocumentConnectionManager { + export interface IOptions { + /** + * The language server manager instance. + */ + languageServerManager: ILanguageServerManager; + } + + /** + * Generate the URI of a virtual document from input + * + * @param virtualDocument - the virtual document + * @param language - language of the document + */ + export function solveUris( + virtualDocument: VirtualDocument, + language: string, + ): IURIs | undefined { + const wsBase = PageConfig.getBaseUrl().replace(/^http/, 'ws'); + const rootUri = PageConfig.getOption('rootUri'); + const virtualDocumentsUri = PageConfig.getOption('virtualDocumentsUri'); + + const baseUri = virtualDocument.hasLspSupportedFile ? rootUri : virtualDocumentsUri; + + // for now take the best match only + const matchingServers = Private.getLanguageServerManager().getMatchingServers({ + language, + }); + const languageServerId = matchingServers.length === 0 ? null : matchingServers[0]; + + if (languageServerId === null) { + return; + } + + // workaround url-parse bug(s) (see https://github.com/jupyter-lsp/jupyterlab-lsp/issues/595) + let documentUri = URL.join(baseUri, virtualDocument.uri); + if (!documentUri.startsWith('file:///') && documentUri.startsWith('file://')) { + documentUri = documentUri.replace('file://', 'file:///'); + if ( + documentUri.startsWith('file:///users/') && + baseUri.startsWith('file:///Users/') + ) { + documentUri = documentUri.replace('file:///users/', 'file:///Users/'); + } + } + + return { + base: baseUri, + document: documentUri, + server: URL.join('ws://jupyter-lsp', language), + socket: URL.join(wsBase, 'lsp', 'ws', languageServerId), + }; + } + + export interface IURIs { + /** + * The root URI set by server. + * + */ + base: string; + + /** + * The URI to the virtual document. + * + */ + document: string; + + /** + * Address of websocket endpoint for LSP services. + * + */ + server: string; + + /** + * Address of websocket endpoint for the language server. + * + */ + socket: string; + } +} + +/** + * Namespace primarily for language-keyed cache of LSPConnections + */ +namespace Private { + const _connections: Map = new Map(); + let _languageServerManager: ILanguageServerManager; + + export function getLanguageServerManager(): ILanguageServerManager { + return _languageServerManager; + } + export function setLanguageServerManager( + languageServerManager: ILanguageServerManager, + ): void { + _languageServerManager = languageServerManager; + } + + export function disconnect(languageServerId: TLanguageServerId): void { + const connection = _connections.get(languageServerId); + if (connection) { + connection.close(); + _connections.delete(languageServerId); + } + } + + /** + * Return (or create and initialize) the WebSocket associated with the language + */ + export async function connection( + language: string, + languageServerId: TLanguageServerId, + uris: DocumentConnectionManager.IURIs, + onCreate: (connection: LSPConnection) => void, + capabilities: ClientCapabilities, + ): Promise { + let connection = _connections.get(languageServerId); + if (!connection) { + const socket = new WebSocket(uris.socket); + + const connection = new LSPConnection({ + languageId: language, + serverUri: uris.server, + rootUri: uris.base, + serverIdentifier: languageServerId, + capabilities: capabilities, + }); + + _connections.set(languageServerId, connection); + connection.connect(socket); + onCreate(connection); + } + + connection = _connections.get(languageServerId)!; + + return connection; + } + + export function updateServerConfiguration( + languageServerId: TLanguageServerId, + settings: protocol.DidChangeConfigurationParams, + ): void { + const connection = _connections.get(languageServerId); + if (connection) { + connection.sendConfigurationChange(settings); + } + } +} diff --git a/packages/libro-lsp/src/connection.ts b/packages/libro-lsp/src/connection.ts new file mode 100644 index 00000000..2909d733 --- /dev/null +++ b/packages/libro-lsp/src/connection.ts @@ -0,0 +1,570 @@ +/* eslint-disable @typescript-eslint/no-parameter-properties */ +/* eslint-disable @typescript-eslint/parameter-properties */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; +import type * as lsp from 'vscode-languageserver-protocol'; +import type { MessageConnection } from 'vscode-ws-jsonrpc'; + +import { Method } from './tokens.js'; +import type { + ClientNotifications, + ClientRequests, + IClientRequestHandler, + IClientRequestParams, + IClientResult, + IDocumentInfo, + ILSPConnection, + ILSPOptions, + IServerRequestHandler, + IServerRequestParams, + IServerResult, + ServerNotifications, + ServerRequests, +} from './tokens.js'; +import { untilReady } from './utils.js'; +import { + registerServerCapability, + unregisterServerCapability, +} from './ws-connection/server-capability-registration.js'; +import { LspWsConnection } from './ws-connection/ws-connection.js'; + +/** + * Helper class to handle client request + */ +class ClientRequestHandler< + T extends keyof IClientRequestParams = keyof IClientRequestParams, +> implements IClientRequestHandler +{ + constructor( + protected connection: MessageConnection, + protected method: T, + protected emitter: LSPConnection, + ) {} + request(params: IClientRequestParams[T]): Promise { + // TODO check if is ready? + this.emitter.log(MessageKind.clientRequested, { + method: this.method, + message: params, + }); + return this.connection + .sendRequest(this.method, params) + .then((result: IClientResult[T]) => { + this.emitter.log(MessageKind.resultForClient, { + method: this.method, + message: params, + }); + return result; + }); + } +} + +/** + * Helper class to handle server responses + */ +class ServerRequestHandler< + T extends keyof IServerRequestParams = keyof IServerRequestParams, +> implements IServerRequestHandler +{ + constructor( + protected connection: MessageConnection, + protected method: T, + protected emitter: LSPConnection, + ) { + // on request accepts "thenable" + this.connection.onRequest(method, this._handle.bind(this)); + this._handler = null; + } + + setHandler( + handler: ( + params: IServerRequestParams[T], + connection?: LSPConnection, + ) => Promise, + ) { + this._handler = handler; + } + + clearHandler() { + this._handler = null; + } + + protected _handler: + | (( + params: IServerRequestParams[T], + connection?: LSPConnection, + ) => Promise) + | null; + + protected _handle( + request: IServerRequestParams[T], + ): Promise { + this.emitter.log(MessageKind.serverRequested, { + method: this.method, + message: request, + }); + if (!this._handler) { + return new Promise(() => undefined); + } + return this._handler(request, this.emitter).then((result) => { + this.emitter.log(MessageKind.responseForServer, { + method: this.method, + message: result, + }); + return result; + }); + } +} + +export const Provider: Record = { + TEXT_DOCUMENT_SYNC: 'textDocumentSync', + COMPLETION: 'completionProvider', + HOVER: 'hoverProvider', + SIGNATURE_HELP: 'signatureHelpProvider', + DECLARATION: 'declarationProvider', + DEFINITION: 'definitionProvider', + TYPE_DEFINITION: 'typeDefinitionProvider', + IMPLEMENTATION: 'implementationProvider', + REFERENCES: 'referencesProvider', + DOCUMENT_HIGHLIGHT: 'documentHighlightProvider', + DOCUMENT_SYMBOL: 'documentSymbolProvider', + CODE_ACTION: 'codeActionProvider', + CODE_LENS: 'codeLensProvider', + DOCUMENT_LINK: 'documentLinkProvider', + COLOR: 'colorProvider', + DOCUMENT_FORMATTING: 'documentFormattingProvider', + DOCUMENT_RANGE_FORMATTING: 'documentRangeFormattingProvider', + DOCUMENT_ON_TYPE_FORMATTING: 'documentOnTypeFormattingProvider', + RENAME: 'renameProvider', + FOLDING_RANGE: 'foldingRangeProvider', + EXECUTE_COMMAND: 'executeCommandProvider', + SELECTION_RANGE: 'selectionRangeProvider', + WORKSPACE_SYMBOL: 'workspaceSymbolProvider', + WORKSPACE: 'workspace', +}; + +type AnyMethodType = + | typeof Method.ServerNotification + | typeof Method.ClientNotification + | typeof Method.ClientRequest + | typeof Method.ServerRequest; +type AnyMethod = + | Method.ServerNotification + | Method.ClientNotification + | Method.ClientRequest + | Method.ServerRequest; + +/** + * Create a map between the request method and its handler + */ +function createMethodMap( + methods: AnyMethodType, + handlerFactory: (method: U) => H, +): T { + const result: { [key in U]?: H } = {}; + for (const method of Object.values(methods)) { + result[method as U] = handlerFactory(method as U); + } + return result as T; +} + +enum MessageKind { + clientNotifiedServer, + serverNotifiedClient, + serverRequested, + clientRequested, + resultForClient, + responseForServer, +} + +interface IMessageLog { + method: T; + message: any; +} + +export class LSPConnection extends LspWsConnection implements ILSPConnection { + constructor(options: ILSPOptions) { + super(options); + this._options = options; + this.logAllCommunication = false; + this.serverIdentifier = options.serverIdentifier; + this.serverLanguage = options.languageId; + this.documentsToOpen = []; + this.clientNotifications = this.constructNotificationHandlers( + Method.ClientNotification, + ); + this.serverNotifications = this.constructNotificationHandlers( + Method.ServerNotification, + ); + } + + /** + * Identifier of the language server + */ + readonly serverIdentifier?: string; + + /** + * Language of the language server + */ + readonly serverLanguage?: string; + + /** + * Notifications comes from the client. + */ + readonly clientNotifications: ClientNotifications; + + /** + * Notifications comes from the server. + */ + readonly serverNotifications: ServerNotifications; + + /** + * Requests comes from the client. + */ + clientRequests: ClientRequests; + + /** + * Responses comes from the server. + */ + serverRequests: ServerRequests; + + /** + * Should log all communication? + */ + logAllCommunication: boolean; + + get capabilities() { + return this.serverCapabilities; + } + + /** + * Signal emitted when the connection is closed. + */ + get closeSignal(): Event { + return this._closeSignal.event; + } + + /** + * Signal emitted when the connection receives an error + * message.. + */ + get errorSignal(): Event { + return this._errorSignal.event; + } + + /** + * Signal emitted when the connection is initialized. + */ + get serverInitialized(): Event> { + return this._serverInitialized.event; + } + + /** + * Dispose the connection. + */ + override dispose(): void { + if (this.isDisposed) { + return; + } + Object.values(this.serverRequests).forEach((request) => request.clearHandler()); + this.close(); + super.dispose(); + } + + /** + * Helper to print the logs to logger, for now we are using + * directly the browser's console. + */ + log(kind: MessageKind, message: IMessageLog): void { + if (this.logAllCommunication) { + // eslint-disable-next-line no-console + console.log(kind, message); + } + } + + /** + * Send the open request to the backend when the server is + * ready. + */ + sendOpenWhenReady(documentInfo: IDocumentInfo): void { + if (this.isReady) { + this.sendOpen(documentInfo); + } else { + this.documentsToOpen.push(documentInfo); + } + } + + /** + * Send the document changes to the server. + */ + sendSelectiveChange( + changeEvent: lsp.TextDocumentContentChangeEvent, + documentInfo: IDocumentInfo, + ): void { + this._sendChange([changeEvent], documentInfo); + } + + /** + * Send all changes to the server. + */ + sendFullTextChange(text: string, documentInfo: IDocumentInfo): void { + this._sendChange([{ text }], documentInfo); + } + + /** + * Check if a provider is available in the registered capabilities. + */ + provides(provider: keyof lsp.ServerCapabilities): boolean { + return !!(this.serverCapabilities && this.serverCapabilities[provider]); + } + + /** + * Close the connection to the server. + */ + override close(): void { + try { + this._closingManually = true; + super.close(); + } catch (e) { + this._closingManually = false; + } + } + + /** + * initialize a connection over a web socket that speaks the LSP + */ + override connect(socket: WebSocket): void { + super.connect(socket); + untilReady(() => { + return this.isConnected; + }, -1) + .then(() => { + const disposable = this.connection.onClose(() => { + this._isConnected = false; + this._closeSignal.fire(this._closingManually); + }); + this._disposables.push(disposable); + return; + }) + .catch(() => { + console.error('Could not connect onClose signal'); + }); + } + + /** + * Get send request to the server to get completion results + * from a completion item + */ + async getCompletionResolve( + completionItem: lsp.CompletionItem, + ): Promise { + if (!this.isReady) { + return; + } + return this.connection.sendRequest( + 'completionItem/resolve', + completionItem, + ); + } + + /** + * List of documents waiting to be opened once the connection + * is ready. + */ + protected documentsToOpen: IDocumentInfo[]; + + /** + * Generate the notification handlers + */ + protected constructNotificationHandlers< + T extends ServerNotifications | ClientNotifications, + >(methods: typeof Method.ServerNotification | typeof Method.ClientNotification): T { + const factory = () => new Emitter(); + return createMethodMap>(methods, factory); + } + + /** + * Generate the client request handler + */ + protected constructClientRequestHandler< + T extends ClientRequests, + U extends keyof T = keyof T, + >(methods: typeof Method.ClientRequest): T { + return createMethodMap( + methods, + (method) => new ClientRequestHandler(this.connection, method as U as any, this), + ); + } + + /** + * Generate the server response handler + */ + protected constructServerRequestHandler< + T extends ServerRequests, + U extends keyof T = keyof T, + >(methods: typeof Method.ServerRequest): T { + return createMethodMap( + methods, + (method) => new ServerRequestHandler(this.connection, method as U as any, this), + ); + } + + /** + * Initialization parameters to be sent to the language server. + * Subclasses can overload this when adding more features. + */ + protected override initializeParams(): lsp.InitializeParams { + return { + ...super.initializeParams(), + capabilities: this._options.capabilities, + initializationOptions: null, + processId: null, + workspaceFolders: null, + }; + } + + /** + * Callback called when the server is initialized. + */ + protected override onServerInitialized(params: lsp.InitializeResult): void { + this.afterInitialized(); + super.onServerInitialized(params); + while (this.documentsToOpen.length) { + this.sendOpen(this.documentsToOpen.pop()!); + } + this._serverInitialized.fire(this.serverCapabilities); + } + + /** + * Once the server is initialized, this method generates the + * client and server handlers + */ + protected afterInitialized(): void { + const disposable = this.connection.onError((e) => this._errorSignal.fire(e)); + this._disposables.push(disposable); + for (const method of Object.values( + Method.ServerNotification, + ) as (keyof ServerNotifications)[]) { + const signal = this.serverNotifications[method] as Emitter; + const disposable = this.connection.onNotification(method, (params) => { + this.log(MessageKind.serverNotifiedClient, { + method, + message: params, + }); + signal.fire(params); + }); + this._disposables.push(disposable); + } + + for (const method of Object.values( + Method.ClientNotification, + ) as (keyof ClientNotifications)[]) { + const signal = this.clientNotifications[method] as Emitter; + signal.event((params) => { + this.log(MessageKind.clientNotifiedServer, { + method, + message: params, + }); + this.connection.sendNotification(method, params).catch(console.error); + }); + } + + this.clientRequests = this.constructClientRequestHandler( + Method.ClientRequest, + ); + this.serverRequests = this.constructServerRequestHandler( + Method.ServerRequest, + ); + + this.serverRequests['client/registerCapability'].setHandler( + async (params: lsp.RegistrationParams) => { + params.registrations.forEach((capabilityRegistration: lsp.Registration) => { + try { + const updatedCapabilities = registerServerCapability( + this.serverCapabilities, + capabilityRegistration, + ); + if (updatedCapabilities === null) { + console.error( + `Failed to register server capability: ${capabilityRegistration}`, + ); + return; + } + this.serverCapabilities = updatedCapabilities; + } catch (err) { + console.error(err); + } + }); + }, + ); + + this.serverRequests['client/unregisterCapability'].setHandler( + async (params: lsp.UnregistrationParams) => { + params.unregisterations.forEach( + (capabilityUnregistration: lsp.Unregistration) => { + this.serverCapabilities = unregisterServerCapability( + this.serverCapabilities, + capabilityUnregistration, + ); + }, + ); + }, + ); + + this.serverRequests['workspace/configuration'].setHandler(async (params) => { + return params.items.map((item) => { + // LSP: "If the client can’t provide a configuration setting for a given scope + // then `null` needs to be present in the returned array." + + // for now we do not support configuration, but yaml server does not respect + // client capability so we have a handler just for that + return null; + }); + }); + } + + /** + * Is the connection is closed manually? + */ + protected _closingManually = false; + + protected _options: ILSPOptions; + + protected _closeSignal = new Emitter(); + protected _errorSignal = new Emitter(); + protected _serverInitialized = new Emitter>(); + + /** + * Send the document changed data to the server. + */ + protected _sendChange( + changeEvents: lsp.TextDocumentContentChangeEvent[], + documentInfo: IDocumentInfo, + ) { + if (!this.isReady) { + return; + } + if (documentInfo.uri.length === 0) { + return; + } + if (!this.openedUris.get(documentInfo.uri)) { + this.sendOpen(documentInfo); + } + const textDocumentChange: lsp.DidChangeTextDocumentParams = { + textDocument: { + uri: documentInfo.uri, + version: documentInfo.version, + } as lsp.VersionedTextDocumentIdentifier, + contentChanges: changeEvents, + }; + this.connection + .sendNotification('textDocument/didChange', textDocumentChange) + .catch(console.error); + documentInfo.version++; + } +} diff --git a/packages/libro-lsp/src/extractors/index.ts b/packages/libro-lsp/src/extractors/index.ts new file mode 100644 index 00000000..75206a1a --- /dev/null +++ b/packages/libro-lsp/src/extractors/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export * from './manager.js'; +export * from './text_extractor.js'; +export * from './types.js'; diff --git a/packages/libro-lsp/src/extractors/manager.ts b/packages/libro-lsp/src/extractors/manager.ts new file mode 100644 index 00000000..5065a727 --- /dev/null +++ b/packages/libro-lsp/src/extractors/manager.ts @@ -0,0 +1,82 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { singleton } from '@difizen/mana-app'; + +import { ILSPCodeExtractorsManager } from '../tokens.js'; + +import type { IForeignCodeExtractor } from './types.js'; + +/** + * Manager for the code extractors + */ +@singleton({ token: ILSPCodeExtractorsManager }) +export class CodeExtractorsManager implements ILSPCodeExtractorsManager { + constructor() { + this._extractorMap = new Map>(); + + this._extractorMapAnyLanguage = new Map(); + } + + /** + * Get the extractors for the input cell type and the main language of + * the document + * + * @param cellType - type of cell + * @param hostLanguage - main language of the document + */ + getExtractors( + cellType: string, + hostLanguage: string | null, + ): IForeignCodeExtractor[] { + if (hostLanguage) { + const currentMap = this._extractorMap.get(cellType); + if (!currentMap) { + return []; + } + return currentMap.get(hostLanguage) ?? []; + } else { + return this._extractorMapAnyLanguage.get(cellType) ?? []; + } + } + + /** + * Register an extractor to extract foreign code from host documents of specified language. + */ + register(extractor: IForeignCodeExtractor, hostLanguage: string | null): void { + const cellType = extractor.cellType; + if (hostLanguage) { + cellType.forEach((type) => { + if (!this._extractorMap.has(type)) { + this._extractorMap.set(type, new Map()); + } + const currentMap = this._extractorMap.get(type)!; + const extractorList = currentMap.get(hostLanguage); + if (!extractorList) { + currentMap.set(hostLanguage, [extractor]); + } else { + extractorList.push(extractor); + } + }); + } else { + cellType.forEach((type) => { + if (!this._extractorMapAnyLanguage.has(type)) { + this._extractorMapAnyLanguage.set(type, []); + } + this._extractorMapAnyLanguage.get(type)!.push(extractor); + }); + } + } + + /** + * The map with key is the type of cell, value is another map between + * the language of cell and its code extractor. + */ + protected _extractorMap: Map>; + + /** + * The map with key is the cell type, value is the code extractor associated + * with this cell type, this is used for the non-code cell types. + */ + protected _extractorMapAnyLanguage: Map; +} diff --git a/packages/libro-lsp/src/extractors/text_extractor.ts b/packages/libro-lsp/src/extractors/text_extractor.ts new file mode 100644 index 00000000..234505e1 --- /dev/null +++ b/packages/libro-lsp/src/extractors/text_extractor.ts @@ -0,0 +1,94 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { LanguageIdentifier } from '../lsp.js'; +import { positionAtOffset } from '../positioning.js'; + +import type { IExtractedCode, IForeignCodeExtractor } from './types.js'; + +/** + * The code extractor for the raw and markdown text. + */ +export class TextForeignCodeExtractor implements IForeignCodeExtractor { + constructor(options: TextForeignCodeExtractor.IOptions) { + this.language = options.language; + this.standalone = options.isStandalone; + this.fileExtension = options.file_extension; + this.cellType = options.cellType; + } + /** + * The foreign language. + */ + readonly language: LanguageIdentifier; + + /** + * Should the foreign code be appended (False) to the previously established virtual document of the same language, + * or is it standalone snippet which requires separate connection? + */ + readonly standalone: boolean; + + /** + * Extension of the virtual document (some servers check extensions of files), e.g. 'py' or 'R'. + */ + readonly fileExtension: string; + + /** + * The supported cell types. + */ + readonly cellType: string[]; + + /** + * Test if there is any foreign code in provided code snippet. + */ + hasForeignCode(code: string, cellType: string): boolean { + return this.cellType.includes(cellType); + } + + /** + * Split the code into the host and foreign code (if any foreign code was detected) + */ + extractForeignCode(code: string): IExtractedCode[] { + const lines = code.split('\n'); + + const extracts = new Array(); + + const foreignCodeFragment = code; + + const start = positionAtOffset(0, lines); + const end = positionAtOffset(foreignCodeFragment.length, lines); + + extracts.push({ + hostCode: '', + foreignCode: foreignCodeFragment, + range: { start, end }, + virtualShift: null, + }); + + return extracts; + } +} + +namespace TextForeignCodeExtractor { + export interface IOptions { + /** + * The foreign language. + */ + language: string; + + /** + * Should the foreign code be appended (False) to the previously established virtual document of the same language, + * or is it standalone snippet which requires separate connection? + */ + isStandalone: boolean; + + /** + * Extension of the virtual document (some servers check extensions of files), e.g. 'py' or 'R'. + */ + file_extension: string; + + /** + * The supported cell types. + */ + cellType: string[]; + } +} diff --git a/packages/libro-lsp/src/extractors/types.ts b/packages/libro-lsp/src/extractors/types.ts new file mode 100644 index 00000000..be835ed0 --- /dev/null +++ b/packages/libro-lsp/src/extractors/types.ts @@ -0,0 +1,78 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { IRange, IPosition } from '@difizen/libro-code-editor'; + +import type { LanguageIdentifier } from '../lsp.js'; + +export interface IExtractedCode { + /** + * Foreign code (may be empty, for example line of '%R') or null if none. + */ + foreignCode: string | null; + /** + * Range of the foreign code relative to the original source. + * `null` is used internally to represent a leftover host code after extraction. + */ + range: IRange | null; + /** + * Shift due to any additional code inserted at the beginning of the virtual document + * (usually in order to mock the arguments passed to a magic, or to provide other context clues for the linters) + */ + virtualShift: IPosition | null; + /** + * Code to be retained in the virtual document of the host. + */ + hostCode: string | null; +} + +/** + * Foreign code extractor makes it possible to analyze code of language X embedded in code (or notebook) of language Y. + * + * The typical examples are: + * - (X=CSS< Y=HTML), or + * - (X=JavaScript, Y=HTML), + * + * while in the data analysis realm, examples include: + * - (X=R, Y=IPython), + * - (X=LATEX Y=IPython), + * - (X=SQL, Y=IPython) + * + * This extension does not aim to provide comprehensive abilities for foreign code extraction, + * but it does intend to provide stable interface for other extensions to build on it. + * + * A simple, regular expression based, configurable foreign extractor is implemented + * to provide a good reference and a good initial experience for the users. + */ +export interface IForeignCodeExtractor { + /** + * The foreign language. + */ + readonly language: LanguageIdentifier; + + /** + * The supported cell types. + */ + readonly cellType: string[]; + + /** + * Split the code into the host and foreign code (if any foreign code was detected) + */ + extractForeignCode(code: string): IExtractedCode[]; + + /** + * Does the extractor produce code which should be appended to the previously established virtual document (False) + * of the same language, or does it produce standalone snippets which require separate connections (True)? + */ + readonly standalone: boolean; + + /** + * Test if there is any foreign code in provided code snippet. + */ + hasForeignCode(code: string, cellType: string): boolean; + + /** + * Extension of the virtual document (some servers check extensions of files), e.g. 'py' or 'R'. + */ + readonly fileExtension: string; +} diff --git a/packages/libro-lsp/src/feature.ts b/packages/libro-lsp/src/feature.ts new file mode 100644 index 00000000..66b265a8 --- /dev/null +++ b/packages/libro-lsp/src/feature.ts @@ -0,0 +1,60 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; +import { singleton } from '@difizen/mana-app'; +import mergeWith from 'lodash.mergewith'; + +import type { ClientCapabilities } from './lsp.js'; +import type { IFeature } from './tokens.js'; +import { ILSPFeatureManager } from './tokens.js'; + +/** + * Class to manager the registered features of the language servers. + */ +@singleton({ token: ILSPFeatureManager }) +export class FeatureManager implements ILSPFeatureManager { + constructor() { + this._featuresRegistered = new Emitter(); + } + /** + * List of registered features + */ + readonly features: IFeature[] = []; + + /** + * Signal emitted when a new feature is registered. + */ + get featuresRegistered(): Event { + return this._featuresRegistered.event; + } + + /** + * Register a new feature, skip if it is already registered. + */ + register(feature: IFeature): void { + if (this.features.some((ft) => ft.id === feature.id)) { + console.warn(`Feature with id ${feature.id} is already registered, skipping.`); + } else { + this.features.push(feature); + this._featuresRegistered.fire(feature); + } + } + + /** + * Get the capabilities of all clients. + */ + clientCapabilities(): ClientCapabilities { + let capabilities: ClientCapabilities = {}; + for (const feature of this.features) { + if (!feature.capabilities) { + continue; + } + capabilities = mergeWith(capabilities, feature.capabilities); + } + return capabilities; + } + + protected _featuresRegistered: Emitter; +} diff --git a/packages/libro-lsp/src/index.ts b/packages/libro-lsp/src/index.ts new file mode 100644 index 00000000..6a8a67f9 --- /dev/null +++ b/packages/libro-lsp/src/index.ts @@ -0,0 +1,21 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/** + * @packageDocumentation + * @module lsp + */ + +export * from './module.js'; +export * from './adapters/adapter.js'; +export * from './connection-manager.js'; +export * from './extractors/index.js'; +export * from './feature.js'; +export * from './manager.js'; +export * from './plugin.js'; +export * from './positioning.js'; +export * from './tokens.js'; +export * from './utils.js'; +export * from './virtual/document.js'; +export * from './connection.js'; +export * from './lsp.js'; +export * from './lsp-protocol.js'; diff --git a/packages/libro-lsp/src/lsp-app-contribution.ts b/packages/libro-lsp/src/lsp-app-contribution.ts new file mode 100644 index 00000000..08301cda --- /dev/null +++ b/packages/libro-lsp/src/lsp-app-contribution.ts @@ -0,0 +1,83 @@ +import type { LibroView } from '@difizen/libro-core'; +import { LibroService } from '@difizen/libro-core'; +import { ServerManager } from '@difizen/libro-kernel'; +import { ApplicationContribution } from '@difizen/mana-app'; +import { inject, singleton } from '@difizen/mana-app'; + +import { NotebookAdapter } from './adapters/notebook-adapter.js'; +import { + ILSPCodeExtractorsManager, + ILSPDocumentConnectionManager, + ILSPFeatureManager, +} from './tokens.js'; + +@singleton({ contrib: [ApplicationContribution] }) +export class LSPAppContribution implements ApplicationContribution { + @inject(LibroService) libroService: LibroService; + @inject(ServerManager) serverManager: ServerManager; + + @inject(ILSPDocumentConnectionManager) + connectionManager: ILSPDocumentConnectionManager; + @inject(ILSPFeatureManager) featureManager: ILSPFeatureManager; + @inject(ILSPCodeExtractorsManager) codeExtractorManager: ILSPCodeExtractorsManager; + + onStart() { + /** + * FIXME:capability声明应该和具体的实现写在一起 + */ + this.featureManager.register({ + id: 'libro-lsp', + capabilities: { + textDocument: { + completion: { + completionItem: { documentationFormat: ['markdown'] }, + }, + diagnostic: { + dynamicRegistration: true, + }, + /** + * jedi-lsp的hover功能没有用hover的格式配置,而是使用completion的格式配置。。。 + */ + hover: { + dynamicRegistration: true, + contentFormat: ['markdown', 'plaintext'], + }, + signatureHelp: { + dynamicRegistration: true, + contextSupport: true, + signatureInformation: { + activeParameterSupport: true, + documentationFormat: ['markdown'], + parameterInformation: { + labelOffsetSupport: true, + }, + }, + }, + }, + }, + }); + this.setupNotebookLanguageServer(); + } + + setupNotebookLanguageServer() { + this.libroService.onNotebookViewCreated(async (notebook) => { + this.activateNotebookLanguageServer(notebook); + }); + } + + /** + * Activate the language server for notebook. + */ + async activateNotebookLanguageServer(notebook: LibroView): Promise { + await notebook.initialized; + await this.serverManager.ready; + + const adapter = new NotebookAdapter(notebook, { + connectionManager: this.connectionManager, + featureManager: this.featureManager, + foreignCodeExtractorsManager: this.codeExtractorManager, + }); + + this.connectionManager.registerAdapter(notebook.model.id, adapter); + } +} diff --git a/packages/libro-lsp/src/lsp-protocol.ts b/packages/libro-lsp/src/lsp-protocol.ts new file mode 100644 index 00000000..16024efc --- /dev/null +++ b/packages/libro-lsp/src/lsp-protocol.ts @@ -0,0 +1,10 @@ +import type { LSPConnection } from './connection.js'; +import type { Document } from './tokens.js'; +import type { VirtualDocument } from './virtual/document.js'; + +export type LSPProviderResult = { + virtualDocument: VirtualDocument; + lspConnection: LSPConnection; + editor: Document.IEditor; +}; +export type LSPProvider = () => Promise; diff --git a/packages/libro-lsp/src/lsp.ts b/packages/libro-lsp/src/lsp.ts new file mode 100644 index 00000000..e59cfe05 --- /dev/null +++ b/packages/libro-lsp/src/lsp.ts @@ -0,0 +1,160 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type * as lsp from 'vscode-languageserver-protocol'; + +export type ClientCapabilities = lsp.ClientCapabilities; + +export enum DiagnosticSeverity { + Error = 1, + Warning = 2, + Information = 3, + Hint = 4, +} + +export enum DiagnosticTag { + Unnecessary = 1, + Deprecated = 2, +} + +export enum CompletionItemTag { + Deprecated = 1, +} + +export enum CompletionItemKind { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} + +export enum DocumentHighlightKind { + Text = 1, + Read = 2, + Write = 3, +} + +export enum CompletionTriggerKind { + Invoked = 1, + TriggerCharacter = 2, + TriggerForIncompleteCompletions = 3, +} + +export enum AdditionalCompletionTriggerKinds { + AutoInvoked = 9999, +} + +export type ExtendedCompletionTriggerKind = + | CompletionTriggerKind + | AdditionalCompletionTriggerKinds; + +export type CompletionItemKindStrings = keyof typeof CompletionItemKind; + +/** + * The language identifier for LSP, with the preferred identifier as defined in the documentation + * see the table in https://microsoft.github.io/language-server-protocol/specification#textDocumentItem + */ +export enum Languages { + 'abap' = 'ABAP', + 'bat' = 'Windows Bat', + 'bibtex' = 'BibTeX', + 'clojure' = 'Clojure', + 'coffeescript' = 'Coffeescript', + 'c' = 'C', + 'cpp' = 'C++', + 'csharp' = 'C#', + 'css' = 'CSS', + 'diff' = 'Diff', + 'dart' = 'Dart', + 'dockerfile' = 'Dockerfile', + 'elixir' = 'Elixir', + 'erlang' = 'Erlang', + 'fsharp' = 'F#', + 'git-commit' = 'Git (commit)', + 'git-rebase' = 'Git (rebase)', + 'go' = 'Go', + 'groovy' = 'Groovy', + 'handlebars' = 'Handlebars', + 'html' = 'HTML', + 'ini' = 'Ini', + 'java' = 'Java', + 'javascript' = 'JavaScript', + 'javascriptreact' = 'JavaScript React', + 'json' = 'JSON', + 'latex' = 'LaTeX', + 'less' = 'Less', + 'lua' = 'Lua', + 'makefile' = 'Makefile', + 'markdown' = 'Markdown', + 'objective-c' = 'Objective-C', + 'objective-cpp' = 'Objective-C++', + 'perl' = 'Perl', + 'perl6' = 'Perl 6', + 'php' = 'PHP', + 'powershell' = 'Powershell', + 'jade' = 'Pug', + 'python' = 'Python', + 'r' = 'R', + 'razor' = 'Razor (cshtml)', + 'ruby' = 'Ruby', + 'rust' = 'Rust', + 'scss' = 'SCSS (syntax using curly brackets)', + 'sass' = 'SCSS (indented syntax)', + 'scala' = 'Scala', + 'shaderlab' = 'ShaderLab', + 'shellscript' = 'Shell Script (Bash)', + 'sql' = 'SQL', + 'swift' = 'Swift', + 'typescript' = 'TypeScript', + 'typescriptreact' = 'TypeScript React', + 'tex' = 'TeX', + 'vb' = 'Visual Basic', + 'xml' = 'XML', + 'xsl' = 'XSL', + 'yaml' = 'YAML', +} + +export type RecommendedLanguageIdentifier = keyof typeof Languages; + +/** + * Language identifier for the LSP server, allowing any string but preferring + * the identifiers as recommended by the LSP documentation. + */ +export type LanguageIdentifier = RecommendedLanguageIdentifier | string; + +/** + * Type represents a location inside a resource, such as a line + * inside a text file. + */ +export type AnyLocation = + | lsp.Location + | lsp.Location[] + | lsp.LocationLink[] + | undefined + | null; + +/** + * Type represents the completion result. + */ +export type AnyCompletion = lsp.CompletionList | lsp.CompletionItem[]; diff --git a/packages/libro-lsp/src/manager.ts b/packages/libro-lsp/src/manager.ts new file mode 100644 index 00000000..441235c0 --- /dev/null +++ b/packages/libro-lsp/src/manager.ts @@ -0,0 +1,358 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { URL } from '@difizen/libro-common'; +import type { ISettings } from '@difizen/libro-kernel'; +import { PageConfig, ServerConnection, ServerManager } from '@difizen/libro-kernel'; +import type { Event } from '@difizen/mana-app'; +import { Deferred, Emitter } from '@difizen/mana-app'; +import { inject, postConstruct, transient } from '@difizen/mana-app'; + +import type { ServerSpecProperties } from './schema.js'; +import { ILanguageServerManagerOptions, URL_NS } from './tokens.js'; +import type { + IGetServerIdOptions, + ILanguageServerManager, + TLanguageServerConfigurations, + TLanguageServerId, + TSessionMap, + TSpecsMap, +} from './tokens.js'; + +@transient() +export class LanguageServerManager implements ILanguageServerManager { + @inject(ServerConnection) serverConnection!: ServerConnection; + @inject(ServerManager) serverManager!: ServerManager; + constructor( + @inject(ILanguageServerManagerOptions) options: ILanguageServerManagerOptions, + ) { + this._baseUrl = options.baseUrl || PageConfig.getBaseUrl(); + this._retries = options.retries || 2; + this._retriesInterval = options.retriesInterval || 10000; + this._statusCode = -1; + this._configuration = {}; + } + + @postConstruct() + init() { + this.serverManager.ready + .then(() => { + this.fetchSessions(); + return; + }) + .catch(console.error); + } + + /** + * Check if the manager is enabled or disabled + */ + get isEnabled(): boolean { + return this._enabled; + } + /** + * Check if the manager is disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Get the language server specs. + */ + get specs(): TSpecsMap { + return this._specs; + } + + /** + * Get the status end point. + */ + get statusUrl(): string { + const baseUrl = this.serverConnection.settings.baseUrl ?? this._baseUrl; + return URL.join(baseUrl, URL_NS, 'status'); + } + + /** + * Signal emitted when a language server session is changed + */ + get sessionsChanged(): Event { + return this._sessionsChanged.event; + } + + /** + * Get the map of language server sessions. + */ + get sessions(): TSessionMap { + return this._sessions; + } + + /** + * A promise resolved when this server manager is ready. + */ + get ready(): Promise { + return this._ready.promise; + } + + /** + * Get the status code of server's responses. + */ + get statusCode(): number { + return this._statusCode; + } + + /** + * Enable the language server services + */ + async enable(): Promise { + this._enabled = true; + await this.fetchSessions(); + } + + /** + * Disable the language server services + */ + disable(): void { + this._enabled = false; + this._sessions = new Map(); + this._sessionsChanged.fire(void 0); + } + + /** + * Dispose the manager. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + } + + /** + * Update the language server configuration. + */ + setConfiguration(configuration: TLanguageServerConfigurations): void { + this._configuration = configuration; + } + + /** + * Get matching language server for input language option. + * + * modify lsp server rank on ~/.jupyter/lab/user-settings/@jupyterlab/lsp-extension + */ + getMatchingServers(options: IGetServerIdOptions): TLanguageServerId[] { + if (!options.language) { + console.error( + 'Cannot match server by language: language not available; ensure that kernel and specs provide language and MIME type', + ); + return []; + } + + const matchingSessionsKeys: TLanguageServerId[] = []; + + for (const [key, session] of this._sessions.entries()) { + if (this.isMatchingSpec(options, session.spec)) { + matchingSessionsKeys.push(key); + } + } + + return matchingSessionsKeys.sort(this.compareRanks.bind(this)); + } + + /** + * Get matching language server spec for input language option. + */ + getMatchingSpecs(options: IGetServerIdOptions): TSpecsMap { + const result: TSpecsMap = new Map(); + + for (const [key, specification] of this._specs.entries()) { + if (this.isMatchingSpec(options, specification)) { + result.set(key, specification); + } + } + return result; + } + + /** + * Fetch the server session list from the status endpoint. The server + * manager is ready once this method finishes. + */ + async fetchSessions(): Promise { + if (!this._enabled) { + return; + } + const response = await this.serverConnection.makeRequest(this.statusUrl, {}); + + this._statusCode = response.status; + if (!response.ok) { + if (this._retries > 0) { + this._retries -= 1; + setTimeout(this.fetchSessions.bind(this), this._retriesInterval); + } else { + this._ready.resolve(undefined); + console.warn('Missing jupyter_lsp server extension, skipping.'); + } + return; + } + + let sessions: Record; + + try { + const data = await response.json(); + sessions = data.sessions; + try { + this.version = data.version; + this._specs = new Map(Object.entries(data.specs)) as TSpecsMap; + } catch (err) { + console.warn(err); + } + } catch (err) { + console.warn(err); + this._ready.resolve(undefined); + return; + } + + for (const key of Object.keys(sessions)) { + const id: TLanguageServerId = key as TLanguageServerId; + if (this._sessions.has(id)) { + Object.assign(this._sessions.get(id)!, sessions[key]); + } else { + this._sessions.set(id, sessions[key]); + } + } + + const oldKeys = this._sessions.keys(); + + for (const oldKey in oldKeys) { + if (!sessions[oldKey]) { + const oldId = oldKey as TLanguageServerId; + this._sessions.delete(oldId); + } + } + this._sessionsChanged.fire(void 0); + this._ready.resolve(undefined); + } + + /** + * Version number of sever session. + */ + protected version: number; + + /** + * Check if input language option maths the language server spec. + */ + protected isMatchingSpec( + options: IGetServerIdOptions, + spec: ServerSpecProperties, + ): boolean { + // most things speak language + // if language is not known, it is guessed based on MIME type earlier + // so some language should be available by now (which can be not so obvious, e.g. "plain" for txt documents) + const lowerCaseLanguage = options.language!.toLocaleLowerCase(); + return spec.languages!.some( + (language: string) => language.toLocaleLowerCase() === lowerCaseLanguage, + ); + } + + /** + * Helper function to warn a message only once. + */ + protected warnOnce(arg: string): void { + if (!this._warningsEmitted.has(arg)) { + this._warningsEmitted.add(arg); + console.warn(arg); + } + } + + /** + * Compare the rank of two servers with the same language. + */ + protected compareRanks(a: TLanguageServerId, b: TLanguageServerId): number { + const DEFAULT_RANK = 50; + const defaultServerRank: Record = { + 'pyright-extended': DEFAULT_RANK + 3, + pyright: DEFAULT_RANK + 2, + pylsp: DEFAULT_RANK + 1, + 'bash-language-server': DEFAULT_RANK, + 'dockerfile-language-server-nodejs': DEFAULT_RANK, + 'javascript-typescript-langserver': DEFAULT_RANK, + 'unified-language-server': DEFAULT_RANK, + 'vscode-css-languageserver-bin': DEFAULT_RANK, + 'vscode-html-languageserver-bin': DEFAULT_RANK, + 'vscode-json-languageserver-bin': DEFAULT_RANK, + 'yaml-language-server': DEFAULT_RANK, + 'r-languageserver': DEFAULT_RANK, + } as const; + const aRank = this._configuration[a]?.rank ?? defaultServerRank[a] ?? DEFAULT_RANK; + const bRank = this._configuration[b]?.rank ?? defaultServerRank[b] ?? DEFAULT_RANK; + + if (aRank === bRank) { + this.warnOnce( + `Two matching servers: ${a} and ${b} have the same rank; choose which one to use by changing the rank in Advanced Settings Editor`, + ); + return a.localeCompare(b); + } + // higher rank = higher in the list (descending order) + return bRank - aRank; + } + + /** + * map of language server sessions. + */ + protected _sessions: TSessionMap = new Map(); + + /** + * Map of language server specs. + */ + protected _specs: TSpecsMap = new Map(); + + /** + * Server connection setting. + */ + protected _settings: ISettings; + + /** + * Base URL to connect to the language server handler. + */ + protected _baseUrl: string; + + /** + * Status code of server response + */ + protected _statusCode: number; + + /** + * Number of connection retry, default to 2. + */ + protected _retries: number; + + /** + * Interval between each retry, default to 10s. + */ + protected _retriesInterval: number; + + /** + * Language server configuration. + */ + protected _configuration: TLanguageServerConfigurations; + + /** + * Set of emitted warning message, message in this set will not be warned again. + */ + protected _warningsEmitted = new Set(); + + /** + * A promise resolved when this server manager is ready. + */ + protected _ready = new Deferred(); + + /** + * Signal emitted when a language server session is changed + */ + protected _sessionsChanged = new Emitter(); + + protected _isDisposed = false; + + /** + * Check if the manager is enabled or disabled + */ + protected _enabled = true; +} diff --git a/packages/libro-lsp/src/module.ts b/packages/libro-lsp/src/module.ts new file mode 100644 index 00000000..9bc05c8f --- /dev/null +++ b/packages/libro-lsp/src/module.ts @@ -0,0 +1,32 @@ +import { LibroServerModule } from '@difizen/libro-kernel'; +import { ManaModule } from '@difizen/mana-app'; + +import { DocumentConnectionManager } from './connection-manager.js'; +import { CodeExtractorsManager } from './extractors/index.js'; +import { FeatureManager } from './feature.js'; +import { LSPAppContribution } from './lsp-app-contribution.js'; +import { LanguageServerManager } from './manager.js'; +import { + ILanguageServerManagerFactory, + ILanguageServerManagerOptions, +} from './tokens.js'; + +export const LibroLSPModule = ManaModule.create() + .register( + LSPAppContribution, + DocumentConnectionManager, + FeatureManager, + CodeExtractorsManager, + LanguageServerManager, + { + token: ILanguageServerManagerFactory, + useFactory: (ctx) => { + return (option: ILanguageServerManagerOptions) => { + const child = ctx.container.createChild(); + child.register({ token: ILanguageServerManagerOptions, useValue: option }); + return child.get(LanguageServerManager); + }; + }, + }, + ) + .dependOn(LibroServerModule); diff --git a/packages/libro-lsp/src/plugin.ts b/packages/libro-lsp/src/plugin.ts new file mode 100644 index 00000000..60c1089a --- /dev/null +++ b/packages/libro-lsp/src/plugin.ts @@ -0,0 +1,62 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +/* eslint-disable */ + +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run jlpm build:schema to regenerate this file. + */ + +/** + * Enable or disable the language server services. + */ +export type Activate = 'off' | 'on'; +/** + * When multiple servers match specific document/language, the server with the highest rank will be used + */ +export type RankOfTheServer = number; +/** + * Whether to ask server to send logs with execution trace (for debugging). Accepted values are: "off", "messages", "verbose". Servers are allowed to ignore this request. + */ +export type AskServersToSendTraceNotifications = 'off' | 'messages' | 'verbose'; +/** + * Enable or disable the logging feature of the language servers. + */ +export type LogCommunication = boolean; + +/** + * Language Server Protocol settings. + */ +export interface LanguageServersExperimental { + activate?: Activate; + languageServers?: LanguageServer; + setTrace?: AskServersToSendTraceNotifications; + logAllCommunication?: LogCommunication; + [k: string]: any; +} +/** + * Language-server specific configuration, keyed by implementation + */ +export interface LanguageServer { + [k: string]: LanguageServer1; +} +/** + * This interface was referenced by `LanguageServer`'s JSON-Schema definition + * via the `patternProperty` ".*". + * + * This interface was referenced by `LanguageServersExperimental`'s JSON-Schema + * via the `definition` "languageServer". + */ +export interface LanguageServer1 { + configuration?: LanguageServerConfigurations; + rank?: RankOfTheServer; + [k: string]: any; +} +/** + * Configuration to be sent to language server over LSP when initialized: see the specific language server's documentation for more + */ +export interface LanguageServerConfigurations { + [k: string]: any; +} diff --git a/packages/libro-lsp/src/positioning.ts b/packages/libro-lsp/src/positioning.ts new file mode 100644 index 00000000..1d0c1dd8 --- /dev/null +++ b/packages/libro-lsp/src/positioning.ts @@ -0,0 +1,121 @@ +/* eslint-disable no-param-reassign */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { IPosition as CodeEditorPosition } from '@difizen/libro-code-editor'; +import type * as lsp from 'vscode-languageserver-protocol'; + +/** + * CM5 position interface. + * + * TODO: Migrate to offset-only mode once `CodeEditor.IPosition` + * is migrated. + */ +export interface Position { + /** + * Line number + */ + line: number; + + /** + * Position of character in line + */ + ch: number; +} + +/** + * is_* attributes are there only to enforce strict interface type checking + */ +export interface ISourcePosition extends Position { + isSource: true; +} + +export interface IEditorPosition extends Position { + isEditor: true; +} + +export interface IVirtualPosition extends Position { + isVirtual: true; +} + +export interface IRootPosition extends ISourcePosition { + isRoot: true; +} + +/** + * Compare two `Position` variable. + * + */ +export function isEqual(self: Position, other: Position): boolean { + return other && self.line === other.line && self.ch === other.ch; +} + +/** + * Given a list of line and an offset from the start, compute the corresponding + * position in form of line and column number + * + * @param offset - number of spaces counted from the start of first line + * @param lines - list of lines to compute the position + * @return - the position of cursor + */ +export function positionAtOffset(offset: number, lines: string[]): CodeEditorPosition { + let line = 0; + let column = 0; + for (const textLine of lines) { + // each line has a new line symbol which is accounted for in offset! + if (textLine.length + 1 <= offset) { + offset -= textLine.length + 1; + line += 1; + } else { + column = offset; + break; + } + } + return { line, column }; +} + +/** + * Given a list of line and position in form of line and column number, + * compute the offset from the start of first line. + * @param position - postion of cursor + * @param lines - list of lines to compute the position + * @param linesIncludeBreaks - should count the line break as space? + * return - offset number + */ +export function offsetAtPosition( + position: CodeEditorPosition, + lines: string[], + linesIncludeBreaks = false, +): number { + const breakIncrement = linesIncludeBreaks ? 0 : 1; + let offset = 0; + for (let i = 0; i < lines.length; i++) { + const textLine = lines[i]; + if (position.line > i) { + offset += textLine.length + breakIncrement; + } else { + offset += position.column; + break; + } + } + return offset; +} + +export namespace ProtocolCoordinates { + /** + * Check if the position is in the input range + * + * @param position - position in form of line and character number. + * @param range - range in from of start and end position. + */ + export function isWithinRange(position: lsp.Position, range: lsp.Range): boolean { + const { line, character } = position; + return ( + line >= range.start.line && + line <= range.end.line && + // need to be non-overlapping see https://github.com/jupyter-lsp/jupyterlab-lsp/issues/628 + (line !== range.start.line || character > range.start.character) && + (line !== range.end.line || character <= range.end.character) + ); + } +} diff --git a/packages/libro-lsp/src/schema.ts b/packages/libro-lsp/src/schema.ts new file mode 100644 index 00000000..6b3e8b8e --- /dev/null +++ b/packages/libro-lsp/src/schema.ts @@ -0,0 +1,249 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +/* eslint-disable */ + +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run jlpm build:schema to regenerate this file. + */ + +/** + * which version of the spec this implements + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "current-version". + */ +export type SpecSchemaVersion = 2; +/** + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "env-var". + * + * This interface was referenced by `EnvironmentVariables`'s JSON-Schema definition + * via the `patternProperty` "[^ ]+". + */ +export type AnEnvironmentVariableMayContainPythonStringTemplateEvaluatedAgainstTheExistingEnvironmentEG$HOME = + string; +/** + * the install commands or description for installing the language server + * + * This interface was referenced by `Installation`'s JSON-Schema definition + * via the `patternProperty` ".+". + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "install-help". + * + * This interface was referenced by `Installation1`'s JSON-Schema definition + * via the `patternProperty` ".+". + */ +export type InstallHelp = string; +/** + * languages supported by this Language Server + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "language-list". + */ +export type LanguageList = [string, ...string[]]; +/** + * a description of a language server that could be started + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "language-server-spec". + * + * This interface was referenced by `LanguageServerSpecsMap`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export type LanguageServerSpec = ServerSpecProperties & { + [k: string]: any; +}; +/** + * the arguments to start the language server normally + */ +export type LaunchArguments = string[]; +/** + * the arguments to start the language server with more verbose output + */ +export type DebugArguments = string[]; +/** + * name shown in the UI + */ +export type DisplayName = string; +/** + * known extensions that can contribute to the Language Server's features + */ +export type Extensions = LanguageServerExtension[]; +/** + * list of MIME types supported by the language server + */ +export type MIMETypes = [string, ...string[]]; +/** + * information on troubleshooting the installation or auto-detection of the language server + */ +export type Troubleshooting = string; +/** + * a date/time that might not have been recorded + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "nullable-date-time". + */ +export type NullableDateTime = string | null; +/** + * the count of currently-connected WebSocket handlers + */ +export type HandlerCount = number; +/** + * a list of tokens for running a command + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "shell-args". + */ +export type ShellArgs = string[]; + +/** + * describes the current state of (potentially) running language servers + */ +export interface JupyterLspServerStatusResponse { + [k: string]: any; +} +/** + * a JSON schema to configure the Language Server or extension behavior from the client + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "client-config-schema". + */ +export interface ClientConfigurationSchema { + [k: string]: any; +} +/** + * a list of installation approaches keyed by package manager, e.g. pip, npm, yarn, apt + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "install-bundle". + */ +export interface Installation { + [k: string]: InstallHelp; +} +/** + * an extension which can extend the functionality of the language server and client + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "language-server-extension". + */ +export interface LanguageServerExtension { + config_schema?: ClientConfigurationSchema; + display_name?: string; + install?: Installation; + [k: string]: any; +} +/** + * all properties that might be required to start and/or describe a Language Server + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "partial-language-server-spec". + */ +export interface ServerSpecProperties { + argv?: LaunchArguments; + config_schema?: ClientConfigurationSchema1; + debug_argv?: DebugArguments; + display_name?: DisplayName; + env?: EnvironmentVariables; + extend?: Extensions; + /** + * Whether to write un-saved documents to disk in a transient `.virtual_documents` directory. Well-behaved language servers that work against in-memory files should set this to `false`, which will become the default in the future. + */ + requires_documents_on_disk?: boolean; + install?: Installation1; + languages?: LanguageList; + mime_types?: MIMETypes; + troubleshoot?: Troubleshooting; + urls?: URLs; + version?: SpecSchemaVersion; + workspace_configuration?: WorkspaceConfiguration; + [k: string]: any; +} +/** + * a JSON schema to configure the Language Server behavior from the client + */ +export interface ClientConfigurationSchema1 { + [k: string]: any; +} +/** + * additional environment variables to set when starting the language server + */ +export interface EnvironmentVariables { + [ + k: string + ]: AnEnvironmentVariableMayContainPythonStringTemplateEvaluatedAgainstTheExistingEnvironmentEG$HOME; +} +/** + * a list of installation approaches keyed by package manager, e.g. pip, npm, yarn, apt + */ +export interface Installation1 { + [k: string]: InstallHelp; +} +/** + * a collection of urls keyed by type, e.g. home, issues + */ +export interface URLs { + [k: string]: string; +} +/** + * default values to include in the client `workspace/configuration` reply (also known as `serverSettings`). User may override these defaults. The keys should be fully qualified (dotted) names of settings (nested specification is not supported). + */ +export interface WorkspaceConfiguration { + [k: string]: any; +} +/** + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "servers-response". + */ +export interface ServersResponse { + sessions: Sessions; + specs?: LanguageServerSpecsMap; + version: SpecSchemaVersion; + [k: string]: any; +} +/** + * named server sessions that are, could be, or were running + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "sessions". + */ +export interface Sessions { + [k: string]: LanguageServerSession; +} +/** + * a language server session + * + * This interface was referenced by `Sessions`'s JSON-Schema definition + * via the `patternProperty` ".*". + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "session". + */ +export interface LanguageServerSession { + handler_count: HandlerCount; + /** + * date-time of last seen message from a WebSocket handler + */ + last_handler_message_at: string | null; + /** + * date-time of last seen message from the language server + */ + last_server_message_at: string | null; + spec: ServerSpecProperties; + /** + * a string describing the current state of the server + */ + status: 'not_started' | 'starting' | 'started' | 'stopping' | 'stopped'; +} +/** + * a set of language servers keyed by their implementation name + * + * This interface was referenced by `JupyterLspServerStatusResponse`'s JSON-Schema + * via the `definition` "language-server-specs-implementation-map". + */ +export interface LanguageServerSpecsMap { + [k: string]: LanguageServerSpec; +} diff --git a/packages/libro-lsp/src/tokens.ts b/packages/libro-lsp/src/tokens.ts new file mode 100644 index 00000000..f61b58d2 --- /dev/null +++ b/packages/libro-lsp/src/tokens.ts @@ -0,0 +1,843 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +// import type { ServerConnection } from '@jupyterlab/services'; + +import type { IEditor as ICodeEditor } from '@difizen/libro-code-editor'; +import type { NotebookView } from '@difizen/libro-core'; +import type { Disposable, Emitter, Event } from '@difizen/mana-app'; +import type * as rpc from 'vscode-jsonrpc'; +import type * as lsp from 'vscode-languageserver-protocol'; + +import type { WidgetLSPAdapter } from './adapters/adapter.js'; +import type { IForeignCodeExtractor } from './extractors/types.js'; +import type { + AnyCompletion, + AnyLocation, + ClientCapabilities, + LanguageIdentifier, +} from './lsp.js'; +import type { LanguageServer1 as LSPLanguageServerSettings } from './plugin.js'; +import type * as SCHEMA from './schema.js'; +import type { VirtualDocument } from './virtual/document.js'; +import type { + IDocumentInfo, + ILspConnection, + ILspOptions, +} from './ws-connection/types.js'; + +export { IDocumentInfo }; + +/** + * Example server keys==ids that are expected. The list is not exhaustive. + * Custom server keys are allowed. Constraining the values helps avoid errors, + * but at runtime any value is allowed. + */ +export type TLanguageServerId = + | 'pyright-extended' + | 'pyright' + | 'pylsp' + | 'bash-language-server' + | 'dockerfile-language-server-nodejs' + | 'javascript-typescript-langserver' + | 'unified-language-server' + | 'vscode-css-languageserver-bin' + | 'vscode-html-languageserver-bin' + | 'vscode-json-languageserver-bin' + | 'yaml-language-server' + | 'r-languageserver'; + +/** + * Type alias for the server ids. + */ +export type TServerKeys = TLanguageServerId; + +/** + * Type of language server configuration, it is a map between server + * id and its setting. + */ +export type TLanguageServerConfigurations = Partial< + Record +>; + +/** + * Type of language server session, it is a map between server + * id and the associated session. + */ +export type TSessionMap = Map; + +/** + * Type of language server specs, it is a map between server + * id and the associated specs. + */ +export type TSpecsMap = Map; + +/** + * Type alias for language server id, it helps to clarify other types. + */ +export type TLanguageId = string; + +// export const ILanguageServerManager = Symbol('ILanguageServerManager'); + +export interface ILanguageServerManager extends Disposable { + /** + * @alpha + * + * Signal emitted when the language server sessions are changed. + */ + sessionsChanged: Event; + + /** + * @alpha + * + * The current session information of running language servers. + */ + readonly sessions: TSessionMap; + + /** + * @alpha + * + * A promise that is fulfilled when the connection manager is ready. + */ + readonly ready: Promise; + + /** + * @alpha + * + * Current endpoint to get the status of running language servers + */ + readonly statusUrl: string; + + /** + * @alpha + * + * Status code of the `fetchSession` request. + */ + readonly statusCode: number; + + /** + * @alpha + * + * Check if the manager is enabled or disabled + */ + readonly isEnabled: boolean; + + /** + * @alpha + * + * Enable the language server services + */ + enable(): void; + + /** + * @alpha + * + * Disable the language server services + */ + disable(): void; + + /** + * @alpha + * + * An ordered list of matching >running< sessions, with servers of higher rank higher in the list + */ + getMatchingServers(options: IGetServerIdOptions): TLanguageServerId[]; + + /** + * @alpha + * + * A list of all known matching specs (whether detected or not). + */ + getMatchingSpecs(options: IGetServerIdOptions): TSpecsMap; + + /** + * @alpha + * + * Set the configuration for language servers + */ + setConfiguration(configuration: TLanguageServerConfigurations): void; + + /** + * @alpha + * + * Send a request to language server handler to get the session information. + */ + fetchSessions(): Promise; +} + +/** + * Virtual document namespace + */ +export namespace Document { + /** + * Code block description. + */ + export interface ICodeBlockOptions { + /** + * CodeEditor accessor + */ + ceEditor: IEditor; + + /** + * Type of the cell holding this block + */ + type: string; + + /** + * Editor text + * + * #### Notes + * This must always be available and should come from the document model directly. + */ + value: string; + } + + /** + * Code editor accessor. + */ + export interface IEditor { + /** + * CodeEditor getter. + * + * It will return `null` if the editor is not yet instantiated; + * e.g. to support windowed notebook. + */ + getEditor(): ICodeEditor | null; + + /** + * Promise getter that resolved when the editor is instantiated. + */ + ready(): Promise; + + /** + * Reveal the code editor in viewport. + * + * ### Notes + * The promise will resolve when the editor is instantiated and in + * the viewport. + */ + reveal(): Promise; + } + + /** + * Foreign context within code block. + */ + export interface IForeignContext { + /** + * The virtual document + */ + foreignDocument: VirtualDocument; + + /** + * The document holding the virtual document. + */ + parentHost: VirtualDocument; + } + + /** + * Virtual document block. + */ + export interface IVirtualDocumentBlock { + /** + * Line corresponding to the block in the entire foreign document + */ + virtualLine: number; + + /** + * The virtual document holding this virtual line. + */ + virtualDocument: VirtualDocument; + + /** + * The CM editor associated with this virtual line. + */ + editor: IEditor; + } +} + +/** + * LSP endpoint prefix. + */ +export const URL_NS = 'lsp'; + +export const ILanguageServerManagerFactory = Symbol('ILanguageServerManagerFactory'); + +export type ILanguageServerManagerFactory = ( + option: ILanguageServerManagerOptions, +) => ILanguageServerManager; + +export const ILanguageServerManagerOptions = Symbol('ILanguageServerManagerOptions'); + +export interface ILanguageServerManagerOptions { + /** + * The Jupyter server settings objec + */ + // settings?: ServerConnection.ISettings; + + /** + * Base URL of current JupyterLab server. + */ + baseUrl?: string; + + /** + * Number of connection retries to fetch the sessions. + * Default 2. + */ + retries?: number; + + /** + * The interval for retries, default 10 seconds. + */ + retriesInterval?: number; +} +/** + * The argument for getting server session or specs. + */ +export interface IGetServerIdOptions { + /** + * Language server id + */ + language?: TLanguageId; + + /** + * Server specs mime type. + */ + mimeType?: string; +} + +/** + * Option to create the websocket connection to the LSP proxy server + * on the backend. + */ +export interface ISocketConnectionOptions { + /** + * The virtual document trying to connect to the LSP server. + */ + virtualDocument: VirtualDocument; + + /** + * The language identifier, corresponding to the API endpoint on the + * LSP proxy server. + */ + language: string; + + /** + * LSP capabilities describing currently supported features + */ + capabilities: ClientCapabilities; + + /** + * Is the file format is supported by LSP? + */ + hasLspSupportedFile: boolean; +} + +/** + * @alpha + * + * Interface describing the LSP connection state + */ +export interface IDocumentConnectionData { + /** + * The virtual document connected to the language server + */ + virtualDocument: VirtualDocument; + /** + * The connection between the virtual document and the language server. + */ + connection: ILSPConnection; +} + +export const ILSPDocumentConnectionManager = Symbol('ILSPDocumentConnectionManager'); + +/** + * @alpha + * + * The LSP connection state manager + */ +export interface ILSPDocumentConnectionManager { + /** + * The mapping of document uri to the connection to language server. + */ + connections: Map; + + /** + * The mapping of document uri to the virtual document. + */ + documents: Map; + + /** + * The mapping of document uri to the widget adapter. + */ + adapters: Map>; + + /** + * Signal emitted when a connection is connected. + */ + connected: Event; + + /** + * Signal emitted when a connection is disconnected. + */ + disconnected: Event; + + /** + * Signal emitted when the language server is initialized. + */ + initialized: Event; + + /** + * Signal emitted when a virtual document is closed. + */ + closed: Event; + + /** + * Signal emitted when the content of a virtual document is changed. + */ + documentsChanged: Event>; + + /** + * The language server manager instance. + */ + languageServerManager: ILanguageServerManager; + + /** + * A promise that is fulfilled when the connection manager is ready. + */ + readonly ready: Promise; + + /** + * Handles the settings that do not require an existing connection + * with a language server (or can influence to which server the + * connection will be created, e.g. `rank`). + * + * This function should be called **before** initialization of servers. + */ + updateConfiguration(allServerSettings: TLanguageServerConfigurations): void; + + /** + * Handles the settings that the language servers accept using + * `onDidChangeConfiguration` messages, which should be passed under + * the "serverSettings" keyword in the setting registry. + * Other configuration options are handled by `updateConfiguration` instead. + * + * This function should be called **after** initialization of servers. + */ + updateServerConfigurations(allServerSettings: TLanguageServerConfigurations): void; + + /** + * Retry to connect to the server each `reconnectDelay` seconds + * and for `retrialsLeft` times. + */ + retryToConnect( + options: ISocketConnectionOptions, + reconnectDelay: number, + retrialsLeft: number, + ): Promise; + + /** + * Create a new connection to the language server + * @return A promise of the LSP connection + */ + connect( + options: ISocketConnectionOptions, + firstTimeoutSeconds?: number, + secondTimeoutMinute?: number, + ): Promise; + + /** + * Disconnect the connection to the language server of the requested + * language. + */ + disconnect(languageId: TLanguageServerId): void; + + /** + * Disconnect the signals of requested virtual document uri. + */ + unregisterDocument(uri: string): void; + + /** + * Register a widget adapter. + * + * @param path - path to current document widget of input adapter + * @param adapter - the adapter need to be registered + */ + registerAdapter(path: string, adapter: WidgetLSPAdapter): void; +} + +/** + * @alpha + * + * Interface describing the client feature + */ +export interface IFeature { + /** + * The feature identifier. It must be the same as the feature plugin id. + */ + id: string; + + /** + * LSP capabilities implemented by the feature. + */ + capabilities?: ClientCapabilities; +} + +export const ILSPFeatureManager = Symbol('ILSPFeatureManager'); + +/** + * @alpha + * + * The LSP feature manager + */ +export interface ILSPFeatureManager { + /** + * A read-only registry of all registered features. + */ + readonly features: IFeature[]; + + /** + * Register the new feature (frontend capability) + * for one or more code editor implementations. + */ + register(feature: IFeature): void; + + /** + * Signal emitted when a feature is registered + */ + featuresRegistered: Event; + + /** + * Get capabilities of all registered features + */ + clientCapabilities(): ClientCapabilities; +} + +export const ILSPCodeExtractorsManager = Symbol('ILSPCodeExtractorsManager'); + +/** + * @alpha + * + * Manages code transclusion plugins. + */ +export interface ILSPCodeExtractorsManager { + /** + * Get the foreign code extractors. + */ + getExtractors(cellType: string, hostLanguage: string | null): IForeignCodeExtractor[]; + + /** + * Register the extraction rules to be applied in documents with language `host_language`. + */ + register( + extractor: IForeignCodeExtractor, + hostLanguage: LanguageIdentifier | null, + ): void; +} + +/** + * Argument for creating a connection to the LSP proxy server. + */ +export interface ILSPOptions extends ILspOptions { + /** + * Client capabilities implemented by the client. + */ + capabilities: ClientCapabilities; + + /** + * Language server id. + */ + serverIdentifier?: string; +} + +/** + * Method strings are reproduced here because a non-typing import of + * `vscode-languageserver-protocol` is ridiculously expensive. + */ +export namespace Method { + /** Server notifications */ + export enum ServerNotification { + PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics', + SHOW_MESSAGE = 'window/showMessage', + LOG_TRACE = '$/logTrace', + LOG_MESSAGE = 'window/logMessage', + } + + /** Client notifications */ + export enum ClientNotification { + DID_CHANGE = 'textDocument/didChange', + DID_CHANGE_CONFIGURATION = 'workspace/didChangeConfiguration', + DID_OPEN = 'textDocument/didOpen', + DID_SAVE = 'textDocument/didSave', + INITIALIZED = 'initialized', + SET_TRACE = '$/setTrace', + } + + /** Server requests */ + export enum ServerRequest { + REGISTER_CAPABILITY = 'client/registerCapability', + SHOW_MESSAGE_REQUEST = 'window/showMessageRequest', + UNREGISTER_CAPABILITY = 'client/unregisterCapability', + WORKSPACE_CONFIGURATION = 'workspace/configuration', + } + + /** Client requests */ + export enum ClientRequest { + COMPLETION = 'textDocument/completion', + COMPLETION_ITEM_RESOLVE = 'completionItem/resolve', + DEFINITION = 'textDocument/definition', + DOCUMENT_HIGHLIGHT = 'textDocument/documentHighlight', + DOCUMENT_SYMBOL = 'textDocument/documentSymbol', + HOVER = 'textDocument/hover', + IMPLEMENTATION = 'textDocument/implementation', + INITIALIZE = 'initialize', + REFERENCES = 'textDocument/references', + RENAME = 'textDocument/rename', + SIGNATURE_HELP = 'textDocument/signatureHelp', + TYPE_DEFINITION = 'textDocument/typeDefinition', + FORMATTING = 'textDocument/formatting', + RANGE_FORMATTING = 'textDocument/rangeFormatting', + } +} + +/** + * Interface describing the notifications that come from the server. + */ +export interface IServerNotifyParams { + [Method.ServerNotification.LOG_MESSAGE]: lsp.LogMessageParams; + [Method.ServerNotification.LOG_TRACE]: rpc.LogTraceParams; + [Method.ServerNotification.PUBLISH_DIAGNOSTICS]: lsp.PublishDiagnosticsParams; + [Method.ServerNotification.SHOW_MESSAGE]: lsp.ShowMessageParams; +} + +/** + * Interface describing the notifications that come from the client. + */ +export interface IClientNotifyParams { + [Method.ClientNotification + .DID_CHANGE_CONFIGURATION]: lsp.DidChangeConfigurationParams; + [Method.ClientNotification.DID_CHANGE]: lsp.DidChangeTextDocumentParams; + [Method.ClientNotification.DID_OPEN]: lsp.DidOpenTextDocumentParams; + [Method.ClientNotification.DID_SAVE]: lsp.DidSaveTextDocumentParams; + [Method.ClientNotification.INITIALIZED]: lsp.InitializedParams; + [Method.ClientNotification.SET_TRACE]: rpc.SetTraceParams; +} + +/** + * Interface describing the requests sent to the server. + */ +export interface IServerRequestParams { + [Method.ServerRequest.REGISTER_CAPABILITY]: lsp.RegistrationParams; + [Method.ServerRequest.SHOW_MESSAGE_REQUEST]: lsp.ShowMessageRequestParams; + [Method.ServerRequest.UNREGISTER_CAPABILITY]: lsp.UnregistrationParams; + [Method.ServerRequest.WORKSPACE_CONFIGURATION]: lsp.ConfigurationParams; +} + +/** + * Interface describing the responses received from the server. + */ +export interface IServerResult { + [Method.ServerRequest.REGISTER_CAPABILITY]: void; + [Method.ServerRequest.SHOW_MESSAGE_REQUEST]: lsp.MessageActionItem | null; + [Method.ServerRequest.UNREGISTER_CAPABILITY]: void; + [Method.ServerRequest.WORKSPACE_CONFIGURATION]: any[]; +} + +/** + * Interface describing the request sent to the client. + */ +export interface IClientRequestParams { + [Method.ClientRequest.COMPLETION_ITEM_RESOLVE]: lsp.CompletionItem; + [Method.ClientRequest.COMPLETION]: lsp.CompletionParams; + [Method.ClientRequest.DEFINITION]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.DOCUMENT_HIGHLIGHT]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.DOCUMENT_SYMBOL]: lsp.DocumentSymbolParams; + [Method.ClientRequest.HOVER]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.IMPLEMENTATION]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.INITIALIZE]: lsp.InitializeParams; + [Method.ClientRequest.REFERENCES]: lsp.ReferenceParams; + [Method.ClientRequest.RENAME]: lsp.RenameParams; + [Method.ClientRequest.SIGNATURE_HELP]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.TYPE_DEFINITION]: lsp.TextDocumentPositionParams; + [Method.ClientRequest.FORMATTING]: lsp.DocumentFormattingParams; + [Method.ClientRequest.RANGE_FORMATTING]: lsp.DocumentRangeFormattingParams; +} + +/** + * Interface describing the responses received from the client. + */ +export interface IClientResult { + [Method.ClientRequest.COMPLETION_ITEM_RESOLVE]: lsp.CompletionItem; + [Method.ClientRequest.COMPLETION]: AnyCompletion; + [Method.ClientRequest.DEFINITION]: AnyLocation; + [Method.ClientRequest.DOCUMENT_HIGHLIGHT]: lsp.DocumentHighlight[]; + [Method.ClientRequest.DOCUMENT_SYMBOL]: lsp.DocumentSymbol[]; + [Method.ClientRequest.HOVER]: lsp.Hover | null; + [Method.ClientRequest.IMPLEMENTATION]: AnyLocation; + [Method.ClientRequest.INITIALIZE]: lsp.InitializeResult; + [Method.ClientRequest.REFERENCES]: lsp.Location[] | null; + [Method.ClientRequest.RENAME]: lsp.WorkspaceEdit; + [Method.ClientRequest.SIGNATURE_HELP]: lsp.SignatureHelp; + [Method.ClientRequest.TYPE_DEFINITION]: AnyLocation; + [Method.ClientRequest.FORMATTING]: lsp.TextEdit[] | null; + [Method.ClientRequest.RANGE_FORMATTING]: lsp.TextEdit[] | null; +} + +/** + * Type of server notification handlers, it is a map between the server + * notification name and the associated `ISignal`. + */ +export type ServerNotifications< + T extends keyof IServerNotifyParams = keyof IServerNotifyParams, +> = { + readonly [key in T]: Emitter; +}; + +/** + * Type of client notification handlers, it is a map between the client + * notification name and the associated signal. + */ +export type ClientNotifications< + T extends keyof IClientNotifyParams = keyof IClientNotifyParams, +> = { + readonly [key in T]: Emitter; +}; + +/** + * Interface describing the client request handler. + */ +export interface IClientRequestHandler< + T extends keyof IClientRequestParams = keyof IClientRequestParams, +> { + request(params: IClientRequestParams[T]): Promise; +} + +/** + * Interface describing the server request handler. + */ +export interface IServerRequestHandler< + T extends keyof IServerRequestParams = keyof IServerRequestParams, +> { + setHandler( + handler: ( + params: IServerRequestParams[T], + connection?: ILSPConnection, + ) => Promise, + ): void; + clearHandler(): void; +} + +/** + * Type of client request handlers, it is a map between the client + * request name and the associated handler. + */ +export type ClientRequests< + T extends keyof IClientRequestParams = keyof IClientRequestParams, +> = { + // has async request(params) returning a promise with result. + readonly [key in T]: IClientRequestHandler; +}; + +/** + * Type of server request handlers, it is a map between the server + * request name and the associated handler. + */ +export type ServerRequests< + T extends keyof IServerRequestParams = keyof IServerRequestParams, +> = { + // has async request(params) returning a promise with result. + readonly [key in T]: IServerRequestHandler; +}; + +/** + * @alpha + * + * Interface describing he connection to the language server. + */ +export interface ILSPConnection extends ILspConnection { + /** + * @alpha + * + * Identifier of the language server + */ + serverIdentifier?: string; + + /** + * @alpha + * + * Language of the language server + */ + serverLanguage?: string; + + /** + * @alpha + * + * Should log all communication? + */ + logAllCommunication: boolean; + + /** + * @alpha + * + * Notifications that come from the client. + */ + clientNotifications: ClientNotifications; + + /** + * @alpha + * + * Notifications that come from the server. + */ + serverNotifications: ServerNotifications; + + /** + * @alpha + * + * Requests that come from the client. + */ + clientRequests: ClientRequests; + + /** + * @alpha + * + * Responses that come from the server. + */ + serverRequests: ServerRequests; + + /** + * @alpha + * + * Signal emitted when the connection is closed. + */ + closeSignal: Event; + + /** + * @alpha + * + * Signal emitted when the connection receives an error + * message.. + */ + errorSignal: Event; + + /** + * @alpha + * + * Signal emitted when the connection is initialized. + */ + serverInitialized: Event>; + + /** + * @alpha + * + * Send the open request to the backend when the server is + * ready. + */ + sendOpenWhenReady(documentInfo: IDocumentInfo): void; + + /** + * @alpha + * + * Send all changes to the server. + */ + sendFullTextChange(text: string, documentInfo: IDocumentInfo): void; +} diff --git a/packages/libro-lsp/src/utils.ts b/packages/libro-lsp/src/utils.ts new file mode 100644 index 00000000..d0172bfe --- /dev/null +++ b/packages/libro-lsp/src/utils.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-param-reassign */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { ReadonlyJSONObject, ReadonlyJSONValue } from '@difizen/libro-common'; +import mergeWith from 'lodash.mergewith'; + +/** + * Helper to wait for timeout. + * + * @param timeout - time out in ms + */ +export async function sleep(timeout: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, timeout); + }); +} + +/** + * Wait for an event by pooling the `isReady` function. + */ +export function untilReady( + isReady: CallableFunction, + maxRetrials = 35, + interval = 50, + intervalModifier = (i: number) => i, +): Promise { + return (async () => { + let i = 0; + while (isReady() !== true) { + i += 1; + if (maxRetrials !== -1 && i > maxRetrials) { + throw Error('Too many retrials'); + } + interval = intervalModifier(interval); + await sleep(interval); + } + return isReady; + })(); +} + +/** + * Convert dotted path into dictionary. + */ +export function expandDottedPaths(obj: ReadonlyJSONObject): ReadonlyJSONObject { + const settings: any = []; + for (const key in obj) { + const parsed = expandPath(key.split('.'), obj[key]); + settings.push(parsed); + } + return mergeWith({}, ...settings); +} + +/** + * The docs for many language servers show settings in the + * VSCode format, e.g.: "pyls.plugins.pyflakes.enabled" + * + * VSCode converts that dot notation to JSON behind the scenes, + * as the language servers themselves don't accept that syntax. + */ +export const expandPath = ( + path: string[], + value: ReadonlyJSONValue, +): ReadonlyJSONObject => { + const obj: any = Object.create(null); + + let curr = obj; + path.forEach((prop: string, i: any) => { + curr[prop] = Object.create(null); + + if (i === path.length - 1) { + curr[prop] = value; + } else { + curr = curr[prop]; + } + }); + + return obj; +}; + +/** + * An extended map which will create value for key on the fly. + */ +export class DefaultMap extends Map { + constructor( + // eslint-disable-next-line @typescript-eslint/parameter-properties, @typescript-eslint/no-parameter-properties + protected defaultFactory: (...args: any[]) => V, + entries?: readonly (readonly [K, V])[] | null, + ) { + super(entries); + } + + override get(k: K): V { + return this.getOrCreate(k); + } + + getOrCreate(k: K, ...args: any[]): V { + if (this.has(k)) { + return super.get(k)!; + } else { + const v = this.defaultFactory(k, ...args); + this.set(k, v); + return v; + } + } +} diff --git a/packages/libro-lsp/src/virtual/document.ts b/packages/libro-lsp/src/virtual/document.ts new file mode 100644 index 00000000..c17a2a0b --- /dev/null +++ b/packages/libro-lsp/src/virtual/document.ts @@ -0,0 +1,1247 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import type { + IRange, + IPosition as CodeEditorPosition, +} from '@difizen/libro-code-editor'; +import { Emitter } from '@difizen/mana-app'; +import type { Disposable, Event } from '@difizen/mana-app'; + +import { DocumentConnectionManager } from '../connection-manager.js'; +import type { IForeignCodeExtractor } from '../extractors/types.js'; +import type { LanguageIdentifier } from '../lsp.js'; +import type { + Position, + IEditorPosition, + IRootPosition, + ISourcePosition, + IVirtualPosition, +} from '../positioning.js'; +import type { Document, ILSPCodeExtractorsManager } from '../tokens.js'; +import { DefaultMap, untilReady } from '../utils.js'; +import type { IDocumentInfo } from '../ws-connection/types.js'; + +type language = string; + +interface IVirtualLine { + /** + * Inspections for which document should be skipped for this virtual line? + */ + skipInspect: VirtualDocument.idPath[]; + + /** + * Where does the virtual line belongs to in the source document? + */ + sourceLine: number | null; + + /** + * The editor holding this virtual line + */ + editor: Document.IEditor; +} + +export type ForeignDocumentsMap = Map; + +interface ISourceLine { + /** + * Line corresponding to the block in the entire foreign document + */ + virtualLine: number; + + /** + * The CM editor associated with this virtual line. + */ + editor: Document.IEditor; + + /** + * Line in the CM editor corresponding to the virtual line. + */ + editorLine: number; + + /** + * Shift of the virtual line + */ + editorShift: CodeEditorPosition; + + /** + * Everything which is not in the range of foreign documents belongs to the host. + */ + foreignDocumentsMap: ForeignDocumentsMap; +} + +/** + * Check if given position is within range. + * Both start and end are inclusive. + * @param position + * @param range + */ +export function isWithinRange(position: CodeEditorPosition, range: IRange): boolean { + if (range.start.line === range.end.line) { + return ( + position.line === range.start.line && + position.column >= range.start.column && + position.column <= range.end.column + ); + } + + return ( + (position.line === range.start.line && + position.column >= range.start.column && + position.line < range.end.line) || + (position.line > range.start.line && + position.column <= range.end.column && + position.line === range.end.line) || + (position.line > range.start.line && position.line < range.end.line) + ); +} + +/** + * A virtual implementation of IDocumentInfo + */ +export class VirtualDocumentInfo implements IDocumentInfo { + /** + * Creates an instance of VirtualDocumentInfo. + * @param document - the virtual document need to + * be wrapped. + */ + constructor(document: VirtualDocument) { + this._document = document; + } + + /** + * Current version of the virtual document. + */ + version = 0; + + /** + * Get the text content of the virtual document. + */ + get text(): string { + return this._document.value; + } + + /** + * Get the uri of the virtual document, if the document is not available, + * it returns an empty string, users need to check for the length of returned + * value before using it. + */ + get uri(): string { + const uris = DocumentConnectionManager.solveUris(this._document, this.languageId); + if (!uris) { + return ''; + } + return uris.document; + } + + /** + * Get the language identifier of the document. + */ + get languageId(): string { + return this._document.language; + } + + /** + * The wrapped virtual document. + */ + protected _document: VirtualDocument; +} + +export interface IVirtualDocumentOptions { + /** + * The language identifier of the document. + */ + language: LanguageIdentifier; + + /** + * The foreign code extractor manager token. + */ + foreignCodeExtractors: ILSPCodeExtractorsManager; + + /** + * Path to the document. + */ + path: string; + + /** + * File extension of the document. + */ + fileExtension: string | undefined; + + /** + * Notebooks or any other aggregates of documents are not supported + * by the LSP specification, and we need to make appropriate + * adjustments for them, pretending they are simple files + * so that the LSP servers do not refuse to cooperate. + */ + hasLspSupportedFile: boolean; + + /** + * Being standalone is relevant to foreign documents + * and defines whether following chunks of code in the same + * language should be appended to this document (false, not standalone) + * or should be considered separate documents (true, standalone) + * + */ + standalone?: boolean; + + /** + * Parent of the current virtual document. + */ + parent?: VirtualDocument; +} + +/** + * + * A notebook can hold one or more virtual documents; there is always one, + * "root" document, corresponding to the language of the kernel. All other + * virtual documents are extracted out of the notebook, based on magics, + * or other syntax constructs, depending on the kernel language. + * + * Virtual documents represent the underlying code in a single language, + * which has been parsed excluding interactive kernel commands (magics) + * which could be misunderstood by the specific LSP server. + * + * VirtualDocument has no awareness of the notebook or editor it lives in, + * however it is able to transform its content back to the notebook space, + * as it keeps editor coordinates for each virtual line. + * + * The notebook/editor aware transformations are preferred to be placed in + * VirtualEditor descendants rather than here. + * + * No dependency on editor implementation (such as CodeMirrorEditor) + * is allowed for VirtualEditor. + */ +export class VirtualDocument implements Disposable { + constructor(options: IVirtualDocumentOptions) { + this.options = options; + this.path = this.options.path; + this.fileExtension = options.fileExtension; + this.hasLspSupportedFile = options.hasLspSupportedFile; + this.parent = options.parent; + this.language = options.language; + + this.virtualLines = new Map(); + this.sourceLines = new Map(); + this.foreignDocuments = new Map(); + this._editorToSourceLine = new Map(); + this._foreignCodeExtractors = options.foreignCodeExtractors; + this.standalone = options.standalone || false; + this.instanceId = VirtualDocument.instancesCount; + VirtualDocument.instancesCount += 1; + this.unusedStandaloneDocuments = new DefaultMap(() => new Array()); + this._remainingLifetime = 6; + + this.unusedDocuments = new Set(); + this.documentInfo = new VirtualDocumentInfo(this); + this.updateManager = new UpdateManager(this); + this.updateManager.updateBegan(this._updateBeganSlot, this); + this.updateManager.blockAdded(this._blockAddedSlot, this); + this.updateManager.updateFinished(this._updateFinishedSlot, this); + this.clear(); + } + + /** + * Convert from code editor position into code mirror position. + */ + static ceToCm(position: CodeEditorPosition): Position { + return { line: position.line, ch: position.column }; + } + + /** + * Number of blank lines appended to the virtual document between + * each cell. + */ + blankLinesBetweenCells = 2; + + /** + * Line number of the last line in the real document. + */ + lastSourceLine: number; + + /** + * Line number of the last line in the virtual document. + */ + lastVirtualLine: number; + + /** + * the remote document uri, version and other server-related info + */ + documentInfo: IDocumentInfo; + + /** + * Parent of the current virtual document. + */ + parent?: VirtualDocument | null; + + /** + * The language identifier of the document. + */ + readonly language: string; + + /** + * Being standalone is relevant to foreign documents + * and defines whether following chunks of code in the same + * language should be appended to this document (false, not standalone) + * or should be considered separate documents (true, standalone) + */ + readonly standalone: boolean; + + /** + * Path to the document. + */ + readonly path: string; + + /** + * File extension of the document. + */ + readonly fileExtension: string | undefined; + + /** + * Notebooks or any other aggregates of documents are not supported + * by the LSP specification, and we need to make appropriate + * adjustments for them, pretending they are simple files + * so that the LSP servers do not refuse to cooperate. + */ + readonly hasLspSupportedFile: boolean; + + /** + * Map holding the children `VirtualDocument` . + */ + readonly foreignDocuments: Map; + + /** + * The update manager object. + */ + readonly updateManager: UpdateManager; + + /** + * Unique id of the virtual document. + */ + readonly instanceId: number; + + /** + * Test whether the document is disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Signal emitted when the foreign document is closed + */ + get foreignDocumentClosed(): Event { + return this._foreignDocumentClosed.event; + } + + /** + * Signal emitted when the foreign document is opened + */ + get foreignDocumentOpened(): Event { + return this._foreignDocumentOpened.event; + } + + /** + * Signal emitted when the foreign document is changed + */ + get changed(): Event { + return this._changed.event; + } + + /** + * Id of the virtual document. + */ + get virtualId(): VirtualDocument.virtualId { + // for easier debugging, the language information is included in the ID: + return this.standalone + ? this.instanceId + '(' + this.language + ')' + : this.language; + } + + /** + * Return the ancestry to this document. + */ + get ancestry(): VirtualDocument[] { + if (!this.parent) { + return [this]; + } + return this.parent.ancestry.concat([this]); + } + + /** + * Return the id path to the virtual document. + */ + get idPath(): VirtualDocument.idPath { + if (!this.parent) { + return this.virtualId; + } + return this.parent.idPath + '-' + this.virtualId; + } + + /** + * Get the uri of the virtual document. + */ + get uri(): VirtualDocument.uri { + const encodedPath = encodeURI(this.path); + if (!this.parent) { + return encodedPath; + } + return encodedPath + '.' + this.idPath + '.' + this.fileExtension; + } + + /** + * Get the text value of the document + */ + get value(): string { + const linesPadding = '\n'.repeat(this.blankLinesBetweenCells); + return this.lineBlocks.join(linesPadding); + } + + /** + * Get the last line in the virtual document + */ + get lastLine(): string { + const linesInLastBlock = this.lineBlocks[this.lineBlocks.length - 1].split('\n'); + return linesInLastBlock[linesInLastBlock.length - 1]; + } + + /** + * Get the root document of current virtual document. + */ + get root(): VirtualDocument { + return this.parent ? this.parent.root : this; + } + + /** + * Dispose the virtual document. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + + this.parent = null; + + this.closeAllForeignDocuments(); + + this.updateManager.dispose(); + // clear all the maps + + this.foreignDocuments.clear(); + this.sourceLines.clear(); + this.unusedDocuments.clear(); + this.unusedStandaloneDocuments.clear(); + this.virtualLines.clear(); + + // just to be sure - if anything is accessed after disposal (it should not) we + // will get altered by errors in the console AND this will limit memory leaks + + this.documentInfo = null as any; + this.lineBlocks = null as any; + } + + /** + * Clear the virtual document and all related stuffs + */ + clear(): void { + for (const document of this.foreignDocuments.values()) { + document.clear(); + } + + // TODO - deep clear (assure that there is no memory leak) + this.unusedStandaloneDocuments.clear(); + + this.unusedDocuments = new Set(); + this.virtualLines.clear(); + this.sourceLines.clear(); + this.lastVirtualLine = 0; + this.lastSourceLine = 0; + this.lineBlocks = []; + } + + /** + * Get the virtual document from the cursor position of the source + * document + * @param position - position in source document + */ + documentAtSourcePosition(position: ISourcePosition): VirtualDocument { + const sourceLine = this.sourceLines.get(position.line); + + if (!sourceLine) { + return this; + } + + const sourcePositionCe: CodeEditorPosition = { + line: sourceLine.editorLine, + column: position.ch, + }; + + for (const [ + range, + { virtualDocument: document }, + ] of sourceLine.foreignDocumentsMap) { + if (isWithinRange(sourcePositionCe, range)) { + const sourcePositionCm = { + line: sourcePositionCe.line - range.start.line, + ch: sourcePositionCe.column - range.start.column, + }; + + return document.documentAtSourcePosition(sourcePositionCm as ISourcePosition); + } + } + + return this; + } + + /** + * Detect if the input source position is belong to the current + * virtual document. + * + * @param sourcePosition - position in the source document + */ + isWithinForeign(sourcePosition: ISourcePosition): boolean { + const sourceLine = this.sourceLines.get(sourcePosition.line)!; + + const sourcePositionCe: CodeEditorPosition = { + line: sourceLine.editorLine, + column: sourcePosition.ch, + }; + for (const [range] of sourceLine.foreignDocumentsMap) { + if (isWithinRange(sourcePositionCe, range)) { + return true; + } + } + return false; + } + + /** + * Compute the position in root document from the position of + * a child editor. + * + * @param editor - the active editor. + * @param position - position in the active editor. + */ + transformFromEditorToRoot( + editor: Document.IEditor, + position: IEditorPosition, + ): IRootPosition | null { + if (!this._editorToSourceLine.has(editor)) { + console.warn('Editor not found in _editorToSourceLine map'); + return null; + } + const shift = this._editorToSourceLine.get(editor)!; + return { + ...(position as Position), + line: position.line + shift, + } as IRootPosition; + } + + /** + * Compute the position in virtual document from the position of + * a child editor. + * + * @param editor - the active editor. + * @param position - position in the active editor. + */ + transformEditorToVirtual( + editor: Document.IEditor, + position: IEditorPosition, + ): IVirtualPosition | null { + const rootPosition = this.transformFromEditorToRoot(editor, position); + if (!rootPosition) { + return null; + } + return this.virtualPositionAtDocument(rootPosition); + } + + /** + * Compute the position in the virtual document from the position + * in the source document. + * + * @param sourcePosition - position in source document + */ + virtualPositionAtDocument(sourcePosition: ISourcePosition): IVirtualPosition { + const sourceLine = this.sourceLines.get(sourcePosition.line); + if (!sourceLine) { + throw new Error('Source line not mapped to virtual position'); + } + const virtualLine = sourceLine.virtualLine; + + // position inside the cell (block) + const sourcePositionCe: CodeEditorPosition = { + line: sourceLine.editorLine, + column: sourcePosition.ch, + }; + + for (const [range, content] of sourceLine.foreignDocumentsMap) { + // eslint-disable-next-line @typescript-eslint/no-shadow + const { virtualLine, virtualDocument: document } = content; + if (isWithinRange(sourcePositionCe, range)) { + // position inside the foreign document block + const sourcePositionCm = { + line: sourcePositionCe.line - range.start.line, + ch: sourcePositionCe.column - range.start.column, + }; + if (document.isWithinForeign(sourcePositionCm as ISourcePosition)) { + return this.virtualPositionAtDocument(sourcePositionCm as ISourcePosition); + } else { + // where in this block in the entire foreign document? + sourcePositionCm.line += virtualLine; + return sourcePositionCm as IVirtualPosition; + } + } + } + + return { + ch: sourcePosition.ch, + line: virtualLine, + } as IVirtualPosition; + } + + /** + * Append a code block to the end of the virtual document. + * + * @param block - block to be appended + * @param editorShift - position shift in source + * document + * @param [virtualShift] - position shift in + * virtual document. + */ + appendCodeBlock( + block: Document.ICodeBlockOptions, + editorShift: CodeEditorPosition = { line: 0, column: 0 }, + virtualShift?: CodeEditorPosition, + ): void { + const cellCode = block.value; + const ceEditor = block.ceEditor; + + if (this.isDisposed) { + console.warn('Cannot append code block: document disposed'); + return; + } + const sourceCellLines = cellCode.split('\n'); + const { lines, foreignDocumentsMap } = this.prepareCodeBlock(block, editorShift); + + for (let i = 0; i < lines.length; i++) { + this.virtualLines.set(this.lastVirtualLine + i, { + skipInspect: [], + editor: ceEditor, + // TODO this is incorrect, wont work if something was extracted + sourceLine: this.lastSourceLine + i, + }); + } + for (let i = 0; i < sourceCellLines.length; i++) { + this.sourceLines.set(this.lastSourceLine + i, { + editorLine: i, + editorShift: { + line: editorShift.line - (virtualShift?.line || 0), + column: i === 0 ? editorShift.column - (virtualShift?.column || 0) : 0, + }, + // TODO: move those to a new abstraction layer (DocumentBlock class) + editor: ceEditor, + foreignDocumentsMap, + // TODO this is incorrect, wont work if something was extracted + virtualLine: this.lastVirtualLine + i, + }); + } + + this.lastVirtualLine += lines.length; + + // one empty line is necessary to separate code blocks, next 'n' lines are to silence linters; + // the final cell does not get the additional lines (thanks to the use of join, see below) + + this.lineBlocks.push(lines.join('\n') + '\n'); + + // adding the virtual lines for the blank lines + for (let i = 0; i < this.blankLinesBetweenCells; i++) { + this.virtualLines.set(this.lastVirtualLine + i, { + skipInspect: [this.idPath], + editor: ceEditor, + sourceLine: null, + }); + } + + this.lastVirtualLine += this.blankLinesBetweenCells; + this.lastSourceLine += sourceCellLines.length; + } + + /** + * Extract a code block into list of string in supported language and + * a map of foreign document if any. + * @param block - block to be appended + * @param editorShift - position shift in source document + */ + prepareCodeBlock( + block: Document.ICodeBlockOptions, + editorShift: CodeEditorPosition = { line: 0, column: 0 }, + ): { + lines: string[]; + foreignDocumentsMap: Map; + } { + const { cellCodeKept, foreignDocumentsMap } = this.extractForeignCode( + block, + editorShift, + ); + const lines = cellCodeKept.split('\n'); + return { lines, foreignDocumentsMap }; + } + + /** + * Extract the foreign code from input block by using the registered + * extractors. + * @param block - block to be appended + * @param editorShift - position shift in source document + */ + extractForeignCode( + block: Document.ICodeBlockOptions, + editorShift: CodeEditorPosition, + ): { + cellCodeKept: string; + foreignDocumentsMap: Map; + } { + const foreignDocumentsMap = new Map(); + + let cellCode = block.value; + const extractorsForAnyLang = this._foreignCodeExtractors.getExtractors( + block.type, + null, + ); + const extractorsForCurrentLang = this._foreignCodeExtractors.getExtractors( + block.type, + this.language, + ); + + for (const extractor of [...extractorsForAnyLang, ...extractorsForCurrentLang]) { + if (!extractor.hasForeignCode(cellCode, block.type)) { + continue; + } + + const results = extractor.extractForeignCode(cellCode); + + let keptCellCode = ''; + + for (const result of results) { + if (result.foreignCode !== null) { + // result.range should only be null if result.foregin_code is null + if (result.range === null) { + console.warn( + 'Failure in foreign code extraction: `range` is null but `foreign_code` is not!', + ); + continue; + } + const foreignDocument = this.chooseForeignDocument(extractor); + foreignDocumentsMap.set(result.range, { + virtualLine: foreignDocument.lastVirtualLine, + virtualDocument: foreignDocument, + editor: block.ceEditor, + }); + const foreignShift = { + line: editorShift.line + result.range.start.line, + column: editorShift.column + result.range.start.column, + }; + foreignDocument.appendCodeBlock( + { + value: result.foreignCode, + ceEditor: block.ceEditor, + type: 'code', + }, + foreignShift, + result.virtualShift!, + ); + } + if (result.hostCode != null) { + keptCellCode += result.hostCode; + } + } + // not breaking - many extractors are allowed to process the code, one after each other + // (think JS and CSS in HTML, or %R inside of %%timeit). + + cellCode = keptCellCode; + } + + return { cellCodeKept: cellCode, foreignDocumentsMap }; + } + + /** + * Close a foreign document and disconnect all associated signals + */ + closeForeign(document: VirtualDocument): void { + this._foreignDocumentClosed.fire({ + foreignDocument: document, + parentHost: this, + }); + // remove it from foreign documents list + this.foreignDocuments.delete(document.virtualId); + // and delete the documents within it + document.closeAllForeignDocuments(); + + // document.foreignDocumentClosed.disconnect(this.forwardClosedSignal, this); + // document.foreignDocumentOpened.disconnect(this.forwardOpenedSignal, this); + document.dispose(); + } + + /** + * Close all foreign documents. + */ + closeAllForeignDocuments(): void { + for (const document of this.foreignDocuments.values()) { + this.closeForeign(document); + } + } + + /** + * Close all expired documents. + */ + closeExpiredDocuments(): void { + for (const document of this.unusedDocuments.values()) { + document.remainingLifetime -= 1; + if (document.remainingLifetime <= 0) { + document.dispose(); + } + } + } + + /** + * Transform the position of the source to the editor + * position. + * + * @param pos - position in the source document + * @return position in the editor. + */ + transformSourceToEditor(pos: ISourcePosition): IEditorPosition { + const sourceLine = this.sourceLines.get(pos.line)!; + const editorLine = sourceLine.editorLine; + const editorShift = sourceLine.editorShift; + return { + // only shift column in the line beginning the virtual document (first list of the editor in cell magics, but might be any line of editor in line magics!) + ch: pos.ch + (editorLine === 0 ? editorShift.column : 0), + line: editorLine + editorShift.line, + // TODO or: + // line: pos.line + editor_shift.line - this.first_line_of_the_block(editor) + } as IEditorPosition; + } + + /** + * Transform the position in the virtual document to the + * editor position. + * Can be null because some lines are added as padding/anchors + * to the virtual document and those do not exist in the source document + * and thus they are absent in the editor. + */ + transformVirtualToEditor(virtualPosition: IVirtualPosition): IEditorPosition | null { + const sourcePosition = this.transformVirtualToSource(virtualPosition); + if (!sourcePosition) { + return null; + } + return this.transformSourceToEditor(sourcePosition); + } + + /** + * Transform the position in the virtual document to the source. + * Can be null because some lines are added as padding/anchors + * to the virtual document and those do not exist in the source document. + */ + transformVirtualToSource(position: IVirtualPosition): ISourcePosition | null { + const line = this.virtualLines.get(position.line)!.sourceLine; + if (line == null) { + return null; + } + return { + ch: position.ch, + line: line, + } as ISourcePosition; + } + + /** + * Get the corresponding editor of the virtual line. + */ + getEditorAtVirtualLine(pos: IVirtualPosition): Document.IEditor { + let line = pos.line; + // tolerate overshot by one (the hanging blank line at the end) + if (!this.virtualLines.has(line)) { + line -= 1; + } + return this.virtualLines.get(line)!.editor; + } + + /** + * Get the corresponding editor of the source line + */ + getEditorAtSourceLine(pos: ISourcePosition): Document.IEditor { + return this.sourceLines.get(pos.line)!.editor; + } + + /** + * Recursively emits changed signal from the document or any descendant foreign document. + */ + maybeEmitChanged(): void { + if (this.value !== this.previousValue) { + this._changed.fire(this); + } + this.previousValue = this.value; + for (const document of this.foreignDocuments.values()) { + document.maybeEmitChanged(); + } + } + + /** + * When this counter goes down to 0, the document will be destroyed and the associated connection will be closed; + * This is meant to reduce the number of open connections when a a foreign code snippet was removed from the document. + * + * Note: top level virtual documents are currently immortal (unless killed by other means); it might be worth + * implementing culling of unused documents, but if and only if JupyterLab will also implement culling of + * idle kernels - otherwise the user experience could be a bit inconsistent, and we would need to invent our own rules. + */ + protected get remainingLifetime(): number { + if (!this.parent) { + return Infinity; + } + return this._remainingLifetime; + } + + protected set remainingLifetime(value: number) { + if (this.parent) { + this._remainingLifetime = value; + } + } + + /** + * Virtual lines keep all the lines present in the document AND extracted to the foreign document. + */ + protected virtualLines: Map; + protected sourceLines: Map; + protected lineBlocks: string[]; + + protected unusedDocuments: Set; + protected unusedStandaloneDocuments: DefaultMap; + + protected _isDisposed = false; + protected _remainingLifetime: number; + protected _editorToSourceLine: Map; + protected _editorToSourceLineNew: Map; + protected _foreignCodeExtractors: ILSPCodeExtractorsManager; + protected previousValue: string; + protected static instancesCount = 0; + protected readonly options: IVirtualDocumentOptions; + + /** + * Get the foreign document that can be opened with the input extractor. + */ + protected chooseForeignDocument(extractor: IForeignCodeExtractor): VirtualDocument { + let foreignDocument: VirtualDocument; + // if not standalone, try to append to existing document + const foreignExists = this.foreignDocuments.has(extractor.language); + if (!extractor.standalone && foreignExists) { + foreignDocument = this.foreignDocuments.get(extractor.language)!; + } else { + // if (previous document does not exists) or (extractor produces standalone documents + // and no old standalone document could be reused): create a new document + foreignDocument = this.openForeign( + extractor.language, + extractor.standalone, + extractor.fileExtension, + ); + } + return foreignDocument; + } + + /** + * Create a foreign document from input language and file extension. + * + * @param language - the required language + * @param standalone - the document type is supported natively by LSP? + * @param fileExtension - File extension. + */ + protected openForeign( + language: language, + standalone: boolean, + fileExtension: string, + ): VirtualDocument { + const document = new VirtualDocument({ + ...this.options, + parent: this, + standalone: standalone, + fileExtension: fileExtension, + language: language, + }); + const context: Document.IForeignContext = { + foreignDocument: document, + parentHost: this, + }; + this._foreignDocumentOpened.fire(context); + // pass through any future signals + document.foreignDocumentClosed(() => this.forwardClosedSignal(context)); + document.foreignDocumentOpened(() => this.forwardOpenedSignal(context)); + + this.foreignDocuments.set(document.virtualId, document); + + return document; + } + + /** + * Forward the closed signal from the foreign document to the host document's + * signal + */ + protected forwardClosedSignal(context: Document.IForeignContext) { + this._foreignDocumentClosed.fire(context); + } + + /** + * Forward the opened signal from the foreign document to the host document's + * signal + */ + protected forwardOpenedSignal(context: Document.IForeignContext) { + this._foreignDocumentOpened.fire(context); + } + + /** + * Slot of the `updateBegan` signal. + */ + protected _updateBeganSlot(): void { + this._editorToSourceLineNew = new Map(); + } + + /** + * Slot of the `blockAdded` signal. + */ + protected _blockAddedSlot(blockData: IBlockAddedInfo): void { + this._editorToSourceLineNew.set( + blockData.block.ceEditor, + blockData.virtualDocument.lastSourceLine, + ); + } + + /** + * Slot of the `updateFinished` signal. + */ + protected _updateFinishedSlot(): void { + this._editorToSourceLine = this._editorToSourceLineNew; + } + + protected _foreignDocumentClosed = new Emitter(); + protected _foreignDocumentOpened = new Emitter(); + protected _changed = new Emitter(); +} + +export namespace VirtualDocument { + /** + * Identifier composed of `virtual_id`s of a nested structure of documents, + * used to aide assignment of the connection to the virtual document + * handling specific, nested language usage; it will be appended to the file name + * when creating a connection. + */ + export type idPath = string; + /** + * Instance identifier for standalone documents (snippets), or language identifier + * for documents which should be interpreted as one when stretched across cells. + */ + export type virtualId = string; + /** + * Identifier composed of the file path and id_path. + */ + export type uri = string; +} + +/** + * Create foreign documents if available from input virtual documents. + * @param virtualDocument - the virtual document to be collected + * @return - Set of generated foreign documents + */ +export function collectDocuments( + virtualDocument: VirtualDocument, +): Set { + const collected = new Set(); + collected.add(virtualDocument); + for (const foreign of virtualDocument.foreignDocuments.values()) { + const foreignLanguages = collectDocuments(foreign); + foreignLanguages.forEach(collected.add, collected); + } + return collected; +} + +export interface IBlockAddedInfo { + /** + * The virtual document. + */ + virtualDocument: VirtualDocument; + + /** + * Option of the code block. + */ + block: Document.ICodeBlockOptions; +} + +export class UpdateManager implements Disposable { + // eslint-disable-next-line @typescript-eslint/parameter-properties, @typescript-eslint/no-parameter-properties + constructor(protected virtualDocument: VirtualDocument) { + this._blockAdded = new Emitter(); + this._documentUpdated = new Emitter(); + this._updateBegan = new Emitter(); + this._updateFinished = new Emitter(); + this.documentUpdated(this._onUpdated); + } + + /** + * Promise resolved when the updating process finishes. + */ + get updateDone(): Promise { + return this._updateDone; + } + /** + * Test whether the document is disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Signal emitted when a code block is added to the document. + */ + get blockAdded(): Event { + return this._blockAdded.event; + } + + /** + * Signal emitted by the editor that triggered the update, + * providing the root document of the updated documents. + */ + get documentUpdated(): Event { + return this._documentUpdated.event; + } + + /** + * Signal emitted when the update is started + */ + get updateBegan(): Event { + return this._updateBegan.event; + } + + /** + * Signal emitted when the update is finished + */ + get updateFinished(): Event { + return this._updateFinished.event; + } + + /** + * Dispose the class + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + } + + /** + * Execute provided callback within an update-locked context, which guarantees that: + * - the previous updates must have finished before the callback call, and + * - no update will happen when executing the callback + * @param fn - the callback to execute in update lock + */ + async withUpdateLock(fn: () => void): Promise { + await untilReady(() => this._canUpdate(), 12, 10).then(() => { + try { + this._updateLock = true; + fn(); + } finally { + this._updateLock = false; + return; + } + }); + } + + /** + * Update all the virtual documents, emit documents updated with root document if succeeded, + * and resolve a void promise. The promise does not contain the text value of the root document, + * as to avoid an easy trap of ignoring the changes in the virtual documents. + */ + async updateDocuments(blocks: Document.ICodeBlockOptions[]): Promise { + const update = new Promise((resolve, reject) => { + // defer the update by up to 50 ms (10 retrials * 5 ms break), + // awaiting for the previous update to complete. + untilReady(() => this._canUpdate(), 10, 5) + .then(() => { + if (this.isDisposed || !this.virtualDocument) { + resolve(); + } + try { + this._isUpdateInProgress = true; + this._updateBegan.fire(blocks); + + this.virtualDocument.clear(); + + for (const codeBlock of blocks) { + this._blockAdded.fire({ + block: codeBlock, + virtualDocument: this.virtualDocument, + }); + this.virtualDocument.appendCodeBlock(codeBlock); + } + + this._updateFinished.fire(blocks); + + if (this.virtualDocument) { + this._documentUpdated.fire(this.virtualDocument); + this.virtualDocument.maybeEmitChanged(); + } + + resolve(); + } catch (e) { + console.warn('Documents update failed:', e); + reject(e); + } finally { + this._isUpdateInProgress = false; + } + }) + .catch(console.error); + }); + this._updateDone = update; + return update; + } + + protected _isDisposed = false; + + /** + * Promise resolved when the updating process finishes. + */ + protected _updateDone: Promise = new Promise((resolve) => { + resolve(); + }); + + /** + * Virtual documents update guard. + */ + protected _isUpdateInProgress = false; + + /** + * Update lock to prevent multiple updates are applied at the same time. + */ + protected _updateLock = false; + + protected _blockAdded: Emitter; + protected _documentUpdated: Emitter; + protected _updateBegan: Emitter; + protected _updateFinished: Emitter; + + /** + * Once all the foreign documents were refreshed, the unused documents (and their connections) + * should be terminated if their lifetime has expired. + */ + protected _onUpdated(rootDocument: VirtualDocument) { + try { + rootDocument.closeExpiredDocuments(); + } catch (e) { + console.warn('Failed to close expired documents'); + } + } + + /** + * Check if the document can be updated. + */ + protected _canUpdate() { + return !this.isDisposed && !this._isUpdateInProgress && !this._updateLock; + } +} diff --git a/packages/libro-lsp/src/ws-connection/server-capability-registration.ts b/packages/libro-lsp/src/ws-connection/server-capability-registration.ts new file mode 100644 index 00000000..3fa70527 --- /dev/null +++ b/packages/libro-lsp/src/ws-connection/server-capability-registration.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +// Disclaimer/acknowledgement: Fragments are based on https://github.com/wylieconlon/lsp-editor-adapter, +// which is copyright of wylieconlon and contributors and ISC licenced. +// ISC licence is, quote, "functionally equivalent to the simplified BSD and MIT licenses, +// but without language deemed unnecessary following the Berne Convention." (Wikipedia). +// Introduced modifications are BSD licenced, copyright JupyterLab development team. + +import type { + Registration, + ServerCapabilities, + Unregistration, +} from 'vscode-languageserver-protocol'; + +interface IFlexibleServerCapabilities extends ServerCapabilities { + [key: string]: any; +} + +/** + * Register the capabilities with the server capabilities provider + * + * @param serverCapabilities - server capabilities provider. + * @param registration - capabilities to be registered. + * @return - the new server capabilities provider + */ +function registerServerCapability( + serverCapabilities: ServerCapabilities, + registration: Registration, +): ServerCapabilities | null { + const serverCapabilitiesCopy = JSON.parse( + JSON.stringify(serverCapabilities), + ) as IFlexibleServerCapabilities; + const { method, registerOptions } = registration; + const providerName = method.substring(13) + 'Provider'; + + if (providerName) { + if (!registerOptions) { + serverCapabilitiesCopy[providerName] = true; + } else { + serverCapabilitiesCopy[providerName] = JSON.parse( + JSON.stringify(registerOptions), + ); + } + } else { + console.warn('Could not register server capability.', registration); + return null; + } + + return serverCapabilitiesCopy; +} + +/** + * Unregister the capabilities with the server capabilities provider + * + * @param serverCapabilities - server capabilities provider. + * @param registration - capabilities to be unregistered. + * @return - the new server capabilities provider + */ +function unregisterServerCapability( + serverCapabilities: ServerCapabilities, + unregistration: Unregistration, +): ServerCapabilities { + const serverCapabilitiesCopy = JSON.parse( + JSON.stringify(serverCapabilities), + ) as IFlexibleServerCapabilities; + const { method } = unregistration; + const providerName = method.substring(13) + 'Provider'; + + delete serverCapabilitiesCopy[providerName]; + + return serverCapabilitiesCopy; +} + +export { registerServerCapability, unregisterServerCapability }; diff --git a/packages/libro-lsp/src/ws-connection/types.ts b/packages/libro-lsp/src/ws-connection/types.ts new file mode 100644 index 00000000..36ccb2d1 --- /dev/null +++ b/packages/libro-lsp/src/ws-connection/types.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +// Disclaimer/acknowledgement: Fragments are based on https://github.com/wylieconlon/lsp-editor-adapter, +// which is copyright of wylieconlon and contributors and ISC licenced. +// ISC licence is, quote, "functionally equivalent to the simplified BSD and MIT licenses, +// but without language deemed unnecessary following the Berne Convention." (Wikipedia). +// Introduced modifications are BSD licenced, copyright JupyterLab development team. + +import type * as lsp from 'vscode-languageserver-protocol'; + +export interface IDocumentInfo { + /** + * URI of the virtual document. + */ + uri: string; + + /** + * Version of the virtual document. + */ + version: number; + + /** + * Text content of the document. + */ + text: string; + + /** + * Language id of the document. + */ + languageId: string; +} + +export interface ILspConnection { + /** + * Is the language server is connected? + */ + isConnected: boolean; + /** + * Is the language server is initialized? + */ + isInitialized: boolean; + + /** + * Is the language server is connected and initialized? + */ + isReady: boolean; + + /** + * Initialize a connection over a web socket that speaks the LSP protocol + */ + connect(socket: WebSocket): void; + + /** + * Close the connection + */ + close(): void; + + // This should support every method from https://microsoft.github.io/language-server-protocol/specification + /** + * The initialize request tells the server which options the client supports + */ + sendInitialize(): void; + /** + * Inform the server that the document was opened + */ + sendOpen(documentInfo: IDocumentInfo): void; + + /** + * Sends the full text of the document to the server + */ + sendChange(documentInfo: IDocumentInfo): void; + + /** + * Send save notification to the server. + */ + sendSaved(documentInfo: IDocumentInfo): void; + + /** + * Send configuration change to the server. + */ + sendConfigurationChange(settings: lsp.DidChangeConfigurationParams): void; +} + +export interface ILspOptions { + /** + * LSP handler endpoint. + */ + serverUri: string; + + /** + * Language Id. + */ + languageId: string; + + /** + * The root URI set by server. + */ + rootUri: string; +} diff --git a/packages/libro-lsp/src/ws-connection/ws-connection.ts b/packages/libro-lsp/src/ws-connection/ws-connection.ts new file mode 100644 index 00000000..7764d157 --- /dev/null +++ b/packages/libro-lsp/src/ws-connection/ws-connection.ts @@ -0,0 +1,320 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +// Disclaimer/acknowledgement: Fragments are based on https://github.com/wylieconlon/lsp-editor-adapter, +// which is copyright of wylieconlon and contributors and ISC licenced. +// ISC licence is, quote, "functionally equivalent to the simplified BSD and MIT licenses, +// but without language deemed unnecessary following the Berne Convention." (Wikipedia). +// Introduced modifications are BSD licenced, copyright JupyterLab development team. + +import type { Disposable, Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; +import type * as protocol from 'vscode-languageserver-protocol'; +//TODO: vscode-ws-jsonrpc has new version +import type { MessageConnection } from 'vscode-ws-jsonrpc'; +import { ConsoleLogger, listen } from 'vscode-ws-jsonrpc'; + +import { + registerServerCapability, + unregisterServerCapability, +} from './server-capability-registration.js'; +import type { IDocumentInfo, ILspConnection, ILspOptions } from './types.js'; + +export class LspWsConnection implements ILspConnection, Disposable { + constructor(options: ILspOptions) { + this._rootUri = options.rootUri; + } + + /** + * Is the language server is connected? + */ + get isConnected(): boolean { + return this._isConnected; + } + + /** + * Is the language server is initialized? + */ + get isInitialized(): boolean { + return this._isInitialized; + } + + /** + * Is the language server is connected and initialized? + */ + get isReady() { + return this._isConnected && this._isInitialized; + } + + /** + * A signal emitted when the connection is disposed. + */ + get onDisposed(): Event { + return this._disposed.event; + } + + /** + * Check if the connection is disposed + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Initialize a connection over a web socket that speaks the LSP protocol + */ + connect(socket: WebSocket): void { + this.socket = socket; + listen({ + webSocket: this.socket, + logger: new ConsoleLogger(), + onConnection: (connection: MessageConnection) => { + connection.listen(); + this._isConnected = true; + + this.connection = connection; + this.sendInitialize(); + + const registerCapabilityDisposable = this.connection.onRequest( + 'client/registerCapability', + (params: protocol.RegistrationParams) => { + params.registrations.forEach( + (capabilityRegistration: protocol.Registration) => { + try { + this.serverCapabilities = registerServerCapability( + this.serverCapabilities, + capabilityRegistration, + )!; + } catch (err) { + console.error(err); + } + }, + ); + }, + ); + this._disposables.push(registerCapabilityDisposable); + + const unregisterCapabilityDisposable = this.connection.onRequest( + 'client/unregisterCapability', + (params: protocol.UnregistrationParams) => { + params.unregisterations.forEach( + (capabilityUnregistration: protocol.Unregistration) => { + this.serverCapabilities = unregisterServerCapability( + this.serverCapabilities, + capabilityUnregistration, + ); + }, + ); + }, + ); + this._disposables.push(unregisterCapabilityDisposable); + + const disposable = this.connection.onClose(() => { + this._isConnected = false; + }); + this._disposables.push(disposable); + }, + }); + } + + /** + * Close the connection + */ + close() { + if (this.connection) { + this.connection.dispose(); + } + this.openedUris.clear(); + this.socket.close(); + } + + /** + * The initialize request telling the server which options the client supports + */ + sendInitialize() { + if (!this._isConnected) { + return; + } + + this.openedUris.clear(); + + const message: protocol.InitializeParams = this.initializeParams(); + + this.connection + .sendRequest('initialize', message) + .then( + (params) => { + this.onServerInitialized(params); + return; + }, + (e) => { + console.warn('LSP websocket connection initialization failure', e); + }, + ) + .catch(console.error); + } + + /** + * Inform the server that the document was opened + */ + sendOpen(documentInfo: IDocumentInfo) { + const textDocumentMessage: protocol.DidOpenTextDocumentParams = { + textDocument: { + uri: documentInfo.uri, + languageId: documentInfo.languageId, + text: documentInfo.text, + version: documentInfo.version, + } as protocol.TextDocumentItem, + }; + this.connection + .sendNotification('textDocument/didOpen', textDocumentMessage) + .catch(console.error); + this.openedUris.set(documentInfo.uri, true); + this.sendChange(documentInfo); + } + + /** + * Sends the full text of the document to the server + */ + sendChange(documentInfo: IDocumentInfo) { + if (!this.isReady) { + return; + } + if (!this.openedUris.get(documentInfo.uri)) { + this.sendOpen(documentInfo); + return; + } + const textDocumentChange: protocol.DidChangeTextDocumentParams = { + textDocument: { + uri: documentInfo.uri, + version: documentInfo.version, + } as protocol.VersionedTextDocumentIdentifier, + contentChanges: [{ text: documentInfo.text }], + }; + this.connection + .sendNotification('textDocument/didChange', textDocumentChange) + .catch(console.error); + documentInfo.version++; + } + + /** + * Send save notification to the server. + */ + sendSaved(documentInfo: IDocumentInfo) { + if (!this.isReady) { + return; + } + + const textDocumentChange: protocol.DidSaveTextDocumentParams = { + textDocument: { + uri: documentInfo.uri, + version: documentInfo.version, + } as protocol.VersionedTextDocumentIdentifier, + text: documentInfo.text, + }; + this.connection + .sendNotification('textDocument/didSave', textDocumentChange) + .catch(console.error); + } + + /** + * Send configuration change to the server. + */ + sendConfigurationChange(settings: protocol.DidChangeConfigurationParams) { + if (!this.isReady) { + return; + } + + this.connection + .sendNotification('workspace/didChangeConfiguration', settings) + .catch(console.error); + } + + /** + * Dispose the connection. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + this._disposables.forEach((disposable) => { + disposable.dispose(); + }); + this._disposed.fire(); + } + + /** + * The internal websocket connection to the LSP handler + */ + protected socket: WebSocket; + + /** + * The json-rpc wrapper over the internal websocket connection. + */ + protected connection: MessageConnection; + + /** + * Map to track opened virtual documents.. + */ + protected openedUris = new Map(); + + /** + * Server capabilities provider. + */ + protected serverCapabilities: protocol.ServerCapabilities; + + /** + * The connection is connected? + */ + protected _isConnected = false; + + /** + * The connection is initialized? + */ + protected _isInitialized = false; + + /** + * Array of LSP callback disposables, it is used to + * clear the callbacks when the connection is disposed. + */ + protected _disposables: protocol.Disposable[] = []; + + /** + * Callback called when the server is initialized. + */ + protected onServerInitialized(params: protocol.InitializeResult): void { + this._isInitialized = true; + this.serverCapabilities = params.capabilities; + this.connection.sendNotification('initialized', {}).catch(console.error); + this.connection + .sendNotification('workspace/didChangeConfiguration', { + settings: {}, + }) + .catch(console.error); + } + + /** + * Initialization parameters to be sent to the language server. + * Subclasses should override this when adding more features. + */ + protected initializeParams(): protocol.InitializeParams { + return { + capabilities: {} as protocol.ClientCapabilities, + processId: null, + rootUri: this._rootUri, + workspaceFolders: null, + }; + } + + /** + * URI of the LSP handler enpoint. + */ + protected _rootUri: string; + + protected _disposed = new Emitter(); + + protected _isDisposed = false; +} diff --git a/packages/libro-codemirror-markdown-cell/tsconfig.json b/packages/libro-lsp/tsconfig.json similarity index 100% rename from packages/libro-codemirror-markdown-cell/tsconfig.json rename to packages/libro-lsp/tsconfig.json diff --git a/packages/libro-markdown-cell/.eslintrc.mjs b/packages/libro-markdown-cell/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-markdown-cell/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-markdown-cell/.fatherrc.ts b/packages/libro-markdown-cell/.fatherrc.ts new file mode 100644 index 00000000..a6745d8a --- /dev/null +++ b/packages/libro-markdown-cell/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types', { allowDeclareFields: true }], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-markdown-cell/CHANGELOG.md b/packages/libro-markdown-cell/CHANGELOG.md new file mode 100644 index 00000000..0e0df1a0 --- /dev/null +++ b/packages/libro-markdown-cell/CHANGELOG.md @@ -0,0 +1,18 @@ +# @difizen/libro-markdown + +## 0.1.0 + +### Minor Changes + +- 1. All modules used to support the notebook editor. + 2. Support lab products. + +### Patch Changes + +- 127cb35: Initia version + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initia version diff --git a/packages/libro-markdown-cell/README.md b/packages/libro-markdown-cell/README.md new file mode 100644 index 00000000..2c78c2af --- /dev/null +++ b/packages/libro-markdown-cell/README.md @@ -0,0 +1 @@ +# libro shared model diff --git a/packages/libro-markdown-cell/babel.config.json b/packages/libro-markdown-cell/babel.config.json new file mode 100644 index 00000000..22ce42df --- /dev/null +++ b/packages/libro-markdown-cell/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-transform-flow-strip-types", { "allowDeclareFields": true }], + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-markdown-cell/jest.config.mjs b/packages/libro-markdown-cell/jest.config.mjs new file mode 100644 index 00000000..dd07ec10 --- /dev/null +++ b/packages/libro-markdown-cell/jest.config.mjs @@ -0,0 +1,3 @@ +import configs from '../../jest.config.mjs'; + +export default { ...configs }; diff --git a/packages/libro-codemirror-markdown-cell/package.json b/packages/libro-markdown-cell/package.json similarity index 88% rename from packages/libro-codemirror-markdown-cell/package.json rename to packages/libro-markdown-cell/package.json index 5f4bd0db..3ae72716 100644 --- a/packages/libro-codemirror-markdown-cell/package.json +++ b/packages/libro-markdown-cell/package.json @@ -1,11 +1,11 @@ { - "name": "@difizen/libro-codemirror-markdown-cell", + "name": "@difizen/libro-markdown-cell", "version": "0.1.0", "description": "", "keywords": [ "libro", "notebook", - "markdown" + "toc" ], "repository": "git@github.com:difizen/libro.git", "license": "MIT", @@ -45,18 +45,20 @@ "lint:eslint": "eslint src", "lint:tsc": "tsc --noEmit" }, - "dependencies": { - "@difizen/libro-code-editor": "^0.1.0", - "@difizen/libro-codemirror": "^0.1.0", - "@difizen/libro-common": "^0.1.0", - "@difizen/libro-core": "^0.1.0", - "@difizen/libro-markdown": "^0.1.0", - "@difizen/mana-app": "latest" - }, "peerDependencies": { "react": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.25" + }, + "dependencies": { + "@difizen/libro-core": "^0.1.0", + "@difizen/libro-common": "^0.1.0", + "@difizen/libro-code-editor": "^0.1.0", + "@difizen/libro-markdown": "^0.1.0", + "@difizen/mana-app": "latest", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1", + "markdown-it-anchor": "^8.6.5" } } diff --git a/packages/libro-codemirror-markdown-cell/src/index.less b/packages/libro-markdown-cell/src/index.less similarity index 100% rename from packages/libro-codemirror-markdown-cell/src/index.less rename to packages/libro-markdown-cell/src/index.less diff --git a/packages/libro-codemirror-markdown-cell/src/index.ts b/packages/libro-markdown-cell/src/index.ts similarity index 100% rename from packages/libro-codemirror-markdown-cell/src/index.ts rename to packages/libro-markdown-cell/src/index.ts diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-cell-contribution.ts b/packages/libro-markdown-cell/src/markdown-cell-contribution.ts similarity index 100% rename from packages/libro-codemirror-markdown-cell/src/markdown-cell-contribution.ts rename to packages/libro-markdown-cell/src/markdown-cell-contribution.ts diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-cell-model.ts b/packages/libro-markdown-cell/src/markdown-cell-model.ts similarity index 88% rename from packages/libro-codemirror-markdown-cell/src/markdown-cell-model.ts rename to packages/libro-markdown-cell/src/markdown-cell-model.ts index 434e4668..047fabc9 100644 --- a/packages/libro-codemirror-markdown-cell/src/markdown-cell-model.ts +++ b/packages/libro-markdown-cell/src/markdown-cell-model.ts @@ -1,7 +1,7 @@ import type { IMarkdownCell } from '@difizen/libro-common'; import { concatMultilineString } from '@difizen/libro-common'; import type { LibroMarkdownCellModel } from '@difizen/libro-core'; -import { LibroCellModel, CellOptions } from '@difizen/libro-core'; +import { CellOptions, LibroCellModel } from '@difizen/libro-core'; import { prop } from '@difizen/mana-app'; import { inject, transient } from '@difizen/mana-app'; @@ -26,9 +26,6 @@ export class MarkdownCellModel @prop() isEdit = false; - @prop() - override value = ''; - @prop() preview = ''; @@ -36,7 +33,7 @@ export class MarkdownCellModel return { id: this.id, cell_type: 'markdown', - source: this.value, + source: this.source, metadata: this.metadata, }; } diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-cell-module.ts b/packages/libro-markdown-cell/src/markdown-cell-module.ts similarity index 100% rename from packages/libro-codemirror-markdown-cell/src/markdown-cell-module.ts rename to packages/libro-markdown-cell/src/markdown-cell-module.ts diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-cell-protocol.ts b/packages/libro-markdown-cell/src/markdown-cell-protocol.ts similarity index 100% rename from packages/libro-codemirror-markdown-cell/src/markdown-cell-protocol.ts rename to packages/libro-markdown-cell/src/markdown-cell-protocol.ts diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-cell-view.tsx b/packages/libro-markdown-cell/src/markdown-cell-view.tsx similarity index 69% rename from packages/libro-codemirror-markdown-cell/src/markdown-cell-view.tsx rename to packages/libro-markdown-cell/src/markdown-cell-view.tsx index 88e5ed92..63ef084d 100644 --- a/packages/libro-codemirror-markdown-cell/src/markdown-cell-view.tsx +++ b/packages/libro-markdown-cell/src/markdown-cell-view.tsx @@ -1,35 +1,32 @@ -import { CodeEditorView } from '@difizen/libro-code-editor'; import type { CodeEditorViewOptions, IEditor, - IRange, + CodeEditorView, } from '@difizen/libro-code-editor'; -import { codeMirrorEditorFactory } from '@difizen/libro-codemirror'; +import { CodeEditorManager } from '@difizen/libro-code-editor'; import type { CellCollapsible, CellViewOptions } from '@difizen/libro-core'; -import { CellService, LibroEditorCellView } from '@difizen/libro-core'; +import { CellService, EditorStatus, LibroEditorCellView } from '@difizen/libro-core'; import { MarkdownParser } from '@difizen/libro-markdown'; +import type { ViewSize } from '@difizen/mana-app'; import { getOrigin, prop, useInject, watch } from '@difizen/mana-app'; import { + view, + ViewInstance, ViewManager, ViewOption, ViewRender, - view, - ViewInstance, } from '@difizen/mana-app'; import { inject, transient } from '@difizen/mana-app'; import { forwardRef, useEffect } from 'react'; +import './index.less'; import type { MarkdownCellModel } from './markdown-cell-model.js'; import { MarkdownPreview } from './markdown-preview.js'; -import './index.less'; export const MarkdownCell = forwardRef( - function MarkdownCell(_props, ref) { + function MarkdownCell(props, ref) { const instance = useInject(ViewInstance); - const isEdit = - instance.editorView?.view && - instance.cellmodel.isEdit && - !instance.parent.model.readOnly; + const isEdit = instance.isEdit; useEffect(() => { if (instance.editorView?.editor) { instance.editor = getOrigin(instance.editorView?.editor); @@ -56,7 +53,7 @@ export const MarkdownCell = forwardRef( {isEdit && instance.editorView ? ( ) : ( - + )}
); @@ -76,10 +73,18 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps viewManager: ViewManager; + codeEditorManager: CodeEditorManager; + @prop() editorView?: CodeEditorView; - override editor: IEditor | undefined = undefined; + @prop() + editorAreaHeight = 0; + + @prop() + override noEditorAreaHeight = 0; + + declare editor: IEditor | undefined; @prop() headingCollapsed = false; @@ -87,6 +92,15 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps @prop() collapsibleChildNumber = 0; + @prop() + override editorStatus: EditorStatus = EditorStatus.NOTLOADED; + + get isEdit() { + return ( + this.editorView?.view && this.cellmodel.isEdit && !this.parent.model.readOnly + ); + } + get cellmodel() { return this.model as MarkdownCellModel; } @@ -97,10 +111,12 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps @inject(ViewOption) options: CellViewOptions, @inject(CellService) cellService: CellService, @inject(ViewManager) viewManager: ViewManager, + @inject(CodeEditorManager) codeEditorManager: CodeEditorManager, @inject(MarkdownParser) markdownParser: MarkdownParser, ) { super(options, cellService); this.viewManager = viewManager; + this.codeEditorManager = codeEditorManager; this.markdownParser = markdownParser; this.className = this.className + ' markdown'; } @@ -116,9 +132,18 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps }); } - async createEditor() { + override onViewResize(size: ViewSize) { + if (size.height) { + this.editorAreaHeight = size.height; + } + } + + calcEditorAreaHeight() { + return this.editorAreaHeight; + } + + protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { - factory: codeMirrorEditorFactory, model: this.model, config: { lineNumbers: false, @@ -129,21 +154,36 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps }, autoFocus: true, }; - const editorView = await this.viewManager.getOrCreateView< - CodeEditorView, - CodeEditorViewOptions - >(CodeEditorView, option); + return option; + } - this.editorView = editorView; + async createEditor() { + const option = this.getEditorOption(); + + this.editorStatus = EditorStatus.LOADING; + // 防止虚拟滚动中编辑器被频繁创建 + if (this.editorView) { + this.editorStatus = EditorStatus.LOADED; + return; + } + + const editorView = await this.codeEditorManager.getOrCreateEditorView(option); + + this.editorView = editorView; await editorView.editorReady; - editorView?.editor?.focus(); - return editorView; + this.editorStatus = EditorStatus.LOADED; + + await this.afterEditorReady(); + } + + protected async afterEditorReady() { + getOrigin(this.editorView)?.editor.focus(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - override shouldEnterEditorMode(_e: React.FocusEvent) { + override shouldEnterEditorMode(e: React.FocusEvent) { if (!this.cellmodel.isEdit) { return false; } @@ -174,15 +214,4 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps // this.editor?.trigger('undo', 'undo', {}); // this.editor?.focus(); } - - getSelections = (): IRange[] => { - return this.editor?.getSelections() ?? []; - }; - - getSelectionsOffsetAt = (selection: IRange) => { - const isSelect = selection; - const start = this.editor?.getOffsetAt(isSelect.start) ?? 0; - const end = this.editor?.getOffsetAt(isSelect.end) ?? 0; - return { start: start, end: end }; - }; } diff --git a/packages/libro-codemirror-markdown-cell/src/markdown-preview.tsx b/packages/libro-markdown-cell/src/markdown-preview.tsx similarity index 74% rename from packages/libro-codemirror-markdown-cell/src/markdown-preview.tsx rename to packages/libro-markdown-cell/src/markdown-preview.tsx index 6293e418..45a36d91 100644 --- a/packages/libro-codemirror-markdown-cell/src/markdown-preview.tsx +++ b/packages/libro-markdown-cell/src/markdown-preview.tsx @@ -1,12 +1,12 @@ -import { useInject } from '@difizen/mana-app'; -import { ViewInstance } from '@difizen/mana-app'; -import React, { useEffect, useRef, useCallback } from 'react'; +import type { FC } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; +import './index.less'; import type { MarkdownCellView } from './markdown-cell-view.js'; -export const MarkdownPreview = () => { +export const MarkdownPreview: FC<{ instance: MarkdownCellView }> = ({ instance }) => { const mktRef = useRef(null); - const instance = useInject(ViewInstance); + // const instance = useInject(ViewInstance); const enterEdit: React.MouseEventHandler = useCallback( (e) => { diff --git a/packages/libro-codemirror-raw-cell/tsconfig.json b/packages/libro-markdown-cell/tsconfig.json similarity index 100% rename from packages/libro-codemirror-raw-cell/tsconfig.json rename to packages/libro-markdown-cell/tsconfig.json diff --git a/packages/libro-markdown/src/anchor.ts b/packages/libro-markdown/src/anchor.ts index a714b3ff..ee8ddb29 100644 --- a/packages/libro-markdown/src/anchor.ts +++ b/packages/libro-markdown/src/anchor.ts @@ -23,7 +23,7 @@ export function renderHref(slug: string) { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function renderAttrs(_slug: string) { +export function renderAttrs(slug: string) { return {}; } diff --git a/packages/libro-markdown/src/index.spec.ts b/packages/libro-markdown/src/index.spec.ts deleted file mode 100644 index a3f99ee6..00000000 --- a/packages/libro-markdown/src/index.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import assert from 'assert'; - -import { MarkdownRender } from './index.js'; -import 'reflect-metadata'; - -describe('libro-markdown', () => { - it('#import', () => { - assert(MarkdownRender); - }); -}); diff --git a/packages/libro-raw-cell/.eslintrc.mjs b/packages/libro-raw-cell/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-raw-cell/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-codemirror-raw-cell/.fatherrc.ts b/packages/libro-raw-cell/.fatherrc.ts similarity index 100% rename from packages/libro-codemirror-raw-cell/.fatherrc.ts rename to packages/libro-raw-cell/.fatherrc.ts diff --git a/packages/libro-codemirror-raw-cell/CHANGELOG.md b/packages/libro-raw-cell/CHANGELOG.md similarity index 100% rename from packages/libro-codemirror-raw-cell/CHANGELOG.md rename to packages/libro-raw-cell/CHANGELOG.md diff --git a/packages/libro-raw-cell/README.md b/packages/libro-raw-cell/README.md new file mode 100644 index 00000000..555f09e7 --- /dev/null +++ b/packages/libro-raw-cell/README.md @@ -0,0 +1,3 @@ +# libro-notebook + +cell diff --git a/packages/libro-raw-cell/babel.config.json b/packages/libro-raw-cell/babel.config.json new file mode 100644 index 00000000..22ce42df --- /dev/null +++ b/packages/libro-raw-cell/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-transform-flow-strip-types", { "allowDeclareFields": true }], + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-raw-cell/jest.config.mjs b/packages/libro-raw-cell/jest.config.mjs new file mode 100644 index 00000000..dd07ec10 --- /dev/null +++ b/packages/libro-raw-cell/jest.config.mjs @@ -0,0 +1,3 @@ +import configs from '../../jest.config.mjs'; + +export default { ...configs }; diff --git a/packages/libro-codemirror-code-cell/package.json b/packages/libro-raw-cell/package.json similarity index 93% rename from packages/libro-codemirror-code-cell/package.json rename to packages/libro-raw-cell/package.json index 58a4a5a2..68df8291 100644 --- a/packages/libro-codemirror-code-cell/package.json +++ b/packages/libro-raw-cell/package.json @@ -1,5 +1,5 @@ { - "name": "@difizen/libro-codemirror-code-cell", + "name": "@difizen/libro-raw-cell", "version": "0.1.0", "description": "", "keywords": [ @@ -46,7 +46,6 @@ }, "dependencies": { "@difizen/libro-code-editor": "^0.1.0", - "@difizen/libro-codemirror": "^0.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-core": "^0.1.0", "@difizen/mana-app": "latest" diff --git a/packages/libro-codemirror-raw-cell/src/index.ts b/packages/libro-raw-cell/src/index.ts similarity index 100% rename from packages/libro-codemirror-raw-cell/src/index.ts rename to packages/libro-raw-cell/src/index.ts diff --git a/packages/libro-codemirror-raw-cell/src/module.ts b/packages/libro-raw-cell/src/module.ts similarity index 100% rename from packages/libro-codemirror-raw-cell/src/module.ts rename to packages/libro-raw-cell/src/module.ts diff --git a/packages/libro-codemirror-raw-cell/src/raw-cell-contribution.ts b/packages/libro-raw-cell/src/raw-cell-contribution.ts similarity index 100% rename from packages/libro-codemirror-raw-cell/src/raw-cell-contribution.ts rename to packages/libro-raw-cell/src/raw-cell-contribution.ts diff --git a/packages/libro-codemirror-raw-cell/src/raw-cell-model.ts b/packages/libro-raw-cell/src/raw-cell-model.ts similarity index 90% rename from packages/libro-codemirror-raw-cell/src/raw-cell-model.ts rename to packages/libro-raw-cell/src/raw-cell-model.ts index 88629421..e27d4e59 100644 --- a/packages/libro-codemirror-raw-cell/src/raw-cell-model.ts +++ b/packages/libro-raw-cell/src/raw-cell-model.ts @@ -1,8 +1,8 @@ import type { IRawCell } from '@difizen/libro-common'; -import { LibroCellModel, CellOptions } from '@difizen/libro-core'; -import { inject, transient } from '@difizen/mana-app'; -import { Emitter } from '@difizen/mana-app'; +import { CellOptions, LibroCellModel } from '@difizen/libro-core'; import type { Event as ManaEvent } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; +import { inject, transient } from '@difizen/mana-app'; @transient() export class LibroRawCellModel extends LibroCellModel { @@ -23,7 +23,7 @@ export class LibroRawCellModel extends LibroCellModel { return { id: this.id, cell_type: 'raw', - source: this.value, + source: this.source, metadata: this.metadata, }; } diff --git a/packages/libro-codemirror-raw-cell/src/raw-cell-protocol.ts b/packages/libro-raw-cell/src/raw-cell-protocol.ts similarity index 100% rename from packages/libro-codemirror-raw-cell/src/raw-cell-protocol.ts rename to packages/libro-raw-cell/src/raw-cell-protocol.ts diff --git a/packages/libro-codemirror-raw-cell/src/raw-cell-view.tsx b/packages/libro-raw-cell/src/raw-cell-view.tsx similarity index 69% rename from packages/libro-codemirror-raw-cell/src/raw-cell-view.tsx rename to packages/libro-raw-cell/src/raw-cell-view.tsx index 52c10835..360c9bf8 100644 --- a/packages/libro-codemirror-raw-cell/src/raw-cell-view.tsx +++ b/packages/libro-raw-cell/src/raw-cell-view.tsx @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-parameter-properties */ /* eslint-disable @typescript-eslint/parameter-properties */ -import type { CodeEditorViewOptions, IRange } from '@difizen/libro-code-editor'; -import { CodeEditorView } from '@difizen/libro-code-editor'; -import { codeMirrorEditorFactory } from '@difizen/libro-codemirror'; +import type { CodeEditorViewOptions, CodeEditorView } from '@difizen/libro-code-editor'; +import { CodeEditorManager } from '@difizen/libro-code-editor'; import type { CellViewOptions } from '@difizen/libro-core'; import { CellService, LibroEditorCellView } from '@difizen/libro-core'; -import { inject, transient } from '@difizen/mana-app'; +import { getOrigin, prop, useInject, watch } from '@difizen/mana-app'; +import { Deferred } from '@difizen/mana-app'; import { view, ViewInstance, @@ -13,8 +13,7 @@ import { ViewOption, ViewRender, } from '@difizen/mana-app'; -import { Deferred } from '@difizen/mana-app'; -import { getOrigin, prop, useInject, watch } from '@difizen/mana-app'; +import { inject, transient } from '@difizen/mana-app'; import React, { useEffect } from 'react'; import type { LibroRawCellModel } from './raw-cell-model.js'; @@ -32,7 +31,7 @@ const CellEditor: React.FC = () => { const CellEditorMemo = React.memo(CellEditor); const CodeEditorViewComponent = React.forwardRef( - function CodeEditorViewComponent(_props, ref) { + function CodeEditorViewComponent(props, ref) { const instance = useInject(ViewInstance); return (
@@ -50,6 +49,8 @@ export class LibroRawCellView extends LibroEditorCellView { viewManager: ViewManager; + codeEditorManager: CodeEditorManager; + @prop() editorView?: CodeEditorView; @@ -63,10 +64,12 @@ export class LibroRawCellView extends LibroEditorCellView { @inject(ViewOption) options: CellViewOptions, @inject(CellService) cellService: CellService, @inject(ViewManager) viewManager: ViewManager, + @inject(CodeEditorManager) codeEditorManager: CodeEditorManager, ) { super(options, cellService); this.viewManager = viewManager; this.className = this.className + ' raw'; + this.codeEditorManager = codeEditorManager; } override onViewMount() { @@ -77,9 +80,8 @@ export class LibroRawCellView extends LibroEditorCellView { } } - createEditor() { + protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { - factory: codeMirrorEditorFactory, model: this.model, config: { readOnly: this.parent.model.readOnly, @@ -91,23 +93,29 @@ export class LibroRawCellView extends LibroEditorCellView { autoClosingBrackets: false, }, }; - this.viewManager - .getOrCreateView(CodeEditorView, option) - .then((editorView) => { - this.editorView = editorView; - this.editorViewReadyDeferred.resolve(); - watch(this.parent.model, 'readOnly', () => { - this.editorView?.editor?.setOption('readOnly', this.parent.model.readOnly); - }); - return; - }) - .catch(() => { - // - }); + return option; + } + + async createEditor() { + const option = this.getEditorOption(); + + const editorView = await this.codeEditorManager.getOrCreateEditorView(option); + + this.editorView = editorView; + await editorView.editorReady; + await this.afterEditorReady(); + } + + protected async afterEditorReady() { + watch(this.parent.model, 'readOnly', () => { + this.editorView?.editor.setOption('readOnly', this.parent.model.readOnly); + }); } override shouldEnterEditorMode(e: React.FocusEvent) { - return getOrigin(this.editorView)?.editor?.host?.contains(e.target as HTMLElement) + return getOrigin(this.editorView)?.editor?.host?.contains( + e.target as HTMLElement, + ) && this.parent.model.commandMode ? true : false; } @@ -125,23 +133,20 @@ export class LibroRawCellView extends LibroEditorCellView { this.editorReady .then(() => { this.editorView?.editorReady.then(() => { - if (this.editorView?.editor?.hasFocus()) { + if (this.editorView?.editor.hasFocus()) { return; } - this.editorView?.editor?.focus(); + this.editorView?.editor.focus(); return; }); return; }) - .catch(() => { - // - }); + .catch(console.error); } else { - if (this.editorView?.editor?.hasFocus()) { + if (this.editorView?.editor.hasFocus()) { return; } - this.editorView?.editor?.focus(); - return; + this.editorView?.editor.focus(); } } else { if (this.container?.current?.parentElement?.contains(document.activeElement)) { @@ -150,15 +155,4 @@ export class LibroRawCellView extends LibroEditorCellView { this.container?.current?.parentElement?.focus(); } }; - - getSelections = (): [] => { - return this.editor?.getSelections() as []; - }; - - getSelectionsOffsetAt = (selection: IRange) => { - const isSelect = selection; - const start = this.editor?.getOffsetAt(isSelect.start) ?? 0; - const end = this.editor?.getOffsetAt(isSelect.end) ?? 0; - return { start: start, end: end }; - }; } diff --git a/packages/libro-search-codemirror-cell/tsconfig.json b/packages/libro-raw-cell/tsconfig.json similarity index 100% rename from packages/libro-search-codemirror-cell/tsconfig.json rename to packages/libro-raw-cell/tsconfig.json diff --git a/packages/libro-rendermime/src/rendermime-registry.ts b/packages/libro-rendermime/src/rendermime-registry.ts index 833fd0e7..f3d6dce5 100644 --- a/packages/libro-rendermime/src/rendermime-registry.ts +++ b/packages/libro-rendermime/src/rendermime-registry.ts @@ -294,8 +294,8 @@ export class RenderMimeRegistry implements IRenderMimeRegistry { this._types = null; } - private _id = 0; - private _ranks: RankMap = {}; - private _types: string[] | null = null; - private _factories: FactoryMap = {}; + protected _id = 0; + protected _ranks: RankMap = {}; + protected _types: string[] | null = null; + protected _factories: FactoryMap = {}; } diff --git a/packages/libro-search-code-cell/.eslintrc.mjs b/packages/libro-search-code-cell/.eslintrc.mjs new file mode 100644 index 00000000..ffd7daa9 --- /dev/null +++ b/packages/libro-search-code-cell/.eslintrc.mjs @@ -0,0 +1,3 @@ +module.exports = { + extends: require.resolve('../../.eslintrc.js'), +}; diff --git a/packages/libro-search-code-cell/.fatherrc.ts b/packages/libro-search-code-cell/.fatherrc.ts new file mode 100644 index 00000000..d7186780 --- /dev/null +++ b/packages/libro-search-code-cell/.fatherrc.ts @@ -0,0 +1,15 @@ +export default { + platform: 'browser', + esm: { + output: 'es', + }, + extraBabelPlugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-flow-strip-types'], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['babel-plugin-parameter-decorator'], + ], + extraBabelPresets: [['@babel/preset-typescript', { onlyRemoveTypeImports: true }]], +}; diff --git a/packages/libro-search-codemirror-cell/CHANGELOG.md b/packages/libro-search-code-cell/CHANGELOG.md similarity index 100% rename from packages/libro-search-codemirror-cell/CHANGELOG.md rename to packages/libro-search-code-cell/CHANGELOG.md diff --git a/packages/libro-search-code-cell/README.md b/packages/libro-search-code-cell/README.md new file mode 100644 index 00000000..555f09e7 --- /dev/null +++ b/packages/libro-search-code-cell/README.md @@ -0,0 +1,3 @@ +# libro-notebook + +cell diff --git a/packages/libro-search-code-cell/babel.config.json b/packages/libro-search-code-cell/babel.config.json new file mode 100644 index 00000000..51a623c5 --- /dev/null +++ b/packages/libro-search-code-cell/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + "@babel/plugin-transform-flow-strip-types", + ["@babel/plugin-transform-private-methods", { "loose": true }], + ["@babel/plugin-transform-private-property-in-object", { "loose": true }], + ["@babel/plugin-transform-class-properties", { "loose": true }], + "babel-plugin-parameter-decorator" + ] +} diff --git a/packages/libro-search-code-cell/jest.config.mjs b/packages/libro-search-code-cell/jest.config.mjs new file mode 100644 index 00000000..dd07ec10 --- /dev/null +++ b/packages/libro-search-code-cell/jest.config.mjs @@ -0,0 +1,3 @@ +import configs from '../../jest.config.mjs'; + +export default { ...configs }; diff --git a/packages/libro-search-codemirror-cell/package.json b/packages/libro-search-code-cell/package.json similarity index 90% rename from packages/libro-search-codemirror-cell/package.json rename to packages/libro-search-code-cell/package.json index 4b6b1028..834e6dd6 100644 --- a/packages/libro-search-codemirror-cell/package.json +++ b/packages/libro-search-code-cell/package.json @@ -1,5 +1,5 @@ { - "name": "@difizen/libro-search-codemirror-cell", + "name": "@difizen/libro-search-code-cell", "version": "0.1.0", "description": "", "keywords": [ @@ -45,10 +45,9 @@ "lint:tsc": "tsc --noEmit" }, "dependencies": { - "@difizen/libro-codemirror": "^0.1.0", - "@difizen/libro-codemirror-code-cell": "^0.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-code-editor": "^0.1.0", + "@difizen/libro-code-cell": "^0.1.0", "@difizen/libro-search": "^0.1.0", "@difizen/libro-core": "^0.1.0", "@difizen/mana-app": "latest", diff --git a/packages/libro-search-code-cell/src/code-cell-search-protocol.ts b/packages/libro-search-code-cell/src/code-cell-search-protocol.ts new file mode 100644 index 00000000..1fa7cd41 --- /dev/null +++ b/packages/libro-search-code-cell/src/code-cell-search-protocol.ts @@ -0,0 +1,40 @@ +import type { LibroCodeCellView } from '@difizen/libro-code-cell'; +import type { IEditor, SearchMatch } from '@difizen/libro-code-editor'; +import type { CellSearchProvider } from '@difizen/libro-search'; + +export type CodeEditorSearchHighlighterFactory = ( + editor: IEditor | undefined, +) => CodeEditorSearchHighlighter; +export const CodeEditorSearchHighlighterFactory = Symbol( + 'CodeEditorSearchHighlighterFactory', +); + +export const CodeCellSearchOption = Symbol('CodeCellSearchOption'); +export interface CodeCellSearchOption { + cell: LibroCodeCellView; +} + +export const CodeCellSearchProviderFactory = Symbol('CodeCellSearchProviderFactory'); +export type CodeCellSearchProviderFactory = ( + option: CodeCellSearchOption, +) => CellSearchProvider; + +export interface CodeEditorSearchHighlighter { + /** + * The list of matches + */ + get matches(): SearchMatch[]; + set matches(v: SearchMatch[]); + + get currentIndex(): number | undefined; + set currentIndex(v: number | undefined); + + setEditor: (editor: IEditor) => void; + + clearHighlight: () => void; + + endQuery: () => Promise; + + highlightNext: () => Promise; + highlightPrevious: () => Promise; +} diff --git a/packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider-contribution.ts b/packages/libro-search-code-cell/src/code-cell-search-provider-contribution.ts similarity index 63% rename from packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider-contribution.ts rename to packages/libro-search-code-cell/src/code-cell-search-provider-contribution.ts index 30a82256..57f50bdf 100644 --- a/packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider-contribution.ts +++ b/packages/libro-search-code-cell/src/code-cell-search-provider-contribution.ts @@ -1,19 +1,18 @@ -import type { CodeMirrorEditor } from '@difizen/libro-codemirror'; -import { LibroCodeCellView } from '@difizen/libro-codemirror-code-cell'; +import { LibroCodeCellView } from '@difizen/libro-code-cell'; import type { CellView } from '@difizen/libro-core'; import { CellSearchProviderContribution } from '@difizen/libro-search'; -import { inject, singleton } from '@difizen/mana-app'; import { ViewManager } from '@difizen/mana-app'; +import { inject, singleton } from '@difizen/mana-app'; -import { CodeMirrorCodeCellSearchProviderFactory } from './codemirror-search-protocol.js'; +import { CodeCellSearchProviderFactory } from './code-cell-search-protocol.js'; @singleton({ contrib: CellSearchProviderContribution }) -export class CodeMirrorCodeCellSearchProviderContribution +export class CodeCellSearchProviderContribution implements CellSearchProviderContribution { @inject(ViewManager) viewManager: ViewManager; - @inject(CodeMirrorCodeCellSearchProviderFactory) - providerfactory: CodeMirrorCodeCellSearchProviderFactory; + @inject(CodeCellSearchProviderFactory) + providerfactory: CodeCellSearchProviderFactory; canHandle = (cell: CellView) => { if (cell instanceof LibroCodeCellView) { return 100; @@ -31,11 +30,7 @@ export class CodeMirrorCodeCellSearchProviderContribution */ getInitialQuery = (cell: CellView): string => { if (cell instanceof LibroCodeCellView) { - const editor = cell?.editor as CodeMirrorEditor | undefined; - const selection = editor?.state.sliceDoc( - editor?.state.selection.main.from, - editor?.state.selection.main.to, - ); + const selection = cell.editor?.getSelectionValue(); // if there are newlines, just return empty string return selection?.search(/\r?\n|\r/g) === -1 ? selection : ''; } diff --git a/packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider.ts b/packages/libro-search-code-cell/src/code-cell-search-provider.ts similarity index 89% rename from packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider.ts rename to packages/libro-search-code-cell/src/code-cell-search-provider.ts index 4d277d7b..e0d28eec 100644 --- a/packages/libro-search-codemirror-cell/src/codemirror-code-cell-search-provider.ts +++ b/packages/libro-search-code-cell/src/code-cell-search-provider.ts @@ -1,21 +1,18 @@ /* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { - GenericSearchProvider, - SearchFilters, - SearchMatch, -} from '@difizen/libro-search'; +import type { SearchMatch } from '@difizen/libro-code-editor'; +import type { GenericSearchProvider, SearchFilters } from '@difizen/libro-search'; import { GenericSearchProviderFactory } from '@difizen/libro-search'; import { inject, prop, transient, watch } from '@difizen/mana-app'; -import { CodemirrorCellSearchProvider } from './codemirror-cell-search-provider.js'; import { - CodeMirrorCodeCellSearchOption, - CodeMirrorSearchHighlighterFactory, -} from './codemirror-search-protocol.js'; + CodeCellSearchOption, + CodeEditorSearchHighlighterFactory, +} from './code-cell-search-protocol.js'; +import { CodeEditorCellSearchProvider } from './code-editor-cell-search-provider.js'; @transient() -export class CodeMirrorCodeCellSearchProvider extends CodemirrorCellSearchProvider { +export class CodeCellSearchProvider extends CodeEditorCellSearchProvider { protected genericSearchProviderFactory: GenericSearchProviderFactory; @prop() protected outputsProvider: GenericSearchProvider[]; @prop() protected currentProviderIndex: number; @@ -25,11 +22,11 @@ export class CodeMirrorCodeCellSearchProvider extends CodemirrorCellSearchProvid * @param cell Cell widget */ constructor( + @inject(CodeEditorSearchHighlighterFactory) + highlighterFactory: CodeEditorSearchHighlighterFactory, @inject(GenericSearchProviderFactory) genericSearchProviderFactory: GenericSearchProviderFactory, - @inject(CodeMirrorSearchHighlighterFactory) - highlighterFactory: CodeMirrorSearchHighlighterFactory, - @inject(CodeMirrorCodeCellSearchOption) option: CodeMirrorCodeCellSearchOption, + @inject(CodeCellSearchOption) option: CodeCellSearchOption, ) { super(highlighterFactory, option.cell); this.genericSearchProviderFactory = genericSearchProviderFactory; @@ -105,7 +102,7 @@ export class CodeMirrorCodeCellSearchProvider extends CodemirrorCellSearchProvid if (this.currentProviderIndex === -1) { const match = await super.highlightNext(); if (match) { - this.currentIndex = this.cmHandler.currentIndex; + this.currentIndex = this.editorHighlighter.currentIndex; return match; } else { this.currentProviderIndex = 0; @@ -167,7 +164,7 @@ export class CodeMirrorCodeCellSearchProvider extends CodemirrorCellSearchProvid const match = await super.highlightPrevious(); if (match) { - this.currentIndex = this.cmHandler.currentIndex; + this.currentIndex = this.editorHighlighter.currentIndex; return match; } else { this.currentIndex = undefined; diff --git a/packages/libro-search-codemirror-cell/src/codemirror-cell-search-provider.ts b/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts similarity index 70% rename from packages/libro-search-codemirror-cell/src/codemirror-cell-search-provider.ts rename to packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts index feca1e1e..a3afb268 100644 --- a/packages/libro-search-codemirror-cell/src/codemirror-cell-search-provider.ts +++ b/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts @@ -1,29 +1,25 @@ -import type { IPosition } from '@difizen/libro-code-editor'; -import type { CodeMirrorEditor } from '@difizen/libro-codemirror'; -import type { LibroCodeCellView } from '@difizen/libro-codemirror-code-cell'; -import type { - BaseSearchProvider, - SearchFilters, - SearchMatch, -} from '@difizen/libro-search'; +import type { LibroCodeCellView } from '@difizen/libro-code-cell'; +import type { IPosition, SearchMatch } from '@difizen/libro-code-editor'; +import type { BaseSearchProvider, SearchFilters } from '@difizen/libro-search'; import { searchText } from '@difizen/libro-search'; import type { Event } from '@difizen/mana-app'; -import { inject, transient } from '@difizen/mana-app'; +import { prop } from '@difizen/mana-app'; import { DisposableCollection, Emitter } from '@difizen/mana-app'; -import { prop, watch } from '@difizen/mana-app'; +import { watch } from '@difizen/mana-app'; +import { inject, transient } from '@difizen/mana-app'; -import type { CodeMirrorSearchHighlighter } from './codemirror-search-highlighter.js'; -import { CodeMirrorSearchHighlighterFactory } from './codemirror-search-protocol.js'; +import type { CodeEditorSearchHighlighter } from './code-cell-search-protocol.js'; +import { CodeEditorSearchHighlighterFactory } from './code-cell-search-protocol.js'; /** * Search provider for cells. */ @transient() -export class CodemirrorCellSearchProvider implements BaseSearchProvider { +export class CodeEditorCellSearchProvider implements BaseSearchProvider { protected toDispose = new DisposableCollection(); /** * CodeMirror search highlighter */ - @prop() protected cmHandler: CodeMirrorSearchHighlighter; + @prop() protected editorHighlighter: CodeEditorSearchHighlighter; /** * Current match index */ @@ -41,7 +37,7 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { protected _isActive = true; protected _isDisposed = false; protected lastReplacementPosition: IPosition | null = null; - protected highlighterFactory: CodeMirrorSearchHighlighterFactory; + protected highlighterFactory: CodeEditorSearchHighlighterFactory; protected cell: LibroCodeCellView; /** * Constructor @@ -49,8 +45,8 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { * @param cell Cell widget */ constructor( - @inject(CodeMirrorSearchHighlighterFactory) - highlighterFactory: CodeMirrorSearchHighlighterFactory, + @inject(CodeEditorSearchHighlighterFactory) + highlighterFactory: CodeEditorSearchHighlighterFactory, cell: LibroCodeCellView, ) { this.cell = cell; @@ -58,11 +54,9 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { this.currentIndex = undefined; this.stateChangedEmitter = new Emitter(); this.toDispose.push(this.stateChangedEmitter); - this.cmHandler = this.highlighterFactory( - this.cell.editor as CodeMirrorEditor | undefined, - ); + this.editorHighlighter = this.highlighterFactory(this.cell.editor); - this.toDispose.push(watch(this.cell.model, 'value', this.updateCodeMirror)); + this.toDispose.push(watch(this.cell.model, 'value', this.updateMatches)); this.toDispose.push( watch(this.cell, 'editor', async () => { await this.cell.editorReady; @@ -82,21 +76,19 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { * @returns Initial value used to populate the search box. */ getInitialQuery(): string { - const editor = this.cell?.editor as CodeMirrorEditor | undefined; - const selection = editor?.state.sliceDoc( - editor?.state.selection.main.from, - editor?.state.selection.main.to, - ); + const selection = this.cell?.editor?.getSelectionValue(); // if there are newlines, just return empty string return selection?.search(/\r?\n|\r/g) === -1 ? selection : ''; } - disposed?: boolean | undefined; + get disposed() { + return this._isDisposed; + } protected async setEditor() { await this.cell.editorReady; if (this.cell.editor) { - this.cmHandler.setEditor(this.cell.editor as CodeMirrorEditor); + this.editorHighlighter.setEditor(this.cell.editor); } } @@ -134,7 +126,11 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { * Number of matches in the cell. */ get matchesCount(): number { - return this.isActive ? this.cmHandler.matches.length : 0; + return this.isActive ? this.editorHighlighter.matches.length : 0; + } + + get isCellSelected(): boolean { + return this.cell.parent.isSelected(this.cell); } /** @@ -142,7 +138,7 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { */ clearHighlight(): Promise { this.currentIndex = undefined; - this.cmHandler.clearHighlight(); + this.editorHighlighter.clearHighlight(); return Promise.resolve(); } @@ -199,7 +195,7 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { } await this.setEditor(); // Search input - await this.updateCodeMirror(); + await this.updateMatches(); } /** @@ -208,7 +204,7 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { async endQuery(): Promise { this.currentIndex = undefined; this.query = null; - await this.cmHandler.endQuery(); + await this.editorHighlighter.endQuery(); } /** @@ -226,9 +222,10 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { } // This starts from the cursor position - const match = await this.cmHandler.highlightNext(); + const match = await this.editorHighlighter.highlightNext(); + if (match) { - this.currentIndex = this.cmHandler.currentIndex; + this.currentIndex = this.editorHighlighter.currentIndex; } else { this.currentIndex = undefined; } @@ -248,9 +245,9 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { this.currentIndex = undefined; } else { // This starts from the cursor position - const match = await this.cmHandler.highlightPrevious(); + const match = await this.editorHighlighter.highlightPrevious(); if (match) { - this.currentIndex = this.cmHandler.currentIndex; + this.currentIndex = this.editorHighlighter.currentIndex; } else { this.currentIndex = undefined; } @@ -277,29 +274,26 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { if ( this.currentIndex !== undefined && - this.currentIndex < this.cmHandler.matches.length + this.currentIndex < this.editorHighlighter.matches.length ) { - const editor = this.cell.editor as CodeMirrorEditor; - const selection = editor.state.sliceDoc( - editor.state.selection.main.from, - editor.state.selection.main.to, - ); + const editor = this.cell.editor; + const selection = editor?.getSelectionValue(); const match = this.getCurrentMatch(); + if (!match) { + return Promise.resolve(occurred); + } // If cursor is not on a selection, highlight the next match if (selection !== match?.text) { this.currentIndex = undefined; // The next will be highlighted as a consequence of this returning false } else { - this.cmHandler.matches.splice(this.currentIndex, 1); + this.editorHighlighter.matches.splice(this.currentIndex, 1); this.currentIndex = undefined; // Store the current position to highlight properly the next search hit - this.lastReplacementPosition = editor.getCursorPosition(); - editor.editor.dispatch({ - changes: { - from: match.position, - to: match.position + match.text.length, - insert: newText, - }, + this.lastReplacementPosition = editor?.getCursorPosition() ?? null; + editor?.replaceSelection(newText, { + start: editor.getPositionAt(match.position)!, + end: editor.getPositionAt(match.position + match.text.length)!, }); occurred = true; } @@ -318,7 +312,7 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { return Promise.resolve(false); } - const occurred = this.cmHandler.matches.length > 0; + const occurred = this.editorHighlighter.matches.length > 0; // const src = this.cell.model.value; // let lastEnd = 0; // const finalSrc = this.cmHandler.matches.reduce((agg, match) => { @@ -329,17 +323,17 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { // return newStep; // }, ''); - if (occurred) { - const editor = this.cell.editor as CodeMirrorEditor; - const changes = this.cmHandler.matches.map((match) => ({ - from: match.position, - to: match.position + match.text.length, - insert: newText, + const editor = this.cell.editor; + if (occurred && editor) { + const changes = this.editorHighlighter.matches.map((match) => ({ + range: { + start: editor.getPositionAt(match.position)!, + end: editor.getPositionAt(match.position + match.text.length)!, + }, + text: newText, })); - editor.editor.dispatch({ - changes, - }); - this.cmHandler.matches = []; + editor?.replaceSelections(changes); + this.editorHighlighter.matches = []; this.currentIndex = undefined; // this.cell.model.setSource(`${finalSrc}${src.slice(lastEnd)}`); } @@ -356,19 +350,27 @@ export class CodemirrorCellSearchProvider implements BaseSearchProvider { return undefined; } else { let match: SearchMatch | undefined = undefined; - if (this.currentIndex < this.cmHandler.matches.length) { - match = this.cmHandler.matches[this.currentIndex]; + if (this.currentIndex < this.editorHighlighter.matches.length) { + match = this.editorHighlighter.matches[this.currentIndex]; } return match; } } - protected updateCodeMirror = async () => { + protected updateMatches = async () => { if (this.query !== null) { if (this.isActive) { - this.cmHandler.matches = await searchText(this.query, this.cell.model.value); + const matches = await searchText(this.query, this.cell.model.value); + this.editorHighlighter.matches = matches; + if (this.isCellSelected) { + const cursorOffset = this.cell.editor!.getOffsetAt( + this.cell.editor?.getCursorPosition() ?? { column: 0, line: 0 }, + ); + const index = matches.findIndex((item) => item.position >= cursorOffset); + this.currentIndex = index; + } } else { - this.cmHandler.matches = []; + this.editorHighlighter.matches = []; } } }; diff --git a/packages/libro-search-code-cell/src/index.ts b/packages/libro-search-code-cell/src/index.ts new file mode 100644 index 00000000..88978d9a --- /dev/null +++ b/packages/libro-search-code-cell/src/index.ts @@ -0,0 +1,4 @@ +export * from './code-cell-search-protocol.js'; +export * from './code-cell-search-provider.js'; +export * from './code-editor-cell-search-provider.js'; +export * from './module.js'; diff --git a/packages/libro-search-code-cell/src/module.ts b/packages/libro-search-code-cell/src/module.ts new file mode 100644 index 00000000..8fdab6b0 --- /dev/null +++ b/packages/libro-search-code-cell/src/module.ts @@ -0,0 +1,46 @@ +import type { IEditor } from '@difizen/libro-code-editor'; +import { LibroSearchModule } from '@difizen/libro-search'; +import { ManaModule } from '@difizen/mana-app'; + +import { + CodeCellSearchOption, + CodeCellSearchProviderFactory, + CodeEditorSearchHighlighterFactory, +} from './code-cell-search-protocol.js'; +import { CodeCellSearchProviderContribution } from './code-cell-search-provider-contribution.js'; +import { CodeCellSearchProvider } from './code-cell-search-provider.js'; +import { GenericSearchHighlighter } from './search-highlighter.js'; + +export const SearchCodeCellModule = ManaModule.create() + .register( + CodeCellSearchProvider, + CodeCellSearchProviderContribution, + CodeCellSearchProviderFactory, + GenericSearchHighlighter, + { + token: CodeEditorSearchHighlighterFactory, + useFactory: (ctx) => { + return (editor: IEditor) => { + const child = ctx.container.createChild(); + const highlighter = child.get(GenericSearchHighlighter); + highlighter.setEditor(editor); + return highlighter; + }; + }, + }, + { + token: CodeCellSearchProviderFactory, + useFactory: (ctx) => { + return (options: CodeCellSearchOption) => { + const child = ctx.container.createChild(); + child.register({ + token: CodeCellSearchOption, + useValue: options, + }); + const model = child.get(CodeCellSearchProvider); + return model; + }; + }, + }, + ) + .dependOn(LibroSearchModule); diff --git a/packages/libro-search-codemirror-cell/src/codemirror-search-highlighter.ts b/packages/libro-search-code-cell/src/search-highlighter.ts similarity index 51% rename from packages/libro-search-codemirror-cell/src/codemirror-search-highlighter.ts rename to packages/libro-search-code-cell/src/search-highlighter.ts index fda63e7b..46c69b9e 100644 --- a/packages/libro-search-codemirror-cell/src/codemirror-search-highlighter.ts +++ b/packages/libro-search-code-cell/src/search-highlighter.ts @@ -1,14 +1,12 @@ /* eslint-disable no-param-reassign */ -import type { StateEffectType } from '@codemirror/state'; -import { StateEffect, StateField } from '@codemirror/state'; -import type { DecorationSet } from '@codemirror/view'; -import { Decoration, EditorView } from '@codemirror/view'; -import type { CodeMirrorEditor } from '@difizen/libro-codemirror'; +import type { IEditor, SearchMatch } from '@difizen/libro-code-editor'; import { deepEqual } from '@difizen/libro-common'; -import type { SearchMatch } from '@difizen/libro-search'; import { LibroSearchUtils } from '@difizen/libro-search'; -import { inject, transient } from '@difizen/mana-app'; import { prop } from '@difizen/mana-app'; +import { inject, transient } from '@difizen/mana-app'; + +import type { CodeEditorSearchHighlighter } from './code-cell-search-protocol.js'; + /** * Helper class to highlight texts in a code mirror editor. * @@ -16,18 +14,12 @@ import { prop } from '@difizen/mana-app'; * the `matches` attributes. */ @transient() -export class CodeMirrorSearchHighlighter { - utils: LibroSearchUtils; - protected cm: CodeMirrorEditor | undefined; +export class GenericSearchHighlighter implements CodeEditorSearchHighlighter { + @inject(LibroSearchUtils) utils: LibroSearchUtils; + + protected editor: IEditor | undefined; @prop() _currentIndex: number | undefined; @prop() protected _matches: SearchMatch[]; - protected highlightEffect: StateEffectType<{ - matches: SearchMatch[]; - currentIndex: number | undefined; - }>; - protected highlightMark: Decoration; - protected selectedMatchMark: Decoration; - protected highlightField: StateField; /** * The list of matches @@ -55,70 +47,9 @@ export class CodeMirrorSearchHighlighter { * * @param editor The CodeMirror editor */ - constructor(@inject(LibroSearchUtils) utils: LibroSearchUtils) { - this.utils = utils; + constructor() { this._matches = new Array(); this.currentIndex = undefined; - - this.highlightEffect = StateEffect.define<{ - matches: SearchMatch[]; - currentIndex: number | undefined; - }>({ - map: (value, mapping) => ({ - matches: value.matches.map((v) => ({ - text: v.text, - position: mapping.mapPos(v.position), - })), - currentIndex: value.currentIndex, - }), - }); - this.highlightMark = Decoration.mark({ class: 'cm-searchMatch' }); - this.selectedMatchMark = Decoration.mark({ - class: 'cm-searchMatch cm-searchMatch-selected libro-selectedtext', - }); - this.highlightField = StateField.define({ - create: () => { - return Decoration.none; - }, - update: (highlights, transaction) => { - highlights = highlights.map(transaction.changes); - for (const ef of transaction.effects) { - if (ef.is(this.highlightEffect)) { - const e = ef as StateEffect<{ - matches: SearchMatch[]; - currentIndex: number | undefined; - }>; - if (e.value.matches.length) { - highlights = highlights.update({ - add: e.value.matches.map((m, index) => { - if (index === e.value.currentIndex) { - return this.selectedMatchMark.range( - m.position, - m.position + m.text.length, - ); - } - return this.highlightMark.range( - m.position, - m.position + m.text.length, - ); - }), - filter: (from, to) => { - return ( - !e.value.matches.some( - (m) => m.position >= from && m.position + m.text.length <= to, - ) || from === to - ); - }, - }); - } else { - highlights = Decoration.none; - } - } - } - return highlights; - }, - provide: (f) => EditorView.decorations.from(f), - }); } /** @@ -136,18 +67,18 @@ export class CodeMirrorSearchHighlighter { this._currentIndex = undefined; this._matches = []; - if (this.cm) { - this.cm.editor.dispatch({ - effects: this.highlightEffect.of({ matches: [], currentIndex: undefined }), - }); + if (this.editor) { + this.editor.highlightMatches([], undefined); + + const selection = this.editor.getSelection(); + + const start = this.editor.getOffsetAt(selection.start); + const end = this.editor.getOffsetAt(selection.end); - const selection = this.cm.state.selection.main; - const from = selection.from; - const to = selection.to; // Setting a reverse selection to allow search-as-you-type to maintain the // current selected match. See comment in _findNext for more details. - if (from !== to) { - this.cm.editor.dispatch({ selection: { anchor: to, head: from } }); + if (start !== end) { + this.editor.setSelection(selection); } } @@ -185,8 +116,8 @@ export class CodeMirrorSearchHighlighter { * * @param editor Editor */ - setEditor(editor: CodeMirrorEditor): void { - this.cm = editor; + setEditor(editor: IEditor): void { + this.editor = editor; this.refresh(); if (this.currentIndex !== undefined) { this._highlightCurrentMatch(); @@ -194,7 +125,7 @@ export class CodeMirrorSearchHighlighter { } protected _highlightCurrentMatch(): void { - if (!this.cm) { + if (!this.editor) { // no-op return; } @@ -203,34 +134,26 @@ export class CodeMirrorSearchHighlighter { if (this.currentIndex !== undefined) { const match = this.matches[this.currentIndex]; // this.cm.editor.focus(); - this.cm.editor.dispatch({ - selection: { - anchor: match.position, - head: match.position + match.text.length, - }, - scrollIntoView: true, - }); + const start = this.editor.getPositionAt(match.position); + const end = this.editor.getPositionAt(match.position + match.text.length); + if (start && end) { + this.editor.setSelection({ start, end }); + this.editor.revealSelection({ start, end }); + } } else { + const start = this.editor.getPositionAt(0)!; + const end = this.editor.getPositionAt(0)!; // Set cursor to remove any selection - this.cm.editor.dispatch({ selection: { anchor: 0 } }); + this.editor.setSelection({ start, end }); } } protected refresh(): void { - if (!this.cm) { + if (!this.editor) { // no-op return; } - const effects: StateEffect[] = [ - this.highlightEffect.of({ - matches: this.matches, - currentIndex: this.currentIndex, - }), - ]; - if (!this.cm.state.field(this.highlightField, false)) { - effects.push(StateEffect.appendConfig.of([this.highlightField])); - } - this.cm.editor.dispatch({ effects }); + this.editor.highlightMatches(this.matches, this.currentIndex); } protected _findNext(reverse: boolean): number | undefined { @@ -238,6 +161,9 @@ export class CodeMirrorSearchHighlighter { // No-op return undefined; } + if (!this.editor) { + return; + } // In order to support search-as-you-type, we needed a way to allow the first // match to be selected when a search is started, but prevent the selected // search to move for each new keypress. To do this, when a search is ended, @@ -250,12 +176,14 @@ export class CodeMirrorSearchHighlighter { // the search proceeds from the 'to' position during normal toggling. If reverse = true, // the search always proceeds from the 'anchor' position, which is at the 'from'. - const cursor = this.cm!.state.selection.main; + const selection = this.editor?.getSelection(); - let lastPosition = reverse ? cursor.anchor : cursor.head; + const start = this.editor?.getOffsetAt(selection.start); + const end = this.editor.getOffsetAt(selection.end); + let lastPosition = reverse ? start : end; if (lastPosition === 0 && reverse && this.currentIndex === undefined) { // The default position is (0, 0) but we want to start from the end in that case - lastPosition = this.cm!.doc.length; + lastPosition = this.editor.model.value.length; } const position = lastPosition; diff --git a/packages/libro-search-code-cell/tsconfig.json b/packages/libro-search-code-cell/tsconfig.json new file mode 100644 index 00000000..c9da781c --- /dev/null +++ b/packages/libro-search-code-cell/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "es", + "declarationDir": "es" + }, + "types": ["jest"], + "exclude": ["node_modules"], + "include": ["src"] +} diff --git a/packages/libro-search-codemirror-cell/src/codemirror-search-protocol.ts b/packages/libro-search-codemirror-cell/src/codemirror-search-protocol.ts deleted file mode 100644 index 348dd0b4..00000000 --- a/packages/libro-search-codemirror-cell/src/codemirror-search-protocol.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CodeMirrorEditor } from '@difizen/libro-codemirror'; -import type { LibroCodeCellView } from '@difizen/libro-codemirror-code-cell'; - -import type { CodeMirrorCodeCellSearchProvider } from './codemirror-code-cell-search-provider.js'; -import type { CodeMirrorSearchHighlighter } from './codemirror-search-highlighter.js'; - -export type CodeMirrorSearchHighlighterFactory = ( - editor: CodeMirrorEditor | undefined, -) => CodeMirrorSearchHighlighter; -export const CodeMirrorSearchHighlighterFactory = Symbol( - 'CodeMirrorSearchHighlighterFactory', -); - -export const CodeMirrorCodeCellSearchOption = Symbol('CodeMirrorCodeCellSearchOption'); -export interface CodeMirrorCodeCellSearchOption { - cell: LibroCodeCellView; -} - -export const CodeMirrorCodeCellSearchProviderFactory = Symbol( - 'CodeMirrorCodeCellSearchProviderFactory', -); -export type CodeMirrorCodeCellSearchProviderFactory = ( - option: CodeMirrorCodeCellSearchOption, -) => CodeMirrorCodeCellSearchProvider; diff --git a/packages/libro-search-codemirror-cell/src/index.spec.ts b/packages/libro-search-codemirror-cell/src/index.spec.ts deleted file mode 100644 index aeca0481..00000000 --- a/packages/libro-search-codemirror-cell/src/index.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import assert from 'assert'; - -import { - CodemirrorCellSearchProvider, - CodeMirrorCodeCellSearchProvider, -} from './index.js'; -import 'reflect-metadata'; - -describe('libro-search-codemirror-cell', () => { - it('#import', () => { - assert(CodemirrorCellSearchProvider); - assert(CodeMirrorCodeCellSearchProvider); - }); -}); diff --git a/packages/libro-search-codemirror-cell/src/index.ts b/packages/libro-search-codemirror-cell/src/index.ts deleted file mode 100644 index 0d531432..00000000 --- a/packages/libro-search-codemirror-cell/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './module.js'; -export * from './codemirror-search-protocol.js'; -export * from './codemirror-cell-search-provider.js'; -export * from './codemirror-code-cell-search-provider.js'; -export * from './codemirror-search-highlighter.js'; diff --git a/packages/libro-search-codemirror-cell/src/module.ts b/packages/libro-search-codemirror-cell/src/module.ts deleted file mode 100644 index f8103583..00000000 --- a/packages/libro-search-codemirror-cell/src/module.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CodeMirrorEditor } from '@difizen/libro-codemirror'; -import { LibroSearchModule } from '@difizen/libro-search'; -import { ManaModule } from '@difizen/mana-app'; - -import { CodeMirrorCodeCellSearchProviderContribution } from './codemirror-code-cell-search-provider-contribution.js'; -import { CodeMirrorCodeCellSearchProvider } from './codemirror-code-cell-search-provider.js'; -import { CodeMirrorSearchHighlighter } from './codemirror-search-highlighter.js'; -import { - CodeMirrorCodeCellSearchOption, - CodeMirrorCodeCellSearchProviderFactory, - CodeMirrorSearchHighlighterFactory, -} from './codemirror-search-protocol.js'; - -export const SearchCodemirrorCellModule = ManaModule.create() - .register( - CodeMirrorCodeCellSearchProvider, - CodeMirrorSearchHighlighter, - CodeMirrorCodeCellSearchProviderContribution, - { - token: CodeMirrorSearchHighlighterFactory, - useFactory: (ctx) => { - return (editor: CodeMirrorEditor) => { - const child = ctx.container.createChild(); - const highlighter = child.get(CodeMirrorSearchHighlighter); - highlighter.setEditor(editor); - return highlighter; - }; - }, - }, - { - token: CodeMirrorCodeCellSearchProviderFactory, - useFactory: (ctx) => { - return (options: CodeMirrorCodeCellSearchOption) => { - const child = ctx.container.createChild(); - child.register({ - token: CodeMirrorCodeCellSearchOption, - useValue: options, - }); - const model = child.get(CodeMirrorCodeCellSearchProvider); - return model; - }; - }, - }, - ) - .dependOn(LibroSearchModule); diff --git a/packages/libro-search/package.json b/packages/libro-search/package.json index 9bb4aabd..8e57ec63 100644 --- a/packages/libro-search/package.json +++ b/packages/libro-search/package.json @@ -51,6 +51,7 @@ "@ant-design/icons": "^5.1.0", "@difizen/libro-common": "^0.1.0", "@difizen/libro-core": "^0.1.0", + "@difizen/libro-code-editor": "^0.1.0", "@types/lodash.debounce": "^4.0.7", "classnames": "^2.3.2", "lodash.debounce": "^4.0.8", diff --git a/packages/libro-search/src/abstract-search-provider.ts b/packages/libro-search/src/abstract-search-provider.ts index 21e06c64..2ddbd844 100644 --- a/packages/libro-search/src/abstract-search-provider.ts +++ b/packages/libro-search/src/abstract-search-provider.ts @@ -1,3 +1,4 @@ +import type { SearchMatch } from '@difizen/libro-code-editor'; import type { Event } from '@difizen/mana-app'; import type { View } from '@difizen/mana-app'; import { Emitter } from '@difizen/mana-app'; @@ -6,10 +7,8 @@ import { transient } from '@difizen/mana-app'; import type { SearchFilter, SearchFilters, - SearchMatch, SearchProvider, } from './libro-search-protocol.js'; - /** * Abstract class implementing the search provider interface. */ diff --git a/packages/libro-search/src/index.less b/packages/libro-search/src/index.less index 0276899e..96b1ea03 100644 --- a/packages/libro-search/src/index.less +++ b/packages/libro-search/src/index.less @@ -2,19 +2,21 @@ position: absolute; top: 0; right: 0; - box-shadow: 0 2px 2px 0 #7c68681a; - background-color: var(--mana-color-bg-elevated); z-index: 2000; + background-color: var(--mana-color-bg-elevated); + box-shadow: 0 2px 2px 0 #7c68681a; } .libro-search-content { display: flex; align-items: center; - padding: 2px 6px; min-width: 320px; + padding: 2px 6px; } .libro-search-row { + display: flex; + align-items: center; height: 32px; input { @@ -22,18 +24,18 @@ } .ant-btn { + margin-left: 4px; border: none; box-shadow: none; - margin-left: 4px; } } .libro-search-replace-toggle { - padding: 4px; display: flex; align-items: center; height: 100%; margin-right: 4px; + padding: 4px; cursor: pointer; &:hover { @@ -42,8 +44,8 @@ } .libro-search-input { - align-items: center; flex: 1; + align-items: center; .ant-input-affix-wrapper-sm { margin-right: 4px; @@ -67,12 +69,12 @@ } .libro-search-index { - // width: 24px; display: flex; align-items: center; justify-content: center; - margin-left: 4px; + width: 50px; margin-right: 16px; + margin-left: 4px; } .libro-search-action { diff --git a/packages/libro-search/src/libro-search-engine-text.ts b/packages/libro-search/src/libro-search-engine-text.ts index 8f4f6c7a..d523b427 100644 --- a/packages/libro-search/src/libro-search-engine-text.ts +++ b/packages/libro-search/src/libro-search-engine-text.ts @@ -1,4 +1,4 @@ -import type { SearchMatch } from './libro-search-protocol.js'; +import type { SearchMatch } from '@difizen/libro-code-editor'; /** * Search for regular expression matches in a string. diff --git a/packages/libro-search/src/libro-search-generic-provider.ts b/packages/libro-search/src/libro-search-generic-provider.ts index 7f1b1ea5..22c1e84b 100644 --- a/packages/libro-search/src/libro-search-generic-provider.ts +++ b/packages/libro-search/src/libro-search-generic-provider.ts @@ -143,7 +143,7 @@ export class GenericSearchProvider extends AbstractSearchProvider { * * @returns A promise that resolves with a boolean indicating whether a replace occurred. */ - async replaceCurrentMatch(_newText: string, _loop?: boolean): Promise { + async replaceCurrentMatch(newText: string, loop?: boolean): Promise { return Promise.resolve(false); } @@ -154,7 +154,7 @@ export class GenericSearchProvider extends AbstractSearchProvider { * * @returns A promise that resolves with a boolean indicating whether a replace occurred. */ - async replaceAllMatches(_newText: string): Promise { + async replaceAllMatches(newText: string): Promise { // This is read only, but we could loosen this in theory for input boxes... return Promise.resolve(false); } @@ -166,7 +166,7 @@ export class GenericSearchProvider extends AbstractSearchProvider { * @param query A RegExp to be use to perform the search * @param filters Filter parameters to pass to provider */ - startQuery = async (query: RegExp | null, _filters = {}): Promise => { + startQuery = async (query: RegExp | null, filters = {}): Promise => { await this.endQuery(); this._query = query; @@ -281,8 +281,8 @@ export class GenericSearchProvider extends AbstractSearchProvider { } protected async _onWidgetChanged( - _mutations: MutationRecord[], - _observer: MutationObserver, + mutations: MutationRecord[], + observer: MutationObserver, ) { this._currentMatchIndex = -1; // This is typically cheap, but we do not control the rate of change or size of the output @@ -290,7 +290,7 @@ export class GenericSearchProvider extends AbstractSearchProvider { this._stateChanged.fire(); } } -function elementInViewport(el: HTMLElement): boolean { +export function elementInViewport(el: HTMLElement): boolean { const boundingClientRect = el.getBoundingClientRect(); return ( boundingClientRect.top >= 0 && diff --git a/packages/libro-search/src/libro-search-manager.ts b/packages/libro-search/src/libro-search-manager.ts index 1374de44..fda7509e 100644 --- a/packages/libro-search/src/libro-search-manager.ts +++ b/packages/libro-search/src/libro-search-manager.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import type { CommandRegistry, KeybindingRegistry } from '@difizen/mana-app'; import { LibroCommandRegister, LibroExtensionSlotContribution, @@ -8,13 +9,13 @@ import type { LibroSlot, LibroView, } from '@difizen/libro-core'; -import type { CommandRegistry, KeybindingRegistry } from '@difizen/mana-app'; import { ViewManager, CommandContribution, KeybindingContribution, + inject, + singleton, } from '@difizen/mana-app'; -import { inject, singleton } from '@difizen/mana-app'; import { LibroSearchView } from './libro-search-view.js'; @@ -60,7 +61,7 @@ export class LibroSearchManager commands, LibroSearchToggleCommand.ShowLibroSearch, { - execute: (_cell, libro, _position) => { + execute: (cell, libro, position) => { if (libro) { this.showSearchView(libro); } diff --git a/packages/libro-search/src/libro-search-model.ts b/packages/libro-search/src/libro-search-model.ts index 3b8a0178..54a71382 100644 --- a/packages/libro-search/src/libro-search-model.ts +++ b/packages/libro-search/src/libro-search-model.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + import type { Disposable } from '@difizen/mana-app'; import { DisposableCollection, Emitter } from '@difizen/mana-app'; import { inject, singleton } from '@difizen/mana-app'; @@ -12,15 +16,15 @@ import { LibroSearchUtils } from './libro-search-utils.js'; */ @singleton() export class LibroSearchModel implements Disposable { - utils: LibroSearchUtils; - protected _disposed?: boolean = false; + @inject(LibroSearchUtils) utils: LibroSearchUtils; + protected _disposed?: boolean | undefined = false; protected _caseSensitive = false; protected parsingError = ''; protected _filters: SearchFilters = { searchCellOutput: true, onlySearchSelectedCells: false, }; - protected _replaceText = ''; + protected _replaceText: string; protected searchDebouncer: any; protected _searchExpression = ''; protected _useRegex = false; @@ -32,19 +36,14 @@ export class LibroSearchModel implements Disposable { } get disposed() { - return !!this._disposed; + return this._disposed; } /** * Search document model * @param searchProvider Provider for the current document * @param searchDebounceTime Debounce search time */ - constructor( - @inject(LibroSearchUtils) utils: LibroSearchUtils, - searchProvider: SearchProvider, - searchDebounceTime: number, - ) { - this.utils = utils; + constructor(searchProvider: SearchProvider, searchDebounceTime: number) { this.searchProvider = searchProvider; // this._filters = {}; // if (this.searchProvider.getFilters) { @@ -227,7 +226,7 @@ export class LibroSearchModel implements Disposable { * @param name Filter name * @param v Filter value */ - async setFilter(_name: string, _v: boolean): Promise { + async setFilter(name: string, v: boolean): Promise { // if (this._filters[name] !== v) { // if (this.searchProvider.validateFilter) { // this._filters[name] = await this.searchProvider.validateFilter(name, v); diff --git a/packages/libro-search/src/libro-search-protocol.ts b/packages/libro-search/src/libro-search-protocol.ts index 881539fc..3dae342a 100644 --- a/packages/libro-search/src/libro-search-protocol.ts +++ b/packages/libro-search/src/libro-search-protocol.ts @@ -1,23 +1,8 @@ +import type { SearchMatch } from '@difizen/libro-code-editor'; import type { CellView } from '@difizen/libro-core'; import type { Disposable, Event } from '@difizen/mana-app'; import type { View } from '@difizen/mana-app'; import { Syringe } from '@difizen/mana-app'; - -/** - * Base search match interface - */ -export interface SearchMatch { - /** - * Text of the exact match itself - */ - readonly text: string; - - /** - * Start location of the match (in a text, this is the column) - */ - position: number; -} - /** * HTML search match interface */ diff --git a/packages/libro-search/src/libro-search-provider.ts b/packages/libro-search/src/libro-search-provider.ts index d9a4d857..5b06c733 100644 --- a/packages/libro-search/src/libro-search-provider.ts +++ b/packages/libro-search/src/libro-search-provider.ts @@ -1,18 +1,30 @@ +import type { SearchMatch } from '@difizen/libro-code-editor'; import type { CellView } from '@difizen/libro-core'; -import { LibroView } from '@difizen/libro-core'; -import { inject, prop, transient, watch, equals } from '@difizen/mana-app'; +import { EditorCellView, LibroView, VirtualizedManager } from '@difizen/libro-core'; +import { inject, prop, transient, equals } from '@difizen/mana-app'; import { Deferred, DisposableCollection } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; import { AbstractSearchProvider } from './abstract-search-provider.js'; import { LibroCellSearchProvider } from './libro-cell-search-provider.js'; +import { SearchProviderOption } from './libro-search-protocol.js'; import type { CellSearchProvider, SearchFilter, - SearchMatch, SearchFilters, } from './libro-search-protocol.js'; -import { SearchProviderOption } from './libro-search-protocol.js'; + +export function elementInViewport(el: HTMLElement): boolean { + const boundingClientRect = el.getBoundingClientRect(); + return ( + boundingClientRect.top >= 0 && + boundingClientRect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + boundingClientRect.left >= 0 && + boundingClientRect.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); +} export type LibroSearchProviderFactory = ( option: SearchProviderOption, @@ -44,6 +56,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { @prop() protected providerMap = new Map(); protected documentHasChanged = false; protected override view: LibroView; + protected virtualizedManager: VirtualizedManager; updateSearchCellOutput(value: boolean): void { this.searchCellOutput = value; @@ -52,16 +65,19 @@ export class LibroSearchProvider extends AbstractSearchProvider { /** * @param option Provide the view to search in */ - constructor(@inject(SearchProviderOption) option: SearchProviderOption) { + constructor( + @inject(SearchProviderOption) option: SearchProviderOption, + @inject(VirtualizedManager) virtualizedManager: VirtualizedManager, + ) { super(option); this.view = option.view as LibroView; - this.toDispose.push(watch(this.view.model, 'active', this.onActiveCellChanged)); - this.toDispose.push(watch(this.view.model, 'cells', this.onCellsChanged)); + this.virtualizedManager = virtualizedManager; } protected getProvider = (cell: CellView) => { return this.providerMap.get(cell.id); }; + /** * Report whether or not this provider has the ability to search on the given object * @@ -230,7 +246,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { */ startQuery = async ( query: RegExp, - _filters?: SearchFilters, + filters?: SearchFilters, highlightNext = true, ): Promise => { if (!this.view) { @@ -379,12 +395,11 @@ export class LibroSearchProvider extends AbstractSearchProvider { this.onSearchProviderChanged(); this.cellsChangeDeferred = undefined; }; - protected onCellsChanged = async (): Promise => { + + onCellsChanged = async (): Promise => { if (!this.cellsChangeDeferred) { this.cellsChangeDeferred = new Deferred(); - this.cellsChangeDeferred.promise.then(this.doCellsChanged).catch(() => { - // - }); + this.cellsChangeDeferred.promise.then(this.doCellsChanged).catch(console.error); this.cellsChangeDeferred.resolve(); } }; @@ -401,35 +416,62 @@ export class LibroSearchProvider extends AbstractSearchProvider { } return index; }; + + protected selectCell(selectIndex: number) { + if (selectIndex >= 0 && selectIndex < this.view.model.cells.length - 1) { + this.view.model.selectCell(this.view.model.cells[selectIndex]); + } + } + protected stepNext = async ( reverse = false, loop = false, ): Promise => { - const activateNewMatch = async () => { - // if (this.getActiveIndex() !== this._currentProviderIndex!) { - // this.widget.content.activeCellIndex = this._currentProviderIndex!; - // } - // const activeCell = this.view.activeCell; - // if (!activeCell.inViewport) { - // try { - // if (this.view.activeCell) { - // this.view.model.scrollToView(this.view.activeCell); - // } - // } catch (error) { - // // no-op - // } - // } - // // Unhide cell - // if (activeCell.inputHidden) { - // activeCell.inputHidden = false; - // } - // if (!activeCell.inViewport) { - // // It will not be possible the cell is not in the view - // return; - // } - // await activeCell.ready; - // const editor = activeCell.editor! as CodeMirrorEditor; - // editor.revealSelection(editor.getSelection()); + const activateNewMatch = async (match: SearchMatch) => { + if (this.getActiveIndex() !== this.currentProviderIndex!) { + this.selectCell(this.currentProviderIndex!); + } + const activeCell = this.view.activeCell; + + if (!activeCell) { + return; + } + + const node = activeCell.container?.current; + + if (!elementInViewport(node!)) { + try { + if (this.view.activeCell) { + if (this.virtualizedManager.isVirtualized) { + if (EditorCellView.is(activeCell)) { + const line = activeCell.editor?.getPositionAt(match.position)?.line; + + this.view.model.scrollToCellView({ + cellIndex: this.view.activeCellIndex, + lineIndex: line, + }); + } + } else { + this.view.model.scrollToView(this.view.activeCell); + } + } + } catch (error) { + // no-op + } + } + // Unhide cell + if (activeCell.hasInputHidden) { + activeCell.hasInputHidden = false; + } + if (!elementInViewport(node!)) { + // It will not be possible the cell is not in the view + return; + } + if (EditorCellView.is(activeCell)) { + // await activeCell.editor; + const editor = activeCell.editor; + editor?.revealSelection(editor.getSelection()); + } }; if (this.currentProviderIndex === undefined) { this.currentProviderIndex = this.getActiveIndex()!; @@ -441,7 +483,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { ? await searchEngine?.highlightPrevious() : await searchEngine?.highlightNext(); if (match) { - await activateNewMatch(); + await activateNewMatch(match); return match; } else { this.currentProviderIndex = this.currentProviderIndex + (reverse ? -1 : 1); @@ -469,7 +511,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { : await searchEngine?.highlightNext(); if (match) { - await activateNewMatch(); + await activateNewMatch(match); return match; } } @@ -478,7 +520,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { return undefined; }; - protected onActiveCellChanged = async () => { + onActiveCellChanged = async () => { await this._onSelectionChanged(); if (this.getActiveIndex() !== this.currentProviderIndex) { diff --git a/packages/libro-search/src/libro-search-utils.spec.ts b/packages/libro-search/src/libro-search-utils.spec.ts index 5f3e5084..44181b5d 100644 --- a/packages/libro-search/src/libro-search-utils.spec.ts +++ b/packages/libro-search/src/libro-search-utils.spec.ts @@ -1,7 +1,7 @@ -import 'reflect-metadata'; import assert from 'assert'; -import type { SearchMatch } from './libro-search-protocol.js'; +import type { SearchMatch } from '@difizen/libro-code-editor'; + import { LibroSearchUtils } from './libro-search-utils.js'; describe('libro search utils', () => { diff --git a/packages/libro-search/src/libro-search-utils.ts b/packages/libro-search/src/libro-search-utils.ts index f1f528ec..c24973a4 100644 --- a/packages/libro-search/src/libro-search-utils.ts +++ b/packages/libro-search/src/libro-search-utils.ts @@ -1,7 +1,6 @@ +import type { SearchMatch } from '@difizen/libro-code-editor'; import { singleton } from '@difizen/mana-app'; -import type { SearchMatch } from './libro-search-protocol.js'; - /** * Search Utils */ @@ -69,15 +68,18 @@ export class LibroSearchUtils { const queryText = regex ? queryString : queryString.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); - const ret = new RegExp(queryText, flag); + try { + const ret = new RegExp(queryText, flag); + // If the empty string is hit, the search logic will freeze the browser tab + // Trying /^/ or /$/ on the codemirror search demo, does not find anything. + // So this is a limitation of the editor. + if (ret.test('')) { + return undefined; + } - // If the empty string is hit, the search logic will freeze the browser tab - // Trying /^/ or /$/ on the codemirror search demo, does not find anything. - // So this is a limitation of the editor. - if (ret.test('')) { + return ret; + } catch (error) { return undefined; } - - return ret; } } diff --git a/packages/libro-search/src/libro-search-view.tsx b/packages/libro-search/src/libro-search-view.tsx index 5b74fecc..4bde0e65 100644 --- a/packages/libro-search/src/libro-search-view.tsx +++ b/packages/libro-search/src/libro-search-view.tsx @@ -1,22 +1,21 @@ import { - RightOutlined, - ArrowUpOutlined, ArrowDownOutlined, - EllipsisOutlined, + ArrowUpOutlined, CloseOutlined, createFromIconfontCN, + EllipsisOutlined, + RightOutlined, } from '@ant-design/icons'; import type { LibroView } from '@difizen/libro-core'; import { LirboContextKey } from '@difizen/libro-core'; -import { prop, useInject } from '@difizen/mana-app'; +import { prop, useInject, watch } from '@difizen/mana-app'; import { BaseView, view, ViewInstance } from '@difizen/mana-app'; import { inject, transient } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; -import { Input, Button, Checkbox, Tag } from 'antd'; +import { Button, Checkbox, Input, Tag } from 'antd'; import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import type { InputRef } from 'antd/es/input'; import classnames from 'classnames'; -import type { FC } from 'react'; import { forwardRef, useEffect, useRef } from 'react'; import type { LibroSearchProvider } from './libro-search-provider.js'; @@ -27,7 +26,7 @@ const IconFont = createFromIconfontCN({ scriptUrl: '//at.alicdn.com/t/a/font_3381673_65wfctnq7rt.js', }); -export const ReplaceToggle: FC = () => { +export const ReplaceToggle = () => { const instance = useInject(ViewInstance); return (
@@ -41,16 +40,24 @@ export const ReplaceToggle: FC = () => { ); }; -export const SearchIndex: FC = () => { +export const SearchIndex = () => { const instance = useInject(ViewInstance); + + // TODO: trigger update when current match index changed, matchesCount dont work + useEffect(() => { + // + }, [instance.currentMatchIndex]); + return (
- {instance.currentMatchIndex ?? '-'}/{instance.matchesCount ?? '-'} + {instance.matchesCount !== undefined + ? `${instance.currentMatchIndex}/${instance.matchesCount}` + : '无结果'}
); }; -export const SearchContent: FC = () => { +export const SearchContent = () => { const instance = useInject(ViewInstance); const findInputRef = useRef(null); useEffect(() => { @@ -67,6 +74,7 @@ export const SearchContent: FC = () => { } return; }, [instance]); + return (
{ value={instance.findStr} onChange={instance.handleFindChange} size="small" + placeholder="搜索" suffix={ { })} onClick={instance.toggleCaseSensitive} type="icon-Aa" + title="Match Case" /> { })} onClick={instance.toggleUseRegex} type="icon-zhengzeshi" + title="Use Regular Expression" /> } @@ -106,11 +117,13 @@ export const SearchContent: FC = () => {
@@ -171,7 +187,7 @@ export const SearchContent: FC = () => { }; // TODO: 更改图标 export const SearchComponent = forwardRef(function SearchComponent( - _props: { top?: number }, + props: { top?: number }, ref, ) { const instance = useInject(ViewInstance); @@ -193,41 +209,18 @@ export const SearchComponent = forwardRef(function SearchCompone @view('libro-search-view') export class LibroSearchView extends BaseView { findInputRef?: React.RefObject | null; - contextKey: LirboContextKey; - utils: LibroSearchUtils; - searchProviderFactory: LibroSearchProviderFactory; - - constructor( - @inject(LirboContextKey) contextKey: LirboContextKey, - @inject(LibroSearchUtils) utils: LibroSearchUtils, - @inject(LibroSearchProviderFactory) - searchProviderFactory: LibroSearchProviderFactory, - ) { - super(); - this.contextKey = contextKey; - this.utils = utils; - this.searchProviderFactory = searchProviderFactory; - } - - protected _libro?: LibroView; - - get libro(): LibroView | undefined { - return this._libro; - } - - set libro(value: LibroView | undefined) { - this._libro = value; - this.searchProvider = this.searchProviderFactory({ view: this.libro! }); - } - + @inject(LirboContextKey) contextKey: LirboContextKey; + @inject(LibroSearchUtils) utils: LibroSearchUtils; + @inject(LibroSearchProviderFactory) searchProviderFactory: LibroSearchProviderFactory; + libro?: LibroView; @prop() searchProvider?: LibroSearchProvider; @prop() searchVisible = false; get replaceVisible(): boolean { return this.searchProvider?.replaceMode ?? false; } @prop() settingVisible = false; - @prop() findStr?: string | undefined = undefined; - @prop() lastSearch?: string | undefined = undefined; + @prop() findStr?: string = undefined; + @prop() lastSearch?: string = undefined; @prop() replaceStr = ''; @prop() caseSensitive = false; @prop() useRegex = false; @@ -255,8 +248,31 @@ export class LibroSearchView extends BaseView { return this.searchProvider?.matchesCount; } - onviewWillUnmount = () => { - this.searchProvider?.endQuery(); + override onViewMount = () => { + if (!this.searchProvider && this.libro) { + this.searchProvider = this.searchProviderFactory({ view: this.libro }); + this.toDispose.push(watch(this.libro.model, 'active', this.onActiveCellChanged)); + this.toDispose.push( + this.libro.model.onSourceChanged(() => this.onCellsChanged()), + ); + } + }; + + onActiveCellChanged = () => { + if (this.searchVisible) { + this.searchProvider?.onActiveCellChanged(); + } + }; + + onCellsChanged = () => { + if (this.searchVisible) { + this.searchProvider?.onCellsChanged(); + } + }; + + onviewWillUnmount = async () => { + await this.searchProvider?.endQuery(); + this.searchProvider?.dispose(); }; show = () => { @@ -269,6 +285,9 @@ export class LibroSearchView extends BaseView { this.searchVisible = false; this.contextKey.enableCommandMode(); this.searchProvider?.endQuery(); + if (this.searchProvider) { + this.searchProvider.replaceMode = false; + } this.libro?.focus(); }; @@ -316,6 +335,7 @@ export class LibroSearchView extends BaseView { toggleUseRegex = () => { this.useRegex = !this.useRegex; + this.search(); }; next = () => { @@ -342,8 +362,8 @@ export class LibroSearchView extends BaseView { const init = this.searchProvider?.getInitialQuery(); if (init) { this.findStr = init; - this.search(false); } + this.search(false); }; getHeaderHeight = () => { let height = 32; diff --git a/packages/libro-shared-model/package.json b/packages/libro-shared-model/package.json index 87fa0ca1..d883859f 100644 --- a/packages/libro-shared-model/package.json +++ b/packages/libro-shared-model/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@difizen/libro-common": "^0.1.0", - "@difizen/mana-common": "latest", + "@difizen/mana-app": "latest", "uuid": "^9.0.0", "y-protocols": "^1.0.5", "yjs": "^13.5.40" diff --git a/packages/libro-shared-model/src/api.ts b/packages/libro-shared-model/src/api.ts index 7cca361f..e36ddcb0 100644 --- a/packages/libro-shared-model/src/api.ts +++ b/packages/libro-shared-model/src/api.ts @@ -23,7 +23,7 @@ import type { IAttachments, IUnrecognizedCell, } from '@difizen/libro-common'; -import type { Disposable, Event } from '@difizen/mana-common'; +import type { Disposable, Event } from '@difizen/mana-app'; /** * Changes on Sequence-like data are expressed as Quill-inspired deltas. diff --git a/packages/libro-shared-model/src/ymodels.ts b/packages/libro-shared-model/src/ymodels.ts index 2556cfbb..e5cb8ff0 100644 --- a/packages/libro-shared-model/src/ymodels.ts +++ b/packages/libro-shared-model/src/ymodels.ts @@ -13,8 +13,8 @@ import type { CellType, } from '@difizen/libro-common'; import { deepCopy, deepEqual } from '@difizen/libro-common'; -import type { Event } from '@difizen/mana-common'; -import { Emitter } from '@difizen/mana-common'; +import type { Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; import { v4 } from 'uuid'; import { Awareness } from 'y-protocols/awareness'; import * as Y from 'yjs'; @@ -181,7 +181,7 @@ export class YDocument implements ISharedDocument { protected _changedEmitter = new Emitter(); protected _changed = this._changedEmitter.event; - private _isDisposed = false; + protected _isDisposed = false; } /** @@ -280,7 +280,7 @@ export class YFile /** * Handle a change to the ymodel. */ - private _modelObserver = (event: Y.YTextEvent) => { + protected _modelObserver = (event: Y.YTextEvent) => { this._changedEmitter.fire({ sourceChange: event.changes.delta as Delta }); }; } @@ -827,7 +827,7 @@ export class YBaseCell /** * Handle a change to the ymodel. */ - private _modelObserver = (events: Y.YEvent[]) => { + protected _modelObserver = (events: Y.YEvent[]) => { this._changedEmitter.fire(this.getChanges(events)); }; @@ -837,13 +837,13 @@ export class YBaseCell * The notebook that this cell belongs to. */ protected _notebook: YNotebook | null = null; - private _awareness: Awareness | null; - private _changedEmitter = new Emitter>(); - private _changed = this._changedEmitter.event; - private _isDisposed = false; - private _prevSourceLength: number; - private _undoManager: Y.UndoManager | null = null; - private _ysource: Y.Text; + protected _awareness: Awareness | null; + protected _changedEmitter = new Emitter>(); + protected _changed = this._changedEmitter.event; + protected _isDisposed = false; + protected _prevSourceLength: number; + protected _undoManager: Y.UndoManager | null = null; + protected _ysource: Y.Text; } /** @@ -999,7 +999,7 @@ export class YCodeCell extends YBaseCell implements ISharedCo return changes; } - private _youtputs: Y.Array; + protected _youtputs: Y.Array; } class YAttachmentCell @@ -1469,7 +1469,7 @@ export class YNotebook extends YDocument implements ISharedNoteb /** * Handle a change to the ystate. */ - private _onMetaChanged = (event: Y.YMapEvent) => { + protected _onMetaChanged = (event: Y.YMapEvent) => { if (event.keysChanged.has('metadata')) { const change = event.changes.keys.get('metadata'); const metadataChange = { @@ -1530,7 +1530,7 @@ export class YNotebook extends YDocument implements ISharedNoteb /** * Handle a change to the list of cells. */ - private _onYCellsChanged = (event: Y.YArrayEvent>) => { + protected _onYCellsChanged = (event: Y.YArrayEvent>) => { // update the type cell mapping by iterating through the added/removed types event.changes.added.forEach((item) => { const type = (item.content as Y.ContentType).type as Y.Map; @@ -1604,6 +1604,6 @@ export class YNotebook extends YDocument implements ISharedNoteb */ protected readonly _ycells: Y.Array> = this.ydoc.getArray('cells'); - private _disableDocumentWideUndoRedo: boolean; - private _ycellMapping: WeakMap, YCellType> = new WeakMap(); + protected _disableDocumentWideUndoRedo: boolean; + protected _ycellMapping: WeakMap, YCellType> = new WeakMap(); } diff --git a/packages/libro-widget/src/base/libro-widgets.ts b/packages/libro-widget/src/base/libro-widgets.ts index 2c109892..ebd133fa 100644 --- a/packages/libro-widget/src/base/libro-widgets.ts +++ b/packages/libro-widget/src/base/libro-widgets.ts @@ -197,7 +197,7 @@ export class LibroWidgets implements IWidgets { * Dictionary of model ids and model instance promises */ @prop() - private models: Map = new Map(); + protected models: Map = new Map(); /** * The comm target name to register diff --git a/packages/libro-widget/src/base/widget-manager.ts b/packages/libro-widget/src/base/widget-manager.ts index 154b671a..33ce442e 100644 --- a/packages/libro-widget/src/base/widget-manager.ts +++ b/packages/libro-widget/src/base/widget-manager.ts @@ -37,5 +37,5 @@ export class LibroWidgetManager implements ApplicationContribution { * Dictionary of model ids and model instance promises */ @prop() - private widgets: Map = new Map(); + protected widgets: Map = new Map(); }