mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 21:47:12 -05:00
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:
@@ -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');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
62
src/vs/workbench/contrib/search/browser/searchActionsBase.ts
Normal file
62
src/vs/workbench/contrib/search/browser/searchActionsBase.ts
Normal 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;
|
||||
}
|
||||
|
||||
256
src/vs/workbench/contrib/search/browser/searchActionsCopy.ts
Normal file
256
src/vs/workbench/contrib/search/browser/searchActionsCopy.ts
Normal 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
|
||||
384
src/vs/workbench/contrib/search/browser/searchActionsFind.ts
Normal file
384
src/vs/workbench/contrib/search/browser/searchActionsFind.ts
Normal 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
|
||||
533
src/vs/workbench/contrib/search/browser/searchActionsNav.ts
Normal file
533
src/vs/workbench/contrib/search/browser/searchActionsNav.ts
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
348
src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts
Normal file
348
src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user