Преглед на файлове

Input now uses CodeMirror editor

n1474335 преди 3 години
родител
ревизия
85ffe48743

+ 197 - 0
package-lock.json

@@ -95,6 +95,11 @@
         "@babel/plugin-transform-runtime": "^7.18.2",
         "@babel/preset-env": "^7.18.2",
         "@babel/runtime": "^7.18.3",
+        "@codemirror/commands": "^6.0.0",
+        "@codemirror/language": "^6.1.0",
+        "@codemirror/search": "^6.0.0",
+        "@codemirror/state": "^6.0.1",
+        "@codemirror/view": "^6.0.2",
         "autoprefixer": "^10.4.7",
         "babel-loader": "^8.2.5",
         "babel-plugin-dynamic-import-node": "^2.3.3",
@@ -1782,6 +1787,60 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@codemirror/commands": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz",
+      "integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==",
+      "dev": true,
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/language": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz",
+      "integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==",
+      "dev": true,
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0",
+        "style-mod": "^4.0.0"
+      }
+    },
+    "node_modules/@codemirror/search": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
+      "integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
+      "dev": true,
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "node_modules/@codemirror/state": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz",
+      "integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==",
+      "dev": true
+    },
+    "node_modules/@codemirror/view": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
+      "integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
+      "dev": true,
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "style-mod": "^4.0.0",
+        "w3c-keyname": "^2.2.4"
+      }
+    },
     "node_modules/@colors/colors": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -2370,6 +2429,30 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
       "dev": true
     },
+    "node_modules/@lezer/common": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
+      "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==",
+      "dev": true
+    },
+    "node_modules/@lezer/highlight": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
+      "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
+      "dev": true,
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/lr": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz",
+      "integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==",
+      "dev": true,
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
     "node_modules/@nightwatch/chai": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/@nightwatch/chai/-/chai-5.0.2.tgz",
@@ -5059,6 +5142,12 @@
         "sha.js": "^2.4.8"
       }
     },
+    "node_modules/crelt": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
+      "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==",
+      "dev": true
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -14244,6 +14333,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/style-mod": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
+      "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
+      "dev": true
+    },
     "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -14982,6 +15077,12 @@
       "resolved": "https://registry.npmjs.org/vkbeautify/-/vkbeautify-0.99.3.tgz",
       "integrity": "sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q=="
     },
+    "node_modules/w3c-keyname": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
+      "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
+      "dev": true
+    },
     "node_modules/watchpack": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
@@ -17001,6 +17102,60 @@
         "to-fast-properties": "^2.0.0"
       }
     },
+    "@codemirror/commands": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz",
+      "integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==",
+      "dev": true,
+      "requires": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "@codemirror/language": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz",
+      "integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==",
+      "dev": true,
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0",
+        "style-mod": "^4.0.0"
+      }
+    },
+    "@codemirror/search": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
+      "integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
+      "dev": true,
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "@codemirror/state": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz",
+      "integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==",
+      "dev": true
+    },
+    "@codemirror/view": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
+      "integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
+      "dev": true,
+      "requires": {
+        "@codemirror/state": "^6.0.0",
+        "style-mod": "^4.0.0",
+        "w3c-keyname": "^2.2.4"
+      }
+    },
     "@colors/colors": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -17452,6 +17607,30 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
       "dev": true
     },
+    "@lezer/common": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
+      "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==",
+      "dev": true
+    },
+    "@lezer/highlight": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
+      "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
+      "dev": true,
+      "requires": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "@lezer/lr": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz",
+      "integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==",
+      "dev": true,
+      "requires": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
     "@nightwatch/chai": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/@nightwatch/chai/-/chai-5.0.2.tgz",
@@ -19640,6 +19819,12 @@
         "sha.js": "^2.4.8"
       }
     },
+    "crelt": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
+      "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==",
+      "dev": true
+    },
     "cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -26720,6 +26905,12 @@
       "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
       "dev": true
     },
+    "style-mod": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
+      "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
+      "dev": true
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -27303,6 +27494,12 @@
       "resolved": "https://registry.npmjs.org/vkbeautify/-/vkbeautify-0.99.3.tgz",
       "integrity": "sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q=="
     },
+    "w3c-keyname": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
+      "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
+      "dev": true
+    },
     "watchpack": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",

+ 5 - 0
package.json

@@ -45,6 +45,11 @@
     "@babel/plugin-transform-runtime": "^7.18.2",
     "@babel/preset-env": "^7.18.2",
     "@babel/runtime": "^7.18.3",
+    "@codemirror/commands": "^6.0.0",
+    "@codemirror/language": "^6.1.0",
+    "@codemirror/search": "^6.0.0",
+    "@codemirror/state": "^6.0.1",
+    "@codemirror/view": "^6.0.2",
     "autoprefixer": "^10.4.7",
     "babel-loader": "^8.2.5",
     "babel-plugin-dynamic-import-node": "^2.3.3",

+ 1 - 1
src/core/operations/ParseColourCode.mjs

@@ -112,7 +112,7 @@ CMYK: ${cmyk}
         useAlpha: true
     }).on('colorpickerChange', function(e) {
         var color = e.color.string('rgba');
-        document.getElementById('input-text').value = color;
+        window.app.manager.input.setInput(color);
         window.app.manager.input.debounceInputChange(new Event("keyup"));
     });
 </script>`;

+ 2 - 2
src/web/Manager.mjs

@@ -146,8 +146,7 @@ class Manager {
         this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
 
         // Input
-        this.addMultiEventListener("#input-text", "keyup", this.input.debounceInputChange, this.input);
-        this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
+        document.getElementById("input-text").addEventListener("keyup", this.input.debounceInputChange.bind(this.input));
         document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
         this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input);
         this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input);
@@ -179,6 +178,7 @@ class Manager {
         this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input);
         document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input));
         document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input));
+        this.addDynamicListener(".eol-select a", "click", this.input.eolSelectClick, this.input);
 
 
         // Output

+ 190 - 0
src/web/extensions/statusBar.mjs

@@ -0,0 +1,190 @@
+/**
+ * A Status bar extension for CodeMirror
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+import {showPanel} from "@codemirror/view";
+
+/**
+ * Counts the stats of a document
+ * @param {element} el
+ * @param {Text} doc
+ */
+function updateStats(el, doc) {
+    const length = el.querySelector("#stats-length-value"),
+        lines = el.querySelector("#stats-lines-value");
+    length.textContent = doc.length;
+    lines.textContent = doc.lines;
+}
+
+/**
+ * Gets the current selection info
+ * @param {element} el
+ * @param {EditorState} state
+ * @param {boolean} selectionSet
+ */
+function updateSelection(el, state, selectionSet) {
+    const selLen = state.selection && state.selection.main ?
+        state.selection.main.to - state.selection.main.from :
+        0;
+
+    const selInfo = el.querySelector("#sel-info"),
+        curOffsetInfo = el.querySelector("#cur-offset-info");
+
+    if (!selectionSet) {
+        selInfo.style.display = "none";
+        curOffsetInfo.style.display = "none";
+        return;
+    }
+
+    if (selLen > 0) { // Range
+        const start = el.querySelector("#sel-start-value"),
+            end = el.querySelector("#sel-end-value"),
+            length = el.querySelector("#sel-length-value");
+
+        selInfo.style.display = "inline-block";
+        curOffsetInfo.style.display = "none";
+
+        start.textContent = state.selection.main.from;
+        end.textContent = state.selection.main.to;
+        length.textContent = state.selection.main.to - state.selection.main.from;
+    } else { // Position
+        const offset = el.querySelector("#cur-offset-value");
+
+        selInfo.style.display = "none";
+        curOffsetInfo.style.display = "inline-block";
+
+        offset.textContent = state.selection.main.from;
+    }
+}
+
+/**
+ * Gets the current character encoding of the document
+ * @param {element} el
+ * @param {EditorState} state
+ */
+function updateCharEnc(el, state) {
+    // const charenc = el.querySelector("#char-enc-value");
+    // TODO
+    // charenc.textContent = "TODO";
+}
+
+/**
+ * Returns what the current EOL separator is set to
+ * @param {element} el
+ * @param {EditorState} state
+ */
+function updateEOL(el, state) {
+    const eolLookup = {
+        "\u000a": "LF",
+        "\u000b": "VT",
+        "\u000c": "FF",
+        "\u000d": "CR",
+        "\u000d\u000a": "CRLF",
+        "\u0085": "NEL",
+        "\u2028": "LS",
+        "\u2029": "PS"
+    };
+
+    const val = el.querySelector("#eol-value");
+    val.textContent = eolLookup[state.lineBreak];
+}
+
+/**
+ * Builds the Left-hand-side widgets
+ * @returns {string}
+ */
+function constructLHS() {
+    return `<span data-toggle="tooltip" title="Input length">
+            <i class="material-icons">abc</i>
+            <span id="stats-length-value"></span>
+        </span>
+        <span data-toggle="tooltip" title="Number of lines">
+            <i class="material-icons">sort</i>
+            <span id="stats-lines-value"></span>
+        </span>
+
+        <span id="sel-info" data-toggle="tooltip" title="Selection">
+            <i class="material-icons">highlight_alt</i>
+            <span id="sel-start-value"></span>\u279E<span id="sel-end-value"></span>
+            (<span id="sel-length-value"></span> selected)
+        </span>
+        <span id="cur-offset-info" data-toggle="tooltip" title="Cursor offset">
+            <i class="material-icons">location_on</i>
+            <span id="cur-offset-value"></span>
+        </span>`;
+}
+
+/**
+ * Builds the Right-hand-side widgets
+ * Event listener set up in Manager
+ * @returns {string}
+ */
+function constructRHS() {
+    return `<span data-toggle="tooltip" title="Input character encoding">
+        <i class="material-icons">language</i>
+        <span id="char-enc-value">UTF-16</span>
+    </span>
+
+    <div class="cm-status-bar-select eol-select">
+        <span class="cm-status-bar-select-btn" data-toggle="tooltip" data-placement="bottom" title="End of line sequence">
+            <i class="material-icons">keyboard_return</i> <span id="eol-value"></span>
+        </span>
+        <div class="cm-status-bar-select-content">
+            <a href="#" data-val="LF">Line Feed, U+000A</a>
+            <a href="#" data-val="VT">Vertical Tab, U+000B</a>
+            <a href="#" data-val="FF">Form Feed, U+000C</a>
+            <a href="#" data-val="CR">Carriage Return, U+000D</a>
+            <a href="#" data-val="CRLF">CR+LF, U+000D U+000A</a>
+            <!-- <a href="#" data-val="NL">Next Line, U+0085</a> This causes problems. -->
+            <a href="#" data-val="LS">Line Separator, U+2028</a>
+            <a href="#" data-val="PS">Paragraph Separator, U+2029</a>
+        </div>
+    </div>`;
+}
+
+/**
+ * A panel constructor building a panel that re-counts the stats every time the document changes.
+ * @param {EditorView} view
+ * @returns {Panel}
+ */
+function wordCountPanel(view) {
+    const dom = document.createElement("div");
+    const lhs = document.createElement("div");
+    const rhs = document.createElement("div");
+
+    dom.className = "cm-status-bar";
+    lhs.innerHTML = constructLHS();
+    rhs.innerHTML = constructRHS();
+
+    dom.appendChild(lhs);
+    dom.appendChild(rhs);
+
+    updateEOL(rhs, view.state);
+    updateCharEnc(rhs, view.state);
+    updateStats(lhs, view.state.doc);
+    updateSelection(lhs, view.state, false);
+
+    return {
+        dom,
+        update(update) {
+            updateEOL(rhs, update.state);
+            updateSelection(lhs, update.state, update.selectionSet);
+            updateCharEnc(rhs, update.state);
+            if (update.docChanged) {
+                updateStats(lhs, update.state.doc);
+            }
+        }
+    };
+}
+
+/**
+ * A function that build the extension that enables the panel in an editor.
+ * @returns {Extension}
+ */
+export function statusBar() {
+    return showPanel.of(wordCountPanel);
+}

+ 1 - 3
src/web/html/index.html

@@ -219,8 +219,6 @@
                             <label for="input-text">Input</label>
                             <span class="pane-controls">
                                 <div class="io-info" id="input-files-info"></div>
-                                <div class="io-info" id="input-selection-info"></div>
-                                <div class="io-info" id="input-info"></div>
                                 <button type="button" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab">
                                     <i class="material-icons">add</i>
                                 </button>
@@ -267,7 +265,7 @@
                         </div>
                         <div class="textarea-wrapper no-select input-wrapper" id="input-wrapper">
                             <div id="input-highlighter" class="no-select"></div>
-                            <textarea id="input-text" class="input-text" spellcheck="false" tabindex="1" autofocus></textarea>
+                            <div id="input-text"></div>
                             <div class="input-file" id="input-file">
                                 <div class="file-overlay" id="file-overlay"></div>
                                 <div style="position: relative; height: 100%;">

BIN
src/web/static/fonts/MaterialIcons-Regular.ttf


BIN
src/web/static/fonts/MaterialIcons-Regular.woff2


+ 93 - 8
src/web/stylesheets/layout/_io.css

@@ -6,7 +6,24 @@
  * @license Apache-2.0
  */
 
-#input-text,
+#input-text {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    background-color: transparent;
+}
+
+.cm-editor {
+    height: 100%;
+}
+
+.cm-editor .cm-content {
+    font-family: var(--fixed-width-font-family);
+    font-size: var(--fixed-width-font-size);
+    color: var(--fixed-width-font-colour);
+}
+
 #output-text,
 #output-html {
     position: relative;
@@ -163,14 +180,14 @@
 
 #input-wrapper,
 #output-wrapper,
-#input-wrapper > * ,
+#input-wrapper > :not(#input-text),
 #output-wrapper > .textarea-wrapper > div,
 #output-wrapper > .textarea-wrapper > textarea {
     height: calc(100% - var(--title-height));
 }
 
 #input-wrapper.show-tabs,
-#input-wrapper.show-tabs > *,
+#input-wrapper.show-tabs > :not(#input-text),
 #output-wrapper.show-tabs,
 #output-wrapper.show-tabs > .textarea-wrapper > div,
 #output-wrapper.show-tabs > .textarea-wrapper > textarea {
@@ -193,7 +210,9 @@
 }
 
 .textarea-wrapper textarea,
-.textarea-wrapper>div {
+.textarea-wrapper #output-text,
+.textarea-wrapper #output-html,
+.textarea-wrapper #output-highlighter {
     font-family: var(--fixed-width-font-family);
     font-size: var(--fixed-width-font-size);
     color: var(--fixed-width-font-colour);
@@ -292,10 +311,6 @@
     align-items: center;
 }
 
-#input-info {
-    line-height: 15px;
-}
-
 .dropping-file {
     border: 5px dashed var(--drop-file-border-colour) !important;
 }
@@ -458,3 +473,73 @@
     cursor: pointer;
     filter: brightness(98%);
 }
+
+
+/* Status bar */
+
+.cm-status-bar {
+    font-family: var(--fixed-width-font-family);
+    font-weight: normal;
+    font-size: 8pt;
+    margin: 0 5px;
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.cm-status-bar i {
+    font-size: 12pt;
+    vertical-align: middle;
+    margin-left: 8px;
+}
+.cm-status-bar>div>span:first-child i {
+    margin-left: 0;
+}
+
+/* Dropup Button */
+.cm-status-bar-select-btn {
+    border: none;
+    cursor: pointer;
+}
+
+/* The container <div> - needed to position the dropup content */
+.cm-status-bar-select {
+    position: relative;
+    display: inline-block;
+}
+
+/* Dropup content (Hidden by Default) */
+.cm-status-bar-select-content {
+    display: none;
+    position: absolute;
+    bottom: 20px;
+    right: 0;
+    background-color: #f1f1f1;
+    min-width: 200px;
+    box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.2);
+    z-index: 1;
+}
+
+/* Links inside the dropup */
+.cm-status-bar-select-content a {
+    color: black;
+    padding: 2px 5px;
+    text-decoration: none;
+    display: block;
+}
+
+/* Change color of dropup links on hover */
+.cm-status-bar-select-content a:hover {
+    background-color: #ddd
+}
+
+/* Show the dropup menu on hover */
+.cm-status-bar-select:hover .cm-status-bar-select-content {
+    display: block;
+}
+
+/* Change the background color of the dropup button when the dropup content is shown */
+.cm-status-bar-select:hover .cm-status-bar-select-btn {
+    background-color: #f1f1f1;
+}

+ 1 - 1
src/web/stylesheets/utils/_overrides.css

@@ -13,7 +13,7 @@
     font-family: 'Material Icons';
     font-style: normal;
     font-weight: 400;
-    src: url("../static/fonts/MaterialIcons-Regular.woff2") format('woff2');
+    src: url("../static/fonts/MaterialIcons-Regular.ttf") format('truetype');
 }
 
 .material-icons {

+ 1 - 1
src/web/waiters/ControlsWaiter.mjs

@@ -140,7 +140,7 @@ class ControlsWaiter {
 
         const params = [
             includeRecipe ? ["recipe", recipeStr] : undefined,
-            includeInput ? ["input", Utils.escapeHtml(input)] : undefined,
+            includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
         ];
 
         const hash = params

+ 6 - 9
src/web/waiters/HighlighterWaiter.mjs

@@ -155,12 +155,11 @@ class HighlighterWaiter {
         this.mouseTarget = INPUT;
         this.removeHighlights();
 
-        const el = e.target;
-        const start = el.selectionStart;
-        const end = el.selectionEnd;
+        const sel = document.getSelection();
+        const start = sel.baseOffset;
+        const end = sel.extentOffset;
 
         if (start !== 0 || end !== 0) {
-            document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
             this.highlightOutput([{start: start, end: end}]);
         }
     }
@@ -248,12 +247,11 @@ class HighlighterWaiter {
             this.mouseTarget !== INPUT)
             return;
 
-        const el = e.target;
-        const start = el.selectionStart;
-        const end = el.selectionEnd;
+        const sel = document.getSelection();
+        const start = sel.baseOffset;
+        const end = sel.extentOffset;
 
         if (start !== 0 || end !== 0) {
-            document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
             this.highlightOutput([{start: start, end: end}]);
         }
     }
@@ -328,7 +326,6 @@ class HighlighterWaiter {
     removeHighlights() {
         document.getElementById("input-highlighter").innerHTML = "";
         document.getElementById("output-highlighter").innerHTML = "";
-        document.getElementById("input-selection-info").innerHTML = "";
         document.getElementById("output-selection-info").innerHTML = "";
     }
 

+ 160 - 147
src/web/waiters/InputWaiter.mjs

@@ -7,9 +7,19 @@
 
 import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js";
 import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs";
-import Utils, { debounce } from "../../core/Utils.mjs";
-import { toBase64 } from "../../core/lib/Base64.mjs";
-import { isImage } from "../../core/lib/FileType.mjs";
+import Utils, {debounce} from "../../core/Utils.mjs";
+import {toBase64} from "../../core/lib/Base64.mjs";
+import {isImage} from "../../core/lib/FileType.mjs";
+
+import {
+    EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor
+} from "@codemirror/view";
+import {EditorState, Compartment} from "@codemirror/state";
+import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@codemirror/commands";
+import {bracketMatching} from "@codemirror/language";
+import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search";
+
+import {statusBar} from "../extensions/statusBar.mjs";
 
 
 /**
@@ -27,6 +37,9 @@ class InputWaiter {
         this.app = app;
         this.manager = manager;
 
+        this.inputTextEl = document.getElementById("input-text");
+        this.initEditor();
+
         // Define keys that don't change the input so we don't have to autobake when they are pressed
         this.badKeys = [
             16, // Shift
@@ -61,6 +74,135 @@ class InputWaiter {
         }
     }
 
+    /**
+     * Sets up the CodeMirror Editor and returns the view
+     */
+    initEditor() {
+        this.inputEditorConf = {
+            eol: new Compartment,
+            lineWrapping: new Compartment
+        };
+
+        const initialState = EditorState.create({
+            doc: null,
+            extensions: [
+                history(),
+                highlightSpecialChars({render: this.renderSpecialChar}),
+                drawSelection(),
+                rectangularSelection(),
+                crosshairCursor(),
+                bracketMatching(),
+                highlightSelectionMatches(),
+                search({top: true}),
+                statusBar(this.inputEditorConf),
+                this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping),
+                this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
+                EditorState.allowMultipleSelections.of(true),
+                keymap.of([
+                    // Explicitly insert a tab rather than indenting the line
+                    { key: "Tab", run: insertTab },
+                    // Explicitly insert a new line (using the current EOL char) rather
+                    // than messing around with indenting, which does not respect EOL chars
+                    { key: "Enter", run: insertNewline },
+                    ...historyKeymap,
+                    ...defaultKeymap,
+                    ...searchKeymap
+                ]),
+            ]
+        });
+
+        this.inputEditorView = new EditorView({
+            state: initialState,
+            parent: this.inputTextEl
+        });
+    }
+
+    /**
+     * Override for rendering special characters.
+     * Should mirror the toDOM function in
+     * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150
+     * But reverts the replacement of line feeds with newline control pictures.
+     * @param {number} code
+     * @param {string} desc
+     * @param {string} placeholder
+     * @returns {element}
+     */
+    renderSpecialChar(code, desc, placeholder) {
+        const s = document.createElement("span");
+        // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back.
+        s.textContent = code === 0x0a ? "\u240a" : placeholder;
+        s.title = desc;
+        s.setAttribute("aria-label", desc);
+        s.className = "cm-specialChar";
+        return s;
+    }
+
+    /**
+     * Handler for EOL Select clicks
+     * Sets the line separator
+     * @param {Event} e
+     */
+    eolSelectClick(e) {
+        e.preventDefault();
+
+        const eolLookup = {
+            "LF": "\u000a",
+            "VT": "\u000b",
+            "FF": "\u000c",
+            "CR": "\u000d",
+            "CRLF": "\u000d\u000a",
+            "NEL": "\u0085",
+            "LS": "\u2028",
+            "PS": "\u2029"
+        };
+        const eolval = eolLookup[e.target.getAttribute("data-val")];
+        const oldInputVal = this.getInput();
+
+        // Update the EOL value
+        this.inputEditorView.dispatch({
+            effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval))
+        });
+
+        // Reset the input so that lines are recalculated, preserving the old EOL values
+        this.setInput(oldInputVal);
+    }
+
+    /**
+     * Sets word wrap on the input editor
+     * @param {boolean} wrap
+     */
+    setWordWrap(wrap) {
+        this.inputEditorView.dispatch({
+            effects: this.inputEditorConf.lineWrapping.reconfigure(
+                wrap ? EditorView.lineWrapping : []
+            )
+        });
+    }
+
+    /**
+     * Gets the value of the current input
+     * @returns {string}
+     */
+    getInput() {
+        const doc = this.inputEditorView.state.doc;
+        const eol = this.inputEditorView.state.lineBreak;
+        return doc.sliceString(0, doc.length, eol);
+    }
+
+    /**
+     * Sets the value of the current input
+     * @param {string} data
+     */
+    setInput(data) {
+        this.inputEditorView.dispatch({
+            changes: {
+                from: 0,
+                to: this.inputEditorView.state.doc.length,
+                insert: data
+            }
+        });
+    }
+
     /**
      * Calculates the maximum number of tabs to display
      */
@@ -339,10 +481,8 @@ class InputWaiter {
             const activeTab = this.manager.tabs.getActiveInputTab();
             if (inputData.inputNum !== activeTab) return;
 
-            const inputText = document.getElementById("input-text");
-
             if (typeof inputData.input === "string") {
-                inputText.value = inputData.input;
+                this.setInput(inputData.input);
                 const fileOverlay = document.getElementById("input-file"),
                     fileName = document.getElementById("input-file-name"),
                     fileSize = document.getElementById("input-file-size"),
@@ -355,17 +495,11 @@ class InputWaiter {
                 fileType.textContent = "";
                 fileLoaded.textContent = "";
 
-                inputText.style.overflow = "auto";
-                inputText.classList.remove("blur");
-                inputText.scroll(0, 0);
-
-                const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ?
-                    inputData.input.count("\n") + 1 : null;
-                this.setInputInfo(inputData.input.length, lines);
+                this.inputTextEl.classList.remove("blur");
 
                 // Set URL to current input
                 const inputStr = toBase64(inputData.input, "A-Za-z0-9+/");
-                if (inputStr.length > 0 && inputStr.length <= 68267) {
+                if (inputStr.length >= 0 && inputStr.length <= 68267) {
                     this.setUrl({
                         includeInput: true,
                         input: inputStr
@@ -414,7 +548,6 @@ class InputWaiter {
             fileLoaded.textContent = inputData.progress + "%";
         }
 
-        this.setInputInfo(inputData.size, null);
         this.displayFilePreview(inputData);
 
         if (!silent) window.dispatchEvent(this.manager.statechange);
@@ -488,12 +621,10 @@ class InputWaiter {
      */
     displayFilePreview(inputData) {
         const activeTab = this.manager.tabs.getActiveInputTab(),
-            input = inputData.input,
-            inputText = document.getElementById("input-text");
+            input = inputData.input;
         if (inputData.inputNum !== activeTab) return;
-        inputText.style.overflow = "hidden";
-        inputText.classList.add("blur");
-        inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096)));
+        this.inputTextEl.classList.add("blur");
+        this.setInput(Utils.arrayBufferToStr(input.slice(0, 4096)));
 
         this.renderFileThumb();
 
@@ -576,7 +707,7 @@ class InputWaiter {
      */
     async getInputValue(inputNum) {
         return await new Promise(resolve => {
-            this.getInput(inputNum, false, r => {
+            this.getInputFromWorker(inputNum, false, r => {
                 resolve(r.data);
             });
         });
@@ -590,7 +721,7 @@ class InputWaiter {
      */
     async getInputObj(inputNum) {
         return await new Promise(resolve => {
-            this.getInput(inputNum, true, r => {
+            this.getInputFromWorker(inputNum, true, r => {
                 resolve(r.data);
             });
         });
@@ -604,7 +735,7 @@ class InputWaiter {
      * @param {Function} callback - The callback to execute when the input is returned
      * @returns {ArrayBuffer | string | object}
      */
-    getInput(inputNum, getObj, callback) {
+    getInputFromWorker(inputNum, getObj, callback) {
         const id = this.callbackID++;
 
         this.callbacks[id] = callback;
@@ -647,29 +778,6 @@ class InputWaiter {
         });
     }
 
-
-    /**
-     * Displays information about the input.
-     *
-     * @param {number} length - The length of the current input string
-     * @param {number} lines - The number of the lines in the current input string
-     */
-    setInputInfo(length, lines) {
-        let width = length.toString().length.toLocaleString();
-        width = width < 2 ? 2 : width;
-
-        const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
-        let msg = "length: " + lengthStr;
-
-        if (typeof lines === "number") {
-            const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
-            msg += "<br>lines: " + linesStr;
-        }
-
-        document.getElementById("input-info").innerHTML = msg;
-
-    }
-
     /**
      * Handler for input change events.
      * Debounces the input so we don't call autobake too often.
@@ -696,17 +804,13 @@ class InputWaiter {
         // Remove highlighting from input and output panes as the offsets might be different now
         this.manager.highlighter.removeHighlights();
 
-        const textArea = document.getElementById("input-text");
-        const value = (textArea.value !== undefined) ? textArea.value : "";
+        const value = this.getInput();
         const activeTab = this.manager.tabs.getActiveInputTab();
 
         this.app.progress = 0;
 
-        const lines = value.length < (this.app.options.ioDisplayThreshold * 1024) ?
-            (value.count("\n") + 1) : null;
-        this.setInputInfo(value.length, lines);
         this.updateInputValue(activeTab, value);
-        this.manager.tabs.updateInputTabHeader(activeTab, value.replace(/[\n\r]/g, "").slice(0, 100));
+        this.manager.tabs.updateInputTabHeader(activeTab, value.slice(0, 100).replace(/[\n\r]/g, ""));
 
         if (e && this.badKeys.indexOf(e.keyCode) < 0) {
             // Fire the statechange event as the input has been modified
@@ -714,62 +818,6 @@ class InputWaiter {
         }
     }
 
-    /**
-     * Handler for input paste events
-     * Checks that the size of the input is below the display limit, otherwise treats it as a file/blob
-     *
-     * @param {event} e
-     */
-    async inputPaste(e) {
-        e.preventDefault();
-        e.stopPropagation();
-
-        const self = this;
-        /**
-         * Triggers the input file/binary data overlay
-         *
-         * @param {string} pastedData
-         */
-        function triggerOverlay(pastedData) {
-            const file = new File([pastedData], "PastedData", {
-                type: "text/plain",
-                lastModified: Date.now()
-            });
-
-            self.loadUIFiles([file]);
-        }
-
-        const pastedData = e.clipboardData.getData("Text");
-        const inputText = document.getElementById("input-text");
-        const selStart = inputText.selectionStart;
-        const selEnd = inputText.selectionEnd;
-        const startVal = inputText.value.slice(0, selStart);
-        const endVal = inputText.value.slice(selEnd);
-        const val = startVal + pastedData + endVal;
-
-        if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) {
-            // Data too large to display, use overlay
-            triggerOverlay(val);
-            return false;
-        } else if (await this.preserveCarriageReturns(val)) {
-            // Data contains a carriage return and the user doesn't wish to edit it, use overlay
-            // We check this in a separate condition to make sure it is not run unless absolutely
-            // necessary.
-            triggerOverlay(val);
-            return false;
-        } else {
-            // Pasting normally fires the inputChange() event before
-            // changing the value, so instead change it here ourselves
-            // and manually fire inputChange()
-            inputText.value = val;
-            inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
-            // Don't debounce here otherwise the keyup event for the Ctrl key will cancel an autobake
-            // (at least for large inputs)
-            this.inputChange(e, true);
-        }
-    }
-
-
     /**
      * Handler for input dragover events.
      * Gives the user a visual cue to show that items can be dropped here.
@@ -818,7 +866,7 @@ class InputWaiter {
 
         if (text) {
             // Append the text to the current input and fire inputChange()
-            document.getElementById("input-text").value += text;
+            this.setInput(this.getInput() + text);
             this.inputChange(e);
             return;
         }
@@ -843,44 +891,6 @@ class InputWaiter {
         }
     }
 
-    /**
-     * Checks if an input contains carriage returns.
-     * If a CR is detected, checks if the preserve CR option has been set,
-     * and if not, asks the user for their preference.
-     *
-     * @param {string} input - The input to be checked
-     * @returns {boolean} - If true, the input contains a CR which should be
-     *      preserved, so display an overlay so it can't be edited
-     */
-    async preserveCarriageReturns(input) {
-        if (input.indexOf("\r") < 0) return false;
-
-        const optionsStr = "This behaviour can be changed in the <a href='#' onclick='document.getElementById(\"options\").click()'>Options pane</a>";
-        const preserveStr = `A carriage return (\\r, 0x0d) was detected in your input. To preserve it, editing has been disabled.<br>${optionsStr}`;
-        const dontPreserveStr = `A carriage return (\\r, 0x0d) was detected in your input. It has not been preserved.<br>${optionsStr}`;
-
-        switch (this.app.options.preserveCR) {
-            case "always":
-                this.app.alert(preserveStr, 6000);
-                return true;
-            case "never":
-                this.app.alert(dontPreserveStr, 6000);
-                return false;
-        }
-
-        // Only preserve for high-entropy inputs
-        const data = Utils.strToArrayBuffer(input);
-        const entropy = Utils.calculateShannonEntropy(data);
-
-        if (entropy > 6) {
-            this.app.alert(preserveStr, 6000);
-            return true;
-        }
-
-        this.app.alert(dontPreserveStr, 6000);
-        return false;
-    }
-
     /**
      * Load files from the UI into the inputWorker
      *
@@ -1080,6 +1090,9 @@ class InputWaiter {
         this.manager.worker.setupChefWorker();
         this.addInput(true);
         this.bakeAll();
+
+        // Fire the statechange event as the input has been modified
+        window.dispatchEvent(this.manager.statechange);
     }
 
     /**

+ 4 - 2
src/web/waiters/OptionsWaiter.mjs

@@ -53,6 +53,9 @@ class OptionsWaiter {
                 selects[i].selectedIndex = 0;
             }
         }
+
+        // Initialise options
+        this.setWordWrap();
     }
 
 
@@ -136,14 +139,13 @@ class OptionsWaiter {
      * Sets or unsets word wrap on the input and output depending on the wordWrap option value.
      */
     setWordWrap() {
-        document.getElementById("input-text").classList.remove("word-wrap");
+        this.manager.input.setWordWrap(this.app.options.wordWrap);
         document.getElementById("output-text").classList.remove("word-wrap");
         document.getElementById("output-html").classList.remove("word-wrap");
         document.getElementById("input-highlighter").classList.remove("word-wrap");
         document.getElementById("output-highlighter").classList.remove("word-wrap");
 
         if (!this.app.options.wordWrap) {
-            document.getElementById("input-text").classList.add("word-wrap");
             document.getElementById("output-text").classList.add("word-wrap");
             document.getElementById("output-html").classList.add("word-wrap");
             document.getElementById("input-highlighter").classList.add("word-wrap");

+ 1 - 4
src/web/waiters/OutputWaiter.mjs

@@ -1019,7 +1019,6 @@ class OutputWaiter {
         }
 
         document.getElementById("output-info").innerHTML = msg;
-        document.getElementById("input-selection-info").innerHTML = "";
         document.getElementById("output-selection-info").innerHTML = "";
     }
 
@@ -1292,9 +1291,7 @@ class OutputWaiter {
         if (this.outputs[activeTab].data.type === "string" &&
             active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
             const dishString = await this.getDishStr(this.getOutputDish(activeTab));
-            if (!await this.manager.input.preserveCarriageReturns(dishString)) {
-                active = dishString;
-            }
+            active = dishString;
         } else {
             transferable.push(active);
         }

+ 1 - 1
tests/browser/nightwatch.js

@@ -82,7 +82,7 @@ module.exports = {
         // Enter input
         browser
             .useCss()
-            .setValue("#input-text", "Don't Panic.")
+            .setValue("#input-text", "Don't Panic.") // TODO
             .pause(1000)
             .click("#bake");
 

+ 3 - 3
tests/browser/ops.js

@@ -409,16 +409,16 @@ function bakeOp(browser, opName, input, args=[]) {
         .click("#clr-recipe")
         .click("#clr-io")
         .waitForElementNotPresent("#rec-list li.operation")
-        .expect.element("#input-text").to.have.property("value").that.equals("");
+        .expect.element("#input-text").to.have.property("value").that.equals(""); // TODO
 
     browser
         .perform(function() {
             console.log(`Current test: ${opName}`);
         })
         .urlHash("recipe=" + recipeConfig)
-        .setValue("#input-text", input)
+        .setValue("#input-text", input) // TODO
         .waitForElementPresent("#rec-list li.operation")
-        .expect.element("#input-text").to.have.property("value").that.equals(input);
+        .expect.element("#input-text").to.have.property("value").that.equals(input); // TODO
 
     browser
         .waitForElementVisible("#stale-indicator", 5000)