From a6f79aa566b99e9fa4ae21aefad9df92df359ca4 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 9 Dec 2025 17:21:02 -0600 Subject: [PATCH 01/29] Fix: work around pnpm bug - use overrides to specify non-upgradable dependency. --- client/package.json5 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/package.json5 b/client/package.json5 index e6e085b..5249d4f 100644 --- a/client/package.json5 +++ b/client/package.json5 @@ -44,6 +44,11 @@ }, type: 'module', version: '0.1.44', + pnpm: { + overrides: { + '@codemirror/view': '<6.39.0', + }, + }, dependencies: { '@codemirror/commands': '^6.10.0', '@codemirror/lang-cpp': '^6.0.3', @@ -61,7 +66,7 @@ '@codemirror/lang-xml': '^6.1.0', '@codemirror/lang-yaml': '^6.1.2', '@codemirror/state': '^6.5.2', - '@codemirror/view': '=6.38.8', + '@codemirror/view': '^6.38.8', '@hpcc-js/wasm-graphviz': '^1.16.0', '@mathjax/mathjax-newcm-font': '4.0.0', codemirror: '^6.0.2', From de91d2247de9244346f09f453f74480ffe62c223 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 9 Dec 2025 22:20:02 -0600 Subject: [PATCH 02/29] Fix: use git version of htmd until my PRs are merged and a new version released. --- server/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 96a6138..11cb563 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -75,7 +75,7 @@ dunce = "1.0.5" # This is also for integration testing. futures = { version = "0.3.31", optional = true } futures-util = "0.3.29" -htmd = { git = "https://github.com/bjones1/htmd.git", branch = "math-support", version = "0.5" } +htmd = { git = "https://github.com/bjones1/htmd.git", branch = "table-fix", version = "0.5" } html5ever = "0.36.1" imara-diff = { version = "0.2", features = [] } indoc = "2.0.5" From fc73e6e27019eb977f3321ba534699591a5290f8 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 10:42:20 -0600 Subject: [PATCH 03/29] Fix: get eslint working. Fix lints. Remove unused Graphviz custom element. --- builder/src/main.rs | 24 ++++----- client/.eslintrc.yml | 42 ---------------- client/eslint.config.js | 45 +++++++++++++++++ client/package.json5 | 3 ++ client/src/CodeChatEditor-test.mts | 6 +-- client/src/CodeChatEditor.mts | 40 ++++----------- client/src/CodeChatEditorFramework.mts | 22 ++++++--- client/src/CodeMirror-integration.mts | 47 +++++++++--------- client/src/EditorComponents.mts | 57 ---------------------- client/src/HashReader.mts | 2 +- client/src/assert.mts | 2 +- client/src/graphviz-webcomponent-setup.mts | 1 + client/src/tinymce-config.mts | 6 +-- client/tsconfig.json | 2 +- extensions/VSCode/.eslintrc.yml | 41 ---------------- extensions/VSCode/eslint.config.js | 45 +++++++++++++++++ extensions/VSCode/package.json | 6 ++- extensions/VSCode/src/extension.ts | 14 +++--- extensions/VSCode/tsconfig.json | 2 +- toc.md | 4 +- 20 files changed, 175 insertions(+), 236 deletions(-) delete mode 100644 client/.eslintrc.yml create mode 100644 client/eslint.config.js delete mode 100644 client/src/EditorComponents.mts delete mode 100644 extensions/VSCode/.eslintrc.yml create mode 100644 extensions/VSCode/eslint.config.js diff --git a/builder/src/main.rs b/builder/src/main.rs index 8c67225..bcdd734 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -440,10 +440,10 @@ fn run_update() -> io::Result<()> { fn run_format_and_lint(check_only: bool) -> io::Result<()> { // The `-D warnings` flag causes clippy to return a non-zero exit status if // it issues warnings. - let (clippy_check_only, check, prettier_check) = if check_only { - ("-Dwarnings", "--check", "--check") + let (clippy_check_only, check, eslint_check) = if check_only { + ("-Dwarnings", "--check", "") } else { - ("", "", "--write") + ("", "", "--fix") }; run_cmd!( info "cargo clippy and fmt"; @@ -464,18 +464,12 @@ fn run_format_and_lint(check_only: bool) -> io::Result<()> { info "VSCode extension: cargo sort"; cargo sort $check; )?; - run_script( - "npx", - &["prettier", "src", prettier_check], - CLIENT_PATH, - true, - )?; - run_script( - "npx", - &["prettier", "src", prettier_check], - VSCODE_PATH, - true, - ) + let mut eslint_args = vec!["eslint", "src"]; + if !eslint_check.is_empty() { + eslint_args.push(eslint_check) + } + run_script("npx", &eslint_args, CLIENT_PATH, true)?; + run_script("npx", &eslint_args, VSCODE_PATH, true) } fn run_test() -> io::Result<()> { diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml deleted file mode 100644 index 73de1d4..0000000 --- a/client/.eslintrc.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (C) 2025 Bryan A. Jones. -# -# This file is part of the CodeChat Editor. -# -# The CodeChat Editor is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# The CodeChat Editor 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 General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# the CodeChat Editor. If not, see -# [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). -# -# `.eslintrc.yml` -- Configure ESLint for this project -# ==================================================== - -env: - browser: true - es2020: true -extends: - - standard - # See the [ESLint config prettier - # docs](https://github.com/prettier/eslint-config-prettier#installation) and - # its parent link, [integrating Prettier with - # linters](https://prettier.io/docs/en/integrating-with-linters.html). - - prettier -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: latest -plugins: - - "@typescript-eslint" -rules: - camelcase: off - # TypeScript already enforces this; otherwise, eslint complains that - # `NodeJS` is undefined. See [this GitHub - # issue](https://github.com/Chatie/eslint-config/issues/45#issuecomment-1003990077). - no-undef: off diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..763e980 --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,45 @@ +// Copyright (C) 2025 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. +// +// The CodeChat Editor is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// The CodeChat Editor 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 General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// the CodeChat Editor. If not, see +// [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). +// +// `.eslintrc.yml` -- Configure ESLint for this project +// ==================================================== +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import eslint from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig( + eslint.configs.recommended, + tseslint.configs.recommended, + eslintPluginPrettierRecommended, + defineConfig([ + { + // This must be the only key in this dict to be treated as a global ignore. Only global ignores can ignore directories. See the [docs](https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores). + ignores: ["src/third-party/**"], + }, + { + name: "local", + rules: { + "@typescript-eslint/no-unused-vars": [ + "off", + { argsIgnorePattern: "^_" }, + ], + }, + }, + ]), +); diff --git a/client/package.json5 b/client/package.json5 index 5249d4f..1452135 100644 --- a/client/package.json5 +++ b/client/package.json5 @@ -78,7 +78,9 @@ 'toastify-js': '^1.12.0', }, devDependencies: { + '@eslint/js': '^9.39.1', '@types/chai': '^5.2.3', + '@types/dom-navigation': '^1.0.6', '@types/js-beautify': '^1.14.3', '@types/mocha': '^10.0.10', '@types/node': '^24.10.2', @@ -94,6 +96,7 @@ mocha: '^11.7.5', prettier: '^3.7.4', typescript: '^5.9.3', + 'typescript-eslint': '^8.49.0', }, scripts: { test: 'echo "Error: no test specified" && exit 1', diff --git a/client/src/CodeChatEditor-test.mts b/client/src/CodeChatEditor-test.mts index f999998..3f7346c 100644 --- a/client/src/CodeChatEditor-test.mts +++ b/client/src/CodeChatEditor-test.mts @@ -27,7 +27,6 @@ import "mocha/mocha.js"; import "mocha/mocha.css"; import { EditorView } from "@codemirror/view"; import { ChangeSpec, EditorState, EditorSelection } from "@codemirror/state"; -import { exportedForTesting } from "./CodeChatEditor.mjs"; import { CodeMirror, CodeMirrorDocBlockTuple } from "./shared_types.mjs"; import { DocBlockPlugin, @@ -41,9 +40,6 @@ import { // // Nothing needed at present. // -// Provide convenient access to all functions tested here. -const {} = exportedForTesting; - // From [SO](https://stackoverflow.com/a/39914235). const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); @@ -58,7 +54,7 @@ window.CodeChatEditor_test = () => { ui: "tdd", // This is required to use Mocha's global teardown from the browser, // AFAIK. - /// @ts-ignore + /// @ts-expect-error("See above.") globalTeardown: [ () => { // On teardown, put the Mocha div at the beginning of the body. diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index 9819e1e..be5756e 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -58,7 +58,6 @@ import { scroll_to_line as codemirror_scroll_to_line, set_CodeMirror_positions, } from "./CodeMirror-integration.mjs"; -import "./EditorComponents.mjs"; import "./graphviz-webcomponent-setup.mts"; // This must be imported *after* the previous setup import, so it's placed here, // instead of in the third-party category above. @@ -95,22 +94,6 @@ enum EditorMode { raw, } -// Since this is experimental, TypeScript doesn't define it. See the -// [docs](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent). -interface NavigateEvent extends Event { - canIntercept: boolean; - destination: any; - downloadRequest: String | null; - formData: any; - hashChange: boolean; - info: any; - navigationType: String; - signal: AbortSignal; - userInitiated: boolean; - intercept: any; - scroll: any; -} - // Tell TypeScript about the global namespace this program defines. declare global { interface Window { @@ -129,7 +112,7 @@ declare global { show_toast: (text: string) => void; allow_navigation: boolean; }; - CodeChatEditor_test: any; + CodeChatEditor_test: unknown; } } @@ -223,7 +206,7 @@ const _open_lp = async ( // This works, but TypeScript marks it as an error. Ignore this error by // including the // [@ts-ignore directive](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html#ts-check). - /// @ts-ignore + /// @ts-expect-error("See above.") const editorMode = EditorMode[urlParams.get("mode") ?? "edit"]; // Get the [current_metadata](#current_metadata) from the @@ -264,10 +247,7 @@ const _open_lp = async ( // [handling editor events](https://www.tiny.cloud/docs/tinymce/6/events/#handling-editor-events), // this is how to create a TinyMCE event handler. setup: (editor: Editor) => { - // The - // [editor core events list](https://www.tiny.cloud/docs/tinymce/6/events/#editor-core-events) - // includes the`Dirty` event. - editor.on("Dirty", (_event: Event) => { + editor.on("input", (_event: Event) => { is_dirty = true; startAutosaveTimer(); }); @@ -309,7 +289,7 @@ const _open_lp = async ( }; const save_lp = (is_dirty: boolean) => { - let update: UpdateMessageContents = { + const update: UpdateMessageContents = { // The Framework will fill in this value. file_path: "", }; @@ -321,7 +301,7 @@ const save_lp = (is_dirty: boolean) => { // Add the contents only if the document is dirty. if (is_dirty) { - /// @ts-expect-error + /// @ts-expect-error("Declare here; it will be completed later.") let code_mirror_diffable: CodeMirrorDiffable = {}; if (is_doc_only()) { // Untypeset all math before saving the document. @@ -378,7 +358,7 @@ const on_save = async (only_if_dirty: boolean = false) => { console_log( "CodeChat Editor Client: sent Update - saving document/updating cursor location.", ); - await new Promise(async (resolve) => { + await new Promise((resolve) => { webSocketComm.send_message({ Update: save_lp(is_dirty) }, () => resolve(0), ); @@ -491,8 +471,7 @@ const on_click = (event: MouseEvent) => { const save_then_navigate = (codeChatEditorUrl: URL) => { on_save(true).then((_value) => { // Avoid recursion! - /// @ts-ignore - navigation.removeEventListener("navigate", on_navigate); + window.navigation.removeEventListener("navigate", on_navigate); parent.window.CodeChatEditorFramework.webSocketComm.current_file( codeChatEditorUrl, ); @@ -509,6 +488,7 @@ const scroll_to_line = (cursor_line?: number, scroll_line?: number) => { } }; +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const console_log = (...args: any) => { if (DEBUG_ENABLED) { console.log(...args); @@ -534,12 +514,10 @@ export const on_error = (event: Event) => { // namespace. on_dom_content_loaded(async () => { // Intercept links in this document to save before following the link. - /// @ts-ignore - navigation.addEventListener("navigate", on_navigate); + window.navigation.addEventListener("navigate", on_navigate); const ccb = document.getElementById("CodeChat-sidebar") as | HTMLIFrameElement | undefined; - /// @ts-ignore ccb?.contentWindow?.navigation.addEventListener("navigate", on_navigate); document.addEventListener("click", on_click); // Provide basic error reporting for uncaught errors. diff --git a/client/src/CodeChatEditorFramework.mts b/client/src/CodeChatEditorFramework.mts index 8a65f9d..69b7634 100644 --- a/client/src/CodeChatEditorFramework.mts +++ b/client/src/CodeChatEditorFramework.mts @@ -100,7 +100,7 @@ class WebSocketComm { // The `ReconnectingWebSocket` doesn't provide ALL the `WebSocket` // methods. Ignore this, since we can't use `ReconnectingWebSocket` as a // type. - /// @ts-ignore + /// @ts-expect-error("This is legacy, third-party code.") this.ws = new ReconnectingWebSocket!(ws_url); // Identify this client on connection. this.ws.onopen = () => { @@ -139,7 +139,7 @@ class WebSocketComm { // Process this message. switch (key) { - case "Update": + case "Update": { // Load this data in. const current_update = value as UpdateMessageContents; // The rest of this should run after all other messages have @@ -228,8 +228,9 @@ class WebSocketComm { this.send_result(id); }); break; + } - case "CurrentFile": + case "CurrentFile": { // Note that we can ignore `value[1]` (if the file is text // or binary); the server only sends text files here. const current_file = value[0] as string; @@ -263,8 +264,9 @@ class WebSocketComm { this.send_result(id); }); break; + } - case "Result": + case "Result": { // Cancel the timer for this message and remove it from // `pending_messages`. const pending_message = this.pending_messages[id]; @@ -284,8 +286,9 @@ class WebSocketComm { ); } break; + } - default: + default: { const msg = `Received unhandled message ${key}(${format_struct( value, )})`; @@ -296,11 +299,14 @@ class WebSocketComm { )})`, }); break; + } } }; } + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ send = (data: any) => this.ws.send(data); + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ close = (...args: any) => this.ws.close(...args); set_root_iframe_src = (url: string) => { @@ -408,7 +414,7 @@ const set_content = async ( cursor_line?: number, scroll_line?: number, ) => { - let client = get_client(); + const client = get_client(); if (client === undefined) { // See if this is the [simple viewer](#Client-simple-viewer). Otherwise, // it's just the bare document to replace. @@ -475,7 +481,7 @@ declare global { CodeChatEditorFramework: { webSocketComm: WebSocketComm; }; - CodeChatEditor_test: any; + CodeChatEditor_test: unknown; } } @@ -488,6 +494,7 @@ const show_toast = (text: string) => { }; // Format a complex data structure as a string when in debug mode. +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const format_struct = (complex_data_structure: any): string => DEBUG_ENABLED ? JSON.stringify(complex_data_structure).substring( @@ -496,6 +503,7 @@ export const format_struct = (complex_data_structure: any): string => ) : ""; +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ const report_error = (text: string, ...objs: any) => { console.error(text); if (objs !== undefined) { diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 659c4f2..7fccd1b 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -112,6 +112,8 @@ const decorationOptions = { declare global { interface Window { + // Tye `#types/MathJax` definitions are out of date. + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ MathJax: any; } } @@ -145,7 +147,7 @@ export const docBlockField = StateField.define({ // the initial value for the field, which is an empty set (no doc blocks). // Therefore, simply return an empty DecorationSet (oddly, the type of // [Decoration.none](https://codemirror.net/docs/ref/#view.Decoration^none)). - create(state: EditorState) { + create(_state: EditorState) { return Decoration.none; }, @@ -163,7 +165,7 @@ export const docBlockField = StateField.define({ } // See [is](https://codemirror.net/docs/ref/#state.StateEffect.is). Add // a doc block, as requested by this effect. - for (let effect of tr.effects) + for (const effect of tr.effects) if (effect.is(addDocBlock)) { // Check that we're not overwriting text. const newlines = current_view.state.doc @@ -259,7 +261,7 @@ export const docBlockField = StateField.define({ doc_blocks = doc_blocks.update({ // Remove the old doc block. We assume there's only one // block in the provided from/to range. - filter: (from, to, value) => from !== effect.value.from, + filter: (from, _to, _value) => from !== effect.value.from, filterFrom: effect.value.from, filterTo: effect.value.from, // This adds the replacement doc block with updated @@ -283,7 +285,7 @@ export const docBlockField = StateField.define({ }); } else if (effect.is(deleteDocBlock)) { doc_blocks = doc_blocks.update({ - filter: (from, to, value) => from !== effect.value.from, + filter: (from, _to, _value) => from !== effect.value.from, filterFrom: effect.value.from, filterTo: effect.value.from, }); @@ -304,8 +306,8 @@ export const docBlockField = StateField.define({ // This provides a straightforward path to transform the entire editor's // contents (including these doc blocks) to JSON, which can then be sent // back to the server for reassembly into a source file. - toJSON: (value: DecorationSet, state: EditorState) => { - let json = []; + toJSON: (value: DecorationSet, _state: EditorState) => { + const json = []; for (const iter = value.iter(); iter.value !== null; iter.next()) { const w = iter.value.spec.widget; json.push([iter.from, iter.to, w.indent, w.delimiter, w.contents]); @@ -315,7 +317,7 @@ export const docBlockField = StateField.define({ // For loading a file from the server back into the editor, use // [fromJSON](https://codemirror.net/docs/ref/#state.StateField^define^config.fromJSON). - fromJSON: (json: any, state: EditorState) => + fromJSON: (json: [CodeMirrorDocBlockTuple], _state: EditorState) => Decoration.set( json.map( ([ @@ -425,7 +427,7 @@ class DocBlockWidget extends WidgetType { // See [toDom](https://codemirror.net/docs/ref/#view.WidgetType.toDOM). toDOM() { // Wrap this in an enclosing div. - let wrap = document.createElement("div"); + const wrap = document.createElement("div"); wrap.className = "CodeChat-doc"; wrap.innerHTML = // This doc block's indent. TODO: allow paste, but must only allow @@ -511,7 +513,7 @@ const saveSelection = () => { // the li's children (a text node where the actual click landed; the offset // in this node is placed in `selection_offset`.) const sel = window.getSelection(); - let selection_path = []; + const selection_path = []; const selection_offset = sel?.anchorOffset; if (sel?.anchorNode) { // Find a path from the selection back to the containing div. @@ -535,7 +537,7 @@ const saveSelection = () => { // trouble when reversing the selection -- sometimes, the // `childNodes` change based on whether text nodes (such as a // newline) are included are not after tinyMCE parses the content. - let p = current_node.parentNode; + const p = current_node.parentNode; // In case we go off the rails, give up if there are no more parents. if (p === null) { return { @@ -606,6 +608,7 @@ export const mathJaxTypeset = async ( ) => { try { await window.MathJax.typesetPromise([node]); + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (err: any) { report_error(`Typeset failed: ${err.message}`); } @@ -615,6 +618,7 @@ export const mathJaxTypeset = async ( export const mathJaxUnTypeset = (node: HTMLElement) => { window.MathJax.startup.document .getMathItemsWithin(node) + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ .forEach((item: any) => { item.removeFromDocument(true); }); @@ -683,7 +687,7 @@ const on_dirty = ( let from; try { from = current_view.posAtDOM(target); - } catch (e) { + } catch (_e) { console.error("Unable to get position from DOM.", target); return; } @@ -714,7 +718,7 @@ const on_dirty = ( export const DocBlockPlugin = ViewPlugin.fromClass( class { - constructor(view: EditorView) {} + constructor(_view: EditorView) {} update(update: ViewUpdate) { // If the editor doesn't have focus, ignore selection changes. This // avoid the case where cursor movement in the IDE produces @@ -729,7 +733,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass( .between( main_selection.from, main_selection.to, - (from: number, to: number, value: Decoration) => { + (from: number, _to: number, _value: Decoration) => { // Is this range contained within this doc block? If // the ranges also contains element outside it, then // don't capture focus. TODO: not certain on the @@ -764,7 +768,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass( // so it can be edited. A simpler alternative is to do this in the // update() method above, but this is VERY slow, since update is // called frequently. - focusin: (event: FocusEvent, view: EditorView) => { + focusin: (event: FocusEvent, _view: EditorView) => { const target_or_false = element_is_in_doc_block(event.target); if (!target_or_false) { return false; @@ -919,8 +923,8 @@ const autosaveExtension = EditorView.updateListener.of( // detected for efficiency. if (!v.docChanged && v.transactions?.length) { // Check each effect of each transaction. - outer: for (let tr of v.transactions) { - for (let effect of tr.effects) { + outer: for (const tr of v.transactions) { + for (const effect of tr.effects) { // Look for a change to a doc block. if (effect.is(addDocBlock) || effect.is(updateDocBlock)) { isChanged = true; @@ -1093,12 +1097,10 @@ export const CodeMirror_load = async ( setup: (editor: Editor) => { // See the // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). - editor.on("input", (event: any) => { - // Get the div TinyMCE stores edits in. TODO: find - // documentation for `event.target.bodyElement`. + editor.on("input", (event: Event) => { const target_or_false = event.target as HTMLElement; if (target_or_false == null) { - return false; + return; } setTimeout(() => on_dirty(target_or_false)); }); @@ -1122,7 +1124,7 @@ export const CodeMirror_load = async ( // same transaction causes the text edits to modify from/to values in // the doc block effects, even when changes to the doc block state is // frozen. - const stateEffects: StateEffect[] = []; + const stateEffects: StateEffect[] = []; for (const transaction of codechat_for_web.source.Diff.doc_blocks) { if ("Add" in transaction) { const add = transaction.Add; @@ -1214,7 +1216,8 @@ export const CodeMirror_save = (): CodeMirrorDiffable => { const code_mirror: CodeMirror = current_view.state.toJSON( CodeMirror_JSON_fields, ); - delete (code_mirror as any).selection; + /// @ts-expect-error("This does exist.") + delete code_mirror.selection; return { Plain: code_mirror }; }; diff --git a/client/src/EditorComponents.mts b/client/src/EditorComponents.mts deleted file mode 100644 index 4491ec2..0000000 --- a/client/src/EditorComponents.mts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2025 Bryan A. Jones. -// -// This file is part of the CodeChat Editor. The CodeChat Editor is free -// software: you can redistribute it and/or modify it under the terms of the GNU -// General Public License as published by the Free Software Foundation, either -// version 3 of the License, or (at your option) any later version. -// -// The CodeChat Editor 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 General Public License for more -// details. -// -// You should have received a copy of the GNU General Public License along with -// the CodeChat Editor. If not, see -// [http://www.gnu.org/licenses](http://www.gnu.org/licenses). -// -// `EditorComponents.mts` -- Custom HTML tags which provide authoring support -// for the CodeChat Editor -// ========================================================================== -// -// Create a combined editor/renderer component. It's not currently used, since -// TinyMCE doesn't allow the editor to be focused. -class GraphVizElement extends HTMLElement { - constructor() { - super(); - // Create the shadow DOM. - const shadowRoot = this.attachShadow({ mode: "open" }); - const editor = document.createElement("graphviz-script-editor"); - const graph = document.createElement("graphviz-graph"); - - // TODO: Copy other attributes (scale, tabs, etc.) which the editor and - // graph renderer support. - - // Propagate the initial value on this tag to the tags in the shadow - // DOM. - const dot = this.getAttribute("graph") ?? ""; - graph.setAttribute("graph", dot); - editor.setAttribute("value", dot); - - // Send edits to both this tag and the graphviz rendering tag. - editor.addEventListener("input", (event) => { - // Ignore InputEvents -- we want the custom event sent by this - // component, which contains new text for the graph. - if (event instanceof CustomEvent) { - const dot = (event as any).detail; - graph.setAttribute("graph", dot); - // Update the root component as well, so that this value will be - // correct when the user saves. - this.setAttribute("graph", dot); - } - }); - - // Populate the shadow DOM now that everything is ready. - shadowRoot.append(editor, graph); - } -} -customElements.define("graphviz-combined", GraphVizElement); diff --git a/client/src/HashReader.mts b/client/src/HashReader.mts index 9671e47..b62ecf5 100644 --- a/client/src/HashReader.mts +++ b/client/src/HashReader.mts @@ -67,7 +67,7 @@ const metafile: Metafile = JSON.parse(data); // Walk the file, looking for the names of specific entry points. Transform // those into paths used to import these files. -let outputContents: Record = {}; +const outputContents: Record = {}; let num_found = 0; for (const output in metafile.outputs) { const outputInfo = metafile.outputs[output]; diff --git a/client/src/assert.mts b/client/src/assert.mts index 93d3ddc..f0d23a7 100644 --- a/client/src/assert.mts +++ b/client/src/assert.mts @@ -8,7 +8,7 @@ // [this](https://github.com/micromark/micromark/issues/87#issuecomment-908924233)). // Taken from the TypeScript // [docs](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#assertion-functions). -export function assert(condition: any, msg?: string): asserts condition { +export function assert(condition: boolean, msg?: string): asserts condition { if (!condition) { throw new Error(msg); } diff --git a/client/src/graphviz-webcomponent-setup.mts b/client/src/graphviz-webcomponent-setup.mts index 92a0ec0..32478e5 100644 --- a/client/src/graphviz-webcomponent-setup.mts +++ b/client/src/graphviz-webcomponent-setup.mts @@ -24,6 +24,7 @@ // Note that this must be in a separate module which is imported before the // graphviz webcomponent; see the [ESBuild // docs](https://esbuild.github.io/content-types/#real-esm-imports). +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ (window as any).graphvizWebComponent = { rendererUrl: "/static/bundled/renderer.js", delayWorkerLoading: true, diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index 0017324..77578f7 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -26,7 +26,7 @@ import { TinyMCE, } from "tinymce"; // TODO: The type of tinymce is broken; I don't know why. Here's a workaround. -export const tinymce = tinymce_ as any as TinyMCE; +export const tinymce = tinymce_ as unknown as TinyMCE; export { Editor }; // Default icons are required for TinyMCE 5.3 or above. @@ -77,7 +77,7 @@ export const init = async ( options: RawEditorOptions, ) => { // Merge the provided options with these default options. - let combinedOptions = Object.assign({}, options, { + const combinedOptions = Object.assign({}, options, { // See the list of // [plugins](https://www.tiny.cloud/docs/tinymce/6/plugins/). These must // be accompanied by the corresponding import above. @@ -156,7 +156,7 @@ export const init = async ( editor.ui.registry.addToggleButton("codeformat", { text: "<>", tooltip: "Format as code", - onAction: (_) => + onAction: () => editor.execCommand("mceToggleFormat", false, "code"), onSetup: (api) => { const changed = editor.formatter.formatChanged( diff --git a/client/tsconfig.json b/client/tsconfig.json index 5603c88..eb70fe9 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -34,5 +34,5 @@ "rootDir": "src", "strict": true }, - "exclude": ["node_modules", "static", "HashReader.js"] + "exclude": ["node_modules", "static", "HashReader.js", "eslint.config.js"] } diff --git a/extensions/VSCode/.eslintrc.yml b/extensions/VSCode/.eslintrc.yml deleted file mode 100644 index 591075f..0000000 --- a/extensions/VSCode/.eslintrc.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2025 Bryan A. Jones. -# -# This file is part of the CodeChat Editor. -# -# The CodeChat Editor is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# The CodeChat Editor 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 General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# the CodeChat Editor. If not, see -# [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). -# -# `.eslintrc.yml` - Configure ESLint for this project -# ============================================================================== -env: - commonjs: true - node: true -extends: - - standard - # See the - # [ESLint config prettier docs](https://github.com/prettier/eslint-config-prettier#installation) - # and its parent link, - # [integrating Prettier with linters](https://prettier.io/docs/en/integrating-with-linters.html). - - prettier -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: latest -plugins: - - "@typescript-eslint" -rules: - camelcase: off - # TypeScript already enforces this; otherwise, eslint complains that - # `NodeJS` is undefined. See - # [this GitHub issue](https://github.com/Chatie/eslint-config/issues/45#issuecomment-1003990077). - no-undef: off diff --git a/extensions/VSCode/eslint.config.js b/extensions/VSCode/eslint.config.js new file mode 100644 index 0000000..444980a --- /dev/null +++ b/extensions/VSCode/eslint.config.js @@ -0,0 +1,45 @@ +// Copyright (C) 2025 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. +// +// The CodeChat Editor is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// The CodeChat Editor 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 General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// the CodeChat Editor. If not, see +// [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). +// +// `.eslintrc.yml` -- Configure ESLint for this project +// ==================================================== +const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); +const eslint = require("@eslint/js"); +const { defineConfig } = require("eslint/config"); +const tseslint = require("typescript-eslint"); + +module.exports = defineConfig( + eslint.configs.recommended, + tseslint.configs.recommended, + eslintPluginPrettierRecommended, + defineConfig([ + { + // This must be the only key in this dict to be treated as a global ignore. Only global ignores can ignore directories. See the [docs](https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores). + ignores: ["src/third-party/**"], + }, + { + name: "local", + rules: { + "@typescript-eslint/no-unused-vars": [ + "off", + { argsIgnorePattern: "^_" }, + ], + }, + }, + ]), +); diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 8728ac3..d59c788 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -19,6 +19,7 @@ "CodeChat Editor", "Visual Studio Code extension" ], + "type": "commonjs", "license": "GPL-3.0-only", "main": "out/extension.js", "napi": { @@ -81,6 +82,7 @@ "devDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", + "@eslint/js": "^9.39.1", "@napi-rs/cli": "^3.5.0", "@tybys/wasm-util": "^0.10.1", "@types/escape-html": "^1.0.4", @@ -95,10 +97,12 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.5.4", "npm-run-all2": "^8.0.4", "ovsx": "^0.10.7", "prettier": "^3.7.4", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.49.0" }, "optionalDependencies": { "bufferutil": "^4.0.9" diff --git a/extensions/VSCode/src/extension.ts b/extensions/VSCode/src/extension.ts index 1e2bc9b..53a3653 100644 --- a/extensions/VSCode/src/extension.ts +++ b/extensions/VSCode/src/extension.ts @@ -36,7 +36,7 @@ import vscode, { TextEditor, TextEditorRevealType, } from "vscode"; -import { CodeChatEditorServer, initServer } from "./index"; +import { CodeChatEditorServer, initServer } from "./index.js"; // ### Local packages import { @@ -50,7 +50,7 @@ import { DEBUG_ENABLED, MAX_MESSAGE_LENGTH, } from "../../../client/src/debug_enabled.mjs"; -import { ResultErrTypes } from "../../../client/src/rust-types/ResultErrTypes"; +import { ResultErrTypes } from "../../../client/src/rust-types/ResultErrTypes.js"; // Globals // ----------------------------------------------------------------------------- @@ -407,7 +407,7 @@ export const activate = (context: vscode.ExtensionContext) => { // Update the cursor and scroll position if // provided. const editor = get_text_editor(doc); - let scroll_line = current_update.scroll_position; + const scroll_line = current_update.scroll_position; if (scroll_line !== undefined && editor) { ignore_selection_change = true; const scroll_position = new vscode.Position( @@ -427,7 +427,7 @@ export const activate = (context: vscode.ExtensionContext) => { ); } - let cursor_line = current_update.cursor_position; + const cursor_line = current_update.cursor_position; if (cursor_line !== undefined && editor) { ignore_selection_change = true; const cursor_position = new vscode.Position( @@ -484,7 +484,7 @@ export const activate = (context: vscode.ExtensionContext) => { // [Built-in Commands](https://code.visualstudio.com/api/references/commands). // For now, simply respond with an OK, since the // following doesn't work. - if (false) { + /** commands .executeCommand( "vscode.open", @@ -504,7 +504,7 @@ export const activate = (context: vscode.ExtensionContext) => { ], }), ); - } + */ await sendResult(id); } break; @@ -580,6 +580,7 @@ export const deactivate = async () => { // ----------------------------------------------------------------------------- // // Format a complex data structure as a string when in debug mode. +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ const format_struct = (complex_data_structure: any): string => DEBUG_ENABLED ? JSON.stringify( @@ -765,6 +766,7 @@ const get_text_editor = (doc: TextDocument): TextEditor | undefined => { } }; +/*eslint-disable-next-line @typescript-eslint/no-explicit-any */ const console_log = (...args: any) => { if (DEBUG_ENABLED) { console.log(...args); diff --git a/extensions/VSCode/tsconfig.json b/extensions/VSCode/tsconfig.json index 10de38a..d2e1caa 100644 --- a/extensions/VSCode/tsconfig.json +++ b/extensions/VSCode/tsconfig.json @@ -26,5 +26,5 @@ "rootDirs": ["src", "../../client/src/*"], "allowJs": true }, - "exclude": ["node_modules", ".vscode-test", "out", "server"] + "exclude": ["node_modules", ".vscode-test", "out", "server", "eslint.config.js"] } diff --git a/toc.md b/toc.md index 5062543..c765d45 100644 --- a/toc.md +++ b/toc.md @@ -86,9 +86,9 @@ Implementation 1. [HashReader.mts](client/src/HashReader.mts) 2. client/package.json 3. [client/tsconfig.json](client/tsconfig.json) - 4. [client/.eslintrc.yml](client/.eslintrc.yml) + 4. [client/eslint.config.js](client/eslint.config.js) 5. [client/.prettierrc.json5](client/.prettierrc.json5) - 6. [extensions/VSCode/.eslintrc.yml](extensions/VSCode/.eslintrc.yml) + 6. [extensions/VSCode/eslint.config.js](extensions/VSCode/eslint.config.js) 7. [extensions/VSCode/tsconfig.json](extensions/VSCode/tsconfig.json) 8. [extensions/VSCode/jsconfig.json](extensions/VSCode/jsconfig.json) 9. [extensions/VSCode/.vscodeignore](extensions/VSCode/.vscodeignore) From fef5d429587d564cb96f90813e6d7a46e005a815 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 11:20:17 -0600 Subject: [PATCH 04/29] Fix: respond to input, not dirty event in document-only mode. correctly save and restore cursor location replace tinymce_singleton --- client/src/CodeChatEditor.mts | 110 +++++++++++++++++-- client/src/CodeMirror-integration.mts | 149 +++++--------------------- 2 files changed, 133 insertions(+), 126 deletions(-) diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index be5756e..a821cbd 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -255,17 +255,19 @@ const _open_lp = async ( }); tinymce.activeEditor!.focus(); } else { - // Save and restore cursor/scroll location after an update per the - // [docs](https://www.tiny.cloud/docs/tinymce/6/apis/tinymce.dom.bookmarkmanager). - // However, this doesn't seem to work for the cursor location. - // Perhaps when TinyMCE normalizes the document, this gets lost? - const bm = tinymce.activeEditor!.selection.getBookmark(); + // Save the cursor location before the update, then restore it + // afterwards, if TinyMCE has focus. + const sel = tinymce.activeEditor!.hasFocus() + ? saveSelection() + : undefined; doc_content = "Plain" in source ? source.Plain.doc : apply_diff_str(doc_content, source.Diff.doc); tinymce.activeEditor!.setContent(doc_content); - tinymce.activeEditor!.selection.moveToBookmark(bm); + if (sel !== undefined) { + restoreSelection(sel); + } } mathJaxTypeset(codechat_body); scroll_to_line(cursor_line, scroll_line); @@ -336,6 +338,102 @@ const save_lp = (is_dirty: boolean) => { return update; }; +export const saveSelection = () => { + // Changing the text inside TinyMCE causes it to loose a selection tied to a + // specific node. So, instead store the selection as an array of indices in + // the childNodes array of each element: for example, a given selection is + // element 10 of the root TinyMCE div's children (selecting an ol tag), + // element 5 of the ol's children (selecting the last li tag), element 0 of + // the li's children (a text node where the actual click landed; the offset + // in this node is placed in `selection_offset`.) + const sel = window.getSelection(); + const selection_path = []; + const selection_offset = sel?.anchorOffset; + if (sel?.anchorNode) { + // Find a path from the selection back to the containing div. + for ( + let current_node = sel.anchorNode, is_first = true; + // Continue until we find the div which contains the doc block + // contents: either it's not an element (such as a div), ... + current_node.nodeType !== Node.ELEMENT_NODE || + // or it's not the doc block contents div. + (!(current_node as Element).classList.contains( + "CodeChat-doc-contents", + ) && + // Sometimes, the parent of a custom node (`wc-mermaid`) skips the TinyMCE div and returns the overall div. I don't know why. + !(current_node as Element).classList.contains("CodeChat-doc")); + current_node = current_node.parentNode!, is_first = false + ) { + // Store the index of this node in its' parent list of child + // nodes/children. Use `childNodes` on the first iteration, since + // the selection is often in a text node, which isn't in the + // `parents` list. However, using `childNodes` all the time causes + // trouble when reversing the selection -- sometimes, the + // `childNodes` change based on whether text nodes (such as a + // newline) are included are not after tinyMCE parses the content. + const p = current_node.parentNode; + // In case we go off the rails, give up if there are no more parents. + if (p === null) { + return { + selection_path: [], + selection_offset: 0, + }; + } + selection_path.unshift( + Array.prototype.indexOf.call( + is_first ? p.childNodes : p.children, + current_node, + ), + ); + } + } + return { selection_path, selection_offset }; +}; + +// Restore the selection produced by `saveSelection` to the active TinyMCE +// instance. +export const restoreSelection = ({ + selection_path, + selection_offset, +}: { + selection_path: number[]; + selection_offset?: number; +}) => { + // Copy the selection over to TinyMCE by indexing the selection path to find + // the selected node. + if (selection_path.length && typeof selection_offset === "number") { + let selection_node = tinymce.activeEditor!.getContentAreaContainer(); + for ( + ; + selection_path.length && + // If something goes wrong, bail out instead of producing + // exceptions. + selection_node !== undefined; + selection_node = + // As before, use the more-consistent `children` except for the + // last element, where we might be selecting a `text` node. + ( + selection_path.length > 1 + ? selection_node.children + : selection_node.childNodes + )[selection_path.shift()!]! as HTMLElement + ); + // Exit on failure. + if (selection_node === undefined) { + return; + } + // Use that to set the selection. + tinymce.activeEditor!.selection.setCursorLocation( + selection_node, + // In case of edits, avoid an offset past the end of the node. + Math.min( + selection_offset, + selection_node.nodeValue?.length ?? Number.MAX_VALUE, + ), + ); + } +}; + // Per // [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples), // here's the least bad way to choose between the control key and the command diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 7fccd1b..135837c 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -85,7 +85,12 @@ import { yaml } from "@codemirror/lang-yaml"; import { Editor, init, tinymce } from "./tinymce-config.mjs"; // ### Local -import { set_is_dirty, startAutosaveTimer } from "./CodeChatEditor.mjs"; +import { + set_is_dirty, + startAutosaveTimer, + saveSelection, + restoreSelection, +} from "./CodeChatEditor.mjs"; import { CodeChatForWeb, CodeMirror, @@ -100,7 +105,6 @@ import { show_toast } from "./show_toast.mjs"; // Globals // ----------------------------------------------------------------------------- let current_view: EditorView; -let tinymce_singleton: Editor | undefined; // This indicates that a call to `on_dirty` is scheduled, but hasn't run yet. let on_dirty_scheduled = false; @@ -461,10 +465,10 @@ class DocBlockWidget extends WidgetType { if (is_tinymce) { // Save the cursor location before the update, then restore it // afterwards, if TinyMCE has focus. - const sel = tinymce_singleton!.hasFocus() + const sel = tinymce.activeEditor!.hasFocus() ? saveSelection() : undefined; - tinymce_singleton!.setContent(this.contents); + tinymce.activeEditor!.setContent(this.contents); if (sel !== undefined) { restoreSelection(sel); } @@ -504,102 +508,6 @@ class DocBlockWidget extends WidgetType { } } -const saveSelection = () => { - // Changing the text inside TinyMCE causes it to loose a selection tied to a - // specific node. So, instead store the selection as an array of indices in - // the childNodes array of each element: for example, a given selection is - // element 10 of the root TinyMCE div's children (selecting an ol tag), - // element 5 of the ol's children (selecting the last li tag), element 0 of - // the li's children (a text node where the actual click landed; the offset - // in this node is placed in `selection_offset`.) - const sel = window.getSelection(); - const selection_path = []; - const selection_offset = sel?.anchorOffset; - if (sel?.anchorNode) { - // Find a path from the selection back to the containing div. - for ( - let current_node = sel.anchorNode, is_first = true; - // Continue until we find the div which contains the doc block - // contents: either it's not an element (such as a div), ... - current_node.nodeType !== Node.ELEMENT_NODE || - // or it's not the doc block contents div. - (!(current_node as Element).classList.contains( - "CodeChat-doc-contents", - ) && - // Sometimes, the parent of a custom node (`wc-mermaid`) skips the TinyMCE div and returns the overall div. I don't know why. - !(current_node as Element).classList.contains("CodeChat-doc")); - current_node = current_node.parentNode!, is_first = false - ) { - // Store the index of this node in its' parent list of child - // nodes/children. Use `childNodes` on the first iteration, since - // the selection is often in a text node, which isn't in the - // `parents` list. However, using `childNodes` all the time causes - // trouble when reversing the selection -- sometimes, the - // `childNodes` change based on whether text nodes (such as a - // newline) are included are not after tinyMCE parses the content. - const p = current_node.parentNode; - // In case we go off the rails, give up if there are no more parents. - if (p === null) { - return { - selection_path: [], - selection_offset: 0, - }; - } - selection_path.unshift( - Array.prototype.indexOf.call( - is_first ? p.childNodes : p.children, - current_node, - ), - ); - } - } - return { selection_path, selection_offset }; -}; - -// Restore the selection produced by `saveSelection` to the active TinyMCE -// instance. -const restoreSelection = ({ - selection_path, - selection_offset, -}: { - selection_path: number[]; - selection_offset?: number; -}) => { - // Copy the selection over to TinyMCE by indexing the selection path to find - // the selected node. - if (selection_path.length && typeof selection_offset === "number") { - let selection_node = tinymce_singleton!.getContentAreaContainer(); - for ( - ; - selection_path.length && - // If something goes wrong, bail out instead of producing - // exceptions. - selection_node !== undefined; - selection_node = - // As before, use the more-consistent `children` except for the - // last element, where we might be selecting a `text` node. - ( - selection_path.length > 1 - ? selection_node.children - : selection_node.childNodes - )[selection_path.shift()!]! as HTMLElement - ); - // Exit on failure. - if (selection_node === undefined) { - return; - } - // Use that to set the selection. - tinymce_singleton!.selection.setCursorLocation( - selection_node, - // In case of edits, avoid an offset past the end of the node. - Math.min( - selection_offset, - selection_node.nodeValue?.length ?? Number.MAX_VALUE, - ), - ); - } -}; - // Typeset the provided node; taken from the // [MathJax docs](https://docs.mathjax.org/en/latest/web/typeset.html#handling-asynchronous-typesetting). export const mathJaxTypeset = async ( @@ -700,7 +608,7 @@ const on_dirty = ( // the actual div. But I don't know how. mathJaxUnTypeset(contents_div); const contents = is_tinymce - ? tinymce_singleton!.save() + ? tinymce.activeEditor!.save() : contents_div.innerHTML; await mathJaxTypeset(contents_div); current_view.dispatch({ @@ -847,7 +755,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass( old_contents_div.className = "CodeChat-doc-contents"; old_contents_div.contentEditable = "true"; old_contents_div.replaceChildren( - ...tinymce_singleton!.getContentAreaContainer() + ...tinymce.activeEditor!.getContentAreaContainer() .childNodes, ); tinymce_div.parentNode!.insertBefore( @@ -863,7 +771,9 @@ export const DocBlockPlugin = ViewPlugin.fromClass( // Setting the content makes TinyMCE consider it dirty // -- ignore this "dirty" event. - tinymce_singleton!.setContent(contents_div.innerHTML); + tinymce.activeEditor!.setContent( + contents_div.innerHTML, + ); contents_div.remove(); // The new div is now a TinyMCE editor. Retypeset this. await mathJaxTypeset(tinymce_div); @@ -871,7 +781,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass( // This process causes TinyMCE to lose focus. Restore // that. However, this causes TinyMCE to lose the // selection, which the next bit of code then restores. - tinymce_singleton!.focus(false); + tinymce.activeEditor!.focus(false); // Copy the selection over to TinyMCE by indexing the // selection path to find the selected node. @@ -1091,22 +1001,21 @@ export const CodeMirror_load = async ( state, scrollTo: scrollSnapshot, }); - tinymce_singleton = ( - await init({ - selector: "#TinyMCE-inst", - setup: (editor: Editor) => { - // See the - // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). - editor.on("input", (event: Event) => { - const target_or_false = event.target as HTMLElement; - if (target_or_false == null) { - return; - } - setTimeout(() => on_dirty(target_or_false)); - }); - }, - }) - )[0]; + + await init({ + selector: "#TinyMCE-inst", + setup: (editor: Editor) => { + // See the + // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). + editor.on("input", (event: Event) => { + const target_or_false = event.target as HTMLElement; + if (target_or_false == null) { + return; + } + setTimeout(() => on_dirty(target_or_false)); + }); + }, + }); } else { // This contains a diff, instead of plain text. Apply the text diff. // From 49c4d435e5b1866ef01886f57bb4bdaeb8d11531 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 11:38:59 -0600 Subject: [PATCH 05/29] Docs: Update/reformat. --- CHANGELOG.md | 4 +++- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5172db..e46633c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,9 @@ Changelog [Github master](https://github.com/bjones1/CodeChat_Editor) -------------------------------------------------------------------------------- -* No changes. +* Fix loss of editing in the Client when in document-only mode. +* Correctly retain Client cursor position during IDE edits. +* Correctly translate tables cells containing blocks from HTML to Markdown. Version 0.1.44 -- 2025-Dec-09 -------------------------------------------------------------------------------- diff --git a/README.md b/README.md index d9d3596..2db004a 100644 --- a/README.md +++ b/README.md @@ -159,14 +159,14 @@ graph TD; A --> B; -The [Mermaid live editor](https://mermaid.live/) provide an focused environment for creating Mermaid chart. +The [Mermaid live editor](https://mermaid.live/) provide an focused environment +for creating Mermaid chart. ### Graphviz The CodeChat Editor supports diagrams created by [Graphviz](https://graphviz.org/). For example, - @@ -195,8 +195,8 @@ digraph { A -> B }
- -Several on-line tools, such as [Edotor](https://edotor.net/), provide a focused editing experience. +Several on-line tools, such as [Edotor](https://edotor.net/), provide a focused +editing experience. ### PlantUML From 80a302549df0121d9ed1279da334be98baf9bfe9 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 13:46:41 -0600 Subject: [PATCH 06/29] Fix: correct data corruption in Client. --- CHANGELOG.md | 4 +- client/src/CodeChatEditor.mts | 6 +- .../overall/overall_core/test_6/test.md | 3 + server/tests/overall_core/mod.rs | 109 ++++++++++++++++++ 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 server/tests/fixtures/overall/overall_core/test_6/test.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e46633c..3294d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,10 @@ Changelog -------------------------------------------------------------------------------- * Fix loss of editing in the Client when in document-only mode. +* Fix data corruption in the Client when in document-only mode and edits are + made in both the IDE and Client. * Correctly retain Client cursor position during IDE edits. -* Correctly translate tables cells containing blocks from HTML to Markdown. +* Correctly translate table cells containing blocks from HTML to Markdown. Version 0.1.44 -- 2025-Dec-09 -------------------------------------------------------------------------------- diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index a821cbd..a10dd27 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -312,14 +312,14 @@ const save_lp = (is_dirty: boolean) => { ) as HTMLDivElement; mathJaxUnTypeset(codechat_body); // To save a document only, simply get the HTML from the only Tiny - // MCE div. - const html = tinymce.activeEditor!.save(); + // MCE div. Update the `doc_contents` to stay in sync with the Server. + doc_content = tinymce.activeEditor!.save(); ( code_mirror_diffable as { Plain: CodeMirror; } ).Plain = { - doc: html, + doc: doc_content, doc_blocks: [], }; // Retypeset all math after saving the document. diff --git a/server/tests/fixtures/overall/overall_core/test_6/test.md b/server/tests/fixtures/overall/overall_core/test_6/test.md new file mode 100644 index 0000000..3b1386f --- /dev/null +++ b/server/tests/fixtures/overall/overall_core/test_6/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/overall_core/mod.rs b/server/tests/overall_core/mod.rs index d3b85a5..263cdeb 100644 --- a/server/tests/overall_core/mod.rs +++ b/server/tests/overall_core/mod.rs @@ -1476,3 +1476,112 @@ async fn test_5_core( Ok(()) } + +make_test!(test_6, test_6_core); + +// Verify that edits in document-only mode don't result in data corruption. +async fn test_6_core( + codechat_server: CodeChatEditorServer, + driver_ref: &WebDriver, + test_dir: &Path, +) -> Result<(), WebDriverError> { + let path = canonicalize(test_dir.join("test.md")).unwrap(); + let path_str = path.to_str().unwrap().to_string(); + let version = 0.0; + let orig_text = indoc!( + " + * a + + b + " + ) + .to_string(); + perform_loadfile( + &codechat_server, + test_dir, + "test.md", + Some((orig_text.clone(), version)), + false, + 6.0, + ) + .await; + + // Target the iframe containing the Client. + select_codechat_iframe(driver_ref).await; + + // Check the content. + let body_css = "#CodeChat-body .CodeChat-doc-contents"; + let body_content = driver_ref.find(By::Css(body_css)).await.unwrap(); + + // Perform edits. + body_content.send_keys("a").await.unwrap(); + let client_id = INITIAL_CLIENT_MESSAGE_ID; + let msg = codechat_server.get_message_timeout(TIMEOUT).await.unwrap(); + let client_version = get_version(&msg); + assert_eq!( + msg, + EditorMessage { + id: client_id, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "markdown".to_string(), + }, + source: CodeMirrorDiffable::Diff(CodeMirrorDiff { + doc: vec![StringDiff { + from: 0, + to: Some(4), + insert: "* aa\n".to_string(), + },], + doc_blocks: vec![], + version, + }), + version: client_version, + }), + cursor_position: None, + scroll_position: None + }) + } + ); + let version = client_version; + codechat_server.send_result(client_id, None).await.unwrap(); + //client_id += MESSAGE_ID_INCREMENT; + + // Send new text, which turns into a diff. + let ide_id = codechat_server + .send_message_update_plain( + path_str.clone(), + Some(( + indoc!( + " + * aaa + + b + " + ) + .to_string(), + version, + )), + Some(1), + None, + ) + .await + .unwrap(); + assert_eq!( + codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), + EditorMessage { + id: ide_id, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + //ide_id += MESSAGE_ID_INCREMENT; + + // Verify the updated text. + assert_eq!( + body_content.inner_html().await.unwrap(), + "
  • aaa

b

" + ); + + Ok(()) +} From e9fc25dc3ec8e388a4909415e0af2a3eb9a0e5d9 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 14:05:12 -0600 Subject: [PATCH 07/29] Freeze for release. --- client/package.json5 | 8 +- client/pnpm-lock.yaml | 67 +++++++--- extensions/VSCode/Cargo.lock | 218 ++++++------------------------- extensions/VSCode/Cargo.toml | 2 +- extensions/VSCode/package.json | 4 +- extensions/VSCode/pnpm-lock.yaml | 217 ++++++++++++++++++++---------- server/Cargo.lock | 195 +++++---------------------- server/Cargo.toml | 2 +- 8 files changed, 276 insertions(+), 437 deletions(-) diff --git a/client/package.json5 b/client/package.json5 index 1452135..fae31e8 100644 --- a/client/package.json5 +++ b/client/package.json5 @@ -43,7 +43,7 @@ url: 'https://github.com/bjones1/CodeChat_editor', }, type: 'module', - version: '0.1.44', + version: '0.1.45', pnpm: { overrides: { '@codemirror/view': '<6.39.0', @@ -72,9 +72,9 @@ codemirror: '^6.0.2', mathjax: '4.0.0', mermaid: '^11.12.2', - 'npm-check-updates': '^19.1.2', + 'npm-check-updates': '^19.2.0', 'pdfjs-dist': '^5.4.449', - tinymce: '^8.2.2', + tinymce: '^8.3.0', 'toastify-js': '^1.12.0', }, devDependencies: { @@ -83,7 +83,7 @@ '@types/dom-navigation': '^1.0.6', '@types/js-beautify': '^1.14.3', '@types/mocha': '^10.0.10', - '@types/node': '^24.10.2', + '@types/node': '^24.10.3', '@types/toastify-js': '^1.12.4', '@typescript-eslint/eslint-plugin': '^8.49.0', '@typescript-eslint/parser': '^8.49.0', diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 143a904..c471c69 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@codemirror/view': <6.39.0 + importers: .: @@ -57,7 +60,7 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: =6.38.8 + specifier: ^6.38.8 version: 6.38.8 '@hpcc-js/wasm-graphviz': specifier: ^1.16.0 @@ -75,21 +78,27 @@ importers: specifier: ^11.12.2 version: 11.12.2 npm-check-updates: - specifier: ^19.1.2 - version: 19.1.2 + specifier: ^19.2.0 + version: 19.2.0 pdfjs-dist: specifier: ^5.4.449 version: 5.4.449 tinymce: - specifier: ^8.2.2 - version: 8.2.2 + specifier: ^8.3.0 + version: 8.3.0 toastify-js: specifier: ^1.12.0 version: 1.12.0 devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 '@types/chai': specifier: ^5.2.3 version: 5.2.3 + '@types/dom-navigation': + specifier: ^1.0.6 + version: 1.0.6 '@types/js-beautify': specifier: ^1.14.3 version: 1.14.3 @@ -97,8 +106,8 @@ importers: specifier: ^10.0.10 version: 10.0.10 '@types/node': - specifier: ^24.10.2 - version: 24.10.2 + specifier: ^24.10.3 + version: 24.10.3 '@types/toastify-js': specifier: ^1.12.4 version: 1.12.4 @@ -135,6 +144,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.49.0 + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) packages: @@ -676,6 +688,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dom-navigation@1.0.6': + resolution: {integrity: sha512-4srBpebg8rFDm0LafYuWhZMgLoSr5J4gx4q1uaTqOXwVk00y+CkTdJ4SC57sR1cMhP0ZRjApMRdHVcFYOvPGTw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -694,8 +709,8 @@ packages: '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@24.10.2': - resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + '@types/node@24.10.3': + resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} @@ -1675,8 +1690,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - npm-check-updates@19.1.2: - resolution: {integrity: sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==} + npm-check-updates@19.2.0: + resolution: {integrity: sha512-XSIuL0FNgzXPDZa4lje7+OwHjiyEt84qQm6QMsQRbixNY5EHEM9nhgOjxjlK9jIbN+ysvSqOV8DKNS0zydwbdg==} engines: {node: '>=20.0.0', npm: '>=8.12.1'} hasBin: true @@ -1969,8 +1984,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinymce@8.2.2: - resolution: {integrity: sha512-CFDSZwciMvFGW2czK/Xig1HcOGpXI0qcQMIqaIcG2F4RuuTdf+LQTreyEZunAJoFTQ9L0KAugOqL7OA5TJkoAA==} + tinymce@8.3.0: + resolution: {integrity: sha512-9IjrEo8HD5mg9QP6/rKcPSIcyRNVSf5eiYTqapb/q1zAIoISRJgI2DJUs4CJgZvio0hmEH394xSHUJuoGf4Msw==} toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} @@ -2008,6 +2023,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2717,6 +2739,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dom-navigation@1.0.6': {} + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} @@ -2729,7 +2753,7 @@ snapshots: '@types/mocha@10.0.10': {} - '@types/node@24.10.2': + '@types/node@24.10.3': dependencies: undici-types: 7.16.0 @@ -3925,7 +3949,7 @@ snapshots: natural-compare@1.4.0: {} - npm-check-updates@19.1.2: {} + npm-check-updates@19.2.0: {} object-inspect@1.13.4: {} @@ -4252,7 +4276,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinymce@8.2.2: {} + tinymce@8.3.0: {} toastify-js@1.12.0: {} @@ -4306,6 +4330,17 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.49.0(eslint@9.39.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} ufo@1.6.1: {} diff --git a/extensions/VSCode/Cargo.lock b/extensions/VSCode/Cargo.lock index 81a7d93..6789e25 100644 --- a/extensions/VSCode/Cargo.lock +++ b/extensions/VSCode/Cargo.lock @@ -72,7 +72,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.2", + "rand", "sha1", "smallvec", "tokio", @@ -522,7 +522,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "codechat-editor-server" -version = "0.1.44" +version = "0.1.45" dependencies = [ "actix-files", "actix-http", @@ -540,13 +540,13 @@ dependencies = [ "dunce", "futures-util", "htmd", - "html5ever 0.36.1", + "html5ever", "imara-diff", "indoc", "lazy_static", "log", "log4rs", - "markup5ever_rcdom 0.36.0+unofficial", + "markup5ever_rcdom", "mime", "mime_guess", "minreq", @@ -555,9 +555,9 @@ dependencies = [ "path-slash", "pest", "pest_derive", - "phf 0.13.1", + "phf", "pulldown-cmark 0.13.0", - "rand 0.9.2", + "rand", "regex", "serde", "serde_json", @@ -573,7 +573,7 @@ dependencies = [ [[package]] name = "codechat-editor-vscode-extension" -version = "0.1.44" +version = "0.1.45" dependencies = [ "codechat-editor-server", "log", @@ -1062,22 +1062,12 @@ dependencies = [ [[package]] name = "htmd" version = "0.5.0" -source = "git+https://github.com/bjones1/htmd.git?branch=math-support#af6bc129874d33eb7f4d7fb6f0a39b04c668b2f5" +source = "git+https://github.com/bjones1/htmd.git?branch=table-fix#e6cbda98d19f8ff9a184ac3d4319d933b08f94a2" dependencies = [ - "html5ever 0.35.0", - "markup5ever_rcdom 0.35.0+unofficial", - "phf 0.13.1", -] - -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever 0.35.0", - "match_token", + "html5ever", + "indoc", + "markup5ever_rcdom", + "phf", ] [[package]] @@ -1087,7 +1077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" dependencies = [ "log", - "markup5ever 0.36.1", + "markup5ever", ] [[package]] @@ -1485,7 +1475,7 @@ dependencies = [ "log-mdc", "mock_instant", "parking_lot", - "rand 0.9.2", + "rand", "serde", "serde-value", "serde_json", @@ -1503,17 +1493,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms 0.1.3", -] - [[package]] name = "markup5ever" version = "0.36.1" @@ -1522,19 +1501,7 @@ checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" dependencies = [ "log", "tendril", - "web_atoms 0.2.0", -] - -[[package]] -name = "markup5ever_rcdom" -version = "0.35.0+unofficial" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bcd53df4748257345b8bc156d620340ce0f015ec1c7ef1cff475543888a31d" -dependencies = [ - "html5ever 0.35.0", - "markup5ever 0.35.0", - "tendril", - "xml5ever 0.35.0", + "web_atoms", ] [[package]] @@ -1543,21 +1510,10 @@ version = "0.36.0+unofficial" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5fc8802e8797c0dfdd2ce5c21aa0aee21abbc7b3b18559100651b3352a7b63" dependencies = [ - "html5ever 0.36.1", - "markup5ever 0.36.1", + "html5ever", + "markup5ever", "tendril", - "xml5ever 0.36.1", -] - -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "xml5ever", ] [[package]] @@ -1884,15 +1840,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" @@ -1900,38 +1847,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared 0.13.1", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", + "phf_generator", + "phf_shared", ] [[package]] @@ -1941,7 +1868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared", ] [[package]] @@ -1950,22 +1877,13 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.111", ] -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.13.1" @@ -2006,7 +1924,7 @@ dependencies = [ "hmac", "md-5", "memchr", - "rand 0.9.2", + "rand", "sha2", "stringprep", ] @@ -2106,15 +2024,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" @@ -2122,7 +2031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -2132,15 +2041,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "rand_core" version = "0.9.3" @@ -2407,19 +2310,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -2428,31 +2318,19 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", "serde", ] -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "string_cache_codegen" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -2682,11 +2560,11 @@ dependencies = [ "log", "parking_lot", "percent-encoding", - "phf 0.13.1", + "phf", "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.2", + "rand", "socket2 0.6.1", "tokio", "tokio-util", @@ -2987,28 +2865,16 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", -] - [[package]] name = "web_atoms" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd0c322f146d0f8aad130ce6c187953889359584497dac6561204c8e17bb43d" dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -3407,16 +3273,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "xml5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494" -dependencies = [ - "log", - "markup5ever 0.35.0", -] - [[package]] name = "xml5ever" version = "0.36.1" @@ -3424,7 +3280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f57dd51b88a4b9f99f9b55b136abb86210629d61c48117ddb87f567e51e66be7" dependencies = [ "log", - "markup5ever 0.36.1", + "markup5ever", ] [[package]] diff --git a/extensions/VSCode/Cargo.toml b/extensions/VSCode/Cargo.toml index e3c449b..1817e94 100644 --- a/extensions/VSCode/Cargo.toml +++ b/extensions/VSCode/Cargo.toml @@ -32,7 +32,7 @@ license = "GPL-3.0-only" name = "codechat-editor-vscode-extension" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.44" +version = "0.1.45" [lib] crate-type = ["cdylib"] diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index d59c788..28d1ca8 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -41,7 +41,7 @@ "type": "git", "url": "https://github.com/bjones1/CodeChat_Editor" }, - "version": "0.1.44", + "version": "0.1.45", "activationEvents": [ "onCommand:extension.codeChatEditorActivate", "onCommand:extension.codeChatEditorDeactivate" @@ -86,7 +86,7 @@ "@napi-rs/cli": "^3.5.0", "@tybys/wasm-util": "^0.10.1", "@types/escape-html": "^1.0.4", - "@types/node": "^24.10.2", + "@types/node": "^24.10.3", "@types/vscode": "1.61.0", "@typescript-eslint/eslint-plugin": "^8.49.0", "@typescript-eslint/parser": "^8.49.0", diff --git a/extensions/VSCode/pnpm-lock.yaml b/extensions/VSCode/pnpm-lock.yaml index 0cd404b..71dd123 100644 --- a/extensions/VSCode/pnpm-lock.yaml +++ b/extensions/VSCode/pnpm-lock.yaml @@ -18,9 +18,12 @@ importers: '@emnapi/runtime': specifier: ^1.7.1 version: 1.7.1 + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 '@napi-rs/cli': specifier: ^3.5.0 - version: 3.5.0(@emnapi/runtime@1.7.1)(@types/node@24.10.2) + version: 3.5.0(@emnapi/runtime@1.7.1)(@types/node@24.10.3) '@tybys/wasm-util': specifier: ^0.10.1 version: 0.10.1 @@ -28,8 +31,8 @@ importers: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^24.10.2 - version: 24.10.2 + specifier: ^24.10.3 + version: 24.10.3 '@types/vscode': specifier: 1.61.0 version: 1.61.0 @@ -60,6 +63,9 @@ importers: eslint-plugin-node: specifier: ^11.1.0 version: 11.1.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1))(eslint@9.39.1)(prettier@3.7.4) npm-run-all2: specifier: ^8.0.4 version: 8.0.4 @@ -72,6 +78,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.49.0 + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) optionalDependencies: bufferutil: specifier: ^4.0.9 @@ -989,6 +998,10 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1071,8 +1084,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@24.10.2': - resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + '@types/node@24.10.3': + resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1651,6 +1664,20 @@ packages: peerDependencies: eslint: '>=5.16.0' + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1711,6 +1738,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2440,6 +2470,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + prettier@3.7.4: resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} @@ -2701,6 +2735,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -2784,6 +2822,13 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3172,122 +3217,122 @@ snapshots: '@inquirer/ansi@2.0.2': {} - '@inquirer/checkbox@5.0.2(@types/node@24.10.2)': + '@inquirer/checkbox@5.0.2(@types/node@24.10.3)': dependencies: '@inquirer/ansi': 2.0.2 - '@inquirer/core': 11.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) '@inquirer/figures': 2.0.2 - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/confirm@6.0.2(@types/node@24.10.2)': + '@inquirer/confirm@6.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/core@11.0.2(@types/node@24.10.2)': + '@inquirer/core@11.0.2(@types/node@24.10.3)': dependencies: '@inquirer/ansi': 2.0.2 '@inquirer/figures': 2.0.2 - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/type': 4.0.2(@types/node@24.10.3) cli-width: 4.1.0 mute-stream: 3.0.0 signal-exit: 4.1.0 wrap-ansi: 9.0.2 optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/editor@5.0.2(@types/node@24.10.2)': + '@inquirer/editor@5.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/external-editor': 2.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/external-editor': 2.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/expand@5.0.2(@types/node@24.10.2)': + '@inquirer/expand@5.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/external-editor@2.0.2(@types/node@24.10.2)': + '@inquirer/external-editor@2.0.2(@types/node@24.10.3)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 '@inquirer/figures@2.0.2': {} - '@inquirer/input@5.0.2(@types/node@24.10.2)': + '@inquirer/input@5.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/number@4.0.2(@types/node@24.10.2)': + '@inquirer/number@4.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/password@5.0.2(@types/node@24.10.2)': + '@inquirer/password@5.0.2(@types/node@24.10.3)': dependencies: '@inquirer/ansi': 2.0.2 - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 - - '@inquirer/prompts@8.0.2(@types/node@24.10.2)': - dependencies: - '@inquirer/checkbox': 5.0.2(@types/node@24.10.2) - '@inquirer/confirm': 6.0.2(@types/node@24.10.2) - '@inquirer/editor': 5.0.2(@types/node@24.10.2) - '@inquirer/expand': 5.0.2(@types/node@24.10.2) - '@inquirer/input': 5.0.2(@types/node@24.10.2) - '@inquirer/number': 4.0.2(@types/node@24.10.2) - '@inquirer/password': 5.0.2(@types/node@24.10.2) - '@inquirer/rawlist': 5.0.2(@types/node@24.10.2) - '@inquirer/search': 4.0.2(@types/node@24.10.2) - '@inquirer/select': 5.0.2(@types/node@24.10.2) + '@types/node': 24.10.3 + + '@inquirer/prompts@8.0.2(@types/node@24.10.3)': + dependencies: + '@inquirer/checkbox': 5.0.2(@types/node@24.10.3) + '@inquirer/confirm': 6.0.2(@types/node@24.10.3) + '@inquirer/editor': 5.0.2(@types/node@24.10.3) + '@inquirer/expand': 5.0.2(@types/node@24.10.3) + '@inquirer/input': 5.0.2(@types/node@24.10.3) + '@inquirer/number': 4.0.2(@types/node@24.10.3) + '@inquirer/password': 5.0.2(@types/node@24.10.3) + '@inquirer/rawlist': 5.0.2(@types/node@24.10.3) + '@inquirer/search': 4.0.2(@types/node@24.10.3) + '@inquirer/select': 5.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/rawlist@5.0.2(@types/node@24.10.2)': + '@inquirer/rawlist@5.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/search@4.0.2(@types/node@24.10.2)': + '@inquirer/search@4.0.2(@types/node@24.10.3)': dependencies: - '@inquirer/core': 11.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) '@inquirer/figures': 2.0.2 - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/select@5.0.2(@types/node@24.10.2)': + '@inquirer/select@5.0.2(@types/node@24.10.3)': dependencies: '@inquirer/ansi': 2.0.2 - '@inquirer/core': 11.0.2(@types/node@24.10.2) + '@inquirer/core': 11.0.2(@types/node@24.10.3) '@inquirer/figures': 2.0.2 - '@inquirer/type': 4.0.2(@types/node@24.10.2) + '@inquirer/type': 4.0.2(@types/node@24.10.3) optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 - '@inquirer/type@4.0.2(@types/node@24.10.2)': + '@inquirer/type@4.0.2(@types/node@24.10.3)': optionalDependencies: - '@types/node': 24.10.2 + '@types/node': 24.10.3 '@isaacs/balanced-match@4.0.1': {} @@ -3304,9 +3349,9 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@napi-rs/cli@3.5.0(@emnapi/runtime@1.7.1)(@types/node@24.10.2)': + '@napi-rs/cli@3.5.0(@emnapi/runtime@1.7.1)(@types/node@24.10.3)': dependencies: - '@inquirer/prompts': 8.0.2(@types/node@24.10.2) + '@inquirer/prompts': 8.0.2(@types/node@24.10.3) '@napi-rs/cross-toolchain': 1.0.3 '@napi-rs/wasm-tools': 1.0.1 '@octokit/rest': 22.0.1 @@ -3691,6 +3736,8 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@pkgr/core@0.2.9': {} + '@rtsao/scc@1.1.0': {} '@secretlint/config-creator@10.2.2': @@ -3810,7 +3857,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@24.10.2': + '@types/node@24.10.3': dependencies: undici-types: 7.16.0 @@ -4557,6 +4604,15 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1))(eslint@9.39.1)(prettier@3.7.4): + dependencies: + eslint: 9.39.1 + prettier: 3.7.4 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.39.1) + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -4636,6 +4692,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5393,6 +5451,10 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + prettier@3.7.4: {} pump@3.0.3: @@ -5710,6 +5772,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + table@6.9.0: dependencies: ajv: 8.17.1 @@ -5824,6 +5890,17 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.7 + typescript-eslint@8.49.0(eslint@9.39.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} uc.micro@2.1.0: {} diff --git a/server/Cargo.lock b/server/Cargo.lock index 6261210..5557b32 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -746,7 +746,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "codechat-editor-server" -version = "0.1.44" +version = "0.1.45" dependencies = [ "actix-files", "actix-http", @@ -768,13 +768,13 @@ dependencies = [ "futures", "futures-util", "htmd", - "html5ever 0.36.1", + "html5ever", "imara-diff", "indoc", "lazy_static", "log", "log4rs", - "markup5ever_rcdom 0.36.0+unofficial", + "markup5ever_rcdom", "mime", "mime_guess", "minreq", @@ -783,7 +783,7 @@ dependencies = [ "path-slash", "pest", "pest_derive", - "phf 0.13.1", + "phf", "predicates", "pretty_assertions", "pulldown-cmark 0.13.0", @@ -1581,22 +1581,12 @@ dependencies = [ [[package]] name = "htmd" version = "0.5.0" -source = "git+https://github.com/bjones1/htmd.git?branch=math-support#af6bc129874d33eb7f4d7fb6f0a39b04c668b2f5" +source = "git+https://github.com/bjones1/htmd.git?branch=table-fix#e6cbda98d19f8ff9a184ac3d4319d933b08f94a2" dependencies = [ - "html5ever 0.35.0", - "markup5ever_rcdom 0.35.0+unofficial", - "phf 0.13.1", -] - -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever 0.35.0", - "match_token", + "html5ever", + "indoc", + "markup5ever_rcdom", + "phf", ] [[package]] @@ -1606,7 +1596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" dependencies = [ "log", - "markup5ever 0.36.1", + "markup5ever", ] [[package]] @@ -2126,9 +2116,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b484ba8d4f775eeca644c452a56650e544bf7e617f1d170fe7298122ead5222" +checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" dependencies = [ "zlib-rs", ] @@ -2247,17 +2237,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms 0.1.3", -] - [[package]] name = "markup5ever" version = "0.36.1" @@ -2266,19 +2245,7 @@ checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" dependencies = [ "log", "tendril", - "web_atoms 0.2.0", -] - -[[package]] -name = "markup5ever_rcdom" -version = "0.35.0+unofficial" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bcd53df4748257345b8bc156d620340ce0f015ec1c7ef1cff475543888a31d" -dependencies = [ - "html5ever 0.35.0", - "markup5ever 0.35.0", - "tendril", - "xml5ever 0.35.0", + "web_atoms", ] [[package]] @@ -2287,21 +2254,10 @@ version = "0.36.0+unofficial" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5fc8802e8797c0dfdd2ce5c21aa0aee21abbc7b3b18559100651b3352a7b63" dependencies = [ - "html5ever 0.36.1", - "markup5ever 0.36.1", + "html5ever", + "markup5ever", "tendril", - "xml5ever 0.36.1", -] - -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "xml5ever", ] [[package]] @@ -2605,15 +2561,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" @@ -2621,38 +2568,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared 0.13.1", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", + "phf_generator", + "phf_shared", ] [[package]] @@ -2662,7 +2589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared", ] [[package]] @@ -2671,22 +2598,13 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.111", ] -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.13.1" @@ -3501,19 +3419,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -3522,31 +3427,19 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", "serde", ] -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "string_cache_codegen" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -3863,7 +3756,7 @@ dependencies = [ "log", "parking_lot", "percent-encoding", - "phf 0.13.1", + "phf", "pin-project-lite", "postgres-protocol", "postgres-types", @@ -4354,28 +4247,16 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", -] - [[package]] name = "web_atoms" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd0c322f146d0f8aad130ce6c187953889359584497dac6561204c8e17bb43d" dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -4841,16 +4722,6 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" -[[package]] -name = "xml5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494" -dependencies = [ - "log", - "markup5ever 0.35.0", -] - [[package]] name = "xml5ever" version = "0.36.1" @@ -4858,7 +4729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f57dd51b88a4b9f99f9b55b136abb86210629d61c48117ddb87f567e51e66be7" dependencies = [ "log", - "markup5ever 0.36.1", + "markup5ever", ] [[package]] @@ -5009,9 +4880,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36134c44663532e6519d7a6dfdbbe06f6f8192bde8ae9ed076e9b213f0e31df7" +checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" [[package]] name = "zopfli" diff --git a/server/Cargo.toml b/server/Cargo.toml index 11cb563..3e636b4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,7 +32,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.44" +version = "0.1.45" # This library allows other packages to use core CodeChat Editor features. [lib] From d686b931d70c9af54d0482d7c80ba39853d269cf Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 10 Dec 2025 14:42:41 -0600 Subject: [PATCH 08/29] Manually fix pnpm version. --- client/pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index c471c69..777abcb 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.38.8 + specifier: <6.39.0 version: 6.38.8 '@hpcc-js/wasm-graphviz': specifier: ^1.16.0 From 734b4021ccff8e5d7e2cea9bd25263a1b2ecd62e Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Thu, 11 Dec 2025 09:15:25 -0600 Subject: [PATCH 09/29] Fix: switch back to using the Dirty message. set TinyMCE content using setContent, not DOM manipulation. --- client/src/CodeChatEditor.mts | 3 ++- client/src/CodeMirror-integration.mts | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index a10dd27..5de47fd 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -247,7 +247,8 @@ const _open_lp = async ( // [handling editor events](https://www.tiny.cloud/docs/tinymce/6/events/#handling-editor-events), // this is how to create a TinyMCE event handler. setup: (editor: Editor) => { - editor.on("input", (_event: Event) => { + editor.on("dirty", () => { + tinymce.activeEditor!.setDirty(false); is_dirty = true; startAutosaveTimer(); }); diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 135837c..8c67998 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -754,10 +754,8 @@ export const DocBlockPlugin = ViewPlugin.fromClass( const old_contents_div = document.createElement("div")!; old_contents_div.className = "CodeChat-doc-contents"; old_contents_div.contentEditable = "true"; - old_contents_div.replaceChildren( - ...tinymce.activeEditor!.getContentAreaContainer() - .childNodes, - ); + old_contents_div.innerHTML = + tinymce.activeEditor!.getContent(); tinymce_div.parentNode!.insertBefore( old_contents_div, null, @@ -1007,8 +1005,13 @@ export const CodeMirror_load = async ( setup: (editor: Editor) => { // See the // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). - editor.on("input", (event: Event) => { - const target_or_false = event.target as HTMLElement; + // This is triggered on edits (just as the `input` event), but also when applying formatting changes, inserting images, etc. that the above callback misses. + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ + editor.on("Dirty", (event: any) => { + // Get the div TinyMCE stores edits in. TODO: find + // documentation for `event.target.bodyElement`. + tinymce.activeEditor!.setDirty(false); + const target_or_false = event.target?.bodyElement; if (target_or_false == null) { return; } From 7bd582dafbb2fd058cb66c752254f1e18628983c Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Thu, 11 Dec 2025 09:18:12 -0600 Subject: [PATCH 10/29] Revert "Fix: remove unused is_user_change flag." This reverts commit 4326eda7b2b5c4573acdd4fc79eb2667247c36ca. --- client/src/CodeMirror-integration.mts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 8c67998..f98b511 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -196,6 +196,7 @@ export const docBlockField = StateField.define({ effect.value.indent, effect.value.delimiter, effect.value.content, + false, ), ...decorationOptions, }).range(effect.value.from, effect.value.to), @@ -282,6 +283,9 @@ export const docBlockField = StateField.define({ prev.spec.widget.contents, effect.value.contents, ), + // Assume this isn't a user change unless it's + // specified. + effect.value.is_user_change ?? false, ), ...decorationOptions, }).range(from, to), @@ -332,7 +336,12 @@ export const docBlockField = StateField.define({ contents, ]: CodeMirrorDocBlockTuple) => Decoration.replace({ - widget: new DocBlockWidget(indent, delimiter, contents), + widget: new DocBlockWidget( + indent, + delimiter, + contents, + false, + ), ...decorationOptions, }).range(from, to), ), @@ -372,6 +381,9 @@ type updateDocBlockType = { indent?: string; delimiter?: string; contents: string | StringDiff[]; + // True if this update comes from a user change, as opposed to an update + // received from the IDE. + is_user_change?: boolean; }; // Define an update. @@ -414,6 +426,7 @@ class DocBlockWidget extends WidgetType { readonly indent: string, readonly delimiter: string, readonly contents: string, + readonly is_user_change: boolean, ) { // TODO: I don't understand why I don't need to store the provided // parameters in the object: `this.indent = indent;`, etc. @@ -456,6 +469,9 @@ class DocBlockWidget extends WidgetType { updateDOM(dom: HTMLElement, _view: EditorView): boolean { // If this change was produced by a user edit, then the DOM was already // updated. Stop here. + if (this.is_user_change) { + return true; + } (dom.childNodes[0] as HTMLDivElement).innerHTML = this.indent; // The contents div could be a TinyMCE instance, or just a plain div. From 5458019565fe83d1c5dd9c05ed17aa9737123702 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Thu, 11 Dec 2025 12:05:26 -0600 Subject: [PATCH 11/29] Fix: update is_user_change to depend on noAutosaveAnnotation. Improve typing of Dirty event. --- client/src/CodeMirror-integration.mts | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index f98b511..96b3918 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -83,6 +83,7 @@ import { rust } from "@codemirror/lang-rust"; import { sql } from "@codemirror/lang-sql"; import { yaml } from "@codemirror/lang-yaml"; import { Editor, init, tinymce } from "./tinymce-config.mjs"; +import { EditorEvent, Events } from "tinymce"; // ### Local import { @@ -283,9 +284,8 @@ export const docBlockField = StateField.define({ prev.spec.widget.contents, effect.value.contents, ), - // Assume this isn't a user change unless it's - // specified. - effect.value.is_user_change ?? false, + // If autosave is allowed (meaning no autosave is not true), then this data came from the user, not the IDE. + tr.annotation(noAutosaveAnnotation) !== true, ), ...decorationOptions, }).range(from, to), @@ -1022,17 +1022,19 @@ export const CodeMirror_load = async ( // See the // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). // This is triggered on edits (just as the `input` event), but also when applying formatting changes, inserting images, etc. that the above callback misses. - /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ - editor.on("Dirty", (event: any) => { - // Get the div TinyMCE stores edits in. TODO: find - // documentation for `event.target.bodyElement`. - tinymce.activeEditor!.setDirty(false); - const target_or_false = event.target?.bodyElement; - if (target_or_false == null) { - return; - } - setTimeout(() => on_dirty(target_or_false)); - }); + editor.on( + "Dirty", + (event: EditorEvent) => { + // Get the div TinyMCE stores edits in. TODO: find + // documentation for `event.target.bodyElement`. + tinymce.activeEditor!.setDirty(false); + const target_or_false = event.target?.bodyElement; + if (target_or_false == null) { + return; + } + setTimeout(() => on_dirty(target_or_false)); + }, + ); }, }); } else { From a01ee6844e5e659507a6fded2514a7fc59a3a2a8 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Thu, 11 Dec 2025 12:25:01 -0600 Subject: [PATCH 12/29] Fix: factor out utility functions from overall tests. --- server/tests/overall.rs | 3 + server/tests/overall_common/mod.rs | 396 ++++++++++++++++++++++++++++ server/tests/overall_core/mod.rs | 397 +++-------------------------- 3 files changed, 438 insertions(+), 358 deletions(-) create mode 100644 server/tests/overall_common/mod.rs diff --git a/server/tests/overall.rs b/server/tests/overall.rs index 01df3db..c2c98d3 100644 --- a/server/tests/overall.rs +++ b/server/tests/overall.rs @@ -15,5 +15,8 @@ // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// `overall.rs` - test the overall system /// ====================================== +#[cfg(feature = "int_tests")] +mod overall_common; + #[cfg(feature = "int_tests")] mod overall_core; diff --git a/server/tests/overall_common/mod.rs b/server/tests/overall_common/mod.rs new file mode 100644 index 0000000..4b3b213 --- /dev/null +++ b/server/tests/overall_common/mod.rs @@ -0,0 +1,396 @@ +// Copyright (C) 2025 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. The CodeChat Editor is free +// software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// The CodeChat Editor 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 General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// the CodeChat Editor. If not, see +// [http://www.gnu.org/licenses](http://www.gnu.org/licenses). +/// `overall_core/mod.rs` - test the overall system +/// ============================================================================ +/// +/// These are functional tests of the overall system, performed by attaching a +/// testing IDE to generate commands then observe results, along with a browser +/// tester. +/// +/// Some subtleties of this approach: development dependencies aren't available +/// to integration tests. Therefore, this crate's `Cargo.toml` file includes the +/// `int_tests` feature, which enables crates needed only for integration +/// testing, while keeping these out of the final binary when compiling for +/// production. This means that the same crate appears both in +/// `dev-dependencies` and in `dependencies`, so it's available for both unit +/// tests and integration tests. In addition, any code used in integration tests +/// must be gated on the `int_tests` feature, since this code fails to compile +/// without that feature's crates enabled. Tests are implemented here, then +/// `use`d in `overall.rs`, so that a single `#[cfg(feature = "int_tests")]` +/// statement there gates everything in this file. See the +/// [test docs](https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-tests) +/// for the correct file and directory names. +/// +/// A second challenge revolves around the lack of an async `Drop` trait: the +/// web driver server should be started before any test, left running during all +/// tests, then terminated as the test program exits. The web driver must be +/// initialized before a test then stopped at the end of that test. Both are +/// ideal for this missing Drop trait. As a workaround: +/// +/// * The web driver server relies on the C `atexit` call to stop the server. +/// However, when tests fail, this doesn't get called, leaving the server +/// running. This causes the server to fail to start on the next test run, +/// since it's still running. Therefore, errors when starting the web driver +/// server are ignored by design. +/// * Tests are run in an async block, and any panics produced inside it are +/// caught using `catch_unwind()`. The driver is shut down before returning an +/// error due to the panic. +// Imports +// ----------------------------------------------------------------------------- +// +// ### Standard library +use std::{collections::HashMap, error::Error, path::Path, time::Duration}; + +// ### Third-party +use dunce::canonicalize; +use pretty_assertions::assert_eq; +use thirtyfour::{By, Key, WebDriver, WebElement}; + +// ### Local +use code_chat_editor::{ + cast, + ide::CodeChatEditorServer, + webserver::{ + EditorMessage, EditorMessageContents, MESSAGE_ID_INCREMENT, ResultOkTypes, + UpdateMessageContents, + }, +}; + +// Utilities +// ----------------------------------------------------------------------------- +// +// Not all messages produced by the server are ordered. To accommodate +// out-of-order messages, this class provides a way to `insert` expected +// messages, then wait until they're all be received (`assert_all_messages`). +pub struct ExpectedMessages(HashMap); + +impl ExpectedMessages { + pub fn new() -> ExpectedMessages { + ExpectedMessages(HashMap::new()) + } + + pub fn insert(&mut self, editor_message: EditorMessage) { + assert!( + self.0 + .insert(editor_message.id as i64, editor_message.message) + .is_none() + ); + } + + pub fn check(&mut self, editor_message: EditorMessage) { + if let Some(editor_message_contents) = self.0.remove(&(editor_message.id as i64)) { + assert_eq!(editor_message.message, editor_message_contents); + } else { + panic!( + "Message not found: looked for \n{:#?}\nin:\n{:#?}", + editor_message, self.0 + ); + } + } + + async fn _assert_message(&mut self, codechat_server: &CodeChatEditorServer, timeout: Duration) { + self.check(codechat_server.get_message_timeout(timeout).await.unwrap()); + } + + pub async fn assert_all_messages( + &mut self, + codechat_server: &CodeChatEditorServer, + timeout: Duration, + ) { + while !self.0.is_empty() { + self.check(codechat_server.get_message_timeout(timeout).await.unwrap()); + } + } +} + +// Time to wait for `ExpectedMessages`. +pub const TIMEOUT: Duration = Duration::from_millis(2000); + +// ### Test harness +// +// A test harness. It runs the webdriver, the Server, opens the Client, then +// runs provided tests. After testing finishes, it cleans up (handling panics +// properly). +// +// The goal was to pass the harness a function which runs the tests. This +// currently doesn't work, due to problems with lifetimes (see comments). So, +// implement this as a macro instead (kludge!). +#[macro_export] +macro_rules! harness { + // The name of the test function to call inside the harness. + ($func: ident) => { + pub async fn harness< + 'a, + F: FnOnce(CodeChatEditorServer, &'a WebDriver, &'a Path) -> Fut, + Fut: Future>, + >( + // The function which performs tests using thirtyfour. TODO: not + // used. + _f: F, + // The output from calling `prep_test_dir!()`. + prep_test_dir: (TempDir, PathBuf), + ) -> Result<(), Box> { + let (temp_dir, test_dir) = prep_test_dir; + // The logger gets configured by (I think) + // `start_webdriver_process`, which delegates to `selenium-manager`. + // Set logging level here. + unsafe { env::set_var("RUST_LOG", "debug") }; + // Start the webdriver. + let server_url = "http://localhost:4444"; + let mut caps = DesiredCapabilities::chrome(); + // Ensure the screen is wide enough for an 80-character line, used + // to word wrapping test in `test_client_updates`. Otherwise, this + // test send the End key to go to the end of the line...but it's not + // the end of the full line on a narrow screen. + caps.add_arg("--window-size=1920,768")?; + caps.add_arg("--headless")?; + // On Ubuntu CI, avoid failures, probably due to running Chrome as + // root. + #[cfg(target_os = "linux")] + if env::var("CI") == Ok("true".to_string()) { + caps.add_arg("--disable-gpu")?; + caps.add_arg("--no-sandbox")?; + } + if let Err(err) = start_webdriver_process(server_url, &caps, true) { + // Often, the "failure" is that the webdriver is already + // running. + eprintln!("Failed to start the webdriver process: {err:#?}"); + } + // Wait for the driver to start up. + sleep(Duration::from_millis(500)).await; + let driver = WebDriver::new(server_url, caps).await?; + let driver_clone = driver.clone(); + let driver_ref = &driver_clone; + + // Run the test inside an async, so we can shut down the driver + // before returning an error. Mark the function as unwind safe. + // though I'm not certain this is correct. Hopefully, it's good + // enough for testing. + let ret = AssertUnwindSafe(async move { + // ### Setup + let p = env::current_exe().unwrap().parent().unwrap().join("../.."); + set_root_path(Some(&p)).unwrap(); + let codechat_server = CodeChatEditorServer::new().unwrap(); + + // Get the resulting web page text. + let opened_id = codechat_server.send_message_opened(true).await.unwrap(); + pretty_assertions::assert_eq!( + codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), + EditorMessage { + id: opened_id, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + let em_html = codechat_server.get_message_timeout(TIMEOUT).await.unwrap(); + codechat_server.send_result(em_html.id, None).await.unwrap(); + + // Parse out the address to use. + let client_html = cast!(&em_html.message, EditorMessageContents::ClientHtml); + let find_str = "