diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js
index f4a17f63..d46a705d 100644
--- a/src/core/ChefWorker.js
+++ b/src/core/ChefWorker.js
@@ -186,7 +186,7 @@ async function getDishTitle(data) {
*
* @param {Object[]} recipeConfig
* @param {string} direction
- * @param {Object} pos - The position object for the highlight.
+ * @param {Object[]} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
diff --git a/src/core/operations/ToHex.mjs b/src/core/operations/ToHex.mjs
index 71893105..092155a9 100644
--- a/src/core/operations/ToHex.mjs
+++ b/src/core/operations/ToHex.mjs
@@ -76,7 +76,7 @@ class ToHex extends Operation {
}
const lineSize = args[1],
- len = (delim === "\r\n" ? 1 : delim.length) + commaLen;
+ len = delim.length + commaLen;
const countLF = function(p) {
// Count the number of LFs from 0 upto p
@@ -105,7 +105,7 @@ class ToHex extends Operation {
* @returns {Object[]} pos
*/
highlightReverse(pos, args) {
- let delim, commaLen;
+ let delim, commaLen = 0;
if (args[0] === "0x with comma") {
delim = "0x";
commaLen = 1;
@@ -114,7 +114,7 @@ class ToHex extends Operation {
}
const lineSize = args[1],
- len = (delim === "\r\n" ? 1 : delim.length) + commaLen,
+ len = delim.length + commaLen,
width = len + 2;
const countLF = function(p) {
diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs
index 2477bb60..a46379e9 100755
--- a/src/web/Manager.mjs
+++ b/src/web/Manager.mjs
@@ -153,10 +153,6 @@ class Manager {
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
- document.getElementById("input-text").addEventListener("scroll", this.highlighter.inputScroll.bind(this.highlighter));
- document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter));
- document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter));
- this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input));
document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input));
@@ -188,10 +184,6 @@ class Manager {
document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output));
- document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
- document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
- document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
- this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
this.addDynamicListener("#output-file-show-all", "click", this.output.showAllFile, this.output);
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs
index d1340165..8b4375fe 100755
--- a/src/web/waiters/HighlighterWaiter.mjs
+++ b/src/web/waiters/HighlighterWaiter.mjs
@@ -4,17 +4,7 @@
* @license Apache-2.0
*/
-/**
- * HighlighterWaiter data type enum for the input.
- * @enum
- */
-const INPUT = 0;
-
-/**
- * HighlighterWaiter data type enum for the output.
- * @enum
- */
-const OUTPUT = 1;
+import {EditorSelection} from "@codemirror/state";
/**
@@ -32,309 +22,81 @@ class HighlighterWaiter {
this.app = app;
this.manager = manager;
- this.mouseButtonDown = false;
- this.mouseTarget = null;
+ this.currentSelectionRanges = [];
}
-
/**
- * Determines if the current text selection is running backwards or forwards.
- * StackOverflow answer id: 12652116
+ * Handler for selection change events in the input and output
*
- * @private
- * @returns {boolean}
- */
- _isSelectionBackwards() {
- let backwards = false;
- const sel = window.getSelection();
-
- if (!sel.isCollapsed) {
- const range = document.createRange();
- range.setStart(sel.anchorNode, sel.anchorOffset);
- range.setEnd(sel.focusNode, sel.focusOffset);
- backwards = range.collapsed;
- range.detach();
- }
- return backwards;
- }
-
-
- /**
- * Calculates the text offset of a position in an HTML element, ignoring HTML tags.
- *
- * @private
- * @param {element} node - The parent HTML node.
- * @param {number} offset - The offset since the last HTML element.
- * @returns {number}
- */
- _getOutputHtmlOffset(node, offset) {
- const sel = window.getSelection();
- const range = document.createRange();
-
- range.selectNodeContents(document.getElementById("output-html"));
- range.setEnd(node, offset);
- sel.removeAllRanges();
- sel.addRange(range);
-
- return sel.toString().length;
- }
-
-
- /**
- * Gets the current selection offsets in the output HTML, ignoring HTML tags.
- *
- * @private
- * @returns {Object} pos
- * @returns {number} pos.start
- * @returns {number} pos.end
- */
- _getOutputHtmlSelectionOffsets() {
- const sel = window.getSelection();
- let range,
- start = 0,
- end = 0,
- backwards = false;
-
- if (sel.rangeCount) {
- range = sel.getRangeAt(sel.rangeCount - 1);
- backwards = this._isSelectionBackwards();
- start = this._getOutputHtmlOffset(range.startContainer, range.startOffset);
- end = this._getOutputHtmlOffset(range.endContainer, range.endOffset);
- sel.removeAllRanges();
- sel.addRange(range);
-
- if (backwards) {
- // If selecting backwards, reverse the start and end offsets for the selection to
- // prevent deselecting as the drag continues.
- sel.collapseToEnd();
- sel.extend(sel.anchorNode, range.startOffset);
- }
- }
-
- return {
- start: start,
- end: end
- };
- }
-
-
- /**
- * Handler for input scroll events.
- * Scrolls the highlighter pane to match the input textarea position.
- *
- * @param {event} e
- */
- inputScroll(e) {
- const el = e.target;
- document.getElementById("input-highlighter").scrollTop = el.scrollTop;
- document.getElementById("input-highlighter").scrollLeft = el.scrollLeft;
- }
-
-
- /**
- * Handler for output scroll events.
- * Scrolls the highlighter pane to match the output textarea position.
- *
- * @param {event} e
- */
- outputScroll(e) {
- const el = e.target;
- document.getElementById("output-highlighter").scrollTop = el.scrollTop;
- document.getElementById("output-highlighter").scrollLeft = el.scrollLeft;
- }
-
-
- /**
- * Handler for input mousedown events.
- * Calculates the current selection info, and highlights the corresponding data in the output.
- *
- * @param {event} e
- */
- inputMousedown(e) {
- this.mouseButtonDown = true;
- this.mouseTarget = INPUT;
- this.removeHighlights();
-
- const sel = document.getSelection();
- const start = sel.baseOffset;
- const end = sel.extentOffset;
-
- if (start !== 0 || end !== 0) {
- this.highlightOutput([{start: start, end: end}]);
- }
- }
-
-
- /**
- * Handler for output mousedown events.
- * Calculates the current selection info, and highlights the corresponding data in the input.
- *
- * @param {event} e
- */
- outputMousedown(e) {
- this.mouseButtonDown = true;
- this.mouseTarget = OUTPUT;
- this.removeHighlights();
-
- const sel = document.getSelection();
- const start = sel.baseOffset;
- const end = sel.extentOffset;
-
- if (start !== 0 || end !== 0) {
- this.highlightInput([{start: start, end: end}]);
- }
- }
-
-
- /**
- * Handler for input mouseup events.
- *
- * @param {event} e
- */
- inputMouseup(e) {
- this.mouseButtonDown = false;
- }
-
-
- /**
- * Handler for output mouseup events.
- *
- * @param {event} e
- */
- outputMouseup(e) {
- this.mouseButtonDown = false;
- }
-
-
- /**
- * Handler for input mousemove events.
- * Calculates the current selection info, and highlights the corresponding data in the output.
- *
- * @param {event} e
- */
- inputMousemove(e) {
- // Check that the left mouse button is pressed
- if (!this.mouseButtonDown ||
- e.which !== 1 ||
- this.mouseTarget !== INPUT)
- return;
-
- const sel = document.getSelection();
- const start = sel.baseOffset;
- const end = sel.extentOffset;
-
- if (start !== 0 || end !== 0) {
- this.highlightOutput([{start: start, end: end}]);
- }
- }
-
-
- /**
- * Handler for output mousemove events.
- * Calculates the current selection info, and highlights the corresponding data in the input.
- *
- * @param {event} e
- */
- outputMousemove(e) {
- // Check that the left mouse button is pressed
- if (!this.mouseButtonDown ||
- e.which !== 1 ||
- this.mouseTarget !== OUTPUT)
- return;
-
- const sel = document.getSelection();
- const start = sel.baseOffset;
- const end = sel.extentOffset;
-
- if (start !== 0 || end !== 0) {
- this.highlightInput([{start: start, end: end}]);
- }
- }
-
-
- /**
- * Given start and end offsets, writes the HTML for the selection info element with the correct
- * padding.
- *
- * @param {number} start - The start offset.
- * @param {number} end - The end offset.
- * @returns {string}
- */
- selectionInfo(start, end) {
- const len = end.toString().length;
- const width = len < 2 ? 2 : len;
- const startStr = start.toString().padStart(width, " ").replace(/ /g, " ");
- const endStr = end.toString().padStart(width, " ").replace(/ /g, " ");
- const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, " ");
-
- return "start: " + startStr + "
end: " + endStr + "
length: " + lenStr;
- }
-
-
- /**
- * Removes highlighting and selection information.
- */
- removeHighlights() {
- document.getElementById("input-highlighter").innerHTML = "";
- document.getElementById("output-highlighter").innerHTML = "";
- }
-
-
- /**
- * Highlights the given offsets in the output.
+ * Highlights the given offsets in the input or output.
* We will only highlight if:
* - input hasn't changed since last bake
* - last bake was a full bake
* - all operations in the recipe support highlighting
*
- * @param {Object} pos - The position object for the highlight.
- * @param {number} pos.start - The start offset.
- * @param {number} pos.end - The end offset.
+ * @param {string} io
+ * @param {ViewUpdate} e
*/
- highlightOutput(pos) {
+ selectionChange(io, e) {
+ // Confirm we are not currently baking
if (!this.app.autoBake_ || this.app.baking) return false;
- this.manager.worker.highlight(this.app.getRecipeConfig(), "forward", pos);
+
+ // Confirm this was a user-generated event to prevent looping
+ // from setting the selection in this class
+ if (!e.transactions[0].isUserEvent("select")) return false;
+
+ const view = io === "input" ?
+ this.manager.output.outputEditorView :
+ this.manager.input.inputEditorView;
+
+ this.currentSelectionRanges = [];
+
+ // Confirm some non-empty ranges are set
+ const selectionRanges = e.state.selection.ranges.filter(r => !r.empty);
+ if (!selectionRanges.length) {
+ this.resetSelections(view);
+ return;
+ }
+
+ // Loop through ranges and send request for output offsets for each one
+ const direction = io === "input" ? "forward" : "reverse";
+ for (const range of selectionRanges) {
+ const pos = [{
+ start: range.from,
+ end: range.to
+ }];
+ this.manager.worker.highlight(this.app.getRecipeConfig(), direction, pos);
+ }
}
-
/**
- * Highlights the given offsets in the input.
- * We will only highlight if:
- * - input hasn't changed since last bake
- * - last bake was a full bake
- * - all operations in the recipe support highlighting
- *
- * @param {Object} pos - The position object for the highlight.
- * @param {number} pos.start - The start offset.
- * @param {number} pos.end - The end offset.
+ * Resets the current set of selections in the given view
+ * @param {EditorView} view
*/
- highlightInput(pos) {
- if (!this.app.autoBake_ || this.app.baking) return false;
- this.manager.worker.highlight(this.app.getRecipeConfig(), "reverse", pos);
+ resetSelections(view) {
+ this.currentSelectionRanges = [];
+
+ // Clear current selection in output or input
+ view.dispatch({
+ selection: EditorSelection.create([EditorSelection.range(0, 0)])
+ });
}
/**
* Displays highlight offsets sent back from the Chef.
*
- * @param {Object} pos - The position object for the highlight.
+ * @param {Object[]} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
* @param {string} direction
*/
displayHighlights(pos, direction) {
if (!pos) return;
-
if (this.manager.tabs.getActiveInputTab() !== this.manager.tabs.getActiveOutputTab()) return;
const io = direction === "forward" ? "output" : "input";
-
- // TODO
- // document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
- this.highlight(
- document.getElementById(io + "-text"),
- document.getElementById(io + "-highlighter"),
- pos);
+ this.highlight(io, pos);
}
@@ -342,74 +104,35 @@ class HighlighterWaiter {
* Adds the relevant HTML to the specified highlight element such that highlighting appears
* underneath the correct offset.
*
- * @param {element} textarea - The input or output textarea.
- * @param {element} highlighter - The input or output highlighter element.
- * @param {Object} pos - The position object for the highlight.
- * @param {number} pos.start - The start offset.
- * @param {number} pos.end - The end offset.
+ * @param {string} io - The input or output
+ * @param {Object[]} ranges - An array of position objects to highlight
+ * @param {number} ranges.start - The start offset
+ * @param {number} ranges.end - The end offset
*/
- async highlight(textarea, highlighter, pos) {
- // if (!this.app.options.showHighlighter) return false;
- // if (!this.app.options.attemptHighlight) return false;
+ async highlight(io, ranges) {
+ if (!this.app.options.showHighlighter) return false;
+ if (!this.app.options.attemptHighlight) return false;
+ if (!ranges || !ranges.length) return false;
- // // Check if there is a carriage return in the output dish as this will not
- // // be displayed by the HTML textarea and will mess up highlighting offsets.
- // if (await this.manager.output.containsCR()) return false;
+ const view = io === "input" ?
+ this.manager.input.inputEditorView :
+ this.manager.output.outputEditorView;
- // const startPlaceholder = "[startHighlight]";
- // const startPlaceholderRegex = /\[startHighlight\]/g;
- // const endPlaceholder = "[endHighlight]";
- // const endPlaceholderRegex = /\[endHighlight\]/g;
- // // let text = textarea.value; // TODO
+ // Add new SelectionRanges to existing ones
+ for (const range of ranges) {
+ if (!range.start || !range.end) continue;
+ this.currentSelectionRanges.push(
+ EditorSelection.range(range.start, range.end)
+ );
+ }
- // // Put placeholders in position
- // // If there's only one value, select that
- // // If there are multiple, ignore the first one and select all others
- // if (pos.length === 1) {
- // if (pos[0].end < pos[0].start) return;
- // text = text.slice(0, pos[0].start) +
- // startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder +
- // text.slice(pos[0].end, text.length);
- // } else {
- // // O(n^2) - Can anyone improve this without overwriting placeholders?
- // let result = "",
- // endPlaced = true;
-
- // for (let i = 0; i < text.length; i++) {
- // for (let j = 1; j < pos.length; j++) {
- // if (pos[j].end < pos[j].start) continue;
- // if (pos[j].start === i) {
- // result += startPlaceholder;
- // endPlaced = false;
- // }
- // if (pos[j].end === i) {
- // result += endPlaceholder;
- // endPlaced = true;
- // }
- // }
- // result += text[i];
- // }
- // if (!endPlaced) result += endPlaceholder;
- // text = result;
- // }
-
- // const cssClass = "hl1";
-
- // // Remove HTML tags
- // text = text
- // .replace(/&/g, "&")
- // .replace(//g, ">")
- // .replace(/\n/g, "
")
- // // Convert placeholders to tags
- // .replace(startPlaceholderRegex, "")
- // .replace(endPlaceholderRegex, "") + " ";
-
- // // Adjust width to allow for scrollbars
- // highlighter.style.width = textarea.clientWidth + "px";
- // highlighter.innerHTML = text;
- // highlighter.scrollTop = textarea.scrollTop;
- // highlighter.scrollLeft = textarea.scrollLeft;
+ // Set selection
+ if (this.currentSelectionRanges.length) {
+ view.dispatch({
+ selection: EditorSelection.create(this.currentSelectionRanges),
+ scrollIntoView: true
+ });
+ }
}
}
diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs
index 0dc44dbe..ff512f69 100644
--- a/src/web/waiters/InputWaiter.mjs
+++ b/src/web/waiters/InputWaiter.mjs
@@ -87,6 +87,7 @@ class InputWaiter {
const initialState = EditorState.create({
doc: null,
extensions: [
+ // Editor extensions
history(),
highlightSpecialChars({render: renderSpecialChar}),
drawSelection(),
@@ -95,13 +96,19 @@ class InputWaiter {
bracketMatching(),
highlightSelectionMatches(),
search({top: true}),
+ EditorState.allowMultipleSelections.of(true),
+
+ // Custom extensions
statusBar({
label: "Input",
eolHandler: this.eolChange.bind(this)
}),
+
+ // Mutable state
this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping),
this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
- EditorState.allowMultipleSelections.of(true),
+
+ // Keymap
keymap.of([
// Explicitly insert a tab rather than indenting the line
{ key: "Tab", run: insertTab },
@@ -112,6 +119,12 @@ class InputWaiter {
...defaultKeymap,
...searchKeymap
]),
+
+ // Event listeners
+ EditorView.updateListener.of(e => {
+ if (e.selectionSet)
+ this.manager.highlighter.selectionChange("input", e);
+ })
]
});
@@ -771,9 +784,6 @@ class InputWaiter {
const fileOverlay = document.getElementById("input-file");
if (fileOverlay.style.display === "block") return;
- // Remove highlighting from input and output panes as the offsets might be different now
- this.manager.highlighter.removeHighlights();
-
const value = this.getInput();
const activeTab = this.manager.tabs.getActiveInputTab();
@@ -1033,9 +1043,6 @@ class InputWaiter {
this.manager.output.removeAllOutputs();
this.manager.output.terminateZipWorker();
- this.manager.highlighter.removeHighlights();
- getSelection().removeAllRanges();
-
const tabsList = document.getElementById("input-tabs");
const tabsListChildren = tabsList.children;
@@ -1073,9 +1080,6 @@ class InputWaiter {
const inputNum = this.manager.tabs.getActiveInputTab();
if (inputNum === -1) return;
- this.manager.highlighter.removeHighlights();
- getSelection().removeAllRanges();
-
this.updateInputValue(inputNum, "", true);
this.set({
diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs
index 496b0ac5..d1fd2532 100755
--- a/src/web/waiters/OutputWaiter.mjs
+++ b/src/web/waiters/OutputWaiter.mjs
@@ -67,6 +67,7 @@ class OutputWaiter {
const initialState = EditorState.create({
doc: null,
extensions: [
+ // Editor extensions
EditorState.readOnly.of(true),
htmlPlugin(this.htmlOutput),
highlightSpecialChars({render: renderSpecialChar}),
@@ -76,18 +77,30 @@ class OutputWaiter {
bracketMatching(),
highlightSelectionMatches(),
search({top: true}),
+ EditorState.allowMultipleSelections.of(true),
+
+ // Custom extensiosn
statusBar({
label: "Output",
bakeStats: this.bakeStats,
eolHandler: this.eolChange.bind(this)
}),
+
+ // Mutable state
this.outputEditorConf.lineWrapping.of(EditorView.lineWrapping),
this.outputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
- EditorState.allowMultipleSelections.of(true),
+
+ // Keymap
keymap.of([
...defaultKeymap,
...searchKeymap
]),
+
+ // Event listeners
+ EditorView.updateListener.of(e => {
+ if (e.selectionSet)
+ this.manager.highlighter.selectionChange("output", e);
+ })
]
});
@@ -817,9 +830,6 @@ class OutputWaiter {
this.hideMagicButton();
- this.manager.highlighter.removeHighlights();
- getSelection().removeAllRanges();
-
if (!this.manager.tabs.changeOutputTab(inputNum)) {
let direction = "right";
if (currentNum > inputNum) {
@@ -1343,21 +1353,6 @@ class OutputWaiter {
document.body.removeChild(textarea);
}
- /**
- * Returns true if the output contains carriage returns
- *
- * @returns {boolean}
- */
- async containsCR() {
- const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
- if (dish === null) return;
-
- if (dish.type === Dish.STRING) {
- const data = await dish.get(Dish.STRING);
- return data.indexOf("\r") >= 0;
- }
- }
-
/**
* Handler for switch click events.
* Moves the current output into the input textarea.
diff --git a/src/web/waiters/TabWaiter.mjs b/src/web/waiters/TabWaiter.mjs
index 384b1ab7..f5b0efd4 100644
--- a/src/web/waiters/TabWaiter.mjs
+++ b/src/web/waiters/TabWaiter.mjs
@@ -305,9 +305,6 @@ class TabWaiter {
changeTab(inputNum, io) {
const tabsList = document.getElementById(`${io}-tabs`);
- this.manager.highlighter.removeHighlights();
- getSelection().removeAllRanges();
-
let found = false;
for (let i = 0; i < tabsList.children.length; i++) {
const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
diff --git a/src/web/waiters/WorkerWaiter.mjs b/src/web/waiters/WorkerWaiter.mjs
index 7fcaa509..a63bfc1f 100644
--- a/src/web/waiters/WorkerWaiter.mjs
+++ b/src/web/waiters/WorkerWaiter.mjs
@@ -794,7 +794,7 @@ class WorkerWaiter {
*
* @param {Object[]} recipeConfig
* @param {string} direction
- * @param {Object} pos - The position object for the highlight.
+ * @param {Object[]} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/