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 8, 2019
1 parent 20251ea commit 703ea54
Showing 1 changed file with 138 additions and 31 deletions.
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 { FileUri } from '@theia/core/lib/node/file-uri';
import URI from '@theia/core/lib/common/uri';
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,8 +198,133 @@ 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(FileUri.create(filePath).withScheme('file')));
if (roots.length > 0) {
return FileUri.create(FileUri.fsPath(roots.sort((r1, r2) => r2.length - r1.length)[0]));
}
return new URI();
}

/**
* Returns all matches in a dirty file.
*/
findMatches(searchTerm: string, fileContent: string): { lineNumber: number, character: number, lineText: string }[] {
const matches = [];
const allLines = fileContent.split('\n');
for (let i = 0; i < allLines.length; i += 1) {
const currLine = allLines[i];
if (currLine.indexOf(searchTerm) !== -1) {
// Gets all matches in a line.
const re = RegExp(searchTerm, 'g');
while (re.exec(currLine)) {
const match = {
lineNumber: i + 1,
character: re.lastIndex - searchTerm.length + 1,
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 { 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);
}
if (fileNode.children.length >= 20 && fileNode.expanded) {
fileNode.expanded = false;
}
} else {
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, rootFolderNode);
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);
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.
*/
searchInDirtyFiles(searchTerm: string): void {
// Gets all dirty editor widgets.
const dirtyWidgets: EditorWidget[] = this.editorManager.all.filter(w => w.saveable.dirty);

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

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: searchTerm.length,
lineText: match.lineText.replace(/[\r\n]+$/, ''),
}));

dirtyResults.forEach(result => this.addToResultTree(result, true));
}
});
}

/**
* Seaches in all files in the current workspace.
*/
async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise<void> {
this.searchTerm = searchTerm;
// Stores URIs of the dirty editors to avoid duplicated search results.
this.dirtyFileUris = new Set<string>();
this.resultTree.clear();
this.cancelIndicator.cancel();
this.cancelIndicator = new CancellationTokenSource();
Expand All @@ -206,40 +333,20 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
this.refreshModelChildren();
return;
}

// Searches in the dirty editors first.
this.searchInDirtyFiles(searchTerm);

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);
}
if (fileNode.children.length >= 20 && fileNode.expanded) {
fileNode.expanded = false;
}
} else {
const newFileNode = this.createFileNode(result.root, name, path, result.fileUri, rootFolderNode);
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);
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 Expand Up @@ -403,7 +510,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
const isResultLineNode = SearchInWorkspaceResultLineNode.is(node);
return <span className={isResultLineNode ? 'replace-result' : 'replace-all-result'}
onClick={e => this.doReplace(node, e)}
title={isResultLineNode ? 'Replace' : 'Replace All'}></span>;
title={isResultLineNode ? 'Replace' : 'Replace All'} ></span >;
}

protected getFileCount(node: TreeNode): number {
Expand Down Expand Up @@ -502,7 +609,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
}

protected renderRemoveButton(node: TreeNode): React.ReactNode {
return <span className='remove-node' onClick={e => this.remove(node, e)} title='Dismiss'></span>;
return <span className='remove-node' onClick={e => this.remove(node, e)} title='Dismiss' ></span >;
}

protected removeNode(node: TreeNode): void {
Expand Down

0 comments on commit 703ea54

Please sign in to comment.