diff --git a/.changeset/pre.json b/.changeset/pre.json index c3d74ed0..11189d01 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -21,7 +21,7 @@ "@difizen/libro-shared-model": "0.0.1", "@difizen/libro-toc": "0.0.1", "@difizen/libro-widget": "0.0.1", - "@difizen/mana-docs": "0.0.1" + "@difizen/libro-docs": "0.0.1" }, "changesets": ["shaggy-lemons-prove"] } diff --git a/README.md b/README.md index c4cda30e..f8fc64b3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # libro -[![Code: CI](https://github.com/difizen/libro/actions/workflows/ci.yml/badge.svg)](https://github.com/difizen/libro/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/difizen/libro/graph/badge.svg?token=8LWLNZK78Z)](https://codecov.io/gh/difizen/libro) +
+Editor +
+
notebook 产品前端解决方案。 +
+ +[![Code: CI](https://github.com/difizen/libro/actions/workflows/ci.yml/badge.svg)](https://github.com/difizen/libro/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/difizen/libro/graph/badge.svg?token=8LWLNZK78Z)](https://codecov.io/gh/difizen/libro) - 优雅的交互和丰富的功能 - 方便扩展和二次开发 diff --git a/apps/docs/CHANGELOG.md b/apps/docs/CHANGELOG.md index 8f55a852..64d57915 100644 --- a/apps/docs/CHANGELOG.md +++ b/apps/docs/CHANGELOG.md @@ -1,4 +1,4 @@ -# @difizen/mana-docs +# @difizen/libro-docs ## 0.0.2-alpha.0 diff --git a/apps/docs/docs/examples/index.md b/apps/docs/docs/examples/index.md index 15851874..b9d2c19a 100644 --- a/apps/docs/docs/examples/index.md +++ b/apps/docs/docs/examples/index.md @@ -3,6 +3,6 @@ title: 输出示例 order: 0 --- -# Libro 输出示例 +# libro 输出示例 diff --git a/apps/docs/docs/examples/lab.md b/apps/docs/docs/examples/lab.md new file mode 100644 index 00000000..d83e51f6 --- /dev/null +++ b/apps/docs/docs/examples/lab.md @@ -0,0 +1,10 @@ +--- +title: Lab +order: 1 +--- + +# libro lab + +本地在某个文件目录下,起一个`jupyterlab`服务,该文件目录中包含以 `.ipynb` 为后缀名的文件。 + + diff --git a/apps/docs/docs/examples/workbench.md b/apps/docs/docs/examples/workbench.md index b846a0e0..34285c1c 100644 --- a/apps/docs/docs/examples/workbench.md +++ b/apps/docs/docs/examples/workbench.md @@ -3,7 +3,7 @@ title: 工作台 order: 1 --- -# Libro 工作台 +# libro 工作台 本地在某个文件目录下,起一个`jupyterlab`服务,该文件目录中包含以 `.ipynb` 为后缀名的文件。 diff --git a/apps/docs/package.json b/apps/docs/package.json index b8236a32..c396a2d0 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,5 +1,5 @@ { - "name": "@difizen/mana-docs", + "name": "@difizen/libro-docs", "version": "0.0.2-alpha.0", "private": true, "license": "MIT", @@ -22,6 +22,7 @@ "@difizen/mana-app": "latest", "@difizen/mana-react": "latest", "@difizen/libro-jupyter": "^0.0.2-alpha.0", + "@difizen/libro-lab": "^0.0.2-alpha.0", "@difizen/libro-core": "^0.0.2-alpha.0", "@ant-design/icons": "^5.1.0", "react": "^18.2.0", diff --git a/apps/docs/src/lab/app.ts b/apps/docs/src/lab/app.ts new file mode 100644 index 00000000..aaee60d1 --- /dev/null +++ b/apps/docs/src/lab/app.ts @@ -0,0 +1,22 @@ +import { ServerConnection, ServerManager } from '@difizen/libro-jupyter'; +import { ConfigurationService } from '@difizen/mana-app'; +import { SlotViewManager } from '@difizen/mana-app'; +import { ApplicationContribution, ViewManager } from '@difizen/mana-app'; +import { inject, singleton } from '@difizen/mana-app'; + +@singleton({ contrib: ApplicationContribution }) +export class LibroApp implements ApplicationContribution { + @inject(ServerConnection) serverConnection: ServerConnection; + @inject(ServerManager) serverManager: ServerManager; + @inject(ViewManager) viewManager: ViewManager; + @inject(SlotViewManager) slotViewManager: SlotViewManager; + @inject(ConfigurationService) configurationService: ConfigurationService; + + async onStart() { + this.serverConnection.updateSettings({ + baseUrl: 'http://localhost:8888/', + wsUrl: 'ws://localhost:8888/', + }); + this.serverManager.launch(); + } +} diff --git a/apps/docs/src/lab/index.less b/apps/docs/src/lab/index.less new file mode 100644 index 00000000..b6b1c821 --- /dev/null +++ b/apps/docs/src/lab/index.less @@ -0,0 +1,9 @@ +.libro-workbench-app { + width: 100%; + height: calc(100vh - 61px); +} + +.dumi-default-doc-layout > main { + margin: unset; + max-width: unset; +} diff --git a/apps/docs/src/lab/index.tsx b/apps/docs/src/lab/index.tsx new file mode 100644 index 00000000..4a25a3b7 --- /dev/null +++ b/apps/docs/src/lab/index.tsx @@ -0,0 +1,21 @@ +import { LibroLabModule } from '@difizen/libro-lab'; +import { ManaAppPreset, ManaComponents, ManaModule } from '@difizen/mana-app'; + +import { LibroApp } from './app.js'; +import './index.less'; + +const BaseModule = ManaModule.create().register(LibroApp); + +const App = (): JSX.Element => { + return ( +
+ +
+ ); +}; + +export default App; diff --git a/apps/docs/src/workbench/index.tsx b/apps/docs/src/workbench/index.tsx index d3618d81..e1e8c323 100644 --- a/apps/docs/src/workbench/index.tsx +++ b/apps/docs/src/workbench/index.tsx @@ -29,11 +29,6 @@ const BaseModule = ManaModule.create().register( view: FileTreeView, slot: LibroWorkbenchSlots.Left, }), - // createViewPreference({ - // autoCreate: true, - // view: FileTreeView, - // slot: LibroWorkbenchSlots.Left, - // }), ); const App = (): JSX.Element => { diff --git a/package.json b/package.json index 504c6efb..1a0592d1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test": "nx run-many --target=test", "ci": "nx run-many --target=lint:prettier,lint,test,test:jest", "ci:affected": "nx affected --target=lint:prettier,lint,test:jest,coverage:jest", - "docs": "nx run @difizen/mana-docs:start", + "docs": "nx run @difizen/libro-docs:start", "changeset": "changeset", "clean": "git clean -fX ." }, diff --git a/packages/libro-jupyter/src/file/file-command.tsx b/packages/libro-jupyter/src/file/file-command.tsx new file mode 100644 index 00000000..a792680e --- /dev/null +++ b/packages/libro-jupyter/src/file/file-command.tsx @@ -0,0 +1,320 @@ +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 new file mode 100644 index 00000000..cb2eb5a7 --- /dev/null +++ b/packages/libro-jupyter/src/file/file-create-modal-contribution.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..6732c25a --- /dev/null +++ b/packages/libro-jupyter/src/file/file-create-modal.tsx @@ -0,0 +1,73 @@ +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 new file mode 100644 index 00000000..2c4034da --- /dev/null +++ b/packages/libro-jupyter/src/file/file-createdir-modal-contribution.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..b861622a --- /dev/null +++ b/packages/libro-jupyter/src/file/file-createdir-modal.tsx @@ -0,0 +1,76 @@ +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-protocol.ts b/packages/libro-jupyter/src/file/file-protocol.ts index b420bab7..248ab0e8 100644 --- a/packages/libro-jupyter/src/file/file-protocol.ts +++ b/packages/libro-jupyter/src/file/file-protocol.ts @@ -5,3 +5,20 @@ export enum FileType { 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 new file mode 100644 index 00000000..a58aa549 --- /dev/null +++ b/packages/libro-jupyter/src/file/file-rename-modal-contribution.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..6f9ae6d5 --- /dev/null +++ b/packages/libro-jupyter/src/file/file-rename-modal.tsx @@ -0,0 +1,64 @@ +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 index a94a0a0f..b56e7388 100644 --- a/packages/libro-jupyter/src/file/file-service.ts +++ b/packages/libro-jupyter/src/file/file-service.ts @@ -1,3 +1,5 @@ +import pathUtil from 'path'; + import type { IContentsModel } from '@difizen/libro-kernel'; import { ContentsManager } from '@difizen/libro-kernel'; import type { @@ -7,6 +9,7 @@ import type { 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'; @@ -50,41 +53,29 @@ export class JupyterFileService extends FileService { } async write(filePath: string, content: string): Promise { - try { - await this.contentsManager.save(filePath, { - content, - }); - return filePath; - } catch (_e) { - // - } - return undefined; + await this.contentsManager.save(filePath, { + content, + }); + return filePath; } async readDir(dirPath: string): Promise { let children: DirItem[] = []; - try { - 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]; - }); - } - } catch (_e) { - // + 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; - try { - const res = await this.contentsManager.get(filePath); - if (res && !this.isDirectory(res)) { - content = res.content as string; - } - } catch (_e) { - // + const res = await this.contentsManager.get(filePath); + if (res && !this.isDirectory(res)) { + content = res.content as string; } return content; } @@ -94,7 +85,7 @@ export class JupyterFileService extends FileService { try { const res = await this.contentsManager.get(filePath); stat = this.toFileMeta(res); - } catch (_e) { + } catch { // } return stat; @@ -135,6 +126,7 @@ export class JupyterFileService extends FileService { _target: URI, _options?: CopyFileOptions, ): Promise { + await this.contentsManager.copy(source.path.toString(), _target.path.toString()); return this.resolve(source); } override async move( @@ -147,7 +139,6 @@ export class JupyterFileService extends FileService { toFileStatMeta(meta: FileMeta): FileStatWithMetadata { const uri = URI.withScheme(new URI(meta.resource), 'file'); - return { ...meta, resource: uri, @@ -176,4 +167,49 @@ export class JupyterFileService extends FileService { 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-view/index.tsx b/packages/libro-jupyter/src/file/file-view/index.tsx index f32b5a59..1a20db0d 100644 --- a/packages/libro-jupyter/src/file/file-view/index.tsx +++ b/packages/libro-jupyter/src/file/file-view/index.tsx @@ -1,3 +1,4 @@ +import { FolderFilled } from '@ant-design/icons'; import type { TreeNode, ViewOpenHandler } from '@difizen/mana-app'; import { FileTreeViewFactory } from '@difizen/mana-app'; import { @@ -19,7 +20,7 @@ import { inject, singleton, } from '@difizen/mana-app'; -import type React from 'react'; +import React from 'react'; import type { LibroNavigatableView } from '../navigatable-view.js'; @@ -53,6 +54,8 @@ export class FileView extends FileTreeView { labelProvider, decoratorService, ); + this.title.label = '文件导航'; + this.title.icon = ; this.toDispose.push(this.model.onOpenNode(this.openNode)); } diff --git a/packages/libro-jupyter/src/file/module.ts b/packages/libro-jupyter/src/file/module.ts index 9e8a9fe6..6f99eea0 100644 --- a/packages/libro-jupyter/src/file/module.ts +++ b/packages/libro-jupyter/src/file/module.ts @@ -1,6 +1,10 @@ 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'; @@ -15,5 +19,9 @@ export const LibroJupyterFileModule = ManaModule.create() 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 index aea1ce00..c2c9be41 100644 --- a/packages/libro-jupyter/src/file/navigatable-view.tsx +++ b/packages/libro-jupyter/src/file/navigatable-view.tsx @@ -1,6 +1,7 @@ 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, @@ -19,6 +20,8 @@ import { } 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); @@ -32,15 +35,23 @@ export const LibroEditorComponent = forwardRef(function LibroEditorComponent() { export const LibroNavigatableViewFactoryId = 'libro-navigatable-view-factory'; @transient() @view(LibroNavigatableViewFactoryId) -export class LibroNavigatableView extends BaseView implements NavigatableView { +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; @@ -56,6 +67,7 @@ export class LibroNavigatableView extends BaseView implements NavigatableView { ) { 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)); @@ -77,7 +89,14 @@ export class LibroNavigatableView extends BaseView implements NavigatableView { 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(); } diff --git a/packages/libro-jupyter/src/file/utils.ts b/packages/libro-jupyter/src/file/utils.ts new file mode 100644 index 00000000..e1a93dff --- /dev/null +++ b/packages/libro-jupyter/src/file/utils.ts @@ -0,0 +1,51 @@ +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.ts b/packages/libro-jupyter/src/index.ts index 333f4826..20e59da3 100644 --- a/packages/libro-jupyter/src/index.ts +++ b/packages/libro-jupyter/src/index.ts @@ -26,6 +26,7 @@ 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'; diff --git a/packages/libro-lab/package.json b/packages/libro-lab/package.json index c10c62c1..b29a0bb9 100644 --- a/packages/libro-lab/package.json +++ b/packages/libro-lab/package.json @@ -48,10 +48,14 @@ "dependencies": { "@difizen/mana-app": "latest", "@difizen/mana-react": "latest", - "@difizen/libro-jupyter": "0.0.2-alpha.0" + "@difizen/libro-jupyter": "0.0.2-alpha.0", + "@difizen/libro-toc": "0.0.2-alpha.0", + "@ant-design/icons": "^5.1.0", + "classnames": "^2.2.6" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.2.0", + "antd": "^5.8.6" }, "devDependencies": { "@types/react": "^18.2.25" diff --git a/packages/libro-lab/src/github-link/index.tsx b/packages/libro-lab/src/github-link/index.tsx new file mode 100644 index 00000000..67b23761 --- /dev/null +++ b/packages/libro-lab/src/github-link/index.tsx @@ -0,0 +1,27 @@ +import { GithubFilled } from '@ant-design/icons'; +import { singleton, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { forwardRef } from 'react'; + +export const GithubLinkComponent = forwardRef(function GithubLinkComponent() { + return ( + + + + ); +}); + +@singleton() +@view('github-link') +export class GithubLinkView extends BaseView { + override view = GithubLinkComponent; + link = ''; + constructor() { + super(); + } +} diff --git a/packages/libro-lab/src/index.less b/packages/libro-lab/src/index.less new file mode 100644 index 00000000..87fd8e3b --- /dev/null +++ b/packages/libro-lab/src/index.less @@ -0,0 +1,69 @@ +#libro-lab-content-layout-left { + .mana-side-tab-view { + .mana-tabs-left { + .mana-tabs-nav { + background-color: #f0f2f51f; + min-width: 40px; + box-shadow: + 1px 0 0 0 rgba(0, 10, 26, 0.84%), + 2px 0 0 0 rgba(255, 255, 255, 4.8%); + + .mana-tabs-ink-bar { + display: none; + } + + .mana-tabs-tab { + width: 40px; + height: 40px; + + &-btn { + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + + .mana-tab-icon { + .anticon { + font-size: 16px; + } + } + } + } + + .mana-tabs-tab-active { + .mana-tabs-tab-btn { + background-color: #000a1a1f; + + .mana-tab-icon { + color: #1890ff; + } + } + } + } + + .mana-tabs-content-holder { + background-image: linear-gradient(269deg, #c8c8cd1f 0%, #ffffff1f 100%); + border-left: none; + background-color: unset; + + .mana-tab-side-pane-content { + height: calc(100% - 32px); + } + } + } + } +} + +#libro-lab-content-layout-main { + background-color: #f8f8fb; + + .mana-tabs-nav { + background-color: #f8f8fb; + } +} + +#libro-lab-content-layout-main-container { + overflow: auto; +} diff --git a/packages/libro-lab/src/kernel-manager/index.tsx b/packages/libro-lab/src/kernel-manager/index.tsx new file mode 100644 index 00000000..da602142 --- /dev/null +++ b/packages/libro-lab/src/kernel-manager/index.tsx @@ -0,0 +1,22 @@ +import { CodeFilled } from '@ant-design/icons'; +import { singleton, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { forwardRef } from 'react'; + +// import './index.less'; + +export const KernelManagerComponent = forwardRef(function KernelManagerComponent() { + return 暂无文件; +}); + +@singleton() +@view('kernel-manager') +export class KernelManagerView extends BaseView { + override view = KernelManagerComponent; + + constructor() { + super(); + this.title.icon = ; + this.title.label = 'Kernel 管理'; + } +} diff --git a/packages/libro-lab/src/lab-app.ts b/packages/libro-lab/src/lab-app.ts index fe4d1f9b..9d4a2b44 100644 --- a/packages/libro-lab/src/lab-app.ts +++ b/packages/libro-lab/src/lab-app.ts @@ -15,7 +15,7 @@ import { singleton, } from '@difizen/mana-app'; -import { LibroLabContentSlots } from './layout/index.js'; +import { LibroLabLayoutSlots } from './layout/index.js'; @singleton({ contrib: ApplicationContribution }) export class LibroLabApp implements ApplicationContribution { @@ -26,13 +26,9 @@ export class LibroLabApp implements ApplicationContribution { @inject(ConfigurationService) configurationService: ConfigurationService; async onStart() { - this.serverConnection.updateSettings({ - baseUrl: 'http://localhost:8888/', - wsUrl: 'ws://localhost:8888/', - }); this.configurationService.set( LibroJupyterConfiguration['OpenSlot'], - LibroLabContentSlots.main, + LibroLabLayoutSlots.content, ); await this.initialWorkspace(); } diff --git a/packages/libro-lab/src/layout/brand/index.less b/packages/libro-lab/src/layout/brand/index.less new file mode 100644 index 00000000..ca6cf6d2 --- /dev/null +++ b/packages/libro-lab/src/layout/brand/index.less @@ -0,0 +1,16 @@ +.libro-lab-brand { + display: flex; + align-items: center; + height: 100%; + width: 100%; + padding: 0 22px; + + &-logo { + height: 16px; + } + + label { + font-size: 16px; + margin-left: 8px; + } +} diff --git a/packages/libro-lab/src/layout/brand/index.tsx b/packages/libro-lab/src/layout/brand/index.tsx new file mode 100644 index 00000000..395a0572 --- /dev/null +++ b/packages/libro-lab/src/layout/brand/index.tsx @@ -0,0 +1,22 @@ +import { BaseView, view, singleton } from '@difizen/mana-app'; +import * as React from 'react'; + +import { Logo } from './logo.js'; +import './index.less'; + +export const Brand: React.ForwardRefExoticComponent = React.forwardRef( + function Brand(_props, ref: React.ForwardedRef) { + return ( +
+ + +
+ ); + }, +); + +@singleton() +@view('libro-brand-view') +export class BrandView extends BaseView { + override view = Brand; +} diff --git a/packages/libro-lab/src/layout/brand/logo.tsx b/packages/libro-lab/src/layout/brand/logo.tsx new file mode 100644 index 00000000..2fc6ff5f --- /dev/null +++ b/packages/libro-lab/src/layout/brand/logo.tsx @@ -0,0 +1,39 @@ +export interface IProps { + className?: string; + width?: string; + height?: string; +} +export function Logo(props: IProps) { + const { className = '' } = props; + return ( + + + + + + + + ); +} diff --git a/packages/libro-lab/src/layout/container.tsx b/packages/libro-lab/src/layout/container.tsx new file mode 100644 index 00000000..73158a97 --- /dev/null +++ b/packages/libro-lab/src/layout/container.tsx @@ -0,0 +1,28 @@ +import { singleton, Slot, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { BoxPanel } from '@difizen/mana-react'; +import { forwardRef } from 'react'; + +import './index.less'; +import { LibroLabLayoutSlots } from './protocol.js'; + +export const LibroLabLayoutContainerComponent = forwardRef( + function LibroLabLayoutContainerComponent() { + return ( + + + + + + + + + ); + }, +); + +@singleton() +@view('libro-lab-layout-container') +export class LibroLabLayoutContainerView extends BaseView { + override view = LibroLabLayoutContainerComponent; +} diff --git a/packages/libro-lab/src/layout/editor-tab-view.tsx b/packages/libro-lab/src/layout/editor-tab-view.tsx new file mode 100644 index 00000000..387b1533 --- /dev/null +++ b/packages/libro-lab/src/layout/editor-tab-view.tsx @@ -0,0 +1,67 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { EditorView } from '@difizen/libro-jupyter'; +import type { View } from '@difizen/mana-app'; +import { + CardTabView, + MenuRender, + transient, + view, + ViewContext, +} from '@difizen/mana-app'; +import { Dropdown } from '@difizen/mana-react'; +import { Badge } from 'antd'; +import classnames from 'classnames'; + +@transient() +@view('LibroLabEditorTab') +export class EditorTabView extends CardTabView { + protected override renderTab(item: View) { + return ( + + } + > +
+ {item.title.icon && ( + + {this.renderTitleIcon(item.title.icon)} + + )} + {this.renderTitleLabel(item.title.label)} + {this.renderTail(item)} +
+
+
+ ); + } + + protected renderTail(item: View) { + const isDirty = EditorView.is(item) && item.dirty; + return ( +
+ {isDirty ? ( +
+ +
+ ) : ( + item.title.closable && ( + { + e.stopPropagation(); + this.close(item); + if (this.children.length > 0) { + this.active = this.children[this.children.length - 1]; + } + }} + className="mana-tab-close" + /> + ) + )} +
+ ); + } +} diff --git a/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx b/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx new file mode 100644 index 00000000..285de16b --- /dev/null +++ b/packages/libro-lab/src/layout/footer/current-file-footer-view.tsx @@ -0,0 +1,44 @@ +import { LibroNavigatableView } from '@difizen/libro-jupyter'; +import { + BaseView, + inject, + singleton, + useInject, + view, + ViewInstance, +} from '@difizen/mana-app'; +import * as React from 'react'; + +import { LayoutService } from '../layout-service.js'; +import { LibroLabLayoutSlots } from '../protocol.js'; +import './index.less'; + +const CurrentFileFooterComponent = React.forwardRef(function CurrentFileFooterComponent( + _props, + ref: React.ForwardedRef, +) { + const currentFileFooterView = useInject(ViewInstance); + + return ( +
+ {`当前文件:${ + currentFileFooterView.libroNavigatableView?.title.label || '' + }`} +
+ ); +}); + +@singleton() +@view('libro-lab-current-file-footer-view') +export class LibroLabCurrentFileFooterView extends BaseView { + override view = CurrentFileFooterComponent; + @inject(LayoutService) protected layoutService: LayoutService; + + get libroNavigatableView() { + const contentView = this.layoutService.getActiveView(LibroLabLayoutSlots.content); + if (contentView instanceof LibroNavigatableView) { + return contentView; + } + return undefined; + } +} diff --git a/packages/libro-lab/src/layout/footer/footer-view.tsx b/packages/libro-lab/src/layout/footer/footer-view.tsx new file mode 100644 index 00000000..a8d28b10 --- /dev/null +++ b/packages/libro-lab/src/layout/footer/footer-view.tsx @@ -0,0 +1,30 @@ +import { DefaultSlotView, singleton, Slot, view } from '@difizen/mana-app'; +import { BoxPanel } from '@difizen/mana-react'; +import * as React from 'react'; +import './index.less'; + +export enum FooterArea { + left = 'libro-lab-footer-left', + right = 'libro-lab-footer-right', +} + +const FooterComponent: React.ForwardRefExoticComponent = React.forwardRef( + function FooterComponent(_props, ref: React.ForwardedRef) { + return ( + + + + + + + + + ); + }, +); + +@singleton() +@view('libro-lab-footer-view') +export class LibroLabLayoutFooterView extends DefaultSlotView { + override view = FooterComponent; +} diff --git a/packages/libro-lab/src/layout/footer/index.less b/packages/libro-lab/src/layout/footer/index.less new file mode 100644 index 00000000..1861f1da --- /dev/null +++ b/packages/libro-lab/src/layout/footer/index.less @@ -0,0 +1,17 @@ +.libro-lab-footer { + .libro-lab-footer-left { + padding: 0 24px; + } +} + +.libro-lab-current-file-footer { + height: 100%; + display: flex; + align-items: center; + + span { + font-weight: 400; + font-size: 12px; + color: rgba(0, 10, 26, 68%); + } +} diff --git a/packages/libro-lab/src/layout/header.tsx b/packages/libro-lab/src/layout/header.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/libro-lab/src/layout/index.less b/packages/libro-lab/src/layout/index.less index 19a38e3a..cbc686ad 100644 --- a/packages/libro-lab/src/layout/index.less +++ b/packages/libro-lab/src/layout/index.less @@ -1,17 +1,59 @@ -/* stylelint-disable color-function-notation */ +@ant-prefix: ant; + .libro-lab-layout { width: 100%; height: 100%; - &-top { - height: 48px; + &-header { + height: 56px; + min-height: 56px; + } + + &-container { + overflow: auto; } &-main { - height: calc(100% - 72px); + overflow: auto; } - &-bottom { - height: 24px; + &-footer { + height: 28px; + min-height: 28px; + background-image: linear-gradient(270deg, #fafafa 0%, #fff 100%); + box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 12%); + } + + .mana-header { + background: var(--mana-color-bg-container); + border-bottom: none; + + .mana-header-left, + .mana-header-right { + flex: unset !important; + } + + .mana-menubar { + height: 100%; + padding: unset; + } + + .mana-menubar-item-text { + color: rgba(0, 10, 26, 89%); + font-weight: 400; + } + } +} + +.libro-lab-editor-tab-tail { + width: 16px; + + .libro-lab-editor-tab-dirty { + padding: 0 4px; + + .ant-badge.ant-badge-status .ant-badge-status-dot { + height: 8px; + width: 8px; + } } } diff --git a/packages/libro-lab/src/layout/index.tsx b/packages/libro-lab/src/layout/index.tsx index cac0b311..c700d834 100644 --- a/packages/libro-lab/src/layout/index.tsx +++ b/packages/libro-lab/src/layout/index.tsx @@ -1,80 +1,4 @@ -import { singleton, Slot, view } from '@difizen/mana-app'; -import { BaseView } from '@difizen/mana-app'; -import { BoxPanel, SplitPanel } from '@difizen/mana-react'; -import { forwardRef } from 'react'; - -import './index.less'; - -export const LibroLabSlots = { - top: 'libro-lab-top', - bottom: 'libro-lab-bottom', -}; -export const LibroLabContentSlots = { - left: 'libro-lab-content-left', - main: 'libro-lab-content-main', - bottom: 'libro-lab-content-bottom', -}; - -export const LibroLabLayoutComponent = forwardRef( - function LibroWorkbenchLayoutComponent() { - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- ); - }, -); - -@singleton() -@view('libro-lab-layout') -export class LibroLabLayoutView extends BaseView { - override view = LibroLabLayoutComponent; -} +export * from './protocol.js'; +export * from './module.js'; +export * from './layout.js'; +export * from './footer/footer-view.js'; diff --git a/packages/libro-lab/src/layout/layout-service.ts b/packages/libro-lab/src/layout/layout-service.ts new file mode 100644 index 00000000..544c9acf --- /dev/null +++ b/packages/libro-lab/src/layout/layout-service.ts @@ -0,0 +1,64 @@ +import type { SlotView, View, ViewOpenHandlerOptions } from '@difizen/mana-app'; +import { + DefaultSlotView, + inject, + Notifier, + prop, + singleton, + SlotViewManager, +} from '@difizen/mana-app'; + +import type { LibroLabLayoutSlotsType } from './protocol.js'; +import { LibroLabLayoutSlots } from './protocol.js'; + +export type VisibilityMap = Record; + +@singleton() +export class LayoutService { + @inject(SlotViewManager) protected readonly slotViewManager: SlotViewManager; + + @prop() + protected visibilityMap: VisibilityMap = { + [LibroLabLayoutSlots.header]: true, + [LibroLabLayoutSlots.container]: true, + [LibroLabLayoutSlots.main]: true, + [LibroLabLayoutSlots.footer]: true, + [LibroLabLayoutSlots.navigator]: true, + [LibroLabLayoutSlots.content]: true, + [LibroLabLayoutSlots.contentBottom]: false, + }; + + isAreaVisible(slot: LibroLabLayoutSlotsType): boolean { + return this.visibilityMap[slot]; + } + + setAreaVisible(slot: LibroLabLayoutSlotsType, visible: boolean) { + this.visibilityMap[slot] = visible; + } + + async addView(view: View, option?: ViewOpenHandlerOptions): Promise { + const { slot = LibroLabLayoutSlots.main, ...viewOpenOption } = option || {}; + const slotView = this.slotViewManager.getSlotView(slot) as SlotView; + await slotView.addView(view, viewOpenOption); + } + + getActiveView(slot: LibroLabLayoutSlotsType) { + if (this.isAreaVisible(slot)) { + const slotView = this.slotViewManager.getSlotView(slot); + if (slotView instanceof DefaultSlotView) { + return slotView.active; + } + } + return undefined; + } + + onSlotActiveChange(slot: LibroLabLayoutSlotsType, handler: () => void) { + if (this.isAreaVisible(slot)) { + const slotView = this.slotViewManager.getSlotView(slot); + if (slotView instanceof DefaultSlotView) { + return Notifier.find(slotView, 'active')?.onChange(() => handler()); + } + } + return undefined; + } +} diff --git a/packages/libro-lab/src/layout/layout.tsx b/packages/libro-lab/src/layout/layout.tsx new file mode 100644 index 00000000..e15518a3 --- /dev/null +++ b/packages/libro-lab/src/layout/layout.tsx @@ -0,0 +1,37 @@ +import { singleton, Slot, useInject, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { BoxPanel } from '@difizen/mana-react'; +import { forwardRef } from 'react'; + +import './index.less'; +import { LayoutService } from './layout-service.js'; +import { LibroLabLayoutSlots } from './protocol.js'; + +export const LibroLabLayoutContainerComponent = forwardRef( + function LibroLabLayoutContainerComponent() { + const layoutService = useInject(LayoutService); + + return ( +
+ + {layoutService.isAreaVisible(LibroLabLayoutSlots.header) && ( + + + + )} + {layoutService.isAreaVisible(LibroLabLayoutSlots.container) && ( + + + + )} + +
+ ); + }, +); + +@singleton() +@view('libro-lab-layout') +export class LibroLabLayoutView extends BaseView { + override view = LibroLabLayoutContainerComponent; +} diff --git a/packages/libro-lab/src/layout/main.tsx b/packages/libro-lab/src/layout/main.tsx new file mode 100644 index 00000000..63d79c76 --- /dev/null +++ b/packages/libro-lab/src/layout/main.tsx @@ -0,0 +1,68 @@ +import { singleton, Slot, useInject, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { SplitPanel } from '@difizen/mana-react'; +import { forwardRef } from 'react'; + +import './index.less'; +import { LayoutService } from './layout-service.js'; +import { LibroLabLayoutSlots } from './protocol.js'; + +export const LibroLabLayoutMainComponent = forwardRef( + function LibroLabLayoutMainComponent() { + const layoutService = useInject(LayoutService); + + return ( + + {layoutService.isAreaVisible(LibroLabLayoutSlots.navigator) && ( + + + + )} + + + {layoutService.isAreaVisible(LibroLabLayoutSlots.content) && ( + + + + )} + {layoutService.isAreaVisible(LibroLabLayoutSlots.contentBottom) && ( + + + + )} + + + + ); + }, +); + +@singleton() +@view('libro-lab-layout-main') +export class LibroLabLayoutMainView extends BaseView { + override view = LibroLabLayoutMainComponent; +} diff --git a/packages/libro-lab/src/layout/module.ts b/packages/libro-lab/src/layout/module.ts new file mode 100644 index 00000000..5c2a4ff3 --- /dev/null +++ b/packages/libro-lab/src/layout/module.ts @@ -0,0 +1,62 @@ +import { + createSlotPreference, + FlexSlotView, + HeaderArea, + HeaderView, + ManaModule, +} from '@difizen/mana-app'; + +import { BrandView } from './brand/index.js'; +import { LibroLabLayoutContainerView } from './container.js'; +import { EditorTabView } from './editor-tab-view.js'; +import { LibroLabCurrentFileFooterView } from './footer/current-file-footer-view.js'; +import { FooterArea, LibroLabLayoutFooterView } from './footer/footer-view.js'; +import { LayoutService } from './layout-service.js'; +import { LibroLabLayoutView } from './layout.js'; +import { LibroLabLayoutMainView } from './main.js'; +import { LibroLabLayoutSlots } from './protocol.js'; + +export const LibroLabLayoutModule = ManaModule.create('LibroLabLayoutModule').register( + LibroLabLayoutView, + LibroLabLayoutContainerView, + LibroLabLayoutMainView, + BrandView, + EditorTabView, + LibroLabLayoutFooterView, + LibroLabCurrentFileFooterView, + LayoutService, + createSlotPreference({ + slot: LibroLabLayoutSlots.header, + view: HeaderView, + }), + createSlotPreference({ + slot: HeaderArea.left, + view: BrandView, + }), + createSlotPreference({ + slot: LibroLabLayoutSlots.container, + view: LibroLabLayoutContainerView, + }), + createSlotPreference({ + slot: LibroLabLayoutSlots.main, + view: LibroLabLayoutMainView, + }), + createSlotPreference({ + slot: LibroLabLayoutSlots.footer, + view: LibroLabLayoutFooterView, + }), + createSlotPreference({ + slot: FooterArea.right, + view: FlexSlotView, + options: { sort: true }, + }), + createSlotPreference({ + slot: FooterArea.left, + view: FlexSlotView, + options: { sort: true }, + }), + createSlotPreference({ + slot: FooterArea.left, + view: LibroLabCurrentFileFooterView, + }), +); diff --git a/packages/libro-lab/src/layout/protocol.tsx b/packages/libro-lab/src/layout/protocol.tsx new file mode 100644 index 00000000..d6c408b7 --- /dev/null +++ b/packages/libro-lab/src/layout/protocol.tsx @@ -0,0 +1,12 @@ +export const LibroLabLayoutSlots = { + header: 'libro-lab-header', + container: 'libro-lab-container', + main: 'libro-lab-main', + footer: 'libro-lab-footer', + navigator: 'libro-lab-navigator', + content: 'libro-lab-content', + contentBottom: 'libro-lab-content-bottom', +}; + +export type LibroLabLayoutSlotsType = + (typeof LibroLabLayoutSlots)[keyof typeof LibroLabLayoutSlots]; diff --git a/packages/libro-lab/src/menu/menu-bar-view.tsx b/packages/libro-lab/src/menu/menu-bar-view.tsx new file mode 100644 index 00000000..c4cad4e2 --- /dev/null +++ b/packages/libro-lab/src/menu/menu-bar-view.tsx @@ -0,0 +1,28 @@ +import { MacCommandOutlined } from '@ant-design/icons'; +import { + BaseView, + MAIN_MENU_BAR, + MenuBarRender, + prop, + singleton, + view, +} from '@difizen/mana-app'; +import { forwardRef } from 'react'; + +export const ManaMenubarComponent = forwardRef(function GithubLinkComponent() { + return ; +}); + +@singleton() +@view('MenuBarView') +export class MenuBarView extends BaseView { + override view = ManaMenubarComponent; + @prop() + count = 0; + constructor() { + super(); + this.title.icon = MacCommandOutlined; + this.title.label = '菜单'; + this.id = 'menu-bar'; + } +} diff --git a/packages/libro-lab/src/menu/menu-command.ts b/packages/libro-lab/src/menu/menu-command.ts new file mode 100644 index 00000000..74a77473 --- /dev/null +++ b/packages/libro-lab/src/menu/menu-command.ts @@ -0,0 +1,138 @@ +export const MenuCommands = { + About: { + id: 'libro-lab-header-menu-help-about', + label: '关于', + }, + OpenTerminal: { + id: 'libro-lab-header-menu-terminal-open', + label: '新建终端', + }, + Save: { + id: 'libro-lab-header-menu-file-save', + label: '保存', + }, + CreateFile: { + id: 'libro-lab-header-menu-file-create', + label: '新建文件', + }, + RedoCellAction: { + id: 'libro-lab-header-menu-edit-redo-cell-action', + label: `恢复单元格操作`, + }, + UndoCellAction: { + id: 'libro-lab-header-menu-edit-undo-cell-action', + label: `撤销单元格操作`, + }, + CutCell: { + id: 'libro-lab-header-menu-edit-cut-cell', + label: `剪切单元格`, + }, + CopyCell: { + id: 'libro-lab-header-menu-edit-copy-cell', + label: `复制单元格`, + }, + PasteCellAbove: { + id: 'libro-lab-header-menu-edit-paste-cell-above', + label: `在上方粘贴单元格`, + }, + PasteCellBelow: { + id: 'libro-lab-header-menu-edit-paste-cell-below', + label: `在下方粘贴单元格`, + }, + PasteAndReplaceCell: { + id: 'libro-lab-header-menu-edit-paste-and-replace-cell', + label: `粘贴单元格并替换`, + }, + DeleteCell: { + id: 'libro-lab-header-menu-edit-delete-cell', + label: `删除单元格`, + }, + SelectAll: { + id: 'libro-lab-header-menu-edit-select-all', + label: `选择所有单元格`, + }, + DeselectAll: { + id: 'libro-lab-header-menu-edit-deselect-all', + label: `取消选择所有单元格`, + }, + MoveCellUp: { + id: 'libro-lab-header-menu-edit-move-cell-up', + label: `上移单元格`, + }, + MoveCellDown: { + id: 'libro-lab-header-menu-edit-move-cell-down', + label: `下移单元格`, + }, + SplitCellAntCursor: { + id: 'libro-lab-header-menu-edit-split-cell-at-cursor', + label: `切分单元格`, + }, + MergeCellAbove: { + id: 'libro-lab-header-menu-edit-merge-cell-above', + label: `合并上方单元格`, + }, + MergeCellBelow: { + id: 'libro-lab-header-menu-edit-merge-cell-below', + label: `合并下方单元格`, + }, + MergeCells: { + id: 'libro-lab-header-menu-edit-merge-cells', + label: `合并选中单元格`, + }, + ClearCellOutput: { + id: 'libro-lab-header-menu-edit-clear-cell-outputs', + label: `清空输出`, + }, + ClearAllCellOutput: { + id: 'libro-lab-header-menu-edit-clear-all-cell-outputs', + label: `清空所有输出`, + }, + HideOrShowCellCode: { + id: 'libro-lab-header-menu-view-hide-or-show-cell-code', + label: `隐藏/显示所选单元格代码`, + }, + HideOrShowOutputs: { + id: 'libro-lab-header-menu-view-hide-or-show-outputs', + label: `隐藏/显示所选单元格输出`, + }, + EnableOutputScrolling: { + id: 'libro-lab-header-menu-view-enable-output-scrolling', + label: `固定输出高度`, + }, + DisableOutputScrolling: { + id: 'libro-lab-header-menu-view-disable-output-scrolling', + label: `取消固定输出高度`, + }, + RestartAndRunToSelected: { + id: 'libro-lab-header-menu-run-restart-and-run-to-selected', + label: '重启并执行至选中单元格', + }, + RestartRunAll: { + id: 'libro-lab-header-menu-run-restart-run-all', + label: '重启并执行全部单元格', + }, + RunAllAbove: { + id: 'libro-lab-header-menu-run-all-above', + label: `执行上方所有单元格`, + }, + RunAllBelow: { + id: 'libro-lab-header-menu-run-all-below', + label: `执行下方所有单元格`, + }, + RunAllCells: { + id: 'libro-lab-header-menu-run-all-cells', + label: `执行全部单元格`, + }, + RunCell: { + id: 'libro-lab-header-menu-run-cell', + label: `执行选中单元格`, + }, + RunCellAndInsertBelow: { + id: 'libro-lab-header-menu-run-cell-and-insert-below', + label: `执行选中并向下插入一个单元格`, + }, + RunCellAndSelectNext: { + id: 'libro-lab-header-menu-run-cell-and-select-next', + label: `执行并选中下一个单元格`, + }, +}; diff --git a/packages/libro-lab/src/menu/menu-contribution.ts b/packages/libro-lab/src/menu/menu-contribution.ts new file mode 100644 index 00000000..2edff016 --- /dev/null +++ b/packages/libro-lab/src/menu/menu-contribution.ts @@ -0,0 +1,658 @@ +import type { LibroJupyterModel } from '@difizen/libro-jupyter'; +import { + LibroJupyterView, + LibroService, + NotebookCommands, +} from '@difizen/libro-jupyter'; +import type { MenuRegistry } from '@difizen/mana-app'; +import { + CommandContribution, + CommandRegistry, + inject, + MAIN_MENU_BAR, + MenuContribution, + singleton, +} from '@difizen/mana-app'; + +import { MenuCommands } from './menu-command.js'; + +export namespace HeaderMenus { + export const FILE = [...MAIN_MENU_BAR, '1_file']; + export const EDIT = [...MAIN_MENU_BAR, '2_edit']; + export const VIEW = [...MAIN_MENU_BAR, '3_view']; + export const RUN = [...MAIN_MENU_BAR, '4_run']; + export const TERMINAL = [...MAIN_MENU_BAR, '5_terminal']; + export const HELP = [...MAIN_MENU_BAR, '6_help']; +} + +@singleton({ contrib: [MenuContribution, CommandContribution] }) +export class HeaderMenu implements MenuContribution, CommandContribution { + @inject(CommandRegistry) protected commandRegistry: CommandRegistry; + @inject(LibroService) protected libroService: LibroService; + + registerMenus(menu: MenuRegistry) { + menu.registerSubmenu(HeaderMenus.FILE, { label: '文件' }); + menu.registerSubmenu(HeaderMenus.EDIT, { label: '编辑' }); + menu.registerSubmenu(HeaderMenus.VIEW, { label: '视图' }); + menu.registerSubmenu(HeaderMenus.RUN, { label: '运行' }); + menu.registerSubmenu(HeaderMenus.TERMINAL, { label: '终端' }); + menu.registerSubmenu(HeaderMenus.HELP, { label: '帮助' }); + menu.registerMenuAction(HeaderMenus.TERMINAL, { + id: MenuCommands.OpenTerminal.id, + command: MenuCommands.OpenTerminal.id, + label: MenuCommands.OpenTerminal.label, + }); + menu.registerMenuAction(HeaderMenus.HELP, { + id: MenuCommands.About.id, + command: MenuCommands.About.id, + label: MenuCommands.About.label, + }); + menu.registerMenuAction(HeaderMenus.FILE, { + id: MenuCommands.Save.id, + command: MenuCommands.Save.id, + label: MenuCommands.Save.label, + }); + menu.registerMenuAction(HeaderMenus.FILE, { + id: MenuCommands.CreateFile.id, + command: MenuCommands.CreateFile.id, + label: MenuCommands.CreateFile.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.UndoCellAction.id, + command: MenuCommands.UndoCellAction.id, + label: MenuCommands.UndoCellAction.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.RedoCellAction.id, + command: MenuCommands.RedoCellAction.id, + label: MenuCommands.RedoCellAction.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.CutCell.id, + command: MenuCommands.CutCell.id, + label: MenuCommands.CutCell.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.CopyCell.id, + command: MenuCommands.CopyCell.id, + label: MenuCommands.CopyCell.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.PasteCellBelow.id, + command: MenuCommands.PasteCellBelow.id, + label: MenuCommands.PasteCellBelow.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.PasteCellAbove.id, + command: MenuCommands.PasteCellAbove.id, + label: MenuCommands.PasteCellAbove.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.PasteAndReplaceCell.id, + command: MenuCommands.PasteAndReplaceCell.id, + label: MenuCommands.PasteAndReplaceCell.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.DeleteCell.id, + command: MenuCommands.DeleteCell.id, + label: MenuCommands.DeleteCell.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.SelectAll.id, + command: MenuCommands.SelectAll.id, + label: MenuCommands.SelectAll.label, + }); + // menu.registerMenuAction(HeaderMenus.EDIT, { + // id: MenuCommands.DeselectAll.id, + // command: MenuCommands.DeselectAll.id, + // label: MenuCommands.DeselectAll.label, + // }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.MoveCellUp.id, + command: MenuCommands.MoveCellUp.id, + label: MenuCommands.MoveCellUp.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.MoveCellDown.id, + command: MenuCommands.MoveCellDown.id, + label: MenuCommands.MoveCellDown.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.SplitCellAntCursor.id, + command: MenuCommands.SplitCellAntCursor.id, + label: MenuCommands.SplitCellAntCursor.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.MergeCells.id, + command: MenuCommands.MergeCells.id, + label: MenuCommands.MergeCells.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.MergeCellAbove.id, + command: MenuCommands.MergeCellAbove.id, + label: MenuCommands.MergeCellAbove.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.MergeCellBelow.id, + command: MenuCommands.MergeCellBelow.id, + label: MenuCommands.MergeCellBelow.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.ClearCellOutput.id, + command: MenuCommands.ClearCellOutput.id, + label: MenuCommands.ClearCellOutput.label, + }); + menu.registerMenuAction(HeaderMenus.EDIT, { + id: MenuCommands.ClearAllCellOutput.id, + command: MenuCommands.ClearAllCellOutput.id, + label: MenuCommands.ClearAllCellOutput.label, + }); + menu.registerMenuAction(HeaderMenus.VIEW, { + id: MenuCommands.HideOrShowCellCode.id, + command: MenuCommands.HideOrShowCellCode.id, + label: MenuCommands.HideOrShowCellCode.label, + }); + menu.registerMenuAction(HeaderMenus.VIEW, { + id: MenuCommands.HideOrShowOutputs.id, + command: MenuCommands.HideOrShowOutputs.id, + label: MenuCommands.HideOrShowOutputs.label, + }); + menu.registerMenuAction(HeaderMenus.VIEW, { + id: MenuCommands.EnableOutputScrolling.id, + command: MenuCommands.EnableOutputScrolling.id, + label: MenuCommands.EnableOutputScrolling.label, + }); + menu.registerMenuAction(HeaderMenus.VIEW, { + id: MenuCommands.DisableOutputScrolling.id, + command: MenuCommands.DisableOutputScrolling.id, + label: MenuCommands.DisableOutputScrolling.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunCell.id, + command: MenuCommands.RunCell.id, + label: MenuCommands.RunCell.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunAllAbove.id, + command: MenuCommands.RunAllAbove.id, + label: MenuCommands.RunAllAbove.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunAllBelow.id, + command: MenuCommands.RunAllBelow.id, + label: MenuCommands.RunAllBelow.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunAllCells.id, + command: MenuCommands.RunAllCells.id, + label: MenuCommands.RunAllCells.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunCellAndSelectNext.id, + command: MenuCommands.RunCellAndSelectNext.id, + label: MenuCommands.RunCellAndSelectNext.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RunCellAndInsertBelow.id, + command: MenuCommands.RunCellAndInsertBelow.id, + label: MenuCommands.RunCellAndInsertBelow.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RestartRunAll.id, + command: MenuCommands.RestartRunAll.id, + label: MenuCommands.RestartRunAll.label, + }); + menu.registerMenuAction(HeaderMenus.RUN, { + id: MenuCommands.RestartAndRunToSelected.id, + command: MenuCommands.RestartAndRunToSelected.id, + label: MenuCommands.RestartAndRunToSelected.label, + }); + } + registerCommands(commands: CommandRegistry) { + commands.registerCommand(MenuCommands.OpenTerminal, { + execute: () => { + //TODO: 增加终端 + }, + }); + commands.registerCommand(MenuCommands.About, { + execute: async () => { + //TODO: 关于 + }, + }); + commands.registerCommand(MenuCommands.Save, { + execute: async () => { + //TODO: 保存 + }, + }); + commands.registerCommand(MenuCommands.UndoCellAction, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['UndoCellAction'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.RedoCellAction, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['RedoCellAction'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.CutCell, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['CutCell'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.CopyCell, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['CopyCell'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.DeleteCell, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['DeleteCell'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.PasteCellBelow, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['PasteCellBelow'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.PasteCellAbove, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['PasteCellAbove'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.PasteAndReplaceCell, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['PasteAndReplaceCell'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.SelectAll, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['SelectAll'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + // commands.registerCommand(MenuCommands.DeselectAll, { + // execute: async () => { + // if (this.libroService.active) + // this.commandRegistry.executeCommand( + // NotebookCommands.DeselectAll.id, + // this.libroService.active.activeCell, + // this.libroService.active, + // ); + // }, + // }); + commands.registerCommand(MenuCommands.MoveCellUp, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['MoveCellUp'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.MoveCellDown, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['MoveCellDown'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.SplitCellAntCursor, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['SplitCellAntCursor'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.MergeCells, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['MergeCells'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.MergeCellAbove, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['MergeCellAbove'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.MergeCellBelow, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['MergeCellBelow'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.ClearCellOutput, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['ClearCellOutput'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.ClearAllCellOutput, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['ClearAllCellOutput'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.HideOrShowCellCode, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['HideOrShowCellCode'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.HideOrShowOutputs, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['HideOrShowOutputs'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.EnableOutputScrolling, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['EnableOutputScrolling'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommand(MenuCommands.DisableOutputScrolling, { + execute: async () => { + if (this.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['DisableOutputScrolling'].id, + this.libroService.active.activeCell, + this.libroService.active, + ); + } + }, + }); + commands.registerCommandWithContext(MenuCommands.RunCell, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunCell'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RunAllAbove, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunAllAbove'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RunAllBelow, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunAllBelow'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RunAllCells, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunAllCells'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RunCellAndInsertBelow, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunCellAndInsertBelow'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RunCellAndSelectNext, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RunCellAndSelectNext'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RestartRunAll, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['RestartRunAll'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RestartRunAll, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + ctx.commandRegistry.executeCommand( + NotebookCommands['RestartRunAll'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + commands.registerCommandWithContext(MenuCommands.RestartAndRunToSelected, this, { + execute: async (ctx) => { + if (ctx.libroService.active) { + this.commandRegistry.executeCommand( + NotebookCommands['RestartAndRunToSelected'].id, + ctx.libroService.active.activeCell, + ctx.libroService.active, + ); + } + }, + isEnabled: (ctx) => { + const libro = ctx.libroService.active; + if (!libro || !(libro instanceof LibroJupyterView)) { + return false; + } + return ( + (libro.model as LibroJupyterModel).kernelConnection !== undefined && + (libro.model as LibroJupyterModel).kernelConnecting === false + ); + }, + }); + } +} diff --git a/packages/libro-lab/src/menu/module.ts b/packages/libro-lab/src/menu/module.ts new file mode 100644 index 00000000..c3534dc0 --- /dev/null +++ b/packages/libro-lab/src/menu/module.ts @@ -0,0 +1,13 @@ +import { createSlotPreference, HeaderArea, ManaModule } from '@difizen/mana-app'; + +import { MenuBarView } from './menu-bar-view.js'; +import { HeaderMenu } from './menu-contribution.js'; + +export const LibroLabHeaderMenuModule = ManaModule.create().register( + HeaderMenu, + MenuBarView, + createSlotPreference({ + slot: HeaderArea.middle, + view: MenuBarView, + }), +); diff --git a/packages/libro-lab/src/module.tsx b/packages/libro-lab/src/module.tsx index ca0fce16..e4241136 100644 --- a/packages/libro-lab/src/module.tsx +++ b/packages/libro-lab/src/module.tsx @@ -1,43 +1,88 @@ +import { FileView, LibroJupyterModule } from '@difizen/libro-jupyter'; import { ManaModule, createSlotPreference, RootSlotId, - CardTabView, SideTabView, createViewPreference, - HeaderView, + HeaderArea, } from '@difizen/mana-app'; -import { FileTreeView } from '@difizen/mana-app'; +import { GithubLinkView } from './github-link/index.js'; +import { KernelManagerView } from './kernel-manager/index.js'; import { LibroLabApp } from './lab-app.js'; +import { EditorTabView } from './layout/editor-tab-view.js'; import { + LibroLabLayoutModule, + LibroLabLayoutSlots, LibroLabLayoutView, - LibroLabSlots, - LibroLabContentSlots, } from './layout/index.js'; +import './index.less'; +import { LibroLabHeaderMenuModule } from './menu/module.js'; +import { LibroLabTocModule } from './toc/module.js'; +import { WelcomeView } from './welcome/index.js'; -export const LibroLabModule = ManaModule.create().register( - LibroLabApp, - LibroLabLayoutView, - createSlotPreference({ - view: LibroLabLayoutView, - slot: RootSlotId, - }), - createSlotPreference({ - slot: LibroLabSlots.top, - view: HeaderView, - }), - createSlotPreference({ - view: CardTabView, - slot: LibroLabContentSlots.main, - }), - createSlotPreference({ - view: SideTabView, - slot: LibroLabContentSlots.left, - }), - createViewPreference({ - view: FileTreeView, - slot: LibroLabContentSlots.left, - autoCreate: true, - }), -); +export const LibroLabModule = ManaModule.create() + .register( + LibroLabApp, + LibroLabLayoutView, + GithubLinkView, + createViewPreference({ + view: GithubLinkView, + slot: HeaderArea.right, + openOptions: { + order: 'github', + }, + autoCreate: true, + }), + KernelManagerView, + createViewPreference({ + view: KernelManagerView, + slot: LibroLabLayoutSlots.navigator, + openOptions: { + reveal: false, + order: 'kernel-manager', + }, + autoCreate: true, + }), + createSlotPreference({ + view: LibroLabLayoutView, + slot: RootSlotId, + }), + createSlotPreference({ + view: EditorTabView, + slot: LibroLabLayoutSlots.content, + }), + createSlotPreference({ + view: SideTabView, + slot: LibroLabLayoutSlots.navigator, + options: { + sort: true, + }, + }), + createViewPreference({ + view: FileView, + slot: LibroLabLayoutSlots.navigator, + autoCreate: true, + openOptions: { + reveal: true, + order: 'file-tree', + }, + }), + WelcomeView, + createViewPreference({ + view: WelcomeView, + slot: LibroLabLayoutSlots.content, + autoCreate: true, + openOptions: { + reveal: true, + order: 'welcome', + }, + }), + ) + .dependOn( + LibroJupyterModule, + LibroLabLayoutModule, + LibroLabHeaderMenuModule, + LibroLabTocModule, + ); diff --git a/packages/libro-lab/src/toc/index.less b/packages/libro-lab/src/toc/index.less new file mode 100644 index 00000000..4f8fa4fc --- /dev/null +++ b/packages/libro-lab/src/toc/index.less @@ -0,0 +1,25 @@ +.libro-lab-toc-panel { + height: 100%; + + .markdown-toc-container-title { + display: none; + } + + .mana-tab-side-pane-content { + height: calc(100% - 32px); + } + + .markdown-toc-container { + width: unset; + padding: 12px; + } +} + +.libro-lab-toc-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + margin: unset; +} diff --git a/packages/libro-lab/src/toc/libro-toc-icons.tsx b/packages/libro-lab/src/toc/libro-toc-icons.tsx new file mode 100644 index 00000000..2e2e1772 --- /dev/null +++ b/packages/libro-lab/src/toc/libro-toc-icons.tsx @@ -0,0 +1,35 @@ +export interface IProps { + className?: string; + width?: string; + height?: string; +} +export function TocIcon(props: IProps) { + return ( + + source + + + + + + + + + + + ); +} diff --git a/packages/libro-lab/src/toc/libro-toc-panel-view.tsx b/packages/libro-lab/src/toc/libro-toc-panel-view.tsx new file mode 100644 index 00000000..7ea4bbe7 --- /dev/null +++ b/packages/libro-lab/src/toc/libro-toc-panel-view.tsx @@ -0,0 +1,87 @@ +import { LibroNavigatableView } from '@difizen/libro-jupyter'; +import { TOCView } from '@difizen/libro-toc'; +import { + BaseView, + inject, + prop, + singleton, + useInject, + view, + ViewInstance, + ViewManager, + ViewRender, +} from '@difizen/mana-app'; +import { Empty } from 'antd'; + +import { LayoutService } from '../layout/layout-service.js'; +import { LibroLabLayoutSlots } from '../layout/protocol.js'; + +import { TocIcon } from './libro-toc-icons.js'; +import './index.less'; + +const TocViewRender: React.FC = () => { + const tocPanelView = useInject(ViewInstance); + return ( +
+ {tocPanelView.libroTocView ? ( + + ) : ( + + )} +
+ ); +}; + +export const TocViewFactoryId = 'libro-lab-toc-view'; +@singleton() +@view(TocViewFactoryId) +export class TocPanelView extends BaseView { + override view = TocViewRender; + @inject(ViewManager) protected viewManager: ViewManager; + @inject(LayoutService) protected layoutService: LayoutService; + @prop() libroTocView: TOCView | undefined; + + constructor() { + super(); + this.title.icon = ; + this.title.label = '大纲'; + } + + override onViewMount(): void { + this.handleEditTabChange(); + this.layoutService.onSlotActiveChange( + LibroLabLayoutSlots.content, + this.handleEditTabChange, + ); + } + + get libroNavigatableView() { + const contentView = this.layoutService.getActiveView(LibroLabLayoutSlots.content); + if (contentView instanceof LibroNavigatableView) { + return contentView; + } + return undefined; + } + + handleEditTabChange = () => { + if (!this.libroNavigatableView) { + return; + } + this.viewManager + .getOrCreateView(TOCView, { + id: this.libroNavigatableView.filePath, + }) + .then((libroTocView) => { + this.libroTocView = libroTocView; + this.libroTocView.parent = this.libroNavigatableView?.libroView; + return; + }) + .catch(() => { + // + }); + }; +} diff --git a/packages/libro-lab/src/toc/module.ts b/packages/libro-lab/src/toc/module.ts new file mode 100644 index 00000000..f66eb1e3 --- /dev/null +++ b/packages/libro-lab/src/toc/module.ts @@ -0,0 +1,21 @@ +import { LibroTOCModule } from '@difizen/libro-toc'; +import { createViewPreference, ManaModule } from '@difizen/mana-app'; + +import { LibroLabLayoutSlots } from '../layout/protocol.js'; + +import { TocPanelView } from './libro-toc-panel-view.js'; + +export const LibroLabTocModule = ManaModule.create() + .register( + TocPanelView, + createViewPreference({ + view: TocPanelView, + slot: LibroLabLayoutSlots.navigator, + autoCreate: true, + openOptions: { + reveal: true, + order: 'toc', + }, + }), + ) + .dependOn(LibroTOCModule); diff --git a/packages/libro-lab/src/welcome/index.less b/packages/libro-lab/src/welcome/index.less new file mode 100644 index 00000000..8d698503 --- /dev/null +++ b/packages/libro-lab/src/welcome/index.less @@ -0,0 +1,21 @@ +.libro-lab-welcome-page { + padding: 32px 54px; + height: 100%; + background: white; + + .libro-lab-welcome-page-title { + /* stylelint-disable-next-line declaration-property-value-disallowed-list */ + font-family: PingFangSC; + font-weight: 600; + font-size: 40px; + color: #000a1a; + letter-spacing: 0; + line-height: 56px; + } + + .libro-lab-welcome-page-title-tip { + margin: 16px 0 30px; + font-size: 16px; + color: rgba(0, 10, 26, 78%); + } +} diff --git a/packages/libro-lab/src/welcome/index.tsx b/packages/libro-lab/src/welcome/index.tsx new file mode 100644 index 00000000..efe3f4ea --- /dev/null +++ b/packages/libro-lab/src/welcome/index.tsx @@ -0,0 +1,30 @@ +import { singleton, view } from '@difizen/mana-app'; +import { BaseView } from '@difizen/mana-app'; +import { forwardRef } from 'react'; + +import { WelcomeIcon } from './welcome-icon.js'; + +import './index.less'; + +export const WelcomeComponent = forwardRef(function WelcomeComponent() { + return ( +
+
欢迎使用 Notebook 工作台 🎉🎉
+
+ 👋 你好,服务正在加载中,请稍后开启你的研发之旅吧~ +
+
+ ); +}); + +@singleton() +@view('welcome-view') +export class WelcomeView extends BaseView { + override view = WelcomeComponent; + + constructor() { + super(); + this.title.icon = ; + this.title.label = '欢迎使用'; + } +} diff --git a/packages/libro-lab/src/welcome/welcome-icon.tsx b/packages/libro-lab/src/welcome/welcome-icon.tsx new file mode 100644 index 00000000..347e3dce --- /dev/null +++ b/packages/libro-lab/src/welcome/welcome-icon.tsx @@ -0,0 +1,64 @@ +export interface IProps { + className?: string; +} +export function WelcomeIcon(props: IProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index c27546ce..cd000633 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,7 +16,7 @@ "noFallthroughCasesInSwitch": false, // "noUncheckedIndexedAccess": true, "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": true, "noImplicitOverride": true, "skipLibCheck": true,