diff --git a/package-lock.json b/package-lock.json index 327a04593..d57487eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,11 @@ "version": "1.11.5", "license": "MIT", "dependencies": { + "@codemirror/commands": "^6.8.1", + "@codemirror/language": "^6.11.3", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.1", "@deadlyjack/ajax": "^1.2.6", "@ungap/custom-elements": "^1.3.0", "@xterm/addon-attach": "^0.11.0", @@ -20,6 +25,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "autosize": "^6.0.1", + "codemirror": "^6.0.2", "cordova": "12.0.0", "core-js": "^3.45.0", "crypto-js": "^4.2.0", @@ -1867,6 +1873,87 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@deadlyjack/ajax": { "version": "1.2.6", "license": "MIT" @@ -2057,6 +2144,36 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@netflix/nerror": { "version": "1.1.3", "license": "MIT", @@ -4312,6 +4429,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "license": "MIT", @@ -5174,6 +5306,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -10128,6 +10266,12 @@ "webpack": "^5.27.0" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "license": "MIT", @@ -10644,6 +10788,12 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/walk-up-path": { "version": "3.0.1", "license": "ISC" diff --git a/package.json b/package.json index c697af747..a0f4e6280 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,30 @@ "webpack-cli": "^6.0.1" }, "dependencies": { + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-cpp": "^6.0.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-java": "^6.0.2", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.4", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sass": "^6.0.2", + "@codemirror/lang-vue": "^0.1.3", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.3", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.1", "@deadlyjack/ajax": "^1.2.6", + "@emmetio/codemirror6-plugin": "^0.4.0", "@ungap/custom-elements": "^1.3.0", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", @@ -105,6 +128,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "autosize": "^6.0.1", + "codemirror": "^6.0.2", "cordova": "12.0.0", "core-js": "^3.45.0", "crypto-js": "^4.2.0", diff --git a/src/codemirror/colorView.js b/src/codemirror/colorView.js new file mode 100644 index 000000000..3ea9ea06f --- /dev/null +++ b/src/codemirror/colorView.js @@ -0,0 +1,149 @@ +import { + Decoration, + EditorView, + ViewPlugin, + WidgetType, +} from "@codemirror/view"; +import pickColor from "dialogs/color"; +import color from "utils/color"; +import { colorRegex, HEX } from "utils/color/regex"; + +// WeakMap to carry state from widget DOM back into handler +const colorState = new WeakMap(); + +const HEX_RE = new RegExp(HEX, "gi"); + +const RGBG = new RegExp(colorRegex.anyGlobal); + +const enumColorType = { hex: "hex", rgb: "rgb", hsl: "hsl", named: "named" }; + +class ColorWidget extends WidgetType { + constructor({ color, colorRaw, ...state }) { + super(); + this.state = state; // from, to, colorType, alpha + this.color = color; // hex for input value + this.colorRaw = colorRaw; // original css color string + } + eq(other) { + return ( + other.state.colorType === this.state.colorType && + other.color === this.color && + other.state.from === this.state.from && + other.state.to === this.state.to && + (other.state.alpha || "") === (this.state.alpha || "") + ); + } + toDOM() { + const wrapper = document.createElement("span"); + wrapper.className = "cm-color-chip"; + wrapper.style.display = "inline-block"; + wrapper.style.width = "0.9em"; + wrapper.style.height = "0.9em"; + wrapper.style.borderRadius = "2px"; + wrapper.style.verticalAlign = "middle"; + wrapper.style.margin = "0 2px"; + wrapper.style.boxSizing = "border-box"; + wrapper.style.border = "1px solid rgba(0,0,0,0.2)"; + wrapper.style.backgroundColor = this.colorRaw; + wrapper.dataset["color"] = this.color; + wrapper.dataset["colorraw"] = this.colorRaw; + wrapper.style.cursor = "pointer"; + colorState.set(wrapper, this.state); + return wrapper; + } + ignoreEvent() { + return false; + } +} + +function colorDecorations(view) { + const deco = []; + const ranges = view.visibleRanges; + for (const { from, to } of ranges) { + const text = view.state.doc.sliceString(from, to); + // Any color using global matcher from utils (captures named/rgb/rgba/hsl/hsla/hex) + RGBG.lastIndex = 0; + for (let m; (m = RGBG.exec(text)); ) { + const raw = m[2]; + const start = from + m.index + m[1].length; + const end = start + raw.length; + const c = color(raw); + const colorHex = c.hex.toString(false); + deco.push( + Decoration.widget({ + widget: new ColorWidget({ + from: start, + to: end, + color: colorHex, + colorRaw: raw, + colorType: enumColorType.named, + }), + side: -1, + }).range(start), + ); + } + } + + return Decoration.set(deco, { sort: true }); +} + +export const colorView = (showPicker = true) => + ViewPlugin.fromClass( + class ColorViewPlugin { + constructor(view) { + this.decorations = colorDecorations(view); + } + update(update) { + if (update.docChanged || update.viewportChanged) { + this.decorations = colorDecorations(update.view); + } + const readOnly = update.view.contentDOM.ariaReadOnly === "true"; + const editable = update.view.contentDOM.contentEditable === "true"; + const canBeEdited = readOnly === false && editable; + this.changePicker(update.view, canBeEdited); + } + changePicker(view, canBeEdited) { + const doms = view.contentDOM.querySelectorAll("input[type=color]"); + doms.forEach((inp) => { + if (!showPicker) { + inp.setAttribute("disabled", ""); + } else { + canBeEdited + ? inp.removeAttribute("disabled") + : inp.setAttribute("disabled", ""); + } + }); + } + }, + { + decorations: (v) => v.decorations, + eventHandlers: { + click: async (e, view) => { + const target = e.target; + const chip = target?.closest?.(".cm-color-chip"); + if (!chip) return false; + // Respect read-only and setting toggle + const readOnly = view.contentDOM.ariaReadOnly === "true"; + const editable = view.contentDOM.contentEditable === "true"; + const canBeEdited = !readOnly && editable; + if (!canBeEdited) return true; + const data = colorState.get(chip); + if (!data) return false; + try { + const picked = await pickColor( + chip.dataset.colorraw || chip.dataset.color, + ); + if (!picked) return true; + view.dispatch({ + changes: { from: data.from, to: data.to, insert: picked }, + }); + } catch { + /* ignore */ + } + return true; + }, + }, + }, + ); + +export default colorView; diff --git a/src/codemirror/modelist.js b/src/codemirror/modelist.js new file mode 100644 index 000000000..a245bae82 --- /dev/null +++ b/src/codemirror/modelist.js @@ -0,0 +1,165 @@ +const modesByName = {}; +const modes = []; + +/** + * Initialize CodeMirror mode list functionality + */ +export function initModes() { + // CodeMirror modes don't need the same ace.define wrapper + // but we maintain the same API structure for compatibility +} + +/** + * Add language mode to CodeMirror editor + * @param {string} name name of the mode + * @param {string|Array} extensions extensions of the mode + * @param {string} [caption] display name of the mode + * @param {Function} [languageExtension] CodeMirror language extension function + */ +export function addMode(name, extensions, caption, languageExtension = null) { + const filename = name.toLowerCase(); + const mode = new Mode(filename, caption, extensions, languageExtension); + modesByName[filename] = mode; + modes.push(mode); +} + +/** + * Remove language mode from CodeMirror editor + * @param {string} name + */ +export function removeMode(name) { + const filename = name.toLowerCase(); + delete modesByName[filename]; + const modeIndex = modes.findIndex((mode) => mode.name === filename); + if (modeIndex >= 0) { + modes.splice(modeIndex, 1); + } +} + +/** + * Get mode for file path + * @param {string} path + * @returns {Mode} + */ +export function getModeForPath(path) { + let mode = modesByName.text; + let fileName = path.split(/[\/\\]/).pop(); + + // Sort modes by specificity (descending) to check most specific first + const sortedModes = [...modes].sort((a, b) => { + return getModeSpecificityScore(b) - getModeSpecificityScore(a); + }); + + for (const iMode of sortedModes) { + if (iMode.supportsFile?.(fileName)) { + mode = iMode; + break; + } + } + return mode; +} + +/** + * Calculates a specificity score for a mode. + * Higher score means more specific. + * - Anchored patterns (e.g., "^Dockerfile") get a base score of 1000. + * - Non-anchored patterns (extensions) are scored by length. + */ +function getModeSpecificityScore(modeInstance) { + const extensionsStr = modeInstance.extensions; + if (!extensionsStr) return 0; + + const patterns = extensionsStr.split("|"); + let maxScore = 0; + + for (const pattern of patterns) { + let currentScore = 0; + if (pattern.startsWith("^")) { + // Exact filename match or anchored pattern + currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^' + } else { + // Extension match + currentScore = pattern.length; + } + if (currentScore > maxScore) { + maxScore = currentScore; + } + } + return maxScore; +} + +/** + * Get all modes by name + * @returns {Object} + */ +export function getModesByName() { + return modesByName; +} + +/** + * Get all modes array + * @returns {Array} + */ +export function getModes() { + return modes; +} + +class Mode { + extensions; + displayName; + name; + mode; + extRe; + languageExtension; + + /** + * Create a new mode + * @param {string} name + * @param {string} caption + * @param {string|Array} extensions + * @param {Function} languageExtension - CodeMirror language extension function + */ + constructor(name, caption, extensions, languageExtension = null) { + if (Array.isArray(extensions)) { + extensions = extensions.join("|"); + } + + this.name = name; + this.mode = name; // CodeMirror uses different mode naming + this.extensions = extensions; + this.caption = caption || this.name.replace(/_/g, " "); + this.languageExtension = languageExtension; + let re; + + if (/\^/.test(extensions)) { + re = + extensions.replace(/\|(\^)?/g, function (a, b) { + return "$|" + (b ? "^" : "^.*\\."); + }) + "$"; + } else { + re = "^.*\\.(" + extensions + ")$"; + } + + this.extRe = new RegExp(re, "i"); + } + + supportsFile(filename) { + return this.extRe.test(filename); + } + + /** + * Get the CodeMirror language extension + * @returns {Function|null} The language extension function or null if not available + */ + getExtension() { + return this.languageExtension; + } + + /** + * Check if the language extension is available (loaded) + * @returns {boolean} + */ + isAvailable() { + return this.languageExtension !== null; + } +} diff --git a/src/codemirror/supportedModes.js b/src/codemirror/supportedModes.js new file mode 100644 index 000000000..4e2d073fe --- /dev/null +++ b/src/codemirror/supportedModes.js @@ -0,0 +1,58 @@ +import { cpp } from "@codemirror/lang-cpp"; + +// Import CodeMirror language extensions that are bundled with the app +import { css } from "@codemirror/lang-css"; +import { go } from "@codemirror/lang-go"; +import { html } from "@codemirror/lang-html"; +import { java } from "@codemirror/lang-java"; +import { javascript } from "@codemirror/lang-javascript"; +import { json } from "@codemirror/lang-json"; +import { markdown } from "@codemirror/lang-markdown"; +import { php } from "@codemirror/lang-php"; +import { python } from "@codemirror/lang-python"; +import { rust } from "@codemirror/lang-rust"; +import { sass } from "@codemirror/lang-sass"; +import { vue } from "@codemirror/lang-vue"; +import { xml } from "@codemirror/lang-xml"; +import { yaml } from "@codemirror/lang-yaml"; +import { addMode } from "./modelist"; + +const modeList = { + // Plain text (fallback/selectable) + Text: { extensions: "txt|text|log|plain", extension: null }, + CSS: { extensions: "css", extension: css }, + Cpp: { extensions: "cpp|c|cc|cxx|h|hh|hpp|ino", extension: cpp }, + golang: { extensions: "go", extension: go }, + HTML: { extensions: "html|htm|xhtml|we|wpy", extension: html }, + Java: { extensions: "java", extension: java }, + JavaScript: { extensions: "js|jsm|jsx|cjs|mjs", extension: javascript }, + JSON: { extensions: "json", extension: json }, + Markdown: { extensions: "md|markdown", extension: markdown }, + PHP: { + extensions: "php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module", + extension: php, + }, + Python: { extensions: "py", extension: python }, + Rust: { extensions: "rs", extension: rust }, + Sass: { extensions: "sass|scss", extension: sass }, + Vue: { extensions: "vue", extension: vue }, + XML: { + extensions: "xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml", + extension: xml, + }, + YAML: { extensions: "yaml|yml", extension: yaml }, +}; + +const languageNames = { + golang: "Go", + JavaScript: "JavaScript/JSX", + Cpp: "C/C++", + Text: "Plain Text", +}; + +Object.keys(modeList).forEach((key) => { + const { extensions, extension } = modeList[key]; + const caption = languageNames[key] || key; + // Pass null extension for Text; modelist will still register it + addMode(key, extensions, caption, extension || null); +}); diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index a331697d4..0b606960f 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -259,7 +259,8 @@ function create($container, $toggler) { root.style.removeProperty("margin-left"); root.style.removeProperty("width"); $el.remove(); - editorManager.editor.resize(true); + // TODO : Codemirror + //editorManager.editor.resize(true); } } diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js index 2e199cba2..9fdc2b420 100644 --- a/src/handlers/editorFileTab.js +++ b/src/handlers/editorFileTab.js @@ -208,14 +208,15 @@ function releaseDrag(e) { } else if ( $target.tagName === "INPUT" || $target.tagName === "TEXTAREA" || - $target.classList.contains("ace_text-input") || - $target.closest(".ace_editor") + $target.isContentEditable || + $target.closest(".cm-editor") ) { - // If released on an input area or ace editor + // If released on an input area or CodeMirror editor const filePath = editorManager.activeFile.uri; if (filePath) { - if ($target.closest(".ace_editor")) { - editorManager.editor.insert(filePath); + if ($target.closest(".cm-editor")) { + const view = editorManager.editor; + view.dispatch(view.state.replaceSelection(filePath)); } else { $target.value += filePath; } diff --git a/src/handlers/quickTools.js b/src/handlers/quickTools.js index 8b86cc887..cca7870a8 100644 --- a/src/handlers/quickTools.js +++ b/src/handlers/quickTools.js @@ -40,7 +40,7 @@ quickTools.$input.addEventListener("input", (e) => { } const event = KeyboardEvent("keydown", keyCombination); - input = input || editorManager.editor.textInput.getElement(); + input = input || editorManager.editor.contentDOM; resetKeys(); input.dispatchEvent(event); @@ -62,8 +62,8 @@ quickTools.$input.addEventListener("keydown", (e) => { if (input && input !== quickTools.$input) { input.dispatchEvent(event); } else { - // Otherwise fallback to editor input - editorManager.editor.textInput.getElement().dispatchEvent(event); + // Otherwise fallback to editor view content + editorManager.editor.contentDOM.dispatchEvent(event); } }); @@ -334,7 +334,8 @@ function toggleSearch() { } $searchInput.focus(); - editor.resize(true); + // TODO : Codemirror + //editor.resize(true); } function toggle() { @@ -367,7 +368,8 @@ function setHeight(height = 1, save = true) { if (save) { appSettings.update({ quickTools: height }, false); } - editor.resize(true); + // TODO : Codemirror + // editor.resize(true); if (!height) { $row1.remove(); diff --git a/src/handlers/quickToolsInit.js b/src/handlers/quickToolsInit.js index b07b7232c..d48418944 100644 --- a/src/handlers/quickToolsInit.js +++ b/src/handlers/quickToolsInit.js @@ -81,11 +81,12 @@ export default function init() { $footer.removeAttribute("data-unsaved"); }); - editorManager.editor.on("focus", () => { - if (key.shift || key.ctrl || key.alt || key.meta) { - quickTools.$input.focus(); - } - }); + // TODO Codemirro + // editorManager.editor.on("focus", () => { + // if (key.shift || key.ctrl || key.alt || key.meta) { + // quickTools.$input.focus(); + // } + // }); root.append($footer, $toggler); document.body.append($input); diff --git a/src/lib/acode.js b/src/lib/acode.js index ea024037e..ab6942e0d 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -1,7 +1,6 @@ import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import ajax from "@deadlyjack/ajax"; -import { addMode, removeMode } from "ace/modelist"; import Contextmenu from "components/contextmenu"; import inputhints from "components/inputhints"; import Page from "components/page"; @@ -43,6 +42,7 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; +import { addMode, removeMode } from "../codemirror/modelist"; import constants from "./constants"; export default class Acode { diff --git a/src/lib/commands.js b/src/lib/commands.js index 8f70ba391..f1b5601d5 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -137,10 +137,15 @@ export default { }); if (!res) return; - - const [line, col] = `${res}`.split("."); - const editor = editorManager.editor; - + const [lineStr, colStr] = String(res).split("."); + const lineNum = Math.max(1, Number.parseInt(lineStr || "1", 10) || 1); + const colNum = Math.max(1, Number.parseInt(colStr || "1", 10) || 1); + const { editor } = editorManager; + const { doc } = editor.state; + const line = doc.line(Math.min(lineNum, doc.lines)); + const col = Math.min(colNum - 1, Math.max(0, line.length)); + const pos = line.from + col; + editor.dispatch({ selection: { anchor: pos }, scrollIntoView: true }); editor.focus(); editor.gotoLine(line, col, true); }, @@ -191,19 +196,19 @@ export default { default: return; } - editorManager.editor.blur(); + editorManager.editor.contentDOM.blur(); }, "open-with"() { editorManager.activeFile.openWith(); }, "open-file"() { - editorManager.editor.blur(); + editorManager.editor.contentDOM.blur(); FileBrowser("file") .then(FileBrowser.openFile) .catch(FileBrowser.openFileError); }, "open-folder"() { - editorManager.editor.blur(); + editorManager.editor.contentDOM.blur(); FileBrowser("folder") .then(FileBrowser.openFolder) .catch(FileBrowser.openFolderError); @@ -241,7 +246,8 @@ export default { this.find(); }, "resize-editor"() { - editorManager.editor.resize(true); + // TODO : Codemirror + //editorManager.editor.resize(true); }, "open-inapp-browser"(url) { browser.open(url); @@ -307,7 +313,7 @@ export default { const range = getColorRange(); let defaultColor = range ? editor.session.getTextRange(range) : ""; - editor.blur(); + editor.contentDOM.blur(); const wasFocused = editorManager.activeFile.focused; const res = await color(defaultColor, () => { if (wasFocused) { diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index fd5c385a1..96fd085a5 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -1,4 +1,6 @@ import fsOperation from "fileSystem"; +// CodeMirror imports for document state management +import { EditorState, Text } from "@codemirror/state"; import Sidebar from "components/sidebar"; import tile from "components/tile"; import confirm from "dialogs/confirm"; @@ -9,15 +11,13 @@ import mimeTypes from "mime-types"; import helpers from "utils/helpers"; import Path from "utils/Path"; import Url from "utils/Url"; +import { getModeForPath } from "../codemirror/modelist"; import constants from "./constants"; import openFolder from "./openFolder"; import run from "./run"; import saveFile from "./saveFile"; import appSettings from "./settings"; -const { Fold } = ace.require("ace/edit_session/fold"); -const { Range } = ace.require("ace/range"); - /** * @typedef {'run'|'save'|'change'|'focus'|'blur'|'close'|'rename'|'load'|'loadError'|'loadStart'|'loadEnd'|'changeMode'|'changeEncoding'|'changeReadOnly'} FileEvents */ @@ -92,8 +92,8 @@ export default class EditorFile { */ deletedFile = false; /** - * EditSession of the file - * @type {AceAjax.IEditSession} + * CodeMirror document state for the file + * @type {EditorState} */ session = null; /** @@ -339,7 +339,9 @@ export default class EditorFile { editorManager.emit("new-file", this); if (this.#type === "editor") { - this.session = ace.createEditSession(options?.text || ""); + this.session = EditorState.create({ + doc: options?.text || "", + }); this.setMode(); this.#setupSession(); } @@ -489,7 +491,7 @@ export default class EditorFile { * End of line */ get eol() { - return /\r/.test(this.session.getValue()) ? "windows" : "unix"; + return /\r/.test(this.session.doc.toString()) ? "windows" : "unix"; } /** @@ -499,7 +501,7 @@ export default class EditorFile { set eol(value) { if (this.type !== "editor") return; if (this.eol === value) return; - let text = this.session.getValue(); + let text = this.session.doc.toString(); if (value === "windows") { text = text.replace(/(? { this.#emit("load", createFileEvent(this)); emit("file-loaded", this); - if (cursorPos) - this.session.selection.moveCursorTo(cursorPos.row, cursorPos.column); - if (scrollTop) this.session.setScrollTop(scrollTop); - if (scrollLeft) this.session.setScrollLeft(scrollLeft); + // TODO: Implement cursor positioning and scrolling for CodeMirror + // if (cursorPos) + // this.session.selection.moveCursorTo(cursorPos.row, cursorPos.column); + // if (scrollTop) this.session.setScrollTop(scrollTop); + // if (scrollLeft) this.session.setScrollLeft(scrollLeft); if (editable !== undefined) this.editable = editable; - if (Array.isArray(folds)) { - const parsedFolds = EditorFile.#parseFolds(folds); - this.session?.addFolds(parsedFolds); - } + // TODO: Implement folding for CodeMirror + // if (Array.isArray(folds)) { + // const parsedFolds = EditorFile.#parseFolds(folds); + // this.session?.addFolds(parsedFolds); + // } }, 0); } catch (error) { this.#emit("loaderror", createFileEvent(this)); @@ -1076,54 +1093,27 @@ export default class EditorFile { } } - static #onfold(e) { - editorManager.editor._emit("fold", e); - } + // TODO: Implement CodeMirror equivalents for folding and scroll events + // static #onfold(e) { + // editorManager.editor._emit("fold", e); + // } - static #onscrolltop(e) { - editorManager.editor._emit("scrolltop", e); - } + // static #onscrolltop(e) { + // editorManager.editor._emit("scrolltop", e); + // } - static #onscrollleft(e) { - editorManager.editor._emit("scrollleft", e); - } + // static #onscrollleft(e) { + // editorManager.editor._emit("scrollleft", e); + // } /** - * Parse folds - * @param {Array} folds + * Parse folds - TODO: Update for CodeMirror folding system + * @param {Array} folds */ static #parseFolds(folds) { if (!Array.isArray(folds)) return []; - - const foldDataAr = []; - - folds.forEach((fold) => { - if (!fold || !fold.range) return; - - const { range } = fold; - const { start, end } = range; - - if (!start || !end) return; - - try { - const foldData = new Fold( - new Range(start.row, start.column, end.row, end.column), - fold.placeholder, - ); - - if (Array.isArray(fold.ranges) && fold.ranges.length > 0) { - const subFolds = EditorFile.#parseFolds(fold.ranges); - foldData.subFolds = subFolds; - foldData.ranges = subFolds; - } - - foldDataAr.push(foldData); - } catch (error) { - console.warn("Error parsing fold:", error); - } - }); - - return foldDataAr; + // TODO: Implement CodeMirror fold parsing + return []; } #save(as) { @@ -1150,35 +1140,26 @@ export default class EditorFile { } /** - * Setup Ace EditSession for the file + * Setup CodeMirror EditorState for the file */ #setupSession() { if (this.type !== "editor") return; - const { value: settings } = appSettings; - - this.session.setTabSize(settings.tabSize); - this.session.setUseSoftTabs(settings.softTab); - this.session.setUseWrapMode(settings.textWrap); - this.session.setUseWorker(false); - - this.session.on("changeScrollTop", EditorFile.#onscrolltop); - this.session.on("changeScrollLeft", EditorFile.#onscrollleft); - this.session.on("changeFold", EditorFile.#onfold); - this.session.on("changeAnnotation", () => { - editorManager.editor._emit("changeAnnotation", this); - }); + // CodeMirror configuration will be handled in the EditorView + // Store settings for when the editor view is created + this.editorSettings = { + tabSize: appSettings.value.tabSize, + softTab: appSettings.value.softTab, + textWrap: appSettings.value.textWrap, + }; } #destroy() { this.#emit("close", createFileEvent(this)); appSettings.off("update:openFileListPos", this.#onFilePosChange); if (this.type === "editor") { - this.session?.off("changeScrollTop", EditorFile.#onscrolltop); - this.session?.off("changeScrollLeft", EditorFile.#onscrollleft); - this.session?.off("changeFold", EditorFile.#onfold); this.#removeCache(); - this.session?.destroy(); - delete this.session; + // CodeMirror EditorState doesn't need explicit cleanup + this.session = null; } else if (this.content) { this.content.remove(); } diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 83151c2bd..27b90adff 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -1,13 +1,42 @@ import sidebarApps from "sidebarApps"; -import initColorView, { deactivateColorView } from "ace/colorView"; -import { setCommands, setKeyBindings } from "ace/commands"; -import touchListeners, { scrollAnimationFrame } from "ace/touchHandler"; + +// TODO: Migrate commands and key bindings to CodeMirror +// import { setCommands, setKeyBindings } from "ace/commands"; +// TODO: Migrate touch handlers to CodeMirror +// import touchListeners, { scrollAnimationFrame } from "ace/touchHandler"; + +import { EditorState } from "@codemirror/state"; +import { oneDark } from "@codemirror/theme-one-dark"; +import { keymap } from "@codemirror/view"; +import { + abbreviationTracker, + emmetConfig, + expandAbbreviation, + wrapWithAbbreviation, +} from "@emmetio/codemirror6-plugin"; +// CodeMirror imports +import { basicSetup, EditorView } from "codemirror"; +// TODO: Add search keymap when implementing search functionality +// import { searchKeymap } from "@codemirror/search"; +// TODO: Add keymaps when implementing command system +// import { defaultKeymap, historyKeymap } from "@codemirror/commands"; +// CodeMirror mode management +import { + getModeForPath, + getModes, + getModesByName, + initModes, +} from "../codemirror/modelist"; +import "../codemirror/supportedModes"; +import { autocompletion } from "@codemirror/autocomplete"; import list from "components/collapsableList"; import quickTools from "components/quickTools"; import ScrollBar from "components/scrollbar"; import SideButton, { sideButtonContainer } from "components/sideButton"; import keyboardHandler, { keydownState } from "handlers/keyboard"; import actions from "handlers/quickTools"; +import colorView from "../codemirror/colorView"; +// TODO: Update EditorFile for CodeMirror compatibility import EditorFile from "./editorFile"; import appSettings from "./settings"; import { @@ -55,6 +84,11 @@ async function EditorManager($header, $body) { }, }; const $container =
; + // Ensure the container participates well in flex layouts and can constrain the editor + $container.style.flex = "1 1 auto"; + $container.style.minHeight = "0"; // allow child scroller to size correctly + $container.style.height = "100%"; + $container.style.width = "100%"; const problemButton = SideButton({ text: strings.problems, icon: "warningreport_problem", @@ -64,7 +98,110 @@ async function EditorManager($header, $body) { acode.exec("open", "problems"); }, }); - const editor = ace.edit($container); + + // Make CodeMirror fill the container height and manage scrolling internally + const fixedHeightTheme = EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { height: "100%", overflow: "auto" }, + }); + + // Create minimal CodeMirror editor + const editorState = EditorState.create({ + doc: "", + extensions: [ + basicSetup, + // Default theme + oneDark, + fixedHeightTheme, + // Emmet abbreviation tracker and common keybindings + abbreviationTracker(), + wrapWithAbbreviation(), + autocompletion(), + keymap.of([{ key: "Mod-e", run: expandAbbreviation }]), + ], + }); + + const editor = new EditorView({ + state: editorState, + parent: $container, + }); + + // Provide minimal Ace-like API compatibility used across the app + /** + * Insert text at the current selection/cursor in the editor + * @param {string} text + * @returns {boolean} success + */ + editor.insert = function (text) { + try { + const { from, to } = editor.state.selection.main; + editor.dispatch({ changes: { from, to, insert: String(text ?? "") } }); + return true; + } catch (_) { + return false; + } + }; + + // Helper: apply a file's content and language to the editor view + function applyFileToEditor(file) { + if (!file || file.type !== "editor") return; + const baseExtensions = [basicSetup, oneDark, fixedHeightTheme]; + const exts = [...baseExtensions]; + try { + const langExtFn = file.currentLanguageExtension; + if (typeof langExtFn === "function") { + exts.push(langExtFn()); + } + } catch (e) { + // ignore language extension errors; fallback to plain text + } + + // Emmet config: set syntax based on file/mode + const syntax = getEmmetSyntaxForFile(file); + exts.push(abbreviationTracker()); + exts.push(wrapWithAbbreviation()); + exts.push(keymap.of([{ key: "Mod-e", run: expandAbbreviation }])); + exts.push(emmetConfig.of({ syntax })); + + // Color preview plugin when enabled + if (appSettings.value.colorPreview) { + exts.push(colorView(true)); + } + + // Apply read-only state based on file.editable/loading + try { + exts.push(EditorState.readOnly.of(!file.editable || !!file.loading)); + } catch (e) { + // safe to ignore; editor will remain editable by default + } + + const doc = file.session ? file.session.doc.toString() : ""; + const state = EditorState.create({ doc, extensions: exts }); + file.session = state; // keep file.session in sync + editor.setState(state); + } + + function getEmmetSyntaxForFile(file) { + const mode = (file?.currentMode || "").toLowerCase(); + const name = (file?.filename || "").toLowerCase(); + const ext = name.includes(".") ? name.split(".").pop() : ""; + if (ext === "xml" || mode.includes("xml")) return "xml"; + if (ext === "jsx" || ext === "tsx") return "jsx"; + if (mode.includes("javascript") && (ext === "jsx" || ext === "tsx")) + return "jsx"; + if (ext === "css" || mode.includes("css")) return "css"; + if (ext === "scss" || mode.includes("scss")) return "scss"; + if (ext === "sass" || mode.includes("sass")) return "sass"; + if (ext === "styl" || ext === "stylus" || mode.includes("styl")) + return "stylus"; + if (ext === "php" || mode.includes("php")) return "html"; // treat PHP as HTML for Emmet + if (ext === "vue" || mode.includes("vue")) return "html"; // Emmet inside templates + if (ext === "html" || ext === "xhtml" || mode.includes("html")) + return "html"; + // Defaults to html per Emmet docs + return "html"; + } + const $vScrollbar = ScrollBar({ width: scrollbarSize, onscroll: onscrollV, @@ -131,9 +268,10 @@ async function EditorManager($header, $body) { }, }; - // set mode text - editor.setSession(ace.createEditSession("", "ace/mode/text")); + // TODO: Implement mode/language support for CodeMirror + // editor.setSession(ace.createEditSession("", "ace/mode/text")); $body.append($container); + initModes(); // Initialize CodeMirror modes await setupEditor(); $hScrollbar.onshow = $vScrollbar.onshow = updateFloatingButton.bind( @@ -152,19 +290,23 @@ async function EditorManager($header, $body) { }); appSettings.on("update:tabSize", function (value) { - manager.files.forEach((file) => file.session.setTabSize(value)); + // TODO: Implement tab size setting for CodeMirror sessions + // manager.files.forEach((file) => file.session.setTabSize(value)); }); appSettings.on("update:softTab", function (value) { - manager.files.forEach((file) => file.session.setUseSoftTabs(value)); + // TODO: Implement soft tabs setting for CodeMirror sessions + // manager.files.forEach((file) => file.session.setUseSoftTabs(value)); }); + // TODO: Implement show invisibles for CodeMirror appSettings.on("update:showSpaces", function (value) { - editor.setOption("showInvisibles", value); + // editor.setOption("showInvisibles", value); }); + // TODO: Implement font size setting for CodeMirror appSettings.on("update:fontSize", function (value) { - editor.setFontSize(value); + // editor.setFontSize(value); }); appSettings.on("update:openFileListPos", function (value) { @@ -172,8 +314,9 @@ async function EditorManager($header, $body) { $vScrollbar.resize(); }); + // TODO: Implement print margin for CodeMirror appSettings.on("update:showPrintMargin", function (value) { - editorManager.editor.setOption("showPrintMargin", value); + // manager.editor.setOption("showPrintMargin", value); }); appSettings.on("update:scrollbarSize", function (value) { @@ -181,21 +324,24 @@ async function EditorManager($header, $body) { $hScrollbar.size = value; }); + // TODO: Implement live autocompletion for CodeMirror appSettings.on("update:liveAutoCompletion", function (value) { - editor.setOption("enableLiveAutocompletion", value); + // editor.setOption("enableLiveAutocompletion", value); }); appSettings.on("update:linenumbers", function (value) { updateMargin(true); - editor.resize(true); + //editor.resize(true); }); + // TODO: Implement line height setting for CodeMirror appSettings.on("update:lineHeight", function (value) { - editor.container.style.lineHeight = value; + // editor.container.style.lineHeight = value; }); + // TODO: Implement relative line numbers for CodeMirror appSettings.on("update:relativeLineNumbers", function (value) { - editor.setOption("relativeLineNumbers", value); + // editor.setOption("relativeLineNumbers", value); }); appSettings.on("update:elasticTabstops", function (value) { @@ -214,12 +360,10 @@ async function EditorManager($header, $body) { editor.setOption("printMarginColumn", value); }); - appSettings.on("update:colorPreview", function (value) { - if (value) { - return initColorView(editor, true); - } - - deactivateColorView(); + // TODO: Implement color preview for CodeMirror + appSettings.on("update:colorPreview", function () { + const file = manager.activeFile; + if (file?.type === "editor") applyFileToEditor(file); }); appSettings.on("update:showSideButtons", function () { @@ -231,8 +375,9 @@ async function EditorManager($header, $body) { updateMargin(true); }); + // TODO: Implement fold widgets for CodeMirror appSettings.on("update:fadeFoldWidgets", function (value) { - editor.setOption("fadeFoldWidgets", value); + // editor.setOption("fadeFoldWidgets", value); }); return manager; @@ -253,8 +398,8 @@ async function EditorManager($header, $body) { * @returns {Promise} A promise that resolves once the editor is set up. */ async function setupEditor() { - const Emmet = ace.require("ace/ext/emmet"); - const textInput = editor.textInput.getElement(); + // TODO: Get input element from CodeMirror + // const textInput = editor.textInput.getElement(); const settings = appSettings.value; const { leftMargin, textWrap, colorPreview, fontSize, lineHeight } = appSettings.value; @@ -267,184 +412,196 @@ async function EditorManager($header, $body) { let autosaveTimeout; let scrollTimeout; - editor.on("focus", async () => { + // TODO: Implement focus event for CodeMirror + // editor.on("focus", async () => { + // const { activeFile } = manager; + // activeFile.focused = true; + // keyboardHandler.on("keyboardShow", scrollCursorIntoView); + // if (isScrolling) return; + // $hScrollbar.hide(); + // $vScrollbar.hide(); + // }); + + // TODO: Implement blur event for CodeMirror + // editor.on("blur", async () => { + // const { hardKeyboardHidden, keyboardHeight } = + // await getSystemConfiguration(); + // const blur = () => { + // const { activeFile } = manager; + // activeFile.focused = false; + // activeFile.focusedBefore = false; + // }; + // if ( + // hardKeyboardHidden === HARDKEYBOARDHIDDEN_NO && + // keyboardHeight < 100 + // ) { + // // external keyboard + // blur(); + // return; + // } + // const onKeyboardHide = () => { + // keyboardHandler.off("keyboardHide", onKeyboardHide); + // blur(); + // }; + // keyboardHandler.on("keyboardHide", onKeyboardHide); + // }); + + // TODO: Implement change event for CodeMirror + // editor.on("change", (e) => { + if (checkTimeout) clearTimeout(checkTimeout); + if (autosaveTimeout) clearTimeout(autosaveTimeout); + + checkTimeout = setTimeout(async () => { const { activeFile } = manager; - activeFile.focused = true; - keyboardHandler.on("keyboardShow", scrollCursorIntoView); - - if (isScrolling) return; - - $hScrollbar.hide(); - $vScrollbar.hide(); - }); - - editor.on("blur", async () => { - const { hardKeyboardHidden, keyboardHeight } = - await getSystemConfiguration(); - const blur = () => { - const { activeFile } = manager; - activeFile.focused = false; - activeFile.focusedBefore = false; - }; - - if ( - hardKeyboardHidden === HARDKEYBOARDHIDDEN_NO && - keyboardHeight < 100 - ) { - // external keyboard - blur(); - return; - } - const onKeyboardHide = () => { - keyboardHandler.off("keyboardHide", onKeyboardHide); - blur(); - }; - - keyboardHandler.on("keyboardHide", onKeyboardHide); - }); - - editor.on("change", (e) => { - if (checkTimeout) clearTimeout(checkTimeout); - if (autosaveTimeout) clearTimeout(autosaveTimeout); - - checkTimeout = setTimeout(async () => { - const { activeFile } = manager; - - if (activeFile.markChanged) { - const changed = await activeFile.isChanged(); - activeFile.isUnsaved = changed; - activeFile.writeToCache(); - events.emit("file-content-changed", activeFile); - manager.onupdate("file-changed"); - manager.emit("update", "file-changed"); - - const { autosave } = appSettings.value; - if (activeFile.uri && changed && autosave) { - autosaveTimeout = setTimeout(() => { - acode.exec("save", false); - }, autosave); - } + if (activeFile.markChanged) { + const changed = await activeFile.isChanged(); + activeFile.isUnsaved = changed; + activeFile.writeToCache(); + events.emit("file-content-changed", activeFile); + manager.onupdate("file-changed"); + manager.emit("update", "file-changed"); + + const { autosave } = appSettings.value; + if (activeFile.uri && changed && autosave) { + autosaveTimeout = setTimeout(() => { + acode.exec("save", false); + }, autosave); } - activeFile.markChanged = true; - }, TIMEOUT_VALUE); - }); - - editor.on("changeAnnotation", toggleProblemButton); - - editor.on("scroll", () => { - clearTimeout(scrollTimeout); - isScrolling = true; - scrollTimeout = setTimeout(() => { - isScrolling = false; - }, 100); - }); - - editor.renderer.on("resize", () => { - $vScrollbar.resize($vScrollbar.visible); - $hScrollbar.resize($hScrollbar.visible); - }); - - editor.on("scrolltop", onscrolltop); - editor.on("scrollleft", onscrollleft); - textInput.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - keydownState.esc = { value: true, target: textInput }; } - }); - - if (colorPreview) { - initColorView(editor); - } - - touchListeners(editor); - setCommands(editor); - await setKeyBindings(editor); - Emmet.setCore(window.emmet); - editor.setFontSize(fontSize); - editor.setHighlightSelectedWord(true); - editor.container.style.lineHeight = lineHeight; - - ace.require("ace/ext/language_tools"); - editor.setOption("animatedScroll", false); - editor.setOption("tooltipFollowsMouse", false); - editor.setOption("theme", settings.editorTheme); - editor.setOption( - "showGutter", - settings.linenumbers || settings.showAnnotations, - ); - editor.setOption("showLineNumbers", settings.linenumbers); - editor.setOption("enableEmmet", true); - editor.setOption("showInvisibles", settings.showSpaces); - editor.setOption("indentedSoftWrap", false); - editor.setOption("scrollPastEnd", 0.5); - editor.setOption("showPrintMargin", settings.showPrintMargin); - editor.setOption("relativeLineNumbers", settings.relativeLineNumbers); - editor.setOption("useElasticTabstops", settings.elasticTabstops); - editor.setOption("useTextareaForIME", settings.useTextareaForIME); - editor.setOption("rtlText", settings.rtlText); - editor.setOption("hardWrap", settings.hardWrap); - editor.setOption("spellCheck", settings.spellCheck); - editor.setOption("printMarginColumn", settings.printMargin); - editor.setOption("enableBasicAutocompletion", true); - editor.setOption("enableLiveAutocompletion", settings.liveAutoCompletion); - editor.setOption("copyWithEmptySelection", true); - editor.setOption("fadeFoldWidgets", settings.fadeFoldWidgets); + activeFile.markChanged = true; + }, TIMEOUT_VALUE); + // }); + + // TODO: Implement change annotation event for CodeMirror + // editor.on("changeAnnotation", toggleProblemButton); + + // TODO: Implement scroll event for CodeMirror + // editor.on("scroll", () => { + // clearTimeout(scrollTimeout); + // isScrolling = true; + // scrollTimeout = setTimeout(() => { + // isScrolling = false; + // }, 100); + // }); + + // TODO: Implement resize event for CodeMirror + // editor.renderer.on("resize", () => { + // $vScrollbar.resize($vScrollbar.visible); + // $hScrollbar.resize($hScrollbar.visible); + // }); + + // TODO: Implement scroll events for CodeMirror + // editor.on("scrolltop", onscrolltop); + // editor.on("scrollleft", onscrollleft); + // TODO: Add keydown listeners to CodeMirror + // textInput.addEventListener("keydown", (e) => { + // if (e.key === "Escape") { + // keydownState.esc = { value: true, target: textInput }; + // } + // }); + + // TODO: Implement color preview for CodeMirror + // if (colorPreview) { + // initColorView(editor); + // } + // TODO: Implement touch listeners for CodeMirror + // touchListeners(editor); + // TODO: Implement commands for CodeMirror + // setCommands(editor); + // TODO: Implement key bindings for CodeMirror + // await setKeyBindings(editor); + // TODO: Implement Emmet for CodeMirror + // Emmet.setCore(window.emmet); + // TODO: Implement font size for CodeMirror + // editor.setFontSize(fontSize); + // TODO: Implement highlight selected word for CodeMirror + // editor.setHighlightSelectedWord(true); + // TODO: Implement line height for CodeMirror + // editor.container.style.lineHeight = lineHeight; + + // TODO: Implement all editor options for CodeMirror + // ace.require("ace/ext/language_tools"); + // editor.setOption("animatedScroll", false); + // editor.setOption("tooltipFollowsMouse", false); + // editor.setOption("theme", settings.editorTheme); + // editor.setOption("showGutter", settings.linenumbers || settings.showAnnotations); + // editor.setOption("showLineNumbers", settings.linenumbers); + // editor.setOption("enableEmmet", true); + // editor.setOption("showInvisibles", settings.showSpaces); + // editor.setOption("indentedSoftWrap", false); + // editor.setOption("scrollPastEnd", 0.5); + // editor.setOption("showPrintMargin", settings.showPrintMargin); + // editor.setOption("relativeLineNumbers", settings.relativeLineNumbers); + // editor.setOption("useElasticTabstops", settings.elasticTabstops); + // editor.setOption("useTextareaForIME", settings.useTextareaForIME); + // editor.setOption("rtlText", settings.rtlText); + // editor.setOption("hardWrap", settings.hardWrap); + // editor.setOption("spellCheck", settings.spellCheck); + // editor.setOption("printMarginColumn", settings.printMargin); + // editor.setOption("enableBasicAutocompletion", true); + // editor.setOption("enableLiveAutocompletion", settings.liveAutoCompletion); + // editor.setOption("copyWithEmptySelection", true); + // editor.setOption("fadeFoldWidgets", settings.fadeFoldWidgets); // editor.setOption('enableInlineAutocompletion', settings.inlineAutoCompletion); updateMargin(true); updateSideButtonContainer(); - editor.renderer.setScrollMargin( - scrollMarginTop, - scrollMarginBottom, - scrollMarginLeft, - scrollMarginRight, - ); + // TODO: Implement scroll margin for CodeMirror + // editor.renderer.setScrollMargin( + // scrollMarginTop, + // scrollMarginBottom, + // scrollMarginLeft, + // scrollMarginRight, + // ); } /** * Scrolls the cursor into view if it is not currently visible. */ + // TODO: Implement cursor scrolling for CodeMirror function scrollCursorIntoView() { - keyboardHandler.off("keyboardShow", scrollCursorIntoView); - if (isCursorVisible()) return; - const { teardropSize } = appSettings.value; - editor.renderer.scrollCursorIntoView(); - editor.renderer.scrollBy(0, teardropSize + 10); - editor._emit("scroll-intoview"); + // keyboardHandler.off("keyboardShow", scrollCursorIntoView); + // if (isCursorVisible()) return; + // const { teardropSize } = appSettings.value; + // editor.renderer.scrollCursorIntoView(); + // editor.renderer.scrollBy(0, teardropSize + 10); + // editor._emit("scroll-intoview"); } /** * Checks if the cursor is visible within the Ace editor. * @returns {boolean} - True if the cursor is visible, false otherwise. */ + // TODO: Implement cursor visibility check for CodeMirror function isCursorVisible() { - const { editor, container } = editorManager; - const { teardropSize } = appSettings.value; - const cursorPos = editor.getCursorPosition(); - const contentTop = container.getBoundingClientRect().top; - const contentBottom = contentTop + container.clientHeight; - const cursorTop = editor.renderer.textToScreenCoordinates( - cursorPos.row, - cursorPos.column, - ).pageY; - const cursorBottom = cursorTop + teardropSize + 10; - return cursorTop >= contentTop && cursorBottom <= contentBottom; + // const { editor, container } = manager; + // const { teardropSize } = appSettings.value; + // const cursorPos = editor.getCursorPosition(); + // const contentTop = container.getBoundingClientRect().top; + // const contentBottom = contentTop + container.clientHeight; + // const cursorTop = editor.renderer.textToScreenCoordinates( + // cursorPos.row, + // cursorPos.column, + // ).pageY; + // const cursorBottom = cursorTop + teardropSize + 10; + // return cursorTop >= contentTop && cursorBottom <= contentBottom; + return true; // Placeholder } /** * Sets the vertical scroll value of the editor. This is called when the editor is scrolled horizontally using the scrollbar. * @param {Number} value */ + // TODO: Implement vertical scrolling for CodeMirror function onscrollV(value) { - preventScrollbarV = true; - const session = editor.getSession(); - const editorHeight = getEditorHeight(editor); - const scroll = editorHeight * value; - - session.setScrollTop(scroll); - editor._emit("scroll", editor); - cancelAnimationFrame(scrollAnimationFrame); + // preventScrollbarV = true; + // const session = editor.getSession(); + // const editorHeight = getEditorHeight(editor); + // const scroll = editorHeight * value; + // session.setScrollTop(scroll); + // editor._emit("scroll", editor); + // cancelAnimationFrame(scrollAnimationFrame); } /** @@ -458,15 +615,15 @@ async function EditorManager($header, $body) { * Sets the horizontal scroll value of the editor. This is called when the editor is scrolled vertically using the scrollbar. * @param {number} value - The scroll value. */ + // TODO: Implement horizontal scrolling for CodeMirror function onscrollH(value) { - preventScrollbarH = true; - const session = editor.getSession(); - const editorWidth = getEditorWidth(editor); - const scroll = editorWidth * value; - - session.setScrollLeft(scroll); - editor._emit("scroll", editor); - cancelAnimationFrame(scrollAnimationFrame); + // preventScrollbarH = true; + // const session = editor.getSession(); + // const editorWidth = getEditorWidth(editor); + // const scroll = editorWidth * value; + // session.setScrollLeft(scroll); + // editor._emit("scroll", editor); + // cancelAnimationFrame(scrollAnimationFrame); } /** @@ -479,55 +636,53 @@ async function EditorManager($header, $body) { /** * Sets scrollbars value based on the editor's scroll position. */ + // TODO: Implement horizontal scroll value for CodeMirror function setHScrollValue() { - if (appSettings.value.textWrap || preventScrollbarH) return; - const session = editor.getSession(); - const scrollLeft = session.getScrollLeft(); - - if (scrollLeft === lastScrollLeft) return; - - const editorWidth = getEditorWidth(editor); - const factor = (scrollLeft / editorWidth).toFixed(2); - - lastScrollLeft = scrollLeft; - $hScrollbar.value = factor; - editor._emit("scroll", "horizontal"); + // if (appSettings.value.textWrap || preventScrollbarH) return; + // const session = editor.getSession(); + // const scrollLeft = session.getScrollLeft(); + // if (scrollLeft === lastScrollLeft) return; + // const editorWidth = getEditorWidth(editor); + // const factor = (scrollLeft / editorWidth).toFixed(2); + // lastScrollLeft = scrollLeft; + // $hScrollbar.value = factor; + // editor._emit("scroll", "horizontal"); } /** * Handles the scroll left event. * Updates the horizontal scroll value and renders the horizontal scrollbar. */ + // TODO: Implement scroll left handler for CodeMirror function onscrollleft() { - setHScrollValue(); - $hScrollbar.render(); + // setHScrollValue(); + // $hScrollbar.render(); } /** * Sets scrollbars value based on the editor's scroll position. */ + // TODO: Implement vertical scroll value for CodeMirror function setVScrollValue() { - if (preventScrollbarV) return; - const session = editor.getSession(); - const scrollTop = session.getScrollTop(); - - if (scrollTop === lastScrollTop) return; - - const editorHeight = getEditorHeight(editor); - const factor = (scrollTop / editorHeight).toFixed(2); - - lastScrollTop = scrollTop; - $vScrollbar.value = factor; - editor._emit("scroll", "vertical"); + // if (preventScrollbarV) return; + // const session = editor.getSession(); + // const scrollTop = session.getScrollTop(); + // if (scrollTop === lastScrollTop) return; + // const editorHeight = getEditorHeight(editor); + // const factor = (scrollTop / editorHeight).toFixed(2); + // lastScrollTop = scrollTop; + // $vScrollbar.value = factor; + // editor._emit("scroll", "vertical"); } /** * Handles the scroll top event. * Updates the vertical scroll value and renders the vertical scrollbar. */ + // TODO: Implement scroll top handler for CodeMirror function onscrolltop() { - setVScrollValue(); - $vScrollbar.render(); + // setVScrollValue(); + // $vScrollbar.render(); } /** @@ -574,18 +729,18 @@ async function EditorManager($header, $body) { /** * Toggles the visibility of the problem button based on the presence of annotations in the files. */ + // TODO: Implement problem button toggle for CodeMirror function toggleProblemButton() { - const fileWithProblems = manager.files.find((file) => { - if (file.type !== "editor") return false; - const annotations = file?.session?.getAnnotations(); - return !!annotations.length; - }); - - if (fileWithProblems) { - problemButton.show(); - } else { - problemButton.hide(); - } + // const fileWithProblems = manager.files.find((file) => { + // if (file.type !== "editor") return false; + // const annotations = file?.session?.getAnnotations(); + // return !!annotations.length; + // }); + // if (fileWithProblems) { + // problemButton.show(); + // } else { + // problemButton.hide(); + // } } /** @@ -613,15 +768,15 @@ async function EditorManager($header, $body) { const bottom = 0; const right = showSideButtons ? 15 : 0; const left = linenumbers ? (showAnnotations ? 0 : -16) : 0; - - editor.renderer.setMargin(top, bottom, left, right); + // TODO + //editor.renderer.setMargin(top, bottom, left, right); if (!updateGutter) return; - editor.setOptions({ - showGutter: linenumbers || showAnnotations, - showLineNumbers: linenumbers, - }); + // editor.setOptions({ + // showGutter: linenumbers || showAnnotations, + // showLineNumbers: linenumbers, + // }); } /** @@ -644,8 +799,8 @@ async function EditorManager($header, $body) { manager.activeFile = file; if (file.type === "editor") { - editor.setSession(file.session); - editor.setReadOnly(!file.editable || !!file.loading); + // Apply active file content and language to CodeMirror + applyFileToEditor(file); $container.style.display = "block"; $hScrollbar.hideImmediately(); @@ -663,8 +818,9 @@ async function EditorManager($header, $body) { $container.parentElement.appendChild(file.content); } } + // TODO: Implement selection clearing for CodeMirror if (manager.activeFile && manager.activeFile.type === "editor") { - manager.activeFile.session.selection.clearSelection(); + // manager.activeFile.session.selection.clearSelection(); } } @@ -685,6 +841,20 @@ async function EditorManager($header, $body) { events.emit("switch-file", file); } + // When a file finishes loading its content, refresh the editor if it's active + manager.on(["file-loaded"], (file) => { + if (!file) return; + if (manager.activeFile?.id === file.id && file.type === "editor") { + applyFileToEditor(file); + } + }); + + // Re-apply state when read-only toggles on the active file + manager.on(["update:read-only"], () => { + const file = manager.activeFile; + if (file?.type === "editor") applyFileToEditor(file); + }); + /** * Initializes the file tab container. */ @@ -784,12 +954,18 @@ async function EditorManager($header, $body) { * @param {AceAjax.Editor} editor * @returns */ + // TODO: Implement editor height calculation for CodeMirror function getEditorHeight(editor) { - const { renderer, session } = editor; - const offset = (renderer.$size.scrollerHeight + renderer.lineHeight) * 0.5; - const editorHeight = - session.getScreenLength() * renderer.lineHeight - offset; - return editorHeight; + try { + const sd = editor?.scrollDOM; + if (!sd) return 0; + // Return the total vertical scrollable range + const total = sd.scrollHeight || 0; + const viewport = sd.clientHeight || 0; + return Math.max(total - viewport, 0); + } catch (_) { + return 0; + } } /** @@ -797,15 +973,22 @@ async function EditorManager($header, $body) { * @param {AceAjax.Editor} editor * @returns */ + // TODO: Implement editor width calculation for CodeMirror function getEditorWidth(editor) { - const { renderer, session } = editor; - const offset = renderer.$size.scrollerWidth - renderer.characterWidth; - const editorWidth = - session.getScreenWidth() * renderer.characterWidth - offset; - if (appSettings.value.textWrap) { - return editorWidth; - } else { - return editorWidth + appSettings.value.leftMargin; + try { + const sd = editor?.scrollDOM; + if (!sd) return 0; + // Return the total horizontal scrollable range + const total = sd.scrollWidth || 0; + const viewport = sd.clientWidth || 0; + let width = Math.max(total - viewport, 0); + if (!appSettings.value.textWrap) { + const { leftMargin = 0 } = appSettings.value; + width += leftMargin || 0; + } + return width; + } catch (_) { + return 0; } } } diff --git a/src/main.js b/src/main.js index d01500c63..e501a2014 100644 --- a/src/main.js +++ b/src/main.js @@ -8,14 +8,13 @@ import "styles/overrideAceStyle.scss"; import "styles/wideScreen.scss"; import "lib/polyfill"; -import "ace/supportedModes"; +import "./codemirror/supportedModes"; import "components/WebComponents"; import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import ajax from "@deadlyjack/ajax"; import { setKeyBindings } from "ace/commands"; -import { initModes } from "ace/modelist"; import Contextmenu from "components/contextmenu"; import Sidebar from "components/sidebar"; import tile from "components/tile"; @@ -51,6 +50,7 @@ import loadPolyFill from "utils/polyfill"; import Url from "utils/Url"; import $_fileMenu from "views/file-menu.hbs"; import $_menu from "views/menu.hbs"; +import { initModes } from "./codemirror/modelist"; import auth, { loginEvents } from "./lib/auth"; const previousVersionCode = Number.parseInt(localStorage.versionCode, 10); @@ -433,7 +433,7 @@ async function loadApp() { $sidebar.onshow = () => { const activeFile = editorManager.activeFile; - if (activeFile) editorManager.editor.blur(); + if (activeFile) editorManager.editor.contentDOM.blur(); }; sdcard.watchFile(KEYBINDING_FILE, async () => { await setKeyBindings(editorManager.editor); @@ -573,7 +573,8 @@ function onClickApp(e) { function mainPageOnShow() { const { editor } = editorManager; - editor.resize(true); + // TODO : Codemirror + //editor.resize(true); } function createMainMenu({ top, bottom, toggler }) { @@ -610,16 +611,17 @@ function createFileMenu({ top, bottom, toggler }) { const { label: encoding } = getEncoding(file.encoding); const isEditorFile = file.type === "editor"; + const cmEditor = window.editorManager?.editor; + const hasSelection = !!cmEditor && !cmEditor.state.selection.main.empty; return mustache.render($_fileMenu, { ...strings, - file_mode: isEditorFile - ? (file.session?.getMode()?.$id || "").split("/").pop() - : "", + // Use CodeMirror mode stored on EditorFile (set in setMode) + file_mode: isEditorFile ? file.currentMode || "" : "", file_encoding: isEditorFile ? encoding : "", file_read_only: !file.editable, file_on_disk: !!file.uri, file_eol: isEditorFile ? file.eol : "", - copy_text: !!window.editorManager.editor.getCopyText(), + copy_text: isEditorFile ? hasSelection : false, is_editor: isEditorFile, }); }, diff --git a/src/palettes/changeMode/index.js b/src/palettes/changeMode/index.js index c2e3e2d96..2327d6ae7 100644 --- a/src/palettes/changeMode/index.js +++ b/src/palettes/changeMode/index.js @@ -1,12 +1,13 @@ import palette from "components/palette"; import Path from "utils/Path"; +import { getModes } from "../../codemirror/modelist"; export default function changeMode() { palette(generateHints, onselect, strings["syntax highlighting"]); } function generateHints() { - const { modes } = ace.require("ace/ext/modelist"); + const modes = getModes(); return modes.map(({ caption, mode, extensions }) => { return { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 9a2edcab1..dda07b2a2 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -3,6 +3,7 @@ import ajax from "@deadlyjack/ajax"; import alert from "dialogs/alert"; import escapeStringRegexp from "escape-string-regexp"; import constants from "lib/constants"; +import { getModeForPath as getCMModeForPath } from "../codemirror/modelist"; import path from "./Path"; import Uri from "./Uri"; import Url from "./Url"; @@ -73,11 +74,17 @@ export default { * @param {string} filename */ getIconForFile(filename) { - const { getModeForPath } = ace.require("ace/ext/modelist"); const type = getFileType(filename); - const { name } = getModeForPath(filename); + // Use CodeMirror's modelist to determine mode name + let modeName = "text"; + try { + const mode = getCMModeForPath?.(filename); + modeName = mode?.name || modeName; + } catch (e) { + // fallback to default if CodeMirror modelist isn't available yet + } - const iconForMode = `file_type_${name}`; + const iconForMode = `file_type_${modeName}`; const iconForType = `file_type_${type}`; return `file file_type_default ${iconForMode} ${iconForType}`; diff --git a/webpack.config.js b/webpack.config.js index 29c42385d..f936408eb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,6 +39,7 @@ module.exports = (env, options) => { // if (mode === 'production') { rules.push({ test: /\.m?js$/, + exclude: /node_modules\/(@codemirror|codemirror)/, // Exclude CodeMirror files from html-tag-js loader use: [ 'html-tag-js/jsx/tag-loader.js', { @@ -49,6 +50,20 @@ module.exports = (env, options) => { }, ], }); + + // Separate rule for CodeMirror files - only babel-loader, no html-tag-js + rules.push({ + test: /\.m?js$/, + include: /node_modules\/(@codemirror|codemirror)/, + use: [ + { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + }, + }, + ], + }); // } const main = { @@ -56,6 +71,7 @@ module.exports = (env, options) => { entry: { main: './src/main.js', console: './src/lib/console.js', + searchInFilesWorker: './src/sidebarApps/searchInFiles/worker.js', }, output: { path: path.resolve(__dirname, 'www/build/'),