From 703ea549f6733486bbf27c5a3954c0718c7cf3e3 Mon Sep 17 00:00:00 2001 From: fangnx Date: Thu, 4 Jul 2019 16:47:05 -0400 Subject: [PATCH] Fixed #5609: supported workspace search in dirty (unsaved) 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 --- ...search-in-workspace-result-tree-widget.tsx | 169 ++++++++++++++---- 1 file changed, 138 insertions(+), 31 deletions(-) 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 c0dfa94a9d5d6..7e572d499e183 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,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 { this.searchTerm = searchTerm; + // 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(); @@ -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) { @@ -403,7 +510,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { const isResultLineNode = SearchInWorkspaceResultLineNode.is(node); return this.doReplace(node, e)} - title={isResultLineNode ? 'Replace' : 'Replace All'}>; + title={isResultLineNode ? 'Replace' : 'Replace All'} >; } protected getFileCount(node: TreeNode): number { @@ -502,7 +609,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } protected renderRemoveButton(node: TreeNode): React.ReactNode { - return this.remove(node, e)} title='Dismiss'>; + return this.remove(node, e)} title='Dismiss' >; } protected removeNode(node: TreeNode): void {