Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions cli/src/hooks/__tests__/use-path-tab-completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
* - Path transformation for display
*/

import {
isAbsolutePath,
isCompleteDirectory,
toRelativePath,
} from '../use-path-tab-completion'

// Helper to expand ~ to home directory (same as in the hook)
const expandPath = (inputPath: string): string => {
if (inputPath.startsWith('~')) {
Expand All @@ -21,27 +27,6 @@ const expandPath = (inputPath: string): string => {
return inputPath
}

// Helper to check if a path is absolute-style (starts with / or ~)
const isAbsolutePath = (searchQuery: string): boolean => {
return searchQuery.startsWith('/') || searchQuery.startsWith('~')
}

// Helper to check if completion result indicates a full directory
const isCompleteDirectory = (completed: string): boolean => {
return completed.endsWith('/')
}

// Helper to convert absolute completion back to relative for display
const toRelativePath = (
completed: string,
currentPath: string,
): string | null => {
if (completed.startsWith(currentPath + path.sep)) {
return completed.slice(currentPath.length + 1)
}
return null
}

describe('usePathTabCompletion - path type detection', () => {
describe('isAbsolutePath', () => {
test('returns true for paths starting with /', () => {
Expand All @@ -56,6 +41,13 @@ describe('usePathTabCompletion - path type detection', () => {
expect(isAbsolutePath('~/')).toBe(true)
})

test('returns true for Windows drive paths', () => {
expect(isAbsolutePath('C:\\Users')).toBe(true)
expect(isAbsolutePath('D:\\projects')).toBe(true)
expect(isAbsolutePath('C:/Users/Documents')).toBe(true)
expect(isAbsolutePath('C:\\')).toBe(true)
})

test('returns false for relative paths', () => {
expect(isAbsolutePath('Documents')).toBe(false)
expect(isAbsolutePath('src/components')).toBe(false)
Expand All @@ -82,9 +74,16 @@ describe('usePathTabCompletion - completion result detection', () => {
expect(isCompleteDirectory('relative/path/')).toBe(true)
})

test('returns false for paths not ending with /', () => {
test('returns true for Windows paths ending with backslash', () => {
expect(isCompleteDirectory('C:\\Users\\')).toBe(true)
expect(isCompleteDirectory('relative\\path\\')).toBe(true)
expect(isCompleteDirectory('\\')).toBe(true)
})

test('returns false for paths not ending with / or \\', () => {
expect(isCompleteDirectory('/usr/local')).toBe(false)
expect(isCompleteDirectory('~/Documents')).toBe(false)
expect(isCompleteDirectory('C:\\Users')).toBe(false)
expect(isCompleteDirectory('partial')).toBe(false)
})

Expand Down
53 changes: 44 additions & 9 deletions cli/src/hooks/use-path-tab-completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,44 @@ export interface UsePathTabCompletionReturn {
handleTabCompletion: () => boolean
}

/**
* Check if a path query represents an absolute path.
* Supports POSIX paths (/...), tilde paths (~...), and Windows drive/UNC paths.
*/
export function isAbsolutePath(searchQuery: string): boolean {
return (
searchQuery.startsWith('/') ||
searchQuery.startsWith('~') ||
path.isAbsolute(searchQuery) ||
path.win32.isAbsolute(searchQuery)
)
}

/**
* Check if a completed path represents a full directory.
* Matches trailing slash (/) or Windows backslash (\).
*/
export function isCompleteDirectory(completed: string): boolean {
return completed.endsWith('/') || completed.endsWith('\\')
}

/**
* Convert absolute completion back to relative path for display under current directory.
*/
export function toRelativePath(
completed: string,
currentPath: string,
): string | null {
if (completed.startsWith(currentPath + path.sep)) {
return completed.slice(currentPath.length + 1)
}
return null
}

/**
* Hook for path tab completion.
* Handles both absolute (/, ~) and relative path completion.
* Always navigates to completed directories when completion ends with /.
* Handles both absolute (/, ~, drive letters) and relative path completion.
* Always navigates to completed directories when completion ends with a directory separator.
*/
export function usePathTabCompletion({
searchQuery,
Expand All @@ -36,12 +70,12 @@ export function usePathTabCompletion({
expandPath,
}: UsePathTabCompletionOptions): UsePathTabCompletionReturn {
const handleTabCompletion = useCallback((): boolean => {
if (searchQuery.startsWith('/') || searchQuery.startsWith('~')) {
if (isAbsolutePath(searchQuery)) {
// Absolute path completion
const completed = getPathCompletion(searchQuery)
if (completed) {
// If completion is a full directory (ends with /), navigate there and keep the path in input
if (completed.endsWith('/')) {
// If completion is a full directory (ends with / or \), navigate there and keep the path in input
if (isCompleteDirectory(completed)) {
const dirPath = expandPath(completed.slice(0, -1))
try {
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
Expand All @@ -60,8 +94,8 @@ export function usePathTabCompletion({
const relativePath = path.join(currentPath, searchQuery)
const completed = getPathCompletion(relativePath)
if (completed) {
// If completion is a full directory (ends with /), navigate there and keep the path in input
if (completed.endsWith('/')) {
// If completion is a full directory (ends with / or \), navigate there and keep the path in input
if (isCompleteDirectory(completed)) {
try {
const dirPath = completed.slice(0, -1)
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
Expand All @@ -74,8 +108,9 @@ export function usePathTabCompletion({
}
}
// Convert back to relative path for display
if (completed.startsWith(currentPath + path.sep)) {
setSearchQuery(completed.slice(currentPath.length + 1))
const rel = toRelativePath(completed, currentPath)
if (rel !== null) {
setSearchQuery(rel)
} else {
setSearchQuery(completed)
}
Expand Down
Loading