From 60e0389b1c8a24b2f5e417cf97521ab9df39d201 Mon Sep 17 00:00:00 2001 From: hgaol Date: Sun, 20 Sep 2026 02:25:45 +0000 Subject: [PATCH 1/5] fix(editor): insert list markers on blank lines Blank lines were skipped by the line replacement logic, causing ordered and unordered list toolbar actions to do nothing in an empty editor. --- ui/package.json | 1 + .../Editor/utils/codemirror/commands.test.cjs | 71 +++++++++++++++++++ .../Editor/utils/codemirror/commands.ts | 24 ++++++- 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/Editor/utils/codemirror/commands.test.cjs diff --git a/ui/package.json b/ui/package.json index 07e5293f3..4f2abbe6b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,6 +7,7 @@ "start": "vite", "build": "node ./scripts/env.js && tsc --noEmit && vite build", "preview": "vite preview", + "test": "node --test src/**/*.test.cjs", "pre-install": "node ./scripts/importPlugins.js && pnpm install && node ./scripts/preinstall.js ", "prepare": "pnpm build:packages", "build:packages": "pnpm -r --filter=./src/plugins/* run build", diff --git a/ui/src/components/Editor/utils/codemirror/commands.test.cjs b/ui/src/components/Editor/utils/codemirror/commands.test.cjs new file mode 100644 index 000000000..237c06bdc --- /dev/null +++ b/ui/src/components/Editor/utils/codemirror/commands.test.cjs @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); +const ts = require('typescript'); + +require.extensions['.ts'] = (module, filename) => { + const source = fs.readFileSync(filename, 'utf8'); + const output = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true, + }, + fileName: filename, + }).outputText; + module._compile(output, filename); +}; + +const { EditorState } = require('@codemirror/state'); +const { createCommandMethods } = require('./commands.ts'); + +function createEditor(doc = '') { + let state = EditorState.create({ doc }); + + return { + get state() { + return state; + }, + dispatch(spec) { + state = state.update(spec).state; + }, + getCursor() { + const range = state.selection.ranges[0]; + return { line: state.doc.lineAt(range.from).number }; + }, + }; +} + +for (const [name, command, expected] of [ + ['unordered', 'insertUnorderedList', '- '], + ['ordered', 'insertOrderedList', '1. '], +]) { + test(`${name} list command inserts a marker in an empty editor`, () => { + const editor = createEditor(); + const commands = createCommandMethods(editor); + + commands[command](); + + assert.equal(editor.state.doc.toString(), expected); + assert.equal(editor.state.selection.main.head, expected.length); + }); +} diff --git a/ui/src/components/Editor/utils/codemirror/commands.ts b/ui/src/components/Editor/utils/codemirror/commands.ts index 546382d46..d83d95b89 100644 --- a/ui/src/components/Editor/utils/codemirror/commands.ts +++ b/ui/src/components/Editor/utils/codemirror/commands.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EditorSelection } from '@codemirror/state'; +import { EditorSelection, Line } from '@codemirror/state'; import { Editor, Level } from '../../types'; @@ -33,6 +33,22 @@ import { Editor, Level } from '../../types'; * @returns Object containing all command methods */ export function createCommandMethods(editor: Editor) { + const insertListMarkerOnBlankLine = (line: Line, marker: string) => { + if (line.text.trim() !== '') { + return false; + } + + editor.dispatch({ + changes: { + from: line.from, + to: line.to, + insert: marker, + }, + selection: EditorSelection.cursor(line.from + marker.length), + }); + return true; + }; + // Create methods object that allows self-reference const methods = { wrapText: (before: string, after = before, defaultText) => { @@ -130,6 +146,9 @@ export function createCommandMethods(editor: Editor) { insertOrderedList: () => { const cursor = editor.getCursor(); const line = editor.state.doc.line(cursor.line); + if (insertListMarkerOnBlankLine(line, '1. ')) { + return; + } const lineText = line.text.trim(); if (/^\d+\.\s/.test(lineText)) { return; @@ -145,6 +164,9 @@ export function createCommandMethods(editor: Editor) { insertUnorderedList: () => { const cursor = editor.getCursor(); const line = editor.state.doc.line(cursor.line); + if (insertListMarkerOnBlankLine(line, '- ')) { + return; + } const lineText = line.text.trim(); if (/^[-*+]\s/.test(lineText)) { return; From f7ab3e4e506a31bace282a44cfbc0a91edfcb94b Mon Sep 17 00:00:00 2001 From: hgaol Date: Sun, 20 Sep 2026 03:05:56 +0000 Subject: [PATCH 2/5] fix(editor): use CodeMirror selection for list actions List commands were calling getCursor on the raw EditorView retained by the command adapter. Read the current line directly from CodeMirror state so toolbar actions reach the list insertion logic. --- .../Editor/utils/codemirror/commands.test.cjs | 25 ++++++++++--------- .../Editor/utils/codemirror/commands.ts | 6 ++--- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ui/src/components/Editor/utils/codemirror/commands.test.cjs b/ui/src/components/Editor/utils/codemirror/commands.test.cjs index 237c06bdc..abc7241e3 100644 --- a/ui/src/components/Editor/utils/codemirror/commands.test.cjs +++ b/ui/src/components/Editor/utils/codemirror/commands.test.cjs @@ -36,22 +36,23 @@ require.extensions['.ts'] = (module, filename) => { }; const { EditorState } = require('@codemirror/state'); -const { createCommandMethods } = require('./commands.ts'); +const { createCodeMirrorAdapter } = require('./adapter.ts'); -function createEditor(doc = '') { +function createEditorView(doc = '') { let state = EditorState.create({ doc }); return { + contentDOM: { + addEventListener() {}, + focus() {}, + removeEventListener() {}, + }, get state() { return state; }, dispatch(spec) { state = state.update(spec).state; }, - getCursor() { - const range = state.selection.ranges[0]; - return { line: state.doc.lineAt(range.from).number }; - }, }; } @@ -59,13 +60,13 @@ for (const [name, command, expected] of [ ['unordered', 'insertUnorderedList', '- '], ['ordered', 'insertOrderedList', '1. '], ]) { - test(`${name} list command inserts a marker in an empty editor`, () => { - const editor = createEditor(); - const commands = createCommandMethods(editor); + test(`${name} list toolbar command inserts a marker in an empty editor`, () => { + const view = createEditorView(); + const editor = createCodeMirrorAdapter(view); - commands[command](); + editor[command](); - assert.equal(editor.state.doc.toString(), expected); - assert.equal(editor.state.selection.main.head, expected.length); + assert.equal(view.state.doc.toString(), expected); + assert.equal(view.state.selection.main.head, expected.length); }); } diff --git a/ui/src/components/Editor/utils/codemirror/commands.ts b/ui/src/components/Editor/utils/codemirror/commands.ts index d83d95b89..153e530fb 100644 --- a/ui/src/components/Editor/utils/codemirror/commands.ts +++ b/ui/src/components/Editor/utils/codemirror/commands.ts @@ -144,8 +144,7 @@ export function createCommandMethods(editor: Editor) { }, insertOrderedList: () => { - const cursor = editor.getCursor(); - const line = editor.state.doc.line(cursor.line); + const line = editor.state.doc.lineAt(editor.state.selection.main.head); if (insertListMarkerOnBlankLine(line, '1. ')) { return; } @@ -162,8 +161,7 @@ export function createCommandMethods(editor: Editor) { }, insertUnorderedList: () => { - const cursor = editor.getCursor(); - const line = editor.state.doc.line(cursor.line); + const line = editor.state.doc.lineAt(editor.state.selection.main.head); if (insertListMarkerOnBlankLine(line, '- ')) { return; } From 9f33e68b29d655fb37f0976c1a173cef95a070cd Mon Sep 17 00:00:00 2001 From: hgaol Date: Sun, 20 Sep 2026 03:05:56 +0000 Subject: [PATCH 3/5] fix(ui): resolve Sass mixed declaration warning --- ui/src/components/Comment/index.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/components/Comment/index.scss b/ui/src/components/Comment/index.scss index 7264c66c4..592863f9c 100644 --- a/ui/src/components/Comment/index.scss +++ b/ui/src/components/Comment/index.scss @@ -23,6 +23,8 @@ .comments-wrap { .comment-item { + border-bottom: 1px solid var(--an-comment-item-border-bottom); + &:hover { @include media-breakpoint-up(md) { .control-area { @@ -30,7 +32,6 @@ } } } - border-bottom: 1px solid var(--an-comment-item-border-bottom); } .fmt { display: inline; From 53fadb1d96863e07241243f482591b5c3f119ccf Mon Sep 17 00:00:00 2001 From: hgaol Date: Sun, 20 Sep 2026 14:14:15 +0800 Subject: [PATCH 4/5] revert ui/package.json --- ui/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index 4f2abbe6b..07e5293f3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,6 @@ "start": "vite", "build": "node ./scripts/env.js && tsc --noEmit && vite build", "preview": "vite preview", - "test": "node --test src/**/*.test.cjs", "pre-install": "node ./scripts/importPlugins.js && pnpm install && node ./scripts/preinstall.js ", "prepare": "pnpm build:packages", "build:packages": "pnpm -r --filter=./src/plugins/* run build", From a87236770391e2256f7eb3ddffbe191edf1c492f Mon Sep 17 00:00:00 2001 From: hgaol Date: Sun, 20 Sep 2026 16:33:41 +0800 Subject: [PATCH 5/5] resolve comments --- .../Editor/utils/codemirror/commands.test.cjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/src/components/Editor/utils/codemirror/commands.test.cjs b/ui/src/components/Editor/utils/codemirror/commands.test.cjs index abc7241e3..294f38515 100644 --- a/ui/src/components/Editor/utils/codemirror/commands.test.cjs +++ b/ui/src/components/Editor/utils/codemirror/commands.test.cjs @@ -22,6 +22,7 @@ const fs = require('node:fs'); const test = require('node:test'); const ts = require('typescript'); +const previousTypeScriptLoader = require.extensions['.ts']; require.extensions['.ts'] = (module, filename) => { const source = fs.readFileSync(filename, 'utf8'); const output = ts.transpileModule(source, { @@ -36,7 +37,16 @@ require.extensions['.ts'] = (module, filename) => { }; const { EditorState } = require('@codemirror/state'); -const { createCodeMirrorAdapter } = require('./adapter.ts'); +let createCodeMirrorAdapter; +try { + ({ createCodeMirrorAdapter } = require('./adapter.ts')); +} finally { + if (previousTypeScriptLoader) { + require.extensions['.ts'] = previousTypeScriptLoader; + } else { + delete require.extensions['.ts']; + } +} function createEditorView(doc = '') { let state = EditorState.create({ doc });