Feat/search in books (#11117)

* clean up unsavedBooks to providedBooks

* added notebooks viewley contribution

* added notebookExporerAction context

* temp shortcut key B

* remove commenred code

* changes with master merge

* fix comments

* initial tests

* fix casing and description

* merged master and resolved errors

* initial commit

* search view changes

* remove extension point & add custom view container

* merge latest from master

* remove unused files

* move book images to common

* remove notebookExplorer contrib & move to notebook

* build fix

* remove explorer specific sryles from common

* vscode convention to define container actions

* rename notebooks/title

* show collapsible search view separately

* style changes

* rename method

* code cleanup

* enable search on type

* merged master and resolved compile errors

* highlight fix

* code cleanup

* rename interface

* fix hygiene floating promises errors

* more hygiene errors

* avoid circular dependency

* new ids for notebookSearch actions

* reuse SearchSortOrder from search

* merged main and resolved compile errors

* reuse search code

* vscode merge regression on highlight

* extend searchView

* missed resoved files

* null to undefined

* make treeView readonly

* revert carbon edit

* address PR comments

* convert async
This commit is contained in:
Maddy
2020-07-09 13:13:36 -07:00
committed by GitHub
parent a69032e317
commit 9c0f415bd9
11 changed files with 1570 additions and 74 deletions

View File

@@ -50,7 +50,7 @@ import { NodeContextKey } from 'sql/workbench/browser/parts/views/nodeContext';
export class TreeViewPane extends ViewPane {
private treeView: ITreeView;
public readonly treeView: ITreeView;
constructor(
options: IViewletViewOptions,
@@ -138,7 +138,7 @@ export class TreeView extends Disposable implements ITreeView {
private tree: Tree | undefined;
private treeLabels: ResourceLabels | undefined;
private root: ITreeItem;
public readonly root: ITreeItem;
private elementsToRefresh: ITreeItem[] = [];
private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());

View File

@@ -33,6 +33,7 @@ export interface ITreeItem extends vsITreeItem {
export interface ITreeView extends vsITreeView {
collapse(element: ITreeItem): boolean
root: ITreeItem;
}

View File

@@ -12,3 +12,14 @@
.monaco-workbench .activitybar .monaco-action-bar .checked .action-label.book {
background-color: rgb(255, 255, 255);
}
.notebookExplorer-viewlet > .header {
height: 41px;
box-sizing: border-box;
padding: 5px 12px 6px 16px;
}
.notebookExplorer-viewlet .result-messages {
margin-top: 5px;
cursor: default;
}

View File

@@ -19,7 +19,7 @@ import { SyncActionDescriptor, registerAction2, MenuRegistry, MenuId, Action2 }
import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor';
import { NewNotebookAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationRegistry, Extensions as ConfigExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { GridOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/gridOutput.component';
import { PlotlyOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/plotlyOutput.component';
import { registerComponentType } from 'sql/workbench/contrib/notebook/browser/outputs/mimeRegistry';
@@ -48,7 +48,8 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
import { NotebookExplorerViewletViewsContribution, OpenNotebookExplorerViewletAction } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet';
import 'vs/css!./media/notebook.contribution';
import { isMacintosh } from 'vs/base/common/platform';
import { SearchSortOrder } from 'vs/workbench/services/search/common/search';
Registry.as<IEditorInputFactoryRegistry>(EditorInputFactoryExtensions.EditorInputFactories)
.registerEditorInputFactory(FileNotebookInput.ID, FileNoteBookEditorInputFactory);
@@ -377,3 +378,186 @@ registry.registerWorkbenchAction(
'View: Show Notebook Explorer',
localize('notebookExplorer.view', "View")
);
// Configuration
configurationRegistry.registerConfiguration({
id: 'notebookExplorerSearch',
order: 13,
title: localize('searchConfigurationTitle', "Search Notebooks"),
type: 'object',
properties: {
'notebookExplorerSearch.exclude': {
type: 'object',
markdownDescription: localize('exclude', "Configure glob patterns for excluding files and folders in fulltext searches and quick open. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."),
default: { '**/node_modules': true, '**/bower_components': true, '**/*.code-search': true },
additionalProperties: {
anyOf: [
{
type: 'boolean',
description: localize('exclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."),
},
{
type: 'object',
properties: {
when: {
type: 'string',
pattern: '\\w*\\$\\(basename\\)\\w*',
default: '$(basename).ext',
description: localize('exclude.when', 'Additional check on the siblings of a matching file. Use $(basename) as variable for the matching file name.')
}
}
}
]
},
scope: ConfigurationScope.RESOURCE
},
'notebookExplorerSearch.useRipgrep': {
type: 'boolean',
description: localize('useRipgrep', "This setting is deprecated and now falls back on \"search.usePCRE2\"."),
deprecationMessage: localize('useRipgrepDeprecated', "Deprecated. Consider \"search.usePCRE2\" for advanced regex feature support."),
default: true
},
'notebookExplorerSearch.maintainFileSearchCache': {
type: 'boolean',
description: localize('search.maintainFileSearchCache', "When enabled, the searchService process will be kept alive instead of being shut down after an hour of inactivity. This will keep the file search cache in memory."),
default: false
},
'notebookExplorerSearch.useIgnoreFiles': {
type: 'boolean',
markdownDescription: localize('useIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files when searching for files."),
default: true,
scope: ConfigurationScope.RESOURCE
},
'notebookExplorerSearch.useGlobalIgnoreFiles': {
type: 'boolean',
markdownDescription: localize('useGlobalIgnoreFiles', "Controls whether to use global `.gitignore` and `.ignore` files when searching for files."),
default: false,
scope: ConfigurationScope.RESOURCE
},
'notebookExplorerSearch.quickOpen.includeSymbols': {
type: 'boolean',
description: localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."),
default: false
},
'notebookExplorerSearch.quickOpen.includeHistory': {
type: 'boolean',
description: localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."),
default: true
},
'notebookExplorerSearch.quickOpen.history.filterSortOrder': {
'type': 'string',
'enum': ['default', 'recency'],
'default': 'default',
'enumDescriptions': [
localize('filterSortOrder.default', 'History entries are sorted by relevance based on the filter value used. More relevant entries appear first.'),
localize('filterSortOrder.recency', 'History entries are sorted by recency. More recently opened entries appear first.')
],
'description': localize('filterSortOrder', "Controls sorting order of editor history in quick open when filtering.")
},
'notebookExplorerSearch.followSymlinks': {
type: 'boolean',
description: localize('search.followSymlinks', "Controls whether to follow symlinks while searching."),
default: true
},
'notebookExplorerSearch.smartCase': {
type: 'boolean',
description: localize('search.smartCase', "Search case-insensitively if the pattern is all lowercase, otherwise, search case-sensitively."),
default: false
},
'notebookExplorerSearch.globalFindClipboard': {
type: 'boolean',
default: false,
description: localize('search.globalFindClipboard', "Controls whether the search view should read or modify the shared find clipboard on macOS."),
included: isMacintosh
},
'notebookExplorerSearch.location': {
type: 'string',
enum: ['sidebar', 'panel'],
default: 'sidebar',
description: localize('search.location', "Controls whether the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space."),
deprecationMessage: localize('search.location.deprecationMessage', "This setting is deprecated. Please use the search view's context menu instead.")
},
'notebookExplorerSearch.collapseResults': {
type: 'string',
enum: ['auto', 'alwaysCollapse', 'alwaysExpand'],
enumDescriptions: [
localize('search.collapseResults.auto', "Files with less than 10 results are expanded. Others are collapsed."),
'',
''
],
default: 'alwaysExpand',
description: localize('search.collapseAllResults', "Controls whether the search results will be collapsed or expanded."),
},
'notebookExplorerSearch.useReplacePreview': {
type: 'boolean',
default: true,
description: localize('search.useReplacePreview', "Controls whether to open Replace Preview when selecting or replacing a match."),
},
'notebookExplorerSearch.showLineNumbers': {
type: 'boolean',
default: false,
description: localize('search.showLineNumbers', "Controls whether to show line numbers for search results."),
},
'notebookExplorerSearch.usePCRE2': {
type: 'boolean',
default: false,
description: localize('search.usePCRE2', "Whether to use the PCRE2 regex engine in text search. This enables using some advanced regex features like lookahead and backreferences. However, not all PCRE2 features are supported - only features that are also supported by JavaScript."),
deprecationMessage: localize('usePCRE2Deprecated', "Deprecated. PCRE2 will be used automatically when using regex features that are only supported by PCRE2."),
},
'notebookExplorerSearch.actionsPosition': {
type: 'string',
enum: ['auto', 'right'],
enumDescriptions: [
localize('search.actionsPositionAuto', "Position the actionbar to the right when the search view is narrow, and immediately after the content when the search view is wide."),
localize('search.actionsPositionRight', "Always position the actionbar to the right."),
],
default: 'auto',
description: localize('search.actionsPosition', "Controls the positioning of the actionbar on rows in the search view.")
},
'notebookExplorerSearch.searchOnType': {
type: 'boolean',
default: true,
description: localize('search.searchOnType', "Search all files as you type.")
},
'notebookExplorerSearch.seedWithNearestWord': {
type: 'boolean',
default: false,
description: localize('search.seedWithNearestWord', "Enable seeding search from the word nearest the cursor when the active editor has no selection.")
},
'notebookExplorerSearch.seedOnFocus': {
type: 'boolean',
default: false,
description: localize('search.seedOnFocus', "Update workspace search query to the editor's selected text when focusing the search view. This happens either on click or when triggering the `workbench.views.search.focus` command.")
},
'notebookExplorerSearch.searchOnTypeDebouncePeriod': {
type: 'number',
default: 1000,
markdownDescription: localize('search.searchOnTypeDebouncePeriod', "When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.")
},
'notebookExplorerSearch.searchEditor.doubleClickBehaviour': {
type: 'string',
enum: ['selectWord', 'goToLocation', 'openLocationToSide'],
default: 'goToLocation',
enumDescriptions: [
localize('search.searchEditor.doubleClickBehaviour.selectWord', "Double clicking selects the word under the cursor."),
localize('search.searchEditor.doubleClickBehaviour.goToLocation', "Double clicking opens the result in the active editor group."),
localize('search.searchEditor.doubleClickBehaviour.openLocationToSide', "Double clicking opens the result in the editor group to the side, creating one if it does not yet exist."),
],
markdownDescription: localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double clicking a result in a search editor.")
},
'notebookExplorerSearch.sortOrder': {
'type': 'string',
'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending],
'default': SearchSortOrder.Default,
'enumDescriptions': [
localize('searchSortOrder.default', 'Results are sorted by folder and file names, in alphabetical order.'),
localize('searchSortOrder.filesOnly', 'Results are sorted by file names ignoring folder order, in alphabetical order.'),
localize('searchSortOrder.type', 'Results are sorted by file extensions, in alphabetical order.'),
localize('searchSortOrder.modified', 'Results are sorted by file last modified date, in descending order.'),
localize('searchSortOrder.countDescending', 'Results are sorted by count per file, in descending order.'),
localize('searchSortOrder.countAscending', 'Results are sorted by count per file, in ascending order.')
],
'description': localize('search.sortOrder', "Controls sorting order of search results.")
},
}
});

View File

@@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IAction } from 'vs/base/common/actions';
import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom';
import { append, $, addClass, toggleClass, Dimension, IFocusTracker, getTotalHeight } from 'vs/base/browser/dom';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -20,12 +20,26 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { NotebookSearchWidget, INotebookExplorerSearchOptions } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget';
import * as Constants from 'sql/workbench/contrib/notebook/common/constants';
import { IChangeEvent } from 'vs/workbench/contrib/search/common/searchModel';
import { Delayer } from 'vs/base/common/async';
import { ITextQuery, IPatternInfo, IFolderQuery } from 'vs/workbench/services/search/common/search';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { QueryBuilder, ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IFileService } from 'vs/platform/files/common/files';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
import { TreeViewPane } from 'sql/workbench/browser/parts/views/treeView';
import { NotebookSearchView } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch';
import * as path from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { isString } from 'vs/base/common/types';
export const VIEWLET_ID = 'workbench.view.notebooks';
@@ -53,8 +67,22 @@ export class NotebookExplorerViewletViewsContribution implements IWorkbenchContr
private registerViews(): void {
let viewDescriptors = [];
viewDescriptors.push(this.createNotebookSearchViewDescriptor());
Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews(viewDescriptors, NOTEBOOK_VIEW_CONTAINER);
}
createNotebookSearchViewDescriptor(): IViewDescriptor {
return {
id: NotebookSearchView.ID,
name: localize('notebookExplorer.searchResults', "Search Results"),
ctorDescriptor: new SyncDescriptor(NotebookSearchView),
weight: 100,
canToggleVisibility: true,
hideByDefault: false,
order: 0,
collapsed: true
};
}
}
export class NotebookExplorerViewlet extends Viewlet {
@@ -75,8 +103,15 @@ export class NotebookExplorerViewlet extends Viewlet {
export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
private root: HTMLElement;
private static readonly MAX_TEXT_RESULTS = 10000;
private notebookSourcesBox: HTMLElement;
private searchWidgetsContainerElement!: HTMLElement;
searchWidget!: NotebookSearchWidget;
private inputBoxFocused: IContextKey<boolean>;
private triggerQueryDelayer: Delayer<void>;
private pauseSearching = false;
private queryBuilder: QueryBuilder;
private searchView: NotebookSearchView;
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@@ -90,30 +125,270 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
@IConfigurationService configurationService: IConfigurationService,
@IMenuService private menuService: IMenuService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IFileService private readonly fileService: IFileService,
) {
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
this.triggerQueryDelayer = this._register(new Delayer<void>(0));
this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
}
create(parent: HTMLElement): void {
addClass(parent, 'notebookExplorer-viewlet');
this.root = parent;
this.searchWidgetsContainerElement = append(this.root, $('.header'));
this.createSearchWidget(this.searchWidgetsContainerElement);
this.notebookSourcesBox = append(this.root, $('.notebookSources'));
return super.create(this.notebookSourcesBox);
}
private createSearchWidget(container: HTMLElement): void {
this.searchWidget = this._register(this.instantiationService.createInstance(NotebookSearchWidget, container, <INotebookExplorerSearchOptions>{
value: '',
replaceValue: undefined,
isRegex: false,
isCaseSensitive: false,
isWholeWords: false,
searchHistory: [],
replaceHistory: [],
preserveCase: false
}));
this._register(this.searchWidget.onSearchSubmit(options => this.triggerQueryChange(options)));
this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus)));
this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.triggerQueryChange()));
this._register(this.searchWidget.onDidHeightChange(() => this.searchView?.reLayout()));
this._register(this.searchWidget.onPreserveCaseChange(async (state) => {
if (this.searchView && this.searchView.searchViewModel) {
this.searchView.searchViewModel.preserveCase = state;
await this.refreshTree();
}
}));
this._register(this.searchWidget.searchInput.onInput(() => this.searchView?.updateActions()));
this.trackInputBox(this.searchWidget.searchInputFocusTracker);
}
cancelSearch(focus: boolean = true): boolean {
if (focus) {
this.searchView?.cancelSearch(focus);
this.searchWidget.focus();
return true;
}
return false;
}
triggerQueryChange(_options?: { preserveFocus?: boolean, triggeredOnType?: boolean, delay?: number }) {
const options = { preserveFocus: true, triggeredOnType: false, delay: 0, ..._options };
if (!this.pauseSearching) {
this.triggerQueryDelayer.trigger(() => {
this._onQueryChanged(options.preserveFocus, options.triggeredOnType);
}, options.delay);
}
// initialize search view
if (!this.searchView) {
this.searchView = <NotebookSearchView>this.getView(NotebookSearchView.ID);
}
}
private _onQueryChanged(preserveFocus: boolean, triggeredOnType = false): void {
if (!this.searchWidget.searchInput.inputBox.isInputValid()) {
return;
}
const isRegex = this.searchWidget.searchInput.getRegex();
const isWholeWords = this.searchWidget.searchInput.getWholeWords();
const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
const contentPattern = this.searchWidget.searchInput.getValue();
if (contentPattern.length === 0) {
this.clearSearchResults(false);
this.updateViewletsState();
return;
}
const content: IPatternInfo = {
pattern: contentPattern,
isRegExp: isRegex,
isCaseSensitive: isCaseSensitive,
isWordMatch: isWholeWords
};
const excludePattern = undefined;
const includePattern = undefined;
// Need the full match line to correctly calculate replace text, if this is a search/replace with regex group references ($1, $2, ...).
// 10000 chars is enough to avoid sending huge amounts of text around, if you do a replace with a longer match, it may or may not resolve the group refs correctly.
// https://github.com/Microsoft/vscode/issues/58374
const charsPerLine = content.isRegExp ? 10000 : 1000;
const options: ITextQueryBuilderOptions = {
_reason: 'searchView',
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
maxResults: NotebookExplorerViewPaneContainer.MAX_TEXT_RESULTS,
disregardIgnoreFiles: undefined,
disregardExcludeSettings: undefined,
excludePattern,
includePattern,
previewOptions: {
matchLines: 1,
charsPerLine
},
isSmartCase: this.searchConfig.smartCase,
expandPatterns: true
};
const onQueryValidationError = (err: Error) => {
this.searchWidget.searchInput.showMessage({ content: err.message, type: MessageType.ERROR });
this.searchView.clearSearchResults();
};
let query: ITextQuery;
try {
query = this.queryBuilder.text(content, [], options);
} catch (err) {
onQueryValidationError(err);
return;
}
this.validateQuery(query).then(() => {
if (this.views.length > 1) {
let filesToIncludeFiltered: string = '';
this.views.forEach(async (v) => {
let booksViewPane = (<TreeViewPane>this.getView(v.id));
if (booksViewPane?.treeView?.root) {
let root = booksViewPane.treeView.root;
if (root.children) {
let items = root.children;
items?.forEach(root => {
this.updateViewletsState();
let folderToSearch: IFolderQuery = { folder: URI.file(path.join(isString(root.tooltip) ? root.tooltip : root.tooltip.value, 'content')) };
query.folderQueries.push(folderToSearch);
filesToIncludeFiltered = filesToIncludeFiltered + path.join(folderToSearch.folder.fsPath, '**', '*.md') + ',' + path.join(folderToSearch.folder.fsPath, '**', '*.ipynb') + ',';
this.searchView.startSearch(query, null, filesToIncludeFiltered, false, this.searchWidget);
});
}
}
});
}
if (!preserveFocus) {
this.searchWidget.focus(false, true); // focus back to input field
}
}, onQueryValidationError);
}
updateViewletsState(): void {
let containerModel = this.viewDescriptorService.getViewContainerModel(this.viewContainer);
let visibleViewDescriptors = containerModel.visibleViewDescriptors;
if (!this.searchView) {
this.searchView = <NotebookSearchView>this.getView(NotebookSearchView.ID);
}
if (this.searchWidget.searchInput.getValue().length > 0) {
if (visibleViewDescriptors.length > 1) {
let allViews = containerModel.allViewDescriptors;
allViews.forEach(v => {
let view = this.getView(v.id);
if (view !== this.searchView) {
view.setExpanded(false);
}
});
this.searchView.setExpanded(true);
}
} else {
let allViews = containerModel.allViewDescriptors;
allViews.forEach(view => {
this.getView(view.id).setExpanded(true);
});
this.searchView.setExpanded(false);
}
}
showSearchResultsView(): void {
if (!this.searchView) {
this.toggleViewVisibility(NotebookSearchView.ID);
} else {
this.searchView.setVisible(true);
}
}
clearSearchResults(clearInput = true): void {
if (!this.searchView) {
this.searchView = <NotebookSearchView>this.getView(NotebookSearchView.ID);
}
this.searchView.clearSearchResults(clearInput);
if (clearInput) {
this.searchWidget.clear();
}
}
private async validateQuery(query: ITextQuery): Promise<void> {
// Validate folderQueries
const folderQueriesExistP =
query.folderQueries.map(fq => {
return this.fileService.exists(fq.folder);
});
return Promise.all(folderQueriesExistP).then(existResults => {
// If no folders exist, show an error message about the first one
const existingFolderQueries = query.folderQueries.filter((folderQuery, i) => existResults[i]);
if (!query.folderQueries.length || existingFolderQueries.length) {
query.folderQueries = existingFolderQueries;
} else {
const nonExistantPath = query.folderQueries[0].folder.fsPath;
const searchPathNotFoundError = localize('searchPathNotFoundError', "Search path not found: {0}", nonExistantPath);
return Promise.reject(new Error(searchPathNotFoundError));
}
return undefined;
});
}
async refreshTree(event?: IChangeEvent): Promise<void> {
await this.searchView.refreshTree(event);
}
private get searchConfig(): Constants.INotebookSearchConfigurationProperties {
return this.configurationService.getValue<Constants.INotebookSearchConfigurationProperties>('notebookExplorerSearch');
}
private trackInputBox(inputFocusTracker: IFocusTracker, contextKey?: IContextKey<boolean>): void {
this._register(inputFocusTracker.onDidFocus(() => {
this.inputBoxFocused.set(true);
contextKey?.set(true);
}));
this._register(inputFocusTracker.onDidBlur(() => {
this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus());
contextKey?.set(false);
}));
}
public updateStyles(): void {
super.updateStyles();
}
focus(): void {
super.focus();
this.searchWidget.focus(undefined, this.searchConfig.seedOnFocus);
}
layout(dimension: Dimension): void {
toggleClass(this.root, 'narrow', dimension.width <= 300);
super.layout(new Dimension(dimension.width, dimension.height));
super.layout(new Dimension(dimension.width, dimension.height - getTotalHeight(this.searchWidgetsContainerElement)));
}
getOptimalWidth(): number {

View File

@@ -0,0 +1,686 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SearchView, SearchUIState } from 'vs/workbench/contrib/search/browser/searchView';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IFileService } from 'vs/platform/files/common/files';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { ISearchWorkbenchService, Match, FileMatch, SearchModel, IChangeEvent, searchMatchComparer, RenderableMatch, FolderMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import * as dom from 'vs/base/browser/dom';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { ISearchComplete, SearchCompletionExitCode, ITextQuery, SearchSortOrder, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import * as aria from 'vs/base/browser/ui/aria/aria';
import * as errors from 'vs/base/common/errors';
import { NotebookSearchWidget } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget';
import { ITreeElement, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { Iterable } from 'vs/base/common/iterator';
import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { Action, IAction } from 'vs/base/common/actions';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { Memento } from 'vs/workbench/common/memento';
const $ = dom.$;
export class NotebookSearchView extends SearchView {
static readonly ID = 'notebookExplorer.searchResults';
private treeSelectionChangeListener: IDisposable;
private viewActions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction> = [];
private cancelSearchAction: CancelSearchAction;
private toggleExpandAction: ToggleCollapseAndExpandAction;
constructor(
options: IViewPaneOptions,
@IFileService readonly fileService: IFileService,
@IEditorService readonly editorService: IEditorService,
@IProgressService readonly progressService: IProgressService,
@INotificationService readonly notificationService: INotificationService,
@IDialogService readonly dialogService: IDialogService,
@IContextViewService readonly contextViewService: IContextViewService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService readonly contextService: IWorkspaceContextService,
@ISearchWorkbenchService readonly searchWorkbenchService: ISearchWorkbenchService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IReplaceService readonly replaceService: IReplaceService,
@ITextFileService readonly textFileService: ITextFileService,
@IPreferencesService readonly preferencesService: IPreferencesService,
@IThemeService themeService: IThemeService,
@ISearchHistoryService readonly searchHistoryService: ISearchHistoryService,
@IContextMenuService contextMenuService: IContextMenuService,
@IMenuService readonly menuService: IMenuService,
@IAccessibilityService readonly accessibilityService: IAccessibilityService,
@IKeybindingService keybindingService: IKeybindingService,
@IStorageService storageService: IStorageService,
@IOpenerService openerService: IOpenerService,
@ITelemetryService telemetryService: ITelemetryService,
@ICommandService readonly commandService: ICommandService,
) {
super(options, fileService, editorService, progressService, notificationService, dialogService, contextViewService, instantiationService, viewDescriptorService, configurationService, contextService, searchWorkbenchService, contextKeyService, replaceService, textFileService, preferencesService, themeService, searchHistoryService, contextMenuService, menuService, accessibilityService, keybindingService, storageService, openerService, telemetryService);
this.memento = new Memento(this.id, storageService);
this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE);
this.viewActions = [
this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)),
];
const collapseDeepestExpandedLevelAction = this.instantiationService.createInstance(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL);
const expandAllAction = this.instantiationService.createInstance(ExpandAllAction, ExpandAllAction.ID, ExpandAllAction.LABEL);
this.cancelSearchAction = this._register(this.instantiationService.createInstance(CancelSearchAction, CancelSearchAction.ID, CancelSearchAction.LABEL));
this.toggleExpandAction = this._register(this.instantiationService.createInstance(ToggleCollapseAndExpandAction, ToggleCollapseAndExpandAction.ID, ToggleCollapseAndExpandAction.LABEL, collapseDeepestExpandedLevelAction, expandAllAction));
}
protected get searchConfig(): ISearchConfigurationProperties {
return this.configurationService.getValue<ISearchConfigurationProperties>('notebookExplorerSearch');
}
get searchViewModel(): SearchModel {
return this.viewModel;
}
hasSearchPattern(): boolean {
return this.viewModel.searchResult.query?.contentPattern?.pattern.length > 0;
}
isSlowSearch(): boolean {
return this.state !== SearchUIState.Idle;
}
public updateActions(): void {
if (!this.isVisible()) {
this.updatedActionsWhileHidden = true;
}
for (const action of this.viewActions) {
action.update();
}
this.cancelSearchAction.update();
this.toggleExpandAction.update();
}
getActions(): IAction[] {
return this.state !== SearchUIState.Idle ? [
this.cancelSearchAction,
...this.viewActions,
this.toggleExpandAction
] : [...this.viewActions,
this.toggleExpandAction];
}
protected onContextMenu(e: ITreeContextMenuEvent<RenderableMatch | null>): void {
if (!this.contextMenu) {
this.contextMenu = this._register(this.menuService.createMenu(MenuId.SearchContext, this.contextKeyService));
}
e.browserEvent.preventDefault();
e.browserEvent.stopPropagation();
const actions: IAction[] = [];
const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions,
getActionsContext: () => e.element,
onHide: () => dispose(actionsDisposable)
});
}
public reLayout(): void {
if (this.isDisposed) {
return;
}
const actionsPosition = this.searchConfig.actionsPosition;
dom.toggleClass(this.getContainer(), SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right');
const messagesSize = this.messagesElement.style.display === 'none' ?
0 :
dom.getTotalHeight(this.messagesElement);
const searchResultContainerHeight = this.size.height -
messagesSize;
this.resultsElement.style.height = searchResultContainerHeight + 'px';
this.tree.layout(searchResultContainerHeight, this.size.width);
}
public onDidNotebooksOpenState(): void {
if (this.contextKeyService.getContextKeyValue('bookOpened') && this.searchWithoutFolderMessageElement) {
dom.hide(this.searchWithoutFolderMessageElement);
}
}
renderBody(parent: HTMLElement): void {
super.callRenderBody(parent);
this.container = dom.append(parent, dom.$('.search-view'));
this.messagesElement = dom.append(this.container, $('.result-messages'));
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
this.showSearchWithoutFolderMessage();
}
this.createSearchResultsView(this.container);
this._register(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event)));
this._register(this.onDidChangeBodyVisibility(visible => this.onVisibilityChanged(visible)));
// initialize as collapsed viewpane
this.setExpanded(false);
}
protected showSearchWithoutFolderMessage(): void {
this.searchWithoutFolderMessageElement = this.clearMessage();
const textEl = dom.append(this.searchWithoutFolderMessageElement,
$('p', undefined, nls.localize('searchWithoutFolder', "You have not opened any folder that contains notebooks/books. ")));
const openFolderLink = dom.append(textEl,
$('a.pointer.prominent', { tabindex: 0 }, nls.localize('openNotebookFolder', "Open Notebooks")));
this.messageDisposables.push(dom.addDisposableListener(openFolderLink, dom.EventType.CLICK, async (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
this.commandService.executeCommand('notebook.command.openNotebookFolder');
this.setExpanded(false);
}));
}
protected createSearchResultsView(container: HTMLElement): void {
super.createSearchResultsView(container);
this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
this._register(this.tree.onDidChangeCollapseState(() =>
this.toggleCollapseStateDelayer.trigger(() => this.toggleExpandAction.onTreeCollapseStateChange())
));
this.treeSelectionChangeListener = this._register(this.tree.onDidChangeSelection(async (e) => {
if (this.tree.getSelection().length) {
let element = this.tree.getSelection()[0] as Match;
const resource = element instanceof Match ? element.parent().resource : (<FileMatch>element).resource;
if (resource.fsPath.endsWith('.md')) {
this.commandService.executeCommand('markdown.showPreview', resource);
} else {
await this.open(this.tree.getSelection()[0] as Match, true, false, false);
}
}
}));
}
public startSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, searchWidget: NotebookSearchWidget): Thenable<void> {
let progressComplete: () => void;
this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => {
return new Promise(resolve => progressComplete = resolve);
});
this.state = SearchUIState.Searching;
this.showEmptyStage();
const slowTimer = setTimeout(() => {
this.state = SearchUIState.SlowSearch;
this.updateActions();
}, 2000);
const onComplete = async (completed?: ISearchComplete) => {
clearTimeout(slowTimer);
this.state = SearchUIState.Idle;
// Complete up to 100% as needed
progressComplete();
// Do final render, then expand if just 1 file with less than 50 matches
await this.onSearchResultsChanged();
const collapseResults = this.searchConfig.collapseResults;
if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) {
const onlyMatch = this.viewModel.searchResult.matches()[0];
if (onlyMatch.count() < 50) {
this.tree.expand(onlyMatch);
}
}
this.updateActions();
const hasResults = !this.viewModel.searchResult.isEmpty();
if (completed?.exit === SearchCompletionExitCode.NewSearchStarted) {
return;
}
if (completed && completed.limitHit) {
searchWidget.searchInput.showMessage({
content: nls.localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results."),
type: MessageType.WARNING
});
}
if (!hasResults) {
const hasExcludes = !!excludePatternText;
const hasIncludes = !!includePatternText;
let message: string;
if (!completed) {
message = nls.localize('searchInProgress', "Search in progress... - ");
} else if (hasIncludes && hasExcludes) {
message = nls.localize('noResultsIncludesExcludes', "No results found in '{0}' excluding '{1}' - ", includePatternText, excludePatternText);
} else if (hasIncludes) {
message = nls.localize('noResultsIncludes', "No results found in '{0}' - ", includePatternText);
} else if (hasExcludes) {
message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText);
} else {
message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
}
// Indicate as status to ARIA
aria.status(message);
const messageEl = this.clearMessage();
const p = dom.append(messageEl, $('p', undefined, message));
if (!completed) {
const searchAgainLink = dom.append(p, $('a.pointer.prominent', undefined, nls.localize('rerunSearch.message', "Search again")));
this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
this.triggerSearchQueryChange(query, excludePatternText, includePatternText, triggeredOnType, searchWidget);
}));
// cancel search
dom.append(p, $('span', undefined, ' / '));
const cancelSearchLink = dom.append(p, $('a.pointer.prominent', undefined, nls.localize('cancelSearch.message', "Cancel Search")));
this.messageDisposables.push(dom.addDisposableListener(cancelSearchLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
this.cancelSearch();
}));
} else if (hasIncludes || hasExcludes) {
const searchAgainLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('rerunSearchInAll.message', "Search again in all files")));
this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
}));
} else {
const openSettingsLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.message', "Open Settings")));
this.addClickEvents(openSettingsLink, this.onOpenSettings);
}
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && !this.contextKeyService.getContextKeyValue('bookOpened')) {
this.showSearchWithoutFolderMessage();
}
this.reLayout();
} else {
this.viewModel.searchResult.toggleHighlights(this.isVisible()); // show highlights
// Indicate final search result count for ARIA
aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(), this.viewModel.searchResult.fileCount()));
}
};
const onError = (e: any) => {
clearTimeout(slowTimer);
this.state = SearchUIState.Idle;
if (errors.isPromiseCanceledError(e)) {
return onComplete(undefined);
} else {
this.updateActions();
progressComplete();
this.viewModel.searchResult.clear();
return Promise.resolve();
}
};
let visibleMatches = 0;
let updatedActionsForFileCount = false;
// Handle UI updates in an interval to show frequent progress and results
const uiRefreshHandle: any = setInterval(() => {
if (this.state === SearchUIState.Idle) {
window.clearInterval(uiRefreshHandle);
return;
}
// Search result tree update
const fileCount = this.viewModel.searchResult.fileCount();
if (visibleMatches !== fileCount) {
visibleMatches = fileCount;
this.refreshAndUpdateCount().catch(errors.onUnexpectedError);
}
if (fileCount > 0 && !updatedActionsForFileCount) {
updatedActionsForFileCount = true;
this.updateActions();
}
}, 100);
return this.viewModel.search(query)
.then(onComplete, onError);
}
protected async refreshAndUpdateCount(event?: IChangeEvent): Promise<void> {
this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles);
return this.refreshTree(event);
}
async refreshTree(event?: IChangeEvent): Promise<void> {
const collapseResults = this.searchConfig.collapseResults;
if (!event || event.added || event.removed) {
// Refresh whole tree
if (this.searchConfig.sortOrder === SearchSortOrder.Modified) {
// Ensure all matches have retrieved their file stat
await this.retrieveFileStats()
.then(() => this.tree.setChildren(null, this.createSearchResultIterator(collapseResults)));
} else {
this.tree.setChildren(null, this.createSearchResultIterator(collapseResults));
}
} else {
// If updated counts affect our search order, re-sort the view.
if (this.searchConfig.sortOrder === SearchSortOrder.CountAscending ||
this.searchConfig.sortOrder === SearchSortOrder.CountDescending) {
this.tree.setChildren(null, this.createSearchResultIterator(collapseResults));
} else {
// FileMatch modified, refresh those elements
event.elements.forEach(element => {
this.tree.setChildren(element, this.createSearchIterator(element, collapseResults));
this.tree.rerender(element);
});
}
}
}
cancelSearch(focus: boolean = true): boolean {
if (this.viewModel.cancelSearch(false)) {
return true;
}
return false;
}
private createSearchResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable<ITreeElement<RenderableMatch>> {
const folderMatches = this.searchResult.folderMatches()
.filter(fm => !fm.isEmpty())
.sort(searchMatchComparer);
if (folderMatches.length === 1) {
return this.createSearchFolderIterator(folderMatches[0], collapseResults);
}
return Iterable.map(folderMatches, folderMatch => {
const children = this.createSearchFolderIterator(folderMatch, collapseResults);
return <ITreeElement<RenderableMatch>>{ element: folderMatch, children };
});
}
private createSearchFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable<ITreeElement<RenderableMatch>> {
const sortOrder = this.searchConfig.sortOrder;
const matches = folderMatch.matches().sort((a, b) => searchMatchComparer(a, b, sortOrder));
return Iterable.map(matches, fileMatch => {
//const children = this.createFileIterator(fileMatch);
let nodeExists = true;
try { this.tree.getNode(fileMatch); } catch (e) { nodeExists = false; }
const collapsed = nodeExists ? undefined :
(collapseResults === 'alwaysCollapse' || (fileMatch.matches().length > 10 && collapseResults !== 'alwaysExpand'));
return <ITreeElement<RenderableMatch>>{ element: fileMatch, undefined, collapsed, collapsible: false };
});
}
private createSearchFileIterator(fileMatch: FileMatch): Iterable<ITreeElement<RenderableMatch>> {
const matches = fileMatch.matches().sort(searchMatchComparer);
return Iterable.map(matches, r => (<ITreeElement<RenderableMatch>>{ element: r }));
}
private createSearchIterator(match: FolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable<ITreeElement<RenderableMatch>> {
return match instanceof SearchResult ? this.createSearchResultIterator(collapseResults) :
match instanceof FolderMatch ? this.createSearchFolderIterator(match, collapseResults) :
this.createSearchFileIterator(match);
}
triggerSearchQueryChange(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, searchWidget: NotebookSearchWidget) {
this.viewModel.cancelSearch(true);
this.currentSearchQ = this.currentSearchQ
.then(() => this.startSearch(query, excludePatternText, includePatternText, triggeredOnType, searchWidget))
.then(() => undefined, () => undefined);
}
public saveState(): void {
const preserveCase = this.viewModel.preserveCase;
this.viewletState['query.preserveCase'] = preserveCase;
this.memento.saveMemento();
ViewPane.prototype.saveState.call(this);
}
dispose(): void {
this.isDisposed = true;
this.saveState();
this.treeSelectionChangeListener.dispose();
ViewPane.prototype.dispose.call(this);
}
}
class ToggleCollapseAndExpandAction extends Action {
static readonly ID: string = 'notebookSearch.action.collapseOrExpandSearchResults';
static LABEL: string = nls.localize('ToggleCollapseAndExpandAction.label', "Toggle Collapse and Expand");
// Cache to keep from crawling the tree too often.
private action: CollapseDeepestExpandedLevelAction | ExpandAllAction | undefined;
constructor(id: string, label: string,
private collapseAction: CollapseDeepestExpandedLevelAction,
private expandAction: ExpandAllAction,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label, collapseAction.class);
this.update();
}
update(): void {
const searchView = getSearchView(this.viewsService);
this.enabled = !!searchView && searchView.hasSearchResults();
this.onTreeCollapseStateChange();
}
onTreeCollapseStateChange() {
this.action = undefined;
this.determineAction();
}
private determineAction(): CollapseDeepestExpandedLevelAction | ExpandAllAction {
if (this.action !== undefined) { return this.action; }
this.action = this.isSomeCollapsible() ? this.collapseAction : this.expandAction;
this.class = this.action.class;
return this.action;
}
private isSomeCollapsible(): boolean {
const searchView = getSearchView(this.viewsService);
if (searchView) {
const viewer = searchView.getControl();
const navigator = viewer.navigate();
let node = navigator.first();
do {
if (!viewer.isCollapsed(node)) {
return true;
}
} while (node = navigator.next());
}
return false;
}
async run(): Promise<void> {
await this.determineAction().run();
}
}
class CancelSearchAction extends Action {
static readonly ID: string = 'notebookSearch.action.cancelSearch';
static LABEL: string = nls.localize('CancelSearchAction.label', "Cancel Search");
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label, 'search-action ' + searchStopIcon.classNames);
this.update();
}
update(): void {
const searchView = getSearchView(this.viewsService);
this.enabled = !!searchView && searchView.isSlowSearch();
}
run(): Promise<void> {
const searchView = getSearchView(this.viewsService);
if (searchView) {
searchView.cancelSearch();
}
return Promise.resolve(undefined);
}
}
class ExpandAllAction extends Action {
static readonly ID: string = 'notebookSearch.action.expandSearchResults';
static LABEL: string = nls.localize('ExpandAllAction.label', "Expand All");
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label, 'search-action ' + searchExpandAllIcon.classNames);
this.update();
}
update(): void {
const searchView = getSearchView(this.viewsService);
this.enabled = !!searchView && searchView.hasSearchResults();
}
run(): Promise<void> {
const searchView = getSearchView(this.viewsService);
if (searchView) {
const viewer = searchView.getControl();
viewer.expandAll();
viewer.domFocus();
viewer.focusFirst();
}
return Promise.resolve(undefined);
}
}
class CollapseDeepestExpandedLevelAction extends Action {
static readonly ID: string = 'notebookSearch.action.collapseSearchResults';
static LABEL: string = nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All");
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label, 'search-action ' + searchCollapseAllIcon.classNames);
this.update();
}
update(): void {
const searchView = getSearchView(this.viewsService);
this.enabled = !!searchView && searchView.hasSearchResults();
}
run(): Promise<void> {
const searchView = getSearchView(this.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 collapseFileMatchLevel = false;
if (node instanceof FolderMatch) {
while (node = navigator.next()) {
if (node instanceof Match) {
collapseFileMatchLevel = true;
break;
}
}
}
if (collapseFileMatchLevel) {
node = navigator.first();
do {
if (node instanceof FileMatch) {
viewer.collapse(node);
}
} while (node = navigator.next());
} else {
viewer.collapseAll();
}
viewer.domFocus();
viewer.focusFirst();
}
return Promise.resolve(undefined);
}
}
class ClearSearchResultsAction extends Action {
static readonly ID: string = 'notebookSearch.action.clearSearchResults';
static LABEL: string = nls.localize('ClearSearchResultsAction.label', "Clear Search Results");
constructor(id: string, label: string,
@IViewsService private readonly viewsService: IViewsService
) {
super(id, label, 'search-action ' + searchClearIcon.classNames);
this.update();
}
update(): void {
const searchView = getSearchView(this.viewsService);
this.enabled = !!searchView && searchView.hasSearchResults();
}
run(): Promise<void> {
const searchView = getSearchView(this.viewsService);
if (searchView) {
searchView.clearSearchResults();
}
return Promise.resolve();
}
}
function getSearchView(viewsService: IViewsService): NotebookSearchView | undefined {
return viewsService.getActiveViewWithId(NotebookSearchView.ID) as NotebookSearchView ?? undefined;
}

View File

@@ -0,0 +1,276 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Widget } from 'vs/base/browser/ui/widget';
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { IFocusTracker, append, $, trackFocus } from 'vs/base/browser/dom';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Emitter, Event } from 'vs/base/common/event';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import * as Constants from 'sql/workbench/contrib/notebook/common/constants';
import { IMessage } from 'vs/base/browser/ui/inputbox/inputBox';
import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActions';
import { ContextScopedFindInput } from 'vs/platform/browser/contextScopedHistoryWidget';
import { attachFindReplaceInputBoxStyler } from 'vs/platform/theme/common/styler';
import { isMacintosh } from 'vs/base/common/platform';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
export interface INotebookExplorerSearchOptions {
value?: string;
isRegex?: boolean;
isCaseSensitive?: boolean;
isWholeWords?: boolean;
searchHistory?: string[];
replaceHistory?: string[];
preserveCase?: boolean;
showContextToggle?: boolean;
}
const ctrlKeyMod = (isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
export const SingleLineInputHeight = 24;
function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | undefined) {
const isMultiline = !!value.match(/\n/);
if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionStart > 0) {
event.stopPropagation();
return;
}
}
function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | undefined) {
const isMultiline = !!value.match(/\n/);
if (textarea && (isMultiline || textarea.clientHeight > SingleLineInputHeight) && textarea.selectionEnd < textarea.value.length) {
event.stopPropagation();
return;
}
}
export class NotebookSearchWidget extends Widget {
domNode!: HTMLElement;
searchInput!: FindInput;
searchInputFocusTracker!: IFocusTracker;
private searchInputBoxFocused: IContextKey<boolean>;
private ignoreGlobalFindBufferOnNextFocus = false;
private previousGlobalFindBufferValue: string | undefined = undefined;
private _onSearchSubmit = this._register(new Emitter<{ triggeredOnType: boolean, delay: number }>());
readonly onSearchSubmit: Event<{ triggeredOnType: boolean, delay: number }> = this._onSearchSubmit.event;
private _onSearchCancel = this._register(new Emitter<{ focus: boolean }>());
readonly onSearchCancel: Event<{ focus: boolean }> = this._onSearchCancel.event;
private _onBlur = this._register(new Emitter<void>());
readonly onBlur: Event<void> = this._onBlur.event;
private _onDidHeightChange = this._register(new Emitter<void>());
readonly onDidHeightChange: Event<void> = this._onDidHeightChange.event;
private _onPreserveCaseChange = this._register(new Emitter<boolean>());
readonly onPreserveCaseChange: Event<boolean> = this._onPreserveCaseChange.event;
constructor(
container: HTMLElement,
options: INotebookExplorerSearchOptions,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IKeybindingService private readonly keyBindingService: IKeybindingService,
@IClipboardService private readonly clipboardServce: IClipboardService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
) {
super();
this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
this.render(container, options);
this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.accessibilitySupport')) {
this.updateAccessibilitySupport();
}
});
this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.updateAccessibilitySupport());
this.updateAccessibilitySupport();
}
searchInputHasFocus(): boolean {
return !!this.searchInputBoxFocused.get();
}
private render(container: HTMLElement, options: INotebookExplorerSearchOptions): void {
this.domNode = append(container, $('.search-widget'));
this.domNode.style.position = 'relative';
this.renderSearchInput(this.domNode, options);
}
focus(select: boolean = true, suppressGlobalSearchBuffer = false): void {
this.ignoreGlobalFindBufferOnNextFocus = suppressGlobalSearchBuffer;
this.searchInput.focus();
if (select) {
this.searchInput.select();
}
}
clear() {
this.searchInput.clear();
}
private renderSearchInput(parent: HTMLElement, options: INotebookExplorerSearchOptions): void {
const inputOptions: IFindInputOptions = {
label: localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
validation: (value: string) => this.validateSearchInput(value),
placeholder: localize('search.placeHolder', "Search"),
appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keyBindingService),
appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService),
appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService),
history: options.searchHistory,
flexibleHeight: true
};
const searchInputContainer = append(parent, $('.search-container.input-box'));
this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, true));
this._register(attachFindReplaceInputBoxStyler(this.searchInput, this.themeService));
this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
this.searchInput.setValue(options.value || '');
this.searchInput.setRegex(!!options.isRegex);
this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
this.searchInput.setWholeWords(!!options.isWholeWords);
this._register(this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged()));
this._register(this.searchInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire()));
this.searchInputFocusTracker = this._register(trackFocus(this.searchInput.inputBox.inputElement));
this._register(this.searchInputFocusTracker.onDidFocus(async () => {
this.searchInputBoxFocused.set(true);
const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) {
const globalBufferText = await this.clipboardServce.readFindText();
if (this.previousGlobalFindBufferValue !== globalBufferText) {
this.searchInput.inputBox.addToHistory();
this.searchInput.setValue(globalBufferText);
this.searchInput.select();
}
this.previousGlobalFindBufferValue = globalBufferText;
}
this.ignoreGlobalFindBufferOnNextFocus = false;
}));
this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
}
private onSearchInputChanged(): void {
this.searchInput.clearMessage();
if (this.searchConfiguration.searchOnType) {
if (this.searchInput.getRegex()) {
try {
const regex = new RegExp(this.searchInput.getValue(), 'ug');
const matchienessHeuristic = `
~!@#$%^&*()_+
\`1234567890-=
qwertyuiop[]\\
QWERTYUIOP{}|
asdfghjkl;'
ASDFGHJKL:"
zxcvbnm,./
ZXCVBNM<>? `.match(regex)?.length ?? 0;
const delayMultiplier =
matchienessHeuristic < 50 ? 1 :
matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w`
10; // only things matching empty string
this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier);
} catch {
// pass
}
} else {
this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod);
}
}
}
private submitSearch(triggeredOnType = false, delay: number = 0): void {
this.searchInput.validate();
if (!this.searchInput.inputBox.isInputValid()) {
return;
}
const value = this.searchInput.getValue();
const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
if (value && useGlobalFindBuffer) {
this.clipboardServce.writeFindText(value);
}
this._onSearchSubmit.fire({ triggeredOnType, delay });
}
private validateSearchInput(value: string): IMessage | undefined {
if (value.length === 0) {
return undefined;
}
if (!this.searchInput.getRegex()) {
return undefined;
}
try {
new RegExp(value, 'u');
} catch (e) {
return { content: e.message };
}
return undefined;
}
private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(ctrlKeyMod | KeyCode.Enter)) {
this.searchInput.inputBox.insertAtCursor('\n');
keyboardEvent.preventDefault();
}
if (keyboardEvent.equals(KeyCode.Enter)) {
this.searchInput.onSearchSubmit();
this.submitSearch();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.Escape)) {
this._onSearchCancel.fire({ focus: true });
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.Tab)) {
this.searchInput.focusOnCaseSensitive();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.UpArrow)) {
stopPropagationForMultiLineUpwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
}
else if (keyboardEvent.equals(KeyCode.DownArrow)) {
stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput.getValue(), this.searchInput.domNode.querySelector('textarea'));
}
}
private updateAccessibilitySupport(): void {
this.searchInput.setFocusInputOnOptionClick(!this.accessibilityService.isScreenReaderOptimized());
}
private get searchConfiguration(): Constants.INotebookSearchConfigurationProperties {
return this.configurationService.getValue<Constants.INotebookSearchConfigurationProperties>('notebookExplorerSearch');
}
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import * as glob from 'vs/base/common/glob';
import { SearchSortOrder } from 'vs/workbench/services/search/common/search';
export const FindInNotebooksActionId = 'workbench.action.findInNotebooks';
export const FocusActiveEditorCommandId = 'notebookSearch.action.focusActiveEditor';
export const FocusSearchFromResults = 'notebookSearch.action.focusSearchFromResults';
export const OpenMatchToSide = 'notebookSearch.action.openResultToSide';
export const CancelActionId = 'notebookSearch.action.cancel';
export const RemoveActionId = 'notebookSearch.action.remove';
export const CopyPathCommandId = 'notebookSearch.action.copyPath';
export const CopyMatchCommandId = 'notebookSearch.action.copyMatch';
export const CopyAllCommandId = 'notebookSearch.action.copyAll';
export const OpenInEditorCommandId = 'notebookSearch.action.openInEditor';
export const ClearSearchHistoryCommandId = 'notebookSearch.action.clearHistory';
export const FocusSearchListCommandID = 'notebookSearch.action.focusSearchList';
export const ToggleCaseSensitiveCommandId = 'toggleSearchCaseSensitive';
export const ToggleWholeWordCommandId = 'toggleSearchWholeWord';
export const ToggleRegexCommandId = 'toggleSearchRegex';
export const AddCursorsAtSearchResults = 'addCursorsAtSearchResults';
export const SearchViewFocusedKey = new RawContextKey<boolean>('notebookSearchViewletFocus', false);
export const InputBoxFocusedKey = new RawContextKey<boolean>('inputBoxFocus', false);
export const SearchInputBoxFocusedKey = new RawContextKey<boolean>('searchInputBoxFocus', false);
export interface INotebookSearchConfigurationProperties {
exclude: glob.IExpression;
useRipgrep: boolean;
/**
* Use ignore file for file search.
*/
useIgnoreFiles: boolean;
useGlobalIgnoreFiles: boolean;
followSymlinks: boolean;
smartCase: boolean;
globalFindClipboard: boolean;
location: 'sidebar' | 'panel';
useReplacePreview: boolean;
showLineNumbers: boolean;
usePCRE2: boolean;
actionsPosition: 'auto' | 'right';
maintainFileSearchCache: boolean;
collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
searchOnType: boolean;
seedOnFocus: boolean;
seedWithNearestWord: boolean;
searchOnTypeDebouncePeriod: number;
searchEditor: {
doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide',
experimental: { reusePriorSearchConfiguration: boolean }
};
sortOrder: SearchSortOrder;
}