fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-21 00:45:23 -07:00
co-authored by Orca
parent e10cfc33ac
commit bb94d4acea
2 changed files with 33 additions and 5 deletions
@@ -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(
@@ -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 {