Skip to content

Commit c2e11d4

Browse files
author
Jackson Kearl
committed
Add re-run search editor search action
1 parent c41d9dc commit c2e11d4

9 files changed

Lines changed: 194 additions & 20 deletions

File tree

extensions/search-result/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@
1616
"*"
1717
],
1818
"contributes": {
19+
"commands": [
20+
{
21+
"command": "searchResult.rerunSearch",
22+
"title": "%searchResult.rerunSearch.title%",
23+
"category": "Search Result",
24+
"icon": {
25+
"light": "./src/media/refresh-light.svg",
26+
"dark": "./src/media/refresh-dark.svg"
27+
}
28+
}
29+
],
30+
"menus": {
31+
"editor/title": [
32+
{
33+
"command": "searchResult.rerunSearch",
34+
"when": "editorLangId == search-result",
35+
"group": "navigation"
36+
}
37+
]
38+
},
1939
"languages": [
2040
{
2141
"id": "search-result",
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"displayName": "Search Result",
3-
"description": "Provides syntax highlighting and language features for tabbed search results."
3+
"description": "Provides syntax highlighting and language features for tabbed search results.",
4+
"searchResult.rerunSearch.title": "Search Again"
45
}

extensions/search-result/src/extension.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@ import * as pathUtils from 'path';
88

99
const FILE_LINE_REGEX = /^(\S.*):$/;
1010
const RESULT_LINE_REGEX = /^(\s+)(\d+):(\s+)(.*)$/;
11+
const LANGUAGE_SELECTOR = { language: 'search-result' };
1112

1213
let cachedLastParse: { version: number, parse: ParsedSearchResults } | undefined;
1314

1415
export function activate() {
1516

16-
vscode.languages.registerDefinitionProvider({ language: 'search-result' }, {
17+
vscode.commands.registerCommand('searchResult.rerunSearch', () => vscode.commands.executeCommand('search.action.rerunEditorSearch'));
18+
19+
vscode.languages.registerCompletionItemProvider(LANGUAGE_SELECTOR, {
20+
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] {
21+
const line = document.lineAt(position.line);
22+
if (line.text.indexOf('# Flags:') === -1) { return []; }
23+
24+
return ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch']
25+
.filter(flag => line.text.indexOf(flag) === -1)
26+
.map(flag => ({ label: flag, insertText: flag + ' ' }));
27+
}
28+
});
29+
30+
vscode.languages.registerDefinitionProvider(LANGUAGE_SELECTOR, {
1731
provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DefinitionLink[] {
1832
const lineResult = parseSearchResults(document, token)[position.line];
1933
if (!lineResult) { return []; }
@@ -27,7 +41,7 @@ export function activate() {
2741
}
2842
});
2943

30-
vscode.languages.registerDocumentLinkProvider({ language: 'search-result' }, {
44+
vscode.languages.registerDocumentLinkProvider(LANGUAGE_SELECTOR, {
3145
async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentLink[]> {
3246
return parseSearchResults(document, token)
3347
.filter(({ type }) => type === 'file')
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

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

Lines changed: 9 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 } 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 } 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';
@@ -56,6 +56,7 @@ import { ISearchConfiguration, ISearchConfigurationProperties, PANEL_ID, VIEWLET
5656
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
5757
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
5858
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
59+
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
5960

6061
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
6162
registerSingleton(ISearchHistoryService, SearchHistoryService, true);
@@ -630,6 +631,13 @@ registry.registerWorkbenchAction(
630631
'Search: Open Results in Editor', category,
631632
ContextKeyExpr.and(Constants.EnableSearchEditorPreview));
632633

634+
registry.registerWorkbenchAction(
635+
SyncActionDescriptor.create(RerunEditorSearchAction, RerunEditorSearchAction.ID, RerunEditorSearchAction.LABEL,
636+
{ primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_R },
637+
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))),
638+
'Search Editor: Search Again', category,
639+
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result')));
640+
633641

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

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
1313
import { ILabelService } from 'vs/platform/label/common/label';
1414
import { ICommandHandler } from 'vs/platform/commands/common/commands';
1515
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
16-
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
16+
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1717
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1818
import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
1919
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
@@ -29,7 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
2929
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
3030
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
3131
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
32-
import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditor';
32+
import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor';
33+
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
34+
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
3335

3436
export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
3537
const searchView = getSearchView(viewletService, panelService);
@@ -451,6 +453,30 @@ export class OpenResultsInEditorAction extends Action {
451453
}
452454
}
453455

456+
export class RerunEditorSearchAction extends Action {
457+
458+
static readonly ID: string = Constants.RerunEditorSearchCommandId;
459+
static readonly LABEL = nls.localize('search.rerunEditorSearch', "Search Again");
460+
461+
constructor(id: string, label: string,
462+
@IInstantiationService private instantiationService: IInstantiationService,
463+
@IEditorService private editorService: IEditorService,
464+
@IConfigurationService private configurationService: IConfigurationService,
465+
@IWorkspaceContextService private contextService: IWorkspaceContextService,
466+
@ILabelService private labelService: ILabelService,
467+
@IProgressService private progressService: IProgressService
468+
) {
469+
super(id, label);
470+
}
471+
472+
async run() {
473+
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
474+
await this.progressService.withProgress({ location: ProgressLocation.Window },
475+
() => refreshActiveEditorSearch(this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService));
476+
}
477+
}
478+
}
479+
454480

455481
export class FocusNextSearchResultAction extends Action {
456482
static readonly ID = 'search.action.focusNextSearchResult';

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

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Match, searchMatchComparer, FileMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
6+
import { Match, searchMatchComparer, FileMatch, SearchResult, SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
77
import { repeat } from 'vs/base/common/strings';
88
import { ILabelService } from 'vs/platform/label/common/label';
99
import { coalesce, flatten } from 'vs/base/common/arrays';
1010
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1111
import { URI } from 'vs/base/common/uri';
12-
import { ITextQuery } from 'vs/workbench/services/search/common/search';
12+
import { ITextQuery, IPatternInfo, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
1313
import * as network from 'vs/base/common/network';
1414
import { Range } from 'vs/editor/common/core/range';
1515
import { ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
16+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
17+
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
18+
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
19+
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
20+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1621

1722
// Using \r\n on Windows inserts an extra newline between results.
1823
const lineDelimiter = '\n';
1924

20-
const translateRangeLines = (n: number) => (range: Range) => new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn);
25+
const translateRangeLines =
26+
(n: number) =>
27+
(range: Range) =>
28+
new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn);
2129

22-
type SearchResultSerialization = { text: string[], matchRanges: Range[] };
23-
24-
function matchToSearchResultFormat(match: Match): { line: string, ranges: Range[], lineNumber: string }[] {
30+
const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[], lineNumber: string }[] => {
2531
const getLinePrefix = (i: number) => `${match.range().startLineNumber + i}`;
2632

2733
const fullMatchLines = match.fullPreviewLines();
@@ -54,8 +60,9 @@ function matchToSearchResultFormat(match: Match): { line: string, ranges: Range[
5460
});
5561

5662
return results;
57-
}
63+
};
5864

65+
type SearchResultSerialization = { text: string[], matchRanges: Range[] };
5966
function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization {
6067
const serializedMatches = flatten(fileMatch.matches()
6168
.sort(searchMatchComparer)
@@ -95,7 +102,7 @@ const flattenSearchResultSerializations = (serializations: SearchResultSerializa
95102
return { text, matchRanges };
96103
};
97104

98-
function contentPatternToSearchResultHeader(pattern: ITextQuery | null, includes: string, excludes: string): string[] {
105+
const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string): string[] => {
99106
if (!pattern) { return []; }
100107

101108
const removeNullFalseAndUndefined = <T>(a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[];
@@ -105,29 +112,119 @@ function contentPatternToSearchResultHeader(pattern: ITextQuery | null, includes
105112
return removeNullFalseAndUndefined([
106113
`# Query: ${escapeNewlines(pattern.contentPattern.pattern)}`,
107114

108-
(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp)
115+
(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles)
109116
&& `# Flags: ${coalesce([
110117
pattern.contentPattern.isCaseSensitive && 'CaseSensitive',
111118
pattern.contentPattern.isWordMatch && 'WordMatch',
112-
pattern.contentPattern.isRegExp && 'RegExp'
119+
pattern.contentPattern.isRegExp && 'RegExp',
120+
pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings'
113121
]).join(' ')}`,
114122
includes ? `# Including: ${includes}` : undefined,
115123
excludes ? `# Excluding: ${excludes}` : undefined,
116124
''
117125
]);
118-
}
126+
};
127+
128+
const searchHeaderToContentPattern = (header: string[]): { pattern: string, flags: { regex: boolean, wholeWord: boolean, caseSensitive: boolean, ignoreExcludes: boolean }, includes: string, excludes: string } => {
129+
const query = {
130+
pattern: '',
131+
flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false },
132+
includes: '',
133+
excludes: ''
134+
};
135+
136+
const unescapeNewlines = (str: string) => str.replace(/\\\\/g, '\\').replace(/\\n/g, '\n');
137+
const parseYML = /^# ([^:]*): (.*)$/;
138+
for (const line of header) {
139+
const parsed = parseYML.exec(line);
140+
if (!parsed) { continue; }
141+
const [, key, value] = parsed;
142+
switch (key) {
143+
case 'Query': query.pattern = unescapeNewlines(value); break;
144+
case 'Including': query.includes = value; break;
145+
case 'Excluding': query.excludes = value; break;
146+
case 'Flags': {
147+
query.flags = {
148+
regex: value.indexOf('RegExp') !== -1,
149+
caseSensitive: value.indexOf('CaseSensitive') !== -1,
150+
ignoreExcludes: value.indexOf('IgnoreExcludeSettings') !== -1,
151+
wholeWord: value.indexOf('WordMatch') !== -1
152+
};
153+
}
154+
}
155+
}
156+
157+
return query;
158+
};
119159

120160
const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelFormatter: (x: URI) => string): SearchResultSerialization => {
121161
const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern);
122162
const allResults =
123163
flattenSearchResultSerializations(
124-
flatten(searchResult.folderMatches()
125-
.map(folderMatch => folderMatch.matches()
164+
flatten(searchResult.folderMatches().sort(searchMatchComparer)
165+
.map(folderMatch => folderMatch.matches().sort(searchMatchComparer)
126166
.map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter)))));
127167

128168
return { matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), text: header.concat(allResults.text) };
129169
};
130170

171+
export const refreshActiveEditorSearch =
172+
async (editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => {
173+
const model = editorService.activeTextEditorWidget?.getModel();
174+
if (!model) { return; }
175+
176+
const textModel = model as ITextModel;
177+
178+
const header = textModel.getValueInRange(new Range(1, 1, 5, 1))
179+
.split(lineDelimiter)
180+
.filter(line => line.indexOf('# ') === 0);
181+
182+
const contentPattern = searchHeaderToContentPattern(header);
183+
184+
const content: IPatternInfo = {
185+
pattern: contentPattern.pattern,
186+
isRegExp: contentPattern.flags.regex,
187+
isCaseSensitive: contentPattern.flags.caseSensitive,
188+
isWordMatch: contentPattern.flags.wholeWord
189+
};
190+
191+
const options: ITextQueryBuilderOptions = {
192+
_reason: 'searchEditor',
193+
extraFileResources: instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
194+
maxResults: 10000,
195+
disregardIgnoreFiles: contentPattern.flags.ignoreExcludes,
196+
disregardExcludeSettings: contentPattern.flags.ignoreExcludes,
197+
excludePattern: contentPattern.excludes,
198+
includePattern: contentPattern.includes,
199+
previewOptions: {
200+
matchLines: 1,
201+
charsPerLine: 1000
202+
},
203+
isSmartCase: configurationService.getValue<ISearchConfigurationProperties>('search').smartCase,
204+
expandPatterns: true
205+
};
206+
207+
const folderResources = contextService.getWorkspace().folders;
208+
209+
let query: ITextQuery;
210+
try {
211+
const queryBuilder = instantiationService.createInstance(QueryBuilder);
212+
query = queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
213+
} catch (err) {
214+
return;
215+
}
216+
217+
const searchModel = instantiationService.createInstance(SearchModel);
218+
await searchModel.search(query);
219+
220+
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
221+
const results = serializeSearchResultForEditor(searchModel.searchResult, '', '', labelFormatter);
222+
223+
textModel.setValue(results.text.join(lineDelimiter));
224+
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'findMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
225+
};
226+
227+
131228
export const createEditorFromSearchResult =
132229
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService) => {
133230
const searchTerm = searchResult.query?.contentPattern.pattern.replace(/[^\w-_.]/g, '') || 'Search';
@@ -154,5 +251,4 @@ export const createEditorFromSearchResult =
154251
const model = control.getModel() as ITextModel;
155252

156253
model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'findMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
157-
158254
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath';
1616
export const CopyMatchCommandId = 'search.action.copyMatch';
1717
export const CopyAllCommandId = 'search.action.copyAll';
1818
export const OpenInEditorCommandId = 'search.action.openInEditor';
19+
export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch';
1920
export const ClearSearchHistoryCommandId = 'search.action.clearHistory';
2021
export const FocusSearchListCommandID = 'search.action.focusSearchList';
2122
export const ReplaceActionId = 'search.action.replace';

0 commit comments

Comments
 (0)