diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index bc0caaee195..2353ef8d953 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -204,6 +204,9 @@ import { DebugConfigurationManager } from './theia/debug/debug-configuration-man import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory'; import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory'; +import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget'; +import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget'; +import { createTreeContainer } from '@theia/core/lib/browser/tree/tree-container'; import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider'; import { MonacoEditorFactory, @@ -610,6 +613,20 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .to(SearchInWorkspaceFactory) .inSingletonScope(); + // Fix regex replace escape sequences (issue #2803) + // The widget uses a factory pattern, so we need to rebind with a custom factory + rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue((ctx) => { + const child = createTreeContainer(ctx.container, { + widget: SearchInWorkspaceResultTreeWidget, + props: { + contextMenuPath: ['search-in-workspace-result-tree-context-menu'], + multiSelect: true, + globalSelection: true, + }, + }); + return child.get(SearchInWorkspaceResultTreeWidget); + }); + // Show a disconnected status bar, when the daemon is not available bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope(); rebind(TheiaApplicationConnectionStatusContribution).toService( diff --git a/arduino-ide-extension/src/browser/theia/search-in-workspace/search-in-workspace-result-tree-widget.ts b/arduino-ide-extension/src/browser/theia/search-in-workspace/search-in-workspace-result-tree-widget.ts new file mode 100644 index 00000000000..ec84ae8c47a --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/search-in-workspace/search-in-workspace-result-tree-widget.ts @@ -0,0 +1,66 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { TreeNode } from '@theia/core/lib/browser/tree'; +import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget'; + +/** + * Parses escape sequences in replacement string when regex mode is enabled. + * Converts \n, \t, \r, \\ to actual characters. + * + * @param replaceString The raw replacement string with escape sequences + * @returns The processed string with escape sequences converted to actual characters + */ +export function parseReplaceString(replaceString: string): string { + let result = ''; + let i = 0; + while (i < replaceString.length) { + const char = replaceString[i]; + if (char === '\\' && i + 1 < replaceString.length) { + const nextChar = replaceString[i + 1]; + switch (nextChar) { + case 'n': + result += '\n'; + i += 2; + continue; + case 't': + result += '\t'; + i += 2; + continue; + case 'r': + result += '\r'; + i += 2; + continue; + case '\\': + result += '\\'; + i += 2; + continue; + default: + result += char; + i++; + continue; + } + } + result += char; + i++; + } + return result; +} + +@injectable() +export class SearchInWorkspaceResultTreeWidget extends TheiaSearchInWorkspaceResultTreeWidget { + /** + * Override replaceResult to parse escape sequences in replacement text + * when regex mode is enabled. Fixes GitHub issue #2803. + * + * https://github.com/arduino/arduino-ide/issues/2803 + */ + protected override async replaceResult( + node: TreeNode, + replaceOne: boolean, + replacementText: string + ): Promise { + const processedText = this.searchOptions?.useRegExp + ? parseReplaceString(replacementText) + : replacementText; + return super.replaceResult(node, replaceOne, processedText); + } +} diff --git a/arduino-ide-extension/src/test/browser/parse-replace-string.test.ts b/arduino-ide-extension/src/test/browser/parse-replace-string.test.ts new file mode 100644 index 00000000000..66fabd08160 --- /dev/null +++ b/arduino-ide-extension/src/test/browser/parse-replace-string.test.ts @@ -0,0 +1,62 @@ +import { expect } from 'chai'; +import { parseReplaceString } from '../../browser/theia/search-in-workspace/search-in-workspace-result-tree-widget'; + +describe('parseReplaceString', () => { + it('should convert \\n to newline', () => { + expect(parseReplaceString('\\n')).to.equal('\n'); + }); + + it('should convert \\t to tab', () => { + expect(parseReplaceString('\\t')).to.equal('\t'); + }); + + it('should convert \\r to carriage return', () => { + expect(parseReplaceString('\\r')).to.equal('\r'); + }); + + it('should convert \\\\ to single backslash', () => { + expect(parseReplaceString('\\\\')).to.equal('\\'); + }); + + it('should handle \\\\n as literal backslash followed by n', () => { + expect(parseReplaceString('\\\\n')).to.equal('\\n'); + }); + + it('should handle mixed content', () => { + expect(parseReplaceString('hello\\nworld')).to.equal('hello\nworld'); + }); + + it('should handle multiple sequences', () => { + expect(parseReplaceString('\\n\\n')).to.equal('\n\n'); + }); + + it('should return unchanged string without escape sequences', () => { + expect(parseReplaceString('hello')).to.equal('hello'); + }); + + it('should handle empty string', () => { + expect(parseReplaceString('')).to.equal(''); + }); + + it('should keep unknown escape sequences as backslash followed by char', () => { + expect(parseReplaceString('\\x')).to.equal('\\x'); + }); + + it('should handle trailing backslash', () => { + expect(parseReplaceString('hello\\')).to.equal('hello\\'); + }); + + it('should handle complex mixed content', () => { + expect(parseReplaceString('line1\\nline2\\ttabbed\\r\\nwindows')).to.equal( + 'line1\nline2\ttabbed\r\nwindows' + ); + }); + + it('should handle multiple escaped backslashes', () => { + expect(parseReplaceString('\\\\\\\\')).to.equal('\\\\'); + }); + + it('should preserve text around escape sequences', () => { + expect(parseReplaceString('prefix\\nsuffix')).to.equal('prefix\nsuffix'); + }); +});