diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx index e5e0ef2f3a1aa..f9368773095c4 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx @@ -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'; @@ -100,6 +101,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { protected _showReplaceButtons = false; protected _replaceTerm = ''; protected searchTerm = ''; + protected dirtyFileUris = new Set(); protected appliedDecorations = new Map(); @@ -196,9 +198,134 @@ 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 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. + */ + 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 { this.searchTerm = searchTerm; - const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults']; + // Stores URIs of the dirty editors to avoid duplicated search results. + this.dirtyFileUris = new Set(); this.resultTree.clear(); this.cancelIndicator.cancel(); this.cancelIndicator = new CancellationTokenSource(); @@ -207,39 +334,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); - } - 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) {