mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-26 01:25:38 -05:00
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:
@@ -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>());
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface ITreeItem extends vsITreeItem {
|
||||
export interface ITreeView extends vsITreeView {
|
||||
|
||||
collapse(element: ITreeItem): boolean
|
||||
root: ITreeItem;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
59
src/sql/workbench/contrib/notebook/common/constants.ts
Normal file
59
src/sql/workbench/contrib/notebook/common/constants.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user