monaco-editor-core
Version:
A browser based code editor
453 lines (452 loc) • 20.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from '../../../base/common/strings.js';
import { getMapForWordSeparators } from '../core/wordCharacterClassifier.js';
import { Position } from '../core/position.js';
import { Range } from '../core/range.js';
import { FindMatch, SearchData } from '../model.js';
const LIMIT_FIND_COUNT = 999;
export class SearchParams {
constructor(searchString, isRegex, matchCase, wordSeparators) {
this.searchString = searchString;
this.isRegex = isRegex;
this.matchCase = matchCase;
this.wordSeparators = wordSeparators;
}
parseSearchRequest() {
if (this.searchString === '') {
return null;
}
// Try to create a RegExp out of the params
let multiline;
if (this.isRegex) {
multiline = isMultilineRegexSource(this.searchString);
}
else {
multiline = (this.searchString.indexOf('\n') >= 0);
}
let regex = null;
try {
regex = strings.createRegExp(this.searchString, this.isRegex, {
matchCase: this.matchCase,
wholeWord: false,
multiline: multiline,
global: true,
unicode: true
});
}
catch (err) {
return null;
}
if (!regex) {
return null;
}
let canUseSimpleSearch = (!this.isRegex && !multiline);
if (canUseSimpleSearch && this.searchString.toLowerCase() !== this.searchString.toUpperCase()) {
// casing might make a difference
canUseSimpleSearch = this.matchCase;
}
return new SearchData(regex, this.wordSeparators ? getMapForWordSeparators(this.wordSeparators, []) : null, canUseSimpleSearch ? this.searchString : null);
}
}
export function isMultilineRegexSource(searchString) {
if (!searchString || searchString.length === 0) {
return false;
}
for (let i = 0, len = searchString.length; i < len; i++) {
const chCode = searchString.charCodeAt(i);
if (chCode === 10 /* CharCode.LineFeed */) {
return true;
}
if (chCode === 92 /* CharCode.Backslash */) {
// move to next char
i++;
if (i >= len) {
// string ends with a \
break;
}
const nextChCode = searchString.charCodeAt(i);
if (nextChCode === 110 /* CharCode.n */ || nextChCode === 114 /* CharCode.r */ || nextChCode === 87 /* CharCode.W */) {
return true;
}
}
}
return false;
}
export function createFindMatch(range, rawMatches, captureMatches) {
if (!captureMatches) {
return new FindMatch(range, null);
}
const matches = [];
for (let i = 0, len = rawMatches.length; i < len; i++) {
matches[i] = rawMatches[i];
}
return new FindMatch(range, matches);
}
class LineFeedCounter {
constructor(text) {
const lineFeedsOffsets = [];
let lineFeedsOffsetsLen = 0;
for (let i = 0, textLen = text.length; i < textLen; i++) {
if (text.charCodeAt(i) === 10 /* CharCode.LineFeed */) {
lineFeedsOffsets[lineFeedsOffsetsLen++] = i;
}
}
this._lineFeedsOffsets = lineFeedsOffsets;
}
findLineFeedCountBeforeOffset(offset) {
const lineFeedsOffsets = this._lineFeedsOffsets;
let min = 0;
let max = lineFeedsOffsets.length - 1;
if (max === -1) {
// no line feeds
return 0;
}
if (offset <= lineFeedsOffsets[0]) {
// before first line feed
return 0;
}
while (min < max) {
const mid = min + ((max - min) / 2 >> 0);
if (lineFeedsOffsets[mid] >= offset) {
max = mid - 1;
}
else {
if (lineFeedsOffsets[mid + 1] >= offset) {
// bingo!
min = mid;
max = mid;
}
else {
min = mid + 1;
}
}
}
return min + 1;
}
}
export class TextModelSearch {
static findMatches(model, searchParams, searchRange, captureMatches, limitResultCount) {
const searchData = searchParams.parseSearchRequest();
if (!searchData) {
return [];
}
if (searchData.regex.multiline) {
return this._doFindMatchesMultiline(model, searchRange, new Searcher(searchData.wordSeparators, searchData.regex), captureMatches, limitResultCount);
}
return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount);
}
/**
* Multiline search always executes on the lines concatenated with \n.
* We must therefore compensate for the count of \n in case the model is CRLF
*/
static _getMultilineMatchRange(model, deltaOffset, text, lfCounter, matchIndex, match0) {
let startOffset;
let lineFeedCountBeforeMatch = 0;
if (lfCounter) {
lineFeedCountBeforeMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex);
startOffset = deltaOffset + matchIndex + lineFeedCountBeforeMatch /* add as many \r as there were \n */;
}
else {
startOffset = deltaOffset + matchIndex;
}
let endOffset;
if (lfCounter) {
const lineFeedCountBeforeEndOfMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex + match0.length);
const lineFeedCountInMatch = lineFeedCountBeforeEndOfMatch - lineFeedCountBeforeMatch;
endOffset = startOffset + match0.length + lineFeedCountInMatch /* add as many \r as there were \n */;
}
else {
endOffset = startOffset + match0.length;
}
const startPosition = model.getPositionAt(startOffset);
const endPosition = model.getPositionAt(endOffset);
return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
}
static _doFindMatchesMultiline(model, searchRange, searcher, captureMatches, limitResultCount) {
const deltaOffset = model.getOffsetAt(searchRange.getStartPosition());
// We always execute multiline search over the lines joined with \n
// This makes it that \n will match the EOL for both CRLF and LF models
// We compensate for offset errors in `_getMultilineMatchRange`
const text = model.getValueInRange(searchRange, 1 /* EndOfLinePreference.LF */);
const lfCounter = (model.getEOL() === '\r\n' ? new LineFeedCounter(text) : null);
const result = [];
let counter = 0;
let m;
searcher.reset(0);
while ((m = searcher.next(text))) {
result[counter++] = createFindMatch(this._getMultilineMatchRange(model, deltaOffset, text, lfCounter, m.index, m[0]), m, captureMatches);
if (counter >= limitResultCount) {
return result;
}
}
return result;
}
static _doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount) {
const result = [];
let resultLen = 0;
// Early case for a search range that starts & stops on the same line number
if (searchRange.startLineNumber === searchRange.endLineNumber) {
const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1, searchRange.endColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount);
return result;
}
// Collect results from first line
const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount);
// Collect results from middle lines
for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && resultLen < limitResultCount; lineNumber++) {
resultLen = this._findMatchesInLine(searchData, model.getLineContent(lineNumber), lineNumber, 0, resultLen, result, captureMatches, limitResultCount);
}
// Collect results from last line
if (resultLen < limitResultCount) {
const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount);
}
return result;
}
static _findMatchesInLine(searchData, text, lineNumber, deltaOffset, resultLen, result, captureMatches, limitResultCount) {
const wordSeparators = searchData.wordSeparators;
if (!captureMatches && searchData.simpleSearch) {
const searchString = searchData.simpleSearch;
const searchStringLen = searchString.length;
const textLength = text.length;
let lastMatchIndex = -searchStringLen;
while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) {
if (!wordSeparators || isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen)) {
result[resultLen++] = new FindMatch(new Range(lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset), null);
if (resultLen >= limitResultCount) {
return resultLen;
}
}
}
return resultLen;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
let m;
// Reset regex to search from the beginning
searcher.reset(0);
do {
m = searcher.next(text);
if (m) {
result[resultLen++] = createFindMatch(new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset), m, captureMatches);
if (resultLen >= limitResultCount) {
return resultLen;
}
}
} while (m);
return resultLen;
}
static findNextMatch(model, searchParams, searchStart, captureMatches) {
const searchData = searchParams.parseSearchRequest();
if (!searchData) {
return null;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
if (searchData.regex.multiline) {
return this._doFindNextMatchMultiline(model, searchStart, searcher, captureMatches);
}
return this._doFindNextMatchLineByLine(model, searchStart, searcher, captureMatches);
}
static _doFindNextMatchMultiline(model, searchStart, searcher, captureMatches) {
const searchTextStart = new Position(searchStart.lineNumber, 1);
const deltaOffset = model.getOffsetAt(searchTextStart);
const lineCount = model.getLineCount();
// We always execute multiline search over the lines joined with \n
// This makes it that \n will match the EOL for both CRLF and LF models
// We compensate for offset errors in `_getMultilineMatchRange`
const text = model.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, lineCount, model.getLineMaxColumn(lineCount)), 1 /* EndOfLinePreference.LF */);
const lfCounter = (model.getEOL() === '\r\n' ? new LineFeedCounter(text) : null);
searcher.reset(searchStart.column - 1);
const m = searcher.next(text);
if (m) {
return createFindMatch(this._getMultilineMatchRange(model, deltaOffset, text, lfCounter, m.index, m[0]), m, captureMatches);
}
if (searchStart.lineNumber !== 1 || searchStart.column !== 1) {
// Try again from the top
return this._doFindNextMatchMultiline(model, new Position(1, 1), searcher, captureMatches);
}
return null;
}
static _doFindNextMatchLineByLine(model, searchStart, searcher, captureMatches) {
const lineCount = model.getLineCount();
const startLineNumber = searchStart.lineNumber;
// Look in first line
const text = model.getLineContent(startLineNumber);
const r = this._findFirstMatchInLine(searcher, text, startLineNumber, searchStart.column, captureMatches);
if (r) {
return r;
}
for (let i = 1; i <= lineCount; i++) {
const lineIndex = (startLineNumber + i - 1) % lineCount;
const text = model.getLineContent(lineIndex + 1);
const r = this._findFirstMatchInLine(searcher, text, lineIndex + 1, 1, captureMatches);
if (r) {
return r;
}
}
return null;
}
static _findFirstMatchInLine(searcher, text, lineNumber, fromColumn, captureMatches) {
// Set regex to search from column
searcher.reset(fromColumn - 1);
const m = searcher.next(text);
if (m) {
return createFindMatch(new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length), m, captureMatches);
}
return null;
}
static findPreviousMatch(model, searchParams, searchStart, captureMatches) {
const searchData = searchParams.parseSearchRequest();
if (!searchData) {
return null;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
if (searchData.regex.multiline) {
return this._doFindPreviousMatchMultiline(model, searchStart, searcher, captureMatches);
}
return this._doFindPreviousMatchLineByLine(model, searchStart, searcher, captureMatches);
}
static _doFindPreviousMatchMultiline(model, searchStart, searcher, captureMatches) {
const matches = this._doFindMatchesMultiline(model, new Range(1, 1, searchStart.lineNumber, searchStart.column), searcher, captureMatches, 10 * LIMIT_FIND_COUNT);
if (matches.length > 0) {
return matches[matches.length - 1];
}
const lineCount = model.getLineCount();
if (searchStart.lineNumber !== lineCount || searchStart.column !== model.getLineMaxColumn(lineCount)) {
// Try again with all content
return this._doFindPreviousMatchMultiline(model, new Position(lineCount, model.getLineMaxColumn(lineCount)), searcher, captureMatches);
}
return null;
}
static _doFindPreviousMatchLineByLine(model, searchStart, searcher, captureMatches) {
const lineCount = model.getLineCount();
const startLineNumber = searchStart.lineNumber;
// Look in first line
const text = model.getLineContent(startLineNumber).substring(0, searchStart.column - 1);
const r = this._findLastMatchInLine(searcher, text, startLineNumber, captureMatches);
if (r) {
return r;
}
for (let i = 1; i <= lineCount; i++) {
const lineIndex = (lineCount + startLineNumber - i - 1) % lineCount;
const text = model.getLineContent(lineIndex + 1);
const r = this._findLastMatchInLine(searcher, text, lineIndex + 1, captureMatches);
if (r) {
return r;
}
}
return null;
}
static _findLastMatchInLine(searcher, text, lineNumber, captureMatches) {
let bestResult = null;
let m;
searcher.reset(0);
while ((m = searcher.next(text))) {
bestResult = createFindMatch(new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length), m, captureMatches);
}
return bestResult;
}
}
function leftIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) {
if (matchStartIndex === 0) {
// Match starts at start of string
return true;
}
const charBefore = text.charCodeAt(matchStartIndex - 1);
if (wordSeparators.get(charBefore) !== 0 /* WordCharacterClass.Regular */) {
// The character before the match is a word separator
return true;
}
if (charBefore === 13 /* CharCode.CarriageReturn */ || charBefore === 10 /* CharCode.LineFeed */) {
// The character before the match is line break or carriage return.
return true;
}
if (matchLength > 0) {
const firstCharInMatch = text.charCodeAt(matchStartIndex);
if (wordSeparators.get(firstCharInMatch) !== 0 /* WordCharacterClass.Regular */) {
// The first character inside the match is a word separator
return true;
}
}
return false;
}
function rightIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) {
if (matchStartIndex + matchLength === textLength) {
// Match ends at end of string
return true;
}
const charAfter = text.charCodeAt(matchStartIndex + matchLength);
if (wordSeparators.get(charAfter) !== 0 /* WordCharacterClass.Regular */) {
// The character after the match is a word separator
return true;
}
if (charAfter === 13 /* CharCode.CarriageReturn */ || charAfter === 10 /* CharCode.LineFeed */) {
// The character after the match is line break or carriage return.
return true;
}
if (matchLength > 0) {
const lastCharInMatch = text.charCodeAt(matchStartIndex + matchLength - 1);
if (wordSeparators.get(lastCharInMatch) !== 0 /* WordCharacterClass.Regular */) {
// The last character in the match is a word separator
return true;
}
}
return false;
}
export function isValidMatch(wordSeparators, text, textLength, matchStartIndex, matchLength) {
return (leftIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength)
&& rightIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength));
}
export class Searcher {
constructor(wordSeparators, searchRegex) {
this._wordSeparators = wordSeparators;
this._searchRegex = searchRegex;
this._prevMatchStartIndex = -1;
this._prevMatchLength = 0;
}
reset(lastIndex) {
this._searchRegex.lastIndex = lastIndex;
this._prevMatchStartIndex = -1;
this._prevMatchLength = 0;
}
next(text) {
const textLength = text.length;
let m;
do {
if (this._prevMatchStartIndex + this._prevMatchLength === textLength) {
// Reached the end of the line
return null;
}
m = this._searchRegex.exec(text);
if (!m) {
return null;
}
const matchStartIndex = m.index;
const matchLength = m[0].length;
if (matchStartIndex === this._prevMatchStartIndex && matchLength === this._prevMatchLength) {
if (matchLength === 0) {
// the search result is an empty string and won't advance `regex.lastIndex`, so `regex.exec` will stuck here
// we attempt to recover from that by advancing by two if surrogate pair found and by one otherwise
if (strings.getNextCodePoint(text, textLength, this._searchRegex.lastIndex) > 0xFFFF) {
this._searchRegex.lastIndex += 2;
}
else {
this._searchRegex.lastIndex += 1;
}
continue;
}
// Exit early if the regex matches the same range twice
return null;
}
this._prevMatchStartIndex = matchStartIndex;
this._prevMatchLength = matchLength;
if (!this._wordSeparators || isValidMatch(this._wordSeparators, text, textLength, matchStartIndex, matchLength)) {
return m;
}
} while (m);
return null;
}
}