Skip to content

Commit eae6eca

Browse files
author
Jackson Kearl
committed
[Search Editor] Add option for context lines
1 parent 5293513 commit eae6eca

9 files changed

Lines changed: 133 additions & 22 deletions

File tree

extensions/search-result/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,23 @@
2828
"light": "./src/media/refresh-light.svg",
2929
"dark": "./src/media/refresh-dark.svg"
3030
}
31+
},
32+
{
33+
"command": "searchResult.rerunSearchWithContext",
34+
"title": "%searchResult.rerunSearchWithContext.title%",
35+
"category": "Search Result",
36+
"icon": {
37+
"light": "./src/media/refresh-light.svg",
38+
"dark": "./src/media/refresh-dark.svg"
39+
}
3140
}
3241
],
3342
"menus": {
3443
"editor/title": [
3544
{
3645
"command": "searchResult.rerunSearch",
3746
"when": "editorLangId == search-result",
47+
"alt": "searchResult.rerunSearchWithContext",
3848
"group": "navigation"
3949
}
4050
]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"displayName": "Search Result",
33
"description": "Provides syntax highlighting and language features for tabbed search results.",
4-
"searchResult.rerunSearch.title": "Search Again"
4+
"searchResult.rerunSearch.title": "Search Again",
5+
"searchResult.rerunSearchWithContext.title": "Search Again (Wth Context)"
56
}

extensions/search-result/src/extension.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import * as vscode from 'vscode';
77
import * as pathUtils from 'path';
88

99
const FILE_LINE_REGEX = /^(\S.*):$/;
10-
const RESULT_LINE_REGEX = /^(\s+)(\d+):(\s+)(.*)$/;
10+
const RESULT_LINE_REGEX = /^(\s+)(\d+)(?::| )(\s+)(.*)$/;
1111
const SEARCH_RESULT_SELECTOR = { language: 'search-result' };
12+
const DIRECTIVES = ['# Query:', '# Flags:', '# Including:', '# Excluding:', '# ContextLines:'];
13+
const FLAGS = ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch'];
1214

1315
let cachedLastParse: { version: number, parse: ParsedSearchResults } | undefined;
1416

1517
export function activate(context: vscode.ExtensionContext) {
1618
context.subscriptions.push(
1719
vscode.commands.registerCommand('searchResult.rerunSearch', () => vscode.commands.executeCommand('search.action.rerunEditorSearch')),
20+
vscode.commands.registerCommand('searchResult.rerunSearchWithContext', () => vscode.commands.executeCommand('search.action.rerunEditorSearchWithContext')),
1821

1922
vscode.languages.registerDocumentSymbolProvider(SEARCH_RESULT_SELECTOR, {
2023
provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.DocumentSymbol[] {
@@ -38,16 +41,16 @@ export function activate(context: vscode.ExtensionContext) {
3841
const line = document.lineAt(position.line);
3942
if (position.line > 3) { return []; }
4043
if (position.character === 0 || (position.character === 1 && line.text === '#')) {
41-
const header = Array.from({ length: 4 }).map((_, i) => document.lineAt(i).text);
44+
const header = Array.from({ length: DIRECTIVES.length }).map((_, i) => document.lineAt(i).text);
4245

43-
return ['# Query:', '# Flags:', '# Including:', '# Excluding:']
46+
return DIRECTIVES
4447
.filter(suggestion => header.every(line => line.indexOf(suggestion) === -1))
4548
.map(flag => ({ label: flag, insertText: (flag.slice(position.character)) + ' ' }));
4649
}
4750

4851
if (line.text.indexOf('# Flags:') === -1) { return []; }
4952

50-
return ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch']
53+
return FLAGS
5154
.filter(flag => line.text.indexOf(flag) === -1)
5255
.map(flag => ({ label: flag, insertText: flag + ' ' }));
5356
}

extensions/search-result/syntaxes/searchResult.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"scopeName": "text.searchResult",
44
"patterns": [
55
{
6-
"match": "^# (Query|Flags|Including|Excluding): .*$",
6+
"match": "^# (Query|Flags|Including|Excluding|ContextLines): .*$",
77
"name": "comment"
88
},
99
{

src/vs/workbench/contrib/search/browser/search.contribution.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition
4141
import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler';
4242
import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler';
4343
import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions';
44-
import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction } from 'vs/workbench/contrib/search/browser/searchActions';
44+
import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction } from 'vs/workbench/contrib/search/browser/searchActions';
4545
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
4646
import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView';
4747
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
@@ -651,6 +651,11 @@ registry.registerWorkbenchAction(
651651
'Search Editor: Search Again', category,
652652
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result')));
653653

654+
registry.registerWorkbenchAction(
655+
SyncActionDescriptor.create(RerunEditorSearchWithContextAction, RerunEditorSearchWithContextAction.ID, RerunEditorSearchWithContextAction.LABEL),
656+
'Search Editor: Search Again (With Context)', category,
657+
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result')));
658+
654659

655660
// Register Quick Open Handler
656661
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler(

src/vs/workbench/contrib/search/browser/searchActions.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
3232
import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor';
3333
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
3434
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
35+
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
3536

3637
export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
3738
const searchView = getSearchView(viewletService, panelService);
@@ -472,7 +473,38 @@ export class RerunEditorSearchAction extends Action {
472473
async run() {
473474
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
474475
await this.progressService.withProgress({ location: ProgressLocation.Window },
475-
() => refreshActiveEditorSearch(this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService));
476+
() => refreshActiveEditorSearch(undefined, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService));
477+
}
478+
}
479+
}
480+
481+
export class RerunEditorSearchWithContextAction extends Action {
482+
483+
static readonly ID: string = Constants.RerunEditorSearchWithContextCommandId;
484+
static readonly LABEL = nls.localize('search.rerunEditorSearchContext', "Search Again (With Context)");
485+
486+
constructor(id: string, label: string,
487+
@IInstantiationService private instantiationService: IInstantiationService,
488+
@IEditorService private editorService: IEditorService,
489+
@IConfigurationService private configurationService: IConfigurationService,
490+
@IWorkspaceContextService private contextService: IWorkspaceContextService,
491+
@ILabelService private labelService: ILabelService,
492+
@IProgressService private progressService: IProgressService,
493+
@IQuickInputService private quickPickService: IQuickInputService
494+
) {
495+
super(id, label);
496+
}
497+
498+
async run() {
499+
const lines = await this.quickPickService.input({
500+
prompt: nls.localize('lines', "Lines of Context"),
501+
value: '2',
502+
validateInput: async (value) => isNaN(parseInt(value)) ? nls.localize('mustBeInteger', "Must enter an integer") : undefined
503+
});
504+
if (lines === undefined) { return; }
505+
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
506+
await this.progressService.withProgress({ location: ProgressLocation.Window },
507+
() => refreshActiveEditorSearch(+lines, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService));
476508
}
477509
}
478510
}

src/vs/workbench/contrib/search/browser/searchEditor.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[
6565
};
6666

6767
type SearchResultSerialization = { text: string[], matchRanges: Range[] };
68+
6869
function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization {
6970
const serializedMatches = flatten(fileMatch.matches()
7071
.sort(searchMatchComparer)
@@ -76,17 +77,37 @@ function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x:
7677

7778
const targetLineNumberToOffset: Record<string, number> = {};
7879

80+
const context: { line: string, lineNumber: number }[] = [];
81+
fileMatch.context.forEach((line, lineNumber) => context.push({ line, lineNumber }));
82+
context.sort((a, b) => a.lineNumber - b.lineNumber);
83+
84+
let lastLine: number | undefined = undefined;
85+
7986
const seenLines = new Set<string>();
8087
serializedMatches.forEach(match => {
8188
if (!seenLines.has(match.line)) {
89+
while (context.length && context[0].lineNumber < +match.lineNumber) {
90+
const { line, lineNumber } = context.shift()!;
91+
if (lastLine !== undefined && lineNumber !== lastLine + 1) {
92+
text.push('');
93+
}
94+
text.push(` ${lineNumber} ${line}`);
95+
lastLine = lineNumber;
96+
}
97+
8298
targetLineNumberToOffset[match.lineNumber] = text.length;
8399
seenLines.add(match.line);
84100
text.push(match.line);
101+
lastLine = +match.lineNumber;
85102
}
86103

87104
matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber])));
88105
});
89106

107+
while (context.length) {
108+
const { line, lineNumber } = context.shift()!;
109+
text.push(` ${lineNumber} ${line}`);
110+
}
90111

91112
return { text, matchRanges };
92113
}
@@ -104,7 +125,7 @@ const flattenSearchResultSerializations = (serializations: SearchResultSerializa
104125
return { text, matchRanges };
105126
};
106127

107-
const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string): string[] => {
128+
const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => {
108129
if (!pattern) { return []; }
109130

110131
const removeNullFalseAndUndefined = <T>(a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[];
@@ -123,16 +144,32 @@ const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes
123144
]).join(' ')}`,
124145
includes ? `# Including: ${includes}` : undefined,
125146
excludes ? `# Excluding: ${excludes}` : undefined,
147+
contextLines ? `# ContextLines: ${contextLines}` : undefined,
126148
''
127149
]);
128150
};
129151

130-
const searchHeaderToContentPattern = (header: string[]): { pattern: string, flags: { regex: boolean, wholeWord: boolean, caseSensitive: boolean, ignoreExcludes: boolean }, includes: string, excludes: string } => {
131-
const query = {
152+
153+
type SearchHeader = {
154+
pattern: string;
155+
flags: {
156+
regex: boolean;
157+
wholeWord: boolean;
158+
caseSensitive: boolean;
159+
ignoreExcludes: boolean;
160+
};
161+
includes: string;
162+
excludes: string;
163+
context: number | undefined;
164+
};
165+
166+
const searchHeaderToContentPattern = (header: string[]): SearchHeader => {
167+
const query: SearchHeader = {
132168
pattern: '',
133169
flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false },
134170
includes: '',
135-
excludes: ''
171+
excludes: '',
172+
context: undefined
136173
};
137174

138175
const unescapeNewlines = (str: string) => str.replace(/\\\\/g, '\\').replace(/\\n/g, '\n');
@@ -145,6 +182,7 @@ const searchHeaderToContentPattern = (header: string[]): { pattern: string, flag
145182
case 'Query': query.pattern = unescapeNewlines(value); break;
146183
case 'Including': query.includes = value; break;
147184
case 'Excluding': query.excludes = value; break;
185+
case 'ContextLines': query.context = +value; break;
148186
case 'Flags': {
149187
query.flags = {
150188
regex: value.indexOf('RegExp') !== -1,
@@ -159,19 +197,20 @@ const searchHeaderToContentPattern = (header: string[]): { pattern: string, flag
159197
return query;
160198
};
161199

162-
const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelFormatter: (x: URI) => string): SearchResultSerialization => {
163-
const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern);
200+
const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string): SearchResultSerialization => {
201+
const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines);
164202
const allResults =
165203
flattenSearchResultSerializations(
166-
flatten(searchResult.folderMatches().sort(searchMatchComparer)
167-
.map(folderMatch => folderMatch.matches().sort(searchMatchComparer)
168-
.map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter)))));
204+
flatten(
205+
searchResult.folderMatches().sort(searchMatchComparer)
206+
.map(folderMatch => folderMatch.matches().sort(searchMatchComparer)
207+
.map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter)))));
169208

170209
return { matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), text: header.concat(allResults.text) };
171210
};
172211

173212
export const refreshActiveEditorSearch =
174-
async (editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => {
213+
async (contextLines: number | undefined, editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => {
175214
const model = editorService.activeTextEditorWidget?.getModel();
176215
if (!model) { return; }
177216

@@ -190,6 +229,8 @@ export const refreshActiveEditorSearch =
190229
isWordMatch: contentPattern.flags.wholeWord
191230
};
192231

232+
contextLines = contextLines ?? contentPattern.context ?? 0;
233+
193234
const options: ITextQueryBuilderOptions = {
194235
_reason: 'searchEditor',
195236
extraFileResources: instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
@@ -202,6 +243,8 @@ export const refreshActiveEditorSearch =
202243
matchLines: 1,
203244
charsPerLine: 1000
204245
},
246+
afterContext: contextLines,
247+
beforeContext: contextLines,
205248
isSmartCase: configurationService.getValue<ISearchConfigurationProperties>('search').smartCase,
206249
expandPatterns: true
207250
};
@@ -220,7 +263,7 @@ export const refreshActiveEditorSearch =
220263
await searchModel.search(query);
221264

222265
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
223-
const results = serializeSearchResultForEditor(searchModel.searchResult, '', '', labelFormatter);
266+
const results = serializeSearchResultForEditor(searchModel.searchResult, contentPattern.includes, contentPattern.excludes, contextLines, labelFormatter);
224267

225268
textModel.setValue(results.text.join(lineDelimiter));
226269
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
@@ -233,7 +276,7 @@ export const createEditorFromSearchResult =
233276

234277
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
235278

236-
const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, labelFormatter);
279+
const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter);
237280

238281
let possible = {
239282
contents: results.text.join(lineDelimiter),
@@ -255,7 +298,6 @@ export const createEditorFromSearchResult =
255298
model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
256299
};
257300

258-
// theming
259301
registerThemingParticipant((theme, collector) => {
260302
collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`);
261303

src/vs/workbench/contrib/search/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const CopyMatchCommandId = 'search.action.copyMatch';
1717
export const CopyAllCommandId = 'search.action.copyAll';
1818
export const OpenInEditorCommandId = 'search.action.openInEditor';
1919
export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch';
20+
export const RerunEditorSearchWithContextCommandId = 'search.action.rerunEditorSearchWithContext';
2021
export const ClearSearchHistoryCommandId = 'search.action.clearHistory';
2122
export const FocusSearchListCommandID = 'search.action.focusSearchList';
2223
export const ReplaceActionId = 'search.action.replace';

0 commit comments

Comments
 (0)