Readded search bar and enhanced it.

- also added android hinting support
This commit is contained in:
chromoxdor
2025-07-24 19:20:45 +02:00
parent 1f7224be4e
commit d3aeb206bf
6 changed files with 579 additions and 237 deletions
+256 -198
View File
@@ -635,18 +635,29 @@ var isSame;
// This is from the file search.js-------------------------------------------------------------------------
(function (search) {
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.
// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.
(function(search) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
search(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], search);
else // Plain browser env
search(CodeMirror);
})(function (CodeMirror) {
})(function(CodeMirror) {
"use strict";
// default search panel location
CodeMirror.defineOption("search", { bottom: false });
CodeMirror.defineOption("search", {bottom: false});
function searchOverlay(query, caseInsensitive) {
if (typeof query == "string")
@@ -654,20 +665,18 @@ var isSame;
else if (!query.global)
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
return {
token: function (stream) {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return "searching";
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
return {token: function(stream) {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return "searching";
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
};
}};
}
function SearchState() {
@@ -685,7 +694,7 @@ var isSame;
function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(query, pos, { caseFold: queryCaseInsensitive(query), multiline: true });
return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});
}
function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
@@ -693,14 +702,14 @@ var isSame;
value: deflt,
selectValueOnOpen: true,
closeOnEnter: false,
onClose: function () { clearSearch(cm); },
onClose: function() { clearSearch(cm); },
onKeyDown: onKeyDown,
bottom: cm.options.search.bottom
});
}
function dialog(cm, text, shortText, deflt, f) {
if (cm.openDialog) cm.openDialog(text, f, { value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom });
if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom});
else f(prompt(shortText, deflt));
}
@@ -710,7 +719,7 @@ var isSame;
}
function parseString(string) {
return string.replace(/\\([nrt\\])/g, function (match, ch) {
return string.replace(/\\([nrt\\])/g, function(match, ch) {
if (ch == "n") return "\n"
if (ch == "r") return "\r"
if (ch == "t") return "\t"
@@ -723,7 +732,7 @@ var isSame;
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
if (isRE) {
try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
catch (e) { } // Not a regular expression after all, do a string search
catch(e) {} // Not a regular expression after all, do a string search
} else {
query = parseString(query)
}
@@ -751,7 +760,7 @@ var isSame;
if (q instanceof RegExp && q.source == "x^") q = null
if (persistent && cm.openDialog) {
var hiding = null
var searchNext = function (query, event) {
var searchNext = function(query, event) {
CodeMirror.e_stop(event);
if (!query) return;
if (query != state.queryText) {
@@ -759,15 +768,15 @@ var isSame;
state.posFrom = state.posTo = cm.getCursor();
}
if (hiding) hiding.style.opacity = 1
findNext(cm, event.shiftKey, function (_, to) {
findNext(cm, event.shiftKey, function(_, to) {
var dialog
if (to.line < 3 && document.querySelector &&
(dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
(dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
(hiding = dialog).style.opacity = .4
})
};
persistentDialog(cm, getQueryDialog(cm), q, searchNext, function (event, query) {
persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) {
var keyName = CodeMirror.keyName(event)
var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
if (cmd == "findNext" || cmd == "findPrev" ||
@@ -785,8 +794,8 @@ var isSame;
findNext(cm, rev);
}
} else {
dialog(cm, getQueryDialog(cm), "Search for:", q, function (query) {
if (query && !state.query) cm.operation(function () {
dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) {
if (query && !state.query) cm.operation(function() {
startSearch(cm, state, query);
state.posFrom = state.posTo = cm.getCursor();
findNext(cm, rev);
@@ -795,31 +804,44 @@ var isSame;
}
}
function findNext(cm, rev, callback) {
cm.operation(function () {
var state = getSearchState(cm);
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
if (!cursor.find(rev)) {
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
if (!cursor.find(rev)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 20);
state.posFrom = cursor.from(); state.posTo = cursor.to();
if (callback) callback(cursor.from(), cursor.to())
});
}
function findNext(cm, rev, callback) {
cm.operation(function () {
const state = getSearchState(cm);
function clearSearch(cm) {
cm.operation(function () {
var state = getSearchState(cm);
state.lastQuery = state.query;
if (!state.query) return;
state.query = state.queryText = null;
cm.removeOverlay(state.overlay);
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
// 🔸 Clear previous highlight
if (state.lastHighlight) {
state.lastHighlight.clear();
state.lastHighlight = null;
}
let cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
if (!cursor.find(rev)) {
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
if (!cursor.find(rev)) return;
}
// 🔸 Highlight current match
const mark = cm.markText(cursor.from(), cursor.to(), {
className: 'search-next-highlight'
});
}
state.lastHighlight = mark;
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 20);
state.posFrom = cursor.from();
state.posTo = cursor.to();
if (callback) callback(cursor.from(), cursor.to());
});
}
function clearSearch(cm) {cm.operation(function() {
var state = getSearchState(cm);
state.lastQuery = state.query;
if (!state.query) return;
state.query = state.queryText = null;
cm.removeOverlay(state.overlay);
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
});}
function el(tag, attrs) {
var element = tag ? document.createElement(tag) : document.createDocumentFragment();
@@ -833,113 +855,159 @@ var isSame;
return element;
}
function getQueryDialog(cm) {
var label = el("label", { className: "CodeMirror-search-label" },
cm.phrase("Search:"),
el("input", {
type: "text", "style": "width: 10em", className: "CodeMirror-search-field",
id: "CodeMirror-search-field"
}));
label.setAttribute("for", "CodeMirror-search-field");
function getQueryDialog(cm) {
var label = el("label", {className: "CodeMirror-search-label"},
cm.phrase("Search:"),
el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field",
id: "CodeMirror-search-field"}));
label.setAttribute("for","CodeMirror-search-field");
return el("", null, label, " ",
el("span", { style: "color: #666", className: "CodeMirror-search-hint" },
cm.phrase("(Use /re/ syntax for regexp search)")));
el("span", {style: "color: #666", className: "CodeMirror-search-hint"},
cm.phrase("(Use /re/ syntax for regexp search)")));
}
function getReplaceQueryDialog(cm) {
return el("", null, " ",
el("input", { type: "text", "style": "width: 10em", className: "CodeMirror-search-field" }), " ",
el("span", { style: "color: #666", className: "CodeMirror-search-hint" },
cm.phrase("(Use /re/ syntax for regexp search)")));
el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field"}), " ",
el("span", {style: "color: #666", className: "CodeMirror-search-hint"},
cm.phrase("(Use /re/ syntax for regexp search)")));
}
function getReplacementQueryDialog(cm) {
return el("", null,
el("span", { className: "CodeMirror-search-label" }, cm.phrase("With:")), " ",
el("input", { type: "text", "style": "width: 10em", className: "CodeMirror-search-field" }));
el("span", {className: "CodeMirror-search-label"}, cm.phrase("With:")), " ",
el("input", {type: "text", "style": "width: 10em", className: "CodeMirror-search-field"}));
}
function getDoReplaceConfirm(cm) {
return el("", null,
el("span", { className: "CodeMirror-search-label" }, cm.phrase("Replace?")), " ",
el("button", {}, cm.phrase("Yes")), " ",
el("button", {}, cm.phrase("No")), " ",
el("button", {}, cm.phrase("All")), " ",
el("button", {}, cm.phrase("Stop")));
el("span", {className: "CodeMirror-search-label"}, cm.phrase("Replace?")), " ",
el("button", {}, cm.phrase("Yes")), " ",
el("button", {}, cm.phrase("No")), " ",
el("button", {}, cm.phrase("All")), " ",
el("button", {}, cm.phrase("Stop")));
}
function replaceAll(cm, query, text) {
cm.operation(function () {
cm.operation(function() {
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
if (typeof query != "string") {
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
cursor.replace(text.replace(/\$(\d)/g, function (_, i) { return match[i]; }));
cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
} else cursor.replace(text);
}
});
}
function replace(cm, all) {
if (cm.getOption("readOnly")) return;
var query = cm.getSelection() || getSearchState(cm).lastQuery;
var dialogText = all ? cm.phrase("Replace all:") : cm.phrase("Replace:")
var fragment = el("", null,
el("span", { className: "CodeMirror-search-label" }, dialogText),
getReplaceQueryDialog(cm))
dialog(cm, fragment, dialogText, query, function (query) {
if (!query) return;
query = parseQuery(query);
dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function (text) {
text = parseString(text)
if (all) {
replaceAll(cm, query, text)
} else {
clearSearch(cm);
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
var advance = function () {
var start = cursor.from(), match;
if (!(match = cursor.findNext())) {
cursor = getSearchCursor(cm, query);
if (!(match = cursor.findNext()) ||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"),
[function () { doReplace(match); }, advance,
function () { replaceAll(cm, query, text) }]);
};
var doReplace = function (match) {
cursor.replace(typeof query == "string" ? text :
text.replace(/\$(\d)/g, function (_, i) { return match[i]; }));
advance();
};
advance();
}
});
if (cm.getOption("readOnly")) return;
let activeHighlight = null; // Will store the current highlight
function highlightCurrentMatch(from, to) {
if (activeHighlight) {
activeHighlight.clear();
}
activeHighlight = cm.markText(from, to, {
className: 'search-next-highlight'
});
}
CodeMirror.commands.find = function (cm) { clearSearch(cm); doSearch(cm); };
CodeMirror.commands.findPersistent = function (cm) { clearSearch(cm); doSearch(cm, false, true); };
CodeMirror.commands.findPersistentNext = function (cm) { doSearch(cm, false, true, true); };
CodeMirror.commands.findPersistentPrev = function (cm) { doSearch(cm, true, true, true); };
function clearHighlight() {
if (activeHighlight) {
activeHighlight.clear();
activeHighlight = null;
}
}
// Clear highlight when dialog is removed (e.g., user presses Esc or clicks ❌)
const observer = new MutationObserver(() => {
if (!document.querySelector('.CodeMirror-dialog')) {
clearHighlight();
observer.disconnect();
}
});
observer.observe(document.querySelector('.CodeMirror'), {
childList: true,
subtree: true
});
var query = cm.getSelection() || getSearchState(cm).lastQuery;
var dialogText = all ? cm.phrase("Replace all:") : cm.phrase("Replace:");
var fragment = el("", null,
el("span", { className: "CodeMirror-search-label" }, dialogText),
getReplaceQueryDialog(cm));
dialog(cm, fragment, dialogText, query, function (query) {
if (!query) return;
query = parseQuery(query);
dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function (text) {
text = parseString(text);
if (all) {
clearHighlight();
replaceAll(cm, query, text);
} else {
clearSearch(cm);
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
var advance = function () {
var start = cursor.from(), match;
if (!(match = cursor.findNext())) {
cursor = getSearchCursor(cm, query);
if (!(match = cursor.findNext()) ||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
highlightCurrentMatch(cursor.from(), cursor.to());
confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"), [
function () { doReplace(match); },
advance,
function () {
clearHighlight();
replaceAll(cm, query, text);
}
]);
};
var doReplace = function (match) {
cursor.replace(typeof query == "string" ? text :
text.replace(/\$(\d)/g, function (_, i) { return match[i]; }));
advance();
};
advance();
}
});
});
}
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
CodeMirror.commands.findNext = doSearch;
CodeMirror.commands.findPrev = function (cm) { doSearch(cm, true); };
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
CodeMirror.commands.clearSearch = clearSearch;
CodeMirror.commands.replace = replace;
CodeMirror.commands.replaceAll = function (cm) { replace(cm, true); };
CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
});
// This is from the file searchcursor.js-------------------------------------------------------------------------
(function (searchcursor) {
(function(searchcursor) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
searchcursor(require("../../lib/codemirror"))
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod)
define(["../../lib/codemirror"], searchcursor)
else // Plain browser env
searchcursor(CodeMirror)
})(function (CodeMirror) {
})(function(CodeMirror) {
"use strict"
var Pos = CodeMirror.Pos
@@ -967,11 +1035,9 @@ var isSame;
regexp.lastIndex = ch
var string = doc.getLine(line), match = regexp.exec(string)
if (match)
return {
from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match
}
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
@@ -997,12 +1063,10 @@ var isSame;
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length
return {
from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match
}
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
@@ -1028,11 +1092,9 @@ var isSame;
var string = doc.getLine(line)
var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch)
if (match)
return {
from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match
}
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
@@ -1051,30 +1113,28 @@ var isSame;
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = line + before.length, startCh = before[before.length - 1].length
return {
from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match
}
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
var doFold, noFold
if (String.prototype.normalize) {
doFold = function (str) { return str.normalize("NFD").toLowerCase() }
noFold = function (str) { return str.normalize("NFD") }
doFold = function(str) { return str.normalize("NFD").toLowerCase() }
noFold = function(str) { return str.normalize("NFD") }
} else {
doFold = function (str) { return str.toLowerCase() }
noFold = function (str) { return str }
doFold = function(str) { return str.toLowerCase() }
noFold = function(str) { return str }
}
// Maps a position in a case-folded line back to a position in the original line
// (compensating for codepoints increasing in number during folding)
function adjustPos(orig, folded, pos, foldFunc) {
if (orig.length == folded.length) return pos
for (var min = 0, max = pos + Math.max(0, orig.length - folded.length); ;) {
for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) {
if (min == max) return min
var mid = (min + max) >> 1
var len = foldFunc(orig.slice(0, mid)).length
@@ -1097,10 +1157,8 @@ var isSame;
var found = string.indexOf(lines[0])
if (found == -1) continue search
var start = adjustPos(orig, string, found, fold) + ch
return {
from: Pos(line, adjustPos(orig, string, found, fold) + ch),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)
}
return {from: Pos(line, adjustPos(orig, string, found, fold) + ch),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)}
} else {
var cutFrom = string.length - lines[0].length
if (string.slice(cutFrom) != lines[0]) continue search
@@ -1108,10 +1166,8 @@ var isSame;
if (fold(doc.getLine(line + i)) != lines[i]) continue search
var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
if (endString.slice(0, lastLine.length) != lastLine) continue search
return {
from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))
}
return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}
}
}
}
@@ -1128,10 +1184,8 @@ var isSame;
if (lines.length == 1) {
var found = string.lastIndexOf(lines[0])
if (found == -1) continue search
return {
from: Pos(line, adjustPos(orig, string, found, fold)),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))
}
return {from: Pos(line, adjustPos(orig, string, found, fold)),
to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))}
} else {
var lastLine = lines[lines.length - 1]
if (string.slice(0, lastLine.length) != lastLine) continue search
@@ -1139,10 +1193,8 @@ var isSame;
if (fold(doc.getLine(start + i)) != lines[i]) continue search
var top = doc.getLine(line + 1 - lines.length), topString = fold(top)
if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search
return {
from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),
to: Pos(line, adjustPos(orig, string, lastLine.length, fold))
}
return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),
to: Pos(line, adjustPos(orig, string, lastLine.length, fold))}
}
}
}
@@ -1152,7 +1204,7 @@ var isSame;
this.afterEmptyMatch = false
this.doc = doc
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
this.pos = { from: pos, to: pos }
this.pos = {from: pos, to: pos}
var caseFold
if (typeof options == "object") {
@@ -1164,27 +1216,27 @@ var isSame;
if (typeof query == "string") {
if (caseFold == null) caseFold = false
this.matches = function (reverse, pos) {
this.matches = function(reverse, pos) {
return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
}
} else {
query = ensureFlags(query, "gm")
if (!options || options.multiline !== false)
this.matches = function (reverse, pos) {
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
}
else
this.matches = function (reverse, pos) {
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
}
}
}
SearchCursor.prototype = {
findNext: function () { return this.find(false) },
findPrevious: function () { return this.find(true) },
findNext: function() {return this.find(false)},
findPrevious: function() {return this.find(true)},
find: function (reverse) {
find: function(reverse) {
var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
if (this.afterEmptyMatch && this.atOccurrence) {
// do not return the same 0 width match twice
@@ -1203,7 +1255,7 @@ var isSame;
}
}
if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) {
return this.atOccurrence = false
return this.atOccurrence = false
}
}
var result = this.matches(reverse, head)
@@ -1215,36 +1267,36 @@ var isSame;
return this.pos.match || true
} else {
var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
this.pos = { from: end, to: end }
this.pos = {from: end, to: end}
return this.atOccurrence = false
}
},
from: function () { if (this.atOccurrence) return this.pos.from },
to: function () { if (this.atOccurrence) return this.pos.to },
from: function() {if (this.atOccurrence) return this.pos.from},
to: function() {if (this.atOccurrence) return this.pos.to},
replace: function (newText, origin) {
replace: function(newText, origin) {
if (!this.atOccurrence) return
var lines = CodeMirror.splitLines(newText)
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
}
}
CodeMirror.defineExtension("getSearchCursor", function (query, pos, caseFold) {
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold)
})
CodeMirror.defineDocExtension("getSearchCursor", function (query, pos, caseFold) {
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this, query, pos, caseFold)
})
CodeMirror.defineExtension("selectMatches", function (query, caseFold) {
CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
var ranges = []
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
while (cur.findNext()) {
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
ranges.push({ anchor: cur.from(), head: cur.to() })
ranges.push({anchor: cur.from(), head: cur.to()})
}
if (ranges.length)
this.setSelections(ranges, 0)
@@ -1255,16 +1307,20 @@ var isSame;
// This is from the file dialog.js-------------------------------------------------------------------------
(function (dialog) {
// Open simple dialogs on top of an editor. Relies on dialog.css.
(function(dialog) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
dialog(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
define(["../../lib/codemirror"], dialog);
else // Plain browser env
dialog(CodeMirror);
})(function (CodeMirror) {
})(function(CodeMirror) {
function dialogDiv(cm, template, bottom) {
var wrap = cm.getWrapperElement();
//var wrap = cm.getWrapperElement(); changed by cxd to get searchfield out of div
var wrap = document.body;
var dialog;
dialog = wrap.appendChild(document.createElement("div"));
if (bottom)
@@ -1277,7 +1333,9 @@ var isSame;
} else { // Assuming it's a detached DOM element.
dialog.appendChild(template);
}
// Append the dialog to <body> instead of inside the clipped container
CodeMirror.addClass(wrap, 'dialog-opened');
return dialog;
}
@@ -1287,7 +1345,7 @@ var isSame;
cm.state.currentNotificationClose = newVal;
}
CodeMirror.defineExtension("openDialog", function (template, callback, options) {
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
if (!options) options = {};
closeNotification(this, null);
@@ -1320,11 +1378,11 @@ var isSame;
}
if (options.onInput)
CodeMirror.on(inp, "input", function (e) { options.onInput(e, inp.value, close); });
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
if (options.onKeyUp)
CodeMirror.on(inp, "keyup", function (e) { options.onKeyUp(e, inp.value, close); });
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
CodeMirror.on(inp, "keydown", function (e) {
CodeMirror.on(inp, "keydown", function(e) {
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
inp.blur();
@@ -1338,7 +1396,7 @@ var isSame;
if (evt.relatedTarget !== null) close();
});
} else if (button = dialog.getElementsByTagName("button")[0]) {
CodeMirror.on(button, "click", function () {
CodeMirror.on(button, "click", function() {
close();
me.focus();
});
@@ -1350,7 +1408,7 @@ var isSame;
return close;
});
CodeMirror.defineExtension("openConfirm", function (template, callbacks, options) {
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
closeNotification(this, null);
var dialog = dialogDiv(this, template, options && options.bottom);
var buttons = dialog.getElementsByTagName("button");
@@ -1365,18 +1423,18 @@ var isSame;
buttons[0].focus();
for (var i = 0; i < buttons.length; ++i) {
var b = buttons[i];
(function (callback) {
CodeMirror.on(b, "click", function (e) {
(function(callback) {
CodeMirror.on(b, "click", function(e) {
CodeMirror.e_preventDefault(e);
close();
if (callback) callback(me);
});
})(callbacks[i]);
CodeMirror.on(b, "blur", function () {
CodeMirror.on(b, "blur", function() {
--blurring;
setTimeout(function () { if (blurring <= 0) close(); }, 200);
setTimeout(function() { if (blurring <= 0) close(); }, 200);
});
CodeMirror.on(b, "focus", function () { ++blurring; });
CodeMirror.on(b, "focus", function() { ++blurring; });
}
});
@@ -1388,7 +1446,7 @@ var isSame;
* If a notification is opened while another is opened, it will close the
* currently opened one and open the new one immediately.
*/
CodeMirror.defineExtension("openNotification", function (template, options) {
CodeMirror.defineExtension("openNotification", function(template, options) {
closeNotification(this, close);
var dialog = dialogDiv(this, template, options && options.bottom);
var closed = false, doneTimer;
@@ -1402,7 +1460,7 @@ var isSame;
dialog.parentNode.removeChild(dialog);
}
CodeMirror.on(dialog, 'click', function (e) {
CodeMirror.on(dialog, 'click', function(e) {
CodeMirror.e_preventDefault(e);
close();
});
@@ -1412,4 +1470,4 @@ var isSame;
return close;
});
});
});
+1 -1
View File
File diff suppressed because one or more lines are too long
+16 -11
View File
@@ -3,7 +3,7 @@
--cmc1: black;
--cmbg2: #eee;
--cmc2: #999;
--cmbd: 1px solid black;
--cmbd: 1px solid black;
--cmbg3: #d7d4f0;
--cmbg4: #e8f2ff;
--cmc3: #708;
@@ -31,11 +31,11 @@
}
[data-theme="dark"] {
--cmbg1: black;
--cmc1: #e8d1a7;
--cmbg1: black;
--cmc1: #e8d1a7;
--cmbg2: #212121;
--cmc2: #e8d2a7;
--cmbd: 1px solid #FFCC00;
--cmbd: 1px solid #FFCC00;
--cmbg3: #80cbc433;
--cmbg4: #00000080;
--cmc3: #ea6962;
@@ -63,7 +63,7 @@
}
@media (prefers-color-scheme:dark) {
[data-theme="auto"] {
[data-theme="auto"] {
--cmbg1: black;
--cmc1: #e8d1a7;
--cmbg2: #212121;
@@ -93,7 +93,7 @@
--cmbg8: #5f5f5f;
--cmbg9: #5f5f5f;
--cmc19: #eee;
}
}
}
/* BASICS */
@@ -637,7 +637,12 @@ div.CodeMirror-dragcursors {
}
.cm-searching {
background-color: rgba(162, 162, 162, 0.396);
background-color: yellow;
color: black;
}
.search-next-highlight {
background-color: orange !important;
}
/* Used to force a border model for a node */
@@ -719,16 +724,16 @@ li.CodeMirror-hint-active {
.CodeMirror-dialog {
position: fixed;
height: 42px;
/* height: 42px; */
left: 0;
width: 100vw;
background: inherit;
z-index: 15;
padding: .1em .8em;
overflow: hidden;
padding: .4em .8em;
/* overflow: hidden;
color: inherit;
display: flex;
align-items: center;
align-items: center; */
}
.CodeMirror-dialog input::selection {
+1 -1
View File
File diff suppressed because one or more lines are too long
+304 -25
View File
@@ -200,9 +200,9 @@ var EXTRAWORDS = commonAtoms.concat(commonPlugins, commonKeywords, commonCommand
var rEdit;
var confirmR = true;
var android = /Android/.test(navigator.userAgent);
function initCM() {
var android = /Android/.test(navigator.userAgent);
if (android) {
if (confirm("Do you want to enable colored rules? (There are some issues with the standard Android Keyboard causing it to fail!)")) {
confirmR = true
@@ -230,25 +230,133 @@ function initCM() {
'Shift-Tab': (cm) => cm.execCommand('indentLess'),
}
});
rEdit.on('change', function () { rEdit.save() });
//hinting on input
rEdit.on("inputRead", function (cm, event) {
var letters = /[\w%,.]/; //characters for activation
var cur = cm.getCursor();
var token = cm.getTokenAt(cur);
if (letters.test(event.text) && token.type != "comment") {
cm.showHint({ completeSingle: false });
};
});
if (!android) {
rEdit.on("inputRead", function (cm, event) {
var letters = /[\w%,.]/; //characters for activation
var cur = cm.getCursor();
var token = cm.getTokenAt(cur);
if (letters.test(event.text) && token.type != "comment") {
cm.showHint({ completeSingle: false });
};
});
}
CodeMirror.keyMap.default["Ctrl-F"] = function (cm) {
openFind(); // Inject your custom buttons
};
CodeMirror.keyMap.default["Cmd-F"] = function (cm) {
openFind();
};
}
}
//--------------------------------------------------------------------------------- add formatting option
//----------------------------------------------------------------------- add search and formatting options
// Add Format button inside the form
function closeSearchDialog() {
const dlg = document.querySelectorAll('.CodeMirror-dialog');
if (dlg.length > 0) {
rEdit.execCommand('clearSearch');
// Remove the highlight classes from marked text spans
const highlighted = document.querySelectorAll('.CodeMirror .search-next-highlight');
highlighted.forEach(el => el.classList.remove('search-next-highlight'));
dlg.forEach(d => d.remove());
document.body.classList.remove('dialog-opened');
}
}
function openFind() {
closeSearchDialog(); // Close any existing search dialog
rEdit.execCommand('findPersistent'); // Show search dialog
const element = document.querySelector('.CodeMirror-selected');
const dialog = document.querySelector('.CodeMirror-dialog');
if (!dialog || dialog.querySelector('.search-button-group')) return;
const buttons = [
{
title: 'Find Previous',
symbol: '▲',
action: () => rEdit.execCommand('findPersistentPrev')
},
{
title: 'Find Next',
symbol: '▼',
action: () => rEdit.execCommand('findPersistentNext')
},
{
title: 'Replace',
symbol: 'Replace',
action: () => {
closeSearchDialog();
rEdit.execCommand('replace');
openFind();
}
},
{
title: 'Close',
symbol: '❌',
action: closeSearchDialog
},
{
title: 'Help',
symbol: '?',
action: () => {
alert(`Available shortcuts:
Ctrl+F / Cmd+F: Open search
Ctrl+G / Cmd+G: Find next
Shift+Ctrl+G / Shift+Cmd+G: Find previous
Shift+Ctrl+F / Cmd+Option+F: Replace
Shift+Ctrl+R / Shift+Cmd+Option+F: Replace all
Use /re/ syntax for regexp search`);
}
}
];
buttons.forEach(({ title, symbol, action }) => {
const btn = document.createElement('span');
btn.title = title;
btn.className = title.toLowerCase() === 'help' ? 'button help' : 'button';
btn.innerHTML = symbol;
btn.style.cssText = `
cursor: pointer;
user-select: none;
`;
btn.addEventListener('click', (e) => {
e.preventDefault();
action();
});
dialog.appendChild(btn);
});
}
// Add Searchfield button
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('rulesselect');
if (form) {
// Add search help button to CodeMirror editor
const btn3 = document.createElement('button');
btn3.type = 'button'; // prevent form submission
btn3.id = 'searchBtn';
btn3.innerHTML = "&#128270;&#xFE0E;"; // magnifier
btn3.style.padding = "2px 5px";
btn3.className = 'button help'; // just the class, no inline style
form.appendChild(btn3);
btn3.addEventListener('click', () => {
if (typeof rEdit !== 'undefined') {
openFind();
}
});
// Add format document button
const btn = document.createElement('button');
btn.type = 'button'; // prevent form submission
btn.id = 'formatBtn';
@@ -262,30 +370,157 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Add hint button to CodeMirror editor
// const form2 = document.getElementsByClassName('CodeMirror')[0];
// if (form2) {
// console.log('Form2 found', form2);
// const btn2 = document.createElement('button');
// btn2.type = 'button'; // prevent form submission
// btn2.id = 'hintBtn';
// btn2.innerHTML = "&#9776;&#xFE0E;"; // ☰︎
// btn2.className = 'button help';
// btn2.style.position = 'absolute';
// btn2.style.right = '0';
// btn2.style.top = '0';
// form2.appendChild(btn2);
// btn2.addEventListener('click', () => {
// if (typeof rEdit !== 'undefined') {
// rEdit.focus(); // Optional: ensure focus
// rEdit.execCommand('autocomplete'); // Show hint menu
// }
// });
// }
let charBuffer = "";
// Global keydown handler
document.addEventListener('keydown', function (e) {
// Ctrl + Shift + F triggers formatting
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'f') {
e.preventDefault();
console.log('Keyboard shortcut detected: Formatting...');
triggerFormatting();
return;
}
// Clear buffer on navigation/editing/meta keys
const ignoreKeys = [
"Backspace", "Delete", "ArrowLeft", "ArrowRight",
"ArrowUp", "ArrowDown", "Enter", "Tab", "Escape",
"Shift", "Control", "Alt", "Meta"
];
if (ignoreKeys.includes(e.key) || e.key.length !== 1) {
charBuffer = "";
}
});
// Workaround of showing hints for Android devices
if (android) {
document.addEventListener("input", (e) => {
if (!rEdit || !e.data || e.data.length !== 1) {
charBuffer = "";
return;
}
const data = e.data;
const doc = rEdit.getDoc();
let cursor = doc.getCursor(); // Cursor is AFTER inserted char
const letters = /[\w%,.]/;
const token = rEdit.getTokenAt(cursor);
if (letters.test(data) && token.type !== "comment") {
charBuffer += data;
// Calculate starting point for re-inserting
const insertPos = {
line: cursor.line,
ch: cursor.ch - charBuffer.length + 1
};
// Replace from where the buffer began to current position
doc.replaceRange(charBuffer, insertPos, cursor);
// Set cursor at end of buffer
rEdit.setCursor({
line: insertPos.line,
ch: insertPos.ch + charBuffer.length
});
// Show hints
rEdit.showHint({ completeSingle: false });
} else if (!letters.test(data)) {
// Reset buffer on non-matching input (e.g., space, enter, etc.)
charBuffer = "";
}
});
rEdit.on('endCompletion', function () {
setTimeout(() => {
rEdit.focus();
}, 10); // small delay may help
});
function forceKeyboardOpen() {
const input = document.createElement("input");
input.type = "text";
input.style.position = "absolute";
input.style.opacity = "0";
input.style.height = "0";
input.style.width = "0";
input.style.border = "none";
input.style.top = "0";
input.style.left = "-9999";
input.style.padding = "0";
input.style.zIndex = "-1";
input.style.fontSize = "16px"; // prevents zoom on iOS
document.body.appendChild(input);
input.focus();
setTimeout(() => {
input.remove();
rEdit.focus(); // bring focus back to CodeMirror
}, 10);
}
}
});
// Function to trigger formatting
function triggerFormatting() {
let textarea;
if (confirmR) {
textarea = rEdit.getValue();
} else {
textarea = document.getElementById('rules').value;
}
const doc = rEdit.getDoc();
const scrollInfo = rEdit.getScrollInfo();
const cursor = doc.getCursor();
const currentLine = cursor.ch === 0 ? cursor.line - 1 : cursor.line;
const lineText = rEdit.getLine(currentLine) || "";
// Store initial text
let textarea = confirmR
? rEdit.getValue()
: document.getElementById('rules').value;
// Apply transformations
textarea = initalAutocorrection(textarea);
textarea = formatLogic(textarea);
if (confirmR) {
rEdit.setValue(textarea);
// Compute new cursor position
const newLine = currentLine;
const newCh = cursor.ch === 0 && lineText.length > 0
? lineText.length
: cursor.ch - 1;
rEdit.setCursor({ line: newLine, ch: newCh });
rEdit.scrollTo(scrollInfo.left, scrollInfo.top);
rEdit.focus();
rEdit.save();
}
else {
} else {
document.getElementById('rules').value = textarea;
}
}
@@ -350,6 +585,10 @@ function formatLogic(text) {
return line.trim().toLowerCase() === 'else';
}
function isElseif(line) {
return line.trim().toLowerCase().startsWith('elseif');
}
function isEndif(line) {
return line.trim().toLowerCase() === 'endif';
}
@@ -426,6 +665,7 @@ function formatLogic(text) {
}
if (isElse(trimmed)) {
console.log("Else found:", trimmed);
if (currentIfStack.length === 0) {
errors.push(`• Line ${i + 1}: "Else" without matching "If"`);
} else {
@@ -437,6 +677,19 @@ function formatLogic(text) {
continue;
}
if (isElseif(trimmed)) {
console.log("Elseif found:", trimmed);
if (currentIfStack.length === 0) {
errors.push(`• Line ${i + 1}: "Elseif" without matching "If"`);
} else {
indentLevel = Math.max(indentLevel - 1, 0);
}
result.push(INDENT.repeat(indentLevel) + trimmed);
indentLevel++;
continue;
}
if (isEndif(trimmed)) {
if (currentIfStack.length === 0) {
errors.push(`• Line ${i + 1}: "Endif" without matching "If"`);
@@ -468,10 +721,17 @@ function formatLogic(text) {
if (errors.length > 0) {
const firstErrorLine = extractFirstErrorLine(errors);
alert("Errors found:\n" + errors.join('\n'));
if (!isNaN(firstErrorLine) && confirmR) {
setTimeout(() => {
jumpToLine(firstErrorLine);
}, 50);
if (!isNaN(firstErrorLine)) {
if (confirmR) {
setTimeout(() => {
jumpToLine(firstErrorLine);
}, 50);
} else {
const textareaR = document.getElementById('rules');
setTimeout(() => {
jumpToLineInTextarea(textareaR, firstErrorLine);
}, 50);
}
}
}
@@ -501,6 +761,25 @@ function extractFirstErrorLine(errors) {
}
return null;
}
function jumpToLineInTextarea(textarea, lineNumber) {
const lines = textarea.value.split('\n');
const clampedLine = Math.max(1, Math.min(lineNumber, lines.length)); // Clamp to valid range
// Calculate the character offset to the start of the target line
let offset = 0;
for (let i = 0; i < clampedLine - 1; i++) {
offset += lines[i].length + 1; // +1 for the newline character
}
// Move cursor and scroll into view
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = offset;
// Scroll to the selection
textarea.scrollTop = textarea.scrollHeight; // Jump to bottom
textarea.scrollTop = textarea.scrollTop - textarea.clientHeight / 2; // Center around selection
}
//--------------------------------------------------------------------------------- end of formatting option
(function (mod) {
+1 -1
View File
File diff suppressed because one or more lines are too long