From ad7e0356dd448fc62b5afb21284accc09debed48 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Fri, 15 Nov 2024 19:16:09 +0800 Subject: [PATCH 1/9] add: search/replace line break * highlight line break * search line break * replace line break --- src/edit_session.js | 9 +- src/ext/searchbox.js | 3 + src/search.js | 188 ++++++++++++++++++++++++++++++++-------- src/search_highlight.js | 26 ++++-- 4 files changed, 179 insertions(+), 47 deletions(-) diff --git a/src/edit_session.js b/src/edit_session.js index f9717aa4779..491a2eec29c 100644 --- a/src/edit_session.js +++ b/src/edit_session.js @@ -192,7 +192,7 @@ class EditSession { /** * Get "widgetManager" from EditSession - * + * * @returns {LineWidgets} object */ get widgetManager() { @@ -202,18 +202,18 @@ class EditSession { if (this.$editor) widgetManager.attach(this.$editor); - + return widgetManager; } /** * Set "widgetManager" in EditSession - * + * * @returns void */ set widgetManager(value) { Object.defineProperty(this, "widgetManager", { - writable: true, + writable: true, enumerable: true, configurable: true, value: value, @@ -2747,4 +2747,3 @@ config.defineOptions(EditSession.prototype, "session", { }); exports.EditSession = EditSession; - diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index e26a9003919..259832c23f4 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -214,6 +214,9 @@ class SearchBox { ? editor.session.getTextRange(this.searchRange) : editor.getValue(); + if (editor.$search.$isMultilineSearch(editor.$search.$options)) + value = value.replace(/\r\n|\r|\n/g, "\n"); + var offset = editor.session.doc.positionToIndex(editor.selection.anchor); if (this.searchRange) offset -= editor.session.doc.positionToIndex(this.searchRange.start); diff --git a/src/search.js b/src/search.js index 17a7c63222d..5766764f18f 100644 --- a/src/search.js +++ b/src/search.js @@ -116,11 +116,17 @@ class Search { row = row + len - 2; } } else { - for (var i = 0; i < lines.length; i++) { - var matches = lang.getMatchOffsets(lines[i], re); - for (var j = 0; j < matches.length; j++) { - var match = matches[j]; - ranges.push(new Range(i, match.offset, i, match.offset + match.length)); + for (var matches, i = 0; i < lines.length; i++) { + if (this.$isMultilineSearch(options)) { + matches = this.$multiLineForward(session, re, i, lines.length); + ranges.push(new Range(matches.startRow, matches.startCol, matches.endRow, matches.endCol)); + } + else { + matches = lang.getMatchOffsets(lines[i], re); + for (var j = 0; j < matches.length; j++) { + var match = matches[j]; + ranges.push(new Range(i, match.offset, i, match.offset + match.length)); + } } } } @@ -166,6 +172,9 @@ class Search { if (!re) return; + if (this.$isMultilineSearch(options)) + input = input.replace(/\r\n|\r|\n/g, "\n"); + var match = re.exec(input); if (!match || match[0].length != input.length) return null; @@ -248,6 +257,76 @@ class Search { return re; } + $isMultilineSearch(options) { + var regexp = typeof options.regExp === 'undefined' ? false : options.regExp; + return /\\r\\n|\\r|\\n/.test(options.re.source) && regexp && !options.$isMultiLine; + } + + $multiLineForward(session, re, start, last) { + var line, + chunk = 1; + + for (var row = start; row <= last;) { + for (var i = 0; i < chunk; i++) { + if (row > last) + break; + var next = session.getLine(row++); + line = line == null ? next : line + "\n" + next; + } + chunk = chunk * 2; + + var match = re.exec(line); + if (match) { + var before = line.slice(0, match.index).split("\n"); + var inside = match[0].split("\n"); + var startRow = start + before.length - 1; + var startCol = before[before.length - 1].length; + var endRow = startRow + inside.length - 1; + var endCol = inside.length == 1 ? startCol + inside[0].length : inside[inside.length - 1].length; + + return { + startRow: startRow, + startCol: startCol, + endRow: endRow, + endCol: endCol + }; + } + } + return false; + } + + $multiLineBackward(session, re, endIndex, start, first) { + var line, + chunk = 1, + endMargin = session.getLine(start).length - endIndex; + + for (var row = start; row >= first;) { + for (var i = 0; i < chunk && row >= first; i++) { + var next = session.getLine(row--); + line = line == null ? next : next + "\n" + line; + } + chunk = chunk * 2; + + var match = multiLineBackwardMatch(line, re, endMargin); + if (match) { + var before = line.slice(0, match.index).split("\n"); + var inside = match[0].split("\n"); + var startRow = row + before.length; + var startCol = before[before.length - 1].length; + var endRow = startRow + inside.length - 1; + var endCol = inside.length == 1 ? startCol + inside[0].length : inside[inside.length - 1].length; + + return { + startRow: startRow, + startCol: startCol, + endRow: endRow, + endCol: endCol + }; + } + } + return false; + } + /** * @param {EditSession} session */ @@ -255,6 +334,11 @@ class Search { var re = this.$assembleRegExp(options); if (!re) return false; + + var multiline = this.$isMultilineSearch(options); + var mtForward = this.$multiLineForward; + var mtBackward = this.$multiLineBackward; + var backwards = options.backwards == true; var skipCurrent = options.skipCurrent != false; var supportsUnicodeFlag = re.unicode; @@ -322,43 +406,61 @@ class Search { } else if (backwards) { var forEachInLine = function(row, endIndex, callback) { - var line = session.getLine(row); - var matches = []; - var m, last = 0; - re.lastIndex = 0; - while((m = re.exec(line))) { - var length = m[0].length; - last = m.index; - if (!length) { - if (last >= line.length) break; - re.lastIndex = last += lang.skipEmptyMatch(line, last, supportsUnicodeFlag); - } - if (m.index + length > endIndex) - break; - matches.push(m.index, length); - } - for (var i = matches.length - 1; i >= 0; i -= 2) { - var column = matches[i - 1]; - var length = matches[i]; - if (callback(row, column, row, column + length)) + if (multiline) { + var pos = mtBackward(session, re, endIndex, row, firstRow); + if (!pos) + return false; + if (callback(pos.startRow, pos.startCol, pos.endRow, pos.endCol)) return true; } + else { + var line = session.getLine(row); + var matches = []; + var m, last = 0; + re.lastIndex = 0; + while((m = re.exec(line))) { + var length = m[0].length; + last = m.index; + if (!length) { + if (last >= line.length) break; + re.lastIndex = last += lang.skipEmptyMatch(line, last, supportsUnicodeFlag); + } + if (m.index + length > endIndex) + break; + matches.push(m.index, length); + } + for (var i = matches.length - 1; i >= 0; i -= 2) { + var column = matches[i - 1]; + var length = matches[i]; + if (callback(row, column, row, column + length)) + return true; + } + } }; } else { var forEachInLine = function(row, startIndex, callback) { - var line = session.getLine(row); - var last; - var m; re.lastIndex = startIndex; - while((m = re.exec(line))) { - var length = m[0].length; - last = m.index; - if (callback(row, last, row,last + length)) + if (multiline) { + var pos = mtForward(session, re, row, lastRow); + if (!pos) + return false; + if (callback(pos.startRow, pos.startCol, pos.endRow, pos.endCol)) return true; - if (!length) { - re.lastIndex = last += lang.skipEmptyMatch(line, last, supportsUnicodeFlag); - if (last >= line.length) return false; + } + else { + var line = session.getLine(row); + var last; + var m; + while((m = re.exec(line))) { + var length = m[0].length; + last = m.index; + if (callback(row, last, row,last + length)) + return true; + if (!length) { + re.lastIndex = last += lang.skipEmptyMatch(line, last, supportsUnicodeFlag); + if (last >= line.length) return false; + } } } }; @@ -397,4 +499,22 @@ function addWordBoundary(needle, options) { return wordBoundary(firstChar) + needle + wordBoundary(lastChar, false); } +function multiLineBackwardMatch(line, re, endMargin) { + var match, + from = 0; + while (from <= line.length) { + re.lastIndex = from; + var newMatch = re.exec(line); + if (!newMatch) + break; + var end = newMatch.index + newMatch[0].length; + if (end > line.length - endMargin) + break; + if (!match || end > match.index + match[0].length) + match = newMatch; + from = newMatch.index + 1; + } + return match; +} + exports.Search = Search; diff --git a/src/search_highlight.js b/src/search_highlight.js index 6a5e864c74d..7d39e6ef06e 100644 --- a/src/search_highlight.js +++ b/src/search_highlight.js @@ -16,7 +16,7 @@ class SearchHighlight { this.clazz = clazz; this.type = type; } - + setRegexp(regExp) { if (this.regExp+"" == regExp+"") return; @@ -35,16 +35,27 @@ class SearchHighlight { return; var start = config.firstRow, end = config.lastRow; var renderedMarkerRanges = {}; + var _search = session.$editor.$search; + var multiline = _search.$isMultilineSearch(session.$editor.getLastSearchOptions()); for (var i = start; i <= end; i++) { var ranges = this.cache[i]; if (ranges == null) { - ranges = lang.getMatchOffsets(session.getLine(i), this.regExp); - if (ranges.length > this.MAX_RANGES) - ranges = ranges.slice(0, this.MAX_RANGES); - ranges = ranges.map(function(match) { - return new Range(i, match.offset, i, match.offset + match.length); - }); + if (multiline) { + ranges = []; + var matches = _search.$multiLineForward(session, this.regExp, i, end, true); + if (matches) ranges.push(new Range(matches.startRow, matches.startCol, matches.endRow, matches.endCol)); + if (ranges.length > this.MAX_RANGES) + ranges = ranges.slice(0, this.MAX_RANGES); + } + else { + ranges = lang.getMatchOffsets(session.getLine(i), this.regExp); + if (ranges.length > this.MAX_RANGES) + ranges = ranges.slice(0, this.MAX_RANGES); + ranges = ranges.map(function(match) { + return new Range(i, match.offset, i, match.offset + match.length); + }); + } this.cache[i] = ranges.length ? ranges : ""; } @@ -59,7 +70,6 @@ class SearchHighlight { } } } - } // needed to prevent long lines from freezing the browser From 2c5dbeb28346c3a8b91937d58cba665f9f9c7e3a Mon Sep 17 00:00:00 2001 From: Sekedus Date: Fri, 15 Nov 2024 20:02:18 +0800 Subject: [PATCH 2/9] search multiline match `\n+` --- src/search.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/search.js b/src/search.js index 5766764f18f..9214a96eae9 100644 --- a/src/search.js +++ b/src/search.js @@ -264,7 +264,7 @@ class Search { $multiLineForward(session, re, start, last) { var line, - chunk = 1; + chunk = chunkEnd(session, start); for (var row = start; row <= last;) { for (var i = 0; i < chunk; i++) { @@ -273,9 +273,9 @@ class Search { var next = session.getLine(row++); line = line == null ? next : line + "\n" + next; } - chunk = chunk * 2; var match = re.exec(line); + re.lastIndex = 0; if (match) { var before = line.slice(0, match.index).split("\n"); var inside = match[0].split("\n"); @@ -297,7 +297,7 @@ class Search { $multiLineBackward(session, re, endIndex, start, first) { var line, - chunk = 1, + chunk = chunkEnd(session, start), endMargin = session.getLine(start).length - endIndex; for (var row = start; row >= first;) { @@ -305,7 +305,6 @@ class Search { var next = session.getLine(row--); line = line == null ? next : next + "\n" + line; } - chunk = chunk * 2; var match = multiLineBackwardMatch(line, re, endMargin); if (match) { @@ -517,4 +516,14 @@ function multiLineBackwardMatch(line, re, endMargin) { return match; } +function chunkEnd(session, start) { + var base = 5000, + startPosition = { row: start, column: 0 }, + startIndex = session.doc.positionToIndex(startPosition), + targetIndex = startIndex + base, + targetPosition = session.doc.indexToPosition(targetIndex), + targetLine = targetPosition.row; + return targetLine + 1; +} + exports.Search = Search; From cdf2fdbce6a9a3bc4efc3821585873c9e8c48491 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Sun, 17 Nov 2024 13:38:10 +0800 Subject: [PATCH 3/9] fix: replace with `\n` and `\t` Replace `.ace_search_form input` value with `\n` and `\t` using regex. --- src/ext/searchbox.js | 2 +- src/search.js | 78 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index 259832c23f4..cb299af0b4b 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -214,7 +214,7 @@ class SearchBox { ? editor.session.getTextRange(this.searchRange) : editor.getValue(); - if (editor.$search.$isMultilineSearch(editor.$search.$options)) + if (editor.$search.$isMultilineSearch(editor.getLastSearchOptions())) value = value.replace(/\r\n|\r|\n/g, "\n"); var offset = editor.session.doc.positionToIndex(editor.selection.anchor); diff --git a/src/search.js b/src/search.js index 9214a96eae9..8f12fd978d8 100644 --- a/src/search.js +++ b/src/search.js @@ -152,6 +152,74 @@ class Search { return ranges; } + parseReplaceString(input, replaceString) { + var CharCode = { + DollarSign: 36, + Ampersand: 38, + Digit0: 48, + Digit1: 49, + Digit9: 57, + Backslash: 92, + n: 110, + t: 116 + }; + + var replacement = ''; + for (var i = 0, len = replaceString.length; i < len; i++) { + var chCode = replaceString.charCodeAt(i); + if (chCode === CharCode.Backslash) { + // move to next char + i++; + if (i >= len) { + // string ends with a \ + break; + } + var nextChCode = replaceString.charCodeAt(i); + switch (nextChCode) { + case CharCode.Backslash: + // \\ => inserts a "\" + replacement = '\\'; + break; + case CharCode.n: + // \n => inserts a LF + replacement= '\n'; + break; + case CharCode.t: + // \t => inserts a TAB + replacement = '\t'; + break; + } + continue; + } + + if (chCode === CharCode.DollarSign) { + // move to next char + i++; + if (i >= len) { + // string ends with a $ + break; + } + const nextChCode = replaceString.charCodeAt(i); + if (nextChCode === CharCode.DollarSign) { + // $$ => inserts a "$" + replacement = '$'; + continue; + } + if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) { + // $& and $0 => inserts the matched substring. + replacement = input; + continue; + } + if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) { + // $n + replacement = replaceString; + continue; + } + } + } + return replacement; + } + /** * Searches for `options.needle` in `input`, and, if found, replaces it with `replacement`. * @param {String} input The text to search in @@ -176,10 +244,11 @@ class Search { input = input.replace(/\r\n|\r|\n/g, "\n"); var match = re.exec(input); - if (!match || match[0].length != input.length) + if (!match || (!this.$isMultilineSearch(options) && match[0].length != input.length)) return null; - if (!options.regExp) { - replacement = replacement.replace(/\$/g, "$$$$"); + + if (options.regExp) { + replacement = this.parseReplaceString(input, replacement); } replacement = input.replace(re, replacement); @@ -258,8 +327,7 @@ class Search { } $isMultilineSearch(options) { - var regexp = typeof options.regExp === 'undefined' ? false : options.regExp; - return /\\r\\n|\\r|\\n/.test(options.re.source) && regexp && !options.$isMultiLine; + return options.re && /\\r\\n|\\r|\\n/.test(options.re.source) && options.regExp && !options.$isMultiLine; } $multiLineForward(session, re, start, last) { From f60060dbca689738452f916c895eb87e998b46e7 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Tue, 19 Nov 2024 06:19:57 +0800 Subject: [PATCH 4/9] searchBox: auto update & escape regex - Auto updates "updateCounter" and "ace_nomatch" in the search box when "onDocumentChange" - If regex mode is enabled, fill the "searchInput" field with "escapeRegExp" --- src/editor.js | 4 ++++ src/ext/searchbox.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/editor.js b/src/editor.js index db888515deb..05fa6db33d4 100644 --- a/src/editor.js +++ b/src/editor.js @@ -617,6 +617,10 @@ class Editor { // Update cursor because tab characters can influence the cursor position. this.$cursorChange(); + + // Updates "updateCounter" and "ace_nomatch" in the search box + if (this.searchBox && this.searchBox.active === true) + this.searchBox.find(false, false, true); } /** diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index cb299af0b4b..75e0c495994 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -293,6 +293,9 @@ class SearchBox { this.element.style.display = ""; this.replaceOption.checked = isReplace; + if (this.editor.$search.$options.regExp) + value = lang.escapeRegExp(value); + if (value) this.searchInput.value = value; From d530af2450a79f6b9f647f7242c28f7e6bdfdf19 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Thu, 21 Nov 2024 20:05:13 +0800 Subject: [PATCH 5/9] fix and add comments --- src/ext/searchbox.js | 4 ++ src/search.js | 85 ++++++++++++++++++++++++----------------- src/search_highlight.js | 13 +++++-- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index 75e0c495994..7c8fe5e6957 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -214,6 +214,10 @@ class SearchBox { ? editor.session.getTextRange(this.searchRange) : editor.getValue(); + /** + * Convert all line ending variations to Unix-style = \n + * Windows (\r\n), MacOS Classic (\r), and Unix (\n) + */ if (editor.$search.$isMultilineSearch(editor.getLastSearchOptions())) value = value.replace(/\r\n|\r|\n/g, "\n"); diff --git a/src/search.js b/src/search.js index 8f12fd978d8..613560a8c5d 100644 --- a/src/search.js +++ b/src/search.js @@ -116,10 +116,15 @@ class Search { row = row + len - 2; } } else { - for (var matches, i = 0; i < lines.length; i++) { + for (var matches, i = 0, lng = lines.length; i < lng; i++) { if (this.$isMultilineSearch(options)) { - matches = this.$multiLineForward(session, re, i, lines.length); - ranges.push(new Range(matches.startRow, matches.startCol, matches.endRow, matches.endCol)); + matches = this.$multiLineForward(session, re, i, lng); + if (matches) { + var end_row = matches.endRow <= lng ? matches.endRow - 1 : lng; + if (end_row > i) + i = end_row; + ranges.push(new Range(matches.startRow, matches.startCol, matches.endRow, matches.endCol)); + } } else { matches = lang.getMatchOffsets(lines[i], re); @@ -152,13 +157,11 @@ class Search { return ranges; } - parseReplaceString(input, replaceString) { + parseReplaceString(replaceString) { var CharCode = { DollarSign: 36, Ampersand: 38, Digit0: 48, - Digit1: 49, - Digit9: 57, Backslash: 92, n: 110, t: 116 @@ -205,19 +208,15 @@ class Search { replacement = '$'; continue; } - if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) { - // $& and $0 => inserts the matched substring. - replacement = input; - continue; - } - if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) { - // $n - replacement = replaceString; + if (nextChCode === CharCode.Digit0) { + // replace $0 to $&, making it compatible with JavaScript + // $0 and $& => inserts the matched substring. + replaceString = replaceString.replace(/\$0/, '$$&'); continue; } } } - return replacement; + return replacement || replaceString; } /** @@ -240,16 +239,21 @@ class Search { if (!re) return; - if (this.$isMultilineSearch(options)) + /** + * Convert all line ending variations to Unix-style = \n + * Windows (\r\n), MacOS Classic (\r), and Unix (\n) + */ + var mtSearch = this.$isMultilineSearch(options); + if (mtSearch) input = input.replace(/\r\n|\r|\n/g, "\n"); var match = re.exec(input); - if (!match || (!this.$isMultilineSearch(options) && match[0].length != input.length)) + if (!match || (!mtSearch && match[0].length != input.length)) return null; - if (options.regExp) { - replacement = this.parseReplaceString(input, replacement); - } + replacement = options.regExp + ? this.parseReplaceString(replacement) + : replacement.replace(/\$/g, "$$$$"); replacement = input.replace(re, replacement); if (options.preserveCase) { @@ -345,12 +349,14 @@ class Search { var match = re.exec(line); re.lastIndex = 0; if (match) { - var before = line.slice(0, match.index).split("\n"); - var inside = match[0].split("\n"); - var startRow = start + before.length - 1; - var startCol = before[before.length - 1].length; - var endRow = startRow + inside.length - 1; - var endCol = inside.length == 1 ? startCol + inside[0].length : inside[inside.length - 1].length; + var beforeMatch = line.slice(0, match.index).split("\n"); + var matchedText = match[0].split("\n"); + var startRow = start + beforeMatch.length - 1; + var startCol = beforeMatch[beforeMatch.length - 1].length; + var endRow = startRow + matchedText.length - 1; + var endCol = matchedText.length == 1 + ? startCol + matchedText[0].length + : matchedText[matchedText.length - 1].length; return { startRow: startRow, @@ -376,12 +382,14 @@ class Search { var match = multiLineBackwardMatch(line, re, endMargin); if (match) { - var before = line.slice(0, match.index).split("\n"); - var inside = match[0].split("\n"); - var startRow = row + before.length; - var startCol = before[before.length - 1].length; - var endRow = startRow + inside.length - 1; - var endCol = inside.length == 1 ? startCol + inside[0].length : inside[inside.length - 1].length; + var beforeMatch = line.slice(0, match.index).split("\n"); + var matchedText = match[0].split("\n"); + var startRow = row + beforeMatch.length; + var startCol = beforeMatch[beforeMatch.length - 1].length; + var endRow = startRow + matchedText.length - 1; + var endCol = matchedText.length == 1 + ? startCol + matchedText[0].length + : matchedText[matchedText.length - 1].length; return { startRow: startRow, @@ -402,7 +410,7 @@ class Search { if (!re) return false; - var multiline = this.$isMultilineSearch(options); + var mtSearch = this.$isMultilineSearch(options); var mtForward = this.$multiLineForward; var mtBackward = this.$multiLineBackward; @@ -473,7 +481,7 @@ class Search { } else if (backwards) { var forEachInLine = function(row, endIndex, callback) { - if (multiline) { + if (mtSearch) { var pos = mtBackward(session, re, endIndex, row, firstRow); if (!pos) return false; @@ -508,8 +516,13 @@ class Search { else { var forEachInLine = function(row, startIndex, callback) { re.lastIndex = startIndex; - if (multiline) { + if (mtSearch) { var pos = mtForward(session, re, row, lastRow); + if (pos) { + var end_row = pos.endRow <= lastRow ? pos.endRow - 1 : lastRow; + if (end_row > row) + row = end_row; + } if (!pos) return false; if (callback(pos.startRow, pos.startCol, pos.endRow, pos.endCol)) @@ -522,7 +535,7 @@ class Search { while((m = re.exec(line))) { var length = m[0].length; last = m.index; - if (callback(row, last, row,last + length)) + if (callback(row, last, row, last + length)) return true; if (!length) { re.lastIndex = last += lang.skipEmptyMatch(line, last, supportsUnicodeFlag); diff --git a/src/search_highlight.js b/src/search_highlight.js index 7d39e6ef06e..f1a4410e646 100644 --- a/src/search_highlight.js +++ b/src/search_highlight.js @@ -36,15 +36,20 @@ class SearchHighlight { var start = config.firstRow, end = config.lastRow; var renderedMarkerRanges = {}; var _search = session.$editor.$search; - var multiline = _search.$isMultilineSearch(session.$editor.getLastSearchOptions()); + var mtSearch = _search.$isMultilineSearch(session.$editor.getLastSearchOptions()); for (var i = start; i <= end; i++) { var ranges = this.cache[i]; if (ranges == null) { - if (multiline) { + if (mtSearch) { ranges = []; - var matches = _search.$multiLineForward(session, this.regExp, i, end, true); - if (matches) ranges.push(new Range(matches.startRow, matches.startCol, matches.endRow, matches.endCol)); + var match = _search.$multiLineForward(session, this.regExp, i, end); + if (match) { + var end_row = match.endRow <= end ? match.endRow - 1 : end; + if (end_row > i) + i = end_row; + ranges.push(new Range(match.startRow, match.startCol, match.endRow, match.endCol)); + } if (ranges.length > this.MAX_RANGES) ranges = ranges.slice(0, this.MAX_RANGES); } From 318dd9a6f4f66d00d82b65bcc14cd82a596b280f Mon Sep 17 00:00:00 2001 From: Sekedus Date: Fri, 22 Nov 2024 09:58:06 +0800 Subject: [PATCH 6/9] fix: search box `updateCounter` --- src/ext/searchbox.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index 7c8fe5e6957..22381028cd8 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -218,8 +218,10 @@ class SearchBox { * Convert all line ending variations to Unix-style = \n * Windows (\r\n), MacOS Classic (\r), and Unix (\n) */ - if (editor.$search.$isMultilineSearch(editor.getLastSearchOptions())) + if (editor.$search.$isMultilineSearch(editor.getLastSearchOptions())) { value = value.replace(/\r\n|\r|\n/g, "\n"); + editor.session.doc.$autoNewLine = "\n"; + } var offset = editor.session.doc.positionToIndex(editor.selection.anchor); if (this.searchRange) From cfb17ca5dabf6b95350cc7eb13f69e7f0d6e9097 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Sat, 23 Nov 2024 07:52:07 +0800 Subject: [PATCH 7/9] fix and add test --- src/search.js | 26 ++++++--- src/search_test.js | 130 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 134 insertions(+), 22 deletions(-) diff --git a/src/search.js b/src/search.js index 613560a8c5d..c7549fafb49 100644 --- a/src/search.js +++ b/src/search.js @@ -116,8 +116,9 @@ class Search { row = row + len - 2; } } else { - for (var matches, i = 0, lng = lines.length; i < lng; i++) { + for (var matches, i = 0; i < lines.length; i++) { if (this.$isMultilineSearch(options)) { + var lng = lines.length - 1; matches = this.$multiLineForward(session, re, i, lng); if (matches) { var end_row = matches.endRow <= lng ? matches.endRow - 1 : lng; @@ -162,6 +163,8 @@ class Search { DollarSign: 36, Ampersand: 38, Digit0: 48, + Digit1: 49, + Digit9: 57, Backslash: 92, n: 110, t: 116 @@ -175,21 +178,22 @@ class Search { i++; if (i >= len) { // string ends with a \ + replacement += "\\"; break; } var nextChCode = replaceString.charCodeAt(i); switch (nextChCode) { case CharCode.Backslash: // \\ => inserts a "\" - replacement = '\\'; + replacement += "\\"; break; case CharCode.n: // \n => inserts a LF - replacement= '\n'; + replacement += "\n"; break; case CharCode.t: // \t => inserts a TAB - replacement = '\t'; + replacement += "\t"; break; } continue; @@ -200,21 +204,29 @@ class Search { i++; if (i >= len) { // string ends with a $ + replacement += "$"; break; } const nextChCode = replaceString.charCodeAt(i); if (nextChCode === CharCode.DollarSign) { // $$ => inserts a "$" - replacement = '$'; + replacement += "$$"; continue; } - if (nextChCode === CharCode.Digit0) { + if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) { // replace $0 to $&, making it compatible with JavaScript // $0 and $& => inserts the matched substring. - replaceString = replaceString.replace(/\$0/, '$$&'); + replacement += "$&"; + continue; + } + if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) { + // $n + replacement += "$" + replaceString[i]; continue; } } + + replacement += replaceString[i]; } return replacement || replaceString; } diff --git a/src/search_test.js b/src/search_test.js index 0ac18959e7d..f1e7674d3c0 100644 --- a/src/search_test.js +++ b/src/search_test.js @@ -153,7 +153,7 @@ module.exports = { var range = search.find(session); assert.position(range.start, 0, 12); assert.position(range.end, 0, 13); - + search.set({ needle: "ab\\{2}" }); range = search.find(session); assert.position(range.start, 1, 8); @@ -369,8 +369,8 @@ module.exports = { assert.position(ranges[1].start, 2, 1); assert.position(ranges[1].end, 2, 3); }, - - + + "test: find all multiline matches" : function() { var session = new EditSession(["juhu", "juhu", "juhu", "juhu"]); @@ -420,13 +420,17 @@ module.exports = { "test: replace with RegExp match and capture groups" : function() { var search = new Search().set({ - needle: "ab(\\d\\d)", + needle: "ab((\\d)\\d)", regExp: true }); assert.equal(search.replace("ab12", "cd$1"), "cd12"); + assert.equal(search.replace("ab56", "pr$17$2"), "pr5675"); assert.equal(search.replace("ab12", "-$&-"), "-ab12-"); - assert.equal(search.replace("ab12", "$$"), "$"); + assert.equal(search.replace("ab12", "_$0_"), "_ab12_"); + + search.set({ needle: "(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)" }); + assert.equal(search.replace("abcdefghijkl", "$2$9$7_$11$9$4_$8$9$4$5_$10$1$3$11_$6$12$1$7_$13"), "big_kid_hide_jack_flag_a3"); }, "test: replace() should correctly handle $$ in the replacement string": function () { @@ -435,11 +439,11 @@ module.exports = { }); // Expecting $$ to be preserved in the output + assert.equal(search.replace("example", "$test"), "$test"); assert.equal(search.replace("example", "$$test"), "$$test"); - - // Expecting $$$$ to be preserved as $$$$ + assert.equal(search.replace("example", "$$$test"), "$$$test"); assert.equal(search.replace("example", "$$$$test"), "$$$$test"); - + search.set({ regExp: true, needle: "(example)" @@ -448,11 +452,40 @@ module.exports = { // Tests that $1 is replaced by the text that matches the capturing group. assert.equal(search.replace("example", "$1test"), "exampletest"); - search.set({regExp: false}); + assert.equal(search.replace("example", "$"), "$"); + assert.equal(search.replace("example", "$$"), "$"); + assert.equal(search.replace("example", "$$$"), "$$"); + assert.equal(search.replace("example", "$$$$"), "$$"); + assert.equal(search.replace("example", "$$$$$"), "$$$"); + assert.equal(search.replace("example", "$$$$$$"), "$$$"); + assert.equal(search.replace("example", "$$$$$$$"), "$$$$"); + + search.set({ regExp: false }); // Tests that without regular expression, "$1test" is treated as a literal string with $ escape. assert.equal(search.replace("(example)", "$1test"), "$1test"); }, + "test: replace() should correctly handle \\\\ in the replacement string": function () { + var search = new Search().set({ + needle: "example" + }); + + // Expecting \\ to be preserved in the output + assert.equal(search.replace("example", "\\test"), "\\test"); + assert.equal(search.replace("example", "\\\\test"), "\\\\test"); + assert.equal(search.replace("example", "\\\\\\test"), "\\\\\\test"); + + search.set({ regExp: true }); + + assert.equal(search.replace("example", "\\"), "\\"); + assert.equal(search.replace("example", "\\\\"), "\\"); + assert.equal(search.replace("example", "\\\\\\"), "\\\\"); + assert.equal(search.replace("example", "\\\\\\\\"), "\\\\"); + assert.equal(search.replace("example", "\\\\\\\\\\"), "\\\\\\"); + assert.equal(search.replace("example", "\\\\\\\\\\\\"), "\\\\\\"); + assert.equal(search.replace("example", "\\\\\\\\\\\\\\"), "\\\\\\\\"); + }, + "test: find all using regular expresion containing $" : function() { var session = new EditSession(["a", " b", "c ", "d"]); @@ -518,7 +551,7 @@ module.exports = { "test: find next empty range" : function() { var session = new EditSession("foo foobar foo"); var editor = new Editor(new MockRenderer(), session); - + var options = { needle: "o*", wrap: true, @@ -526,7 +559,7 @@ module.exports = { backwards: false }; var positions = [4, 5.2, 7, 8, 9, 10, 11, 12.2, 14, 0, 1.2, 3]; - + session.selection.moveCursorTo(0, 3); for (var i = 0; i < positions.length; i++) { editor.find(options); @@ -545,10 +578,11 @@ module.exports = { assert.equal(start + 0.1 * len, positions[i]); } }, + "test: repeating text": function() { var session = new EditSession("tttttt\ntttttt\ntttttt\ntttttt\ntttttt\ntttttt"); var editor = new Editor(new MockRenderer(), session); - + var options = { needle: "^", wrap: true, @@ -560,15 +594,15 @@ module.exports = { var range = editor.selection.getRange(); assert.range(range, sl, sc, el, ec); } - + session.selection.moveCursorTo(1, 3); check(2, 0, 2, 0); - + options.needle = "tttt\ntttt"; check(2, 2, 3, 4); check(4, 2, 5, 4); check(0, 2, 1, 4); - + options.backwards = true; check(4, 2, 5, 4); check(2, 2, 3, 4); @@ -601,6 +635,72 @@ module.exports = { assert.position(ranges[1].end, 1, 39); assert.position(ranges[2].start, 2, 4); assert.position(ranges[2].end, 2, 7); + }, + + "test: find all line breaks (\\r\\n, \\n) using regular expression" : function() { + var session = new EditSession('\nfunction foo(items, nada) {\n for (var i=0; i Date: Tue, 26 Nov 2024 07:00:56 +0800 Subject: [PATCH 8/9] fix build --- src/search.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/search.js b/src/search.js index c7549fafb49..8903847aea4 100644 --- a/src/search.js +++ b/src/search.js @@ -378,7 +378,7 @@ class Search { }; } } - return false; + return null; } $multiLineBackward(session, re, endIndex, start, first) { @@ -411,7 +411,7 @@ class Search { }; } } - return false; + return null; } /** From b9ca36914cf2a52673cd48f27e1dab2439b26292 Mon Sep 17 00:00:00 2001 From: Sekedus Date: Fri, 29 Nov 2024 06:48:25 +0800 Subject: [PATCH 9/9] fix: codecov missing coverage --- src/editor.js | 4 ---- src/ext/searchbox.js | 8 ++++++++ src/search.js | 4 ++-- src/search_highlight.js | 9 +++++++-- src/search_test.js | 2 -- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/editor.js b/src/editor.js index 05fa6db33d4..db888515deb 100644 --- a/src/editor.js +++ b/src/editor.js @@ -617,10 +617,6 @@ class Editor { // Update cursor because tab characters can influence the cursor position. this.$cursorChange(); - - // Updates "updateCounter" and "ace_nomatch" in the search box - if (this.searchBox && this.searchBox.active === true) - this.searchBox.find(false, false, true); } /** diff --git a/src/ext/searchbox.js b/src/ext/searchbox.js index 22381028cd8..2a34ecce978 100644 --- a/src/ext/searchbox.js +++ b/src/ext/searchbox.js @@ -51,6 +51,7 @@ class SearchBox { this.element = div.firstChild; this.setSession = this.setSession.bind(this); + this.$onEditorInput = this.onEditorInput.bind(this); this.$init(); this.setEditor(editor); @@ -72,6 +73,11 @@ class SearchBox { this.$syncOptions(true); } + // Auto update "updateCounter" and "ace_nomatch" + onEditorInput() { + this.find(false, false, true); + } + /** * @param {HTMLElement} sb */ @@ -283,6 +289,7 @@ class SearchBox { this.active = false; this.setSearchRange(null); this.editor.off("changeSession", this.setSession); + this.editor.off("input", this.$onEditorInput); this.element.style.display = "none"; this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb); @@ -296,6 +303,7 @@ class SearchBox { show(value, isReplace) { this.active = true; this.editor.on("changeSession", this.setSession); + this.editor.on("input", this.$onEditorInput); this.element.style.display = ""; this.replaceOption.checked = isReplace; diff --git a/src/search.js b/src/search.js index 8903847aea4..93bce2a943c 100644 --- a/src/search.js +++ b/src/search.js @@ -592,8 +592,8 @@ function addWordBoundary(needle, options) { } function multiLineBackwardMatch(line, re, endMargin) { - var match, - from = 0; + var match = null; + var from = 0; while (from <= line.length) { re.lastIndex = from; var newMatch = re.exec(line); diff --git a/src/search_highlight.js b/src/search_highlight.js index f1a4410e646..cd9f0f643c1 100644 --- a/src/search_highlight.js +++ b/src/search_highlight.js @@ -15,6 +15,7 @@ class SearchHighlight { this.setRegexp(regExp); this.clazz = clazz; this.type = type; + this.docLen = 0; } setRegexp(regExp) { @@ -33,14 +34,15 @@ class SearchHighlight { update(html, markerLayer, session, config) { if (!this.regExp) return; - var start = config.firstRow, end = config.lastRow; + var start = config.firstRow; + var end = config.lastRow; var renderedMarkerRanges = {}; var _search = session.$editor.$search; var mtSearch = _search.$isMultilineSearch(session.$editor.getLastSearchOptions()); for (var i = start; i <= end; i++) { var ranges = this.cache[i]; - if (ranges == null) { + if (ranges == null || session.getValue().length != this.docLen) { if (mtSearch) { ranges = []; var match = _search.$multiLineForward(session, this.regExp, i, end); @@ -64,6 +66,8 @@ class SearchHighlight { this.cache[i] = ranges.length ? ranges : ""; } + if (ranges.length === 0) continue; + for (var j = ranges.length; j --; ) { var rangeToAddMarkerTo = ranges[j].toScreenRange(session); var rangeAsString = rangeToAddMarkerTo.toString(); @@ -74,6 +78,7 @@ class SearchHighlight { html, rangeToAddMarkerTo, this.clazz, config); } } + this.docLen = session.getValue().length; } } diff --git a/src/search_test.js b/src/search_test.js index f1e7674d3c0..de9c6ef238f 100644 --- a/src/search_test.js +++ b/src/search_test.js @@ -684,8 +684,6 @@ module.exports = { }, "test: replace with line breaks (\\n) and TAB (\\t) using regular expression" : function() { - var session = new EditSession('\nfunction foo(items, nada) {\n for (var i=0; i