mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 15:35:20 +01:00
Merge pull request #110273 from microsoft/connor4312/fix-search-freeze-on-long-lines
search: fix freezing ui on long lines
This commit is contained in:
Vendored
+1
-4
@@ -41,10 +41,7 @@
|
||||
"port": 5876,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as pathUtils from 'path';
|
||||
|
||||
const FILE_LINE_REGEX = /^(\S.*):$/;
|
||||
const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
|
||||
const ELISION_REGEX = /⟪ ([0-9]+) characters skipped ⟫/g;
|
||||
const SEARCH_RESULT_SELECTOR = { language: 'search-result', exclusive: true };
|
||||
const DIRECTIVES = ['# Query:', '# Flags:', '# Including:', '# Excluding:', '# ContextLines:'];
|
||||
const FLAGS = ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch'];
|
||||
@@ -80,12 +81,18 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
return lineResult.allLocations;
|
||||
}
|
||||
|
||||
const translateRangeSidewaysBy = (r: vscode.Range, n: number) =>
|
||||
r.with({ start: new vscode.Position(r.start.line, Math.max(0, n - r.start.character)), end: new vscode.Position(r.end.line, Math.max(0, n - r.end.character)) });
|
||||
const location = lineResult.locations.find(l => l.originSelectionRange.contains(position));
|
||||
if (!location) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetPos = new vscode.Position(
|
||||
location.targetSelectionRange.start.line,
|
||||
location.targetSelectionRange.start.character + (position.character - location.originSelectionRange.start.character)
|
||||
);
|
||||
return [{
|
||||
...lineResult.location,
|
||||
targetSelectionRange: translateRangeSidewaysBy(lineResult.location.targetSelectionRange!, position.character - 1)
|
||||
...location,
|
||||
targetSelectionRange: new vscode.Range(targetPos, targetPos),
|
||||
}];
|
||||
}
|
||||
}),
|
||||
@@ -93,7 +100,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.languages.registerDocumentLinkProvider(SEARCH_RESULT_SELECTOR, {
|
||||
async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentLink[]> {
|
||||
return parseSearchResults(document, token)
|
||||
.filter(({ type }) => type === 'file')
|
||||
.filter(isFileLine)
|
||||
.map(({ location }) => ({ range: location.originSelectionRange!, target: location.targetUri }));
|
||||
}
|
||||
}),
|
||||
@@ -162,7 +169,7 @@ function relativePathToUri(path: string, resultsUri: vscode.Uri): vscode.Uri | u
|
||||
}
|
||||
|
||||
type ParsedSearchFileLine = { type: 'file', location: vscode.LocationLink, allLocations: vscode.LocationLink[], path: string };
|
||||
type ParsedSearchResultLine = { type: 'result', location: vscode.LocationLink, isContext: boolean, prefixRange: vscode.Range };
|
||||
type ParsedSearchResultLine = { type: 'result', locations: Required<vscode.LocationLink>[], isContext: boolean, prefixRange: vscode.Range };
|
||||
type ParsedSearchResults = Array<ParsedSearchFileLine | ParsedSearchResultLine>;
|
||||
const isFileLine = (line: ParsedSearchResultLine | ParsedSearchFileLine): line is ParsedSearchFileLine => line.type === 'file';
|
||||
const isResultLine = (line: ParsedSearchResultLine | ParsedSearchFileLine): line is ParsedSearchResultLine => line.type === 'result';
|
||||
@@ -211,17 +218,35 @@ function parseSearchResults(document: vscode.TextDocument, token?: vscode.Cancel
|
||||
const lineNumber = +_lineNumber - 1;
|
||||
const resultStart = (indentation + _lineNumber + seperator + resultIndentation).length;
|
||||
const metadataOffset = (indentation + _lineNumber + seperator).length;
|
||||
const targetRange = new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length);
|
||||
|
||||
const location: vscode.LocationLink = {
|
||||
targetRange: new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length),
|
||||
targetSelectionRange: new vscode.Range(lineNumber, metadataOffset, lineNumber, metadataOffset),
|
||||
targetUri: currentTarget,
|
||||
originSelectionRange: new vscode.Range(i, resultStart, i, line.length),
|
||||
};
|
||||
let lastEnd = resultStart;
|
||||
let offset = 0;
|
||||
let locations: Required<vscode.LocationLink>[] = [];
|
||||
ELISION_REGEX.lastIndex = resultStart;
|
||||
for (let match: RegExpExecArray | null; (match = ELISION_REGEX.exec(line));) {
|
||||
locations.push({
|
||||
targetRange,
|
||||
targetSelectionRange: new vscode.Range(lineNumber, offset, lineNumber, offset),
|
||||
targetUri: currentTarget,
|
||||
originSelectionRange: new vscode.Range(i, lastEnd, i, ELISION_REGEX.lastIndex - match[0].length),
|
||||
});
|
||||
|
||||
currentTargetLocations?.push(location);
|
||||
offset += (ELISION_REGEX.lastIndex - lastEnd - match[0].length) + Number(match[1]);
|
||||
lastEnd = ELISION_REGEX.lastIndex;
|
||||
}
|
||||
|
||||
links[i] = { type: 'result', location, isContext: seperator === ' ', prefixRange: new vscode.Range(i, 0, i, metadataOffset) };
|
||||
if (lastEnd < line.length) {
|
||||
locations.push({
|
||||
targetRange,
|
||||
targetSelectionRange: new vscode.Range(lineNumber, offset, lineNumber, offset),
|
||||
targetUri: currentTarget,
|
||||
originSelectionRange: new vscode.Range(i, lastEnd, i, line.length),
|
||||
});
|
||||
}
|
||||
|
||||
currentTargetLocations?.push(...locations);
|
||||
links[i] = { type: 'result', locations, isContext: seperator === ' ', prefixRange: new vscode.Range(i, 0, i, metadataOffset) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ const scopes = {
|
||||
meta: 'meta.resultLine.search',
|
||||
metaSingleLine: 'meta.resultLine.singleLine.search',
|
||||
metaMultiLine: 'meta.resultLine.multiLine.search',
|
||||
elision: 'comment meta.resultLine.elision',
|
||||
prefix: {
|
||||
meta: 'constant.numeric.integer meta.resultLinePrefix.search',
|
||||
metaContext: 'meta.resultLinePrefix.contextLinePrefix.search',
|
||||
@@ -220,6 +221,10 @@ const plainText = [
|
||||
'4': { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaContext].join(' ') },
|
||||
'5': { name: scopes.resultBlock.result.prefix.lineNumber },
|
||||
}
|
||||
},
|
||||
{
|
||||
match: '⟪ [0-9]+ characters skipped ⟫',
|
||||
name: [scopes.resultBlock.meta, scopes.resultBlock.result.elision].join(' '),
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -263,6 +263,10 @@
|
||||
"name": "meta.resultLinePrefix.lineNumber.search"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "⟪ [0-9]+ characters skipped ⟫",
|
||||
"name": "meta.resultBlock.search comment meta.resultLine.elision"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
|
||||
@@ -24,6 +24,12 @@ export const VIEW_ID = 'workbench.view.search';
|
||||
|
||||
export const SEARCH_EXCLUDE_CONFIG = 'search.exclude';
|
||||
|
||||
// Warning: this pattern is used in the search editor to detect offsets. If you
|
||||
// change this, also change the search-result built-in extension
|
||||
const SEARCH_ELIDED_PREFIX = '⟪ ';
|
||||
const SEARCH_ELIDED_SUFFIX = ' characters skipped ⟫';
|
||||
const SEARCH_ELIDED_MIN_LEN = (SEARCH_ELIDED_PREFIX.length + SEARCH_ELIDED_SUFFIX.length + 5) * 2;
|
||||
|
||||
export const ISearchService = createDecorator<ISearchService>('searchService');
|
||||
|
||||
/**
|
||||
@@ -260,24 +266,32 @@ export class TextSearchMatch implements ITextSearchMatch {
|
||||
// Trim preview if this is one match and a single-line match with a preview requested.
|
||||
// Otherwise send the full text, like for replace or for showing multiple previews.
|
||||
// TODO this is fishy.
|
||||
if (previewOptions && previewOptions.matchLines === 1 && (!Array.isArray(range) || range.length === 1) && isSingleLineRange(range)) {
|
||||
const oneRange = Array.isArray(range) ? range[0] : range;
|
||||
|
||||
const ranges = Array.isArray(range) ? range : [range];
|
||||
if (previewOptions && previewOptions.matchLines === 1 && isSingleLineRangeList(ranges)) {
|
||||
// 1 line preview requested
|
||||
text = getNLines(text, previewOptions.matchLines);
|
||||
|
||||
let result = '';
|
||||
let shift = 0;
|
||||
let lastEnd = 0;
|
||||
const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
|
||||
const previewStart = Math.max(oneRange.startColumn - leadingChars, 0);
|
||||
const previewText = text.substring(previewStart, previewOptions.charsPerLine + previewStart);
|
||||
const matches: ISearchRange[] = [];
|
||||
for (const range of ranges) {
|
||||
const previewStart = Math.max(range.startColumn - leadingChars, 0);
|
||||
const previewEnd = range.startColumn + previewOptions.charsPerLine;
|
||||
if (previewStart > lastEnd + leadingChars + SEARCH_ELIDED_MIN_LEN) {
|
||||
const elision = SEARCH_ELIDED_PREFIX + (previewStart - lastEnd) + SEARCH_ELIDED_SUFFIX;
|
||||
result += elision + text.slice(previewStart, previewEnd);
|
||||
shift += previewStart - (lastEnd + elision.length);
|
||||
} else {
|
||||
result += text.slice(lastEnd, previewEnd);
|
||||
}
|
||||
|
||||
const endColInPreview = (oneRange.endLineNumber - oneRange.startLineNumber + 1) <= previewOptions.matchLines ?
|
||||
Math.min(previewText.length, oneRange.endColumn - previewStart) : // if number of match lines will not be trimmed by previewOptions
|
||||
previewText.length; // if number of lines is trimmed
|
||||
matches.push(new OneLineRange(0, range.startColumn - shift, range.endColumn - shift));
|
||||
lastEnd = previewEnd;
|
||||
}
|
||||
|
||||
const oneLineRange = new OneLineRange(0, oneRange.startColumn - previewStart, endColInPreview);
|
||||
this.preview = {
|
||||
text: previewText,
|
||||
matches: Array.isArray(range) ? [oneLineRange] : oneLineRange
|
||||
};
|
||||
this.preview = { text: result, matches: Array.isArray(this.ranges) ? matches : matches[0] };
|
||||
} else {
|
||||
const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber;
|
||||
|
||||
@@ -289,10 +303,15 @@ export class TextSearchMatch implements ITextSearchMatch {
|
||||
}
|
||||
}
|
||||
|
||||
function isSingleLineRange(range: ISearchRange | ISearchRange[]): boolean {
|
||||
return Array.isArray(range) ?
|
||||
range[0].startLineNumber === range[0].endLineNumber :
|
||||
range.startLineNumber === range.endLineNumber;
|
||||
function isSingleLineRangeList(ranges: ISearchRange[]): boolean {
|
||||
const line = ranges[0].startLineNumber;
|
||||
for (const r of ranges) {
|
||||
if (r.startLineNumber !== line || r.endLineNumber !== line) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export class SearchRange implements ISearchRange {
|
||||
|
||||
@@ -95,6 +95,20 @@ suite('TextSearchResult', () => {
|
||||
assert.equal((<SearchRange>result.preview.matches).endColumn, 3);
|
||||
});
|
||||
|
||||
test('compacts multiple ranges on long lines', () => {
|
||||
const previewOptions: ITextSearchPreviewOptions = {
|
||||
matchLines: 1,
|
||||
charsPerLine: 10
|
||||
};
|
||||
|
||||
const range1 = new SearchRange(5, 4, 5, 7);
|
||||
const range2 = new SearchRange(5, 133, 5, 136);
|
||||
const range3 = new SearchRange(5, 141, 5, 144);
|
||||
const result = new TextSearchMatch('foo bar 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 foo bar baz bar', [range1, range2, range3], previewOptions);
|
||||
assert.deepEqual(result.preview.matches, [new SearchRange(0, 4, 0, 7), new SearchRange(0, 42, 0, 45), new SearchRange(0, 50, 0, 53)]);
|
||||
assert.equal(result.preview.text, 'foo bar 123456⟪ 117 characters skipped ⟫o bar baz bar');
|
||||
});
|
||||
|
||||
// test('all lines of multiline match', () => {
|
||||
// const previewOptions: ITextSearchPreviewOptions = {
|
||||
// matchLines: 5,
|
||||
|
||||
Reference in New Issue
Block a user