Skip to content
Merged

Dev4 #131

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions client/src/CodeChatEditor.mts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ declare global {
cursor_position?: CursorPosition,
scroll_line?: number,
) => Promise<void>;
do_debug: () => void;
send_update: (_only_if_dirty: boolean) => Promise<void>;
scroll_to_line: (
cursor_position?: CursorPosition,
Expand Down Expand Up @@ -136,6 +137,9 @@ let current_metadata: {

const webSocketComm = () => parent.window.CodeChatEditorFramework.webSocketComm;

// This set when a TinyMCE `input` event occurs, which usually produces a duplicate `Dirty` event which should be ignored.
let ignoreDirty = false;

// True if the document is dirty (needs saving).
let is_dirty = false;

Expand Down Expand Up @@ -197,6 +201,14 @@ const open_lp = async (
// incorrect results. This text is the unmodified content sent from the IDE.
let doc_content = "";

// For debugging, allow the extension or server to run this routine by sending
// the appropriate message.
const do_debug = () => {
if (DEBUG_ENABLED) {
tinymce.activeEditor?.save({ format: "raw" });
}
};

// This function is called on page load to "load" a file. Before this point, the
// server has already lexed the source file into code and doc blocks; this
// function transforms the code and doc blocks into HTML and updates the current
Expand Down Expand Up @@ -298,21 +310,18 @@ const _open_lp = async (
// [handling editor events](https://www.tiny.cloud/docs/tinymce/6/events/#handling-editor-events),
// this is how to create a TinyMCE event handler.
setup: (editor: Editor) => {
editor.on(
"dirty",
(
event: EditorEvent<
Events.EditorEventMap["dirty"]
>,
) => {
// Sometimes, `tinymce.activeEditor` is null
// (perhaps when it's not focused). Use the
// `event` data instead.
event.target.setDirty(false);
editor.on("Dirty", () => {
if (!ignoreDirty) {
is_dirty = true;
startAutoUpdateTimer();
},
);
}
startAutoUpdateTimer();
});

editor.on("input", () => {
ignoreDirty = true;
is_dirty = true;
});

// Send updates on cursor movement.
editor.on(
"SelectionChange",
Expand Down Expand Up @@ -430,6 +439,8 @@ const save_lp = async (
// Tiny MCE div. Update the `doc_contents` to stay in sync with
// the Server.
doc_content = tinymce.activeEditor!.save({ format: "raw" });
// The `save()` flushes any duplicate `Dirty` events. After this, following `Dirty` events are genuine.
ignoreDirty = false;
(
code_mirror_diffable as {
Plain: CodeMirror;
Expand Down Expand Up @@ -683,12 +694,12 @@ const scroll_to_line = (
}
};

/*eslint-disable-next-line @typescript-eslint/no-explicit-any */
export const console_log = (...args: any) => {
if (DEBUG_ENABLED) {
console.log(...args);
}
};
// If debug is enabled, show the line number of the caller, not the current line
// number, in the log output.
export const console_log = DEBUG_ENABLED
? console.log.bind(console)
: /*eslint-disable-next-line @typescript-eslint/no-explicit-any */
(..._args: any) => undefined;

// A global error handler: this is called on any uncaught exception.
export const on_error = (event: Event) => {
Expand Down Expand Up @@ -720,6 +731,7 @@ on_dom_content_loaded(async () => {
window.addEventListener("error", on_error);

window.CodeChatEditor = {
do_debug,
open_lp,
send_update,
scroll_to_line,
Expand Down
8 changes: 7 additions & 1 deletion client/src/CodeChatEditorFramework.mts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ class WebSocketComm {
}

case "Result": {
// If the result has the magic ID, then call a debug
// routine.
if (id === 1e6 && DEBUG_ENABLED) {
root_iframe!.contentWindow?.CodeChatEditor?.do_debug();
break;
}
// Cancel the timer for this message and remove it from
// `pending_messages`.
const pending_message = this.pending_messages[id];
Expand Down Expand Up @@ -407,7 +413,7 @@ class WebSocketComm {
Result: result === undefined ? { Ok: "Void" } : { Err: result },
};
console_log(
`CodeChat Client: sending result id = ${id}, message = ${format_struct(message)}`,
`CodeChat Editor Client: sending result id = ${id}, message = ${format_struct(message)}`,
);
// We can't simply call `send_message` because that function expects a
// result message back from the server.
Expand Down
111 changes: 95 additions & 16 deletions client/src/CodeMirror-integration.mts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ import { CursorPosition } from "./rust-types/CursorPosition";
let current_view: EditorView;
// This indicates that a call to `on_dirty` is scheduled, but hasn't run yet.
let on_dirty_scheduled = false;
// This set when an `input` event occurs, which usually produces a duplicate `Dirty` event which should be ignored.
let ignoreDirty = false;

// Options used when creating a `Decoration`.
const decorationOptions = {
Expand Down Expand Up @@ -473,11 +475,13 @@ class DocBlockWidget extends WidgetType {
wrap.innerHTML =
// This doc block's indent. TODO: allow paste, but must only allow
// pasting whitespace.
`<div class="CodeChat-doc-indent" contenteditable onpaste="return false" data-delimiter=${JSON.stringify(
`<div class="CodeChat-doc-indent" contenteditable tabIndex="-1" onpaste="return false" data-delimiter=${JSON.stringify(
this.delimiter,
)}>${sanitize_html(this.indent)}</div>` +
// The contents of this doc block.
`<div class="CodeChat-doc-contents" spellcheck="true" contenteditable>` +
// The contents of this doc block. Make it focusable by assigning a
// tab stop, but not editable (until it's replaced by the TinyMCE
// editor).
`<div class="CodeChat-doc-contents" spellcheck="true" tabIndex="0">` +
this.contents +
"</div>";
// TODO: this is an async call. However, CodeMirror doesn't provide
Expand Down Expand Up @@ -548,8 +552,8 @@ class DocBlockWidget extends WidgetType {
codechat_body.insertBefore(tinymce_div, null);
// Make TinyMCE invisible, since it's placed below the body of the
// page.
tinymce_div.classList.add(CODECHAT_DOC_HIDDEN);
tinymce.activeEditor?.resetContent();
tinymce.get(0)!.dom.addClass(tinymce_div, CODECHAT_DOC_HIDDEN);
tinymce.get(0)!.resetContent();
}
}
}
Expand Down Expand Up @@ -638,6 +642,7 @@ const on_dirty = (
if (on_dirty_scheduled) {
return;
}
set_is_dirty();
on_dirty_scheduled = true;

// Only run this after typesetting is done.
Expand Down Expand Up @@ -669,6 +674,8 @@ const on_dirty = (
const contents = is_tinymce
? tinymce.activeEditor!.save({ format: "raw" })
: contents_div.innerHTML;
// The `save()` flushes any duplicate `Dirty` events. After this, following `Dirty` events are genuine.
ignoreDirty = false;
await mathJaxTypeset(contents_div);
current_view.dispatch({
effects: [
Expand Down Expand Up @@ -833,9 +840,9 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
// If the contents aren't editable, then the div
// won't receive a `focusin` message (it instead
// goes to a CodeMirror layer).
old_contents_div.contentEditable = "true";
old_contents_div.tabIndex = 0;
old_contents_div.innerHTML =
tinymce.activeEditor!.save({ format: "raw" });
tinymce.activeEditor!.save();
tinymce_div.parentNode!.insertBefore(
old_contents_div,
null,
Expand All @@ -848,11 +855,21 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
// div it will replace.
target.insertBefore(tinymce_div, null);

tinymce.activeEditor!.setContent(
// Calling `setContent()` instead produces spurious
// `Dirty` events, observed after receiving a
// re-translation. In addition, `resetContent()` clears
// the undo history, which is appropriate given that
// edits to the previous doc block no longer apply here.
// TODO: Eventually, we need a way to chain TinyMCE's
// undo history with CodeMirror's undo history.
tinymce.activeEditor!.resetContent(
contents_div.innerHTML,
);
contents_div.remove();
tinymce_div.classList.remove(CODECHAT_DOC_HIDDEN);
tinymce.activeEditor!.dom.removeClass(
tinymce_div,
CODECHAT_DOC_HIDDEN,
);
// The new div is now a TinyMCE editor. Retypeset this.
await mathJaxTypeset(tinymce_div);

Expand Down Expand Up @@ -1092,24 +1109,86 @@ export const CodeMirror_load = async (
setup: (editor: Editor) => {
// See the
// [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events).
// This is triggered on edits (just as the `input` event), but
// also when applying formatting changes, inserting images, etc.
// that the above callback misses.
// After much experimentation, using both an `input` event
// (which suppresses the redundant `Dirty` event which follows
// it) combined with a `Dirty` event (which catches GUI
// interactions, undo, etc. which doesn't produce an `input`
// event). Just using `Dirty` produces one failing case: insert
// a character (dirty event), delete the character (no dirty
// event), left arrow (delayed dirty event from backspace).
//
// Here's a demonstration of the bug and its fix:
//
// ```
//
// <!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>TinyMCE Dirty Event Test</title>
// </head>
// <body>
// <h1>TinyMCE Dirty Event Test</h1>
// <textarea id="editor">
// <p>Edit this content to trigger the dirty event.</p>
// </textarea>
// <script
// src="https://cdn.tiny.cloud/1/rrqw1m3511pf4ag8c5zao97ad7ymvnhqu6z0995b1v63rqb5/tinymce/8/tinymce.min.js"
// referrerpolicy="origin" crossorigin="anonymous">
// </script>
// <script>
// // Version 1: `dirty` event only; buggy.
// // Version 2: `input` and `dirty`; works.
// const version = 2;
// let ignoreDirty = false;
// const saveEditor = (eventDescription) => {
// console.log(`${eventDescription} fired. save() output: ${tinymce.activeEditor.save()}`);
// ignoreDirty = false;
// };
// tinymce.init({
// selector: '#editor',
// setup(editor) {
// editor.on('dirty', () => {
// if (!ignoreDirty || version === 1) {
// saveEditor('dirty');
// }
// });
// editor.on('input', () => {
// if (version === 2) {
// ignoreDirty = true;
// saveEditor('input');
// }
// });
// }
// });
// </script>
// </body>
// ```
editor.on(
"Dirty",
(event: EditorEvent<Events.EditorEventMap["dirty"]>) => {
// Sometimes, `tinymce.activeEditor` is null (perhaps
// when it's not focused). Use the `event` data instead.
event.target.setDirty(false);
// Get the div TinyMCE stores edits in.
const target_or_false = event.target.bodyElement;
if (target_or_false == null) {
const target = event.target.bodyElement;
if (target == null) {
return;
}
setTimeout(() => on_dirty(target_or_false));
if (!ignoreDirty) {
on_dirty(target);
}
},
);

editor.on("input", (event: InputEvent) => {
const target = event.target as HTMLElement;
if (target == null) {
return;
}
ignoreDirty = true;
on_dirty(target);
});

// Send updates on cursor movement.
editor.on(
"SelectionChange",
Expand Down
3 changes: 0 additions & 3 deletions server/src/translation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,9 +1355,6 @@ fn compare_html(
// paragraph</p><iframe>...</iframe>`, which minifies to `<p>Previous
// paragraph</p><iframe>...</iframe>`. Fix up this difference.
let raw_html = raw_html.replace("<p><iframe ", "</p><iframe ");
if normalized_html != raw_html {
println!("Comparison failed.\n IDE: {normalized_html:#?}\nTinyMCE: {raw_html:#?}");
}

normalized_html == raw_html
} else {
Expand Down
Loading
Loading