diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index 93f1eb9cc40..18f40e21b0b 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { FindMatch } from 'vs/editor/common/editorCommon'; +import { FindMatch, EndOfLinePreference } from 'vs/editor/common/editorCommon'; import { CharCode } from 'vs/base/common/charCode'; import { TextModel } from 'vs/editor/common/model/textModel'; @@ -112,9 +112,50 @@ export class TextModelSearch { return this._doFindMatchesLineByLine(model, searchRange, regex, 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 + */ + private static _getMultilineMatchRange(model: TextModel, deltaOffset: number, text: string, matchIndex: number, match0: string): Range { + let startOffset: number; + if (model.getEOL() === '\r\n') { + let lineFeedCountBeforeMatch = 0; + for (let i = 0; i < matchIndex; i++) { + let chCode = text.charCodeAt(i); + if (chCode === CharCode.LineFeed) { + lineFeedCountBeforeMatch++; + } + } + startOffset = deltaOffset + matchIndex + lineFeedCountBeforeMatch /* add as many \r as there were \n */; + } else { + startOffset = deltaOffset + matchIndex; + } + + let endOffset: number; + if (model.getEOL() === '\r\n') { + let lineFeedCountInMatch = 0; + for (let i = 0, len = match0.length; i < len; i++) { + let chCode = text.charCodeAt(i + matchIndex); + if (chCode === CharCode.LineFeed) { + lineFeedCountInMatch++; + } + } + 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); + } + private static _doFindMatchesMultiline(model: TextModel, searchRange: Range, searchRegex: RegExp, captureMatches: boolean, limitResultCount: number): FindMatch[] { const deltaOffset = model.getOffsetAt(searchRange.getStartPosition()); - const text = model.getValueInRange(searchRange); + // 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, EndOfLinePreference.LF); const result: FindMatch[] = []; let prevStartOffset = 0; @@ -131,11 +172,8 @@ export class TextModelSearch { return result; } - const startPosition = model.getPositionAt(startOffset); - const endPosition = model.getPositionAt(endOffset); - result[counter++] = createFindMatch( - new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), + this._getMultilineMatchRange(model, deltaOffset, text, m.index, m[0]), m, captureMatches ); @@ -221,22 +259,21 @@ export class TextModelSearch { const searchTextStart = new Position(searchStart.lineNumber, 1); const deltaOffset = model.getOffsetAt(searchTextStart); const lineCount = model.getLineCount(); - const text = model.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, lineCount, model.getLineMaxColumn(lineCount))); + // 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)), EndOfLinePreference.LF); searchRegex.lastIndex = searchStart.column - 1; let m = searchRegex.exec(text); if (m) { - const startOffset = deltaOffset + m.index; - const endOffset = startOffset + m[0].length; - const startPosition = model.getPositionAt(startOffset); - const endPosition = model.getPositionAt(endOffset); return createFindMatch( - new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), + this._getMultilineMatchRange(model, deltaOffset, text, m.index, m[0]), m, captureMatches ); } - if (searchStart.lineNumber !== 1 || searchStart.column !== -1) { + if (searchStart.lineNumber !== 1 || searchStart.column !== 1) { // Try again from the top return this._doFindNextMatchMultiline(model, new Position(1, 1), searchRegex, captureMatches); } diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index e4aaea8e273..7352f9f3644 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; -import { FindMatch } from 'vs/editor/common/editorCommon'; +import { FindMatch, EndOfLineSequence } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; @@ -18,13 +18,7 @@ suite('TextModelSearch', () => { assert.deepEqual(actual, new FindMatch(expectedRange, expectedMatches)); } - function assertFindMatches(text: string, searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, _expected: [number, number, number, number][]): void { - let expectedRanges = _expected.map(entry => new Range(entry[0], entry[1], entry[2], entry[3])); - let expectedMatches = expectedRanges.map(entry => new FindMatch(entry, null)); - let model = new TextModel([], TextModel.toRawText(text, TextModel.DEFAULT_CREATION_OPTIONS)); - - let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); - + function _assertFindMatches(model: TextModel, searchParams: SearchParams, expectedMatches: FindMatch[]): void { let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), false, 1000); assert.deepEqual(actual, expectedMatches, 'findMatches OK'); @@ -47,8 +41,22 @@ suite('TextModelSearch', () => { match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); assert.deepEqual(match, expectedMatches[i], `findPrevMatch ${startPos}`); } + } + function assertFindMatches(text: string, searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, _expected: [number, number, number, number][]): void { + let expectedRanges = _expected.map(entry => new Range(entry[0], entry[1], entry[2], entry[3])); + let expectedMatches = expectedRanges.map(entry => new FindMatch(entry, null)); + let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); + + let model = new TextModel([], TextModel.toRawText(text, TextModel.DEFAULT_CREATION_OPTIONS)); + _assertFindMatches(model, searchParams, expectedMatches); model.dispose(); + + + let model2 = new TextModel([], TextModel.toRawText(text, TextModel.DEFAULT_CREATION_OPTIONS)); + model2.setEOL(EndOfLineSequence.CRLF); + _assertFindMatches(model2, searchParams, expectedMatches); + model2.dispose(); } let regularText = [ @@ -498,6 +506,42 @@ suite('TextModelSearch', () => { model.dispose(); }); + test('\\n matches \\r\\n', () => { + let model = new TextModel([], TextModel.toRawText('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni', TextModel.DEFAULT_CREATION_OPTIONS)); + + assert.equal(model.getEOL(), '\r\n'); + + let searchParams = new SearchParams('h\\n', true, false, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000)[0]; + assertFindMatch(actual, new Range(8, 1, 9, 1), ['h\n']); + + searchParams = new SearchParams('g\\nh\\n', true, false, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000)[0]; + assertFindMatch(actual, new Range(7, 1, 9, 1), ['g\nh\n']); + + searchParams = new SearchParams('\\ni', true, false, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000)[0]; + assertFindMatch(actual, new Range(8, 2, 9, 2), ['\ni']); + + model.dispose(); + }); + + test('\\r can never be found', () => { + let model = new TextModel([], TextModel.toRawText('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni', TextModel.DEFAULT_CREATION_OPTIONS)); + + assert.equal(model.getEOL(), '\r\n'); + + let searchParams = new SearchParams('\\r\\n', true, false, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); + assert.equal(actual, null); + assert.deepEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); + + model.dispose(); + }); + function assertParseSearchResult(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: RegExp): void { let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); let actual = searchParams.parseSearchRequest();