-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Multiline Search Support: line breaks \n
#5675
base: master
Are you sure you want to change the base?
Changes from 4 commits
ad7e035
2c5dbeb
cdf2fdb
f60060d
d530af2
318dd9a
cfb17ca
45851b9
b9ca369
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 as the previous case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ACK. |
||
} | ||
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)); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -146,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 | ||
|
@@ -166,11 +240,15 @@ class Search { | |
if (!re) | ||
return; | ||
|
||
if (this.$isMultilineSearch(options)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are used at 2 places should we create another method as this looks like the reusable code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ACK. |
||
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); | ||
|
@@ -248,13 +326,86 @@ class Search { | |
return re; | ||
} | ||
|
||
$isMultilineSearch(options) { | ||
return options.re && /\\r\\n|\\r|\\n/.test(options.re.source) && options.regExp && !options.$isMultiLine; | ||
nlujjawal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
$multiLineForward(session, re, start, last) { | ||
var line, | ||
chunk = chunkEnd(session, start); | ||
|
||
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; | ||
} | ||
|
||
var match = re.exec(line); | ||
re.lastIndex = 0; | ||
if (match) { | ||
var before = line.slice(0, match.index).split("\n"); | ||
nlujjawal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 = chunkEnd(session, start), | ||
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; | ||
} | ||
|
||
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 | ||
*/ | ||
$matchIterator(session, options) { | ||
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 +473,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we reset regex lastIndex above this line just to reset it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, see: https://stackoverflow.com/a/4724920 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I wasn’t focused earlier. No need, because it’s already set in |
||
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); | ||
nlujjawal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 +566,32 @@ 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; | ||
} | ||
|
||
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please move this to an input event handler in searchbox, something like
editor.on('input', this.$onEditorInput)
and remove the listener when deactivating.onDocumentChange
can be called multiple times during one edit, and input event debounces that with a timeout.Also in one of tests add a scenario of changing editor value, e.g.
to prevent coverlay complaining about untested lines