Skip to content

Commit

Permalink
Fixed eclipse-theia#5609: supported workspace search in dirty (unsave…
Browse files Browse the repository at this point in the history
…d) file content

- The `Search-in-Workspace` now can correctly search content in dirty files (file with unsaved changes).
- Search results in dirty files will be replaced in correct line and character position.
- Implemented by conducting a search in all currently tracked dirty files, before the backend ripgrep search.

Signed-off-by: fangnx <[email protected]>
  • Loading branch information
fangnx committed Jul 29, 2019
1 parent 60dcb33 commit 5ef79f5
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 29 deletions.
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": "^0.8.0",
"@theia/process": "^0.8.0",
"@theia/workspace": "^0.8.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 @@ -39,10 +39,11 @@ import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileResourceResolver } from '@theia/filesystem/lib/browser';
import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
import { SearchInWorkspaceService } from './search-in-workspace-service';
import { SearchInWorkspacePreferences } from './search-in-workspace-preferences';
import { MEMORY_TEXT } from './in-memory-text-resource';
import URI from '@theia/core/lib/common/uri';
import * as minimatch from 'minimatch';
import * as React from 'react';
import { SearchInWorkspacePreferences } from './search-in-workspace-preferences';

const ROOT_ID = 'ResultTree';

Expand Down Expand Up @@ -100,6 +101,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 Down Expand Up @@ -196,9 +198,166 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
});
}

/**
* Returns the root folder URI that a file belongs to.
* In the case that a file belongs to more than one root folders, returns the root folder closest to the file.
* If the file is not from the current workspace, returns an empty URI.
* @param {string} filePath - path of the file.
* @param {stirng[]} rootUris - URIs of the root folders in the workspace
* @returns URI of the root folder.
*/
getRoot(filePath: string, rootUris: string[]): URI {
const roots = rootUris.filter(root => new URI(root).withScheme('file').isEqualOrParent(new URI(filePath).withScheme('file')));
if (roots.length > 0) {
return new URI(roots.sort((r1, r2) => r2.length - r1.length)[0]);
}
return new URI();
}

/**
* Returns all matches in a dirty file.
*/
findMatches(searchTerm: string, fileContent: string, options: SearchInWorkspaceOptions): { lineNumber: number, character: number, length: number, lineText: string }[] {
const matches = [];
const allLines: string[] = fileContent.split(/[\n\u0085\u2028\u2029]|\r\n?/);
// Check if RegEx search is supported.
// Since RegEx is still used for result matching, if RegEx search option is off, all the special characters need to be escaped.
if (!options.useRegExp) {
searchTerm = searchTerm.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&');
}
// Check if results should be matched case sensitively.
const reFlags = options.matchCase ? 'g' : 'gi';
// Check if only whole words should be matched.
if (options.matchWholeWord) {
searchTerm = '\\b' + searchTerm + '\\b';
console.log(searchTerm);
}
const re = RegExp(searchTerm, reFlags);

for (let i = 0; i < allLines.length; i++) {
const currLine = allLines[i];
let reMatch: RegExpExecArray | null;
while (reMatch = re.exec(currLine)) {
const match = {
lineNumber: i + 1,
character: reMatch.index + 1,
length: reMatch[0].length,
lineText: currLine
};
matches.push(match);
}
}
return matches;
}

/**
* Adds a search result to the result tree.
* If result is from a dirty file, it will be added to the `dirtyFileUris`.
* @param {SearchInWorkspaceResult} result - the search result.
* @param {boolean} [isDirtyResult] - whether the search result is from a dirty file.
*/
addToResultTree(result: SearchInWorkspaceResult, isDirtyResult?: boolean): void {
const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults'];
const { name, path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
const rootFolderNode = tree.get(result.root);

if (rootFolderNode) {
const fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (fileNode) {
if (isDirtyResult) {
this.dirtyFileUris.add(fileNode.fileUri);
}

const line = this.createResultLineNode(result, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
}
this.collapseFileNode(fileNode, collapseValue);
} else {
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, rootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
if (isDirtyResult) {
this.dirtyFileUris.add(newFileNode.fileUri);
}

const line = this.createResultLineNode(result, newFileNode);
newFileNode.children.push(line);
rootFolderNode.children.push(newFileNode);
}

} else {
const newRootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, newRootFolderNode);
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, newRootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
if (isDirtyResult) {
this.dirtyFileUris.add(newFileNode.fileUri);
}

const line = this.createResultLineNode(result, newFileNode);
newFileNode.children.push(line);
newRootFolderNode.children.push(newFileNode);
}
}

/**
* Searches in all dirty editors in the current workspace.
* @returns The number of match results in dirty files.
*/
searchInDirtyFiles(searchTerm: string, searchOptions: SearchInWorkspaceOptions): number {
// Gets all dirty editor widgets.
let dirtyWidgets: EditorWidget[] = this.editorManager.all.filter(w => w.saveable.dirty);
// Filter to only search dirty widgets in `files to include`.
if (searchOptions.include && searchOptions.include.length > 0) {
const includedPatterns: string[] = searchOptions.include;
dirtyWidgets = dirtyWidgets.filter(widget => includedPatterns.some(pattern => minimatch(widget.title.label, pattern)));
}
// Filter to only search dirty widgets that are not in `files to exclude`.
if (searchOptions.exclude && searchOptions.exclude.length) {
const excludedPatterns: string[] = searchOptions.exclude;
dirtyWidgets = dirtyWidgets.filter(widget => !excludedPatterns.some(pattern => minimatch(widget.title.label, pattern)));
}
// TODO: support includeIgnored option.
// if (!searchOptions.includeIgnored) {}

let numberOfResults = 0;
dirtyWidgets.forEach(async w => {
const fileUri: string = w.editor.uri.toString();
const roots = await this.workspaceService.roots;
const root: string = this.getRoot(w.editor.uri.path.toString(), roots.map(r => r.uri)).toString();
const fileContent: string = w.editor.document.getText();
const matches = this.findMatches(searchTerm, fileContent, searchOptions);

if (matches.length) {
// Gets all match results in a file.
const dirtyResults: SearchInWorkspaceResult[] = matches.map(match => ({
fileUri,
root,
line: match.lineNumber,
character: match.character,
length: match.length,
lineText: match.lineText.replace(/[\r\n]+$/, ''),
}));
// Check if the number of match results exceed the maximum amount.
if (searchOptions.maxResults && numberOfResults + matches.length >= searchOptions.maxResults) {
dirtyResults.slice(0, searchOptions.maxResults - numberOfResults).forEach(result => this.addToResultTree(result, true));
return searchOptions.maxResults;
}
dirtyResults.forEach(result => this.addToResultTree(result, true));
numberOfResults += matches.length;
}
});
return numberOfResults;
}

/**
* Seaches in all files in the current workspace.
*/
async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise<void> {
this.searchTerm = searchTerm;
const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults'];
// Stores URIs of the dirty editors to avoid duplicated search results.
this.dirtyFileUris.clear();
this.resultTree.clear();
this.cancelIndicator.cancel();
this.cancelIndicator = new CancellationTokenSource();
Expand All @@ -207,39 +366,23 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
this.refreshModelChildren();
return;
}

// Searches in the dirty editors first.
const numberOfDirtyResults = this.searchInDirtyFiles(searchTerm, searchOptions);
if (searchOptions.maxResults) {
searchOptions.maxResults -= numberOfDirtyResults;
}

const searchId = await this.searchService.search(searchTerm, {
onResult: (aSearchId: number, result: SearchInWorkspaceResult) => {
if (token.isCancellationRequested || aSearchId !== searchId) {
return;
}
const { name, path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
const rootFolderNode = tree.get(result.root);

if (rootFolderNode) {
const fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (fileNode) {
const line = this.createResultLineNode(result, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
}
this.collapseFileNode(fileNode, collapseValue);
} else {
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, rootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
const line = this.createResultLineNode(result, newFileNode);
newFileNode.children.push(line);
rootFolderNode.children.push(newFileNode);
}

} else {
const newRootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, newRootFolderNode);
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, newRootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
newFileNode.children.push(this.createResultLineNode(result, newFileNode));
newRootFolderNode.children.push(newFileNode);
// Breaks if the match is from a dirty file (already searched).
if (this.dirtyFileUris.has(result.fileUri)) {
return;
}
this.addToResultTree(result);
},
onDone: () => {
if (token.isCancellationRequested) {
Expand Down

0 comments on commit 5ef79f5

Please sign in to comment.