diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2b2f3eeb..e111e669 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/.prettierignore b/.prettierignore index 8e87ea3b..dee2f55e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/README.md b/README.md index e3da35bd..7d88ad47 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Contributions to the code are welcome and encouraged! License ------- -Copyright (C) 2022 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/builder/.gitignore b/builder/.gitignore index e96184ee..2d3f357d 100644 --- a/builder/.gitignore +++ b/builder/.gitignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/builder/Cargo.toml b/builder/Cargo.toml index 1b0af726..2480f52f 100644 --- a/builder/Cargo.toml +++ b/builder/Cargo.toml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/builder/src/main.rs b/builder/src/main.rs index 4a3c115b..24fcfa87 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -311,13 +311,13 @@ fn patch_client_npm() -> io::Result<()> { // Copy across the parts of MathJax that are needed, since bundling it is // difficult. quick_copy_dir( - "../client/node_modules/mathjax/", + "../client/node_modules/@mathjax/src/bundle/", "../client/static/mathjax", None, )?; quick_copy_dir( - "../client/node_modules/mathjax-modern-font/chtml/", - "../client/static/mathjax-modern-font/chtml", + "../client/node_modules/@mathjax/mathjax-newcm-font/chtml/", + "../client/static/mathjax-newcm-font/chtml", None, )?; // Copy over the graphviz files needed. diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml index 0ef013aa..73de1d4a 100644 --- a/client/.eslintrc.yml +++ b/client/.eslintrc.yml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/client/.gitignore b/client/.gitignore index af96fcdc..67326985 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/client/.prettierignore b/client/.prettierignore index 0e979463..9330a1e3 100644 --- a/client/.prettierignore +++ b/client/.prettierignore @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/.prettierrc.json5 b/client/.prettierrc.json5 index 399e5800..61f802a3 100644 --- a/client/.prettierrc.json5 +++ b/client/.prettierrc.json5 @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/package-lock.json b/client/package-lock.json index 2cb5bfed..2d2c777d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "license": "GPL-3.0-or-later", "dependencies": { "@codemirror/lang-cpp": "^6", @@ -22,14 +22,14 @@ "@codemirror/lang-rust": "^6", "@codemirror/lang-xml": "^6", "@codemirror/view": "^6", + "@mathjax/mathjax-newcm-font": "4.0.0-rc.4", + "@mathjax/src": "4.0.0-rc.4", "codemirror": "^6", "graphviz-webcomponent": "^2", - "mathjax": "4.0.0-beta.7", - "mathjax-modern-font": "4.0.0-beta.7", "mermaid": "^11", "npm-check-updates": "^18", "pdfjs-dist": "^5", - "tinymce": "^7" + "tinymce": "^8" }, "devDependencies": { "@types/chai": "^5", @@ -940,9 +940,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { @@ -1237,6 +1237,24 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mathjax/mathjax-newcm-font": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.0.0-rc.4.tgz", + "integrity": "sha512-uPl8EKpV3L+ojeNwWE1VpLgdmsn7+Ok1/rkVZcp5NxSf1Qmmh6bIKQoWFwl4N0h+hgYafQwca3JfinyRVX2/eQ==", + "license": "Apache-2.0" + }, + "node_modules/@mathjax/src": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@mathjax/src/-/src-4.0.0-rc.4.tgz", + "integrity": "sha512-oRFauAXbQUFozwH+sAZY3NwLBIzf2vdibO9UmmdoJVL3HbpphB/NkyTX9iD87JM1ZorOFPUvfMrLqDfwSdH8fg==", + "license": "Apache-2.0", + "dependencies": { + "@mathjax/mathjax-newcm-font": "4.0.0-rc.4", + "mhchemparser": "^4.2.1", + "mj-context-menu": "^0.9.1", + "speech-rule-engine": "5.0.0-alpha.8" + } + }, "node_modules/@mermaid-js/parser": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", @@ -2053,12 +2071,12 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/acorn": { @@ -3408,9 +3426,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { @@ -3420,8 +3438,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4903,23 +4921,6 @@ "node": ">= 0.4" } }, - "node_modules/mathjax": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-4.0.0-beta.7.tgz", - "integrity": "sha512-Tk07n0f4fedb/YNXJ5bgHUIW+3ONyLkfUdoZ3wsUeW7yOt6SeAkGAjegICbHuyV3k7bcddWLzDetTyb5LLEwaA==", - "license": "Apache-2.0", - "dependencies": { - "@xmldom/xmldom": "^0.8.10", - "mathjax-modern-font": "^4.0.0-beta.7", - "wicked-good-xpath": "^1.3.0" - } - }, - "node_modules/mathjax-modern-font": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/mathjax-modern-font/-/mathjax-modern-font-4.0.0-beta.7.tgz", - "integrity": "sha512-mGrlxuFPRoHpGYyRaXJpfyM9k77XCpH11lIVLO7+VNTFJXIeqnMXv+AGFiYL/GftQpdhPlCg4zqd/JXlse9S0A==", - "license": "Apache-2.0" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4958,6 +4959,12 @@ "uuid": "^11.1.0" } }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4998,6 +5005,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mj-context-menu": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.9.1.tgz", + "integrity": "sha512-ECPcVXZFRfeYOxb1MWGzctAtnQcZ6nRucE3orfkKX7t/KE2mlXO2K/bq4BcCGOuhdz3Wg2BZDy2S8ECK73/iIw==", + "license": "Apache-2.0" + }, "node_modules/mlly": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", @@ -5792,6 +5805,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/speech-rule-engine": { + "version": "5.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-5.0.0-alpha.8.tgz", + "integrity": "sha512-Tfwe3yUEZJR2zhvnn0xQnZU0re/729y4RLimX0DgWznQb1GUXTEjn2BLXgtMYd0uzpSGpuCTPDb75tScGWR68w==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.8", + "commander": "14.0.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5949,9 +5985,9 @@ "license": "MIT" }, "node_modules/tinymce": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.9.1.tgz", - "integrity": "sha512-zaOHwmiP1EqTeLRXAvVriDb00JYnfEjWGPdKEuac7MiZJ5aiDMZ4Unc98Gmajn+PBljOmO1GKV6G0KwWn3+k8A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-8.0.0.tgz", + "integrity": "sha512-E1OwCXXCzmZLx6sQVeMHdb61Hsp+7AxWtYstXp7Yw59Et4AdHQ0N36n7InVaYDmq2aBlCM8qkTQYKEqKgecP3A==", "license": "GPL-2.0-or-later" }, "node_modules/to-regex-range": { diff --git a/client/package.json b/client/package.json index 183cb22c..0b46ed8f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "description": "The CodeChat Editor Client, part of a web-based literate programming editor (the CodeChat Editor).", "homepage": "https://github.com/bjones1/CodeChat_Editor", "type": "module", @@ -43,12 +43,12 @@ "@codemirror/view": "^6", "codemirror": "^6", "graphviz-webcomponent": "^2", - "mathjax": "4.0.0-beta.7", - "mathjax-modern-font": "4.0.0-beta.7", + "@mathjax/mathjax-newcm-font": "4.0.0-rc.4", + "@mathjax/src": "4.0.0-rc.4", "mermaid": "^11", "npm-check-updates": "^18", "pdfjs-dist": "^5", - "tinymce": "^7" + "tinymce": "^8" }, "repository": { "type": "git", diff --git a/client/src/CodeChatEditor-test.mts b/client/src/CodeChatEditor-test.mts index c17f2b75..2c996115 100644 --- a/client/src/CodeChatEditor-test.mts +++ b/client/src/CodeChatEditor-test.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index 1d28d65c..487b4437 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -118,7 +118,6 @@ declare global { allow_navigation: boolean; }; CodeChatEditor_test: any; - MathJax: any; } } @@ -248,7 +247,7 @@ const _open_lp = async ( // Disable autosave when updating the document. autosaveEnabled = false; clearAutosaveTimer(); - // Before calling any MathJax, make sure it's fully loaded. + // Before calling any MathJax, make sure it's fully loaded and the initial render is finished. await window.MathJax.startup.promise; // Per // the[docs](https://docs.mathjax.org/en/latest/web/typeset.html#updating-previously-typeset-content), diff --git a/client/src/CodeChatEditorFramework.mts b/client/src/CodeChatEditorFramework.mts index 5ac9238e..8f1a8343 100644 --- a/client/src/CodeChatEditorFramework.mts +++ b/client/src/CodeChatEditorFramework.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 2c35b7d7..9b813eb9 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -193,10 +193,14 @@ export const docBlockField = StateField.define({ effect.value.from, effect.value.from, (from, to_found, value) => { - // Only look for blocks whose from is as specified. `between` will also return blocks whose to matches -- for example, given from = 1, one doc block of [0, 1], and another of [1, 2], *both* will be found; we want only the [1, 2] doc block. + // Only look for blocks whose from is as specified. + // `between` will also return blocks whose to matches -- + // for example, given from = 1, one doc block of \[0, + // 1\], and another of \[1, 2\], *both* will be found; + // we want only the \[1, 2\] doc block. if (effect.value.from === from) { - // For the given `from`, there should be exactly one doc - // block. + // For the given `from`, there should be exactly one + // doc block. if (prev !== undefined) { console.error({ doc_blocks, effect }); assert( @@ -207,7 +211,10 @@ export const docBlockField = StateField.define({ prev = value; to = to_found; - // We could return `false` here to stop the search for efficiency. However, we let it continue in case there are two doc blocks with the same `from` value, so we can at least flag this error. + // We could return `false` here to stop the search + // for efficiency. However, we let it continue in + // case there are two doc blocks with the same + // `from` value, so we can at least flag this error. } }, ); @@ -477,9 +484,14 @@ export const mathJaxTypeset = async ( // An optional function to run when the typeset finishes. afterTypesetFunc: () => void = () => {}, ) => { + // Don't await this promise -- other MathJax processing may still be + // running. See the [release + // notes](https://github.com/mathjax/MathJax-src/releases/tag/4.0.0-rc.4#api). + window.MathJax.typesetPromise([node]); try { - await window.MathJax.typesetPromise([node]); - afterTypesetFunc(); + // Instead, this function calls `afterTypesetFunc` after it awaits all + // internal MathJax promises. + window.MathJax.whenReady(afterTypesetFunc); } catch (err: any) { console.log("Typeset failed: " + err.message); } @@ -734,9 +746,10 @@ export const DocBlockPlugin = ViewPlugin.fromClass( ; selection_path.length; selection_node = - // As before, use the more-consistent `children` - // except for the last element, where we might - // be selecting a `text` 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 diff --git a/client/src/EditorComponents.mts b/client/src/EditorComponents.mts index 4c87f6a8..4491ec2d 100644 --- a/client/src/EditorComponents.mts +++ b/client/src/EditorComponents.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/HashReader.mts b/client/src/HashReader.mts index 60948bb7..9671e47b 100644 --- a/client/src/HashReader.mts +++ b/client/src/HashReader.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/css/CodeChatEditor.css b/client/src/css/CodeChatEditor.css index 4f845c24..1422a70b 100644 --- a/client/src/css/CodeChatEditor.css +++ b/client/src/css/CodeChatEditor.css @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Bryan A. Jones. +/* Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/client/src/css/CodeChatEditorProject.css b/client/src/css/CodeChatEditorProject.css index 6121d84d..9c886e53 100644 --- a/client/src/css/CodeChatEditorProject.css +++ b/client/src/css/CodeChatEditorProject.css @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Bryan A. Jones. +/* Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/client/src/css/themes/light.css b/client/src/css/themes/light.css index 2a20a25d..bf28b5dc 100644 --- a/client/src/css/themes/light.css +++ b/client/src/css/themes/light.css @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Bryan A. Jones. +/* Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/client/src/graphviz-webcomponent-setup.mts b/client/src/graphviz-webcomponent-setup.mts index 640132b5..65147f34 100644 --- a/client/src/graphviz-webcomponent-setup.mts +++ b/client/src/graphviz-webcomponent-setup.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/shared_types.mts b/client/src/shared_types.mts index 8a953494..3a2f73a4 100644 --- a/client/src/shared_types.mts +++ b/client/src/shared_types.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index 9214e1bf..c4459d74 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/client/static/.gitignore b/client/static/.gitignore index ec6aa808..bb529539 100644 --- a/client/static/.gitignore +++ b/client/static/.gitignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # @@ -20,7 +20,7 @@ # ======================================= bundled/ mathjax/ -mathjax-modern-font/ +mathjax-newcm-font/ graphviz-webcomponent/ # CodeChat Editor lexer: python. See TODO. diff --git a/client/tsconfig.json b/client/tsconfig.json index d1acd713..670962a0 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/docs/changelog.md b/docs/changelog.md index 5d8adcca..b0ab2075 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,4 +1,4 @@ -Copyright (C) 2023 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. @@ -20,6 +20,14 @@ Changelog ========= * [Github master](https://github.com/bjones1/CodeChat_Editor): + * Fix indexing in diffs for characters that use more than one UTF-16 code + unit, such as πŸ˜„,πŸ‘‰πŸΏ,πŸ‘¨β€πŸ‘¦, and πŸ‡ΊπŸ‡³. + * Fix data corruption with adjacent doc blocks. + * Translate line endings when loading a file from disk. +* v0.1.23, 2025-Jul-24 + * Correct diff errors in IDE with CRLF line endings. + * Upgrade to newest release of MathJax, TinyMCE. +* v0.1.22, 2025-Jul-24Β  * Better support for opening a page in a web browser. * Support HTTP basic authentication to restrict access; use `codechat-editor-server serve --auth username:password`. diff --git a/docs/design.md b/docs/design.md index 865b9d5e..e668c375 100644 --- a/docs/design.md +++ b/docs/design.md @@ -213,7 +213,7 @@ added. License ------- -Copyright (C) 2022 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/docs/implementation.md b/docs/implementation.md index c1933cf5..64b51ad0 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1,4 +1,4 @@ -Copyright (C) 2023 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/docs/index.md b/docs/index.md index d4e7e4eb..ec54c9ff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -Copyright (C) 2023 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/docs/style_guide.cpp b/docs/style_guide.cpp index 9a423549..fc364119 100644 --- a/docs/style_guide.cpp +++ b/docs/style_guide.cpp @@ -5,7 +5,7 @@ // of the CodeChat Editor in literate programming. It should be viewed using the // CodeChat Editor. // -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/extensions/VSCode/.eslintrc.yml b/extensions/VSCode/.eslintrc.yml index f2853c81..ea76c368 100644 --- a/extensions/VSCode/.eslintrc.yml +++ b/extensions/VSCode/.eslintrc.yml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/extensions/VSCode/.gitignore b/extensions/VSCode/.gitignore index acdac515..dc31cccc 100644 --- a/extensions/VSCode/.gitignore +++ b/extensions/VSCode/.gitignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/extensions/VSCode/.vscodeignore b/extensions/VSCode/.vscodeignore index 8af78d46..e8705710 100644 --- a/extensions/VSCode/.vscodeignore +++ b/extensions/VSCode/.vscodeignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/extensions/VSCode/jsconfig.json b/extensions/VSCode/jsconfig.json index 3e844676..3d9d7a69 100644 --- a/extensions/VSCode/jsconfig.json +++ b/extensions/VSCode/jsconfig.json @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/extensions/VSCode/package-lock.json b/extensions/VSCode/package-lock.json index 50cfb695..8294d79d 100644 --- a/extensions/VSCode/package-lock.json +++ b/extensions/VSCode/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "license": "GPL-3.0-only", "dependencies": { "escape-html": "^1", @@ -472,9 +472,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { @@ -1154,24 +1154,24 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.0.tgz", - "integrity": "sha512-nr9wEiZCNYafGZ++uWFZgPlDX3Bi7u4T2d5swpaoMvc1G2toXsBfe7UNVwXZq5dvYDbQN7vDeb3ltlKQ8JnPNQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.1.tgz", + "integrity": "sha512-20fEcLPsXg81yWpApv4FQxrZmlFF/Ta7/kz1HGIL+pJo5cSTmkc+eCki3GpOPZIoZk0tbJU8hrlwUb91F+3SNQ==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.0.tgz", - "integrity": "sha512-L+fM2OTs17hRxPCLKUdPjHce7cJp81gV9ku53FCL+cXnq5bZx0XYYkqKdtC0jnXujkQmrTYU3SYFrb4DgXqbtA==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.1.tgz", + "integrity": "sha512-oollG/BHa07+mMt372amxHohteASC+Zxgollc1sZgiyxo4S6EuureV3a4QIQB0NecA+Ak3d0cl0WI/8nou38jw==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.2.0", - "@textlint/resolver": "15.2.0", - "@textlint/types": "15.2.0", + "@textlint/module-interop": "15.2.1", + "@textlint/resolver": "15.2.1", + "@textlint/types": "15.2.1", "chalk": "^4.1.2", "debug": "^4.4.1", "js-yaml": "^3.14.1", @@ -1214,27 +1214,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.0.tgz", - "integrity": "sha512-M3y1s2dZZH8PSHo4RUlnPOdK3qN90wmYGaEdy+il9/BQfrrift7S9R8lOfhHoPS0m9FEsnwyj3dQLkCUugPd9Q==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.1.tgz", + "integrity": "sha512-b/C/ZNrm05n1ypymDknIcpkBle30V2ZgE3JVqQlA9PnQV46Ky510qrZk6s9yfKgA3m1YRnAw04m8xdVtqjq1qg==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.0.tgz", - "integrity": "sha512-1UC+5bEtuoht7uu0uGofb7sX7j17Mvyst9InrRtI4XgKhh1uMZz5YFiMYpNwry1GgCZvq7Wyq1fqtEIsvYWqFw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.1.tgz", + "integrity": "sha512-FY3aK4tElEcOJVUsaMj4Zro4jCtKEEwUMIkDL0tcn6ljNcgOF7Em+KskRRk/xowFWayqDtdz5T3u7w/6fjjuJQ==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.0.tgz", - "integrity": "sha512-wpF+xjGJgJK2JiwUdYjuNZrbuas3KfC9VDnHKac6aBLFyrI1iXuXtuxKXQDFi5/hebACactSJOuVVbuQbdJZ1Q==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.1.tgz", + "integrity": "sha512-zyqNhSatK1cwxDUgosEEN43hFh3WCty9Zm2Vm3ogU566IYegifwqN54ey/CiRy/DiO4vMcFHykuQnh2Zwp6LLw==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.2.0" + "@textlint/ast-node-types": "15.2.1" } }, "node_modules/@tybys/wasm-util": { @@ -3027,9 +3027,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { @@ -3039,8 +3039,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 0acf8155..fe854993 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.22", + "version": "0.1.24", "publisher": "CodeChat", "engines": { "vscode": "^1.61.0" diff --git a/extensions/VSCode/src/extension.ts b/extensions/VSCode/src/extension.ts index 9ca0ff5a..268269ff 100644 --- a/extensions/VSCode/src/extension.ts +++ b/extensions/VSCode/src/extension.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -364,9 +364,9 @@ export const activate = (context: vscode.ExtensionContext) => { if (current_update.contents !== null) { const source = current_update.contents.source; - // Is this plain text, or a diff? - // This will produce a change event, which - // we'll ignore. + // Is this plain text, or a diff? This will + // produce a change event, which we'll + // ignore. ignore_text_document_change = true; // Use a workspace edit, since calls to // `TextEditor.edit` must be made to the @@ -389,8 +389,10 @@ export const activate = (context: vscode.ExtensionContext) => { assert("Diff" in source); const diffs = source.Diff.doc; for (const diff of diffs) { - // Convert from character offsets from the beginning of the document to a `Position` - // (line, then offset on that line) needed by VSCode. + // Convert from character offsets from the + // beginning of the document to a + // `Position` (line, then offset on that + // line) needed by VSCode. const from = doc.positionAt(diff.from); if (diff.to === undefined) { // This is an insert. @@ -408,7 +410,8 @@ export const activate = (context: vscode.ExtensionContext) => { } vscode.workspace.applyEdit(wse).then(() => ignore_text_document_change = false); } else { - // TODO: handle cursor/scroll position updates. + // TODO: handle cursor/scroll position + // updates. assert(false); } send_result(id); diff --git a/extensions/VSCode/tsconfig.json b/extensions/VSCode/tsconfig.json index 9032bf03..10de38ac 100644 --- a/extensions/VSCode/tsconfig.json +++ b/extensions/VSCode/tsconfig.json @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/.gitignore b/server/.gitignore index 633877c8..64ef3051 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/server/Cargo.lock b/server/Cargo.lock index 60c6869c..94d13f22 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -591,7 +591,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codechat-editor-server" -version = "0.1.22" +version = "0.1.24" dependencies = [ "actix-files", "actix-http", diff --git a/server/Cargo.toml b/server/Cargo.toml index 19a43a6c..e74e4a38 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # @@ -31,7 +31,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.22" +version = "0.1.24" # This library allows other packages to use core CodeChat Editor features. [lib] diff --git a/server/bt b/server/bt index de16787e..928babf1 100755 --- a/server/bt +++ b/server/bt @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/server/bt.ps1 b/server/bt.ps1 index e319a686..7ea31aa7 100644 --- a/server/bt.ps1 +++ b/server/bt.ps1 @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/server/dist-workspace.toml b/server/dist-workspace.toml index cb33a669..4ddbd54e 100644 --- a/server/dist-workspace.toml +++ b/server/dist-workspace.toml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/server/log4rs.yml b/server/log4rs.yml index 39326cb8..ff3752ed 100644 --- a/server/log4rs.yml +++ b/server/log4rs.yml @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Bryan A. Jones. +# Copyright (C) 2025 Bryan A. Jones. # # This file is part of the CodeChat Editor. # diff --git a/server/src/capture.rs b/server/src/capture.rs index 6872c830..3f8f7c15 100644 --- a/server/src/capture.rs +++ b/server/src/capture.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer.rs b/server/src/lexer.rs index 754e1c84..c06395fc 100644 --- a/server/src/lexer.rs +++ b/server/src/lexer.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer/lexer-walkthrough.md b/server/src/lexer/lexer-walkthrough.md index bfb69d4b..a3d78817 100644 --- a/server/src/lexer/lexer-walkthrough.md +++ b/server/src/lexer/lexer-walkthrough.md @@ -1,4 +1,4 @@ -Copyright (C) 2022 Bryan A. Jones. +Copyright (C) 2025 Bryan A. Jones. This file is part of the CodeChat Editor. diff --git a/server/src/lexer/pest/c.pest b/server/src/lexer/pest/c.pest index 4891c9a6..f2ddc708 100644 --- a/server/src/lexer/pest/c.pest +++ b/server/src/lexer/pest/c.pest @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer/pest/python.pest b/server/src/lexer/pest/python.pest index 0e96d79e..c0a45a1c 100644 --- a/server/src/lexer/pest/python.pest +++ b/server/src/lexer/pest/python.pest @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer/pest/shared.pest b/server/src/lexer/pest/shared.pest index 1f8b5911..f3034cd8 100644 --- a/server/src/lexer/pest/shared.pest +++ b/server/src/lexer/pest/shared.pest @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer/pest_parser.rs b/server/src/lexer/pest_parser.rs index 5b14e766..f3c0c536 100644 --- a/server/src/lexer/pest_parser.rs +++ b/server/src/lexer/pest_parser.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/lexer/supported_languages.rs b/server/src/lexer/supported_languages.rs index ba4a5573..c77045b8 100644 --- a/server/src/lexer/supported_languages.rs +++ b/server/src/lexer/supported_languages.rs @@ -1,4 +1,4 @@ -/// Copyright (C) 2023 Bryan A. Jones. +/// 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 diff --git a/server/src/lexer/tests.rs b/server/src/lexer/tests.rs index 8428f13e..a3f89738 100644 --- a/server/src/lexer/tests.rs +++ b/server/src/lexer/tests.rs @@ -1,4 +1,4 @@ -/// Copyright (C) 2023 Bryan A. Jones. +/// 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 diff --git a/server/src/lib.rs b/server/src/lib.rs index 7baa051b..f1834d54 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,4 +1,4 @@ -/// Copyright (C) 2023 Bryan A. Jones. +/// 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 diff --git a/server/src/main.rs b/server/src/main.rs index 6e59cf97..cecc9aa6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -79,7 +79,8 @@ enum Commands { #[arg(short, long)] log: Option, - /// Define the username:password used to limit access to the server. By default, access is unlimited. + /// Define the username:password used to limit access to the server. By + /// default, access is unlimited. #[arg(short, long, value_parser = parse_credentials)] auth: Option, }, @@ -100,7 +101,10 @@ enum Commands { impl Cli { fn run(self, addr: &SocketAddr) -> Result<(), Box> { match &self.command { - Commands::Serve { log, auth: credentials } => { + Commands::Serve { + log, + auth: credentials, + } => { #[cfg(debug_assertions)] if let Some(TestMode::Sleep) = self.test_mode { // For testing, don't start the server at all. @@ -297,7 +301,8 @@ impl Cli { const PORT_RANGE: RangeInclusive = 1..=65535; -// Copied from the [clap docs](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#validated-values). +// Copied from the [clap +// docs](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#validated-values). fn port_in_range(s: &str) -> Result { let port: usize = s .parse() diff --git a/server/src/processing.rs b/server/src/processing.rs index 08943e4f..455c25bc 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -128,15 +128,16 @@ pub enum CodeMirrorDocBlockTransaction { // `CodeMirrorDocBlockTuple`. #[ts(as = "CodeMirrorDocBlockTuple")] pub struct CodeMirrorDocBlock { - // From -- the starting character this doc block is anchored to. + /// The starting character this doc block is anchored to, measured in UTF-16 + /// code units. `to` is measured the same way. pub from: usize, - // To -- the ending character this doc block is anchored to. + /// The ending character this doc block is anchored to. pub to: usize, - // Indent. + /// Indent. pub indent: String, - // Delimiter. + /// Delimiter. pub delimiter: String, - // Contents. + /// Contents. pub contents: String, } @@ -144,21 +145,19 @@ pub struct CodeMirrorDocBlock { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TS)] #[ts(optional_fields)] pub struct CodeMirrorDocBlockUpdate { - /// From -- the starting character this doc block is anchored to before this - /// update. In the JSON encoding, there's little gain from making this an - /// `Option`, since `undefined` takes more characters than most line - /// numbers. + /// The starting character this doc block is anchored to before this update. + /// Like `CodeMirrorDocBlock`, units for this, `from_update`, and `to` are + /// in UTF-16 code units. pub from: usize, /// The starting character this doc block is anchored to after this update. pub from_new: usize, - /// To -- the ending character this doc block is anchored to. Likewise, - /// avoid using an `Option` here. + /// The ending character this doc block is anchored to. pub to: usize, - /// Indent, or None if unchanged. Since the indent may be many characters, - /// use an `Option` here. + /// `None` if the indent is unchanged. Since the indent may be many + /// characters, use an `Option` here. #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option, - /// Delimiter. Again, this is usually too short to merit an `Option`. + /// Delimiter. pub delimiter: String, /// Contents, as a diff of the previous contents. pub contents: Vec, @@ -193,7 +192,7 @@ pub struct StringDiff { #[derive(Debug, PartialEq)] pub enum TranslationResults { /// This file is unknown to and therefore not supported by the CodeChat - // Editor. + /// Editor. Unknown, /// This is a CodeChat Editor file but it contains errors that prevent its /// translation. The string contains the error message. @@ -395,26 +394,46 @@ pub fn codechat_for_web_to_source( code_doc_block_vec_to_source(&code_doc_block_vec, lexer) } +/// Return the byte index of `s[u16_16_index]`, where the indexing operation is +/// in UTF-16 code units. +fn byte_index_of(s: &str, utf_16_index: usize) -> usize { + let mut byte_index = 0; + let mut current_index = 0; + for c in s.chars() { + if current_index >= utf_16_index { + return byte_index; + } + current_index += c.len_utf16(); + byte_index += c.len_utf8(); + } + // This index refers to the end of the string -- return that. + s.len() +} + /// Translate from CodeMirror to CodeDocBlocks. fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec { let doc_blocks = &code_mirror.doc_blocks; - // A CodeMirror "document" is really source code. Convert it from UTF-8 - // bytes to an array of characters, which is indexable by character. - let code: Vec = code_mirror.doc.chars().collect(); + // Translate between UTF-16 code units (the `from` and `to` provided by + // CodeMirror) and byte indexes (which Rust uses). Keep track of the current + // byte index/UTF-16 index; we always move forward from that location. + let mut byte_index: usize = 0; + let mut utf16_index: usize = 0; let mut code_doc_block_arr: Vec = Vec::new(); - // Keep track of the to index of the previous doc block. Since we haven't - // processed any doc blocks, start at 0. - let mut code_index: usize = 0; // Walk through each doc block, inserting the previous code block followed // by the doc block. for codemirror_doc_block in doc_blocks { + // Translate `from`. + let byte_index_prev = byte_index; + byte_index += byte_index_of( + &code_mirror.doc[byte_index..], + codemirror_doc_block.from - utf16_index, + ); + utf16_index = codemirror_doc_block.from; // Append the code block, unless it's empty. - let code_contents = &code[code_index..codemirror_doc_block.from]; + let code_contents = &code_mirror.doc[byte_index_prev..byte_index]; if !code_contents.is_empty() { - // Convert back from a character array to a string. - let s: String = code_contents.iter().collect(); - code_doc_block_arr.push(CodeDocBlock::CodeBlock(s.to_string())) + code_doc_block_arr.push(CodeDocBlock::CodeBlock(code_contents.to_string())) } // Append the doc block. code_doc_block_arr.push(CodeDocBlock::DocBlock(DocBlock { @@ -423,15 +442,18 @@ fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec contents: codemirror_doc_block.contents.to_string(), lines: 0, })); - code_index = codemirror_doc_block.to; + // Translate `to`. + byte_index += byte_index_of( + &code_mirror.doc[byte_index..], + codemirror_doc_block.to - utf16_index, + ); + utf16_index = codemirror_doc_block.to; } // See if there's a code block after the last doc block. - let code_contents = &code[code_index..]; + let code_contents = &code_mirror.doc[byte_index..]; if !code_contents.is_empty() { - // Convert back from a character array to a string. - let s: String = code_contents.iter().collect(); - code_doc_block_arr.push(CodeDocBlock::CodeBlock(s.to_string())); + code_doc_block_arr.push(CodeDocBlock::CodeBlock(code_contents.to_string())); } code_doc_block_arr @@ -850,7 +872,19 @@ pub fn diff_str(before: &str, after: &str) -> Vec { let count_before_chars = |lines: Range| { input.before[lines.start as usize..lines.end as usize] .iter() - .map(|&line| input.interner[line].chars().count()) + .map(|&line| { + input.interner[line].chars().fold( + // Count offsets into the string in UTF-16 code units, + // since the offsets produced are used by the Client + // ([JavaScript uses + // UTF-16](https://developer.mozilla.org/en-US/docs/Glossary/UTF-16#utf-16_in_javascript), + // as does + // [CodeMirror](https://codemirror.net/docs/guide/#document-offsets)) + // and VSCode (also JavaScript). + 0, + |acc, e| acc + e.len_utf16(), + ) + }) .sum::() }; // Sum characters between the last change and this change. @@ -918,7 +952,7 @@ pub fn diff_code_mirror_doc_blocks( // This compare all fields, not just the `contents`, of two // `CodeMirrorDocBlock`s. It should be applied to every entry that the // `diff` function sees as equal. - let mut diff_all = |hunk: &Hunk, change_spec: &mut Vec| { + let mut diff_all = |hunk: &Hunk, change_specs: &mut Vec| { // First, compare blocks from the previous point until this point. The // diff used only compares contents; this checks everything. while prev_before_range_end < hunk.before.start && prev_after_range_end < hunk.after.start { @@ -936,7 +970,7 @@ pub fn diff_code_mirror_doc_blocks( // Second phase: if before and after are different, insert an // update. if prev_before_range_start_val != prev_after_range_start_val { - change_spec.push(CodeMirrorDocBlockTransaction::Update( + change_specs.push(CodeMirrorDocBlockTransaction::Update( CodeMirrorDocBlockUpdate { from: prev_before_range_start_val.from, from_new: prev_after_range_start_val.from, @@ -964,10 +998,10 @@ pub fn diff_code_mirror_doc_blocks( prev_after_range_end = hunk.after.end; }; - let mut change_spec = Vec::new(); + let mut change_specs = Vec::new(); let diff = Diff::compute(Algorithm::Histogram, &input); for hunk in diff.hunks() { - diff_all(&hunk, &mut change_spec); + diff_all(&hunk, &mut change_specs); // Update the `prev` values so we start processing immediately after // this change. @@ -982,7 +1016,7 @@ pub fn diff_code_mirror_doc_blocks( // still correct. if before_index < hunk.before.end { let before_val = &before[before_index as usize]; - change_spec.push(CodeMirrorDocBlockTransaction::Update( + change_specs.push(CodeMirrorDocBlockTransaction::Update( CodeMirrorDocBlockUpdate { from: before_val.from, from_new: after_val.from, @@ -999,7 +1033,7 @@ pub fn diff_code_mirror_doc_blocks( before_index += 1; } else { // Otherwise, this in an insert. - change_spec.push(CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { + change_specs.push(CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: after_val.from, to: after_val.to, indent: after_val.indent.clone(), @@ -1010,7 +1044,7 @@ pub fn diff_code_mirror_doc_blocks( } if before_index < hunk.before.end { - change_spec.push(CodeMirrorDocBlockTransaction::Delete( + change_specs.push(CodeMirrorDocBlockTransaction::Delete( CodeMirrorDocBlockDelete { from: before[before_index as usize].from, }, @@ -1025,13 +1059,48 @@ pub fn diff_code_mirror_doc_blocks( before: (before.len() as u32..0), after: after.len() as u32..0, }, - &mut change_spec, + &mut change_specs, ); - // Apply the changes in reverse order, so that later from/to ranges don't - // overlap with earlier ranges during the update. - change_spec.reverse(); - change_spec + // If two doc blocks immediately follow each other: `# foo\n # bar\n`, for + // example, and a line is inserted before both, then a problem occurs when + // applying the change: applying the change to the first block sets its + // `from` value to the `from` value for the second block. This violates a + // doc blocks invariant -- each doc block must have a unique `from` value; + // therefore, these two doc blocks can no longer be distinguished, making it + // impossible to apply the change to the second doc block. More generally, + // this can occur with an insert before a series of blocks which immediately + // follow each other. Therefore, look for sequences of updates where + // `from_new` of a previous entry == `from` of the current entry and swap + // these sequences. + let mut immediate_sequence_start_index: Option = None; + for index in 1..change_specs.len() { + if let CodeMirrorDocBlockTransaction::Update(prev_update) = &change_specs[index - 1] + && let CodeMirrorDocBlockTransaction::Update(update) = &change_specs[index] + && prev_update.from_new == update.from + && prev_update.from < prev_update.from_new + { + // We've found two elements in a sequence. + if immediate_sequence_start_index.is_none() { + // This is the start of the sequence -- mark it. + immediate_sequence_start_index = Some(index - 1); + } + } else { + // These two elements aren't a sequence. + if let Some(prev_index) = immediate_sequence_start_index { + // This is the end of a sequence. Reverse it. + change_specs[prev_index..index].reverse(); + } + // Mark that there's no sequence now. + immediate_sequence_start_index = None; + } + } + // If a sequence ended at the end of the document, process it. + if let Some(prev_index) = immediate_sequence_start_index { + change_specs[prev_index..].reverse(); + } + + change_specs } // Goal: make it easy to update the data structure. We update on every diff --git a/server/src/processing/tests.rs b/server/src/processing/tests.rs index 28cc0983..6c8b7d94 100644 --- a/server/src/processing/tests.rs +++ b/server/src/processing/tests.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -37,9 +37,9 @@ use crate::{ prep_test_dir, processing::{ CodeMirrorDiffable, CodeMirrorDocBlockDelete, CodeMirrorDocBlockTransaction, - CodeMirrorDocBlockUpdate, code_doc_block_vec_to_source, code_mirror_to_code_doc_blocks, - codechat_for_web_to_source, diff_code_mirror_doc_blocks, diff_str, - source_to_codechat_for_web, + CodeMirrorDocBlockUpdate, byte_index_of, code_doc_block_vec_to_source, + code_mirror_to_code_doc_blocks, codechat_for_web_to_source, diff_code_mirror_doc_blocks, + diff_str, source_to_codechat_for_web, }, test_utils::stringit, }; @@ -143,14 +143,24 @@ fn test_codemirror_to_code_doc_blocks_py() { vec![build_doc_block("", "#", "Test")] ); - // Pass one doc block containing Unicode. + // Pass a code and doc block containing Unicode. assert_eq!( run_test( "python", "Οƒ\n", - vec![build_codemirror_doc_block(1, 2, "", "#", "Test")], + vec![build_codemirror_doc_block(1, 2, "", "#", "β‘€")], ), - vec![build_code_block("Οƒ"), build_doc_block("", "#", "Test")] + vec![build_code_block("Οƒ"), build_doc_block("", "#", "β‘€")] + ); + + // Pass one doc block containing Unicode composed of two UTF-16 code units. + assert_eq!( + run_test( + "python", + "πŸ˜„\n", + vec![build_codemirror_doc_block(2, 3, "", "#", "πŸ‘¨β€πŸ‘¦")], + ), + vec![build_code_block("πŸ˜„"), build_doc_block("", "#", "πŸ‘¨β€πŸ‘¦")] ); // A code block then a doc block @@ -722,26 +732,20 @@ fn test_find_path_to_toc_1() { temp_dir.close().unwrap(); } +// Given a diff, apply it to the provided `before` string to produce the +// resulting `after` string. fn apply_str_diff(before: &str, diffs: &[StringDiff]) -> String { let mut before = before.to_string(); // Walk from the last diff to the first. for diff in diffs.iter().rev() { // Convert from a character index to a byte index. If the index is past // the end of the string, report the length of the string. - let from_index = before - .char_indices() - .nth(diff.from) - .unwrap_or((before.len(), 'x')) - .0; + let from_index = byte_index_of(&before, diff.from); if let Some(to) = diff.to { - let to_index = before - .char_indices() - .nth(to) - .unwrap_or((before.len(), 'x')) - .0; + let to_index = byte_index_of(&before, to); before.replace_range(from_index..to_index, &diff.insert); } else { - before.insert_str(diff.from, &diff.insert); + before.insert_str(from_index, &diff.insert); }; } before @@ -861,11 +865,21 @@ fn test_diff_1() { // Test with unicode. test_diff( - "β‘ \nβ‘‘β‘’β‘£\nβ‘€β‘₯", - "β‘ \n❷❸\nβ‘€β‘₯", + // This encodes to the following UTF-16 string: + // + // ``` + // \ud83d \ude04 \u000a \ud83d \udc49 \ud83c \udfff \ud83d \udc68 \u200d \ud83d \udc66 \ud83c \uddfa \ud83c \uddf3 \u000a \u2464 \u2465 + // index: 0 1 2 [ 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ] 17 18 + // char: ---πŸ˜„--- \n ----------πŸ‘‰πŸΏ--------- --------------πŸ‘¨β€πŸ‘¦-------------- -----------πŸ‡ΊπŸ‡³---------- \n β‘€ β‘₯ + // ``` + // + // These are taken from the [MDN UTF-16 + // docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters). + "πŸ˜„\nπŸ‘‰πŸΏπŸ‘¨β€πŸ‘¦πŸ‡ΊπŸ‡³\nβ‘€β‘₯", + "πŸ˜„\n❷❸\nβ‘€β‘₯", &[StringDiff { - from: 2, - to: Some(6), + from: 3, + to: Some(17), insert: "❷❸\n".to_string(), }], ); @@ -988,13 +1002,6 @@ fn test_diff_2() { // The "dumb" (non-diff) algorithm see this as a replace followed by an // insert, not a single insert. vec![ - CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { - from: 11, - to: 12, - indent: "".to_string(), - delimiter: "#".to_string(), - contents: "test".to_string() - }), CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { from: 11, from_new: 10, @@ -1003,6 +1010,13 @@ fn test_diff_2() { delimiter: "#".to_string(), contents: vec![] }), + CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { + from: 11, + to: 12, + indent: "".to_string(), + delimiter: "#".to_string(), + contents: "test".to_string() + }), ] ); @@ -1090,4 +1104,137 @@ fn test_diff_2() { CodeMirrorDocBlockDelete { from: 11 } )] ); + + // Test inserts before adjacent doc blocks. + // + // First, with end adjacent doc blocks at the end of the document. + let before = vec![ + build_codemirror_doc_block(10, 11, "", "#", "test1"), + build_codemirror_doc_block(11, 12, " ", "#", "test2"), + build_codemirror_doc_block(12, 13, "", "#", "test3"), + ]; + let after = vec![ + build_codemirror_doc_block(11, 12, "", "#", "test1"), + build_codemirror_doc_block(12, 13, " ", "#", "test2"), + build_codemirror_doc_block(13, 14, "", "#", "test3"), + ]; + let ret = diff_code_mirror_doc_blocks(&before, &after); + assert_eq!( + ret, + vec![ + // Order is important -- first, 12->13, then 11->12, 10->11 + // (reversed order). + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 12, + from_new: 13, + to: 14, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 11, + from_new: 12, + to: 13, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 10, + from_new: 11, + to: 12, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + ] + ); + + // Next, with end adjacent doc blocks not at the end of the document. + let before = vec![ + build_codemirror_doc_block(10, 11, "", "#", "test1"), + build_codemirror_doc_block(11, 12, " ", "#", "test2"), + build_codemirror_doc_block(13, 14, "", "#", "test3"), + ]; + let after = vec![ + build_codemirror_doc_block(11, 12, "", "#", "test1"), + build_codemirror_doc_block(12, 13, " ", "#", "test2"), + build_codemirror_doc_block(14, 15, "", "#", "test3"), + ]; + let ret = diff_code_mirror_doc_blocks(&before, &after); + assert_eq!( + ret, + vec![ + // Order is important -- 11->12, then 10->11 (reversed), then + // 13->14. + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 11, + from_new: 12, + to: 13, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 10, + from_new: 11, + to: 12, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 13, + from_new: 14, + to: 15, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + ] + ); + + // Test deletes before adjacent doc blocks. + let before = vec![ + build_codemirror_doc_block(10, 11, "", "#", "test1"), + build_codemirror_doc_block(11, 12, " ", "#", "test2"), + build_codemirror_doc_block(12, 13, "", "#", "test3"), + ]; + let after = vec![ + build_codemirror_doc_block(9, 10, "", "#", "test1"), + build_codemirror_doc_block(10, 11, " ", "#", "test2"), + build_codemirror_doc_block(11, 12, "", "#", "test3"), + ]; + let ret = diff_code_mirror_doc_blocks(&before, &after); + assert_eq!( + ret, + vec![ + // Order: no reversal needed. + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 10, + from_new: 9, + to: 10, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 11, + from_new: 10, + to: 11, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + CodeMirrorDocBlockTransaction::Update(CodeMirrorDocBlockUpdate { + from: 12, + from_new: 11, + to: 12, + indent: None, + delimiter: "#".to_string(), + contents: vec![] + }), + ] + ); } diff --git a/server/src/test_utils.rs b/server/src/test_utils.rs index bd982613..c183d999 100644 --- a/server/src/test_utils.rs +++ b/server/src/test_utils.rs @@ -1,4 +1,4 @@ -/// Copyright (C) 2023 Bryan A. Jones. +/// 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 @@ -47,7 +47,7 @@ use crate::testing_logger; #[macro_export] macro_rules! cast { // For an enum containing a single value (the typical case). - ($target: expr_2021, $pat: path) => {{ + ($target: expr, $pat: path) => {{ // The if let exploits recent Rust compiler's smart pattern matching. // Contrary to other solutions like `into_variant`` and friends, this // one macro covers all ownership usage like` self``, `&self`` and `&mut @@ -64,7 +64,7 @@ macro_rules! cast { } }}; // For an enum containing multiple values, return a tuple. I can't figure out how to automatically do this; for now, the caller must provide the correct number of tuple parameters. - ($target: expr_2021, $pat: path, $( $tup: ident),*) => {{ + ($target: expr, $pat: path, $( $tup: ident),*) => {{ if let $pat($($tup,)*) = $target { ($($tup,)*) } diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 6ed99d8b..3838dff5 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -391,7 +391,7 @@ const MATHJAX_TAGS: &str = indoc!( MathJax = { // See the [docs](https://docs.mathjax.org/en/latest/options/output/chtml.html#option-descriptions). chtml: { - fontURL: "/static/mathjax-modern-font/chtml/woff", + fontURL: "/static/mathjax-newcm-font/chtml/woff2", }, tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] @@ -701,6 +701,8 @@ async fn make_simple_http_response( // If this file is currently being edited, this is the body of an `Update` // message to send. Option, + // The resulting file contents, if this is a CodeChat Editor file + Option, ) { // Convert the provided URL back into a file name. let file_path = &http_request.file_path; @@ -710,19 +712,14 @@ async fn make_simple_http_response( Err(err) => ( SimpleHttpResponse::Err(SimpleHttpResponseError::Io(err)), None, + None, ), Ok(mut fc) => { let file_contents = try_read_as_text(&mut fc).await; // If this is a binary file (meaning // we can't read the contents as UTF-8), send the contents as none // to signal this isn't a text file. - file_to_response( - http_request, - current_filepath, - file_contents.as_deref(), - use_pdf_js, - ) - .await + file_to_response(http_request, current_filepath, file_contents, use_pdf_js).await } } } @@ -755,7 +752,7 @@ async fn file_to_response( // `try_canonicalize`. current_filepath: &Path, // Contents of this file, if it's text; None if it was binary data. - file_contents: Option<&str>, + file_contents: Option, // True to use the PDF.js viewer for this file. use_pdf_js: bool, ) -> ( @@ -765,6 +762,8 @@ async fn file_to_response( // populate the Client with the parsed `file_contents`. In all other cases, // return None. Option, + // The `file_contents` if this is a + Option, ) { // Use a lossy conversion, since this is UI display, not filesystem access. let file_path = &http_request.file_path; @@ -774,6 +773,7 @@ async fn file_to_response( file_path.to_path_buf(), )), None, + None, ); }; let name = escape_html(&file_name.to_string_lossy()); @@ -791,6 +791,7 @@ async fn file_to_response( codechat_editor_js_name, )), None, + None, ); }; let codechat_editor_css_name = format!("CodeChatEditor{js_test_suffix}.css"); @@ -800,6 +801,7 @@ async fn file_to_response( codechat_editor_css_name, )), None, + file_contents, ); }; @@ -807,20 +809,20 @@ async fn file_to_response( // `try_canonical`. let is_current_file = file_path == current_filepath; let is_toc = http_request.flags == ProcessingTaskHttpRequestFlags::Toc; - let (translation_results_string, path_to_toc) = if let Some(file_contents_text) = file_contents - { - if is_current_file || is_toc { - source_to_codechat_for_web_string(file_contents_text, file_path, is_toc) + let (translation_results_string, path_to_toc) = + if let Some(ref file_contents_text) = file_contents { + if is_current_file || is_toc { + source_to_codechat_for_web_string(file_contents_text, file_path, is_toc) + } else { + // If this isn't the current file, then don't parse it. + (TranslationResultsString::Unknown, None) + } } else { - // If this isn't the current file, then don't parse it. - (TranslationResultsString::Unknown, None) - } - } else { - ( - TranslationResultsString::Binary, - find_path_to_toc(file_path), - ) - }; + ( + TranslationResultsString::Binary, + find_path_to_toc(file_path), + ) + }; let is_project = path_to_toc.is_some(); // For project files, add in the sidebar. Convert this from a Windows path // to a Posix path if necessary. @@ -852,6 +854,7 @@ async fn file_to_response( file_name, ))), None, + None, ); }; return ( @@ -871,13 +874,14 @@ async fn file_to_response( }, ), None, + None, ); } let codechat_for_web = match translation_results_string { // The file type is binary. Ask the HTTP server to serve it raw. TranslationResultsString::Binary => return - (SimpleHttpResponse::Bin(file_path.to_path_buf()), None) + (SimpleHttpResponse::Bin(file_path.to_path_buf()), None, None) , // The file type is unknown. Serve it raw. TranslationResultsString::Unknown => { @@ -887,11 +891,12 @@ async fn file_to_response( mime_guess::from_path(file_path).first_or_text_plain(), ), None, + None ); } // Report a lexer error. TranslationResultsString::Err(err_string) => { - return (SimpleHttpResponse::Err(SimpleHttpResponseError::LexerError(err_string)), None); + return (SimpleHttpResponse::Err(SimpleHttpResponseError::LexerError(err_string)), None, None); } // This is a CodeChat file. The following code wraps the CodeChat for // web results in a CodeChat Editor Client webpage. @@ -919,6 +924,7 @@ async fn file_to_response( "#, )), None, + None ); } }; @@ -940,6 +946,7 @@ async fn file_to_response( file_path.to_path_buf(), )), None, + None, ); }; let dir = path_display(raw_dir); @@ -949,6 +956,7 @@ async fn file_to_response( file_path.to_path_buf(), )), None, + None, ); }; // Build and return the webpage. @@ -996,6 +1004,7 @@ async fn file_to_response( cursor_position: None, scroll_position: None, }), + file_contents, ) } diff --git a/server/src/webserver/filewatcher.rs b/server/src/webserver/filewatcher.rs index fc148a14..6f22bf62 100644 --- a/server/src/webserver/filewatcher.rs +++ b/server/src/webserver/filewatcher.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -534,7 +534,7 @@ async fn processing_task(file_path: &Path, app_state: web::Data, conne // file, which will still produce an error. let empty_path = PathBuf::new(); let cfp = current_filepath.as_ref().unwrap_or(&empty_path); - let (simple_http_response, option_update) = make_simple_http_response(&http_request, cfp, false).await; + let (simple_http_response, option_update, _) = make_simple_http_response(&http_request, cfp, false).await; if let Some(update) = option_update { // Send the update to the client. queue_send!(to_websocket_tx.send(EditorMessage { diff --git a/server/src/webserver/tests.rs b/server/src/webserver/tests.rs index 2139e004..2a69000c 100644 --- a/server/src/webserver/tests.rs +++ b/server/src/webserver/tests.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 diff --git a/server/src/webserver/vscode.rs b/server/src/webserver/vscode.rs index da0aa7a4..9b7a85a5 100644 --- a/server/src/webserver/vscode.rs +++ b/server/src/webserver/vscode.rs @@ -1,4 +1,4 @@ -/// Copyright (C) 2023 Bryan A. Jones. +/// 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 @@ -55,9 +55,9 @@ use crate::{ queue_send, webserver::{ INITIAL_MESSAGE_ID, MESSAGE_ID_INCREMENT, ProcessingTaskHttpRequest, ResultOkTypes, - SyncState, UpdateMessageContents, escape_html, file_to_response, filesystem_endpoint, - get_server_url, html_wrapper, make_simple_http_response, path_to_url, try_canonicalize, - try_read_as_text, url_to_path, + SimpleHttpResponse, SimpleHttpResponseError, SyncState, UpdateMessageContents, escape_html, + file_to_response, filesystem_endpoint, get_server_url, html_wrapper, path_to_url, + try_canonicalize, try_read_as_text, url_to_path, }, }; @@ -106,7 +106,6 @@ pub fn find_eol_type(s: &str) -> EolType { } } -// // This is the processing task for the Visual Studio Code IDE. It handles all // the core logic to moving data between the IDE and the client. #[get("/vsc/ws-ide/{connection_id}")] @@ -346,11 +345,11 @@ pub async fn vscode_ide_websocket( let mut id: f64 = INITIAL_MESSAGE_ID + MESSAGE_ID_INCREMENT; let mut source_code = String::new(); let mut code_mirror_doc = String::new(); - // The initial state will be overwritten by the first `Update` or `LoadFile`, so this - // value doesn't matter. + // The initial state will be overwritten by the first `Update` or + // `LoadFile`, so this value doesn't matter. let mut eol = EolType::Lf; - // Some means this contains valid HTML; None means don't use it (since it would have - // contained Markdown). + // Some means this contains valid HTML; None means don't use it + // (since it would have contained Markdown). let mut code_mirror_doc_blocks = Some(Vec::new()); // To send a diff from Server to Client or vice versa, we need to // ensure they are in sync: @@ -455,19 +454,47 @@ pub async fn vscode_ide_websocket( // is a PDF file. (TODO: look at the magic // number also -- "%PDF"). let use_pdf_js = http_request.file_path.extension() == Some(OsStr::new("pdf")); - let (simple_http_response, option_update) = match file_contents_option { + let (simple_http_response, option_update, file_contents) = match file_contents_option { Some(file_contents) => { - // If there are Windows newlines, replace with Unix; this is reversed when the file is sent back to the IDE. + // If there are Windows newlines, replace + // with Unix; this is reversed when the + // file is sent back to the IDE. eol = find_eol_type(&file_contents); - let file_contents = file_contents.replace("\r\n", "\n"); - let ret = file_to_response(&http_request, ¤t_file, Some(&file_contents), use_pdf_js).await; - source_code = file_contents; - ret + let file_contents = if use_pdf_js { file_contents } else { file_contents.replace("\r\n", "\n") }; + file_to_response(&http_request, ¤t_file, Some(file_contents), use_pdf_js).await }, None => { // The file wasn't available in the IDE. // Look for it in the filesystem. - make_simple_http_response(&http_request, ¤t_file, use_pdf_js).await + match File::open(&http_request.file_path).await { + Err(err) => ( + SimpleHttpResponse::Err(SimpleHttpResponseError::Io(err)), + None, + None + ), + Ok(mut fc) => { + let option_file_contents = try_read_as_text(&mut fc).await; + let option_file_contents = if let Some(file_contents) = option_file_contents { + eol = find_eol_type(&file_contents); + let file_contents = if use_pdf_js { file_contents } else { file_contents.replace("\r\n", "\n") }; + Some(file_contents) + } else { + None + }; + // If this + // is a binary file (meaning we can't read + // the contents as UTF-8), send the + // contents as none to signal this isn't a + // text file. + file_to_response( + &http_request, + ¤t_file, + option_file_contents, + use_pdf_js, + ) + .await + } + } } }; if let Some(update) = option_update { @@ -481,6 +508,7 @@ pub async fn vscode_ide_websocket( }; // We must clone here, since the original is // placed in the TX queue. + source_code = file_contents.unwrap(); code_mirror_doc = plain.doc.clone(); code_mirror_doc_blocks = Some(plain.doc_blocks.clone()); sync_state = SyncState::Pending(id); @@ -508,7 +536,9 @@ pub async fn vscode_ide_websocket( match contents.source { CodeMirrorDiffable::Diff(_diff) => Err("TODO: support for updates with diffable sources.".to_string()), CodeMirrorDiffable::Plain(code_mirror) => { - // If there are Windows newlines, replace with Unix; this is reversed when the file is sent back to the IDE. + // If there are Windows newlines, replace + // with Unix; this is reversed when the + // file is sent back to the IDE. eol = find_eol_type(&code_mirror.doc); let doc_normalized_eols = code_mirror.doc.replace("\r\n", "\n"); // Translate the file. @@ -531,7 +561,8 @@ pub async fn vscode_ide_websocket( let doc_diff = diff_str(&code_mirror_doc, &ccfw_source_plain.doc); let code_mirror_diff = diff_code_mirror_doc_blocks(&cmdb, &ccfw_source_plain.doc_blocks); CodeChatForWeb { - // Clone needed here, so we can copy it later. + // Clone needed here, so we can copy it + // later. metadata: ccfw.metadata.clone(), source: CodeMirrorDiffable::Diff(CodeMirrorDiff { doc: doc_diff, @@ -719,16 +750,15 @@ pub async fn vscode_ide_websocket( Some(cfw) => match codechat_for_web_to_source( &cfw) { - Ok(mut result) => { - if eol == EolType::Crlf { - // Before sending back to the IDE, fix EOLs for Windows. - result = result.replace("\n", "\r\n"); - } + Ok(result) => { let ccfw = if sync_state == SyncState::InSync { Some(CodeChatForWeb { metadata: cfw.metadata, source: CodeMirrorDiffable::Diff(CodeMirrorDiff { - doc: diff_str(&source_code, &result), + // Diff with correct EOLs, so that (for + // CRLF files as well as LF files) offsets + // are correct. + doc: diff_str(&eol_convert(source_code, &eol), &eol_convert(result.clone(), &eol)), doc_blocks: vec![], }), }) @@ -736,12 +766,15 @@ pub async fn vscode_ide_websocket( Some(CodeChatForWeb { metadata: cfw.metadata, source: CodeMirrorDiffable::Plain(CodeMirror { - // We must clone here, so that it can be placed in the TX queue. - doc: result.clone(), + // We must clone here, so that it can be + // placed in the TX queue. + doc: eol_convert(result.clone(), &eol), doc_blocks: vec![], }), }) }; + // Store the document with Unix-style EOLs + // (LFs). source_code = result; let CodeMirrorDiffable::Plain(cmd) = cfw.source else { // TODO: support diffable! @@ -749,8 +782,9 @@ pub async fn vscode_ide_websocket( break; }; code_mirror_doc = cmd.doc; - // TODO: instead of `cmd.doc_blocks`, use `None` to indicate that - // the doc blocks contain Markdown instead of HTML. + // TODO: instead of `cmd.doc_blocks`, use + // `None` to indicate that the doc blocks + // contain Markdown instead of HTML. code_mirror_doc_blocks = None; ccfw }, @@ -925,3 +959,13 @@ async fn serve_vscode_fs( ) -> HttpResponse { filesystem_endpoint(request_path, &req, &app_state).await } + +// If a string is encoded using CRLFs (Windows style), convert it to LFs only +// (Unix style). +fn eol_convert(s: String, eol_type: &EolType) -> String { + if eol_type == &EolType::Crlf { + s.replace("\n", "\r\n") + } else { + s + } +} diff --git a/server/src/webserver/vscode/tests.rs b/server/src/webserver/vscode/tests.rs index d7fc02c1..83027af2 100644 --- a/server/src/webserver/vscode/tests.rs +++ b/server/src/webserver/vscode/tests.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Bryan A. Jones. +// 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 @@ -831,6 +831,7 @@ async fn test_vscode_ide_websocket4() { // Message ids: IDE - 4, Server - 3, Client - 2->5. let file_path_temp = fs::canonicalize(test_dir.join("test.py")).unwrap(); let file_path = simplified(&file_path_temp); + let file_path_str = file_path.to_str().unwrap().to_string(); send_message( &mut ws_client, &EditorMessage { @@ -849,10 +850,7 @@ async fn test_vscode_ide_websocket4() { read_message(&mut ws_ide).await, EditorMessage { id: 2.0, - message: EditorMessageContents::CurrentFile( - file_path.to_str().unwrap().to_string(), - Some(true) - ) + message: EditorMessageContents::CurrentFile(file_path_str.clone(), Some(true)) } ); @@ -875,6 +873,7 @@ async fn test_vscode_ide_websocket4() { // The Client should send a GET request for this file. let test_dir_thread = test_dir.clone(); let join_handle = thread::spawn(move || { + // Get the file itself. assert_eq!( minreq::get(format!( "http://localhost:8080/vsc/fs/{connection_id}/{}/{}", @@ -887,7 +886,7 @@ async fn test_vscode_ide_websocket4() { .unwrap() .status_code, 200 - ) + ); }); // This should produce a `LoadFile` message. @@ -917,7 +916,7 @@ async fn test_vscode_ide_websocket4() { EditorMessage { id: 6.0, message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path.to_str().unwrap().to_string(), + file_path: file_path_str.clone(), contents: Some(CodeChatForWeb { metadata: SourceFileMetadata { mode: "python".to_string(), @@ -954,6 +953,113 @@ async fn test_vscode_ide_websocket4() { } ); + // Simulate a related fetch for a project -- the `toc.md` file. + let test_dir_thread = test_dir.clone(); + let join_handle = thread::spawn(move || { + assert_eq!( + minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}/toc.md", + test_dir_thread.to_slash().unwrap() + )) + .send() + .unwrap() + .status_code, + 200 + ); + }); + + // This should also produce a `LoadFile` message. + // + // Message ids: IDE - 4, Server - 9->12, Client - 5. + let em = read_message(&mut ws_ide).await; + let msg = cast!(em.message, EditorMessageContents::LoadFile); + assert_eq!( + fs::canonicalize(&msg).unwrap(), + fs::canonicalize(test_dir.join("toc.md")).unwrap() + ); + assert_eq!(em.id, 9.0); + + // Reply to the `LoadFile` message: the IDE doesn't have the file. + send_message( + &mut ws_ide, + &EditorMessage { + id: 9.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), + }, + ) + .await; + join_handle.join().unwrap(); + + // Send an update from the Client, which should produce a diff. + // + // Message ids: IDE - 4, Server - 12, Client - 5->8. + send_message( + &mut ws_client, + &EditorMessage { + id: 5.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirrorDiffable::Plain(CodeMirror { + doc: "More\n".to_string(), + doc_blocks: vec![CodeMirrorDocBlock { + from: 5, + to: 6, + indent: "".to_string(), + delimiter: "#".to_string(), + contents: "test.py".to_string(), + }], + }), + }), + cursor_position: None, + scroll_position: None, + }), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 5.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirrorDiffable::Diff(CodeMirrorDiff { + doc: vec![StringDiff { + from: 0, + to: None, + insert: format!("More{}", if cfg!(windows) { "\r\n" } else { "\n" }), + }], + doc_blocks: vec![], + }), + }), + cursor_position: None, + scroll_position: None, + }) + } + ); + send_message( + &mut ws_ide, + &EditorMessage { + id: 5.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 5.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + check_logger_errors(0); // Report any errors produced when removing the temporary directory. temp_dir.close().unwrap(); diff --git a/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4/toc.md b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4/toc.md new file mode 100644 index 00000000..d3ae8809 --- /dev/null +++ b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4/toc.md @@ -0,0 +1,2 @@ +toc.md +======