From aab948396d9cce9f8c8aa1f7bea5cd06316c56e0 Mon Sep 17 00:00:00 2001 From: Christian Radke Date: Sun, 26 Mar 2023 13:21:21 +0200 Subject: [PATCH 01/11] search-in-workspace: add support for multi-line searches Signed-off-by: Christian Radke --- .../components/search-in-workspace-input.tsx | 66 +++++++++++---- ...search-in-workspace-result-tree-widget.tsx | 84 ++++++++++++------- .../browser/search-in-workspace-widget.tsx | 26 +++--- .../src/browser/styles/index.css | 27 ++++-- .../common/search-in-workspace-interface.ts | 4 + .../ripgrep-search-in-workspace-server.ts | 4 + 6 files changed, 141 insertions(+), 70 deletions(-) diff --git a/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx b/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx index 99b9a9e8da86d..576e82a00d540 100644 --- a/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx +++ b/packages/search-in-workspace/src/browser/components/search-in-workspace-input.tsx @@ -22,14 +22,15 @@ interface HistoryState { history: string[]; index: number; }; -type InputAttributes = React.InputHTMLAttributes; +type TextareaAttributes = React.TextareaHTMLAttributes; -export class SearchInWorkspaceInput extends React.Component { +export class SearchInWorkspaceTextArea extends React.Component { static LIMIT = 100; + static MAX_ROWS = 7; - private input = React.createRef(); + private textarea = React.createRef(); - constructor(props: InputAttributes) { + constructor(props: TextareaAttributes) { super(props); this.state = { history: [], @@ -52,31 +53,58 @@ export class SearchInWorkspaceInput extends React.Component): void => { - if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { + protected readonly onKeyDown = (e: React.KeyboardEvent): void => { + // Navigate history only when cursor is at first or last position of the textarea + if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionStart === 0) { e.preventDefault(); this.previousValue(); - } else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) { + } else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionEnd === e.currentTarget.value.length) { e.preventDefault(); this.nextValue(); } + + // Prevent newline on enter + if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && !e.nativeEvent.shiftKey) { + e.preventDefault(); + } + this.props.onKeyDown?.(e); }; /** - * Switch the input's text to the previous value, if any. + * Switch the textarea's text to the previous value, if any. */ previousValue(): void { const { history, index } = this.state; @@ -88,7 +116,7 @@ export class SearchInWorkspaceInput extends React.Component): void => { + protected readonly onChange = (e: React.ChangeEvent): void => { this.addToHistory(); + this.resizeTextarea(); this.props.onChange?.(e); }; @@ -121,18 +150,21 @@ export class SearchInWorkspaceInput extends React.Component term !== this.value) .concat(this.value) - .slice(-SearchInWorkspaceInput.LIMIT); + .slice(-SearchInWorkspaceTextArea.LIMIT); this.updateState(history.length - 1, history); } override render(): React.ReactNode { return ( - ); } 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 b2e4bcda5d117..b78db3df30c38 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 @@ -873,19 +873,34 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { // Open the file only if the function is called to replace all matches under a specific node. const widget: EditorWidget = replaceOne ? await this.doOpen(toReplace[0]) : await this.doGetWidget(toReplace[0]); const source: string = widget.editor.document.getText(); - const replaceOperations = toReplace.map(resultLineNode => ({ - text: replacementText, - range: { - start: { - line: resultLineNode.line - 1, - character: resultLineNode.character - 1 - }, - end: { - line: resultLineNode.line - 1, - character: resultLineNode.character - 1 + resultLineNode.length + + const replaceOperations = toReplace.map(resultLineNode => { + const lineText = typeof resultLineNode.lineText === 'string' ? resultLineNode.lineText : resultLineNode.lineText.text; + const lines = lineText.split('\n'); + let endCharacter = resultLineNode.character - 1 + resultLineNode.length; + if (lines.length > 1) { + endCharacter = resultLineNode.length - lines[0].length + resultLineNode.character - lines.length; + if (lines.length > 2) { + for (const lineNum of Array(lines.length - 2).keys()) { + endCharacter -= lines[lineNum + 1].length; + } } } - })); + return { + text: replacementText, + range: { + start: { + line: resultLineNode.line - 1, + character: resultLineNode.character - 1 + }, + end: { + line: resultLineNode.line + lines.length - 2, + character: endCharacter + } + } + }; + }); + // Replace the text. await widget.editor.replaceText({ source, @@ -1018,6 +1033,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { } const before = lineText.slice(start, character - 1).trimLeft(); + const multiline = lineText.includes('\n'); return
{this.searchInWorkspacePreferences['search.lineNumbers'] && {node.line}} @@ -1025,21 +1041,24 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { {before} {this.renderMatchLinePart(node)} - + {multiline || {lineText.slice(node.character + node.length - 1, 250 - before.length + node.length)} - + }
; } protected renderMatchLinePart(node: SearchInWorkspaceResultLineNode): React.ReactNode { - const replaceTerm = this.isReplacing ? {this._replaceTerm} : ''; + const replaceTermLines = this._replaceTerm.split('\n'); + const replaceTerm = this.isReplacing ? {replaceTermLines[0]} : ''; const className = `match${this.isReplacing ? ' strike-through' : ''}`; - const match = typeof node.lineText === 'string' ? - node.lineText.substring(node.character - 1, node.length + node.character - 1) - : node.lineText.text.substring(node.lineText.character - 1, node.length + node.lineText.character - 1); + const text = typeof node.lineText === 'string' ? node.lineText : node.lineText.text; + const match = text.substr(node.character - 1, node.length + node.character - 1); + const matchLines = match.split('\n'); + const matchLineNum = matchLines.length > 1 ? +{matchLines.length + node.character - 1} : ''; return - {match} + {matchLines[0]} {replaceTerm} + {matchLineNum} ; } @@ -1065,6 +1084,19 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { fileUri = new URI(node.fileUri); } + const lineText = typeof node.lineText === 'string' ? node.lineText : node.lineText.text; + const lines = lineText.split('\n'); + + let endCharacter = node.character - 1 + node.length; + if (lines.length > 1) { + endCharacter = node.length - lines[0].length + node.character - lines.length; + if (lines.length > 2) { + for (const lineNum of Array(lines.length - 2).keys()) { + endCharacter -= lines[lineNum + 1].length; + } + } + } + const opts: EditorOpenerOptions = { selection: { start: { @@ -1072,8 +1104,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { character: node.character - 1 }, end: { - line: node.line - 1, - character: node.character - 1 + node.length + line: node.line + lines.length - 2, + character: endCharacter } }, mode: preview ? 'reveal' : 'activate', @@ -1100,16 +1132,8 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { content = await resource.readContents(); } - const lines = content.split('\n'); - node.children.forEach(l => { - const leftPositionedNodes = node.children.filter(rl => rl.line === l.line && rl.character < l.character); - const diff = (this._replaceTerm.length - this.searchTerm.length) * leftPositionedNodes.length; - const start = lines[l.line - 1].substring(0, l.character - 1 + diff); - const end = lines[l.line - 1].substring(l.character - 1 + diff + l.length); - lines[l.line - 1] = start + this._replaceTerm + end; - }); - - return fileUri.withScheme(MEMORY_TEXT).withQuery(lines.join('\n')); + const searchTermRegExp = new RegExp(this.searchTerm, 'g'); + return fileUri.withScheme(MEMORY_TEXT).withQuery(content.replace(searchTermRegExp, this._replaceTerm)); } protected decorateEditor(node: SearchInWorkspaceFileNode | undefined, editorWidget: EditorWidget): void { diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx index fc6755c562255..914d6ad2bf98d 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.tsx @@ -27,7 +27,7 @@ import { CancellationTokenSource } from '@theia/core'; import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory'; import { EditorManager } from '@theia/editor/lib/browser'; import { SearchInWorkspacePreferences } from './search-in-workspace-preferences'; -import { SearchInWorkspaceInput } from './components/search-in-workspace-input'; +import { SearchInWorkspaceTextArea } from './components/search-in-workspace-input'; import { nls } from '@theia/core/lib/common/nls'; export interface SearchFieldState { @@ -65,10 +65,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge protected searchTerm = ''; protected replaceTerm = ''; - private searchRef = React.createRef(); - private replaceRef = React.createRef(); - private includeRef = React.createRef(); - private excludeRef = React.createRef(); + private searchRef = React.createRef(); + private replaceRef = React.createRef(); + private includeRef = React.createRef(); + private excludeRef = React.createRef(); protected _showReplaceField = false; protected get showReplaceField(): boolean { @@ -142,6 +142,7 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge matchCase: false, matchWholeWord: false, useRegExp: false, + multiline: false, includeIgnored: false, include: [], exclude: [], @@ -447,7 +448,8 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge const searchOptions: SearchInWorkspaceOptions = { ...this.searchInWorkspaceOptions, followSymlinks: this.shouldFollowSymlinks(), - matchCase: this.shouldMatchCase() + matchCase: this.shouldMatchCase(), + multiline: this.searchTerm.includes('\n') }; this.resultTreeWidget.search(this.searchTerm, searchOptions); } @@ -471,12 +473,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge } protected renderSearchField(): React.ReactNode { - const input = -
{nls.localizeByDefault('files to ' + kind)}
-