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 5, 2019
1 parent 20251ea commit 7ae52cc
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 6 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,13 @@ export class ApplicationShell extends Widget {
return [...this.tracker.widgets];
}

/**
* Returns all tracked dirty widgets.
*/
get dirtyWidgets(): ReadonlyArray<Widget> {
return [...this.tracker.widgets.filter(w => Saveable.isDirty(w))];
}

canToggleMaximized(): boolean {
const area = this.currentWidget && this.getAreaFor(this.currentWidget);
return area === 'main' || area === 'bottom';
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 { 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 @@ -196,6 +197,106 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
});
}

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) {
if (allLines[i].indexOf(searchTerm) !== -1) {
const match = {
lineNumber: i + 1,
character: allLines[i].indexOf(searchTerm) + 1,
lineText: allLines[i]
};
matches.push(match);
}
}
return matches;
}

/**
* Searches in all currently tracked dirty files.
* @returns URIs of all the dirty files.
*/
searchInDirtyFiles(searchTerm: string): string[] {
// Stores URIs of the dirty files.
const dirtyFileUris: string[] = [];
const dirtyWidgets: EditorWidget[] = this.shell.dirtyWidgets.map((w: EditorWidget) => w);
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) {
// Get 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]+$/, ''),
}));

const { name, path } = this.filenameAndPath(root, fileUri);
const tree = this.resultTree;
const rootFolderNode = tree.get(dirtyResults[0].root);

if (rootFolderNode) {
const fileNode = rootFolderNode.children.find(f => f.fileUri === fileUri);
if (fileNode) {
dirtyFileUris.push(fileNode.fileUri);

dirtyResults.forEach(dirtyResult => {
const line = this.createResultLineNode(dirtyResult, 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(root, name, path, fileUri, rootFolderNode);
dirtyFileUris.push(newFileNode.fileUri);

dirtyResults.forEach(dirtyResult => {
const line = this.createResultLineNode(dirtyResult, newFileNode);
newFileNode.children.push(line);
});
rootFolderNode.children.push(newFileNode);
}
} else {
const newRootFolderNode = this.createRootFolderNode(root);
tree.set(root, newRootFolderNode);
const newFileNode = this.createFileNode(root, name, path, fileUri, newRootFolderNode);
dirtyFileUris.push(newFileNode.fileUri);

dirtyResults.forEach(dirtyResult => {
const line = this.createResultLineNode(dirtyResult, newFileNode);
newFileNode.children.push(line);
});
newRootFolderNode.children.push(newFileNode);
}
}
});

this.refreshModelChildren();
return dirtyFileUris;
}

async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise<void> {
this.searchTerm = searchTerm;
this.resultTree.clear();
Expand All @@ -206,11 +307,20 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
this.refreshModelChildren();
return;
}

// Searches in the dirty files first.
// Gets the list of URIs of the dirty files to avoid duplicated search results.
const dirtyFileUris: string[] = this.searchInDirtyFiles(searchTerm);

const searchId = await this.searchService.search(searchTerm, {
onResult: (aSearchId: number, result: SearchInWorkspaceResult) => {
if (token.isCancellationRequested || aSearchId !== searchId) {
return;
}
// Breaks if the match is from a dirty file (already searched).
if (dirtyFileUris.indexOf(result.fileUri) > -1) {
return;
}
const { name, path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
const rootFolderNode = tree.get(result.root);
Expand All @@ -235,7 +345,6 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
} 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);
Expand Down Expand Up @@ -402,8 +511,9 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
protected renderReplaceButton(node: TreeNode): React.ReactNode {
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>;
onClick={e => this.doReplace(node, e)
}
title={isResultLineNode ? 'Replace' : 'Replace All'} ></span >;
}

protected getFileCount(node: TreeNode): number {
Expand Down Expand Up @@ -502,7 +612,8 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
length,
lineText: lineText.replace(/[\r\n]+$/, ''),
};

numResults++;
if (this.client) {
this.client.onResult(searchId, result);
Expand Down

0 comments on commit 7ae52cc

Please sign in to comment.