From 8c911c40eb5c6e91bb12434356212da6aae51a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Sat, 9 Sep 2023 00:07:33 +0800 Subject: [PATCH 1/8] =?UTF-8?q?perf(jupyter-client):=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E7=9B=91=E5=90=AC=E5=99=A8=20|=20Improve=20e?= =?UTF-8?q?vent=20listener.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index e35a6be..bd56ff2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -385,16 +385,31 @@ export default class JupyterClientPlugin extends siyuan.Plugin { this.eventBus.off("click-editorcontent", this.clickEditorContentEventListener); this.eventBus.off("loaded-protyle", this.loadedProtyleEventListener); + for (const objectURL of this.kernelName2logoObjectURL.values()) { + URL.revokeObjectURL(objectURL); + } + + this.doc2session.clear(); + this.doc2info.clear(); + this.session2docs.clear(); + this.kernelName2logoObjectURL.clear(); + this.kernelName2language.clear(); + this.kernelName2displayName.clear(); + if (this.worker) { this.bridge ?.call("unload") .then(() => { this.bridge?.terminate(); + this.bridge = undefined; + this.worker?.terminate(); + this.worker = undefined; }); } else { this.bridge?.terminate(); + this.bridge = undefined; } } @@ -1584,14 +1599,14 @@ export default class JupyterClientPlugin extends siyuan.Plugin { ); action.ariaLabel = this.i18n.menu.run.label; action.innerHTML = ``; - action.addEventListener("click", async () => { + action.onclick = async e => { const cells = blockDOM2codeCells(block_element.outerHTML, false); await this.requestExecuteCells( cells, session, this.config.jupyter.execute.output.parser, ); - }); + }; action_last.parentElement?.insertBefore(action, action_last); } @@ -1656,7 +1671,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { button.dataset.type = "jupyter-client-notebook-menu"; button.ariaLabel = "Jupyter"; button.innerHTML = ``; - button.addEventListener("click", async e => { + button.onclick = async e => { // this.logger.debug(e); e.preventDefault(); e.stopPropagation(); @@ -1678,7 +1693,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { if (menu_items.length > 0) { const menu = new this.siyuan.Menu(); menu_items.forEach(item => menu.addItem(item)); - + menu.open({ x: e.clientX, y: e.clientY, @@ -1686,7 +1701,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { }); } } - }, true); + }; exit_focus_element.parentElement!.insertBefore( button, From 5f020b6b5881bcf2321aa51a27b14d9d3b673c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:57:16 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix(jupyter-client):=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=B6=88=E6=81=AF=E5=BA=8F=E5=88=97=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E4=B8=8D=E8=A7=84=E8=8C=83=E7=9A=84=E6=83=85=E5=86=B5?= =?UTF-8?q?=20|=20Compatible=20with=20the=20case=20where=20the=20output=20?= =?UTF-8?q?message=20sequence=20is=20not=20standardized.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REF: https://github.com/Zuoqiu-Yingyi/siyuan-plugin-jupyter-client/issues/11 --- src/index.ts | 2 +- src/types/jupyter.d.ts | 1 + src/workers/jupyter.ts | 86 +++++++++++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index bd56ff2..7359282 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1697,7 +1697,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { menu.open({ x: e.clientX, y: e.clientY, - // isLeft: true, + isLeft: true, }); } } diff --git a/src/types/jupyter.d.ts b/src/types/jupyter.d.ts index d6874c0..473594b 100644 --- a/src/types/jupyter.d.ts +++ b/src/types/jupyter.d.ts @@ -47,6 +47,7 @@ export interface IExecuteContext { output: { id: string; // 输出块 ID new: boolean; // 是否为新的输出块 + reply: boolean; // 运行请求是否已回复 attrs: Record; // 输出块 IAL stream: { // 输出流 attrs: { // 输出流显示块 diff --git a/src/workers/jupyter.ts b/src/workers/jupyter.ts index 0953924..fb1015e 100644 --- a/src/workers/jupyter.ts +++ b/src/workers/jupyter.ts @@ -302,6 +302,35 @@ async function updateBlockAttrs(context: IExecuteContext): Promise { }); } +/** + * 插入新块 + * @param context 执行上下文 + * @param nextID 下一个块 ID + * @param data 块内容文本 (kramdown) + * @param dataType 块内容类型 + */ +async function insertBlock( + context: IExecuteContext, + nextID: string, + data: string, + dataType: "markdown" | "dom" = "markdown", +): Promise { + if (context.output.reply) { + await client.appendBlock({ + parentID: context.output.id, + data, + dataType, + }); + } + else { + await client.insertBlock({ + nextID, + data, + dataType, + }); + } +} + export type TExtendedParams = [ Omit[0], "code">?, Parameters[1]?, @@ -343,6 +372,7 @@ async function executeCode( output: { new: true, id: id(), + reply: false, attrs: {}, stream: { attrs: { @@ -576,11 +606,11 @@ async function handleStreamMessage( context.output.stream.initialized = true; context.output.hrs.stream.used = true; - await client.insertBlock({ - nextID: context.output.hrs.stream.id, - data: kramdowns.join("\n"), - dataType: "markdown", - }); + await insertBlock( + context, + context.output.hrs.stream.id, + kramdowns.join("\n"), + ); } } @@ -617,11 +647,11 @@ async function handleErrorMessage( ].join("\n"); context.output.hrs.error.used = true; - await client.insertBlock({ - nextID: context.output.hrs.error.id, - data: kramdown, - dataType: "markdown", - }); + await insertBlock( + context, + context.output.hrs.error.id, + kramdown, + ); } /** @@ -681,11 +711,11 @@ async function handleDisplayDataMessage( } context.output.hrs.display_data.used = true; - await client.insertBlock({ - nextID: context.output.hrs.display_data.id, - data: kramdown, - dataType: "markdown", - }); + await insertBlock( + context, + context.output.hrs.display_data.id, + kramdown, + ); } /** @@ -743,11 +773,11 @@ async function handleExecuteResultMessage( context.output.hrs.execute_result.used = true; for (const kramdown of kramdowns) { - await client.insertBlock({ - nextID: context.output.hrs.execute_result.id, - data: kramdown, - dataType: "markdown", - }); + await insertBlock( + context, + context.output.hrs.execute_result.id, + kramdown, + ); } } @@ -782,11 +812,11 @@ async function handleStdinMessage( : code; context.output.hrs.stream.used = true; - await client.insertBlock({ - nextID: context.output.hrs.stream.id, - data: kramdown, - dataType: "markdown", - }); + await insertBlock( + context, + context.output.hrs.stream.id, + kramdown, + ); future.sendInputReply( { @@ -812,6 +842,8 @@ async function handleExecuteReplyMessage( message: KernelMessage.IExecuteReplyMsg, context: IExecuteContext, ): Promise { + context.output.reply = true; + /* 块运行结束时间 */ context.code.attrs[CONSTANTS.attrs.code.execute_reply] = message.header.date; @@ -851,9 +883,9 @@ async function handleExecuteReplyMessage( } } - context.output.hrs.execute_result.used = true; + context.output.hrs.execute_reply.used = true; await client.insertBlock({ - nextID: context.output.hrs.execute_result.id, + nextID: context.output.hrs.execute_reply.id, data: [ "{{{row", kramdowns.join("\n\n"), From 53f45bb51411306a7b49d3129e723bbab76bca93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Sun, 17 Sep 2023 23:35:29 +0800 Subject: [PATCH 3/8] =?UTF-8?q?perf(typewriter):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=9D=97=E8=B7=9F=E9=9A=8F=E8=A1=8C=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E5=8A=9F=E8=83=BD=20|=20Optimize=20the=20function=20o?= =?UTF-8?q?f=20code=20block=20following=20the=20line=20scrolling.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 7359282..62fb3cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1578,7 +1578,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { /* 为代码块添加运行按钮 */ const block_element = getCurrentBlock(); if (block_element) { // 当前块存在 - if (block_element.dataset.type === sdk.siyuan.NodeType.NodeCodeBlock) { // 当前块为代码块 + if (block_element.dataset.type === sdk.siyuan.NodeType.NodeCodeBlock && block_element.classList.contains("code-block")) { // 当前块为代码块 const session = this.doc2session.get(protyle.block.rootID!); // 当前文档连接的会话 const action_run = block_element.querySelector(`.${CONSTANTS.JUPYTER_CODE_CELL_ACTION_RUN_CLASS_NAME}`); // 代码块运行按钮 if (session // 当前文档已连接会话 @@ -1586,6 +1586,8 @@ export default class JupyterClientPlugin extends siyuan.Plugin { || block_element.querySelector(".protyle-action__language")?.innerText === protyle.background?.ial?.[CONSTANTS.attrs.kernel.language] // 语言与内核语言一致 ) ) { // 可运行的代码块 + // TODO: q请求提示信息 + if (!action_run) { // 若运行按钮不存在, 添加该按钮 const action_last = block_element.querySelector(".protyle-icon--last"); // 最后一个按钮 From ec70aa1428b94eb8294096c01fdd5f68dd9c9e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 18 Sep 2023 03:30:50 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat(jupyter-client):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E5=B8=AE=E5=8A=A9=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=20|=20Add=20context=20help=20panel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/i18n/en_US.json | 3 + public/i18n/zh_CHT.json | 3 + public/i18n/zh_CN.json | 3 + .../icon-jupyter-client-inspect.symbol | 8 + src/components/JupyterDock.svelte | 4 +- src/components/JupyterInspectDock.svelte | 66 +++++++ src/components/Settings.svelte | 1 + src/components/XtermOutputElement.ts | 162 +++++++++++++----- src/configs/default.ts | 5 +- src/index.ts | 124 +++++++++++++- src/types/config.d.ts | 6 +- src/workers/jupyter.ts | 24 +++ 12 files changed, 355 insertions(+), 54 deletions(-) create mode 100644 src/assets/symbols/icon-jupyter-client-inspect.symbol create mode 100644 src/components/JupyterInspectDock.svelte diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 59c52fd..44d59d0 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -44,6 +44,9 @@ }, "title": "Jupyter" }, + "inspectDock": { + "title": "Jupyter Contextual Help" + }, "menu": { "import": { "accelerator": "*.ipynb", diff --git a/public/i18n/zh_CHT.json b/public/i18n/zh_CHT.json index 7bd6b9d..b63c9ef 100644 --- a/public/i18n/zh_CHT.json +++ b/public/i18n/zh_CHT.json @@ -44,6 +44,9 @@ }, "title": "Jupyter" }, + "inspectDock": { + "title": "Jupyter 上下文幫助" + }, "menu": { "import": { "accelerator": "*.ipynb", diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json index 13cd55d..6af8753 100644 --- a/public/i18n/zh_CN.json +++ b/public/i18n/zh_CN.json @@ -44,6 +44,9 @@ }, "title": "Jupyter" }, + "inspectDock": { + "title": "Jupyter 上下文帮助" + }, "menu": { "import": { "accelerator": "*.ipynb", diff --git a/src/assets/symbols/icon-jupyter-client-inspect.symbol b/src/assets/symbols/icon-jupyter-client-inspect.symbol new file mode 100644 index 0000000..c121dcc --- /dev/null +++ b/src/assets/symbols/icon-jupyter-client-inspect.symbol @@ -0,0 +1,8 @@ + + + diff --git a/src/components/JupyterDock.svelte b/src/components/JupyterDock.svelte index 5957712..b5da469 100644 --- a/src/components/JupyterDock.svelte +++ b/src/components/JupyterDock.svelte @@ -37,12 +37,12 @@ import moment from "@workspace/utils/date/moment"; import type { IBar } from "@workspace/components/siyuan/dock/index"; - import type Plugin from "@/index"; + import type JupyterClientPlugin from "@/index"; import { FileTreeNodeType, type IFileTreeFileNode, type IFileTreeFolderNode, type IFileTreeRootNode } from "@workspace/components/siyuan/tree/file"; import type { KernelSpec, Kernel, Session } from "@jupyterlab/services"; import type { WorkerHandlers } from "@/workers/jupyter"; - export let plugin: InstanceType; // 插件对象 + export let plugin: InstanceType; // 插件对象 export let kernelspecs: KernelSpec.ISpecModels = { default: "", kernelspecs: {}, diff --git a/src/components/JupyterInspectDock.svelte b/src/components/JupyterInspectDock.svelte new file mode 100644 index 0000000..b3ae3b2 --- /dev/null +++ b/src/components/JupyterInspectDock.svelte @@ -0,0 +1,66 @@ + + + + + + + + + + diff --git a/src/components/Settings.svelte b/src/components/Settings.svelte index a3ccba0..b5086ae 100644 --- a/src/components/Settings.svelte +++ b/src/components/Settings.svelte @@ -453,6 +453,7 @@ type={ItemType.text} settingKey="fontFamily" settingValue={config.xterm.options.fontFamily} + placeholder="--b3-font-family-code" on:changed={async e => { config.xterm.options.fontFamily = e.detail.value; await updated(); diff --git a/src/components/XtermOutputElement.ts b/src/components/XtermOutputElement.ts index 71d96be..20ad985 100644 --- a/src/components/XtermOutputElement.ts +++ b/src/components/XtermOutputElement.ts @@ -35,6 +35,8 @@ import type { ISiyuanGlobal } from "@workspace/types/siyuan"; declare var globalThis: ISiyuanGlobal; +export type TObservedAttributes = "data-stream"; + export default function (plugin: InstanceType) { return class extends XtermOutputElement { constructor() { @@ -47,6 +49,17 @@ export default function (plugin: InstanceType) { * REF: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components */ export class XtermOutputElement extends HTMLElement { + /** + * REF: https://github.com/mdn/web-components-examples/blob/main/life-cycle-callbacks/main.js + * + * Specify observed attributes so that {@link XtermOutputElement.attributeChangedCallback attributeChangedCallback} will work + */ + static get observedAttributes(): TObservedAttributes[] { + return [ + "data-stream", + ]; + } + public static readonly TAG_NAME = "jupyter-xterm-output"; protected static readonly ELEMENT_ID_STYLE = "style"; protected static readonly ELEMENT_ID_CONTENT = "content"; @@ -62,10 +75,19 @@ export class XtermOutputElement extends HTMLElement { protected preview?: HTMLDivElement | null; // 存放渲染结果的标签 protected data: string = ""; // 输出流的原始内容 - protected terminal?: InstanceType; // xterm 终端实例 + protected _terminal?: InstanceType; // xterm 终端实例 protected fitAddon?: InstanceType; // xterm 终端实例 protected resizeObserver?: InstanceType; // xterm 终端实例 + public get fontSize(): number | undefined { + return parseFloat(globalThis.getComputedStyle(this).getPropertyValue("font-size")) + || globalThis.siyuan?.config?.editor?.fontSize; + } + + public get terminal(): InstanceType | undefined { + return this._terminal; + } + constructor( protected readonly plugin: InstanceType, ) { @@ -79,8 +101,10 @@ export class XtermOutputElement extends HTMLElement { /** * 当 custom element 首次被插入文档 DOM 时调用 + * REF: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_the_lifecycle_callbacks */ connectedCallback(): void { + this.plugin.xtermElements.add(this); this.link = this.querySelector(`link#${XtermOutputElement.ELEMENT_ID_STYLE}`); this.content = this.querySelector(`pre#${XtermOutputElement.ELEMENT_ID_CONTENT}`); this.preview = this.querySelector(`div#${XtermOutputElement.ELEMENT_ID_PREVIEW}`); @@ -94,43 +118,57 @@ export class XtermOutputElement extends HTMLElement { this.shadowRoot.appendChild(this.link); } - if (this.content) { // 存在输出流的内容 + if (this.dataset.stream !== undefined) { // 存在数据流内容 (该组件的属性) + this.data = decode(this.dataset.stream); + } + else if (this.content) { // 存在输出流的内容 (pre 元素属性) this.shadowRoot.appendChild(this.content); this.content.style.display = "none"; // 隐藏文本内容 this.data = decode(this.content.dataset.stream ?? ""); + } - if (this.preview) { // 已渲染 - this.shadowRoot.appendChild(this.preview); - } - else { // 未渲染 - this.preview = document.createElement("div"); - this.preview.id = XtermOutputElement.ELEMENT_ID_PREVIEW; - this.sizeObserver(this.preview); - - this.shadowRoot.appendChild(this.preview); - const theme = isLightTheme() - ? light_theme - : dark_theme - - this.terminal = new Terminal({ - rows: 0, - theme, - fontSize: globalThis.siyuan?.config?.editor?.fontSize, - ...this.plugin.config.xterm.options, - }); - this.fitAddon = new FitAddon(); - - this.terminal.loadAddon(this.fitAddon); - this.terminal.loadAddon(this.customAddon); - - this.terminal.open(this.preview); - this.terminal.write(this.data); + if (this.preview) { // 已渲染 + this.shadowRoot.appendChild(this.preview); + } + else { // 未渲染 + this.preview = document.createElement("div"); + this.preview.id = XtermOutputElement.ELEMENT_ID_PREVIEW; + this.sizeObserver(this.preview); + + this.shadowRoot.appendChild(this.preview); + const theme = isLightTheme() + ? light_theme + : dark_theme + + this._terminal = new Terminal({ + rows: 0, + theme, + fontSize: this.fontSize, + ...this.plugin.config.xterm.options, + }); + this.fitAddon = new FitAddon(); - } + this._terminal.loadAddon(this.fitAddon); + this._terminal.loadAddon(this.customAddon); + + this._terminal.open(this.preview); + this._terminal.write(this.data, this.fit); } } + protected async write(data: Parameters[0]): Promise { + return new Promise(resolve => { + this._terminal?.write(data, resolve); + }); + } + + protected async writeln(data: Parameters[0]): Promise { + return new Promise(resolve => { + this._terminal?.writeln(data, resolve); + }); + } + protected readonly customAddon = { activate: (terminal: Terminal) => { // TODO: 注册一个渲染完成后保存功能 @@ -174,32 +212,41 @@ export class XtermOutputElement extends HTMLElement { }, dispose() { }, fit: () => { - // this.plugin.logger.debug(this.terminal.buffer); - this.terminal?.resize( - this.terminal.cols, - this.terminal.buffer.active.baseY - + this.terminal.buffer.active.cursorY + // this.plugin.logger.debug(this._terminal.buffer); + this._terminal?.resize( + this._terminal.cols, + this._terminal.buffer.active.baseY + + this._terminal.buffer.active.cursorY + 1, ); } }; + protected readonly fit = () => { + try { + if (this._terminal) { + if (this._terminal.options.fontSize !== this.fontSize) this._terminal.options.fontSize = this.fontSize; + } + + // ! 错误无法被捕获 + this.fitAddon?.fit(); + this.customAddon?.fit(); + } + catch (error) { + this.plugin.logger.warn(error); + } + } + protected sizeObserver(element: HTMLElement): void { /** * 监听元素尺寸变化 * REF: https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver */ this.resizeObserver ??= new ResizeObserver( - deshake(() => { // 消除抖动 - try { - // ! 错误无法被捕获 - this.fitAddon?.fit(); - this.customAddon?.fit(); - } - catch (error) { - this.plugin.logger.warn(error); - } - }, 125), + deshake( // 消除抖动 + this.fit, + 125, + ), ); this.resizeObserver.observe( element, @@ -213,8 +260,9 @@ export class XtermOutputElement extends HTMLElement { * 当 custom element 从文档 DOM 中删除时调用 */ disconnectedCallback(): void { + this.plugin.xtermElements.delete(this); this.resizeObserver?.disconnect(); - this.terminal?.dispose(); + this._terminal?.dispose(); } /** @@ -226,8 +274,30 @@ export class XtermOutputElement extends HTMLElement { /** * 当 custom element 增加、删除、修改自身属性时调用 + * + * 需要设置 {@link XtermOutputElement.observedAttributes observedAttributes} */ - attributeChangedCallback(): void { + async attributeChangedCallback( + name: TObservedAttributes, + _oldValue: string, + newValue: string, + ): Promise { + // this.plugin.logger.debug(arguments); + + switch (name) { + case "data-stream": + if (this._terminal) { + this.data = decode(newValue); + + await this.write("\n"); + this._terminal.clear(); + this._terminal.write(this.data, this.fit); + } + break; + + default: + break; + } } } diff --git a/src/configs/default.ts b/src/configs/default.ts index 4335d1d..da2a3a1 100644 --- a/src/configs/default.ts +++ b/src/configs/default.ts @@ -48,6 +48,9 @@ export const DEFAULT_CONFIG: IConfig = { }, }, }, + inspect: { + detail_level: 1, + }, import: { parser: { xterm: false, // ⚙ @@ -62,7 +65,7 @@ export const DEFAULT_CONFIG: IConfig = { disableStdin: true, convertEol: true, logLevel: "off", - fontFamily: "JetBrainsMono-Regular", // ⚙ + fontFamily: "", // ⚙ }, }, }; diff --git a/src/index.ts b/src/index.ts index 62fb3cd..e9f0a93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import manifest from "~/public/plugin.json"; import "./index.less"; import icon_jupyter_client from "./assets/symbols/icon-jupyter-client.symbol?raw"; +import icon_jupyter_client_inspect from "./assets/symbols/icon-jupyter-client-inspect.symbol?raw"; import icon_jupyter_client_text from "./assets/symbols/icon-jupyter-client-text.symbol?raw"; import icon_jupyter_client_simple from "./assets/symbols/icon-jupyter-client-simple.symbol?raw"; import icon_jupyter_client_terminal from "./assets/symbols/icon-jupyter-client-terminal.symbol?raw"; @@ -48,6 +49,7 @@ import * as sdk from "@siyuan-community/siyuan-sdk"; import Item from "@workspace/components/siyuan/menu/Item.svelte" import Settings from "./components/Settings.svelte"; import JupyterDock from "./components/JupyterDock.svelte"; +import JupyterInspectDock from "./components/JupyterInspectDock.svelte"; import SessionManager from "./components/SessionManager.svelte"; import XtermOutputElement from "./components/XtermOutputElement"; import { asyncPrompt } from "@workspace/components/siyuan/dialog/prompt"; @@ -65,6 +67,8 @@ import { isSiyuanBlock, isSiyuanDocument, isSiyuanDocumentTitle, + type ICodeBlockCursorPosition, + getCodeBlockCursorPosition, } from "@workspace/utils/siyuan/dom"; import { Logger } from "@workspace/utils/logger"; import { fn__code } from "@workspace/utils/siyuan/text/span"; @@ -73,6 +77,7 @@ import { WorkerBridgeMaster } from "@workspace/utils/worker/bridge/master"; import { sleep } from "@workspace/utils/misc/sleep"; import { Counter } from "@workspace/utils/misc/iterator"; import { toUint8Array } from "@workspace/utils/misc/base64"; +import { encode } from "@workspace/utils/misc/base64"; import uuid from "@workspace/utils/misc/uuid"; import CONSTANTS from "./constants"; @@ -107,6 +112,7 @@ import type { import type { THandlersWrapper } from "@workspace/utils/worker/bridge"; import type { WorkerHandlers } from "./workers/jupyter"; import type { ComponentEvents } from "svelte"; +import type xterm from "xterm"; declare var globalThis: ISiyuanGlobal; export type PluginHandlers = THandlersWrapper; @@ -132,12 +138,17 @@ export default class JupyterClientPlugin extends siyuan.Plugin { public bridge?: InstanceType; // worker 桥 protected jupyterDock!: { - // editor: InstanceType, dock: ReturnType, model?: siyuan.IDockModel, component?: InstanceType, }; // Jupyter 管理面板 + protected jupyterInspectDock!: { + dock: ReturnType, + model?: siyuan.IDockModel, + component?: InstanceType, + }; // Jupyter 上下文帮助面板 + public readonly doc2session = new Map(); // 文档 ID 到会话的映射 public readonly doc2info = new Map(); // 文档 ID 到文档信息的映射 public readonly session2docs = new Map>(); // 会话 ID 到文档 ID 集合的映射 @@ -148,6 +159,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { public readonly kernelName2logoObjectURL = new Map(); // 内核名称 -> object URL public readonly kernelName2language = new Map(); // 内核名称 -> 内核语言名称 public readonly kernelName2displayName = new Map(); // 内核名称 -> 内核显示名称 + public readonly xtermElements = new Set>>(); // xterm 组件集合 public readonly counter = Counter(); public readonly username = `siyuan-${siyuan.getBackend()}-${siyuan.getFrontend()}`; // 用户名 public readonly clientId = globalThis.Lute.NewNodeID(); // 客户端 ID @@ -208,6 +220,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { /* 注册图标 */ this.addIcons([ icon_jupyter_client, + icon_jupyter_client_inspect, icon_jupyter_client_text, icon_jupyter_client_simple, icon_jupyter_client_terminal, @@ -264,6 +277,40 @@ export default class JupyterClientPlugin extends siyuan.Plugin { }, }), }; + this.jupyterInspectDock = { + dock: this.addDock({ + config: { + position: "BottomLeft", + size: { width: 256, height: 0 }, + icon: "icon-jupyter-client-inspect", + title: this.i18n.inspectDock.title, + show: true, + }, + data: { + stream: "", + }, + type: "-inspect-dock", + init() { + // plugin.logger.debug(this); + + this.element.classList.add("fn__flex-column"); + const dock = new JupyterInspectDock({ + target: this.element, + props: { + plugin, + ...this.data, + }, + }); + plugin.jupyterInspectDock.model = this; + plugin.jupyterInspectDock.component = dock; + }, + destroy() { + plugin.jupyterInspectDock.component?.$destroy(); + delete plugin.jupyterInspectDock.component; + delete plugin.jupyterInspectDock.model; + }, + }), + }; /** * 注册快捷键命令 @@ -457,6 +504,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { if (config && config !== this.config) { this.config = config; } + this.updateXtermElement(); await this.updateWorkerConfig(restart); await this.saveData(JupyterClientPlugin.GLOBAL_CONFIG_NAME, this.config); } @@ -556,6 +604,21 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } } + /* 更新 xterm 组件配置 */ + public updateXtermElement(options: xterm.ITerminalOptions = this.config.xterm.options): void { + for (const element of this.xtermElements) { + if (element.terminal) { + for (const [key, value] of Object.entries(options)) { + // @ts-ignore + if (element.terminal.options[key] !== value) { + // @ts-ignore + element.terminal.options[key] = value; + } + } + } + } + } + /** * jupyter 请求 * @param pathname 请求路径 @@ -1264,6 +1327,55 @@ export default class JupyterClientPlugin extends siyuan.Plugin { return submenu; } + /** + * 请求上下文参考 + * @param sessionID 会话 ID + * @param position 光标位置 + */ + public async requestInspect( + sessionID: string, + position: ICodeBlockCursorPosition, + ): Promise { + const message = await this.bridge?.call( + "jupyter.session.kernel.connection.requestInspect", + this.clientId, + sessionID, + { + code: position.code, + cursor_pos: position.offset, + detail_level: this.config.jupyter.inspect.detail_level, + }, + ); + // this.logger.debug(message); + + if (message) { + switch (message.content.status) { + case "ok": + if (message.content.found) { // 查询到上下文帮助信息 + const text = message.content.data["text/plain"]; + if (text !== undefined) { + const stream = encode( + Array.isArray(text) + ? text.join() + : text as string, + true, + ); + this.jupyterInspectDock.component?.$set({ stream }); + } + else { + this.logger.warn(message); + } + } + break; + case "abort": + this.logger.info(message); + case "error": + this.logger.warn(message); + break; + } + } + } + /** * 请求运行代码块 * @param code 代码 @@ -1586,7 +1698,15 @@ export default class JupyterClientPlugin extends siyuan.Plugin { || block_element.querySelector(".protyle-action__language")?.innerText === protyle.background?.ial?.[CONSTANTS.attrs.kernel.language] // 语言与内核语言一致 ) ) { // 可运行的代码块 - // TODO: q请求提示信息 + /* 请求上下文帮助 */ + const position = getCodeBlockCursorPosition(); + // this.logger.debug(position); + if (position) { + this.requestInspect( + session.id, + position, + ); + } if (!action_run) { // 若运行按钮不存在, 添加该按钮 const action_last = block_element.querySelector(".protyle-icon--last"); // 最后一个按钮 diff --git a/src/types/config.d.ts b/src/types/config.d.ts index f91adcd..47622d0 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -55,14 +55,14 @@ export interface IJupyterInput { goto: boolean; // 输入请求时是否跳转到对应的代码块 } - -export interface IJupyterImport { - parser: IJupyterParserOptions; +export interface IJupyterInspect { + detail_level: KernelMessage.IInspectRequestMsg["content"]["detail_level"]; } export interface IJupyter { server: IJupyterServer; execute: IJupyterExecute; + inspect: IJupyterInspect; import: IJupyterImport; } diff --git a/src/workers/jupyter.ts b/src/workers/jupyter.ts index fb1015e..d405c9c 100644 --- a/src/workers/jupyter.ts +++ b/src/workers/jupyter.ts @@ -1287,6 +1287,30 @@ const handlers = { return connection?.model; }, }, + "jupyter.session.kernel.connection.requestComplete": { // 请求自动补全 + this: self, + async func( + clientID: string, // 客户端 ID + sessionID: string, // 会话 ID + content: KernelMessage.ICompleteRequestMsg["content"], // 请求内容 + ): Promise { + const connection = id_2_session_connection.get(sessionID); + const message = await connection?.kernel?.requestComplete(content); + return message; + }, + }, + "jupyter.session.kernel.connection.requestInspect": { // 请求上下文参考 + this: self, + async func( + clientID: string, // 客户端 ID + sessionID: string, // 会话 ID + content: KernelMessage.IInspectRequestMsg["content"], // 请求内容 + ): Promise { + const connection = id_2_session_connection.get(sessionID); + const message = await connection?.kernel?.requestInspect(content); + return message; + }, + }, importIpynb: { this: self, func: importIpynb, From 7f0875489210cf4de90e2932da83712211f0e2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 18 Sep 2023 05:36:53 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat(jupyter-client):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E4=BA=8B=E4=BB=B6=E7=9B=91=E5=90=AC=E5=99=A8?= =?UTF-8?q?=20|=20Add=20edit=20event=20listener.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/configs/default.ts | 3 + src/index.ts | 148 +++++++++++++++++++++++++++++++++++++++-- src/types/config.d.ts | 9 +++ src/utils/cell.ts | 16 ++--- src/workers/jupyter.ts | 2 - 5 files changed, 161 insertions(+), 17 deletions(-) diff --git a/src/configs/default.ts b/src/configs/default.ts index da2a3a1..5b9ec1d 100644 --- a/src/configs/default.ts +++ b/src/configs/default.ts @@ -58,6 +58,9 @@ export const DEFAULT_CONFIG: IConfig = { cntrl: true, // ⚙ }, }, + edit: { + delay: 125, + }, }, xterm: { options: { diff --git a/src/index.ts b/src/index.ts index e9f0a93..c084bce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -107,12 +107,15 @@ import type { IClickBlockIconEvent, IClickEditorContentEvent, IClickEditorTitleIconEvent, + IDestroyProtyleEvent, ILoadedProtyleEvent, } from "@workspace/types/siyuan/events"; import type { THandlersWrapper } from "@workspace/utils/worker/bridge"; import type { WorkerHandlers } from "./workers/jupyter"; import type { ComponentEvents } from "svelte"; import type xterm from "xterm"; +import type { IProtyle } from "siyuan/types/protyle"; +import { deshake } from "@workspace/utils/misc/deshake"; declare var globalThis: ISiyuanGlobal; export type PluginHandlers = THandlersWrapper; @@ -137,6 +140,9 @@ export default class JupyterClientPlugin extends siyuan.Plugin { protected worker?: InstanceType; // worker public bridge?: InstanceType; // worker 桥 + protected editEventHandler!: ReturnType Promise>>; + protected readonly protyles = new WeakMap>(); // 已监听的编辑器对象 + protected jupyterDock!: { dock: ReturnType, model?: siyuan.IDockModel, @@ -212,6 +218,9 @@ export default class JupyterClientPlugin extends siyuan.Plugin { XtermOutputElementWrap.TAG_NAME, XtermOutputElementWrap, ); + + /* 初始化编辑事件处理函数 */ + this.updateEditEventHandler(); } onload(): void { @@ -420,6 +429,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { this.eventBus.on("click-blockicon", this.blockMenuEventListener); this.eventBus.on("click-editorcontent", this.clickEditorContentEventListener); this.eventBus.on("loaded-protyle", this.loadedProtyleEventListener); + this.eventBus.off("destroy-protyle", this.destroyProtyleEventListener); }); } @@ -431,6 +441,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { this.eventBus.off("click-blockicon", this.blockMenuEventListener); this.eventBus.off("click-editorcontent", this.clickEditorContentEventListener); this.eventBus.off("loaded-protyle", this.loadedProtyleEventListener); + this.eventBus.off("destroy-protyle", this.destroyProtyleEventListener); for (const objectURL of this.kernelName2logoObjectURL.values()) { URL.revokeObjectURL(objectURL); @@ -505,6 +516,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { this.config = config; } this.updateXtermElement(); + this.updateEditEventHandler(); await this.updateWorkerConfig(restart); await this.saveData(JupyterClientPlugin.GLOBAL_CONFIG_NAME, this.config); } @@ -619,6 +631,32 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } } + + /* 更新编辑事件处理函数 */ + public updateEditEventHandler(delay: number = this.config.jupyter.edit.delay): void { + this.editEventHandler = deshake(async (protyle: IProtyle) => { + const session = this.doc2session.get(protyle.block.rootID!); + if (session) { // 当前文档已连接会话 + const block = getCurrentBlock(); + if (isCodeCell(block)) { // 当前块是代码单元格 + const position = getCodeBlockCursorPosition(); + if (position) { // 成功获取光标位置 + await Promise.allSettled([ + this.requestInspect( // 更新上下文帮助 + session.id, + position, + ), + this.requestComplete( // 自动补全 + session.id, + position, + ), + ]); + } + } + } + }, delay); + } + /** * jupyter 请求 * @param pathname 请求路径 @@ -1328,17 +1366,17 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } /** - * 请求上下文参考 + * 请求上下文帮助 * @param sessionID 会话 ID * @param position 光标位置 + * @returns 是否成功获取 */ - public async requestInspect( + protected async requestInspect( sessionID: string, position: ICodeBlockCursorPosition, - ): Promise { + ): Promise { const message = await this.bridge?.call( "jupyter.session.kernel.connection.requestInspect", - this.clientId, sessionID, { code: position.code, @@ -1361,6 +1399,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { true, ); this.jupyterInspectDock.component?.$set({ stream }); + return true; } else { this.logger.warn(message); @@ -1374,6 +1413,42 @@ export default class JupyterClientPlugin extends siyuan.Plugin { break; } } + return false; + } + + /** + * 请求上下文自动补全 + * @param sessionID 会话 ID + * @param position 光标位置 + * @returns 是否成功获取 + */ + protected async requestComplete( + sessionID: string, + position: ICodeBlockCursorPosition, + ): Promise { + const message = await this.bridge?.call( + "jupyter.session.kernel.connection.requestComplete", + sessionID, + { + code: position.code, + cursor_pos: position.offset, + }, + ); + // this.logger.debug(message); + + if (message) { + switch (message.content.status) { + case "ok": + // TODO: 使用菜单实现自动补全 + break; + case "abort": + this.logger.info(message); + case "error": + this.logger.warn(message); + break; + } + } + return false; } /** @@ -1640,6 +1715,47 @@ export default class JupyterClientPlugin extends siyuan.Plugin { }); } + /** + * 切换监听器 + * @param protyle 编辑器 + * @param enable 是否启用编辑事件监听 + */ + protected toggleEditEventListener( + protyle: IProtyle, + enable: boolean, + ): void { + + if (enable) { + if (!this.protyles.has(protyle)) { // 未加入监听的编辑器 + const listener = [ + "keyup", + e => this.editEventListener(e, protyle), + { + capture: true, + }, + ] as Parameters; + this.protyles.set(protyle, listener); + protyle.wysiwyg?.element?.addEventListener(...listener); + } + } + else { + const listener = this.protyles.get(protyle); + if (listener) { // 已加入监听的编辑器 + protyle.wysiwyg?.element?.removeEventListener(...listener); + } + } + } + + /* 编辑事件监听 */ + protected readonly editEventListener = ( + e: Event, + protyle: IProtyle, + ) => { + // this.logger.debugs(e, protyle); + + this.editEventHandler(protyle); + } + /* 块菜单菜单弹出事件监听器 */ protected readonly blockMenuEventListener = (e: IClickBlockIconEvent | IClickEditorTitleIconEvent) => { // this.logger.debug(e); @@ -1687,14 +1803,21 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } } + const session = this.doc2session.get(protyle.block.rootID!) // 当前文档连接的会话 + if (session) { // 当前文档已连接会话 + this.toggleEditEventListener(protyle, true); // 启用编辑事件监听 + } + else { // 当前文档未连接会话 + this.toggleEditEventListener(protyle, false); // 禁用编辑事件监听 + } + /* 为代码块添加运行按钮 */ const block_element = getCurrentBlock(); if (block_element) { // 当前块存在 if (block_element.dataset.type === sdk.siyuan.NodeType.NodeCodeBlock && block_element.classList.contains("code-block")) { // 当前块为代码块 - const session = this.doc2session.get(protyle.block.rootID!); // 当前文档连接的会话 const action_run = block_element.querySelector(`.${CONSTANTS.JUPYTER_CODE_CELL_ACTION_RUN_CLASS_NAME}`); // 代码块运行按钮 if (session // 当前文档已连接会话 - && (block_element.getAttribute(CONSTANTS.attrs.code.type.key) === CONSTANTS.attrs.code.type.value // 代码单元格 + && (isCodeCell(block_element) // 代码单元格 || block_element.querySelector(".protyle-action__language")?.innerText === protyle.background?.ial?.[CONSTANTS.attrs.kernel.language] // 语言与内核语言一致 ) ) { // 可运行的代码块 @@ -1751,7 +1874,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { const protyle = e.detail; /* 更新内核状态 */ - if (!this.doc2session.has(protyle.block.rootID!)) { + if (!this.doc2session.has(protyle.block.rootID!)) { // 当前文档未连接会话 const attrs: Record = {}; if (protyle.background?.ial?.[CONSTANTS.attrs.kernel.connection_status] && protyle.background.ial[CONSTANTS.attrs.kernel.connection_status] !== "disconnected" @@ -1770,6 +1893,9 @@ export default class JupyterClientPlugin extends siyuan.Plugin { }); } } + else { // 当前文档已连接会话 + this.toggleEditEventListener(protyle, true); // 启用编辑事件监听 + } /* 在面包屑栏右侧添加按钮 */ if (protyle.title?.editElement?.innerText.endsWith(".ipynb") // 文档名以 `.ipynb` 结尾 @@ -1834,6 +1960,14 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } } + /* 编辑器关闭事件监听器 */ + protected readonly destroyProtyleEventListener = (e: IDestroyProtyleEvent) => { + // this.logger.debug(e); + + const protyle = e.detail.protyle; + this.toggleEditEventListener(protyle, false); // 禁用编辑事件监听 + } + /** * 请求输入 * @param blockID 块 ID diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 47622d0..22c3cb6 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -59,11 +59,20 @@ export interface IJupyterInspect { detail_level: KernelMessage.IInspectRequestMsg["content"]["detail_level"]; } +export interface IJupyterComplete { +} + +export interface IJupyterEdit { + delay: number; // 上下文帮助/自动补全 延时时间 +} + export interface IJupyter { server: IJupyterServer; execute: IJupyterExecute; inspect: IJupyterInspect; + complete: IJupyterComplete; import: IJupyterImport; + edit: IJupyterEdit; } export interface IXterm { diff --git a/src/utils/cell.ts b/src/utils/cell.ts index 827fccf..2008f2c 100644 --- a/src/utils/cell.ts +++ b/src/utils/cell.ts @@ -123,7 +123,10 @@ export function buildNewCodeCell( */ export function isCodeCell(element: any): boolean { return isSiyuanBlock(element) - && element?.getAttribute(CONSTANTS.attrs.code.type.key) === CONSTANTS.attrs.code.type.value; + && element instanceof HTMLElement + && element.getAttribute(CONSTANTS.attrs.code.type.key) === CONSTANTS.attrs.code.type.value + && element.dataset.type === "NodeCodeBlock" + && element.classList.contains("code-block"); } /** @@ -133,7 +136,9 @@ export function isCodeCell(element: any): boolean { */ export function isOutputCell(element: any): boolean { return isSiyuanBlock(element) - && element?.getAttribute(CONSTANTS.attrs.output.type.key) === CONSTANTS.attrs.output.type.value; + && element instanceof HTMLElement + && element.getAttribute(CONSTANTS.attrs.output.type.key) === CONSTANTS.attrs.output.type.value + && element.dataset.type === "NodeSuperBlock"; } /** @@ -142,10 +147,5 @@ export function isOutputCell(element: any): boolean { * @returns 是否为单元格元素 */ export function isCell(element: any): boolean { - return isSiyuanBlock(element) - && ( - element?.getAttribute(CONSTANTS.attrs.code.type.key) === CONSTANTS.attrs.code.type.value - || - element?.getAttribute(CONSTANTS.attrs.output.type.key) === CONSTANTS.attrs.output.type.value - ); + return isCodeCell(element) || isOutputCell(element); } diff --git a/src/workers/jupyter.ts b/src/workers/jupyter.ts index d405c9c..63adf6e 100644 --- a/src/workers/jupyter.ts +++ b/src/workers/jupyter.ts @@ -1290,7 +1290,6 @@ const handlers = { "jupyter.session.kernel.connection.requestComplete": { // 请求自动补全 this: self, async func( - clientID: string, // 客户端 ID sessionID: string, // 会话 ID content: KernelMessage.ICompleteRequestMsg["content"], // 请求内容 ): Promise { @@ -1302,7 +1301,6 @@ const handlers = { "jupyter.session.kernel.connection.requestInspect": { // 请求上下文参考 this: self, async func( - clientID: string, // 客户端 ID sessionID: string, // 会话 ID content: KernelMessage.IInspectRequestMsg["content"], // 请求内容 ): Promise { From e29362540c1b8d0b5c386467f36b8e7fb12b11fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:22:47 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat(jupyter-client):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=A8=E5=8A=9F=E8=83=BD=20|=20Im?= =?UTF-8?q?plement=20auto-completion=20function.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + .../symbols/icon-jupyter-client-simple.symbol | 40 +--- src/configs/default.ts | 2 + src/index.ts | 212 +++++++++++++++++- src/jupyter/icon.ts | 131 +++++++++++ 5 files changed, 340 insertions(+), 47 deletions(-) create mode 100644 src/jupyter/icon.ts diff --git a/package.json b/package.json index 5d3ee2b..5d1f4bc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { + "@jupyter-lsp/theme-vscode": "3.0.0-rc.0", "@jupyterlab/services": "^7.0.5", + "@jupyterlab/ui-components": "^4.0.6", "@sveltejs/vite-plugin-svelte": "^2.4.5", "@tsconfig/svelte": "^5.0.2", "less": "^4.2.0", diff --git a/src/assets/symbols/icon-jupyter-client-simple.symbol b/src/assets/symbols/icon-jupyter-client-simple.symbol index 38f17c1..fd58444 100644 --- a/src/assets/symbols/icon-jupyter-client-simple.symbol +++ b/src/assets/symbols/icon-jupyter-client-simple.symbol @@ -4,40 +4,8 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > - - logo - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/configs/default.ts b/src/configs/default.ts index 5b9ec1d..05cda37 100644 --- a/src/configs/default.ts +++ b/src/configs/default.ts @@ -51,6 +51,8 @@ export const DEFAULT_CONFIG: IConfig = { inspect: { detail_level: 1, }, + complete: { + }, import: { parser: { xterm: false, // ⚙ diff --git a/src/index.ts b/src/index.ts index c084bce..f8e0301 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,11 +78,17 @@ import { sleep } from "@workspace/utils/misc/sleep"; import { Counter } from "@workspace/utils/misc/iterator"; import { toUint8Array } from "@workspace/utils/misc/base64"; import { encode } from "@workspace/utils/misc/base64"; +import { select } from "@workspace/utils/dom/selection"; +import { replaceRangeWithText } from "@workspace/utils/dom/range"; import uuid from "@workspace/utils/misc/uuid"; import CONSTANTS from "./constants"; import { DEFAULT_SETTINGS } from "./jupyter/settings"; import { DEFAULT_CONFIG } from "./configs/default"; +import { + LIGHT_ICON_MAP, + DARK_ICON_MAP, +} from "./jupyter/icon"; import { blockDOM2codeCells, buildNewCodeCell, @@ -116,6 +122,10 @@ import type { ComponentEvents } from "svelte"; import type xterm from "xterm"; import type { IProtyle } from "siyuan/types/protyle"; import { deshake } from "@workspace/utils/misc/deshake"; +import { isLightTheme } from "@workspace/utils/siyuan/theme"; +import { isMatchedKeyboardEvent } from "@workspace/utils/shortcut/match"; +import type { IKeyboardStatus } from "@workspace/utils/shortcut"; +import { escapeHTML } from "@workspace/utils/misc/html"; declare var globalThis: ISiyuanGlobal; export type PluginHandlers = THandlersWrapper; @@ -127,6 +137,22 @@ export type TMenuContext = IBlockMenuContext | { export default class JupyterClientPlugin extends siyuan.Plugin { static readonly GLOBAL_CONFIG_NAME = "global-config"; + static readonly EDIT_KEYBOARD_EVENT_STATUS: IKeyboardStatus = { + type: "keyup", + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + key: (key: string) => !/^(Escape)$/.test(key), + } as const; + static readonly EDIT_KEYBOARD_EVENT_COMPLATING_STATUS: IKeyboardStatus = { + type: "keyup", + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + key: (key: string) => /^(\S|Tab|Delete|Backspace)$/.test(key), + } as const; declare public readonly i18n: I18N; @@ -139,6 +165,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { public config: IConfig = DEFAULT_CONFIG; protected worker?: InstanceType; // worker public bridge?: InstanceType; // worker 桥 + protected complating: boolean = false; // 菜单已打开 protected editEventHandler!: ReturnType Promise>>; protected readonly protyles = new WeakMap>(); // 已监听的编辑器对象 @@ -631,7 +658,6 @@ export default class JupyterClientPlugin extends siyuan.Plugin { } } - /* 更新编辑事件处理函数 */ public updateEditEventHandler(delay: number = this.config.jupyter.edit.delay): void { this.editEventHandler = deshake(async (protyle: IProtyle) => { @@ -1426,21 +1452,154 @@ export default class JupyterClientPlugin extends siyuan.Plugin { sessionID: string, position: ICodeBlockCursorPosition, ): Promise { + const payload = { + code: position.code, + cursor_pos: position.offset, + }; const message = await this.bridge?.call( "jupyter.session.kernel.connection.requestComplete", sessionID, - { - code: position.code, - cursor_pos: position.offset, - }, + payload, ); - // this.logger.debug(message); + // this.logger.debugs(payload, message); if (message) { switch (message.content.status) { - case "ok": - // TODO: 使用菜单实现自动补全 + case "ok": { + /* 使用菜单实现自动补全 */ + const content = message.content; + const menu_items: siyuan.IMenuItemOption[] = []; + const icon_map = isLightTheme() + ? LIGHT_ICON_MAP + : DARK_ICON_MAP; + + const advices = content.metadata["_jupyter_types_experimental"] as { + start: number, + end: number, + text: string, + type: string, + signature: string, + }[] | void; + if (Array.isArray(advices) + && advices.length > 0 + ) { + (advices).reduce((previous, current) => { + if (previous.type !== current.type) { + menu_items.push({ type: "separator" }); + } + + const click = () => this.complete( + position, + current.start, + current.end, + current.text, + ); + const item: siyuan.IMenuItemOption = { + label: current.text, + accelerator: fn__code(current.type), + // accelerator: fn__code(current.signature), + click, + submenu: current.signature + ? [{ + // type: "readonly", + iconHTML: "", // 移除图标 + label: fn__code(escapeHTML(current.signature)), + click, + }] + : undefined, + }; + const icon = icon_map.get(current.type); + if (icon) { + item.iconHTML = icon; + } + else { + switch (current.type) { + case "magic": + // item.icon = "icon-jupyter-client-kernel"; + item.icon = "icon-jupyter-client-simple"; + break; + + case "path": + switch (true) { + case current.text.endsWith("/"): + item.iconHTML = icon_map.get("folder"); + break; + + default: + item.iconHTML = icon_map.get("file"); + break; + } + break; + + default: + item.icon = undefined; + break; + } + } + menu_items.push(item); + return current; + }, advices[0]); + } + else { + menu_items.push(...content.matches.map(label => ({ + label, + click: () => this.complete( + position, + content.cursor_start, + content.cursor_end, + label, + ), + } as siyuan.IMenuItemOption))) + } + + if (menu_items.length > 0) { + const range = position.current; + const rect: DOMRect = (() => { + var rect: DOMRect | void; + + rect = range.getBoundingClientRect(); + if (rect.x > 0 && rect.y > 0) return rect; + + rect = range.commonAncestorContainer instanceof Element + ? range.commonAncestorContainer.getBoundingClientRect() + : range.commonAncestorContainer.parentElement?.getBoundingClientRect(); + if (rect && rect.x > 0 && rect.y > 0) return rect; + + return position.container.getBoundingClientRect(); + })(); + + const menu = new siyuan.Menu(message.header.msg_id, () => { + this.complating = false; + if (menu.menu.element.lastElementChild instanceof HTMLElement) { + menu.menu.element.lastElementChild.style.maxHeight = ""; + } + }); + + // menu_items[0].current = true; // 仅高亮显示, 无法自动获取焦点 + menu_items.forEach(item => menu.addItem(item)); + // menu_items.forEach(menu.addItem); // 无效 + + this.complating = true; + menu.open({ + x: rect.left, + y: rect.bottom, + }); + + /* 调整菜单位置 */ + const menu_rect = menu.menu.element.getBoundingClientRect(); + if (menu_rect.y !== rect.bottom) { + const items_element = menu.menu.element.lastElementChild; + if (items_element instanceof HTMLElement) { + const max_height = globalThis.innerHeight - rect.bottom - 32; + + items_element.style.maxHeight = `${max_height}px`; + menu.element.style.top = `${rect.bottom}px`; + } + } + } + break; + } case "abort": this.logger.info(message); case "error": @@ -1451,6 +1610,27 @@ export default class JupyterClientPlugin extends siyuan.Plugin { return false; } + /** + * 补全上下文 + * @param position 光标位置 + * @param start 光标起始偏移量 + * @param end 光标末尾偏移量 + * @param text 补全文本 + */ + protected complete( + position: ICodeBlockCursorPosition, + start: number, + end: number, + text: string, + ): void { + select(position.container, { start, end }); + const range = globalThis.getSelection()?.getRangeAt(0); + if (range) { + replaceRangeWithText(range, text); + select(position.container, { start: start + text.length }); + } + } + /** * 请求运行代码块 * @param code 代码 @@ -1729,7 +1909,7 @@ export default class JupyterClientPlugin extends siyuan.Plugin { if (!this.protyles.has(protyle)) { // 未加入监听的编辑器 const listener = [ "keyup", - e => this.editEventListener(e, protyle), + e => this.editEventListener(e as KeyboardEvent, protyle), { capture: true, }, @@ -1748,12 +1928,22 @@ export default class JupyterClientPlugin extends siyuan.Plugin { /* 编辑事件监听 */ protected readonly editEventListener = ( - e: Event, + e: KeyboardEvent, protyle: IProtyle, ) => { // this.logger.debugs(e, protyle); - this.editEventHandler(protyle); + switch (true) { + case this.complating // 自动补全状态 + && isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_COMPLATING_STATUS): + case !this.complating // 非自动补全状态 + && isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_STATUS): + + this.editEventHandler(protyle); + break; + default: + break; + } } /* 块菜单菜单弹出事件监听器 */ diff --git a/src/jupyter/icon.ts b/src/jupyter/icon.ts new file mode 100644 index 0000000..5598415 --- /dev/null +++ b/src/jupyter/icon.ts @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2023 Zuoqiu Yingyi + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import light_file from "@jupyter-lsp/theme-vscode/style/icons/light/file.svg?raw"; +import light_folder from "@jupyter-lsp/theme-vscode/style/icons/light/folder.svg?raw"; +import light_json from "@jupyter-lsp/theme-vscode/style/icons/light/json.svg?raw"; +import light_value from "@jupyter-lsp/theme-vscode/style/icons/light/note.svg?raw"; +import light_references from "@jupyter-lsp/theme-vscode/style/icons/light/references.svg?raw"; +import light_symbol_class from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-class.svg?raw"; +import light_symbol_color from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-color.svg?raw"; +import light_symbol_constant from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-constant.svg?raw"; +import light_symbol_enumerator_member from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-enumerator-member.svg?raw"; +import light_symbol_enumerator from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-enumerator.svg?raw"; +import light_symbol_event from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-event.svg?raw"; +import light_symbol_field from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-field.svg?raw"; +import light_symbol_interface from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-interface.svg?raw"; +import light_symbol_keyword from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-keyword.svg?raw"; +import light_symbol_method from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-method.svg?raw"; +import light_symbol_operator from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-operator.svg?raw"; +import light_symbol_parameter from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-parameter.svg?raw"; +import light_symbol_property from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-property.svg?raw"; +import light_symbol_ruler from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-ruler.svg?raw"; +import light_symbol_snippet from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-snippet.svg?raw"; +import light_symbol_string from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-string.svg?raw"; +import light_symbol_structure from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-structure.svg?raw"; +import light_symbol_variable from "@jupyter-lsp/theme-vscode/style/icons/light/symbol-variable.svg?raw"; + +import dark_file from "@jupyter-lsp/theme-vscode/style/icons/dark/file.svg?raw"; +import dark_folder from "@jupyter-lsp/theme-vscode/style/icons/dark/folder.svg?raw"; +import dark_json from "@jupyter-lsp/theme-vscode/style/icons/dark/json.svg?raw"; +import dark_value from "@jupyter-lsp/theme-vscode/style/icons/dark/note.svg?raw"; +import dark_references from "@jupyter-lsp/theme-vscode/style/icons/dark/references.svg?raw"; +import dark_symbol_class from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-class.svg?raw"; +import dark_symbol_color from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-color.svg?raw"; +import dark_symbol_constant from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-constant.svg?raw"; +import dark_symbol_enumerator_member from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-enumerator-member.svg?raw"; +import dark_symbol_enumerator from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-enumerator.svg?raw"; +import dark_symbol_event from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-event.svg?raw"; +import dark_symbol_field from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-field.svg?raw"; +import dark_symbol_interface from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-interface.svg?raw"; +import dark_symbol_keyword from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-keyword.svg?raw"; +import dark_symbol_method from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-method.svg?raw"; +import dark_symbol_operator from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-operator.svg?raw"; +import dark_symbol_parameter from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-parameter.svg?raw"; +import dark_symbol_property from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-property.svg?raw"; +import dark_symbol_ruler from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-ruler.svg?raw"; +import dark_symbol_snippet from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-snippet.svg?raw"; +import dark_symbol_string from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-string.svg?raw"; +import dark_symbol_structure from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-structure.svg?raw"; +import dark_symbol_variable from "@jupyter-lsp/theme-vscode/style/icons/dark/symbol-variable.svg?raw"; + +function wash(svg: string): string { + const element = document.createElement("span"); + element.innerHTML = svg; + if (element.firstElementChild instanceof SVGElement) { + element.firstElementChild.classList.toggle("b3-menu__icon", true); + } + return element.innerHTML; +} + +export const LIGHT_ICON_MAP = new Map([ + ["class", wash(light_symbol_class)], + ["color", wash(light_symbol_color)], + ["constant", wash(light_symbol_constant)], + ["constructor", wash(light_symbol_method)], + ["enum", wash(light_symbol_enumerator)], + ["enumMember", wash(light_symbol_enumerator_member)], + ["event", wash(light_symbol_event)], + ["field", wash(light_symbol_field)], + ["file", wash(light_file)], + ["folder", wash(light_folder)], + ["function", wash(light_symbol_method)], + ["instance", wash(light_symbol_variable)], // ➕ + ["interface", wash(light_symbol_interface)], + ["keyword", wash(light_symbol_keyword)], + ["method", wash(light_symbol_method)], + ["module", wash(light_json)], + ["operator", wash(light_symbol_operator)], + ["property", wash(light_symbol_property)], + ["reference", wash(light_references)], + ["snippet", wash(light_symbol_snippet)], + ["struct", wash(light_symbol_structure)], + ["text", wash(light_symbol_string)], + ["typeParameter", wash(light_symbol_parameter)], + ["unit", wash(light_symbol_ruler)], + ["value", wash(light_value)], + ["variable", wash(light_symbol_variable)], +]); + +export const DARK_ICON_MAP = new Map([ + ["class", wash(dark_symbol_class)], + ["color", wash(dark_symbol_color)], + ["constant", wash(dark_symbol_constant)], + ["constructor", wash(dark_symbol_method)], + ["enum", wash(dark_symbol_enumerator)], + ["enumMember", wash(dark_symbol_enumerator_member)], + ["event", wash(dark_symbol_event)], + ["field", wash(dark_symbol_field)], + ["file", wash(dark_file)], + ["folder", wash(dark_folder)], + ["function", wash(dark_symbol_method)], + ["instance", wash(dark_symbol_variable)], // ➕ + ["interface", wash(dark_symbol_interface)], + ["keyword", wash(dark_symbol_keyword)], + ["method", wash(dark_symbol_method)], + ["module", wash(dark_json)], + ["operator", wash(dark_symbol_operator)], + ["property", wash(dark_symbol_property)], + ["reference", wash(dark_references)], + ["snippet", wash(dark_symbol_snippet)], + ["struct", wash(dark_symbol_structure)], + ["text", wash(dark_symbol_string)], + ["typeParameter", wash(dark_symbol_parameter)], + ["unit", wash(dark_symbol_ruler)], + ["value", wash(dark_value)], + ["variable", wash(dark_symbol_variable)], +]); From 18498c86dc91232ff1c6edb0e36942e78d13ddd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:53:01 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat(jupyter-client):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF=20|=20Improve=20settings?= =?UTF-8?q?=20panel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/README.md | 20 ++++++ public/README_zh_CN.md | 20 ++++++ public/i18n/en_US.json | 4 ++ public/i18n/zh_CHT.json | 4 ++ public/i18n/zh_CN.json | 4 ++ src/components/Settings.svelte | 22 +++++++ src/index.ts | 109 ++++++++++++++++++--------------- src/workers/jupyter.ts | 18 +++--- 8 files changed, 145 insertions(+), 56 deletions(-) diff --git a/public/README.md b/public/README.md index f4e18f9..dc3736d 100644 --- a/public/README.md +++ b/public/README.md @@ -246,6 +246,15 @@ This is a plugin for [SiYuan Note](https://github.com/siyuan-note/siyuan) that c * The code cannot be executed until the document is connected to a session (see the *Session Management* section for details) +#### Language Services + +* Contextual help + + * The Jupyter Contextual Help side panel displays contextual information about the current code. +* Code suggestions and completion + + * When editing in code cells, suggestions for the current code are displayed, and selecting one will complete the code. + #### Import `*.ipynb` Files * Calling method @@ -272,6 +281,17 @@ This is a plugin for [SiYuan Note](https://github.com/siyuan-note/siyuan) that c * When enabled, this plugin will attempt to connect to the Jupyter service * When disabled, all sessions created by this plugin will be interrupted and disconnected from the Jupyter service + * `Language service call delay` + + * The delay time when calling Jupyter kernel language service to retrieve contextual help and code suggestions. + * Unit: milliseconds + * After entering text in a code cell, there is a certain delay before calling the Jupyter kernel language service. + + * The language service requires support from the corresponding Jupyter kernel session. + * Currently supported language services: + + * Retrieve and update the content of the Jupyter contextual help sidebar. + * Display code suggestion list. * Server settings * `Jupyter Server URL` diff --git a/public/README_zh_CN.md b/public/README_zh_CN.md index fb3ec5a..c835569 100644 --- a/public/README_zh_CN.md +++ b/public/README_zh_CN.md @@ -246,6 +246,15 @@ * 必须将文档与一个会话建立连接后才能运行代码 (详情请参考 *会话管理* 一节) +#### 语言服务 + +* 上下文帮助 + + * 在 Jupyter 上下文帮助 侧边面板显示当前代码的上下文信息 +* 代码建议与补全 + + * 在代码单元格中进行编辑时显示当前代码的输入建议, 选择后可补全代码 + #### 导入 `*.ipynb` 文件 * 调用方式 @@ -272,6 +281,17 @@ * 开启后将本插件将尝试与 Jupyter 服务建立连接 * 关闭后将中断所有本插件建立的会话并断开与 Jupyter 服务的连接 + * `语言服务调用延时` + + * 调用 Jupyter 内核语言服务获取上下文帮助代码建议时的延时时间 + * 单位:毫秒 + * 在代码单元格中输入文本后经过一定延时后再调用 Jupyter 内核语言服务 + + * 语言服务需要会话对应的 Jupyter 内核支持 + * 目前支持的语言服务 + + * 获取并更新 Jupyter 上下文帮助 侧边栏的内容 + * 显示代码建议列表 * 服务设置 * `Jupyter 服务 URL` diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 44d59d0..939ff87 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -201,6 +201,10 @@ "description": "Enable to attempt to establish connection with Jupyter service", "title": "Enable Jupyter client" }, + "delay": { + "description": "The delay time when calling the Jupyter kernel language service to get context help and code suggestions.
Unit: milliseconds", + "title": "Language service call delay" + }, "title": "Global" }, "importTab": { diff --git a/public/i18n/zh_CHT.json b/public/i18n/zh_CHT.json index b63c9ef..45327b3 100644 --- a/public/i18n/zh_CHT.json +++ b/public/i18n/zh_CHT.json @@ -201,6 +201,10 @@ "description": "開啟後將嘗試與 Jupyter 服務建立連接", "title": "啟用 Jupyter 客戶端" }, + "delay": { + "description": "調用 Jupyter 內核語言服務獲取上下文幫助與代碼建議時的延時時間
單位:毫秒", + "title": "語言服務調用延時" + }, "title": "全局" }, "importTab": { diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json index 6af8753..217f45c 100644 --- a/public/i18n/zh_CN.json +++ b/public/i18n/zh_CN.json @@ -201,6 +201,10 @@ "description": "开启后将尝试与 Jupyter 服务建立连接", "title": "启用 Jupyter 客户端" }, + "delay": { + "description": "调用 Jupyter 内核语言服务获取上下文帮助与代码建议时的延时时间
单位:毫秒", + "title": "语言服务调用延时" + }, "title": "全局" }, "importTab": { diff --git a/src/components/Settings.svelte b/src/components/Settings.svelte index b5086ae..554c9f6 100644 --- a/src/components/Settings.svelte +++ b/src/components/Settings.svelte @@ -181,6 +181,28 @@ }} /> + + + + { + config.jupyter.edit.delay = e.detail.value; + await updated(); + }} + /> + diff --git a/src/index.ts b/src/index.ts index f8e0301..ca6b6ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,22 +137,22 @@ export type TMenuContext = IBlockMenuContext | { export default class JupyterClientPlugin extends siyuan.Plugin { static readonly GLOBAL_CONFIG_NAME = "global-config"; - static readonly EDIT_KEYBOARD_EVENT_STATUS: IKeyboardStatus = { + static readonly EDIT_KEYBOARD_EVENT_STATUS_INSPECT: IKeyboardStatus = { type: "keyup", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, - key: (key: string) => !/^(Escape)$/.test(key), - } as const; - static readonly EDIT_KEYBOARD_EVENT_COMPLATING_STATUS: IKeyboardStatus = { + key: (key: string) => /^(\S|Tab|Delete|Backspace|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Home|End)$/.test(key), + } as const; // 触发上下文帮助的事件 + static readonly EDIT_KEYBOARD_EVENT_STATUS_COMPLATE: IKeyboardStatus = { type: "keyup", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, key: (key: string) => /^(\S|Tab|Delete|Backspace)$/.test(key), - } as const; + } as const; // 触发自动补全的事件 declare public readonly i18n: I18N; @@ -167,7 +167,11 @@ export default class JupyterClientPlugin extends siyuan.Plugin { public bridge?: InstanceType; // worker 桥 protected complating: boolean = false; // 菜单已打开 - protected editEventHandler!: ReturnType Promise>>; + protected editEventHandler!: ReturnType Promise>>; protected readonly protyles = new WeakMap>(); // 已监听的编辑器对象 protected jupyterDock!: { @@ -388,6 +392,8 @@ export default class JupyterClientPlugin extends siyuan.Plugin { await this.gotoBlock(next_cell.id, false); } else { // 不存在下一个代码单元格 + if (blocks.cells.length == 0) return; // 仅在当前为代码单元格时才会插入 + /* 插入新代码单元格 */ const new_cell = await this.insertNewCodeCell(blocks); @@ -660,23 +666,33 @@ export default class JupyterClientPlugin extends siyuan.Plugin { /* 更新编辑事件处理函数 */ public updateEditEventHandler(delay: number = this.config.jupyter.edit.delay): void { - this.editEventHandler = deshake(async (protyle: IProtyle) => { - const session = this.doc2session.get(protyle.block.rootID!); - if (session) { // 当前文档已连接会话 - const block = getCurrentBlock(); - if (isCodeCell(block)) { // 当前块是代码单元格 - const position = getCodeBlockCursorPosition(); - if (position) { // 成功获取光标位置 - await Promise.allSettled([ - this.requestInspect( // 更新上下文帮助 - session.id, - position, - ), - this.requestComplete( // 自动补全 - session.id, - position, - ), - ]); + this.editEventHandler = deshake(async ( + protyle: IProtyle, + inspect: boolean, + complate: boolean, + ) => { + if (inspect || complate) { + const session = this.doc2session.get(protyle.block.rootID!); + if (session) { // 当前文档已连接会话 + const block = getCurrentBlock(); + if (isCodeCell(block)) { // 当前块是代码单元格 + const position = getCodeBlockCursorPosition(); + if (position) { // 成功获取光标位置 + const promises: Promise[] = []; + if (inspect) { + promises.push(this.requestInspect( // 更新上下文帮助 + session.id, + position, + )); + } + if (complate) { + promises.push(this.requestComplete( // 自动补全 + session.id, + position, + )); + } + await Promise.allSettled(promises); + } } } } @@ -1554,18 +1570,24 @@ export default class JupyterClientPlugin extends siyuan.Plugin { if (menu_items.length > 0) { const range = position.current; - const rect: DOMRect = (() => { + const options: { x: number, y: number } = (() => { var rect: DOMRect | void; rect = range.getBoundingClientRect(); - if (rect.x > 0 && rect.y > 0) return rect; + if (rect.x > 0 && rect.y > 0) return { x: rect.right, y: rect.bottom }; rect = range.commonAncestorContainer instanceof Element ? range.commonAncestorContainer.getBoundingClientRect() - : range.commonAncestorContainer.parentElement?.getBoundingClientRect(); - if (rect && rect.x > 0 && rect.y > 0) return rect; + : undefined; + if (rect && rect.x > 0 && rect.y > 0) return { x: rect.right, y: rect.bottom }; + + rect = range.commonAncestorContainer.parentElement instanceof HTMLSpanElement + ? range.commonAncestorContainer.parentElement?.getBoundingClientRect() + : undefined; + if (rect && rect.x > 0 && rect.y > 0) return { x: rect.right, y: rect.bottom }; - return position.container.getBoundingClientRect(); + rect = position.container.getBoundingClientRect(); + return { x: rect.left, y: rect.bottom }; })(); const menu = new siyuan.Menu(message.header.msg_id, () => { @@ -1580,20 +1602,17 @@ export default class JupyterClientPlugin extends siyuan.Plugin { // menu_items.forEach(menu.addItem); // 无效 this.complating = true; - menu.open({ - x: rect.left, - y: rect.bottom, - }); + menu.open(options); /* 调整菜单位置 */ const menu_rect = menu.menu.element.getBoundingClientRect(); - if (menu_rect.y !== rect.bottom) { + if (menu_rect.y !== options.y) { const items_element = menu.menu.element.lastElementChild; if (items_element instanceof HTMLElement) { - const max_height = globalThis.innerHeight - rect.bottom - 32; + const max_height = globalThis.innerHeight - options.y - 32; items_element.style.maxHeight = `${max_height}px`; - menu.element.style.top = `${rect.bottom}px`; + menu.element.style.top = `${options.y}px`; } } } @@ -1625,10 +1644,10 @@ export default class JupyterClientPlugin extends siyuan.Plugin { ): void { select(position.container, { start, end }); const range = globalThis.getSelection()?.getRangeAt(0); - if (range) { + if (range && range.toString() !== text) { replaceRangeWithText(range, text); - select(position.container, { start: start + text.length }); } + select(position.container, { start: start + text.length }); } /** @@ -1933,17 +1952,11 @@ export default class JupyterClientPlugin extends siyuan.Plugin { ) => { // this.logger.debugs(e, protyle); - switch (true) { - case this.complating // 自动补全状态 - && isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_COMPLATING_STATUS): - case !this.complating // 非自动补全状态 - && isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_STATUS): - - this.editEventHandler(protyle); - break; - default: - break; - } + this.editEventHandler( + protyle, + isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_STATUS_INSPECT), + isMatchedKeyboardEvent(e, JupyterClientPlugin.EDIT_KEYBOARD_EVENT_STATUS_COMPLATE), + ); } /* 块菜单菜单弹出事件监听器 */ diff --git a/src/workers/jupyter.ts b/src/workers/jupyter.ts index 63adf6e..be50e73 100644 --- a/src/workers/jupyter.ts +++ b/src/workers/jupyter.ts @@ -38,6 +38,15 @@ import { type IData, } from "@/jupyter/parse"; +import type { + KernelSpec, + Kernel, + Session, + KernelMessage, +} from "@jupyterlab/services"; +import type { IHeader } from "@jupyterlab/services/lib/kernel/messages"; +import type { IShellFuture } from "@jupyterlab/services/lib/kernel/kernel"; + import type { IConfig, IJupyterParserOptions, @@ -46,18 +55,11 @@ import type { IFunction, THandlersWrapper, } from "@workspace/utils/worker/bridge"; -import type { - KernelSpec, - Kernel, - Session, - KernelMessage, -} from "@jupyterlab/services"; import type { BlockID } from "@workspace/types/siyuan"; + import type { PluginHandlers } from "@/index"; -import type { IHeader } from "@jupyterlab/services/lib/kernel/messages"; import type { IExecuteContext } from "@/types/jupyter"; import type { I18N } from "@/utils/i18n"; -import type { IShellFuture } from "@jupyterlab/services/lib/kernel/kernel"; const config: IConfig = DEFAULT_CONFIG; const logger = new Logger(`${self.name}-worker:${CONSTANTS.JUPYTER_WORKER_FILE_NAME}`); From 93bd42f8c921d9b792325dd345163f7b718eda60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:53:17 +0800 Subject: [PATCH 8/8] chore(jupyter-client): release v0.2.0 --- .github/workflows/release-please.yml | 2 +- package.json | 8 ++++---- public/plugin.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 2e4fb11..7e4626e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -14,7 +14,7 @@ permissions: env: PACKAGE_NAME: jupyter-client - PACKAGE_VERSION: 0.1.2 + PACKAGE_VERSION: 0.2.0 jobs: release-please: diff --git a/package.json b/package.json index 5d1f4bc..2e8d76a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jupyter-client", "private": true, - "version": "0.1.2", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", @@ -14,9 +14,9 @@ }, "devDependencies": { "@jupyter-lsp/theme-vscode": "3.0.0-rc.0", - "@jupyterlab/services": "^7.0.5", + "@jupyterlab/services": "^7.0.6", "@jupyterlab/ui-components": "^4.0.6", - "@sveltejs/vite-plugin-svelte": "^2.4.5", + "@sveltejs/vite-plugin-svelte": "^2.4.6", "@tsconfig/svelte": "^5.0.2", "less": "^4.2.0", "strip-ansi": "^7.1.0", @@ -26,7 +26,7 @@ "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^4.4.9", - "xterm": "^5.2.1", + "xterm": "^5.3.0", "xterm-addon-fit": "^0.7.0" }, "dependencies": { diff --git a/public/plugin.json b/public/plugin.json index 3df46f1..5972800 100644 --- a/public/plugin.json +++ b/public/plugin.json @@ -2,7 +2,7 @@ "name": "jupyter-client", "author": "Zuoqiu Yingyi", "url": "https://github.com/Zuoqiu-Yingyi/siyuan-plugin-jupyter-client", - "version": "0.1.2", + "version": "0.2.0", "minAppVersion": "2.10.3", "displayName": { "default": "Jupyter Client",