From 9c0f415bd903b9873d0535115efac229fac13f62 Mon Sep 17 00:00:00 2001 From: Maddy <12754347+MaddyDev@users.noreply.github.com> Date: Thu, 9 Jul 2020 13:13:36 -0700 Subject: [PATCH] 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 --- extensions/notebook/src/book/bookTreeView.ts | 2 +- .../workbench/browser/parts/views/treeView.ts | 4 +- src/sql/workbench/common/views.ts | 1 + .../browser/media/notebook.contribution.css | 11 + .../notebook/browser/notebook.contribution.ts | 188 ++++- .../notebookExplorerViewlet.ts | 285 +++++++- .../notebookExplorer/notebookSearch.ts | 686 ++++++++++++++++++ .../notebookExplorer/notebookSearchWidget.ts | 276 +++++++ .../contrib/notebook/common/constants.ts | 59 ++ .../search/browser/searchResultsView.ts | 6 +- .../contrib/search/browser/searchView.ts | 126 ++-- 11 files changed, 1570 insertions(+), 74 deletions(-) create mode 100644 src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts create mode 100644 src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts create mode 100644 src/sql/workbench/contrib/notebook/common/constants.ts diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 0c8d6c38f0..49a2c02149 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -242,7 +242,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider = this._register(new Emitter()); diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts index 28f1138cb9..efcdaa6cee 100644 --- a/src/sql/workbench/common/views.ts +++ b/src/sql/workbench/common/views.ts @@ -33,6 +33,7 @@ export interface ITreeItem extends vsITreeItem { export interface ITreeView extends vsITreeView { collapse(element: ITreeItem): boolean + root: ITreeItem; } diff --git a/src/sql/workbench/contrib/notebook/browser/media/notebook.contribution.css b/src/sql/workbench/contrib/notebook/browser/media/notebook.contribution.css index 0808ab3a26..711e9d0414 100644 --- a/src/sql/workbench/contrib/notebook/browser/media/notebook.contribution.css +++ b/src/sql/workbench/contrib/notebook/browser/media/notebook.contribution.css @@ -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; +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index aa10b2c88b..6c2f71b9a6 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -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(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.") + }, + } +}); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts index 6073109d23..9c2ee7c9fa 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts @@ -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(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; + private triggerQueryDelayer: Delayer; + 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(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, { + 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 = 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 = (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 = 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 = this.getView(NotebookSearchView.ID); + } + this.searchView.clearSearchResults(clearInput); + + if (clearInput) { + this.searchWidget.clear(); + } + } + + private async validateQuery(query: ITextQuery): Promise { + // 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 { + await this.searchView.refreshTree(event); + } + + private get searchConfig(): Constants.INotebookSearchConfigurationProperties { + return this.configurationService.getValue('notebookExplorerSearch'); + } + + private trackInputBox(inputFocusTracker: IFocusTracker, contextKey?: IContextKey): 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 { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts new file mode 100644 index 0000000000..37b25f5497 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts @@ -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 = []; + 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('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): 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 : (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 { + 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 { + this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles); + return this.refreshTree(event); + } + + async refreshTree(event?: IChangeEvent): Promise { + 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> { + 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 >{ element: folderMatch, children }; + }); + } + + private createSearchFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { + 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 >{ element: fileMatch, undefined, collapsed, collapsible: false }; + }); + } + + private createSearchFileIterator(fileMatch: FileMatch): Iterable> { + const matches = fileMatch.matches().sort(searchMatchComparer); + return Iterable.map(matches, r => (>{ element: r })); + } + + private createSearchIterator(match: FolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts new file mode 100644 index 0000000000..6c59c8ae19 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget.ts @@ -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; + 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()); + readonly onBlur: Event = this._onBlur.event; + + private _onDidHeightChange = this._register(new Emitter()); + readonly onDidHeightChange: Event = this._onDidHeightChange.event; + + private _onPreserveCaseChange = this._register(new Emitter()); + readonly onPreserveCaseChange: Event = 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('notebookExplorerSearch'); + } +} diff --git a/src/sql/workbench/contrib/notebook/common/constants.ts b/src/sql/workbench/contrib/notebook/common/constants.ts new file mode 100644 index 0000000000..9b2cfe7261 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/common/constants.ts @@ -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('notebookSearchViewletFocus', false); +export const InputBoxFocusedKey = new RawContextKey('inputBoxFocus', false); +export const SearchInputBoxFocusedKey = new RawContextKey('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; +} diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index e5d8038e6d..de83746972 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -31,14 +31,14 @@ import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; -interface IFolderMatchTemplate { +export interface IFolderMatchTemplate { // {{SQL CARBON EDIT}} label: IResourceLabel; badge: CountBadge; actions: ActionBar; disposables: IDisposable[]; } -interface IFileMatchTemplate { +export interface IFileMatchTemplate { // {{SQL CARBON EDIT}} el: HTMLElement; label: IResourceLabel; badge: CountBadge; @@ -46,7 +46,7 @@ interface IFileMatchTemplate { disposables: IDisposable[]; } -interface IMatchTemplate { +export interface IMatchTemplate { // {{SQL CARBON EDIT}} parent: HTMLElement; before: HTMLElement; match: HTMLElement; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 3f9b6276b8..5ae74579ec 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -73,7 +73,7 @@ import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIco const $ = dom.$; -enum SearchUIState { +export enum SearchUIState { // {{SQL CARBON EDIT}} Idle, Searching, SlowSearch @@ -89,14 +89,14 @@ export class SearchView extends ViewPane { private static readonly MAX_TEXT_RESULTS = 10000; - private static readonly ACTIONS_RIGHT_CLASS_NAME = 'actions-right'; + protected static readonly ACTIONS_RIGHT_CLASS_NAME = 'actions-right'; // {{SQL CARBON EDIT}} - private isDisposed = false; + protected isDisposed = false; // {{SQL CARBON EDIT}} - private container!: HTMLElement; + protected container!: HTMLElement; // {{SQL CARBON EDIT}} private queryBuilder: QueryBuilder; - private viewModel: SearchModel; - private memento: Memento; + protected viewModel: SearchModel; // {{SQL CARBON EDIT}} + protected memento: Memento; // {{SQL CARBON EDIT}} private viewletVisible: IContextKey; private inputBoxFocused: IContextKey; @@ -109,74 +109,74 @@ export class SearchView extends ViewPane { private fileMatchFocused: IContextKey; private folderMatchFocused: IContextKey; private matchFocused: IContextKey; - private hasSearchResultsKey: IContextKey; + protected hasSearchResultsKey: IContextKey; // {{SQL CARBON EDIT}} - private state: SearchUIState = SearchUIState.Idle; + protected state: SearchUIState = SearchUIState.Idle; // {{SQL CARBON EDIT}} private actions: Array = []; private toggleCollapseAction: ToggleCollapseAndExpandAction; private cancelAction: CancelSearchAction; private refreshAction: RefreshAction; - private contextMenu: IMenu | null = null; + protected contextMenu: IMenu | null = null; // {{SQL CARBON EDIT}} - private tree!: WorkbenchObjectTree; - private treeLabels!: ResourceLabels; - private viewletState: MementoObject; - private messagesElement!: HTMLElement; - private messageDisposables: IDisposable[] = []; + protected tree!: WorkbenchObjectTree; // {{SQL CARBON EDIT}} + protected treeLabels!: ResourceLabels; // {{SQL CARBON EDIT}} + protected viewletState: MementoObject; // {{SQL CARBON EDIT}} + protected messagesElement!: HTMLElement; // {{SQL CARBON EDIT}} + protected messageDisposables: IDisposable[] = []; // {{SQL CARBON EDIT}} private searchWidgetsContainerElement!: HTMLElement; private searchWidget!: SearchWidget; - private size!: dom.Dimension; + protected size!: dom.Dimension; // {{SQL CARBON EDIT}} private queryDetails!: HTMLElement; private toggleQueryDetailsButton!: HTMLElement; private inputPatternExcludes!: ExcludePatternInputWidget; private inputPatternIncludes!: PatternInputWidget; - private resultsElement!: HTMLElement; + protected resultsElement!: HTMLElement; // {{SQL CARBON EDIT}} private currentSelectedFileMatch: FileMatch | undefined; private delayedRefresh: Delayer; private changedWhileHidden: boolean = false; - private updatedActionsWhileHidden = false; + protected updatedActionsWhileHidden = false; // {{SQL CARBON EDIT}} - private searchWithoutFolderMessageElement: HTMLElement | undefined; + protected searchWithoutFolderMessageElement: HTMLElement | undefined; // {{SQL CARBON EDIT}} - private currentSearchQ = Promise.resolve(); + protected currentSearchQ = Promise.resolve(); // {{SQL CARBON EDIT}} private addToSearchHistoryDelayer: Delayer; - private toggleCollapseStateDelayer: Delayer; + protected toggleCollapseStateDelayer: Delayer; // {{SQL CARBON EDIT}} private triggerQueryDelayer: Delayer; private pauseSearching = false; - private treeAccessibilityProvider: SearchAccessibilityProvider; + protected treeAccessibilityProvider: SearchAccessibilityProvider; // {{SQL CARBON EDIT}} constructor( options: IViewPaneOptions, - @IFileService private readonly fileService: IFileService, - @IEditorService private readonly editorService: IEditorService, - @IProgressService private readonly progressService: IProgressService, - @INotificationService private readonly notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, - @IContextViewService private readonly contextViewService: IContextViewService, - @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IConfigurationService configurationService: IConfigurationService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ISearchWorkbenchService private readonly searchWorkbenchService: ISearchWorkbenchService, - @IContextKeyService readonly contextKeyService: IContextKeyService, - @IReplaceService private readonly replaceService: IReplaceService, - @ITextFileService private readonly textFileService: ITextFileService, - @IPreferencesService private readonly preferencesService: IPreferencesService, - @IThemeService themeService: IThemeService, - @ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService, - @IContextMenuService contextMenuService: IContextMenuService, - @IMenuService private readonly menuService: IMenuService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IKeybindingService keybindingService: IKeybindingService, - @IStorageService storageService: IStorageService, - @IOpenerService openerService: IOpenerService, - @ITelemetryService telemetryService: ITelemetryService, + @IFileService protected readonly fileService: IFileService, // {{SQL CARBON EDIT}} + @IEditorService protected readonly editorService: IEditorService, // {{SQL CARBON EDIT}} + @IProgressService protected readonly progressService: IProgressService, // {{SQL CARBON EDIT}} + @INotificationService protected readonly notificationService: INotificationService, // {{SQL CARBON EDIT}} + @IDialogService protected readonly dialogService: IDialogService, // {{SQL CARBON EDIT}} + @IContextViewService protected readonly contextViewService: IContextViewService, // {{SQL CARBON EDIT}} + @IInstantiationService instantiationService: IInstantiationService, // {{SQL CARBON EDIT}} + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, // {{SQL CARBON EDIT}} + @IConfigurationService configurationService: IConfigurationService, // {{SQL CARBON EDIT}} + @IWorkspaceContextService protected readonly contextService: IWorkspaceContextService, // {{SQL CARBON EDIT}} + @ISearchWorkbenchService protected readonly searchWorkbenchService: ISearchWorkbenchService, // {{SQL CARBON EDIT}} + @IContextKeyService readonly contextKeyService: IContextKeyService, // {{SQL CARBON EDIT}} + @IReplaceService protected readonly replaceService: IReplaceService, // {{SQL CARBON EDIT}} + @ITextFileService protected readonly textFileService: ITextFileService, // {{SQL CARBON EDIT}} + @IPreferencesService protected readonly preferencesService: IPreferencesService, // {{SQL CARBON EDIT}} + @IThemeService themeService: IThemeService, // {{SQL CARBON EDIT}} + @ISearchHistoryService protected readonly searchHistoryService: ISearchHistoryService, // {{SQL CARBON EDIT}} + @IContextMenuService contextMenuService: IContextMenuService, // {{SQL CARBON EDIT}} + @IMenuService protected readonly menuService: IMenuService, // {{SQL CARBON EDIT}} + @IAccessibilityService protected readonly accessibilityService: IAccessibilityService, // {{SQL CARBON EDIT}} + @IKeybindingService keybindingService: IKeybindingService, // {{SQL CARBON EDIT}} + @IStorageService storageService: IStorageService, // {{SQL CARBON EDIT}} + @IOpenerService openerService: IOpenerService, // {{SQL CARBON EDIT}} + @ITelemetryService telemetryService: ITelemetryService, // {{SQL CARBON EDIT}} ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -361,7 +361,11 @@ export class SearchView extends ViewPane { this._register(this.onDidChangeBodyVisibility(visible => this.onVisibilityChanged(visible))); } - private onVisibilityChanged(visible: boolean): void { + protected callRenderBody(parent: HTMLElement): void { // {{SQL CARBON EDIT}} + super.renderBody(parent); // {{SQL CARBON EDIT}} + } // {{SQL CARBON EDIT}} + + protected onVisibilityChanged(visible: boolean): void { // {{SQL CARBON EDIT}} this.viewletVisible.set(visible); if (visible) { if (this.changedWhileHidden) { @@ -492,7 +496,7 @@ export class SearchView extends ViewPane { })); } - private onSearchResultsChanged(event?: IChangeEvent): void { + protected onSearchResultsChanged(event?: IChangeEvent): void { // {{SQL CARBON EDIT}} if (this.isVisible()) { return this.refreshAndUpdateCount(event); } else { @@ -500,7 +504,7 @@ export class SearchView extends ViewPane { } } - private refreshAndUpdateCount(event?: IChangeEvent): void { + protected refreshAndUpdateCount(event?: IChangeEvent): void { // {{SQL CARBON EDIT}} this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty()); this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles); return this.refreshTree(event); @@ -682,7 +686,7 @@ export class SearchView extends ViewPane { return nls.localize('replaceAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files?", occurrences, fileCount); } - private clearMessage(): HTMLElement { + protected clearMessage(): HTMLElement { // {{SQL CARBON EDIT}} this.searchWithoutFolderMessageElement = undefined; dom.clearNode(this.messagesElement); @@ -693,7 +697,7 @@ export class SearchView extends ViewPane { return dom.append(this.messagesElement, $('.message')); } - private createSearchResultsView(container: HTMLElement): void { + protected createSearchResultsView(container: HTMLElement): void { // {{SQL CARBON EDIT}} this.resultsElement = dom.append(container, $('.results.show-file-icons')); const delegate = this.instantiationService.createInstance(SearchDelegate); @@ -765,7 +769,7 @@ export class SearchView extends ViewPane { })); } - private onContextMenu(e: ITreeContextMenuEvent): void { + protected onContextMenu(e: ITreeContextMenuEvent): void { // {{SQL CARBON EDIT}} if (!this.contextMenu) { this.contextMenu = this._register(this.menuService.createMenu(MenuId.SearchContext, this.contextKeyService)); } @@ -972,7 +976,7 @@ export class SearchView extends ViewPane { } } - private reLayout(): void { + protected reLayout(): void { // {{SQL CARBON EDIT}} if (this.isDisposed) { return; } @@ -1529,7 +1533,7 @@ export class SearchView extends ViewPane { .then(onComplete, onError); } - private addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => { + protected addClickEvents = (element: HTMLElement, handler: (event: any) => void): void => { // {{SQL CARBON EDIT}} this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.CLICK, handler)); this.messageDisposables.push(dom.addDisposableListener(element, dom.EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); @@ -1548,7 +1552,7 @@ export class SearchView extends ViewPane { })); }; - private onOpenSettings = (e: dom.EventLike): void => { + protected onOpenSettings = (e: dom.EventLike): void => { // {{SQL CARBON EDIT}} dom.EventHelper.stop(e, false); this.openSettings('.exclude'); @@ -1561,13 +1565,13 @@ export class SearchView extends ViewPane { this.preferencesService.openGlobalSettings(undefined, options); } - private onLearnMore = (e: MouseEvent): void => { + protected onLearnMore = (e: MouseEvent): void => { // {{SQL CARBON EDIT}} dom.EventHelper.stop(e, false); this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977')); }; - private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void { + protected updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void { // {{SQL CARBON EDIT}} const fileCount = this.viewModel.searchResult.fileCount(); this.hasSearchResultsKey.set(fileCount > 0); @@ -1592,7 +1596,7 @@ export class SearchView extends ViewPane { this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue()); + this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern?.getValue(), this.searchExcludePattern?.getValue()); // {{SQL CARBON EDIT}} })); this.reLayout(); @@ -1613,7 +1617,7 @@ export class SearchView extends ViewPane { } } - private showSearchWithoutFolderMessage(): void { + protected showSearchWithoutFolderMessage(): void { // {{SQL CARBON EDIT}} this.searchWithoutFolderMessageElement = this.clearMessage(); const textEl = dom.append(this.searchWithoutFolderMessageElement, @@ -1639,7 +1643,7 @@ export class SearchView extends ViewPane { })); } - private showEmptyStage(forceHideMessages = false): void { + protected showEmptyStage(forceHideMessages = false): void { // {{SQL CARBON EDIT}} // disable 'result'-actions this.updateActions(); @@ -1786,7 +1790,7 @@ export class SearchView extends ViewPane { ]; } - private get searchConfig(): ISearchConfigurationProperties { + protected get searchConfig(): ISearchConfigurationProperties { // {{SQL CARBON EDIT}} return this.configurationService.getValue('search'); } @@ -1848,7 +1852,7 @@ export class SearchView extends ViewPane { super.saveState(); } - private async retrieveFileStats(): Promise { + protected async retrieveFileStats(): Promise { // {{SQL CARBON EDIT}} const files = this.searchResult.matches().filter(f => !f.fileStat).map(f => f.resolveFileStat(this.fileService)); await Promise.all(files); }