Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
const processedText = this.searchOptions?.useRegExp
? parseReplaceString(replacementText)
: replacementText;
return super.replaceResult(node, replaceOne, processedText);
}
}
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading