mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -250,6 +250,62 @@ describe('remapOpenEditorTabsForPathChange', () => {
|
||||
expect(useAppStore.getState().editorDrafts[moved.id]).toBe('dirty windows work')
|
||||
})
|
||||
|
||||
it('rebuilds the moved path across WSL UNC aliases without fabricating segments', () => {
|
||||
// The shared matcher equates \\wsl$\Ubuntu and \\wsl.localhost\ubuntu (same
|
||||
// filesystem), but they differ in raw length — a raw slice(fromPath.length)
|
||||
// would corrupt the destination. Segment-count reconstruction must not.
|
||||
const state = useAppStore.getState()
|
||||
state.openFile(
|
||||
{
|
||||
filePath: '\\\\wsl.localhost\\ubuntu\\repo\\src\\a.ts',
|
||||
relativePath: 'src\\a.ts',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ suppressActiveRuntimeFallback: true }
|
||||
)
|
||||
const id = useAppStore.getState().openFiles[0]!.id
|
||||
state.setEditorDraft(id, 'wsl draft')
|
||||
state.markFileDirty(id, true)
|
||||
|
||||
const result = remapOpenEditorTabsForPathChange({
|
||||
fromPath: '\\\\wsl$\\Ubuntu\\repo\\src',
|
||||
toPath: '\\\\wsl$\\Ubuntu\\repo\\dst',
|
||||
worktreePath: '\\\\wsl$\\Ubuntu\\repo',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
const moved = useAppStore.getState().openFiles[0]!
|
||||
expect(moved.filePath).toBe('\\\\wsl$\\Ubuntu\\repo\\dst\\a.ts')
|
||||
expect(useAppStore.getState().editorDrafts[moved.id]).toBe('wsl draft')
|
||||
})
|
||||
|
||||
it('rebuilds the moved path when the source root has duplicate separators', () => {
|
||||
const state = useAppStore.getState()
|
||||
state.openFile(
|
||||
{
|
||||
filePath: 'C:\\Repo\\Src\\deep\\a.ts',
|
||||
relativePath: 'Src\\deep\\a.ts',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ suppressActiveRuntimeFallback: true }
|
||||
)
|
||||
|
||||
remapOpenEditorTabsForPathChange({
|
||||
fromPath: 'C:\\Repo\\\\Src',
|
||||
toPath: 'C:\\Repo\\Dst',
|
||||
worktreePath: 'C:\\Repo',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
// Nested suffix preserved, no dropped or doubled separator.
|
||||
expect(useAppStore.getState().openFiles[0]!.filePath).toBe('C:\\Repo\\Dst\\deep\\a.ts')
|
||||
})
|
||||
|
||||
it('clears the untitled marker when remapping a renamed new markdown file', () => {
|
||||
const state = useAppStore.getState()
|
||||
const oldPath = '/repo/untitled.md'
|
||||
|
||||
@@ -63,10 +63,14 @@ function splitAbsolutePath(path: string): { prefix: string; segments: string[] }
|
||||
}
|
||||
if (normalized.startsWith('//')) {
|
||||
const segments = normalized.slice(2).split('/').filter(Boolean)
|
||||
return {
|
||||
prefix: `//${segments.slice(0, 2).join('/').toLowerCase()}`,
|
||||
segments: segments.slice(2)
|
||||
}
|
||||
const server = (segments[0] ?? '').toLowerCase()
|
||||
// Canonicalize the two WSL UNC aliases to one identity (matches the shared
|
||||
// normalizer) so `\\wsl$\Distro` and `\\wsl.localhost\Distro` compare equal.
|
||||
const prefix =
|
||||
server === 'wsl.localhost' || server === 'wsl$'
|
||||
? `//wsl/${(segments[1] ?? '').toLowerCase()}`
|
||||
: `//${segments.slice(0, 2).join('/').toLowerCase()}`
|
||||
return { prefix, segments: segments.slice(2) }
|
||||
}
|
||||
if (normalized.startsWith('/')) {
|
||||
return { prefix: '/', segments: normalized.slice(1).split('/').filter(Boolean) }
|
||||
@@ -74,6 +78,28 @@ function splitAbsolutePath(path: string): { prefix: string; segments: string[] }
|
||||
return { prefix: '', segments: normalized.split('/').filter(Boolean) }
|
||||
}
|
||||
|
||||
// Rebuild a moved descendant's path WITHOUT raw-length slicing. The shared
|
||||
// containment check matches paths equal under normalization but differing in raw
|
||||
// length (wsl$ vs wsl.localhost aliases, duplicate/trailing separators), so
|
||||
// `slice(fromPath.length)` would fabricate a path. Derive the suffix by segment
|
||||
// count from the normalized relative path, then re-emit the ORIGINAL-case tail of
|
||||
// filePath onto toPath in toPath's separator style.
|
||||
function computeMovedPath(fromPath: string, toPath: string, filePath: string): string {
|
||||
const suffix = relativePathInsideRoot(fromPath, filePath)
|
||||
if (suffix === null) {
|
||||
// Selection already matched, so this is unreachable; degrade to the raw splice.
|
||||
return toPath + filePath.slice(fromPath.length)
|
||||
}
|
||||
if (suffix === '') {
|
||||
return toPath
|
||||
}
|
||||
const segmentCount = suffix.split('/').filter(Boolean).length
|
||||
const rawSegments = filePath.split(/[\\/]+/).filter(Boolean)
|
||||
const tail = rawSegments.slice(rawSegments.length - segmentCount)
|
||||
const separator = toPath.includes('\\') ? '\\' : '/'
|
||||
return `${toPath.replace(/[\\/]+$/, '')}${separator}${tail.join(separator)}`
|
||||
}
|
||||
|
||||
function getRelativePathFromRoot(rootPath: string, candidatePath: string): string {
|
||||
const insideRoot = relativePathInsideRoot(rootPath, candidatePath)
|
||||
if (insideRoot !== null) {
|
||||
@@ -88,10 +114,10 @@ function getRelativePathFromRoot(rootPath: string, candidatePath: string): strin
|
||||
|
||||
// Windows drive/UNC segments are case-insensitive, so a case-only difference
|
||||
// is the SAME directory and must not emit a spurious `..`. The WSL Linux suffix
|
||||
// (prefix //wsl…) stays case-sensitive; POSIX roots stay case-sensitive.
|
||||
// (canonical prefix //wsl/<distro>) stays case-sensitive, as do POSIX roots.
|
||||
const foldCase =
|
||||
/^[a-z]:$/i.test(root.prefix) ||
|
||||
(root.prefix.startsWith('//') && !root.prefix.startsWith('//wsl'))
|
||||
(root.prefix.startsWith('//') && !root.prefix.startsWith('//wsl/'))
|
||||
const sameSegment = (a: string, b: string): boolean =>
|
||||
foldCase ? a.toLowerCase() === b.toLowerCase() : a === b
|
||||
|
||||
@@ -173,7 +199,7 @@ export function remapOpenEditorTabsForPathChange({
|
||||
// close+reopen: preserves the full OpenFile + all id-keyed state and closes
|
||||
// the watcher-race window that close/reopen opened.
|
||||
const updatedPathOf = (file: { filePath: string }): string =>
|
||||
toPath + file.filePath.slice(fromPath.length)
|
||||
computeMovedPath(fromPath, toPath, file.filePath)
|
||||
const relativeOf = (file: {
|
||||
filePath: string
|
||||
relativePath: string
|
||||
|
||||
Reference in New Issue
Block a user