Skip to content

Commit

Permalink
siw: sync search results with editor content changes
Browse files Browse the repository at this point in the history
+ 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 <duc.a.nguyen@ericsson.com>
  • Loading branch information
DucNgn committed Dec 1, 2020
1 parent 0a72196 commit 8d3633d
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ export const searchInWorkspacePreferencesSchema: PreferenceSchema = {
default: 'auto',
type: 'string',
enum: ['auto', 'alwaysCollapse', 'alwaysExpand'],
},
'search.searchOnEditorModification': {
description: 'Search the active editor when modified.',
default: true,
type: 'boolean',
}
}
};

export class SearchInWorkspaceConfiguration {
'search.lineNumbers': boolean;
'search.collapseResults': string;
'search.searchOnEditorModification': boolean;
}

export const SearchInWorkspacePreferences = Symbol('SearchInWorkspacePreferences');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -262,31 +282,66 @@ 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(uri, pattern), opts));
}

/**
* Check whether the given editor meets the filtering criteria.
* Determine if the given editor satisfies the filtering criteria.
* An editor is valid if:
* - it is not excluded through the `excludes` list.
* - it is not explicitly present in a non-empty `includes` list.
*/
protected isEditorValid(editorWidget: EditorWidget, searchOptions: SearchInWorkspaceOptions): boolean {
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 })));
if (this.inPatternList(editorWidget.editor.uri, ignoredPatterns)) {
return false;
}

const includePatterns = searchOptions.include;
if (!!includePatterns?.length && !this.inPatternList(editorWidget.editor.uri, includePatterns)) {
return false;
}

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);

// 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 })));
// Check if outside workspace results are present before searching.
const hasExternalResultsBefore = includesExternalResults();

// 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();
}

/**
Expand All @@ -304,18 +359,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);
}
});

Expand All @@ -325,6 +374,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.isEditorValid(editorWidget, searchOptions)) {
return;
}

const matches: SearchMatch[] = this.findMatches(searchTerm, editorWidget, searchOptions);
if (matches.length <= 0) {
return;
}

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.
Expand Down Expand Up @@ -360,8 +436,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();
}
Expand All @@ -380,8 +458,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<void> {
this.searchTerm = searchTerm;
this.searchOptions = searchOptions;
searchOptions = {
...searchOptions,
exclude: this.getExcludeGlobs(searchOptions.exclude)
Expand Down Expand Up @@ -560,7 +644,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);
Expand Down

0 comments on commit 8d3633d

Please sign in to comment.