Skip to content

Commit

Permalink
siw: Perform workspace search in dirty file content
Browse files Browse the repository at this point in the history
+ `Search In Workspace` can now search content in dirty files and display the results in siw view.
+ Utilized the `findMatches` function from `monaco editor` to get the search matches.
+ Added `minimatch` as a dependency in `siw`

Co-authored-by: fangnx <[email protected]>
Co-authored-by: vince-fugnitto <[email protected]>
Signed-off-by: DukeNgn <[email protected]>
  • Loading branch information
3 people committed Oct 6, 2020
1 parent 56c158d commit 311ddfb
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 22 deletions.
52 changes: 52 additions & 0 deletions packages/editor/src/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,46 @@ export const enum EncodingMode {
Decode
}

/**
* Options for searching matches in a model.
*/
export interface FindMatchesOptions {
/**
* The string used to search. If it is a regular expression, set `isRegex` to true.
*/
searchString: string;
/**
* Used to indicate the `searchString` is a regular expression.
*/
isRegex: boolean;
/**
* Force the matching to match lower/upper case exactly.
*/
matchCase: boolean;
/**
* Force the matching to match entire words only.
*/
matchWholeWord: boolean;
/**
* Limit the number of results.
*/
limitResultCount?: number;
}

/**
* The result of searching matches in a model.
*/
export interface FindMatch {
/**
* The matched string. Undefined if nothing has been found.
*/
readonly matches: string[] | undefined;
/**
* The ranges where the matches are. It is empty if no matches have been found.
*/
readonly range: Range;
}

export interface TextEditor extends Disposable, TextEditorSelection, Navigatable {
readonly node: HTMLElement;

Expand Down Expand Up @@ -237,6 +277,18 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable
setEncoding(encoding: string, mode: EncodingMode): void;

readonly onEncodingChanged: Event<string>;

/**
* Find matches in an editor for the given options.
* @param options the options for finding matches.
* @returns the list of matches.
*/
findMatches?(options: FindMatchesOptions): FindMatch[];

/**
* Get the text for a certain line.
*/
getLineContent?(lineNumber: number): string;
}

export interface Dimension {
Expand Down
29 changes: 28 additions & 1 deletion packages/monaco/src/browser/monaco-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
ReplaceTextParams,
EditorDecoration,
EditorMouseEvent,
EncodingMode
EncodingMode, FindMatchesOptions, FindMatch
} from '@theia/editor/lib/browser';
import { MonacoEditorModel } from './monaco-editor-model';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
Expand Down Expand Up @@ -121,6 +121,33 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
return this.document.setEncoding(encoding, mode);
}

findMatches(options: FindMatchesOptions): FindMatch[] {
const model = this.editor.getModel();
if (!model) {
return [];
}
const results: monaco.editor.FindMatch[] = model.findMatches(
options.searchString,
false,
options.isRegex,
options.matchCase,
// eslint-disable-next-line no-null/no-null
options.matchWholeWord ? options.searchString : null,
true,
options.limitResultCount
);
const extractedMatches: FindMatch[] = results.map(r => ({
matches: r.matches ? r.matches : undefined,
range: Range.create(r.range.startLineNumber, r.range.startColumn, r.range.endLineNumber, r.range.endColumn)
}));
return extractedMatches;
}

getLineContent(lineNumber: number): string {
const model = this.editor.getModel();
return model ? model.getLineContent(lineNumber) : '';
}

protected create(options?: IStandaloneEditorConstructionOptions, override?: monaco.editor.IEditorOverrideServices): Disposable {
return this.editor = monaco.editor.create(this.node, {
...options,
Expand Down
1 change: 1 addition & 0 deletions packages/search-in-workspace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@theia/navigator": "^1.6.0",
"@theia/process": "^1.6.0",
"@theia/workspace": "^1.6.0",
"minimatch": "^3.0.4",
"vscode-ripgrep": "^1.2.4"
},
"publishConfig": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import {
DiffUris
} from '@theia/core/lib/browser';
import { CancellationTokenSource, Emitter, Event } from '@theia/core';
import { EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane, EditorWidget, ReplaceOperation, EditorOpenerOptions } from '@theia/editor/lib/browser';
import {
EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane,
EditorWidget, ReplaceOperation, EditorOpenerOptions, FindMatch
} from '@theia/editor/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileResourceResolver, FileSystemPreferences } from '@theia/filesystem/lib/browser';
import { SearchInWorkspaceResult, SearchInWorkspaceOptions, SearchMatch } from '../common/search-in-workspace-interface';
Expand All @@ -42,6 +45,7 @@ import * as React from 'react';
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';

const ROOT_ID = 'ResultTree';

Expand Down Expand Up @@ -102,6 +106,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
protected _showReplaceButtons = false;
protected _replaceTerm = '';
protected searchTerm = '';
protected dirtyFileUris = new Set<string>();

protected appliedDecorations = new Map<string, string[]>();

Expand All @@ -117,10 +122,10 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(TreeExpansionService) protected readonly expansionService: TreeExpansionService;
@inject(FileSystemPreferences) protected readonly fileSystemPreferences: FileSystemPreferences;
@inject(SearchInWorkspacePreferences) protected readonly searchInWorkspacePreferences: SearchInWorkspacePreferences;
@inject(ProgressService) protected readonly progressService: ProgressService;
@inject(ColorRegistry) protected readonly colorRegistry: ColorRegistry;
@inject(FileSystemPreferences) protected readonly filesystemPreferences: FileSystemPreferences;

constructor(
@inject(TreeProps) readonly props: TreeProps,
Expand Down Expand Up @@ -199,13 +204,130 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
});
}

/**
* Find matches in a single widget.
* @param searchTerm The search keyword.
* @param widget The widget that is being searched.
* @param searchOptions The options for search operation.
*/
protected findMatches(searchTerm: string, widget: EditorWidget, searchOptions: SearchInWorkspaceOptions): SearchMatch[] {
const matches: SearchMatch[] = [];
if (!widget.editor.findMatches || !widget.editor.getLineContent) {
return [];
}
const results: FindMatch[] = widget.editor.findMatches({
searchString: searchTerm,
isRegex: !!searchOptions.useRegExp,
matchCase: !!searchOptions.matchCase,
matchWholeWord: !!searchOptions.matchWholeWord,
limitResultCount: searchOptions.maxResults
});
if (results.length > 0) {
results.forEach(r => {
if (!widget.editor.getLineContent) {
return [];
}
const lineText: string = widget.editor.getLineContent(r.range.start.line);
matches.push({
line: r.range.start.line,
character: r.range.start.character,
length: r.range.end.character - r.range.start.character,
lineText
});
});
if (searchOptions.maxResults) {
searchOptions.maxResults -= results.length;
}
}
return matches;
}

protected filterEditorWidgets(widgets: EditorWidget[], searchOptions: SearchInWorkspaceOptions): EditorWidget[] {
if (!widgets.length) {
return [];
}
// Exclude dirty widgets that should be ignored in glob.
if (!searchOptions.includeIgnored) {
const ignoredPatterns = this.fileSystemPreferences.get('files.exclude');
widgets = widgets.filter(widget => {
for (const pattern in ignoredPatterns) {
if (ignoredPatterns[pattern] && minimatch(widget.editor.uri.toString(), pattern)) {
return false;
}
}
return true;
});
}
// Only include dirty widgets that in `files to include`.
if (searchOptions.include && searchOptions.include.length > 0) {
const includePatterns: string[] = searchOptions.include;
widgets = widgets.filter(widget => includePatterns.some(pattern => minimatch(widget.editor.uri.toString(), pattern)));
}
// Exclude dirty widgets that are in `files to exclude`
if (searchOptions.exclude && searchOptions.exclude.length > 0) {
const excludePatterns: string[] = searchOptions.exclude;
widgets = widgets.filter(widget => !excludePatterns.some(pattern => minimatch(widget.editor.uri.toString(), pattern)));
}
return widgets;
}

/**
* Perform searching in all dirty editors.
* @param searchTerm The search keyword.
* @param searchOptions The options for search operation.
* @returns The number of matches in dirty editors.
*/
protected searchInDirtyEditors(searchTerm: string, searchOptions: SearchInWorkspaceOptions): void {
let dirtyWidgets: EditorWidget[] = this.editorManager.all.filter(widget => widget.saveable.dirty);
dirtyWidgets = this.filterEditorWidgets(dirtyWidgets, searchOptions);
dirtyWidgets.forEach(async widget => {
const matches = this.findMatches(searchTerm, widget, searchOptions);
if (matches?.length) {
const fileUri: string = widget.editor.uri.toString();
const root: string = this.workspaceService.getWorkspaceRootUri(widget.editor.uri)?.toString()!;
this.appendToResultTree({ root, fileUri, matches }, true);
}
});
}

/**
* Append search results to the result tree
* @param result Search results
* @param isDirtyResult Whether the search results are from a dirty file
*/
protected appendToResultTree(result: SearchInWorkspaceResult, isDirtyResult?: boolean): void {
const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults'];
const { path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
let rootFolderNode = tree.get(result.root);
if (!rootFolderNode) {
rootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, rootFolderNode);
}
let fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (!fileNode) {
fileNode = this.createFileNode(result.root, path, result.fileUri, rootFolderNode);
rootFolderNode.children.push(fileNode);
}
if (isDirtyResult) {
this.dirtyFileUris.add(fileNode.fileUri);
}
for (const match of result.matches) {
const line = this.createResultLineNode(result, match, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
}
}
this.collapseFileNode(fileNode, collapseValue);
}

async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise<void> {
this.searchTerm = searchTerm;
const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults'];
searchOptions = {
...searchOptions,
exclude: this.getExcludeGlobs(searchOptions.exclude)
};
this.dirtyFileUris.clear();
this.resultTree.clear();
if (this.cancelIndicator) {
this.cancelIndicator.cancel();
Expand All @@ -226,32 +348,22 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
this.cancelIndicator = undefined;
this.changeEmitter.fire(this.resultTree);
});
this.searchInDirtyEditors(searchTerm, searchOptions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let pendingRefreshTimeout: any;
const searchId = await this.searchService.search(searchTerm, {
onResult: (aSearchId: number, result: SearchInWorkspaceResult) => {
if (token.isCancellationRequested || aSearchId !== searchId) {
return;
}
const { path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
let rootFolderNode = tree.get(result.root);
if (!rootFolderNode) {
rootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, rootFolderNode);
}
let fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (!fileNode) {
fileNode = this.createFileNode(result.root, path, result.fileUri, rootFolderNode);
rootFolderNode.children.push(fileNode);
}
for (const match of result.matches) {
const line = this.createResultLineNode(result, match, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
if (this.dirtyFileUris.has(result.fileUri)) {
// Give back one result counter to maxResults since this is a duplicate
if (searchOptions.maxResults) {
searchOptions.maxResults++;
}
return;
}
this.collapseFileNode(fileNode, collapseValue);
this.appendToResultTree(result);
if (pendingRefreshTimeout) {
clearTimeout(pendingRefreshTimeout);
}
Expand Down Expand Up @@ -785,7 +897,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
* @returns the list of exclude globs.
*/
protected getExcludeGlobs(excludeOptions?: string[]): string[] {
const excludePreferences = this.filesystemPreferences['files.exclude'];
const excludePreferences = this.fileSystemPreferences['files.exclude'];
const excludePreferencesGlobs = Object.keys(excludePreferences).filter(key => !!excludePreferences[key]);
return [...new Set([...excludePreferencesGlobs, ...excludeOptions])];
}
Expand Down

0 comments on commit 311ddfb

Please sign in to comment.