From c42cf1545565ff144a763301b3c1b70ec79ff81e Mon Sep 17 00:00:00 2001 From: DukeNgn Date: Mon, 2 Nov 2020 15:33:42 -0500 Subject: [PATCH] siw: sync search results with editor content changes + Add preference `search.searchOnEditorModification`. Enabling this option will make the search results in search view automatically get updated as user modify the editor. Signed-off-by: Duc Nguyen --- .../search-in-workspace-preferences.ts | 6 + ...search-in-workspace-result-tree-widget.tsx | 157 ++++++++++++++---- 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts b/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts index 2f5fc8a4dec28..77e70f6b344e1 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts @@ -42,6 +42,11 @@ export const searchInWorkspacePreferencesSchema: PreferenceSchema = { default: 300, type: 'number', }, + 'search.searchOnEditorModification': { + description: 'Search the active editor when modified.', + default: true, + type: 'boolean', + } } }; @@ -50,6 +55,7 @@ export class SearchInWorkspaceConfiguration { 'search.collapseResults': string; 'search.searchOnType': boolean; 'search.searchOnTypeDebouncePeriod': number; + 'search.searchOnEditorModification': boolean; } export const SearchInWorkspacePreferences = Symbol('SearchInWorkspacePreferences'); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx index e5e0769290a09..c7310761618ce 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx @@ -46,6 +46,8 @@ import { SearchInWorkspacePreferences } from './search-in-workspace-preferences' import { ProgressService } from '@theia/core'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import * as minimatch from 'minimatch'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import debounce = require('lodash.debounce'); const ROOT_ID = 'ResultTree'; @@ -106,6 +108,15 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { protected _showReplaceButtons = false; protected _replaceTerm = ''; protected searchTerm = ''; + protected searchOptions: SearchInWorkspaceOptions; + + protected readonly startSearchOnModification = (activeEditor: EditorWidget) => debounce( + () => this.searchActiveEditor(activeEditor, this.searchTerm, this.searchOptions), + this.searchOnEditorModificationDelay + ); + + protected readonly searchOnEditorModificationDelay = 300; + protected readonly toDisposeOnActiveEditorChanged = new DisposableCollection(); // The default root name to add external search results in the case that a workspace is opened. protected readonly defaultRootName = 'Other files'; @@ -163,8 +174,17 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { this.toDispose.push(this.changeEmitter); this.toDispose.push(this.focusInputEmitter); - this.toDispose.push(this.editorManager.onActiveEditorChanged(() => { + this.toDispose.push(this.editorManager.onActiveEditorChanged(activeEditor => { this.updateCurrentEditorDecorations(); + this.toDisposeOnActiveEditorChanged.dispose(); + this.toDispose.push(this.toDisposeOnActiveEditorChanged); + if (activeEditor) { + this.toDisposeOnActiveEditorChanged.push(activeEditor.editor.onDocumentContentChanged(() => { + if (this.searchTerm !== '' && this.searchInWorkspacePreferences['search.searchOnEditorModification']) { + this.startSearchOnModification(activeEditor)(); + } + })); + } })); this.toDispose.push(this.searchInWorkspacePreferences.onPreferenceChanged(() => { @@ -262,31 +282,69 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } /** - * Find the list of editors which meet the filtering criteria. - * @param editors the list of editors to filter. - * @param searchOptions the search options to apply. + * Determine if the URI matches any of the patterns. + * @param uri the editor URI. + * @param patterns the glob patterns to verify. */ - protected findMatchedEditors(editors: EditorWidget[], searchOptions: SearchInWorkspaceOptions): EditorWidget[] { - if (!editors.length) { - return []; + protected inPatternList(uri: URI, patterns: string[]): boolean { + const opts: minimatch.IOptions = { dot: true, matchBase: true }; + return patterns.some(pattern => minimatch( + uri.toString(), + this.convertPatternToGlob(this.workspaceService.getWorkspaceRootUri(uri), pattern), + opts + )); + } + + /** + * Determine if the given editor satisfies the filtering criteria. + * An editor should be searched only if: + * - it is not excluded through the `excludes` list. + * - it is not explicitly present in a non-empty `includes` list. + */ + protected shouldApplySearch(editorWidget: EditorWidget, searchOptions: SearchInWorkspaceOptions): boolean { + const excludePatterns = this.getExcludeGlobs(searchOptions.exclude); + if (this.inPatternList(editorWidget.editor.uri, excludePatterns)) { + return false; + } + + const includePatterns = searchOptions.include; + if (!!includePatterns?.length && !this.inPatternList(editorWidget.editor.uri, includePatterns)) { + return false; } - const ignoredPatterns = this.getExcludeGlobs(searchOptions.exclude); - editors = editors.filter(widget => !ignoredPatterns.some(pattern => minimatch( - widget.editor.uri.toString(), - this.convertPatternToGlob(this.workspaceService.getWorkspaceRootUri(widget.editor.uri), pattern), - { dot: true, matchBase: true }))); + return true; + } + + /** + * Search the active editor only and update the tree with those results. + */ + protected searchActiveEditor(activeEditor: EditorWidget, searchTerm: string, searchOptions: SearchInWorkspaceOptions): void { + const includesExternalResults = () => !!this.resultTree.get(this.defaultRootName); + + // Check if outside workspace results are present before searching. + const hasExternalResultsBefore = includesExternalResults(); - // Only include widgets that in `files to include`. - if (searchOptions.include && searchOptions.include.length > 0) { - const includePatterns: string[] = searchOptions.include; - editors = editors.filter(widget => includePatterns.some(pattern => minimatch( - widget.editor.uri.toString(), - this.convertPatternToGlob(this.workspaceService.getWorkspaceRootUri(widget.editor.uri), pattern), - { dot: true, matchBase: true }))); + // Collect search results for the given editor. + const results = this.searchInEditor(activeEditor, searchTerm, searchOptions); + + // Update the tree by removing the result node, and add new results if applicable. + this.getFileNodesByUri(activeEditor.editor.uri).forEach(fileNode => this.removeFileNode(fileNode)); + if (results) { + this.appendToResultTree(results); + } + + // Check if outside workspace results are present after searching. + const hasExternalResultsAfter = includesExternalResults(); + + // Redo a search to update the tree node visibility if: + // + `Other files` node was present, now it is not. + // + `Other files` node was not present, now it is. + if (hasExternalResultsBefore ? !hasExternalResultsAfter : hasExternalResultsAfter) { + this.search(this.searchTerm, this.searchOptions); + return; } - return editors; + this.handleSearchCompleted(); } /** @@ -304,18 +362,12 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { let numberOfResults = 0; const searchResults: SearchInWorkspaceResult[] = []; - const editors = this.findMatchedEditors(this.editorManager.all, searchOptions); - editors.forEach(async widget => { - const matches = this.findMatches(searchTerm, widget, searchOptions); - if (matches.length > 0) { - numberOfResults += matches.length; - const fileUri: string = widget.editor.uri.toString(); - const root: string | undefined = this.workspaceService.getWorkspaceRootUri(widget.editor.uri)?.toString(); - searchResults.push({ - root: root ?? this.defaultRootName, - fileUri, - matches - }); + + this.editorManager.all.forEach(e => { + const editorResults = this.searchInEditor(e, searchTerm, searchOptions); + if (editorResults) { + numberOfResults += editorResults.matches.length; + searchResults.push(editorResults); } }); @@ -325,6 +377,33 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { }; } + /** + * Perform a search in the target editor. + * @param editorWidget the editor widget. + * @param searchTerm the search term. + * @param searchOptions the search options to apply. + * + * @returns the search results from the given editor, undefined if the editor is either filtered or has no matches found. + */ + protected searchInEditor(editorWidget: EditorWidget, searchTerm: string, searchOptions: SearchInWorkspaceOptions): SearchInWorkspaceResult | undefined { + if (!this.shouldApplySearch(editorWidget, searchOptions)) { + return undefined; + } + + const matches: SearchMatch[] = this.findMatches(searchTerm, editorWidget, searchOptions); + if (matches.length <= 0) { + return undefined; + } + + const fileUri = editorWidget.editor.uri.toString(); + const root: string | undefined = this.workspaceService.getWorkspaceRootUri(editorWidget.editor.uri)?.toString(); + return { + root: root ?? this.defaultRootName, + fileUri, + matches + }; + } + /** * Append search results to the result tree. * @param result Search result. @@ -360,8 +439,10 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { /** * Handle when searching completed. */ - protected handleSearchCompleted(cancelIndicator: CancellationTokenSource): void { - cancelIndicator.cancel(); + protected handleSearchCompleted(cancelIndicator?: CancellationTokenSource): void { + if (cancelIndicator) { + cancelIndicator.cancel(); + } this.sortResultTree(); this.refreshModelChildren(); } @@ -380,8 +461,14 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { }); } + /** + * Search and populate the result tree with matches. + * @param searchTerm the search term. + * @param searchOptions the search options to apply. + */ async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise { this.searchTerm = searchTerm; + this.searchOptions = searchOptions; searchOptions = { ...searchOptions, exclude: this.getExcludeGlobs(searchOptions.exclude) @@ -560,7 +647,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { const fileUri = uri.withScheme('file').toString(); for (const rootFolderNode of this.resultTree.values()) { const rootUri = new URI(rootFolderNode.path).withScheme('file'); - if (rootUri.isEqualOrParent(uri)) { + if (rootUri.isEqualOrParent(uri) || rootFolderNode.id === this.defaultRootName) { for (const fileNode of rootFolderNode.children) { if (fileNode.fileUri === fileUri) { nodes.push(fileNode);