diff --git a/CHANGELOG.md b/CHANGELOG.md index c87e8502..e928a360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ Changelog [Github master](https://github.com/bjones1/CodeChat_Editor) -------------------------------------------------------------------------------- +* No changes. + +Version 0.1.42 -- 2025-Dec-04 +-------------------------------------------------------------------------------- + * Drag and drop of images creates a mess; disable drop and drop for this reason. * Send sync data when doc blocks receive focus. * Improve error handling. diff --git a/client/package.json5 b/client/package.json5 index b6d22ae4..de5e9599 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.41', + version: '0.1.42', dependencies: { '@codemirror/commands': '^6.10.0', '@codemirror/lang-cpp': '^6.0.3', diff --git a/client/src/CodeChatEditorFramework.mts b/client/src/CodeChatEditorFramework.mts index 43f26b8e..8a65f9d4 100644 --- a/client/src/CodeChatEditorFramework.mts +++ b/client/src/CodeChatEditorFramework.mts @@ -81,8 +81,8 @@ class WebSocketComm { // IDE and passed back to it, but not otherwise used by the Framework. current_filename: string | undefined = undefined; - // The version number of the current file. This default value will be overwritten when - // the first `Update` is sent. + // The version number of the current file. This default value will be + // overwritten when the first `Update` is sent. version = 0.0; // True when the iframe is loading, so that an `Update` should be postponed @@ -164,7 +164,9 @@ class WebSocketComm { const contents = current_update.contents; const cursor_position = current_update.cursor_position; if (contents !== undefined) { - // Check and update the version. If this is a diff, ensure the diff was made against the version of the file we have. + // Check and update the version. If this is a diff, + // ensure the diff was made against the version of + // the file we have. if ("Diff" in contents.source) { if ( contents.source.Diff.version !== diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index a7e2839f..4314e48f 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -119,7 +119,8 @@ declare global { // When this is included in a transaction, don't update from/to of doc blocks. const docBlockFreezeAnnotation = Annotation.define(); -// When this is included in a transaction, don't send autosave scroll/cursor location updates. +// When this is included in a transaction, don't send autosave scroll/cursor +// location updates. const noAutosaveAnnotation = Annotation.define(); // Doc blocks in CodeMirror @@ -276,7 +277,8 @@ export const docBlockField = StateField.define({ prev.spec.widget.contents, effect.value.contents, ), - // Assume this isn't a user change unless it's specified. + // Assume this isn't a user change unless it's + // specified. effect.value.is_user_change ?? false, ), ...decorationOptions, @@ -373,7 +375,8 @@ 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. + // True if this update comes from a user change, as opposed to an update + // received from the IDE. is_user_change?: boolean; }; @@ -447,7 +450,8 @@ class DocBlockWidget extends WidgetType { `
` + this.contents + "
"; - // TODO: this is an async call. However, CodeMirror doesn't provide async support. + // TODO: this is an async call. However, CodeMirror doesn't provide + // async support. mathJaxTypeset(wrap); return wrap; } @@ -457,7 +461,8 @@ class DocBlockWidget extends WidgetType { // "Update a DOM element created by a widget of the same type (but // different, non-eq content) to reflect this widget." 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 change was produced by a user edit, then the DOM was already + // updated. Stop here. if (this.is_user_change) { console.log("user change -- skipping DOM update."); return true; @@ -469,7 +474,8 @@ class DocBlockWidget extends WidgetType { const [contents_div, is_tinymce] = get_contents(dom); window.MathJax.typesetClear(contents_div); if (is_tinymce) { - // Save the cursor location before the update, then restore it afterwards, if TinyMCE has focus. + // Save the cursor location before the update, then restore it + // afterwards, if TinyMCE has focus. const sel = tinymce_singleton!.hasFocus() ? saveSelection() : undefined; @@ -482,7 +488,8 @@ class DocBlockWidget extends WidgetType { } mathJaxTypeset(contents_div); - // Indicate the update was successful. TODO: but, contents are still pending... + // Indicate the update was successful. TODO: but, contents are still + // pending... return true; } @@ -513,27 +520,22 @@ 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`.) + // 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(); let selection_path = []; const selection_offset = sel?.anchorOffset; if (sel?.anchorNode) { - // Find a path from the selection back to the - // containing div. + // 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), ... + // 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( @@ -541,16 +543,13 @@ const saveSelection = () => { ); 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. + // 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. let p = current_node.parentNode!; selection_path.unshift( Array.prototype.indexOf.call( @@ -563,7 +562,8 @@ const saveSelection = () => { return { selection_path, selection_offset }; }; -// Restore the selection produced by `saveSelection` to the active TinyMCE instance. +// Restore the selection produced by `saveSelection` to the active TinyMCE +// instance. const restoreSelection = ({ selection_path, selection_offset, @@ -571,20 +571,19 @@ const restoreSelection = ({ selection_path: number[]; selection_offset?: number; }) => { - // Copy the selection over to TinyMCE by indexing the - // selection path to find the selected node. + // 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. + // 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. + // 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 @@ -662,8 +661,8 @@ const element_is_in_doc_block = ( // untypeset, then the dirty ignored. // 3. When MathJax typesets math on a TinyMCE focus out event, the dirty flag // gets set. This should be ignored. However, typesetting is an async -// operation, so we assume it's OK to await the typeset completion. -// This will lead to nasty bugs at some point. +// operation, so we assume it's OK to await the typeset completion. This will +// lead to nasty bugs at some point. // 4. When an HTML doc block is assigned to the TinyMCE instance for editing, // the dirty flag is set. This must be ignored. const on_dirty = ( @@ -684,8 +683,8 @@ const on_dirty = ( ".CodeChat-doc", )! as HTMLDivElement; - // We can only get the position (the `from` value) for the doc block. Use - // this to find the `to` value for the doc block. + // We can only get the position (the `from` value) for the doc block. + // Use this to find the `to` value for the doc block. let from; try { from = current_view.posAtDOM(target); @@ -698,7 +697,8 @@ const on_dirty = ( const indent = indent_div.innerHTML; const delimiter = indent_div.getAttribute("data-delimiter")!; const [contents_div, is_tinymce] = get_contents(target); - // I'd like to extract this string, then untypeset only that string, not the actual div. But I don't know how. + // I'd like to extract this string, then untypeset only that string, not + // the actual div. But I don't know how. mathJaxUnTypeset(contents_div); const contents = is_tinymce ? tinymce_singleton!.save() @@ -828,7 +828,8 @@ export const DocBlockPlugin = ViewPlugin.fromClass( // cursor position (the selection) to be set in the // contenteditable div. Then, save that location. setTimeout(async () => { - // Untypeset math in the old doc block and the current doc block before moving its contents around. + // Untypeset math in the old doc block and the current + // doc block before moving its contents around. const tinymce_div = document.getElementById("TinyMCE-inst")!; mathJaxUnTypeset(tinymce_div); @@ -854,7 +855,8 @@ export const DocBlockPlugin = ViewPlugin.fromClass( old_contents_div, null, ); - // The previous content edited by TinyMCE is now a div. Retypeset this after the transition. + // The previous content edited by TinyMCE is now a div. + // Retypeset this after the transition. await mathJaxTypeset(old_contents_div); // Move TinyMCE to the new location, then remove the old // div it will replace. @@ -1065,9 +1067,11 @@ export const CodeMirror_load = async ( // [docs](https://codemirror.net/examples/tab/). TODO: // document a way to escape the tab key per the same docs. keymap.of([indentWithTab]), - // Change the font size. See [this post](https://discuss.codemirror.net/t/changing-the-font-size-of-cm6/2935/6). + // Change the font size. See + // [this post](https://discuss.codemirror.net/t/changing-the-font-size-of-cm6/2935/6). [ - // TODO: get these values from the IDE, so we match its size. + // TODO: get these values from the IDE, so we match its + // size. EditorView.theme({ "&": { fontSize: "14px", @@ -1092,7 +1096,8 @@ export const CodeMirror_load = async ( await init({ selector: "#TinyMCE-inst", setup: (editor: Editor) => { - // See the [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). + // See the + // [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events). editor.on("Dirty", (event: any) => { // Get the div TinyMCE stores edits in. TODO: find // documentation for `event.target.bodyElement`. @@ -1158,7 +1163,9 @@ export const scroll_to_line = (cursor_line?: number, scroll_line?: number) => { return; } - // Create a transaction to set the cursor and scroll position. Avoid an autosave that sends updated cursor/scroll positions produced by this transaction. + // Create a transaction to set the cursor and scroll position. Avoid an + // autosave that sends updated cursor/scroll positions produced by this + // transaction. const dispatch_data: TransactionSpec = { annotations: noAutosaveAnnotation.of(true), }; diff --git a/client/src/debug_enabled.mts b/client/src/debug_enabled.mts index 871b04bf..a59aed4d 100644 --- a/client/src/debug_enabled.mts +++ b/client/src/debug_enabled.mts @@ -15,9 +15,10 @@ // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). // // `debug_enable.mts` -- Configure debug features -// ============================================== +// ============================================================================= // True to enable additional debug logging. export const DEBUG_ENABLED = false; -// The max length of a message to show in the console when debug logging is enabled. +// The max length of a message to show in the console when debug logging is +// enabled. export const MAX_MESSAGE_LENGTH = 20000; diff --git a/client/src/show_toast.mts b/client/src/show_toast.mts index af6472ba..461a4d46 100644 --- a/client/src/show_toast.mts +++ b/client/src/show_toast.mts @@ -15,7 +15,7 @@ // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). // // `show_toast.mts` -- Show a toast message -// ======================================== +// ============================================================================= import Toastify from "toastify-js"; import "toastify-js/src/toastify.css"; diff --git a/extensions/VSCode/Cargo.lock b/extensions/VSCode/Cargo.lock index ca59d901..032b5b04 100644 --- a/extensions/VSCode/Cargo.lock +++ b/extensions/VSCode/Cargo.lock @@ -522,7 +522,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "codechat-editor-server" -version = "0.1.41" +version = "0.1.42" dependencies = [ "actix-files", "actix-http", @@ -570,7 +570,7 @@ dependencies = [ [[package]] name = "codechat-editor-vscode-extension" -version = "0.1.41" +version = "0.1.42" dependencies = [ "codechat-editor-server", "log", @@ -1575,9 +1575,9 @@ checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", diff --git a/extensions/VSCode/Cargo.toml b/extensions/VSCode/Cargo.toml index fac7846a..ad3e423c 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.41" +version = "0.1.42" [lib] crate-type = ["cdylib"] diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 9a52f24e..4aa1e83a 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -40,7 +40,7 @@ "type": "git", "url": "https://github.com/bjones1/CodeChat_Editor" }, - "version": "0.1.41", + "version": "0.1.42", "activationEvents": [ "onCommand:extension.codeChatEditorActivate", "onCommand:extension.codeChatEditorDeactivate" diff --git a/extensions/VSCode/pnpm-lock.yaml b/extensions/VSCode/pnpm-lock.yaml index 8c3838d7..a886af20 100644 --- a/extensions/VSCode/pnpm-lock.yaml +++ b/extensions/VSCode/pnpm-lock.yaml @@ -117,16 +117,16 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} - '@azure/msal-browser@4.26.2': - resolution: {integrity: sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==} + '@azure/msal-browser@4.27.0': + resolution: {integrity: sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==} engines: {node: '>=0.8.0'} - '@azure/msal-common@15.13.2': - resolution: {integrity: sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==} + '@azure/msal-common@15.13.3': + resolution: {integrity: sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==} engines: {node: '>=0.8.0'} - '@azure/msal-node@3.8.3': - resolution: {integrity: sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==} + '@azure/msal-node@3.8.4': + resolution: {integrity: sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw==} engines: {node: '>=16'} '@babel/code-frame@7.27.1': @@ -2123,15 +2123,15 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} keytar@7.9.0: resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} @@ -2991,8 +2991,8 @@ snapshots: '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 - '@azure/msal-browser': 4.26.2 - '@azure/msal-node': 3.8.3 + '@azure/msal-browser': 4.27.0 + '@azure/msal-node': 3.8.4 open: 10.2.0 tslib: 2.8.1 transitivePeerDependencies: @@ -3005,16 +3005,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/msal-browser@4.26.2': + '@azure/msal-browser@4.27.0': dependencies: - '@azure/msal-common': 15.13.2 + '@azure/msal-common': 15.13.3 - '@azure/msal-common@15.13.2': {} + '@azure/msal-common@15.13.3': {} - '@azure/msal-node@3.8.3': + '@azure/msal-node@3.8.4': dependencies: - '@azure/msal-common': 15.13.2 - jsonwebtoken: 9.0.2 + '@azure/msal-common': 15.13.3 + jsonwebtoken: 9.0.3 uuid: 8.3.2 '@babel/code-frame@7.27.1': @@ -5058,9 +5058,9 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.2: + jsonwebtoken@9.0.3: dependencies: - jws: 3.2.2 + jws: 4.0.1 lodash.includes: 4.3.0 lodash.isboolean: 3.0.3 lodash.isinteger: 4.0.4 @@ -5071,15 +5071,15 @@ snapshots: ms: 2.1.3 semver: 7.7.3 - jwa@1.4.2: + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: + jws@4.0.1: dependencies: - jwa: 1.4.2 + jwa: 2.0.1 safe-buffer: 5.2.1 keytar@7.9.0: diff --git a/extensions/VSCode/src/extension.ts b/extensions/VSCode/src/extension.ts index 81f049e7..5ad1f404 100644 --- a/extensions/VSCode/src/extension.ts +++ b/extensions/VSCode/src/extension.ts @@ -335,8 +335,10 @@ export const activate = (context: vscode.ExtensionContext) => { } if (current_update.contents !== undefined) { const source = current_update.contents.source; - // This will - // produce a change event, which we'll ignore. The change may also produce a selection change, which should also be ignored. + // This will produce a change event, which we'll + // ignore. The change may also produce a + // selection change, which should also be + // ignored. ignore_text_document_change = true; ignore_selection_change = true; // Use a workspace edit, since calls to @@ -359,10 +361,12 @@ export const activate = (context: vscode.ExtensionContext) => { ); } else { assert("Diff" in source); - // If this diff was not made against the text we currently have, reject it. + // If this diff was not made against the + // text we currently have, reject it. if (source.Diff.version !== version) { await sendResult(id, "OutOfSync"); - // Send an `Update` with the full text to re-sync the Client. + // Send an `Update` with the full text to + // re-sync the Client. send_update(true); break; } @@ -395,7 +399,8 @@ export const activate = (context: vscode.ExtensionContext) => { ignore_text_document_change = false; ignore_selection_change = false; }); - // Now that we've updated our text, update the associated version as well. + // Now that we've updated our text, update the + // associated version as well. version = current_update.contents.version; } @@ -720,7 +725,8 @@ const can_render = () => { (vscode.window.activeTextEditor !== undefined || current_editor !== undefined) && codeChatEditorServer !== undefined && - // TODO: I don't think these matter -- the Server is in charge of sending output to the Client. + // TODO: I don't think these matter -- the Server is in charge of + // sending output to the Client. (codechat_client_location === CodeChatEditorClientLocation.browser || webview_panel !== undefined) ); diff --git a/server/Cargo.lock b/server/Cargo.lock index 5cfd343b..a5b3dc92 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -746,7 +746,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "codechat-editor-server" -version = "0.1.41" +version = "0.1.42" dependencies = [ "actix-files", "actix-http", @@ -2319,9 +2319,9 @@ checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", diff --git a/server/Cargo.toml b/server/Cargo.toml index 0792f084..561ceb60 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,10 +17,10 @@ # [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). # # `Cargo.toml` -- Rust build/package management config for the server -# =================================================================== +# ============================================================================== # # General package configurations -# ------------------------------ +# ------------------------------------------------------------------------------ [package] authors = ["Bryan A. Jones", "Peter Loux"] categories = ["development-tools", "text-editors"] @@ -32,14 +32,14 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.41" +version = "0.1.42" # This library allows other packages to use core CodeChat Editor features. [lib] name = "code_chat_editor" # Features -# -------- +# ------------------------------------------------------------------------------ # # See the [docs](https://doc.rust-lang.org/cargo/reference/features.html). [features] @@ -51,7 +51,7 @@ lexer_explain = [] int_tests = ["assert_fs", "assertables", "futures"] # Dependencies -# ------------ +# ------------------------------------------------------------------------------ [dependencies] actix-files = "0.6" actix-http = "3.9.0" @@ -104,8 +104,7 @@ url = "2.5.2" urlencoding = "2" webbrowser = "1.0.5" -# [Windows-only -# dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies). +# [Windows-only dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies). [target.'cfg(windows)'.dependencies] win_partitions = "0.3.0" @@ -127,7 +126,7 @@ tokio-tungstenite = "0.28" #actix-rt = { path = "../../actix-net/actix-rt" } # Release -# ------- +# ------------------------------------------------------------------------------ # # Specify release-only features for pulldown. See the # [docs](https://docs.rs/crate/pulldown-cmark/latest). @@ -138,7 +137,7 @@ panic = "abort" strip = "symbols" # Distribution -# ------------ +# ------------------------------------------------------------------------------ # # This uses [cargo dist](https://opensource.axo.dev/cargo-dist) to build # binaries across multiple platforms using github's CI/CD. diff --git a/server/src/ide.rs b/server/src/ide.rs index 6a7119ae..7492bcc2 100644 --- a/server/src/ide.rs +++ b/server/src/ide.rs @@ -14,12 +14,12 @@ // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// `ide.rs` -- Provide interfaces with common IDEs -/// =============================================== +/// ============================================================================ pub mod filewatcher; pub mod vscode; // Imports -// ------- +// ----------------------------------------------------------------------------- // // ### Standard library use std::{ @@ -57,7 +57,7 @@ use crate::{ }; // Code -// ---- +// ----------------------------------------------------------------------------- // // Using this macro is critical -- otherwise, the Actix system doesn't get // correctly initialized, which makes calls to `actix_rt::spawn` fail. In diff --git a/server/src/ide/filewatcher.rs b/server/src/ide/filewatcher.rs index 7bc05741..9509fac9 100644 --- a/server/src/ide/filewatcher.rs +++ b/server/src/ide/filewatcher.rs @@ -14,9 +14,9 @@ // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// `filewatcher.rs` -- Implement the File Watcher "IDE" -/// ==================================================== +/// ============================================================================ // Imports -// ------- +// ----------------------------------------------------------------------------- // // ### Standard library use std::{ @@ -73,7 +73,7 @@ use crate::{ }; // Globals -// ------- +// ----------------------------------------------------------------------------- lazy_static! { /// Matches a bare drive letter. Only needed on Windows. static ref DRIVE_LETTER_REGEX: Regex = Regex::new("^[a-zA-Z]:$").unwrap(); @@ -82,7 +82,7 @@ lazy_static! { pub const FILEWATCHER_PATH_PREFIX: &[&str] = &["fw", "fsc"]; /// File browser endpoints -/// ---------------------- +/// ---------------------------------------------------------------------------- /// /// The file browser provides a very crude interface, allowing a user to select /// a file from the local filesystem for editing. Long term, this should be @@ -335,10 +335,10 @@ async fn processing_task( // // This is a CodeChat Editor file. Start up the Filewatcher IDE tasks: // - // 1. A task to watch for changes to the file, notifying the CodeChat - // Editor Client when the file should be reloaded. - // 2. A task to receive and respond to messages from the CodeChat Editor - // Client. + // 1. A task to watch for changes to the file, notifying the CodeChat Editor + // Client when the file should be reloaded. + // 2. A task to receive and respond to messages from the CodeChat Editor + // Client. // // First, allocate variables needed by these two tasks. // @@ -536,7 +536,12 @@ async fn processing_task( doc: file_contents, doc_blocks: vec![], }), - // The filewatcher doesn't store a version, since it only accepts plain (non-diff) results. Provide a version so the Client stays in sync with any diffs. Produce a whole number to avoid encoding difference with fractional values. + // The filewatcher doesn't store a version, + // since it only accepts plain (non-diff) + // results. Provide a version so the Client + // stays in sync with any diffs. Produce a + // whole number to avoid encoding + // difference with fractional values. version: random::() as f64, }), cursor_position: None, @@ -693,11 +698,12 @@ pub fn get_connection_id_raw(app_state: &WebAppState) -> u32 { } // Tests -// ----- +// ----------------------------------------------------------------------------- #[cfg(test)] mod tests { use std::{ - fs, + backtrace::Backtrace, + env, fs, path::{Path, PathBuf}, str::FromStr, time::Duration, @@ -774,7 +780,10 @@ mod tests { println!("{} - {:?}", m.id, m.message); m } - _ = sleep(Duration::from_secs(3)) => panic!("Timeout waiting for message") + _ = sleep(Duration::from_secs(3)) => { + // The backtrace shows what message the code was waiting for; otherwise, it's an unhelpful error message. + panic!("Timeout waiting for message:\n{}", Backtrace::force_capture()); + } } } @@ -833,7 +842,7 @@ mod tests { let url_path = url_path.canonicalize().unwrap(); assert_eq!(url_path, test_path); - // 2. After fetching the file, we should get an update. + // 2. After fetching the file, we should get an update. // // Message ids: IDE - 1, Server - 2->3, Client - 0. let uri = format!( @@ -855,7 +864,8 @@ mod tests { false, false, ); - let codechat_for_web = cast!(cast!(translation_results, Ok), TranslationResults::CodeChat); + let tr = cast!(translation_results, Ok); + let codechat_for_web = cast!(tr, TranslationResults::CodeChat); assert_eq!(umc.contents, Some(codechat_for_web)); // Report any errors produced when removing the temporary directory. @@ -871,8 +881,8 @@ mod tests { let from_client_tx = wq.from_websocket_tx; let mut to_client_rx = wq.to_websocket_rx; - // 1. The initial web request for the Client framework produces a - // `CurrentFile`. + // 1. The initial web request for the Client framework produces a + // `CurrentFile`. // // Message ids: IDE - 0->1, Server - 2, Client - 0. let (id, (..)) = get_message_as!( @@ -884,9 +894,9 @@ mod tests { assert_eq!(id, INITIAL_IDE_MESSAGE_ID); send_response(&from_client_tx, id, Ok(ResultOkTypes::Void)).await; - // 2. After fetching the file, we should get an update. The Server - // sends a `LoadFile` to the IDE using message the next ID; - // therefore, this consumes two IDs. + // 2. After fetching the file, we should get an update. The Server sends + // a `LoadFile` to the IDE using message the next ID; therefore, this + // consumes two IDs. // // Message ids: IDE - 1, Server - 2->3, Client - 0. let mut file_path = test_dir.clone(); @@ -906,7 +916,7 @@ mod tests { assert_eq!(id, INITIAL_MESSAGE_ID + 2.0 * MESSAGE_ID_INCREMENT); send_response(&from_client_tx, id, Ok(ResultOkTypes::Void)).await; - // 3. Send an update message with no contents. + // 3. Send an update message with no contents. // // Message ids: IDE - 1, Server - 3, Client - 0->1. from_client_tx @@ -928,7 +938,7 @@ mod tests { (INITIAL_CLIENT_MESSAGE_ID, Ok(ResultOkTypes::Void)) ); - // 4. Send invalid messages. + // 4. Send invalid messages. // // Message ids: IDE - 1, Server - 3, Client - 1->4. for (id, msg) in [ @@ -954,7 +964,7 @@ mod tests { matches!(cast!(&msg_rx, Err), ResultErrTypes::ClientIllegalMessage); } - // 5. Send an update message with no path. + // 5. Send an update message with no path. // // Message ids: IDE - 1, Server - 3, Client - 4->5. from_client_tx @@ -982,9 +992,14 @@ mod tests { // Check that it produces an error. let (id, err_msg) = get_message_as!(to_client_rx, EditorMessageContents::Result); assert_eq!(id, INITIAL_CLIENT_MESSAGE_ID + 4.0 * MESSAGE_ID_INCREMENT); - cast!(cast!(err_msg, Err), ResultErrTypes::WrongFileUpdate, _a, _b); + cast!( + err_msg.as_ref().unwrap_err(), + ResultErrTypes::WrongFileUpdate, + _a, + _b + ); - // 6. Send an update message with unknown source language. + // 6. Send an update message with unknown source language. // // Message ids: IDE - 1, Server - 3, Client - 5->6. from_client_tx @@ -1015,9 +1030,12 @@ mod tests { msg_id, INITIAL_CLIENT_MESSAGE_ID + 5.0 * MESSAGE_ID_INCREMENT ); - cast!(cast!(msg, Err), ResultErrTypes::CannotTranslateCodeChat); + cast!( + msg.as_ref().unwrap_err(), + ResultErrTypes::CannotTranslateCodeChat + ); - // 7. Send a valid message. + // 7. Send a valid message. // // Message ids: IDE - 1, Server - 3, Client - 6->7. from_client_tx @@ -1053,13 +1071,21 @@ mod tests { let mut s = fs::read_to_string(&file_path).unwrap(); assert_eq!(s, "testing()"); - // 8. Change this file and verify that this produces an update. + // 8. Change this file and verify that this produces an update. // // Message ids: IDE - 1->2, Server - 3, Client - 7. s.push_str("123"); fs::write(&file_path, s).unwrap(); // Wait for the filewatcher to debounce this file write. - sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs( + // Mac in CI seems to need a long delay here. + if cfg!(target_os = "macos") && env::var("CI") == Ok("true".to_string()) { + 5 + } else { + 2 + }, + )) + .await; // The version is random; don't check it with a fixed value. let msg = get_message_as!(to_client_rx, EditorMessageContents::Update); assert_eq!( @@ -1091,8 +1117,8 @@ mod tests { ) .await; - // 9. Rename it and check for an close (the file watcher can't detect - // the destination file, so it's treated as the file is deleted). + // 9. Rename it and check for an close (the file watcher can't detect + // the destination file, so it's treated as the file is deleted). // // Message ids: IDE - 2->3, Server - 3, Client - 7. let mut dest = PathBuf::from(&file_path).parent().unwrap().to_path_buf(); diff --git a/server/src/ide/vscode.rs b/server/src/ide/vscode.rs index 18b378ec..94d8883a 100644 --- a/server/src/ide/vscode.rs +++ b/server/src/ide/vscode.rs @@ -15,14 +15,14 @@ // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// `vscode.rs` -- Implement server-side functionality for the Visual Studio /// Code IDE -/// ======================================================================== +/// ============================================================================ // Modules -// ------- +// ----------------------------------------------------------------------------- #[cfg(test)] pub mod tests; // Imports -// ------- +// ----------------------------------------------------------------------------- // // ### Standard library // @@ -52,12 +52,12 @@ use crate::{ }; // Globals -// ------- +// ----------------------------------------------------------------------------- const VSCODE_PATH_PREFIX: &[&str] = &["vsc", "fs"]; const VSC: &str = "vsc-"; // Code -// ---- +// ----------------------------------------------------------------------------- #[get("/vsc/ws-ide/{connection_id_raw}")] pub async fn vscode_ide_websocket( connection_id_raw: web::Path, diff --git a/server/src/processing.rs b/server/src/processing.rs index 1bd0b633..41375610 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -948,8 +948,8 @@ pub fn source_to_codechat_for_web_string( TranslationResults::CodeChat(codechat_for_web) => { if is_toc { // For the table of contents sidebar, which is pure - // markdown, just return the resulting HTML, rather than the - // editable CodeChat for web format. + // markdown, just return the resulting HTML, rather than + // the editable CodeChat for web format. let CodeMirrorDiffable::Plain(plain) = codechat_for_web.source else { panic!("No diff!"); }; diff --git a/server/src/test_utils.rs b/server/src/test_utils.rs index e2225fb1..a80d2205 100644 --- a/server/src/test_utils.rs +++ b/server/src/test_utils.rs @@ -62,7 +62,7 @@ macro_rules! cast { else { // If the variant and value mismatch, the macro will simply panic // and report the expected pattern. - panic!("mismatch variant when cast to {}", stringify!($pat)); + panic!("mismatch variant when cast to {}; received {:?} instead.", stringify!($pat), $target); } }}; // For an enum containing multiple values, return a tuple. I can't figure @@ -73,7 +73,7 @@ macro_rules! cast { ($($tup,)*) } else { - panic!("mismatch variant when cast to {}", stringify!($pat)); + panic!("mismatch variant when cast to {}; received {:?} instead.", stringify!($pat), $target); } }}; } diff --git a/server/src/translation.rs b/server/src/translation.rs index 4fa64f59..769e21cc 100644 --- a/server/src/translation.rs +++ b/server/src/translation.rs @@ -14,7 +14,7 @@ // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// `translation.rs` -- translate messages between the IDE and the Client -/// ===================================================================== +/// ============================================================================ /// /// The IDE extension client (IDE for short) and the CodeChat Editor Client (or /// Editor for short) exchange messages with each other, mediated by the @@ -25,7 +25,7 @@ /// the processing module. /// /// Overview -/// -------- +/// ---------------------------------------------------------------------------- /// /// ### Architecture /// @@ -196,12 +196,12 @@ /// each message. To achieve this, the Server uses IDs that are multiples of 3 /// (0, 3, 6, ...), the Client multiples of 3 + 1 (1, 4, 7, ...) and the IDE /// multiples of 3 + 2 (2, 5, 8, ...). A double-precision floating point number -/// (the standard [numeric -/// type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type) +/// (the standard +/// [numeric type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type) /// in JavaScript) has a 53-bit mantissa, meaning IDs won't wrap around for a /// very long time. // Imports -// ------- +// ----------------------------------------------------------------------------- // // ### Standard library use std::{collections::HashMap, ffi::OsStr, fmt::Debug, path::PathBuf}; @@ -234,18 +234,18 @@ use crate::{ }; // Globals -// ------- +// ----------------------------------------------------------------------------- // // The max length of a message to show in the console. const MAX_MESSAGE_LENGTH: usize = 3000; lazy_static! { - /// A regex to determine the type of the first EOL. See 'PROCESSINGS`. + /// A regex to determine the type of the first EOL. See 'PROCESSINGS\`. pub static ref EOL_FINDER: Regex = Regex::new("[^\r\n]*(\r?\n)").unwrap(); } // Data structures -// --------------- +// ----------------------------------------------------------------------------- #[derive(Clone, Debug, PartialEq)] pub enum EolType { Lf, @@ -253,7 +253,7 @@ pub enum EolType { } // Code -// ---- +// ----------------------------------------------------------------------------- pub fn find_eol_type(s: &str) -> EolType { match EOL_FINDER.captures(s) { // Assume a line type for strings with no newlines. @@ -375,7 +375,8 @@ pub fn create_translation_queues( }) } -/// This holds the state used by the main loop of the translation task; this allows factoring out lengthy contents in the loop into subfunctions. +/// This holds the state used by the main loop of the translation task; this +/// allows factoring out lengthy contents in the loop into subfunctions. struct TranslationTask { // These parameters are passed to us. connection_id_raw: String, @@ -388,44 +389,43 @@ struct TranslationTask { from_http_rx: Receiver, // These parameters are internal state. - // /// The file currently loaded in the Client. current_file: PathBuf, /// A map of `LoadFile` requests sent to the IDE, awaiting its response. load_file_requests: HashMap, - /// The id for messages created by the server. Leave space for a server message during the init phase. + /// The id for messages created by the server. Leave space for a server + /// message during the init phase. id: f64, - /// The source code, provided by the IDE. It will use whatever the - /// IDE provides for EOLs, which is stored in `eol` below. + /// The source code, provided by the IDE. It will use whatever the IDE + /// provides for EOLs, which is stored in `eol` below. source_code: String, code_mirror_doc: String, eol: EolType, - /// 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). code_mirror_doc_blocks: Option>, prefix_str: String, - /// To support sending diffs, we must provide a way to determine if - /// the sender and receiver have the same file contents before - /// applying a diff. File contents can become unsynced due to: + /// To support sending diffs, we must provide a way to determine if the + /// sender and receiver have the same file contents before applying a diff. + /// File contents can become unsynced due to: /// /// 1. A dropped/lost message between the IDE and Client. - /// 2. Edits to file contents in two locations before updates from - /// one location (the Client, for example) propagate to the other - /// location (the IDE). + /// 2. Edits to file contents in two locations before updates from one + /// location (the Client, for example) propagate to the other location + /// (the IDE). /// - /// Therefore, assign each file a version number. All files are sent - /// with a unique, randomly-generated version number which define the - /// file's version after this update is applied. Diffs also include - /// the version number of the file before applying the diff; the + /// Therefore, assign each file a version number. All files are sent with a + /// unique, randomly-generated version number which define the file's + /// version after this update is applied. Diffs also include the version + /// number of the file before applying the diff; the // receiver's current version number must match with the sender's - /// pre-diff version number in order to apply the diff. When the - /// versions don't match, the IDE must send a full text file to the - /// Server and Client to re-sync. When a file is first loaded, its - /// version number is None, signaling that the sender must always - /// provide the full text, not a diff. + /// pre-diff version number in order to apply the diff. When the versions + /// don't match, the IDE must send a full text file to the Server and Client + /// to re-sync. When a file is first loaded, its version number is None, + /// signaling that the sender must always provide the full text, not a diff. version: f64, - /// Has the full (non-diff) version of the current file been sent? - /// Don't send diffs until this is sent. + /// Has the full (non-diff) version of the current file been sent? Don't + /// send diffs until this is sent. sent_full: bool, } @@ -508,8 +508,8 @@ pub async fn translation_task( EditorMessageContents::Result(_) => continue_loop = tt.ide_result(ide_message).await, EditorMessageContents::Update(_) => continue_loop = tt.ide_update(ide_message).await, - // Update the current file; translate it to a URL - // then pass it to the Client. + // Update the current file; translate it to a URL then + // pass it to the Client. EditorMessageContents::CurrentFile(file_path, _is_text) => { debug!("Translating and forwarding it to the Client."); match try_canonicalize(&file_path) { @@ -570,7 +570,8 @@ pub async fn translation_task( EditorMessageContents::Result(ref result) => { debug!("Forwarding it to the IDE."); - // If the Client can't read our diff, send the full text next time. + // If the Client can't read our diff, send the full + // text next time. if matches!(result, Err(ResultErrTypes::OutOfSync)) { tt.sent_full = false; } @@ -579,8 +580,8 @@ pub async fn translation_task( // Open a web browser when requested. EditorMessageContents::OpenUrl(url) => { - // This doesn't work in Codespaces. TODO: send - // this back to the VSCode window, then call + // This doesn't work in Codespaces. TODO: send this + // back to the VSCode window, then call // `vscode.env.openExternal(vscode.Uri.parse(url))`. if let Err(err) = webbrowser::open(&url) { let err = ResultErrTypes::WebBrowserOpenFailed(err.to_string()); @@ -593,8 +594,8 @@ pub async fn translation_task( EditorMessageContents::Update(_) => continue_loop = tt.client_update(client_message).await, - // Update the current file; translate it to a URL - // then pass it to the IDE. + // Update the current file; translate it to a URL then + // pass it to the IDE. EditorMessageContents::CurrentFile(url_string, _is_text) => { debug!("Forwarding translated path to IDE."); let result = match url_to_path(&url_string, tt.prefix) { @@ -603,9 +604,10 @@ pub async fn translation_task( match file_path.to_str() { None => Err(ResultErrTypes::NoPathToString(file_path)), Some(file_path_string) => { - // Use a [binary file - // sniffer](#binary-file-sniffer) to - // determine if the file is text or binary. + // Use a + // [binary file sniffer](#binary-file-sniffer) + // to determine if the file is text or + // binary. let is_text = if let Ok(mut fc) = File::open(&file_path).await { try_read_as_text(&mut fc).await.is_some() } else { @@ -616,7 +618,8 @@ pub async fn translation_task( message: EditorMessageContents::CurrentFile(file_path_string.to_string(), Some(is_text)) })); tt.current_file = file_path; - // Since this is a new file, the full text hasn't been sent yet. + // Since this is a new file, the full text + // hasn't been sent yet. tt.sent_full = false; Ok(()) } @@ -680,15 +683,13 @@ pub async fn translation_task( // These provide translation for messages passing through the Server. impl TranslationTask { - // Pass a `Result` message to the Client, unless - // it's a `LoadFile` result. + // Pass a `Result` message to the Client, unless it's a `LoadFile` result. async fn ide_result(&mut self, ide_message: EditorMessage) -> bool { let EditorMessageContents::Result(ref result) = ide_message.message else { panic!("Should only be called with a result."); }; let is_loadfile = match result { - // See if this error was produced by a - // `LoadFile` result. + // See if this error was produced by a `LoadFile` result. Err(_) => self .load_file_requests .contains_key(&ide_message.id.to_bits()), @@ -697,9 +698,8 @@ impl TranslationTask { ResultOkTypes::LoadFile(_) => true, }, }; - // Pass the message to the client if this isn't - // a `LoadFile` result (the only type of result - // which the Server should handle). + // Pass the message to the client if this isn't a `LoadFile` result (the + // only type of result which the Server should handle). if !is_loadfile { debug!("Forwarding it to the Client."); // If the Server can't read our diff, send the full text next time. @@ -709,8 +709,7 @@ impl TranslationTask { queue_send_func!(self.to_client_tx.send(ide_message)); return true; } - // Ensure there's an HTTP request for this - // `LoadFile` result. + // Ensure there's an HTTP request for this `LoadFile` result. let Some(http_request) = self.load_file_requests.remove(&ide_message.id.to_bits()) else { error!( "Error: no HTTP request found for LoadFile result ID {}.", @@ -719,13 +718,13 @@ impl TranslationTask { return true; }; - // Take ownership of the result after sending it - // above (which requires ownership). + // Take ownership of the result after sending it above (which requires + // ownership). let EditorMessageContents::Result(result) = ide_message.message else { panic!("Not a result."); }; - // Get the file contents from a `LoadFile` - // result; otherwise, this is None. + // Get the file contents from a `LoadFile` result; otherwise, this is + // None. let file_contents_option = match result { Err(err) => { error!("{err:?}"); @@ -737,15 +736,15 @@ impl TranslationTask { }, }; - // Process the file contents. Since VSCode - // doesn't have a PDF viewer, determine if this - // is a PDF file. (TODO: look at the magic - // number also -- "%PDF"). + // Process the file contents. Since VSCode doesn't have a PDF viewer, + // determine if this 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), file_contents) = match file_contents_option { Some((file_contents, new_version)) => { self.version = new_version; - // The IDE just sent the full contents; we're sending full contents to the Client. + // The IDE just sent the full contents; we're sending full + // contents to the Client. self.sent_full = true; ( file_to_response( @@ -760,16 +759,16 @@ impl TranslationTask { ) } None => { - // The file wasn't available in the IDE. - // Look for it in the filesystem. + // The file wasn't available in the IDE. Look for it in the + // filesystem. match File::open(&http_request.file_path).await { Err(err) => ( ( SimpleHttpResponse::Err(SimpleHttpResponseError::Io(err)), None, ), - // There's no file, so return empty - // contents, which will be ignored. + // There's no file, so return empty contents, which will + // be ignored. "".to_string(), ), Ok(mut fc) => { @@ -783,8 +782,8 @@ impl TranslationTask { use_pdf_js, ) .await, - // If the file is binary, return empty - // contents, which will be ignored. + // If the file is binary, return empty contents, + // which will be ignored. option_file_contents.unwrap_or("".to_string()), ) } @@ -800,8 +799,7 @@ impl TranslationTask { }; self.source_code = file_contents; self.eol = find_eol_type(&self.source_code); - // We must clone here, since the original is - // placed in the TX queue. + // We must clone here, since the original is placed in the TX queue. self.code_mirror_doc = plain.doc.clone(); self.code_mirror_doc_blocks = Some(plain.doc_blocks.clone()); @@ -847,9 +845,9 @@ impl TranslationTask { match contents.source { CodeMirrorDiffable::Diff(_diff) => Err(ResultErrTypes::TodoDiffSupport), 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. self.eol = find_eol_type(&code_mirror.doc); let doc_normalized_eols = code_mirror.doc.replace("\r\n", "\n"); // Translate the file. @@ -907,7 +905,8 @@ impl TranslationTask { self.code_mirror_doc = code_mirror_translated.doc; self.code_mirror_doc_blocks = Some(code_mirror_translated.doc_blocks); - // Update to the version of the file just sent. + // Update to the version of the file just + // sent. self.version = contents.version; Ok(ResultOkTypes::Void) } @@ -952,9 +951,8 @@ impl TranslationTask { } } }; - // If there's an error, then report it; - // otherwise, the message is passed to the - // Client, which will provide the result. + // If there's an error, then report it; otherwise, the message is passed + // to the Client, which will provide the result. if let Err(err) = &result { error!("{err:?}"); send_response(&self.to_ide_tx, ide_message.id, result).await; @@ -963,16 +961,21 @@ impl TranslationTask { true } - /// Return a `CodeChatForWeb` struct containing a diff between `self.code_mirror_doc` / `self.code_mirror_doc_blocks` and `code_mirror_translated`. + /// Return a `CodeChatForWeb` struct containing a diff between + /// `self.code_mirror_doc` / `self.code_mirror_doc_blocks` and + /// `code_mirror_translated`. fn diff_code_mirror( &self, - // The `metadata` and `version` fields will be copied from this to the returned `CodeChatForWeb` struct. + // The `metadata` and `version` fields will be copied from this to the + // returned `CodeChatForWeb` struct. metadata: SourceFileMetadata, - // The version number of the previous (before) data. Typically, `self.version`. + // The version number of the previous (before) data. Typically, + // `self.version`. before_version: f64, // The version number for the resulting return struct. version: f64, - // This provides the after data for the diff; before data comes from `self.code_mirror` / `self.code_mirror_doc`. + // This provides the after data for the diff; before data comes from + // `self.code_mirror` / `self.code_mirror_doc`. code_mirror_after: &CodeMirror, ) -> CodeChatForWeb { assert!(self.sent_full); @@ -982,13 +985,13 @@ impl TranslationTask { }; let doc_blocks_diff = diff_code_mirror_doc_blocks(cmdb, &code_mirror_after.doc_blocks); CodeChatForWeb { - // Clone needed here, so we can copy it - // later. + // Clone needed here, so we can copy it later. metadata, source: CodeMirrorDiffable::Diff(CodeMirrorDiff { doc: doc_diff, doc_blocks: doc_blocks_diff, - // The diff was made between the before version (this) and the after version (`ccfw.version`). + // The diff was made between the before version (this) and the + // after version (`ccfw.version`). version: before_version, }), version, @@ -1012,17 +1015,23 @@ impl TranslationTask { None => None, Some(cfw) => match codechat_for_web_to_source(&cfw) { Ok(new_source_code) => { - // Update the stored CodeMirror data structures with what we just received. This must be updated before we can translate back to check for changes (the next step). + // Update the stored CodeMirror data structures with + // what we just received. This must be updated + // before we can translate back to check for changes + // (the next step). let CodeMirrorDiffable::Plain(code_mirror) = cfw.source else { // TODO: support diffable! panic!("Diff not supported."); }; self.code_mirror_doc = code_mirror.doc; self.code_mirror_doc_blocks = Some(code_mirror.doc_blocks); - // We may need to change this version if we send a diff back to the Client. + // We may need to change this version if we send a + // diff back to the Client. let mut cfw_version = cfw.version; - // Translate back to the Client to see if there are any changes after this conversion. Only check CodeChat documents, not Markdown docs. + // Translate back to the Client to see if there are + // any changes after this conversion. Only check + // CodeChat documents, not Markdown docs. if cfw.metadata.mode != MARKDOWN_MODE && let Ok(ccfws) = source_to_codechat_for_web_string( &new_source_code, @@ -1035,17 +1044,34 @@ impl TranslationTask { ccfw.source && self.sent_full { - // Determine if the re-translation includes changes (such as line wrapping in doc blocks which changes line numbering, creation of a new doc block from previous code block text, or updates from future document intelligence such as renamed headings, etc.) For doc blocks that haven't been edited by TinyMCE, this is easy; equality is sufficient. Doc blocks that have been edited are a different case: TinyMCE removes newlines, causing a lot of "changes" to re-insert these. Therefore, use the following approach: + // Determine if the re-translation includes + // changes (such as line wrapping in doc blocks + // which changes line numbering, creation of a + // new doc block from previous code block text, + // or updates from future document intelligence + // such as renamed headings, etc.) For doc + // blocks that haven't been edited by TinyMCE, + // this is easy; equality is sufficient. Doc + // blocks that have been edited are a different + // case: TinyMCE removes newlines, causing a lot + // of "changes" to re-insert these. Therefore, + // use the following approach: // - // 1. Compare the `doc` values. If they differ, then the the Client needs an update. - // 2. Compare each code block using simple equality. If this fails, compare the doc block text excluding newlines. If still different, then the Client needs an update. + // 1. Compare the `doc` values. If they differ, + // then the the Client needs an update. + // 2. Compare each code block using simple + // equality. If this fails, compare the doc + // block text excluding newlines. If still + // different, then the Client needs an + // update. if code_mirror_translated.doc != self.code_mirror_doc || !doc_block_compare( &code_mirror_translated.doc_blocks, self.code_mirror_doc_blocks.as_ref().unwrap(), ) { - // Use a whole number to avoid encoding differences with fractional values. + // Use a whole number to avoid encoding + // differences with fractional values. cfw_version = random::() as f64; // The Client needs an update. let client_contents = self.diff_code_mirror( @@ -1060,7 +1086,9 @@ impl TranslationTask { UpdateMessageContents { file_path: update_message_contents.file_path, contents: Some(client_contents), - // Don't change the current position, since the Client editing position should be left undisturbed. + // Don't change the current position, since + // the Client editing position should be + // left undisturbed. cursor_position: None, scroll_position: None } @@ -1069,8 +1097,7 @@ impl TranslationTask { self.id += MESSAGE_ID_INCREMENT; } }; - // Correct EOL endings for use with the - // IDE. + // Correct EOL endings for use with the IDE. let new_source_code_eol = eol_convert(new_source_code, &self.eol); let ccfw = if self.sent_full && self.allow_source_diffs { Some(CodeChatForWeb { @@ -1149,7 +1176,9 @@ fn doc_block_compare(a: &CodeMirrorDocBlockVec, b: &CodeMirrorDocBlockVec) -> bo && a.indent == b.indent && a.delimiter == b.delimiter && (a.contents == b.contents - // TinyMCE replaces newlines inside paragraphs with a space; for a crude comparison, translate all newlines back to spaces, then ignore leading/trailing newlines. + // TinyMCE replaces newlines inside paragraphs with a space; for + // a crude comparison, translate all newlines back to spaces, + // then ignore leading/trailing newlines. || map_newlines_to_spaces(&a.contents).eq(map_newlines_to_spaces(&b.contents))) }) } @@ -1179,7 +1208,7 @@ fn debug_shorten(val: T) -> String { } // Tests -// ----- +// ----------------------------------------------------------------------------- #[cfg(test)] mod tests { use crate::{processing::CodeMirrorDocBlock, translation::doc_block_compare}; diff --git a/server/src/webserver.rs b/server/src/webserver.rs index baef0294..1e1473c7 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -247,8 +247,8 @@ pub type MessageResult = Result< pub enum ResultOkTypes { /// Most messages have no result. Void, - /// The `LoadFile` message provides file contents and a revision number, if available. This - /// message may only be sent from the IDE to the Server. + /// The `LoadFile` message provides file contents and a revision number, if + /// available. This message may only be sent from the IDE to the Server. LoadFile(Option<(String, f64)>), } diff --git a/server/tests/overall_core/mod.rs b/server/tests/overall_core/mod.rs index 688e88b9..d3ec76da 100644 --- a/server/tests/overall_core/mod.rs +++ b/server/tests/overall_core/mod.rs @@ -14,7 +14,7 @@ // 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 @@ -30,8 +30,8 @@ /// 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) +/// 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 @@ -40,16 +40,16 @@ /// 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. +/// * 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::{ @@ -84,7 +84,7 @@ use code_chat_editor::{ }; // Utilities -// --------- +// ----------------------------------------------------------------------------- // // Not all messages produced by the server are ordered. To accommodate // out-of-order messages, this class provides a way to `insert` expected @@ -164,7 +164,10 @@ macro_rules! harness { // 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. + // 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 @@ -179,6 +182,8 @@ macro_rules! harness { // 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; @@ -253,7 +258,7 @@ fn get_version(msg: &EditorMessage) -> f64 { } // Tests -// ----- +// ----------------------------------------------------------------------------- // // ### Server-side test // @@ -357,7 +362,8 @@ async fn test_server_core( // Focus it. doc_block_contents.click().await.unwrap(); - // The click produces an updated cursor/scroll location after an autosave delay. + // The click produces an updated cursor/scroll location after an autosave + // delay. let mut client_id = INITIAL_CLIENT_MESSAGE_ID; assert_eq!( codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), @@ -379,7 +385,8 @@ async fn test_server_core( // Verify the updated text. client_id += MESSAGE_ID_INCREMENT; - // Update the version from the value provided by the client, which varies randomly. + // Update the version from the value provided by the client, which varies + // randomly. let msg = codechat_server.get_message_timeout(TIMEOUT).await.unwrap(); let client_version = get_version(&msg); assert_eq!( @@ -1166,7 +1173,8 @@ async fn test_client_updates_core( codechat_server.send_result(client_id, None).await.unwrap(); client_id += MESSAGE_ID_INCREMENT; - // The Server sends the Client a wrapped version of the text; the Client replies with a Result(Ok). + // The Server sends the Client a wrapped version of the text; the Client + // replies with a Result(Ok). assert_eq!( codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), EditorMessage { @@ -1198,7 +1206,8 @@ async fn test_client_updates_core( .send_keys("4" + Key::Enter) .await .unwrap(); - // The cursor movement produces a cursor/scroll position update after an autosave delay. + // The cursor movement produces a cursor/scroll position update after an + // autosave delay. assert_eq!( codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), EditorMessage {