Search Contributions Cleanup (#165538)

Major cleanup on search contribs/actions, namely:
- converting all `Action` and related registrations to `Action2`
- Converting the `Actionbar` that appears on hover of a search result to `MenuWorkbenchToolBar`
- Overall organization
- Converting any `ICommandHandler` to functions if they don't need to be `ICommandHandler`
- Adding titles to all search commands
This commit is contained in:
Andrea Mah
2022-11-16 14:37:44 -08:00
committed by GitHub
parent 838b48504c
commit 9318bcc518
18 changed files with 2135 additions and 1778 deletions

View File

@@ -103,6 +103,7 @@ export class MenuId {
static readonly SCMSourceControl = new MenuId('SCMSourceControl');
static readonly SCMTitle = new MenuId('SCMTitle');
static readonly SearchContext = new MenuId('SearchContext');
static readonly SearchActionMenu = new MenuId('SearchActionContext');
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu');
static readonly StickyScrollContext = new MenuId('StickyScrollContext');

View File

@@ -1,778 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import { Action, IAction } from 'vs/base/common/actions';
import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings';
import { isWindows, OS } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label';
import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IViewsService } from 'vs/workbench/common/views';
import { searchRemoveIcon, searchReplaceAllIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { arrayContainsElementOrParent, FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWithResource, FolderMatchWorkspaceRoot, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { OpenEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants';
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution';
import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ISearchConfiguration, ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { URI } from 'vs/base/common/uri';
export function isSearchViewFocused(viewsService: IViewsService): boolean {
const searchView = getSearchView(viewsService);
const activeElement = document.activeElement;
return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer()));
}
export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string {
if (typeof inputKeyBinding === 'number') {
const keybinding = createKeybinding(inputKeyBinding, OS);
if (keybinding) {
const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding);
return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined);
}
return doAppendKeyBindingLabel(label, undefined);
} else {
return doAppendKeyBindingLabel(label, inputKeyBinding);
}
}
export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise<SearchView | undefined> {
return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined));
}
export function getSearchView(viewsService: IViewsService): SearchView | undefined {
return viewsService.getActiveViewWithId(VIEW_ID) as SearchView ?? undefined;
}
function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string {
return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label;
}
export const toggleCaseSensitiveCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleCaseSensitive();
};
export const toggleWholeWordCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleWholeWords();
};
export const toggleRegexCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleRegex();
};
export const togglePreserveCaseCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.togglePreserveCase();
};
export class FocusNextInputAction extends Action {
static readonly ID = 'search.focus.nextInputBox';
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService,
@IEditorService private readonly editorService: IEditorService,
) {
super(id, label);
}
override async run(): Promise<any> {
const input = this.editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
(this.editorService.activeEditorPane as SearchEditor).focusNextInput();
}
const searchView = getSearchView(this.viewsService);
searchView?.focusNextInputBox();
}
}
export class FocusPreviousInputAction extends Action {
static readonly ID = 'search.focus.previousInputBox';
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService,
@IEditorService private readonly editorService: IEditorService,
) {
super(id, label);
}
override async run(): Promise<any> {
const input = this.editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
(this.editorService.activeEditorPane as SearchEditor).focusPrevInput();
}
const searchView = getSearchView(this.viewsService);
searchView?.focusPreviousInputBox();
}
}
export interface IFindInFilesArgs {
query?: string;
replace?: string;
preserveCase?: boolean;
triggerSearch?: boolean;
filesToInclude?: string;
filesToExclude?: string;
isRegex?: boolean;
isCaseSensitive?: boolean;
matchWholeWord?: boolean;
useExcludeSettingsAndIgnoreFiles?: boolean;
onlyOpenEditors?: boolean;
}
export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFilesArgs = {}) => {
const searchConfig = accessor.get(IConfigurationService).getValue<ISearchConfiguration>().search;
const mode = searchConfig.mode;
if (mode === 'view') {
const viewsService = accessor.get(IViewsService);
openSearchView(viewsService, false).then(openedView => {
if (openedView) {
const searchAndReplaceWidget = openedView.searchAndReplaceWidget;
searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string');
let updatedText = false;
if (typeof args.query === 'string') {
openedView.setSearchParameters(args);
} else {
updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' });
}
openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText);
}
});
} else {
const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({
location: mode === 'newEditor' ? 'new' : 'reuse',
query: args.query,
filesToInclude: args.filesToInclude,
filesToExclude: args.filesToExclude,
matchWholeWord: args.matchWholeWord,
isCaseSensitive: args.isCaseSensitive,
isRegexp: args.isRegex,
useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles,
onlyOpenEditors: args.onlyOpenEditors,
showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles),
});
accessor.get(ICommandService).executeCommand(OpenEditorCommandId, convertArgs(args));
}
};
export class CloseReplaceAction extends Action {
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label);
}
override run(): Promise<any> {
const searchView = getSearchView(this.viewsService);
if (searchView) {
searchView.searchAndReplaceWidget.toggleReplace(false);
searchView.searchAndReplaceWidget.focus();
}
return Promise.resolve(null);
}
}
export function expandAll(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
if (searchView) {
const viewer = searchView.getControl();
viewer.expandAll();
}
}
export function clearSearchResults(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.clearSearchResults();
}
export function cancelSearch(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.cancelSearch();
}
export function refreshSearch(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.triggerQueryChange({ preserveFocus: false });
}
export function collapseDeepestExpandedLevel(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
if (searchView) {
const viewer = searchView.getControl();
/**
* one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren,
* i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels.
*/
const navigator = viewer.navigate();
let node = navigator.first();
let canCollapseFileMatchLevel = false;
let canCollapseFirstLevel = false;
if (node instanceof FolderMatchWorkspaceRoot) {
while (node = navigator.next()) {
if (node instanceof Match) {
canCollapseFileMatchLevel = true;
break;
}
if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) {
let nodeToTest = node;
if (node instanceof FolderMatch) {
nodeToTest = node.compressionStartParent ?? node;
}
const immediateParent = nodeToTest.parent();
if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) {
canCollapseFirstLevel = true;
}
}
}
}
if (canCollapseFileMatchLevel) {
node = navigator.first();
do {
if (node instanceof FileMatch) {
viewer.collapse(node);
}
} while (node = navigator.next());
} else if (canCollapseFirstLevel) {
node = navigator.first();
if (node) {
do {
let nodeToTest = node;
if (node instanceof FolderMatch) {
nodeToTest = node.compressionStartParent ?? node;
}
const immediateParent = nodeToTest.parent();
if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) {
if (viewer.hasElement(node)) {
viewer.collapse(node, true);
} else {
viewer.collapseAll();
}
}
} while (node = navigator.next());
}
} else {
viewer.collapseAll();
}
const firstFocusParent = viewer.getFocus()[0]?.parent();
if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) &&
viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) {
viewer.domFocus();
viewer.focusFirst();
viewer.setSelection(viewer.getFocus());
}
}
}
export async function focusNextSearchResult(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
return (editorService.activeEditorPane as SearchEditor).focusNextResult();
}
return openSearchView(accessor.get(IViewsService)).then(searchView => {
searchView?.selectNextMatch();
});
}
export async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
return (editorService.activeEditorPane as SearchEditor).focusPreviousResult();
}
return openSearchView(accessor.get(IViewsService)).then(searchView => {
searchView?.selectPreviousMatch();
});
}
export async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise<any> {
return openSearchView(accessor.get(IViewsService), false).then(openedView => {
if (openedView) {
const searchAndReplaceWidget = openedView.searchAndReplaceWidget;
searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget);
const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget });
openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText);
}
});
}
class ReplaceActionRunner {
constructor(
private viewer: WorkbenchCompressibleObjectTree<RenderableMatch>,
private viewlet: SearchView | undefined,
// Services
@IReplaceService private readonly replaceService: IReplaceService,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IViewsService private readonly viewsService: IViewsService
) { }
async performReplace(element: RenderableMatch): Promise<any> {
// since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace.
const opInfo = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue<ISearchConfigurationProperties>('search'));
const elementsToReplace = opInfo.elements;
let focusElement = this.viewer.getFocus()[0];
if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) {
focusElement = element;
}
if (elementsToReplace.length === 0) {
return;
}
let nextFocusElement;
if (focusElement) {
nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToReplace);
}
const searchResult = getSearchView(this.viewsService)?.searchResult;
if (searchResult) {
searchResult.batchReplace(elementsToReplace);
}
if (focusElement) {
if (!nextFocusElement) {
nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement);
}
if (nextFocusElement) {
this.viewer.reveal(nextFocusElement);
this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent());
if (nextFocusElement instanceof Match) {
const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
if (!useReplacePreview || this.hasToOpenFile(nextFocusElement)) {
this.viewlet?.open(nextFocusElement, true);
} else {
this.replaceService.openReplacePreview(nextFocusElement, true);
}
} else if (nextFocusElement instanceof FileMatch) {
this.viewlet?.open(nextFocusElement, true);
}
}
}
this.viewer.domFocus();
}
private hasToOpenFile(currBottomElem: RenderableMatch): boolean {
if (!(currBottomElem instanceof Match)) {
return false;
}
const activeEditor = this.editorService.activeEditor;
const file = activeEditor?.resource;
if (file) {
return this.uriIdentityService.extUri.isEqual(file, currBottomElem.parent().resource);
}
return false;
}
}
export class RemoveAction implements IAction {
static readonly LABEL = nls.localize('RemoveAction.label', "Dismiss");
readonly id = Constants.RemoveActionId;
public class = ThemeIcon.asClassName(searchRemoveIcon);
public label: string;
public tooltip = '';
public enabled = true;
constructor(
private readonly viewer: WorkbenchCompressibleObjectTree<RenderableMatch>,
private readonly element: RenderableMatch,
@IKeybindingService keyBindingService: IKeybindingService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IViewsService private readonly viewsService: IViewsService,
) {
this.label = appendKeyBindingLabel(RemoveAction.LABEL, keyBindingService.lookupKeybinding(Constants.RemoveActionId), keyBindingService);
}
run(): void {
const opInfo = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue<ISearchConfigurationProperties>('search'));
const elementsToRemove = opInfo.elements;
let focusElement = this.viewer.getFocus()[0];
if (elementsToRemove.length === 0) {
return;
}
if (!focusElement || (focusElement instanceof SearchResult)) {
focusElement = this.element;
}
let nextFocusElement;
if (opInfo.mustReselect && focusElement) {
nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToRemove);
}
const searchResult = getSearchView(this.viewsService)?.searchResult;
if (searchResult) {
searchResult.batchRemove(elementsToRemove);
}
if (opInfo.mustReselect && focusElement) {
if (!nextFocusElement) {
nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement);
}
if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) {
this.viewer.reveal(nextFocusElement);
this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent());
}
}
this.viewer.domFocus();
return;
}
}
export class ReplaceAction extends Action {
static readonly LABEL = nls.localize('match.replace.label', "Replace");
static runQ = Promise.resolve();
private replaceRunner: ReplaceActionRunner;
constructor(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, private element: Match, viewlet: SearchView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService keyBindingService: IKeybindingService,
) {
super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceIcon));
this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, viewlet);
}
override async run(): Promise<any> {
return this.replaceRunner.performReplace(this.element);
}
}
export class ReplaceAllAction extends Action {
static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All");
private replaceRunner: ReplaceActionRunner;
constructor(
viewlet: SearchView,
private fileMatch: FileMatch,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService keyBindingService: IKeybindingService,
) {
super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon));
this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewlet.getControl(), viewlet);
}
override async run(): Promise<any> {
return this.replaceRunner.performReplace(this.fileMatch);
}
}
export class ReplaceAllInFolderAction extends Action {
static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All");
private replaceRunner: ReplaceActionRunner;
constructor(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, private folderMatch: FolderMatch,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService keyBindingService: IKeybindingService
) {
super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon));
this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, undefined);
}
override run(): Promise<any> {
return this.replaceRunner.performReplace(this.folderMatch);
}
}
export const copyPathCommand: ICommandHandler = async (accessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) => {
if (!fileMatch) {
const selection = getSelectedRow(accessor);
if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) {
return;
}
fileMatch = selection;
}
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true });
await clipboardService.writeText(text);
};
function matchToString(match: Match, indent = 0): string {
const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`;
const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + '';
const fullMatchLines = match.fullPreviewLines();
const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => {
const thisSize = i === 0 ?
getFirstLinePrefix().length :
getOtherLinePrefix(i).length;
return Math.max(thisSize, largest);
}, 0);
const formattedLines = fullMatchLines
.map((line, i) => {
const prefix = i === 0 ?
getFirstLinePrefix() :
getOtherLinePrefix(i);
const paddingStr = ' '.repeat(largestPrefixSize - prefix.length);
const indentStr = ' '.repeat(indent);
return `${indentStr}${prefix}: ${paddingStr}${line}`;
});
return formattedLines.join('\n');
}
function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } {
if (match instanceof FileMatch) {
return fileMatchToString(match, labelService);
} else {
return folderMatchToString(match, labelService);
}
}
const lineDelimiter = isWindows ? '\r\n' : '\n';
function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } {
const matchTextRows = fileMatch.matches()
.sort(searchMatchComparer)
.map(match => matchToString(match, 2));
const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true });
return {
text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`,
count: matchTextRows.length
};
}
function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } {
const results: string[] = [];
let numMatches = 0;
const matches = folderMatch.matches().sort(searchMatchComparer);
matches.forEach(match => {
const result = fileFolderMatchToString(match, labelService);
numMatches += result.count;
results.push(result.text);
});
return {
text: results.join(lineDelimiter + lineDelimiter),
count: numMatches
};
}
export const copyMatchCommand: ICommandHandler = async (accessor, match: RenderableMatch | undefined) => {
if (!match) {
const selection = getSelectedRow(accessor);
if (!selection) {
return;
}
match = selection;
}
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
let text: string | undefined;
if (match instanceof Match) {
text = matchToString(match);
} else if (match instanceof FileMatch) {
text = fileMatchToString(match, labelService).text;
} else if (match instanceof FolderMatch) {
text = folderMatchToString(match, labelService).text;
}
if (text) {
await clipboardService.writeText(text);
}
};
function allFolderMatchesToString(folderMatches: Array<FolderMatchWithResource | FolderMatch>, labelService: ILabelService): string {
const folderResults: string[] = [];
folderMatches = folderMatches.sort(searchMatchComparer);
for (let i = 0; i < folderMatches.length; i++) {
const folderResult = folderMatchToString(folderMatches[i], labelService);
if (folderResult.count) {
folderResults.push(folderResult.text);
}
}
return folderResults.join(lineDelimiter + lineDelimiter);
}
function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
return searchView?.getControl().getSelection()[0];
}
export const copyAllCommand: ICommandHandler = async (accessor) => {
const viewsService = accessor.get(IViewsService);
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
const searchView = getSearchView(viewsService);
if (searchView) {
const root = searchView.searchResult;
const text = allFolderMatchesToString(root.folderMatches(), labelService);
await clipboardService.writeText(text);
}
};
export const clearHistoryCommand: ICommandHandler = accessor => {
const searchHistoryService = accessor.get(ISearchHistoryService);
searchHistoryService.clearHistory();
};
export const focusSearchListCommand: ICommandHandler = accessor => {
const viewsService = accessor.get(IViewsService);
openSearchView(viewsService).then(searchView => {
searchView?.moveFocusToResults();
});
};
export function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree<RenderableMatch, void>, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] {
return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements
.map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource))
.filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null));
}
function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree<RenderableMatch, void>, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } {
let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder));
const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove.
// if selection doesn't include multiple elements, just return current focus element.
if (currElement && !(elements.length > 1 && elements.includes(currElement))) {
elements = [currElement];
}
return { elements, mustReselect };
}
function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) {
if (elem1 instanceof Match) {
if (elem2 instanceof Match) {
return 0;
} else {
return -1;
}
} else if (elem1 instanceof FileMatch) {
if (elem2 instanceof Match) {
return 1;
} else if (elem2 instanceof FileMatch) {
return 0;
} else {
return -1;
}
} else {
// FolderMatch
if (elem2 instanceof FolderMatch) {
return 0;
} else {
return 1;
}
}
}
/**
* Returns element to focus after removing the given element
*/
export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined {
const navigator: ITreeNavigator<any> = viewer.navigate(element);
if (element instanceof FolderMatch) {
while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { }
} else if (element instanceof FileMatch) {
while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) {
viewer.expand(navigator.current());
}
} else {
while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) {
viewer.expand(navigator.current());
}
}
return navigator.current();
}
/***
* Finds the last element in the tree with the same type as `element`
*/
export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, element: RenderableMatch): RenderableMatch | undefined {
let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null;
while (lastElem) {
const compareVal = compareLevels(element, lastElem);
if (compareVal === -1) {
viewer.expand(lastElem);
lastElem = viewer.lastVisibleElement;
} else if (compareVal === 1) {
lastElem = viewer.getParentElement(lastElem);
} else {
return lastElem;
}
}
return undefined;
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings';
import { OS } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { IViewsService } from 'vs/workbench/common/views';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import { RenderableMatch, searchComparer } from 'vs/workbench/contrib/search/common/searchModel';
import { ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search';
export const category = { value: nls.localize('search', "Search"), original: 'Search' };
export function isSearchViewFocused(viewsService: IViewsService): boolean {
const searchView = getSearchView(viewsService);
const activeElement = document.activeElement;
return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer()));
}
export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string {
if (typeof inputKeyBinding === 'number') {
const keybinding = createKeybinding(inputKeyBinding, OS);
if (keybinding) {
const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding);
return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined);
}
return doAppendKeyBindingLabel(label, undefined);
} else {
return doAppendKeyBindingLabel(label, inputKeyBinding);
}
}
export function getSearchView(viewsService: IViewsService): SearchView | undefined {
return viewsService.getActiveViewWithId(VIEW_ID) as SearchView;
}
export function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree<RenderableMatch, void>, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } {
let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder));
const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove.
// if selection doesn't include multiple elements, just return current focus element.
if (currElement && !(elements.length > 1 && elements.includes(currElement))) {
elements = [currElement];
}
return { elements, mustReselect };
}
export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise<SearchView | undefined> {
return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined));
}
function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string {
return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label;
}

View File

@@ -0,0 +1,256 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { IViewsService } from 'vs/workbench/common/views';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer } from 'vs/workbench/contrib/search/common/searchModel';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
import { isWindows } from 'vs/base/common/platform';
//#region Actions
registerAction2(class CopyMatchCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.CopyMatchCommandId,
title: {
value: nls.localize('copyMatchLabel', "Copy"),
original: 'Copy'
},
category: category.value,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.FileMatchOrMatchFocusKey,
primary: KeyMod.CtrlCmd | KeyCode.KeyC,
},
menu: [{
id: MenuId.SearchContext,
when: Constants.FileMatchOrMatchFocusKey,
group: 'search_2',
order: 1
}]
});
}
override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise<any> {
await copyMatchCommand(accessor, match);
}
});
registerAction2(class CopyPathCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.CopyPathCommandId,
title: {
value: nls.localize('copyPathLabel', "Copy Path"),
original: 'Copy Path'
},
category: category.value,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.FileMatchOrFolderMatchWithResourceFocusKey,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC,
win: {
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC
},
},
menu: [{
id: MenuId.SearchContext,
when: Constants.FileMatchOrFolderMatchWithResourceFocusKey,
group: 'search_2',
order: 2
}]
});
}
override async run(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined): Promise<any> {
await copyPathCommand(accessor, fileMatch);
}
});
registerAction2(class CopyAllCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.CopyAllCommandId,
title: {
value: nls.localize('copyAllLabel', "Copy All"),
original: 'Copy All'
},
category: category.value,
menu: [{
id: MenuId.SearchContext,
when: Constants.HasSearchResults,
group: 'search_2',
order: 3
}]
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
await copyAllCommand(accessor);
}
});
//#endregion
//#region Helpers
export const lineDelimiter = isWindows ? '\r\n' : '\n';
async function copyPathCommand(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) {
if (!fileMatch) {
const selection = getSelectedRow(accessor);
if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) {
return;
}
fileMatch = selection;
}
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true });
await clipboardService.writeText(text);
}
async function copyMatchCommand(accessor: ServicesAccessor, match: RenderableMatch | undefined) {
if (!match) {
const selection = getSelectedRow(accessor);
if (!selection) {
return;
}
match = selection;
}
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
let text: string | undefined;
if (match instanceof Match) {
text = matchToString(match);
} else if (match instanceof FileMatch) {
text = fileMatchToString(match, labelService).text;
} else if (match instanceof FolderMatch) {
text = folderMatchToString(match, labelService).text;
}
if (text) {
await clipboardService.writeText(text);
}
}
async function copyAllCommand(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const clipboardService = accessor.get(IClipboardService);
const labelService = accessor.get(ILabelService);
const searchView = getSearchView(viewsService);
if (searchView) {
const root = searchView.searchResult;
const text = allFolderMatchesToString(root.folderMatches(), labelService);
await clipboardService.writeText(text);
}
}
function matchToString(match: Match, indent = 0): string {
const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`;
const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + '';
const fullMatchLines = match.fullPreviewLines();
const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => {
const thisSize = i === 0 ?
getFirstLinePrefix().length :
getOtherLinePrefix(i).length;
return Math.max(thisSize, largest);
}, 0);
const formattedLines = fullMatchLines
.map((line, i) => {
const prefix = i === 0 ?
getFirstLinePrefix() :
getOtherLinePrefix(i);
const paddingStr = ' '.repeat(largestPrefixSize - prefix.length);
const indentStr = ' '.repeat(indent);
return `${indentStr}${prefix}: ${paddingStr}${line}`;
});
return formattedLines.join('\n');
}
function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } {
if (match instanceof FileMatch) {
return fileMatchToString(match, labelService);
} else {
return folderMatchToString(match, labelService);
}
}
function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } {
const matchTextRows = fileMatch.matches()
.sort(searchMatchComparer)
.map(match => matchToString(match, 2));
const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true });
return {
text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`,
count: matchTextRows.length
};
}
function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } {
const results: string[] = [];
let numMatches = 0;
const matches = folderMatch.matches().sort(searchMatchComparer);
matches.forEach(match => {
const result = fileFolderMatchToString(match, labelService);
numMatches += result.count;
results.push(result.text);
});
return {
text: results.join(lineDelimiter + lineDelimiter),
count: numMatches
};
}
function allFolderMatchesToString(folderMatches: Array<FolderMatchWithResource | FolderMatch>, labelService: ILabelService): string {
const folderResults: string[] = [];
folderMatches = folderMatches.sort(searchMatchComparer);
for (let i = 0; i < folderMatches.length; i++) {
const folderResult = folderMatchToString(folderMatches[i], labelService);
if (folderResult.count) {
folderResults.push(folderResult.text);
}
}
return folderResults.join(lineDelimiter + lineDelimiter);
}
function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
return searchView?.getControl().getSelection()[0];
}
//#endregion

View File

@@ -0,0 +1,384 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { dirname } from 'vs/base/common/resources';
import * as nls from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IListService, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';
import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel';
import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { URI } from 'vs/base/common/uri';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { resolveResourcesForSearchIncludes } from 'vs/workbench/services/search/common/queryBuilder';
import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { onUnexpectedError } from 'vs/base/common/errors';
import { category, getElementsToOperateOnInfo, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
//#region Interfaces
export interface IFindInFilesArgs {
query?: string;
replace?: string;
preserveCase?: boolean;
triggerSearch?: boolean;
filesToInclude?: string;
filesToExclude?: string;
isRegex?: boolean;
isCaseSensitive?: boolean;
matchWholeWord?: boolean;
useExcludeSettingsAndIgnoreFiles?: boolean;
onlyOpenEditors?: boolean;
}
//#endregion
registerAction2(class RestrictSearchToFolderAction extends Action2 {
constructor() {
super({
id: Constants.RestrictSearchToFolderId,
title: {
value: nls.localize('restrictResultsToFolder', "Restrict Search to Folder"),
original: 'Restrict Search to Folder'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey),
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF,
},
menu: [
{
id: MenuId.SearchContext,
group: 'search',
order: 3,
when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey)
}
]
});
}
async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) {
await searchWithFolderCommand(accessor, false, true, undefined, folderMatch);
}
});
registerAction2(class ExcludeFolderFromSearchAction extends Action2 {
constructor() {
super({
id: Constants.ExcludeFolderFromSearchId,
title: {
value: nls.localize('excludeFolderFromSearch', "Exclude Folder from Search"),
original: 'Exclude Folder from Search'
},
category,
menu: [
{
id: MenuId.SearchContext,
group: 'search',
order: 4,
when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey)
}
]
});
}
async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) {
await searchWithFolderCommand(accessor, false, false, undefined, folderMatch);
}
});
registerAction2(class RevealInSideBarForSearchResultsAction extends Action2 {
constructor(
) {
super({
id: Constants.RevealInSideBarForSearchResults,
title: {
value: nls.localize('revealInSideBar', "Reveal in Explorer View"),
original: 'Reveal in Explorer View'
},
category: category,
menu: [{
id: MenuId.SearchContext,
when: ContextKeyExpr.and(Constants.FileFocusKey, Constants.HasSearchResults),
group: 'search_3',
order: 1
}]
});
}
override async run(accessor: ServicesAccessor, args: any): Promise<any> {
const paneCompositeService = accessor.get(IPaneCompositePartService);
const explorerService = accessor.get(IExplorerService);
const contextService = accessor.get(IWorkspaceContextService);
const searchView = getSearchView(accessor.get(IViewsService));
if (!searchView) {
return;
}
let fileMatch: FileMatch;
if (!(args instanceof FileMatch)) {
args = searchView.getControl().getFocus()[0];
}
if (args instanceof FileMatch) {
fileMatch = args;
} else {
return;
}
paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => {
if (!viewlet) {
return;
}
const explorerViewContainer = viewlet.getViewPaneContainer() as ExplorerViewPaneContainer;
const uri = fileMatch.resource;
if (uri && contextService.isInsideWorkspace(uri)) {
const explorerView = explorerViewContainer.getExplorerView();
explorerView.setExpanded(true);
explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError);
}
});
}
});
// Find in Files by default is the same as View: Show Search, but can be configured to open a search editor instead with the `search.mode` binding
registerAction2(class FindInFilesAction extends Action2 {
constructor(
) {
super({
id: Constants.FindInFilesActionId,
title: {
value: nls.localize('findInFiles', "Find in Files"),
mnemonicTitle: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files"),
original: 'Find in Files'
},
description: {
description: nls.localize('findInFiles.description', "Open a workspace search"),
args: [
{
name: nls.localize('findInFiles.args', "A set of options for the search"),
schema: {
type: 'object',
properties: {
query: { 'type': 'string' },
replace: { 'type': 'string' },
preserveCase: { 'type': 'boolean' },
triggerSearch: { 'type': 'boolean' },
filesToInclude: { 'type': 'string' },
filesToExclude: { 'type': 'string' },
isRegex: { 'type': 'boolean' },
isCaseSensitive: { 'type': 'boolean' },
matchWholeWord: { 'type': 'boolean' },
useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' },
onlyOpenEditors: { 'type': 'boolean' },
}
}
},
]
},
category: category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF,
},
menu: [{
id: MenuId.MenubarEditMenu,
group: '4_find_global',
order: 1,
}],
f1: true
});
}
override async run(accessor: ServicesAccessor, args: IFindInFilesArgs = {}): Promise<any> {
findInFilesCommand(accessor, args);
}
});
registerAction2(class FindInFolderAction extends Action2 {
// from explorer
constructor() {
super({
id: Constants.FindInFolderId,
title: {
value: nls.localize('findInFolder', "Find in Folder..."),
original: 'Find in Folder...'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext),
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF,
},
menu: [
{
id: MenuId.ExplorerContext,
group: '4_search',
order: 10,
when: ContextKeyExpr.and(ExplorerFolderContext)
}
]
});
}
async run(accessor: ServicesAccessor, resource?: URI) {
await searchWithFolderCommand(accessor, true, true, resource);
}
});
registerAction2(class FindInWorkspaceAction extends Action2 {
// from explorer
constructor() {
super({
id: Constants.FindInWorkspaceId,
title: {
value: nls.localize('findInWorkspace', "Find in Workspace..."),
original: 'Find in Workspace...'
},
category,
menu: [
{
id: MenuId.ExplorerContext,
group: '4_search',
order: 10,
when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated())
}
]
});
}
async run(accessor: ServicesAccessor) {
const searchConfig = accessor.get(IConfigurationService).getValue<ISearchConfiguration>().search;
const mode = searchConfig.mode;
if (mode === 'view') {
const searchView = await openSearchView(accessor.get(IViewsService), true);
searchView?.searchInFolders();
}
else {
return accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, {
location: mode === 'newEditor' ? 'new' : 'reuse',
filesToInclude: '',
});
}
}
});
//#region Helpers
async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) {
const listService = accessor.get(IListService);
const fileService = accessor.get(IFileService);
const viewsService = accessor.get(IViewsService);
const contextService = accessor.get(IWorkspaceContextService);
const commandService = accessor.get(ICommandService);
const searchConfig = accessor.get(IConfigurationService).getValue<ISearchConfiguration>().search;
const mode = searchConfig.mode;
let resources: URI[];
if (isFromExplorer) {
resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService));
} else {
const searchView = getSearchView(accessor.get(IViewsService));
if (!searchView) {
return;
}
resources = getMultiSelectedSearchResources(searchView.getControl(), folderMatch, searchConfig);
}
const resolvedResources = fileService.resolveAll(resources.map(resource => ({ resource }))).then(results => {
const folders: URI[] = [];
results.forEach(result => {
if (result.success && result.stat) {
folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource));
}
});
return resolveResourcesForSearchIncludes(folders, contextService);
});
if (mode === 'view') {
const searchView = await openSearchView(viewsService, true);
if (resources && resources.length && searchView) {
if (isIncludes) {
searchView.searchInFolders(await resolvedResources);
} else {
searchView.searchOutsideOfFolders(await resolvedResources);
}
}
return undefined;
} else {
if (isIncludes) {
return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, {
filesToInclude: (await resolvedResources).join(', '),
showIncludesExcludes: true,
location: mode === 'newEditor' ? 'new' : 'reuse',
});
}
else {
return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, {
filesToExclude: (await resolvedResources).join(', '),
showIncludesExcludes: true,
location: mode === 'newEditor' ? 'new' : 'reuse',
});
}
}
}
function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree<RenderableMatch, void>, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] {
return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements
.map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource))
.filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null));
}
export function findInFilesCommand(accessor: ServicesAccessor, args: IFindInFilesArgs = {}) {
const searchConfig = accessor.get(IConfigurationService).getValue<ISearchConfiguration>().search;
const mode = searchConfig.mode;
if (mode === 'view') {
const viewsService = accessor.get(IViewsService);
openSearchView(viewsService, false).then(openedView => {
if (openedView) {
const searchAndReplaceWidget = openedView.searchAndReplaceWidget;
searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string');
let updatedText = false;
if (typeof args.query === 'string') {
openedView.setSearchParameters(args);
} else {
updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' });
}
openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText);
}
});
} else {
const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({
location: mode === 'newEditor' ? 'new' : 'reuse',
query: args.query,
filesToInclude: args.filesToInclude,
filesToExclude: args.filesToExclude,
matchWholeWord: args.matchWholeWord,
isCaseSensitive: args.isCaseSensitive,
isRegexp: args.isRegex,
useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles,
onlyOpenEditors: args.onlyOpenEditors,
showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles),
});
accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, convertArgs(args));
}
}
//#endregion

View File

@@ -0,0 +1,533 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isMacintosh } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
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 { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { IViewsService } from 'vs/workbench/common/views';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';
import { FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel';
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { assertIsDefined } from 'vs/base/common/types';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/browser/findModel';
import { category, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
//#region Actions: Changing Search Input Options
registerAction2(class ToggleQueryDetailsAction extends Action2 {
constructor() {
super({
id: Constants.ToggleQueryDetailsActionId,
title: {
value: nls.localize('ToggleQueryDetailsAction.label', "Toggle Query Details"),
original: 'Toggle Query Details'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ,
},
});
}
run(accessor: ServicesAccessor) {
const contextService = accessor.get(IContextKeyService).getContext(document.activeElement);
if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) {
(accessor.get(IEditorService).activeEditorPane as SearchEditor).toggleQueryDetails();
} else if (contextService.getValue(Constants.SearchViewFocusedKey.serialize())) {
const searchView = getSearchView(accessor.get(IViewsService));
assertIsDefined(searchView).toggleQueryDetails();
}
}
});
registerAction2(class CloseReplaceAction extends Action2 {
constructor() {
super({
id: Constants.CloseReplaceWidgetActionId,
title: {
value: nls.localize('CloseReplaceWidget.label', "Close Replace Widget"),
original: 'Close Replace Widget'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey),
primary: KeyCode.Escape,
},
});
}
run(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
searchView.searchAndReplaceWidget.toggleReplace(false);
searchView.searchAndReplaceWidget.focus();
}
return Promise.resolve(null);
}
});
registerAction2(class ToggleCaseSensitiveCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.ToggleCaseSensitiveCommandId,
title: {
value: nls.localize('ToggleCaseSensitiveCommandId.label', "Toggle Case Sensitive"),
original: 'Toggle Case Sensitive'
},
category: category,
keybinding: Object.assign({
weight: KeybindingWeight.WorkbenchContrib,
when: isMacintosh ? ContextKeyExpr.and(Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()) : Constants.SearchViewFocusedKey,
}, ToggleCaseSensitiveKeybinding)
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
toggleCaseSensitiveCommand(accessor);
}
});
registerAction2(class ToggleWholeWordCommandAction extends Action2 {
constructor() {
super({
id: Constants.ToggleWholeWordCommandId,
title: {
value: nls.localize('ToggleWholeWordCommandId.label', 'Toggle Whole Word'),
original: 'Toggle Whole Word'
},
keybinding: Object.assign({
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.SearchViewFocusedKey,
}, ToggleWholeWordKeybinding),
category: category.value,
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return toggleWholeWordCommand(accessor);
}
});
registerAction2(class ToggleRegexCommandAction extends Action2 {
constructor() {
super({
id: Constants.ToggleRegexCommandId,
title: {
value: nls.localize('ToggleRegexCommandId.label', 'Toggle Regex'),
original: 'Toggle Regex'
},
keybinding: Object.assign({
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.SearchViewFocusedKey,
}, ToggleRegexKeybinding),
category: category.value,
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return toggleRegexCommand(accessor);
}
});
registerAction2(class TogglePreserveCaseAction extends Action2 {
constructor() {
super({
id: Constants.TogglePreserveCaseId,
title: {
value: nls.localize('TogglePreserveCaseId.label', 'Toggle Preserve Case'),
original: 'Toggle Preserve Case'
},
keybinding: Object.assign({
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.SearchViewFocusedKey,
}, TogglePreserveCaseKeybinding),
category: category.value,
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return togglePreserveCaseCommand(accessor);
}
});
//#endregion
//#region Actions: Opening Matches
registerAction2(class OpenMatchAction extends Action2 {
constructor() {
super({
id: Constants.OpenMatch,
title: {
value: nls.localize('OpenMatch.label', "Open Match"),
original: 'Open Match'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyCode.Enter,
mac: {
primary: KeyCode.Enter,
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow]
},
},
});
}
run(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
const tree: WorkbenchCompressibleObjectTree<RenderableMatch> = searchView.getControl();
const viewer = searchView.getControl();
const focus = tree.getFocus()[0];
if (focus instanceof FolderMatch) {
viewer.toggleCollapsed(focus);
} else {
searchView.open(<FileMatchOrMatch>tree.getFocus()[0], false, false, true);
}
}
}
});
registerAction2(class OpenMatchToSideAction extends Action2 {
constructor() {
super({
id: Constants.OpenMatchToSide,
title: {
value: nls.localize('OpenMatchToSide.label', "Open Match To Side"),
original: 'Open Match To Side'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyMod.CtrlCmd | KeyCode.Enter,
mac: {
primary: KeyMod.WinCtrl | KeyCode.Enter
},
},
});
}
run(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
const tree: WorkbenchCompressibleObjectTree<RenderableMatch> = searchView.getControl();
searchView.open(<FileMatchOrMatch>tree.getFocus()[0], false, true, true);
}
}
});
registerAction2(class AddCursorsAtSearchResultsAction extends Action2 {
constructor() {
super({
id: Constants.AddCursorsAtSearchResults,
title: {
value: nls.localize('AddCursorsAtSearchResults.label', 'Add Cursors at Search Results'),
original: 'Add Cursors at Search Results'
},
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
},
category: category.value,
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
const tree: WorkbenchCompressibleObjectTree<RenderableMatch> = searchView.getControl();
searchView.openEditorWithMultiCursor(<FileMatchOrMatch>tree.getFocus()[0]);
}
}
});
//#endregion
//#region Actions: Toggling Focus
registerAction2(class FocusNextInputAction extends Action2 {
constructor() {
super({
id: Constants.FocusNextInputActionId,
title: {
value: nls.localize('FocusNextInputAction.label', "Focus Next Input"),
original: 'Focus Next Input'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.or(
ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey),
ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)),
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
},
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
(editorService.activeEditorPane as SearchEditor).focusNextInput();
}
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.focusNextInputBox();
}
});
registerAction2(class FocusPreviousInputAction extends Action2 {
constructor() {
super({
id: Constants.FocusPreviousInputActionId,
title: {
value: nls.localize('FocusPreviousInputAction.label', "Focus Previous Input"),
original: 'Focus Previous Input'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.or(
ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey),
ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())),
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
},
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
(editorService.activeEditorPane as SearchEditor).focusPrevInput();
}
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.focusPreviousInputBox();
}
});
registerAction2(class FocusSearchFromResultsAction extends Action2 {
constructor() {
super({
id: Constants.FocusSearchFromResults,
title: {
value: nls.localize('FocusSearchFromResults.label', "Focus Search From Results"),
original: 'Focus Search From Results'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey),
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
},
});
}
run(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.focusPreviousInputBox();
}
});
registerAction2(class ToggleSearchOnTypeAction extends Action2 {
private static readonly searchOnTypeKey = 'search.searchOnType';
constructor(
) {
super({
id: Constants.ToggleSearchOnTypeActionId,
title: {
value: nls.localize('toggleTabs', 'Toggle Search on Type'),
original: 'Toggle Search on Type'
},
category: category.value,
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
const configurationService = accessor.get(IConfigurationService);
const searchOnType = configurationService.getValue<boolean>(ToggleSearchOnTypeAction.searchOnTypeKey);
return configurationService.updateValue(ToggleSearchOnTypeAction.searchOnTypeKey, !searchOnType);
}
});
registerAction2(class FocusSearchListCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.FocusSearchListCommandID,
title: {
value: nls.localize('focusSearchListCommandLabel', "Focus List"),
original: 'Focus List'
},
category: category,
f1: true
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
focusSearchListCommand(accessor);
}
});
registerAction2(class FocusNextSearchResultAction extends Action2 {
constructor() {
super({
id: Constants.FocusNextSearchResultActionId,
title: {
value: nls.localize('FocusNextSearchResult.label', 'Focus Next Search Result'),
original: 'Focus Next Search Result'
},
keybinding: [{
primary: KeyCode.F4,
weight: KeybindingWeight.WorkbenchContrib,
}],
category: category.value,
precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor),
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return await focusNextSearchResult(accessor);
}
});
registerAction2(class FocusPreviousSearchResultAction extends Action2 {
constructor() {
super({
id: Constants.FocusPreviousSearchResultActionId,
title: {
value: nls.localize('FocusPreviousSearchResult.label', 'Search: Focus Previous Search Result'),
original: 'Search: Focus Previous Search Result'
},
keybinding: [{
primary: KeyMod.Shift | KeyCode.F4,
weight: KeybindingWeight.WorkbenchContrib,
}],
category: category.value,
precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor),
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return await focusPreviousSearchResult(accessor);
}
});
registerAction2(class ReplaceInFilesAction extends Action2 {
constructor() {
super({
id: Constants.ReplaceInFilesActionId,
title: {
value: nls.localize('replaceInFiles', 'Search: Replace in Files'),
original: 'Search: Replace in Files'
},
keybinding: [{
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH,
weight: KeybindingWeight.WorkbenchContrib,
}],
category: category.value,
menu: [{
id: MenuId.MenubarEditMenu,
group: '4_find_global',
order: 2
}],
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
return await findOrReplaceInFiles(accessor, true);
}
});
//#endregion
//#region Helpers
function toggleCaseSensitiveCommand(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleCaseSensitive();
}
function toggleWholeWordCommand(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleWholeWords();
}
function toggleRegexCommand(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.toggleRegex();
}
function togglePreserveCaseCommand(accessor: ServicesAccessor) {
const searchView = getSearchView(accessor.get(IViewsService));
searchView?.togglePreserveCase();
}
const focusSearchListCommand: ICommandHandler = accessor => {
const viewsService = accessor.get(IViewsService);
openSearchView(viewsService).then(searchView => {
searchView?.moveFocusToResults();
});
};
async function focusNextSearchResult(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
return (editorService.activeEditorPane as SearchEditor).focusNextResult();
}
return openSearchView(accessor.get(IViewsService)).then(searchView => {
searchView?.selectNextMatch();
});
}
async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const input = editorService.activeEditor;
if (input instanceof SearchEditorInput) {
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
return (editorService.activeEditorPane as SearchEditor).focusPreviousResult();
}
return openSearchView(accessor.get(IViewsService)).then(searchView => {
searchView?.selectPreviousMatch();
});
}
async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise<any> {
return openSearchView(accessor.get(IViewsService), false).then(openedView => {
if (openedView) {
const searchAndReplaceWidget = openedView.searchAndReplaceWidget;
searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget);
const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget });
openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText);
}
});
}
//#endregion

View File

@@ -0,0 +1,408 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { IViewsService } from 'vs/workbench/common/views';
import { searchRemoveIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { arrayContainsElementOrParent, FileMatch, FolderMatch, Match, RenderableMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { category, getElementsToOperateOnInfo, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
//#region Interfaces
export interface ISearchActionContext {
readonly viewer: WorkbenchCompressibleObjectTree<RenderableMatch>;
readonly element: RenderableMatch;
}
export interface IFindInFilesArgs {
query?: string;
replace?: string;
preserveCase?: boolean;
triggerSearch?: boolean;
filesToInclude?: string;
filesToExclude?: string;
isRegex?: boolean;
isCaseSensitive?: boolean;
matchWholeWord?: boolean;
useExcludeSettingsAndIgnoreFiles?: boolean;
onlyOpenEditors?: boolean;
}
//#endregion
//#region Actions
registerAction2(class RemoveAction extends Action2 {
constructor(
) {
super({
id: Constants.RemoveActionId,
title: {
value: nls.localize('RemoveAction.label', "Dismiss"),
original: 'Dismiss'
},
category,
icon: searchRemoveIcon,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyCode.Delete,
mac: {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
},
},
menu: [
{
id: MenuId.SearchContext,
group: 'search',
order: 2,
},
{
id: MenuId.SearchActionMenu,
group: 'inline',
order: 2,
},
]
});
}
run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): void {
const viewsService = accessor.get(IViewsService);
const configurationService = accessor.get(IConfigurationService);
const searchView = getSearchView(viewsService);
if (!searchView) {
return;
}
let element = context?.element;
let viewer = context?.viewer;
if (!viewer) {
viewer = searchView.getControl();
}
if (!element) {
element = viewer.getFocus()[0] ?? undefined;
}
const opInfo = getElementsToOperateOnInfo(viewer, element, configurationService.getValue<ISearchConfigurationProperties>('search'));
const elementsToRemove = opInfo.elements;
let focusElement = viewer.getFocus()[0] ?? undefined;
if (elementsToRemove.length === 0) {
return;
}
if (!focusElement || (focusElement instanceof SearchResult)) {
focusElement = element;
}
let nextFocusElement;
if (opInfo.mustReselect && focusElement) {
nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToRemove);
}
const searchResult = searchView.searchResult;
if (searchResult) {
searchResult.batchRemove(elementsToRemove);
}
if (opInfo.mustReselect && focusElement) {
if (!nextFocusElement) {
nextFocusElement = getLastNodeFromSameType(viewer, focusElement);
}
if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) {
viewer.reveal(nextFocusElement);
viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent());
}
}
viewer.domFocus();
return;
}
});
registerAction2(class ReplaceAction extends Action2 {
constructor(
) {
super({
id: Constants.ReplaceActionId,
title: {
value: nls.localize('match.replace.label', "Replace"),
original: 'Replace'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1,
},
icon: searchReplaceIcon,
menu: [
{
id: MenuId.SearchContext,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey),
group: 'search',
order: 1
},
{
id: MenuId.SearchActionMenu,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey),
group: 'inline',
order: 1
}
]
});
}
override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise<any> {
return performReplace(accessor, context);
}
});
registerAction2(class ReplaceAllAction extends Action2 {
constructor(
) {
super({
id: Constants.ReplaceAllInFileActionId,
title: {
value: nls.localize('file.replaceAll.label', "Replace All"),
original: 'Replace All'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter],
},
icon: searchReplaceIcon,
menu: [
{
id: MenuId.SearchContext,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey),
group: 'search',
order: 1
},
{
id: MenuId.SearchActionMenu,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey),
group: 'inline',
order: 1
}
]
});
}
override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise<any> {
return performReplace(accessor, context);
}
});
registerAction2(class ReplaceAllInFolderAction extends Action2 {
constructor(
) {
super({
id: Constants.ReplaceAllInFolderActionId,
title: {
value: nls.localize('file.replaceAll.label', "Replace All"),
original: 'Replace All'
},
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter],
},
icon: searchReplaceIcon,
menu: [
{
id: MenuId.SearchContext,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey),
group: 'search',
order: 1
},
{
id: MenuId.SearchActionMenu,
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey),
group: 'inline',
order: 1
}
]
});
}
override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise<any> {
return performReplace(accessor, context);
}
});
//#endregion
//#region Helpers
function performReplace(accessor: ServicesAccessor,
context: ISearchActionContext | undefined): void {
const configurationService = accessor.get(IConfigurationService);
const viewsService = accessor.get(IViewsService);
const viewlet: SearchView | undefined = getSearchView(viewsService);
const viewer: WorkbenchCompressibleObjectTree<RenderableMatch> | undefined = context?.viewer ?? viewlet?.getControl();
if (!viewer) {
return;
}
const element: RenderableMatch | null = context?.element ?? viewer.getFocus()[0];
// since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace.
const opInfo = getElementsToOperateOnInfo(viewer, element ?? undefined, configurationService.getValue<ISearchConfigurationProperties>('search'));
const elementsToReplace = opInfo.elements;
let focusElement = viewer.getFocus()[0];
if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) {
focusElement = element;
}
if (elementsToReplace.length === 0) {
return;
}
let nextFocusElement;
if (focusElement) {
nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToReplace);
}
const searchResult = viewlet?.searchResult;
if (searchResult) {
searchResult.batchReplace(elementsToReplace);
}
if (focusElement) {
if (!nextFocusElement) {
nextFocusElement = getLastNodeFromSameType(viewer, focusElement);
}
if (nextFocusElement) {
viewer.reveal(nextFocusElement);
viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent());
if (nextFocusElement instanceof Match) {
const useReplacePreview = configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
if (!useReplacePreview || hasToOpenFile(accessor, nextFocusElement)) {
viewlet?.open(nextFocusElement, true);
} else {
accessor.get(IReplaceService).openReplacePreview(nextFocusElement, true);
}
} else if (nextFocusElement instanceof FileMatch) {
viewlet?.open(nextFocusElement, true);
}
}
}
viewer.domFocus();
}
function hasToOpenFile(accessor: ServicesAccessor, currBottomElem: RenderableMatch): boolean {
if (!(currBottomElem instanceof Match)) {
return false;
}
const activeEditor = accessor.get(IEditorService).activeEditor;
const file = activeEditor?.resource;
if (file) {
return accessor.get(IUriIdentityService).extUri.isEqual(file, currBottomElem.parent().resource);
}
return false;
}
function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) {
if (elem1 instanceof Match) {
if (elem2 instanceof Match) {
return 0;
} else {
return -1;
}
} else if (elem1 instanceof FileMatch) {
if (elem2 instanceof Match) {
return 1;
} else if (elem2 instanceof FileMatch) {
return 0;
} else {
return -1;
}
} else {
// FolderMatch
if (elem2 instanceof FolderMatch) {
return 0;
} else {
return 1;
}
}
}
/**
* Returns element to focus after removing the given element
*/
export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined {
const navigator: ITreeNavigator<any> = viewer.navigate(element);
if (element instanceof FolderMatch) {
while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { }
} else if (element instanceof FileMatch) {
while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) {
viewer.expand(navigator.current());
}
} else {
while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) {
viewer.expand(navigator.current());
}
}
return navigator.current();
}
/***
* Finds the last element in the tree with the same type as `element`
*/
export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree<RenderableMatch>, element: RenderableMatch): RenderableMatch | undefined {
let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null;
while (lastElem) {
const compareVal = compareLevels(element, lastElem);
if (compareVal === -1) {
viewer.expand(lastElem);
lastElem = viewer.lastVisibleElement;
} else if (compareVal === 1) {
lastElem = viewer.getParentElement(lastElem);
} else {
return lastElem;
}
}
return undefined;
}
//#endregion

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
//#region Actions
registerAction2(class ShowAllSymbolsAction extends Action2 {
static readonly ID = 'workbench.action.showAllSymbols';
static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace...");
static readonly ALL_SYMBOLS_PREFIX = '#';
constructor(
) {
super({
id: Constants.ShowAllSymbolsActionId,
title: {
value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."),
original: 'Go to Symbol in Workspace...',
mnemonicTitle: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...")
},
f1: true,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyCode.KeyT
},
menu: {
id: MenuId.MenubarGoMenu,
group: '3_global_nav',
order: 2
}
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX);
}
});
//#endregion

View File

@@ -0,0 +1,348 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { ICommandHandler } from 'vs/platform/commands/common/commands';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
import { IViewsService } from 'vs/workbench/common/views';
import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { VIEW_ID } from 'vs/workbench/services/search/common/search';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode } from 'vs/base/common/keyCodes';
import { SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search';
import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
//#region Actions
registerAction2(class ClearSearchHistoryCommandAction extends Action2 {
constructor(
) {
super({
id: Constants.ClearSearchHistoryCommandId,
title: {
value: nls.localize('clearSearchHistoryLabel', "Clear Search History"),
original: 'Clear Search History'
},
category: category,
f1: true
});
}
override async run(accessor: ServicesAccessor): Promise<any> {
clearHistoryCommand(accessor);
}
});
registerAction2(class CancelSearchAction extends Action2 {
constructor() {
super({
id: Constants.CancelSearchActionId,
title: {
value: nls.localize('CancelSearchAction.label', "Cancel Search"),
original: 'Cancel Search'
},
icon: searchStopIcon,
category,
f1: true,
precondition: SearchStateKey.isEqualTo(SearchUIState.Idle).negate(),
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey),
primary: KeyCode.Escape,
},
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 0,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)),
}]
});
}
run(accessor: ServicesAccessor) {
return cancelSearch(accessor);
}
});
registerAction2(class RefreshAction extends Action2 {
constructor() {
super({
id: Constants.RefreshSearchResultsActionId,
title: {
value: nls.localize('RefreshAction.label', "Refresh"),
original: 'Refresh'
},
icon: searchRefreshIcon,
precondition: Constants.ViewHasSearchPatternKey,
category,
f1: true,
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 0,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
return refreshSearch(accessor);
}
});
registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 {
constructor() {
super({
id: Constants.CollapseSearchResultsActionId,
title: {
value: nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All"),
original: 'Collapse All'
},
category,
icon: searchCollapseAllIcon,
f1: true,
precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey),
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 3,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
return collapseDeepestExpandedLevel(accessor);
}
});
registerAction2(class ExpandAllAction extends Action2 {
constructor() {
super({
id: Constants.ExpandSearchResultsActionId,
title: {
value: nls.localize('ExpandAllAction.label', "Expand All"),
original: 'Expand All'
},
category,
icon: searchExpandAllIcon,
f1: true,
precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()),
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 3,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
return expandAll(accessor);
}
});
registerAction2(class ClearSearchResultsAction extends Action2 {
constructor() {
super({
id: Constants.ClearSearchResultsActionId,
title: {
value: nls.localize('ClearSearchResultsAction.label', "Clear Search Results"),
original: 'Clear Search Results'
},
category,
icon: searchClearIcon,
f1: true,
precondition: ContextKeyExpr.or(Constants.HasSearchResults, Constants.ViewHasSearchPatternKey, Constants.ViewHasReplacePatternKey, Constants.ViewHasFilePatternKey),
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 1,
when: ContextKeyExpr.equals('view', VIEW_ID),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
return clearSearchResults(accessor);
}
});
registerAction2(class ViewAsTreeAction extends Action2 {
constructor() {
super({
id: Constants.ViewAsTreeActionId,
title: {
value: nls.localize('ViewAsTreeAction.label', "View as Tree"),
original: 'View as Tree'
},
category,
icon: searchShowAsList,
f1: true,
precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()),
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
searchView.setTreeView(true);
}
}
});
registerAction2(class ViewAsListAction extends Action2 {
constructor() {
super({
id: Constants.ViewAsListActionId,
title: {
value: nls.localize('ViewAsListAction.label', "View as List"),
original: 'View as List'
},
category,
icon: searchShowAsTree,
f1: true,
precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey),
menu: [{
id: MenuId.ViewTitle,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
const searchView = getSearchView(accessor.get(IViewsService));
if (searchView) {
searchView.setTreeView(false);
}
}
});
//#endregion
//#region Helpers
const clearHistoryCommand: ICommandHandler = accessor => {
const searchHistoryService = accessor.get(ISearchHistoryService);
searchHistoryService.clearHistory();
};
function expandAll(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
if (searchView) {
const viewer = searchView.getControl();
viewer.expandAll();
}
}
function clearSearchResults(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.clearSearchResults();
}
function cancelSearch(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.cancelSearch();
}
function refreshSearch(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
searchView?.triggerQueryChange({ preserveFocus: false });
}
function collapseDeepestExpandedLevel(accessor: ServicesAccessor) {
const viewsService = accessor.get(IViewsService);
const searchView = getSearchView(viewsService);
if (searchView) {
const viewer = searchView.getControl();
/**
* one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren,
* i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels.
*/
const navigator = viewer.navigate();
let node = navigator.first();
let canCollapseFileMatchLevel = false;
let canCollapseFirstLevel = false;
if (node instanceof FolderMatchWorkspaceRoot) {
while (node = navigator.next()) {
if (node instanceof Match) {
canCollapseFileMatchLevel = true;
break;
}
if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) {
let nodeToTest = node;
if (node instanceof FolderMatch) {
nodeToTest = node.compressionStartParent ?? node;
}
const immediateParent = nodeToTest.parent();
if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) {
canCollapseFirstLevel = true;
}
}
}
}
if (canCollapseFileMatchLevel) {
node = navigator.first();
do {
if (node instanceof FileMatch) {
viewer.collapse(node);
}
} while (node = navigator.next());
} else if (canCollapseFirstLevel) {
node = navigator.first();
if (node) {
do {
let nodeToTest = node;
if (node instanceof FolderMatch) {
nodeToTest = node.compressionStartParent ?? node;
}
const immediateParent = nodeToTest.parent();
if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) {
if (viewer.hasElement(node)) {
viewer.collapse(node, true);
} else {
viewer.collapseAll();
}
}
} while (node = navigator.next());
}
} else {
viewer.collapseAll();
}
const firstFocusParent = viewer.getFocus()[0]?.parent();
if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) &&
viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) {
viewer.domFocus();
viewer.focusFirst();
viewer.setSelection(viewer.getFocus());
}
}
}
//#endregion

View File

@@ -4,35 +4,38 @@
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ITreeNode } from 'vs/base/browser/ui/tree/tree';
import { IAction } from 'vs/base/common/actions';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as paths from 'vs/base/common/path';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FileKind } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot } from 'vs/workbench/contrib/search/common/searchModel';
import { isEqual } from 'vs/base/common/resources';
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { ISearchActionContext } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { FileFocusKey, FolderFocusKey, MatchFocusKey } from 'vs/workbench/contrib/search/common/constants';
interface IFolderMatchTemplate {
label: IResourceLabel;
badge: CountBadge;
actions: ActionBar;
actions: MenuWorkbenchToolBar;
disposables: DisposableStore;
disposableActions: DisposableStore;
}
@@ -41,7 +44,7 @@ interface IFileMatchTemplate {
el: HTMLElement;
label: IResourceLabel;
badge: CountBadge;
actions: ActionBar;
actions: MenuWorkbenchToolBar;
disposables: DisposableStore;
}
@@ -52,7 +55,8 @@ interface IMatchTemplate {
replace: HTMLElement;
after: HTMLElement;
lineNumber: HTMLElement;
actions: ActionBar;
actions: MenuWorkbenchToolBar;
disposables: DisposableStore;
}
export class SearchDelegate implements IListVirtualDelegate<RenderableMatch> {
@@ -82,13 +86,13 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree
readonly templateId = FolderMatchRenderer.TEMPLATE_ID;
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
private labels: ResourceLabels,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@ILabelService private readonly labelService: ILabelService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
}
@@ -109,8 +113,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree
templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files"));
}
templateData.actions.clear();
templateData.actions.context = folder;
this.renderFolderDetails(folder, templateData);
}
@@ -123,12 +125,22 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree
const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
disposables.add(attachBadgeStyler(badge, this.themeService));
const actionBarContainer = DOM.append(folderMatchElement, DOM.$('.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
disposables.add(actions);
const disposableElements = new DisposableStore();
disposables.add(disposableElements);
const contextKeyService = this.contextKeyService.createOverlay([[FolderFocusKey.key, true]]);
const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, {
menuOptions: {
shouldForwardArgs: true
},
hiddenItemStrategy: HiddenItemStrategy.Ignore,
toolbarOptions: {
primaryGroup: g => /^inline/.test(g),
},
}));
return {
label,
badge,
@@ -151,7 +163,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree
} else {
templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files"));
}
templateData.actions.clear();
this.renderFolderDetails(folderMatch, templateData);
}
@@ -172,16 +183,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
const actions: IAction[] = [];
if (this.searchModel.isReplaceActive() && count > 0) {
const replaceAction = this.instantiationService.createInstance(ReplaceAllInFolderAction, this.searchView.getControl(), folder);
actions.push(replaceAction);
templateData.disposableActions.add(replaceAction);
}
const removeAction = this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), folder);
actions.push(removeAction);
templateData.actions.push(actions, { icon: true, label: false });
templateData.actions.context = <ISearchActionContext>{ viewer: this.searchView.getControl(), element: folder };
}
}
@@ -191,13 +193,13 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe
readonly templateId = FileMatchRenderer.TEMPLATE_ID;
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
private labels: ResourceLabels,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
}
@@ -214,15 +216,25 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe
const badge = new CountBadge(DOM.append(fileMatchElement, DOM.$('.badge')));
disposables.add(attachBadgeStyler(badge, this.themeService));
const actionBarContainer = DOM.append(fileMatchElement, DOM.$('.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
disposables.add(actions);
const contextKeyService = this.contextKeyService.createOverlay([[FileFocusKey.key, true]]);
const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, {
menuOptions: {
shouldForwardArgs: true
},
hiddenItemStrategy: HiddenItemStrategy.Ignore,
toolbarOptions: {
primaryGroup: g => /^inline/.test(g),
},
}));
return {
el: fileMatchElement,
label,
badge,
actions,
disposables
disposables,
};
}
@@ -236,14 +248,7 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count));
templateData.actions.clear();
const actions: IAction[] = [];
if (this.searchModel.isReplaceActive() && count > 0) {
actions.push(this.instantiationService.createInstance(ReplaceAllAction, this.searchView, fileMatch));
}
actions.push(this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), fileMatch));
templateData.actions.push(actions, { icon: true, label: false });
templateData.actions.context = <ISearchActionContext>{ viewer: this.searchView.getControl(), element: fileMatch };
}
disposeElement(element: ITreeNode<RenderableMatch, any>, index: number, templateData: IFileMatchTemplate): void {
@@ -262,9 +267,10 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
}
@@ -282,7 +288,20 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
const after = DOM.append(parent, DOM.$('span'));
const lineNumber = DOM.append(container, DOM.$('span.matchLineNum'));
const actionBarContainer = DOM.append(container, DOM.$('span.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
const disposables = new DisposableStore();
const contextKeyService = this.contextKeyService.createOverlay([[MatchFocusKey.key, true]]);
const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, {
menuOptions: {
shouldForwardArgs: true
},
hiddenItemStrategy: HiddenItemStrategy.Ignore,
toolbarOptions: {
primaryGroup: g => /^inline/.test(g),
},
}));
return {
parent,
@@ -291,7 +310,8 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
replace,
after,
lineNumber,
actions
actions,
disposables,
};
}
@@ -317,19 +337,12 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
templateData.lineNumber.textContent = lineNumberStr + extraLinesStr;
templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers));
templateData.actions.clear();
if (this.searchModel.isReplaceActive()) {
templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, this.searchView.getControl(), match, this.searchView), this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false });
} else {
templateData.actions.push([this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false });
}
}
templateData.actions.context = <ISearchActionContext>{ viewer: this.searchView.getControl(), element: match };
disposeElement(element: ITreeNode<Match, any>, index: number, templateData: IMatchTemplate): void {
}
disposeTemplate(templateData: IMatchTemplate): void {
templateData.actions.dispose();
templateData.disposables.dispose();
}
private getMatchTitle(match: Match, showLineNumbers: boolean): string {

View File

@@ -62,7 +62,8 @@ import { IViewDescriptorService } from 'vs/workbench/common/views';
import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget';
import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor';
import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions';
import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActionsBase';
import { IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind';
import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchMessage';
import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView';
@@ -750,8 +751,8 @@ export class SearchView extends ViewPane {
this.resultsElement,
delegate,
[
this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)),
this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)),
this._register(this.instantiationService.createInstance(FolderMatchRenderer, this, this.treeLabels)),
this._register(this.instantiationService.createInstance(FileMatchRenderer, this, this.treeLabels)),
this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)),
],
{

View File

@@ -27,7 +27,7 @@ import { ISearchConfigurationProperties } from 'vs/workbench/services/search/com
import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { isMacintosh } from 'vs/base/common/platform';

View File

@@ -42,6 +42,11 @@ export const ViewAsTreeActionId = 'search.action.viewAsTree';
export const ViewAsListActionId = 'search.action.viewAsList';
export const ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails';
export const ExcludeFolderFromSearchId = 'search.action.excludeFromSearch';
export const FocusNextInputActionId = 'search.focus.nextInputBox';
export const FocusPreviousInputActionId = 'search.focus.previousInputBox';
export const RestrictSearchToFolderId = 'search.action.restrictSearchToFolder';
export const FindInFolderId = 'filesExplorer.findInFolder';
export const FindInWorkspaceId = 'filesExplorer.findInWorkspace';
export const SearchViewVisibleKey = new RawContextKey<boolean>('searchViewletVisible', true);
export const SearchViewFocusedKey = new RawContextKey<boolean>('searchViewletFocus', false);

View File

@@ -15,7 +15,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { IFileMatch } from 'vs/workbench/services/search/common/search';
import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActions';
import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace';
import { FileMatch, FileMatchOrMatch, FolderMatch, Match, SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree';
import { IThemeService } from 'vs/platform/theme/common/themeService';

View File

@@ -23,7 +23,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo
import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
import { ActiveEditorContext } from 'vs/workbench/common/contextkeys';
import { IViewsService } from 'vs/workbench/common/views';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
import { searchNewEditorIcon, searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import * as SearchConstants from 'vs/workbench/contrib/search/common/constants';
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';

View File

@@ -16,7 +16,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { EditorsOrder } from 'vs/workbench/common/editor';
import { IViewsService } from 'vs/workbench/common/views';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase';
import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution';

View File

@@ -33,7 +33,7 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { ResourceContextKey } from 'vs/workbench/common/contextkeys';
import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions';
import { findInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind';
import { Direction, ICreateTerminalOptions, IInternalXtermTerminal, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess';
import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal';
@@ -1610,7 +1610,7 @@ export function registerTerminalActions() {
}
run(accessor: ServicesAccessor) {
const query = accessor.get(ITerminalService).activeInstance?.selection;
FindInFilesCommand(accessor, { query } as IFindInFilesArgs);
findInFilesCommand(accessor, { query } as IFindInFilesArgs);
}
});
registerAction2(class extends Action2 {