mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
fix: compare file explorer paths case-insensitively on Windows (#3911)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
__clearSelfWriteRegistryForTests,
|
||||
clearSelfWrite,
|
||||
hasRecentSelfWrite,
|
||||
recordSelfWrite
|
||||
} from './editor-self-write-registry'
|
||||
|
||||
describe('editor self-write registry', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
__clearSelfWriteRegistryForTests()
|
||||
})
|
||||
|
||||
it('matches Windows drive paths case-insensitively', () => {
|
||||
recordSelfWrite('C:\\Repo\\a.md')
|
||||
|
||||
expect(hasRecentSelfWrite('c:\\repo\\a.md')).toBe(true)
|
||||
|
||||
clearSelfWrite('c:\\repo\\a.md')
|
||||
expect(hasRecentSelfWrite('C:\\Repo\\a.md')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches Windows UNC paths case-insensitively', () => {
|
||||
recordSelfWrite('\\\\Server\\Share\\Repo\\a.md')
|
||||
|
||||
expect(hasRecentSelfWrite('\\\\server\\share\\repo\\a.md')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps POSIX path casing distinct', () => {
|
||||
recordSelfWrite('/Repo/a.md')
|
||||
|
||||
expect(hasRecentSelfWrite('/repo/a.md')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeAbsolutePath } from '@/components/right-sidebar/file-explorer-paths'
|
||||
import { normalizeAbsolutePathForComparison } from '@/components/right-sidebar/file-explorer-paths'
|
||||
|
||||
// Why: the editor's own save path writes to disk, which fans out as an
|
||||
// fs:changed event back to useEditorExternalWatch a few ms later. Treating
|
||||
@@ -24,18 +24,18 @@ type SelfWriteStamp = RecentSelfWrite & {
|
||||
const stamps = new Map<string, SelfWriteStamp>()
|
||||
|
||||
export function recordSelfWrite(absolutePath: string, content?: string): void {
|
||||
stamps.set(normalizeAbsolutePath(absolutePath), {
|
||||
stamps.set(normalizeAbsolutePathForComparison(absolutePath), {
|
||||
content: content ?? null,
|
||||
expiresAt: Date.now() + SELF_WRITE_TTL_MS
|
||||
})
|
||||
}
|
||||
|
||||
export function clearSelfWrite(absolutePath: string): void {
|
||||
stamps.delete(normalizeAbsolutePath(absolutePath))
|
||||
stamps.delete(normalizeAbsolutePathForComparison(absolutePath))
|
||||
}
|
||||
|
||||
export function getRecentSelfWrite(absolutePath: string): RecentSelfWrite | null {
|
||||
const key = normalizeAbsolutePath(absolutePath)
|
||||
const key = normalizeAbsolutePathForComparison(absolutePath)
|
||||
const stamp = stamps.get(key)
|
||||
if (!stamp) {
|
||||
return null
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getRevealAncestorDirs,
|
||||
isPathEqualOrDescendant,
|
||||
normalizeAbsolutePath
|
||||
} from './file-explorer-paths'
|
||||
|
||||
describe('file explorer path helpers', () => {
|
||||
it('preserves UNC roots while normalizing separators', () => {
|
||||
expect(normalizeAbsolutePath('\\\\Server\\Share\\Repo\\')).toBe('//Server/Share/Repo')
|
||||
})
|
||||
|
||||
it('matches Windows drive paths case-insensitively with segment boundaries', () => {
|
||||
expect(isPathEqualOrDescendant('c:\\repo\\src\\a.ts', 'C:\\Repo')).toBe(true)
|
||||
expect(isPathEqualOrDescendant('C:\\Repository\\src\\a.ts', 'C:\\Repo')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches Windows UNC paths case-insensitively with segment boundaries', () => {
|
||||
expect(isPathEqualOrDescendant('\\\\server\\share\\repo\\src', '\\\\Server\\Share\\Repo')).toBe(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
isPathEqualOrDescendant('\\\\server\\share\\repository\\src', '\\\\Server\\Share\\Repo')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps POSIX path comparisons case-sensitive', () => {
|
||||
expect(isPathEqualOrDescendant('/Repo/src/a.ts', '/repo')).toBe(false)
|
||||
})
|
||||
|
||||
it('builds reveal ancestors from the worktree casing and target relative casing', () => {
|
||||
expect(getRevealAncestorDirs('C:\\Repo', 'c:\\repo\\Src\\Nested\\File.ts')).toEqual([
|
||||
'C:\\Repo\\Src',
|
||||
'C:\\Repo\\Src\\Nested'
|
||||
])
|
||||
expect(getRevealAncestorDirs('/repo', '/Repo/Src/File.ts')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,14 @@
|
||||
import { joinPath, normalizeRelativePath } from '@/lib/path'
|
||||
import {
|
||||
isPathInsideOrEqual,
|
||||
normalizeRuntimePathForComparison,
|
||||
normalizeRuntimePathSeparators,
|
||||
relativePathInsideRoot
|
||||
} from '../../../../shared/cross-platform-path'
|
||||
import { splitPathSegments } from './path-tree'
|
||||
|
||||
export function normalizeAbsolutePath(path: string): string {
|
||||
const normalizedPath = path.replace(/[\\/]+/g, '/')
|
||||
const normalizedPath = normalizeRuntimePathSeparators(path)
|
||||
|
||||
if (normalizedPath === '/') {
|
||||
return normalizedPath
|
||||
@@ -15,28 +21,21 @@ export function normalizeAbsolutePath(path: string): string {
|
||||
return normalizedPath.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
export function normalizeAbsolutePathForComparison(path: string): string {
|
||||
return normalizeRuntimePathForComparison(path)
|
||||
}
|
||||
|
||||
export function isPathEqualOrDescendant(candidatePath: string, targetPath: string): boolean {
|
||||
const normalizedCandidate = normalizeAbsolutePath(candidatePath)
|
||||
const normalizedTarget = normalizeAbsolutePath(targetPath)
|
||||
return (
|
||||
normalizedCandidate === normalizedTarget ||
|
||||
normalizedCandidate.startsWith(`${normalizedTarget}/`)
|
||||
)
|
||||
return isPathInsideOrEqual(targetPath, candidatePath)
|
||||
}
|
||||
|
||||
export function getRevealAncestorDirs(worktreePath: string, filePath: string): string[] | null {
|
||||
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath)
|
||||
const normalizedTargetPath = normalizeAbsolutePath(filePath)
|
||||
const prefix = `${normalizedWorktreePath}/`
|
||||
|
||||
if (normalizedTargetPath !== normalizedWorktreePath && !normalizedTargetPath.startsWith(prefix)) {
|
||||
const relativePath = relativePathInsideRoot(worktreePath, filePath)
|
||||
if (relativePath === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const relativePath = normalizeRelativePath(
|
||||
normalizedTargetPath === normalizedWorktreePath ? '' : normalizedTargetPath.slice(prefix.length)
|
||||
)
|
||||
const segments = splitPathSegments(relativePath)
|
||||
const segments = splitPathSegments(normalizeRelativePath(relativePath))
|
||||
const ancestorDirs: string[] = []
|
||||
let currentPath = worktreePath
|
||||
|
||||
|
||||
Reference in New Issue
Block a user