提交 8bd5387d 编写于 作者: R Rob Lourens

Implement multiline search for #13155

上级 17d36c47
......@@ -686,4 +686,20 @@ export function containsUppercaseCharacter(target: string, ignoreEscapedChars =
export function uppercaseFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
\ No newline at end of file
}
export function getNLines(str: string, n = 1): string {
if (n === 0) {
return '';
}
let idx = -1;
do {
idx = str.indexOf('\n', idx + 1);
n--;
} while (n > 0 && idx >= 0);
return idx >= 0 ?
str.substr(0, idx) :
str;
}
......@@ -391,4 +391,16 @@ suite('Strings', () => {
assert.equal(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`);
});
});
test('getNLines', () => {
assert.equal(strings.getNLines('', 5), '');
assert.equal(strings.getNLines('foo', 5), 'foo');
assert.equal(strings.getNLines('foo\nbar', 5), 'foo\nbar');
assert.equal(strings.getNLines('foo\nbar', 2), 'foo\nbar');
assert.equal(strings.getNLines('foo\nbar', 1), 'foo');
assert.equal(strings.getNLines('foo\nbar'), 'foo');
assert.equal(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar');
assert.equal(strings.getNLines('foo', 0), '');
});
});
......@@ -26,34 +26,6 @@ export class SearchParams {
this.wordSeparators = wordSeparators;
}
private static _isMultilineRegexSource(searchString: string): boolean {
if (!searchString || searchString.length === 0) {
return false;
}
for (let i = 0, len = searchString.length; i < len; i++) {
const chCode = searchString.charCodeAt(i);
if (chCode === CharCode.Backslash) {
// move to next char
i++;
if (i >= len) {
// string ends with a \
break;
}
const nextChCode = searchString.charCodeAt(i);
if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W) {
return true;
}
}
}
return false;
}
public parseSearchRequest(): SearchData {
if (this.searchString === '') {
return null;
......@@ -62,7 +34,7 @@ export class SearchParams {
// Try to create a RegExp out of the params
let multiline: boolean;
if (this.isRegex) {
multiline = SearchParams._isMultilineRegexSource(this.searchString);
multiline = isMultilineRegexSource(this.searchString);
} else {
multiline = (this.searchString.indexOf('\n') >= 0);
}
......@@ -93,6 +65,34 @@ export class SearchParams {
}
}
export function isMultilineRegexSource(searchString: string): boolean {
if (!searchString || searchString.length === 0) {
return false;
}
for (let i = 0, len = searchString.length; i < len; i++) {
const chCode = searchString.charCodeAt(i);
if (chCode === CharCode.Backslash) {
// move to next char
i++;
if (i >= len) {
// string ends with a \
break;
}
const nextChCode = searchString.charCodeAt(i);
if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W) {
return true;
}
}
}
return false;
}
export class SearchData {
/**
......
......@@ -7,7 +7,7 @@ import { Position } from 'vs/editor/common/core/position';
import { FindMatch, EndOfLineSequence } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range';
import { TextModel } from 'vs/editor/common/model/textModel';
import { TextModelSearch, SearchParams, SearchData } from 'vs/editor/common/model/textModelSearch';
import { TextModelSearch, SearchParams, SearchData, isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch';
import { getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier';
import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
......@@ -720,4 +720,17 @@ suite('TextModelSearch', () => {
]
);
});
test('isMultilineRegexSource', () => {
assert(!isMultilineRegexSource('foo'));
assert(!isMultilineRegexSource(''));
assert(!isMultilineRegexSource('foo\\sbar'));
assert(!isMultilineRegexSource('\\\\notnewline'));
assert(isMultilineRegexSource('foo\\nbar'));
assert(isMultilineRegexSource('foo\\nbar\\s'));
assert(isMultilineRegexSource('foo\\r\\n'));
assert(isMultilineRegexSource('\\n'));
assert(isMultilineRegexSource('foo\\W'));
});
});
......@@ -13,6 +13,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
import { getNLines } from 'vs/base/common/strings';
export const VIEW_ID = 'workbench.view.search';
......@@ -235,41 +236,53 @@ export class TextSearchResult implements ITextSearchResult {
range: ISearchRange;
preview: ITextSearchResultPreview;
constructor(fullLine: string, range: ISearchRange, previewOptions?: ITextSearchPreviewOptions) {
constructor(text: string, range: ISearchRange, previewOptions?: ITextSearchPreviewOptions) {
this.range = range;
if (previewOptions) {
text = getNLines(text, previewOptions.matchLines);
const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
const endColumnByTrimmedLines = (range.startLineNumber + previewOptions.matchLines - 1) === range.endLineNumber ? // if single line...
range.endColumn :
previewOptions.charsPerLine;
// This doesn't handle all previewOptions correctly
const previewStart = Math.max(range.startColumn - leadingChars, 0);
const previewEnd = previewOptions.charsPerLine + previewStart;
const endOfMatchRangeInPreview = Math.min(previewEnd, range.endColumn - previewStart);
const endByCharsPerLine = previewOptions.charsPerLine + previewStart;
const trimmedEndOfMatchRangeInPreview = Math.min(endByCharsPerLine, endColumnByTrimmedLines - previewStart);
this.preview = {
text: fullLine.substring(previewStart, previewEnd),
match: new OneLineRange(0, range.startColumn - previewStart, endOfMatchRangeInPreview)
text: text.substring(previewStart, endByCharsPerLine),
match: new OneLineRange(0, range.startColumn - previewStart, trimmedEndOfMatchRangeInPreview)
};
} else {
this.preview = {
text: fullLine,
text: text,
match: new OneLineRange(0, range.startColumn, range.endColumn)
};
}
}
}
export class OneLineRange implements ISearchRange {
export class SearchRange implements ISearchRange {
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
constructor(lineNumber: number, startColumn: number, endColumn: number) {
this.startLineNumber = lineNumber;
constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
this.startLineNumber = startLineNumber;
this.startColumn = startColumn;
this.endLineNumber = lineNumber;
this.endLineNumber = endLineNumber;
this.endColumn = endColumn;
}
}
export class OneLineRange extends SearchRange {
constructor(lineNumber: number, startColumn: number, endColumn: number) {
super(lineNumber, startColumn, lineNumber, endColumn);
}
}
export interface ISearchConfigurationProperties {
exclude: glob.IExpression;
useRipgrep: boolean;
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ITextSearchPreviewOptions, OneLineRange, TextSearchResult } from 'vs/platform/search/common/search';
import { ITextSearchPreviewOptions, OneLineRange, TextSearchResult, SearchRange } from 'vs/platform/search/common/search';
suite('TextSearchResult', () => {
......@@ -78,4 +78,28 @@ suite('TextSearchResult', () => {
assert.deepEqual(result.range, range);
assertPreviewRangeText('b', result);
});
test('one line of multiline match', () => {
const previewOptions: ITextSearchPreviewOptions = {
matchLines: 1,
charsPerLine: 10000
};
const range = new SearchRange(5, 4, 6, 3);
const result = new TextSearchResult('foo bar\nfoo bar', range, previewOptions);
assert.deepEqual(result.range, range);
assertPreviewRangeText('bar', result);
});
// test('all lines of multiline match', () => {
// const previewOptions: ITextSearchPreviewOptions = {
// matchLines: 5,
// charsPerLine: 10000
// };
// const range = new SearchRange(5, 4, 6, 3);
// const result = new TextSearchResult('foo bar\nfoo bar', range, previewOptions);
// assert.deepEqual(result.range, range);
// assertPreviewRangeText('bar\nfoo', result);
// });
});
\ No newline at end of file
......@@ -17,6 +17,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { IPatternInfo, IQueryOptions, IFolderQuery, ISearchQuery, QueryType, ISearchConfiguration, getExcludes, pathIncludedInQuery } from 'vs/platform/search/common/search';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch';
export interface ISearchPathPattern {
searchPath: uri;
......@@ -77,7 +78,8 @@ export class QueryBuilder {
const ignoreSymlinks = !this.configurationService.getValue<ISearchConfiguration>().search.followSymlinks;
if (contentPattern) {
this.resolveSmartCaseToCaseSensitive(contentPattern);
contentPattern.isCaseSensitive = this.isCaseSensitive(contentPattern);
contentPattern.isMultiline = this.isMultiline(contentPattern);
contentPattern.wordSeparators = this.configurationService.getValue<ISearchConfiguration>().editor.wordSeparators;
}
......@@ -113,19 +115,33 @@ export class QueryBuilder {
}
/**
* Fix the isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
* Resolve isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
*/
private resolveSmartCaseToCaseSensitive(contentPattern: IPatternInfo): void {
private isCaseSensitive(contentPattern: IPatternInfo): boolean {
if (contentPattern.isSmartCase) {
if (contentPattern.isRegExp) {
// Consider it case sensitive if it contains an unescaped capital letter
if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
contentPattern.isCaseSensitive = true;
return true;
}
} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
contentPattern.isCaseSensitive = true;
return true;
}
}
return contentPattern.isCaseSensitive;
}
private isMultiline(contentPattern: IPatternInfo): boolean {
if (contentPattern.isMultiline) {
return true;
}
if (contentPattern.isRegExp && isMultilineRegexSource(contentPattern.pattern)) {
return true;
}
return false;
}
/**
......
......@@ -992,19 +992,9 @@ export class RangeHighlightDecorations implements IDisposable {
});
}
/**
* While search doesn't support multiline matches, collapse editor matches to a single line
*/
export function editorMatchToTextSearchResult(match: FindMatch, model: ITextModel, previewOptions: ITextSearchPreviewOptions): TextSearchResult {
let endLineNumber = match.range.endLineNumber - 1;
let endCol = match.range.endColumn - 1;
if (match.range.endLineNumber !== match.range.startLineNumber) {
endLineNumber = match.range.startLineNumber - 1;
endCol = model.getLineLength(match.range.startLineNumber);
}
return new TextSearchResult(
model.getLineContent(match.range.startLineNumber),
new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, endLineNumber, endCol),
new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, match.range.endLineNumber - 1, match.range.endColumn - 1),
previewOptions);
}
......@@ -285,19 +285,24 @@ export class RipgrepParser extends EventEmitter {
private submatchToResult(parsedLine: any, match: any, uri: URI): TextSearchResult {
const lineNumber = parsedLine.data.line_number - 1;
let matchText = bytesOrTextToString(parsedLine.data.lines);
let start = match.start;
let end = match.end;
let lineText = bytesOrTextToString(parsedLine.data.lines);
let matchText = bytesOrTextToString(match.match);
const newlineMatches = matchText.match(/\n/g);
const newlines = newlineMatches ? newlineMatches.length : 0;
let startCol = match.start;
const endLineNumber = lineNumber + newlines;
let endCol = match.end - (lineText.lastIndexOf('\n', lineText.length - 2) + 1);
if (lineNumber === 0) {
if (strings.startsWithUTF8BOM(matchText)) {
matchText = strings.stripUTF8BOM(matchText);
start -= 3;
end -= 3;
if (strings.startsWithUTF8BOM(lineText)) {
lineText = strings.stripUTF8BOM(lineText);
startCol -= 3;
endCol -= 3;
}
}
const range = new Range(lineNumber, start, lineNumber, end);
return new TextSearchResult(matchText, range, this.previewOptions);
const range = new Range(lineNumber, startCol, endLineNumber, endCol);
return new TextSearchResult(lineText, range, this.previewOptions);
}
private getFileMatch(relativeOrAbsolutePath: string): FileMatch {
......@@ -490,6 +495,10 @@ function getRgArgs(config: IRawSearch) {
args.push('--json');
if (config.contentPattern.isMultiline) {
args.push('--multiline');
}
// Folder to search
args.push('--');
......
......@@ -589,19 +589,9 @@ export class DiskSearch implements ISearchResultProvider {
}
}
/**
* While search doesn't support multiline matches, collapse editor matches to a single line
*/
function editorMatchToTextSearchResult(match: FindMatch, model: ITextModel, previewOptions: ITextSearchPreviewOptions): TextSearchResult {
let endLineNumber = match.range.endLineNumber - 1;
let endCol = match.range.endColumn - 1;
if (match.range.endLineNumber !== match.range.startLineNumber) {
endLineNumber = match.range.startLineNumber - 1;
endCol = model.getLineLength(match.range.startLineNumber);
}
return new TextSearchResult(
model.getLineContent(match.range.startLineNumber),
new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, endLineNumber, endCol),
new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, match.range.endLineNumber - 1, match.range.endColumn - 1),
previewOptions);
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册