From bb94d4aceae1f0ca689c9dcb0a2ac8ebe1f00d98 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:45:23 -0700 Subject: [PATCH] fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes Co-authored-by: Orca --- ...p-open-editor-tabs-for-path-change.test.ts | 24 +++++++++++++++++++ .../remap-open-editor-tabs-for-path-change.ts | 14 +++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts b/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts index 1a9afd28bc5..48916e9eac2 100644 --- a/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts +++ b/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts @@ -307,6 +307,30 @@ describe('remapOpenEditorTabsForPathChange', () => { expect(useAppStore.getState().openFiles[0]!.filePath).toBe('/repo/dst/a\\b.txt') }) + it('does not strip a trailing backslash from a POSIX destination directory name', () => { + const state = useAppStore.getState() + state.openFile( + { + filePath: '/repo/src/a.ts', + relativePath: 'src/a.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit' + }, + { suppressActiveRuntimeFallback: true } + ) + + // Destination directory is literally named `dst\` (legal on POSIX). + remapOpenEditorTabsForPathChange({ + fromPath: '/repo/src', + toPath: '/repo/dst\\', + worktreePath: '/repo', + worktreeId: 'wt-1' + }) + + expect(useAppStore.getState().openFiles[0]!.filePath).toBe('/repo/dst\\/a.ts') + }) + it('keeps `/` for a POSIX destination whose ancestor contains a legal backslash', () => { const state = useAppStore.getState() state.openFile( diff --git a/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts b/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts index 12c1f692565..63f898f1b1a 100644 --- a/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts +++ b/src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts @@ -30,7 +30,11 @@ function stripTrailingSeparators(path: string): string { if (path === '/' || /^[A-Za-z]:[\\/]?$/.test(path)) { return normalizeRuntimePathSeparators(path) } - return normalizeRuntimePathSeparators(path).replace(/\/+$/, '') + // Only fold backslashes to `/` for Windows/UNC paths; on POSIX a backslash is + // legal filename data and must not be treated as a separator. + return isWindowsAbsolutePathLike(path) + ? normalizeRuntimePathSeparators(path).replace(/\/+$/, '') + : path.replace(/\/+$/, '') } function deriveRelativeRootFromOpenFile(filePath: string, relativePath: string): string { @@ -94,14 +98,14 @@ function computeMovedPath(fromPath: string, toPath: string, filePath: string): s if (suffix === '') { return toPath } - const strippedToPath = toPath.replace(/[\\/]+$/, '') // Flavor by SYNTAX (drive/UNC), never by "contains a backslash": a POSIX/SSH - // path may hold a legal backslash, so it must keep `/` and never gain one. + // path may hold a legal backslash, so it must keep `/`, never gain one, and + // never have a trailing backslash stripped as if it were a separator. if (isWindowsAbsolutePathLike(toPath)) { const separator = toPath.includes('\\') ? '\\' : '/' - return `${strippedToPath}${separator}${suffix.split('/').join(separator)}` + return `${toPath.replace(/[\\/]+$/, '')}${separator}${suffix.split('/').join(separator)}` } - return `${strippedToPath}/${suffix}` + return `${toPath.replace(/\/+$/, '')}/${suffix}` } function getRelativePathFromRoot(rootPath: string, candidatePath: string): string {