Skip to content

Commit

Permalink
Add re-run search editor search action
Browse files Browse the repository at this point in the history
  • Loading branch information
Jackson Kearl committed Nov 25, 2019
1 parent c41d9dc commit c2e11d4
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 20 deletions.
20 changes: 20 additions & 0 deletions extensions/search-result/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@
"*"
],
"contributes": {
"commands": [
{
"command": "searchResult.rerunSearch",
"title": "%searchResult.rerunSearch.title%",
"category": "Search Result",
"icon": {
"light": "./src/media/refresh-light.svg",
"dark": "./src/media/refresh-dark.svg"
}
}
],
"menus": {
"editor/title": [
{
"command": "searchResult.rerunSearch",
"when": "editorLangId == search-result",
"group": "navigation"
}
]
},
"languages": [
{
"id": "search-result",
Expand Down
3 changes: 2 additions & 1 deletion extensions/search-result/package.nls.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"displayName": "Search Result",
"description": "Provides syntax highlighting and language features for tabbed search results."
"description": "Provides syntax highlighting and language features for tabbed search results.",
"searchResult.rerunSearch.title": "Search Again"
}
18 changes: 16 additions & 2 deletions extensions/search-result/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ import * as pathUtils from 'path';

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

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

export function activate() {

vscode.languages.registerDefinitionProvider({ language: 'search-result' }, {
vscode.commands.registerCommand('searchResult.rerunSearch', () => vscode.commands.executeCommand('search.action.rerunEditorSearch'));

vscode.languages.registerCompletionItemProvider(LANGUAGE_SELECTOR, {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] {
const line = document.lineAt(position.line);
if (line.text.indexOf('# Flags:') === -1) { return []; }

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

vscode.languages.registerDefinitionProvider(LANGUAGE_SELECTOR, {
provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DefinitionLink[] {
const lineResult = parseSearchResults(document, token)[position.line];
if (!lineResult) { return []; }
Expand All @@ -27,7 +41,7 @@ export function activate() {
}
});

vscode.languages.registerDocumentLinkProvider({ language: 'search-result' }, {
vscode.languages.registerDocumentLinkProvider(LANGUAGE_SELECTOR, {
async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentLink[]> {
return parseSearchResults(document, token)
.filter(({ type }) => type === 'file')
Expand Down
4 changes: 4 additions & 0 deletions extensions/search-result/src/media/refresh-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions extensions/search-result/src/media/refresh-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/vs/workbench/contrib/search/browser/search.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition
import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler';
import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler';
import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions';
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';
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';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView';
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
Expand All @@ -56,6 +56,7 @@ import { ISearchConfiguration, ISearchConfigurationProperties, PANEL_ID, VIEWLET
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';

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

registry.registerWorkbenchAction(
SyncActionDescriptor.create(RerunEditorSearchAction, RerunEditorSearchAction.ID, RerunEditorSearchAction.LABEL,
{ primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_R },
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))),
'Search Editor: Search Again', category,
ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result')));


// Register Quick Open Handler
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler(
Expand Down
30 changes: 28 additions & 2 deletions src/vs/workbench/contrib/search/browser/searchActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
import { ILabelService } from 'vs/platform/label/common/label';
import { ICommandHandler } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
Expand All @@ -29,7 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditor';
import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';

export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
const searchView = getSearchView(viewletService, panelService);
Expand Down Expand Up @@ -451,6 +453,30 @@ export class OpenResultsInEditorAction extends Action {
}
}

export class RerunEditorSearchAction extends Action {

static readonly ID: string = Constants.RerunEditorSearchCommandId;
static readonly LABEL = nls.localize('search.rerunEditorSearch', "Search Again");

constructor(id: string, label: string,
@IInstantiationService private instantiationService: IInstantiationService,
@IEditorService private editorService: IEditorService,
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@ILabelService private labelService: ILabelService,
@IProgressService private progressService: IProgressService
) {
super(id, label);
}

async run() {
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
await this.progressService.withProgress({ location: ProgressLocation.Window },
() => refreshActiveEditorSearch(this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService));
}
}
}


export class FocusNextSearchResultAction extends Action {
static readonly ID = 'search.action.focusNextSearchResult';
Expand Down
124 changes: 110 additions & 14 deletions src/vs/workbench/contrib/search/browser/searchEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Match, searchMatchComparer, FileMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { Match, searchMatchComparer, FileMatch, SearchResult, SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { repeat } from 'vs/base/common/strings';
import { ILabelService } from 'vs/platform/label/common/label';
import { coalesce, flatten } from 'vs/base/common/arrays';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { URI } from 'vs/base/common/uri';
import { ITextQuery } from 'vs/workbench/services/search/common/search';
import { ITextQuery, IPatternInfo, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import * as network from 'vs/base/common/network';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';

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

const translateRangeLines = (n: number) => (range: Range) => new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn);
const translateRangeLines =
(n: number) =>
(range: Range) =>
new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn);

type SearchResultSerialization = { text: string[], matchRanges: Range[] };

function matchToSearchResultFormat(match: Match): { line: string, ranges: Range[], lineNumber: string }[] {
const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[], lineNumber: string }[] => {
const getLinePrefix = (i: number) => `${match.range().startLineNumber + i}`;

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

return results;
}
};

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

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

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

(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp)
(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles)
&& `# Flags: ${coalesce([
pattern.contentPattern.isCaseSensitive && 'CaseSensitive',
pattern.contentPattern.isWordMatch && 'WordMatch',
pattern.contentPattern.isRegExp && 'RegExp'
pattern.contentPattern.isRegExp && 'RegExp',
pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings'
]).join(' ')}`,
includes ? `# Including: ${includes}` : undefined,
excludes ? `# Excluding: ${excludes}` : undefined,
''
]);
}
};

const searchHeaderToContentPattern = (header: string[]): { pattern: string, flags: { regex: boolean, wholeWord: boolean, caseSensitive: boolean, ignoreExcludes: boolean }, includes: string, excludes: string } => {
const query = {
pattern: '',
flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false },
includes: '',
excludes: ''
};

const unescapeNewlines = (str: string) => str.replace(/\\\\/g, '\\').replace(/\\n/g, '\n');
const parseYML = /^# ([^:]*): (.*)$/;
for (const line of header) {
const parsed = parseYML.exec(line);
if (!parsed) { continue; }
const [, key, value] = parsed;
switch (key) {
case 'Query': query.pattern = unescapeNewlines(value); break;
case 'Including': query.includes = value; break;
case 'Excluding': query.excludes = value; break;
case 'Flags': {
query.flags = {
regex: value.indexOf('RegExp') !== -1,
caseSensitive: value.indexOf('CaseSensitive') !== -1,
ignoreExcludes: value.indexOf('IgnoreExcludeSettings') !== -1,
wholeWord: value.indexOf('WordMatch') !== -1
};
}
}
}

return query;
};

const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelFormatter: (x: URI) => string): SearchResultSerialization => {
const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern);
const allResults =
flattenSearchResultSerializations(
flatten(searchResult.folderMatches()
.map(folderMatch => folderMatch.matches()
flatten(searchResult.folderMatches().sort(searchMatchComparer)
.map(folderMatch => folderMatch.matches().sort(searchMatchComparer)
.map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter)))));

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

export const refreshActiveEditorSearch =
async (editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => {
const model = editorService.activeTextEditorWidget?.getModel();
if (!model) { return; }

const textModel = model as ITextModel;

const header = textModel.getValueInRange(new Range(1, 1, 5, 1))
.split(lineDelimiter)
.filter(line => line.indexOf('# ') === 0);

const contentPattern = searchHeaderToContentPattern(header);

const content: IPatternInfo = {
pattern: contentPattern.pattern,
isRegExp: contentPattern.flags.regex,
isCaseSensitive: contentPattern.flags.caseSensitive,
isWordMatch: contentPattern.flags.wholeWord
};

const options: ITextQueryBuilderOptions = {
_reason: 'searchEditor',
extraFileResources: instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
maxResults: 10000,
disregardIgnoreFiles: contentPattern.flags.ignoreExcludes,
disregardExcludeSettings: contentPattern.flags.ignoreExcludes,
excludePattern: contentPattern.excludes,
includePattern: contentPattern.includes,
previewOptions: {
matchLines: 1,
charsPerLine: 1000
},
isSmartCase: configurationService.getValue<ISearchConfigurationProperties>('search').smartCase,
expandPatterns: true
};

const folderResources = contextService.getWorkspace().folders;

let query: ITextQuery;
try {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
query = queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
} catch (err) {
return;
}

const searchModel = instantiationService.createInstance(SearchModel);
await searchModel.search(query);

const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
const results = serializeSearchResultForEditor(searchModel.searchResult, '', '', labelFormatter);

textModel.setValue(results.text.join(lineDelimiter));
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'findMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
};


export const createEditorFromSearchResult =
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService) => {
const searchTerm = searchResult.query?.contentPattern.pattern.replace(/[^\w-_.]/g, '') || 'Search';
Expand All @@ -154,5 +251,4 @@ export const createEditorFromSearchResult =
const model = control.getModel() as ITextModel;

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

};
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/search/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath';
export const CopyMatchCommandId = 'search.action.copyMatch';
export const CopyAllCommandId = 'search.action.copyAll';
export const OpenInEditorCommandId = 'search.action.openInEditor';
export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch';
export const ClearSearchHistoryCommandId = 'search.action.clearHistory';
export const FocusSearchListCommandID = 'search.action.focusSearchList';
export const ReplaceActionId = 'search.action.replace';
Expand Down

0 comments on commit c2e11d4

Please sign in to comment.