提交 aba240a4 编写于 作者: R Rob Lourens

Fix #59919 - text search combines matches on one line

上级 b8c5d976
......@@ -540,3 +540,9 @@ export function find<T>(arr: ArrayLike<T>, predicate: (value: T, index: number,
return undefined;
}
export function mapArrayOrNot<T, U>(items: T | T[], fn: (_: T) => U): U | U[] {
return Array.isArray(items) ?
items.map(fn) :
fn(items);
}
......@@ -3,17 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Event } from 'vs/base/common/event';
import * as glob from 'vs/base/common/glob';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as objects from 'vs/base/common/objects';
import * as paths from 'vs/base/common/paths';
import { getNLines } from 'vs/base/common/strings';
import { URI as uri, UriComponents } from 'vs/base/common/uri';
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';
......@@ -170,12 +171,12 @@ export interface ISearchRange {
export interface ITextSearchResultPreview {
text: string;
match: ISearchRange;
matches: ISearchRange | ISearchRange[];
}
export interface ITextSearchResult {
uri?: uri;
range: ISearchRange;
ranges: ISearchRange | ISearchRange[];
preview: ITextSearchResultPreview;
}
......@@ -248,13 +249,13 @@ export class FileMatch implements IFileMatch {
}
export class TextSearchResult implements ITextSearchResult {
range: ISearchRange;
ranges: ISearchRange | ISearchRange[];
preview: ITextSearchResultPreview;
constructor(text: string, range: ISearchRange, previewOptions?: ITextSearchPreviewOptions) {
this.range = range;
constructor(text: string, range: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions) {
this.ranges = range;
if (previewOptions && previewOptions.matchLines === 1) {
if (previewOptions && previewOptions.matchLines === 1 && !Array.isArray(range)) {
// 1 line preview requested
text = getNLines(text, previewOptions.matchLines);
const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
......@@ -267,13 +268,15 @@ export class TextSearchResult implements ITextSearchResult {
this.preview = {
text: previewText,
match: new OneLineRange(0, range.startColumn - previewStart, endColInPreview)
matches: new OneLineRange(0, range.startColumn - previewStart, endColInPreview)
};
} else {
// n line or no preview requested
const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber;
// n line, no preview requested, or multiple matches in the preview
this.preview = {
text,
match: new SearchRange(0, range.startColumn, range.endLineNumber - range.startLineNumber, range.endColumn)
matches: mapArrayOrNot(range, r => new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn))
};
}
}
......
......@@ -14,56 +14,56 @@ suite('TextSearchResult', () => {
function assertPreviewRangeText(text: string, result: TextSearchResult): void {
assert.equal(
result.preview.text.substring(result.preview.match.startColumn, result.preview.match.endColumn),
result.preview.text.substring((<SearchRange>result.preview.matches).startColumn, (<SearchRange>result.preview.matches).endColumn),
text);
}
test('empty without preview options', () => {
const range = new OneLineRange(5, 0, 0);
const result = new TextSearchResult('', range);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('', result);
});
test('empty with preview options', () => {
const range = new OneLineRange(5, 0, 0);
const result = new TextSearchResult('', range, previewOptions1);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('', result);
});
test('short without preview options', () => {
const range = new OneLineRange(5, 4, 7);
const result = new TextSearchResult('foo bar', range);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('bar', result);
});
test('short with preview options', () => {
const range = new OneLineRange(5, 4, 7);
const result = new TextSearchResult('foo bar', range, previewOptions1);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('bar', result);
});
test('leading', () => {
const range = new OneLineRange(5, 25, 28);
const result = new TextSearchResult('long text very long text foo', range, previewOptions1);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('foo', result);
});
test('trailing', () => {
const range = new OneLineRange(5, 0, 3);
const result = new TextSearchResult('foo long text very long text long text very long text long text very long text long text very long text long text very long text', range, previewOptions1);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('foo', result);
});
test('middle', () => {
const range = new OneLineRange(5, 30, 33);
const result = new TextSearchResult('long text very long text long foo text very long text long text very long text long text very long text long text very long text', range, previewOptions1);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('foo', result);
});
......@@ -75,7 +75,7 @@ suite('TextSearchResult', () => {
const range = new OneLineRange(0, 4, 7);
const result = new TextSearchResult('foo bar', range, previewOptions);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('b', result);
});
......@@ -87,7 +87,7 @@ suite('TextSearchResult', () => {
const range = new SearchRange(5, 4, 6, 3);
const result = new TextSearchResult('foo bar\nfoo bar', range, previewOptions);
assert.deepEqual(result.range, range);
assert.deepEqual(result.ranges, range);
assertPreviewRangeText('bar', result);
});
......
......@@ -234,8 +234,9 @@ declare module 'vscode' {
/**
* The Range within `text` corresponding to the text of the match.
* The number of matches must match the TextSearchResult's range property.
*/
match: Range;
matches: Range | Range[];
}
/**
......@@ -248,9 +249,9 @@ declare module 'vscode' {
uri: Uri;
/**
* The range of the match within the document.
* The range of the match within the document, or multiple ranges for multiple matches.
*/
range: Range;
ranges: Range | Range[];
/**
* A preview of the text result.
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { join, relative } from 'path';
import { delta as arrayDelta } from 'vs/base/common/arrays';
import { delta as arrayDelta, mapArrayOrNot } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { TernarySearchTree } from 'vs/base/common/map';
import { Counter } from 'vs/base/common/numbers';
......@@ -425,9 +425,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
uri: URI.revive(p.resource),
preview: {
text: match.preview.text,
match: new Range(match.preview.match.startLineNumber, match.preview.match.startColumn, match.preview.match.endLineNumber, match.preview.match.endColumn)
matches: mapArrayOrNot(
match.preview.matches,
m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn))
},
range: new Range(match.range.startLineNumber, match.range.startColumn, match.range.endLineNumber, match.range.endColumn)
ranges: mapArrayOrNot(
match.ranges,
r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn))
});
});
};
......
......@@ -21,7 +21,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
import { ReplacePattern } from 'vs/platform/search/common/replace';
import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchService, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, TextSearchResult, ITextQuery } from 'vs/platform/search/common/search';
import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, TextSearchResult } from 'vs/platform/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
......@@ -37,17 +37,21 @@ export class Match {
private _rangeInPreviewText: Range;
constructor(private _parent: FileMatch, _result: ITextSearchResult) {
if (Array.isArray(_result.ranges) || Array.isArray(_result.preview.matches)) {
throw new Error('A Match can only be built from a single search result');
}
this._range = new Range(
_result.range.startLineNumber + 1,
_result.range.startColumn + 1,
_result.range.endLineNumber + 1,
_result.range.endColumn + 1);
_result.ranges.startLineNumber + 1,
_result.ranges.startColumn + 1,
_result.ranges.endLineNumber + 1,
_result.ranges.endColumn + 1);
this._rangeInPreviewText = new Range(
_result.preview.match.startLineNumber + 1,
_result.preview.match.startColumn + 1,
_result.preview.match.endLineNumber + 1,
_result.preview.match.endColumn + 1);
_result.preview.matches.startLineNumber + 1,
_result.preview.matches.startColumn + 1,
_result.preview.matches.endLineNumber + 1,
_result.preview.matches.endColumn + 1);
this._previewText = _result.preview.text;
this._id = this._parent.id() + '>' + this._range + this.getMatchString();
......@@ -171,8 +175,8 @@ export class FileMatch extends Disposable {
this.updateMatchesForModel();
} else {
this.rawMatch.matches.forEach(rawMatch => {
let match = new Match(this, rawMatch);
this.add(match);
textSearchResultToMatches(rawMatch, this)
.forEach(m => this.add(m));
});
}
}
......@@ -416,8 +420,8 @@ export class FolderMatch extends Disposable {
if (this._fileMatches.has(rawFileMatch.resource)) {
const existingFileMatch = this._fileMatches.get(rawFileMatch.resource);
rawFileMatch.matches.forEach(m => {
let match = new Match(existingFileMatch, m);
existingFileMatch.add(match);
textSearchResultToMatches(m, existingFileMatch)
.forEach(m => existingFileMatch.add(m));
});
updated.push(existingFileMatch);
} else {
......@@ -1011,3 +1015,21 @@ export function editorMatchToTextSearchResult(match: FindMatch, model: ITextMode
new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, match.range.endLineNumber - 1, match.range.endColumn - 1),
previewOptions);
}
function textSearchResultToMatches(rawMatch: ITextSearchResult, fileMatch: FileMatch): Match[] {
if (Array.isArray(rawMatch.ranges)) {
return rawMatch.ranges.map((r, i) => {
return new Match(fileMatch, {
uri: rawMatch.uri,
ranges: r,
preview: {
text: rawMatch.preview.text,
matches: rawMatch.preview.matches[i]
}
});
});
} else {
let match = new Match(fileMatch, rawMatch);
return [match];
}
}
......@@ -134,7 +134,7 @@ suite('Search Actions', () => {
function aMatch(fileMatch: FileMatch): Match {
const line = ++counter;
const range = {
const ranges = {
startLineNumber: line,
startColumn: 0,
endLineNumber: line,
......@@ -143,9 +143,9 @@ suite('Search Actions', () => {
let match = new Match(fileMatch, {
preview: {
text: 'some match',
match: range
matches: ranges
},
range
ranges
});
fileMatch.add(match);
return match;
......
......@@ -36,7 +36,7 @@ suite('Search - Viewlet', () => {
}]
};
const range = {
const ranges = {
startLineNumber: 1,
startColumn: 0,
endLineNumber: 1,
......@@ -47,9 +47,9 @@ suite('Search - Viewlet', () => {
matches: [{
preview: {
text: 'bar',
match: range
matches: ranges
},
range
ranges
}]
}]);
......
......@@ -7,6 +7,7 @@ import { startsWith } from 'vs/base/common/strings';
import { ILogService } from 'vs/platform/log/common/log';
import { SearchRange, TextSearchResult } from 'vs/platform/search/common/search';
import * as vscode from 'vscode';
import { mapArrayOrNot } from 'vs/base/common/arrays';
export type Maybe<T> = T | null | undefined;
......@@ -14,26 +15,32 @@ export function anchorGlob(glob: string): string {
return startsWith(glob, '**') || startsWith(glob, '/') ? glob : `/${glob}`;
}
export function createTextSearchResult(uri: vscode.Uri, text: string, range: Range, previewOptions?: vscode.TextSearchPreviewOptions): vscode.TextSearchResult {
const searchRange: SearchRange = {
startLineNumber: range.start.line,
startColumn: range.start.character,
endLineNumber: range.end.line,
endColumn: range.end.character,
};
/**
* Create a vscode.TextSearchResult by using our internal TextSearchResult type for its previewOptions logic.
*/
export function createTextSearchResult(uri: vscode.Uri, text: string, range: Range | Range[], previewOptions?: vscode.TextSearchPreviewOptions): vscode.TextSearchResult {
const searchRange = mapArrayOrNot(range, rangeToSearchRange);
const internalResult = new TextSearchResult(text, searchRange, previewOptions);
const internalPreviewRange = internalResult.preview.match;
const internalPreviewRange = internalResult.preview.matches;
return {
range: new Range(internalResult.range.startLineNumber, internalResult.range.startColumn, internalResult.range.endLineNumber, internalResult.range.endColumn),
ranges: mapArrayOrNot(searchRange, searchRangeToRange),
uri,
preview: {
text: internalResult.preview.text,
match: new Range(internalPreviewRange.startLineNumber, internalPreviewRange.startColumn, internalPreviewRange.endLineNumber, internalPreviewRange.endColumn),
matches: mapArrayOrNot(internalPreviewRange, searchRangeToRange)
}
};
}
function rangeToSearchRange(range: Range): SearchRange {
return new SearchRange(range.start.line, range.start.character, range.end.line, range.end.character);
}
function searchRangeToRange(range: SearchRange): Range {
return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
}
export class Position {
constructor(public readonly line, public readonly character) { }
......
......@@ -138,6 +138,7 @@ export function rgErrorMsgForDisplay(msg: string): Maybe<string> {
export class RipgrepParser extends EventEmitter {
private remainder = '';
private isDone = false;
private hitLimit = false;
private stringDecoder: NodeStringDecoder;
private numResults = 0;
......@@ -190,56 +191,62 @@ export class RipgrepParser extends EventEmitter {
}
if (parsedLine.type === 'match') {
let hitLimit = false;
const uri = URI.file(path.join(this.rootFolder, parsedLine.data.path.text));
parsedLine.data.submatches.map((match: any) => {
if (hitLimit) {
return null;
}
if (this.numResults >= this.maxResults) {
// Finish the line, then report the result below
hitLimit = true;
}
const result = this.submatchesToResult(parsedLine, parsedLine.data.submatches, uri);
this.onResult(result);
return this.submatchToResult(parsedLine, match, uri);
}).forEach((result: any) => {
if (result) {
this.onResult(result);
}
});
if (hitLimit) {
if (this.hitLimit) {
this.cancel();
this.emit('hitLimit');
}
}
}
private submatchToResult(parsedLine: any, match: any, uri: vscode.Uri): vscode.TextSearchResult {
private submatchesToResult(parsedLine: any, matches: any[], uri: vscode.Uri): vscode.TextSearchResult {
const lineNumber = parsedLine.data.line_number - 1;
let lineText = bytesOrTextToString(parsedLine.data.lines);
let matchText = bytesOrTextToString(match.match);
const newlineMatches = matchText.match(/\n/g);
const newlines = newlineMatches ? newlineMatches.length : 0;
const fullText = bytesOrTextToString(parsedLine.data.lines);
const fullTextBytes = Buffer.from(fullText);
let prevMatchEnd = 0;
let prevMatchEndCol = 0;
let prevMatchEndLine = lineNumber;
const ranges = matches.map((match, i) => {
if (this.hitLimit) {
return null;
}
this.numResults++;
if (this.numResults >= this.maxResults) {
// Finish the line, then report the result below
this.hitLimit = true;
}
const textBytes = Buffer.from(lineText);
let startCol = textBytes.slice(0, match.start).toString().length;
const endChars = startCol + textBytes.slice(match.start, match.end).toString().length;
let matchText = bytesOrTextToString(match.match);
const inBetweenChars = fullTextBytes.slice(prevMatchEnd, match.start).toString().length;
let startCol = prevMatchEndCol + inBetweenChars;
const endLineNumber = lineNumber + newlines;
let endCol = endChars - (lineText.lastIndexOf('\n', lineText.length - 2) + 1);
const stats = getNumLinesAndLastNewlineLength(matchText);
let startLineNumber = prevMatchEndLine;
let endLineNumber = stats.numLines + startLineNumber;
let endCol = stats.numLines > 0 ?
stats.lastLineLength :
stats.lastLineLength + startCol;
if (lineNumber === 0) {
if (startsWithUTF8BOM(matchText)) {
if (lineNumber === 0 && i === 0 && startsWithUTF8BOM(matchText)) {
matchText = stripUTF8BOM(matchText);
startCol -= 3;
endCol -= 3;
}
}
const range = new Range(lineNumber, startCol, endLineNumber, endCol);
return createTextSearchResult(uri, lineText, range, this.previewOptions);
prevMatchEnd = match.end;
prevMatchEndCol = endCol;
prevMatchEndLine = endLineNumber;
return new Range(startLineNumber, startCol, endLineNumber, endCol);
})
.filter(r => !!r);
return createTextSearchResult(uri, fullText, ranges, this.previewOptions);
}
private onResult(match: vscode.TextSearchResult): void {
......@@ -253,6 +260,23 @@ function bytesOrTextToString(obj: any): string {
obj.text;
}
function getNumLinesAndLastNewlineLength(text: string): { numLines: number, lastLineLength: number } {
const re = /\n/g;
let numLines = 0;
let lastNewlineIdx = -1;
let match: ReturnType<typeof re.exec>;
while (match = re.exec(text)) {
numLines++;
lastNewlineIdx = match.index;
}
const lastLineLength = lastNewlineIdx >= 0 ?
text.length - lastNewlineIdx :
text.length;
return { numLines, lastLineLength };
}
function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions): string[] {
const args = ['--hidden', '--heading', '--line-number', '--color', 'ansi', '--colors', 'path:none', '--colors', 'line:none', '--colors', 'match:fg:red', '--colors', 'match:style:nobold'];
args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case');
......
......@@ -50,6 +50,6 @@ function fileMatchToSerialized(match: IFileMatch): ISerializedFileMatch {
return {
path: match.resource.fsPath,
matches: match.matches,
numMatches: match.matches.length
numMatches: match.matches.reduce((sum, m) => sum + (Array.isArray(m.ranges) ? m.ranges.length : 1), 0)
};
}
\ No newline at end of file
......@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as glob from 'vs/base/common/glob';
......@@ -82,6 +83,8 @@ export class TextSearchManager {
const testingPs: TPromise<void>[] = [];
const progress = {
report: (result: vscode.TextSearchResult) => {
// TODO: validate result.ranges vs result.preview.matches
const hasSibling = folderQuery.folder.scheme === 'file' && glob.hasSiblingPromiseFn(() => {
return this.readdir(path.dirname(result.uri.fsPath));
});
......@@ -201,20 +204,20 @@ function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSe
// Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these...
return {
preview: {
match: {
startLineNumber: data.preview.match.start.line,
startColumn: data.preview.match.start.character,
endLineNumber: data.preview.match.end.line,
endColumn: data.preview.match.end.character
},
matches: mapArrayOrNot(data.preview.matches, m => ({
startLineNumber: m.start.line,
startColumn: m.start.character,
endLineNumber: m.end.line,
endColumn: m.end.character
})),
text: data.preview.text
},
range: {
startLineNumber: data.range.start.line,
startColumn: data.range.start.character,
endLineNumber: data.range.end.line,
endColumn: data.range.end.character
}
ranges: mapArrayOrNot(data.ranges, r => ({
startLineNumber: r.start.line,
startColumn: r.start.character,
endLineNumber: r.end.line,
endColumn: r.end.character
}))
};
}
......
......@@ -385,13 +385,13 @@ suite('Search-integration', function () {
};
return doRipgrepSearchTest(config, 1).then(results => {
const matchRange = results[0].matches[0].range;
assert.deepEqual(matchRange, {
const matchRange = results[0].matches[0].ranges;
assert.deepEqual(matchRange, [{
startLineNumber: 0,
startColumn: 1,
endLineNumber: 0,
endColumn: 2
});
}]);
});
});
});
......
......@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { dispose } from 'vs/base/common/lifecycle';
......@@ -625,7 +626,7 @@ suite('ExtHostSearch', () => {
function makePreview(text: string): vscode.TextSearchResult['preview'] {
return {
match: new Range(0, 0, 0, text.length),
matches: new Range(0, 0, 0, text.length),
text
};
}
......@@ -633,7 +634,7 @@ suite('ExtHostSearch', () => {
function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchResult {
return {
preview: makePreview('foo'),
range: new Range(0, 0, 0, 3),
ranges: new Range(0, 0, 0, 3),
uri: joinPath(baseFolder, relativePath)
};
}
......@@ -663,9 +664,14 @@ suite('ExtHostSearch', () => {
actualTextSearchResults.push({
preview: {
text: lineMatch.preview.text,
match: new Range(lineMatch.preview.match.startLineNumber, lineMatch.preview.match.startColumn, lineMatch.preview.match.endLineNumber, lineMatch.preview.match.endColumn)
matches: mapArrayOrNot(
lineMatch.preview.matches,
m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn))
},
range: new Range(lineMatch.range.startLineNumber, lineMatch.range.startColumn, lineMatch.range.endLineNumber, lineMatch.range.endColumn),
ranges: mapArrayOrNot(
lineMatch.ranges,
r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn),
),
uri: fileMatch.resource
});
}
......@@ -679,7 +685,7 @@ suite('ExtHostSearch', () => {
...r,
...{
uri: r.uri.toString(),
range: rangeToString(r.range),
range: mapArrayOrNot(r.ranges, rangeToString),
preview: {
text: r.preview.text,
match: null // Don't care about this right now
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册