Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[siw]: sync search results with editor content changes #8765

Merged
merged 1 commit into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
}
};

Expand All @@ -50,6 +55,7 @@ export class SearchInWorkspaceConfiguration {
'search.collapseResults': string;
'search.searchOnType': boolean;
'search.searchOnTypeDebouncePeriod': number;
'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,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();
}

/**
Expand All @@ -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);
}
});

Expand All @@ -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.
Expand Down Expand Up @@ -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();
}
Expand All @@ -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<void> {
this.searchTerm = searchTerm;
this.searchOptions = searchOptions;
searchOptions = {
...searchOptions,
exclude: this.getExcludeGlobs(searchOptions.exclude)
Expand Down Expand Up @@ -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);
Expand Down