Skip to content

Commit 6592e22

Browse files
committed
Support Windows paths and directory separators in path tab completion
1 parent de0954c commit 6592e22

2 files changed

Lines changed: 65 additions & 31 deletions

File tree

cli/src/hooks/__tests__/use-path-tab-completion.test.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
1313
* - Path transformation for display
1414
*/
1515

16+
import {
17+
isAbsolutePath,
18+
isCompleteDirectory,
19+
toRelativePath,
20+
} from '../use-path-tab-completion'
21+
1622
// Helper to expand ~ to home directory (same as in the hook)
1723
const expandPath = (inputPath: string): string => {
1824
if (inputPath.startsWith('~')) {
@@ -21,27 +27,6 @@ const expandPath = (inputPath: string): string => {
2127
return inputPath
2228
}
2329

24-
// Helper to check if a path is absolute-style (starts with / or ~)
25-
const isAbsolutePath = (searchQuery: string): boolean => {
26-
return searchQuery.startsWith('/') || searchQuery.startsWith('~')
27-
}
28-
29-
// Helper to check if completion result indicates a full directory
30-
const isCompleteDirectory = (completed: string): boolean => {
31-
return completed.endsWith('/')
32-
}
33-
34-
// Helper to convert absolute completion back to relative for display
35-
const toRelativePath = (
36-
completed: string,
37-
currentPath: string,
38-
): string | null => {
39-
if (completed.startsWith(currentPath + path.sep)) {
40-
return completed.slice(currentPath.length + 1)
41-
}
42-
return null
43-
}
44-
4530
describe('usePathTabCompletion - path type detection', () => {
4631
describe('isAbsolutePath', () => {
4732
test('returns true for paths starting with /', () => {
@@ -56,6 +41,13 @@ describe('usePathTabCompletion - path type detection', () => {
5641
expect(isAbsolutePath('~/')).toBe(true)
5742
})
5843

44+
test('returns true for Windows drive paths', () => {
45+
expect(isAbsolutePath('C:\\Users')).toBe(true)
46+
expect(isAbsolutePath('D:\\projects')).toBe(true)
47+
expect(isAbsolutePath('C:/Users/Documents')).toBe(true)
48+
expect(isAbsolutePath('C:\\')).toBe(true)
49+
})
50+
5951
test('returns false for relative paths', () => {
6052
expect(isAbsolutePath('Documents')).toBe(false)
6153
expect(isAbsolutePath('src/components')).toBe(false)
@@ -82,9 +74,16 @@ describe('usePathTabCompletion - completion result detection', () => {
8274
expect(isCompleteDirectory('relative/path/')).toBe(true)
8375
})
8476

85-
test('returns false for paths not ending with /', () => {
77+
test('returns true for Windows paths ending with backslash', () => {
78+
expect(isCompleteDirectory('C:\\Users\\')).toBe(true)
79+
expect(isCompleteDirectory('relative\\path\\')).toBe(true)
80+
expect(isCompleteDirectory('\\')).toBe(true)
81+
})
82+
83+
test('returns false for paths not ending with / or \\', () => {
8684
expect(isCompleteDirectory('/usr/local')).toBe(false)
8785
expect(isCompleteDirectory('~/Documents')).toBe(false)
86+
expect(isCompleteDirectory('C:\\Users')).toBe(false)
8887
expect(isCompleteDirectory('partial')).toBe(false)
8988
})
9089

cli/src/hooks/use-path-tab-completion.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,44 @@ export interface UsePathTabCompletionReturn {
2323
handleTabCompletion: () => boolean
2424
}
2525

26+
/**
27+
* Check if a path query represents an absolute path.
28+
* Supports POSIX paths (/...), tilde paths (~...), and Windows drive/UNC paths.
29+
*/
30+
export function isAbsolutePath(searchQuery: string): boolean {
31+
return (
32+
searchQuery.startsWith('/') ||
33+
searchQuery.startsWith('~') ||
34+
path.isAbsolute(searchQuery) ||
35+
path.win32.isAbsolute(searchQuery)
36+
)
37+
}
38+
39+
/**
40+
* Check if a completed path represents a full directory.
41+
* Matches trailing slash (/) or Windows backslash (\).
42+
*/
43+
export function isCompleteDirectory(completed: string): boolean {
44+
return completed.endsWith('/') || completed.endsWith('\\')
45+
}
46+
47+
/**
48+
* Convert absolute completion back to relative path for display under current directory.
49+
*/
50+
export function toRelativePath(
51+
completed: string,
52+
currentPath: string,
53+
): string | null {
54+
if (completed.startsWith(currentPath + path.sep)) {
55+
return completed.slice(currentPath.length + 1)
56+
}
57+
return null
58+
}
59+
2660
/**
2761
* Hook for path tab completion.
28-
* Handles both absolute (/, ~) and relative path completion.
29-
* Always navigates to completed directories when completion ends with /.
62+
* Handles both absolute (/, ~, drive letters) and relative path completion.
63+
* Always navigates to completed directories when completion ends with a directory separator.
3064
*/
3165
export function usePathTabCompletion({
3266
searchQuery,
@@ -36,12 +70,12 @@ export function usePathTabCompletion({
3670
expandPath,
3771
}: UsePathTabCompletionOptions): UsePathTabCompletionReturn {
3872
const handleTabCompletion = useCallback((): boolean => {
39-
if (searchQuery.startsWith('/') || searchQuery.startsWith('~')) {
73+
if (isAbsolutePath(searchQuery)) {
4074
// Absolute path completion
4175
const completed = getPathCompletion(searchQuery)
4276
if (completed) {
43-
// If completion is a full directory (ends with /), navigate there and keep the path in input
44-
if (completed.endsWith('/')) {
77+
// If completion is a full directory (ends with / or \), navigate there and keep the path in input
78+
if (isCompleteDirectory(completed)) {
4579
const dirPath = expandPath(completed.slice(0, -1))
4680
try {
4781
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
@@ -60,8 +94,8 @@ export function usePathTabCompletion({
6094
const relativePath = path.join(currentPath, searchQuery)
6195
const completed = getPathCompletion(relativePath)
6296
if (completed) {
63-
// If completion is a full directory (ends with /), navigate there and keep the path in input
64-
if (completed.endsWith('/')) {
97+
// If completion is a full directory (ends with / or \), navigate there and keep the path in input
98+
if (isCompleteDirectory(completed)) {
6599
try {
66100
const dirPath = completed.slice(0, -1)
67101
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
@@ -74,8 +108,9 @@ export function usePathTabCompletion({
74108
}
75109
}
76110
// Convert back to relative path for display
77-
if (completed.startsWith(currentPath + path.sep)) {
78-
setSearchQuery(completed.slice(currentPath.length + 1))
111+
const rel = toRelativePath(completed, currentPath)
112+
if (rel !== null) {
113+
setSearchQuery(rel)
79114
} else {
80115
setSearchQuery(completed)
81116
}

0 commit comments

Comments
 (0)