@@ -3,6 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
33import { FileIcon } from "@opencode-ai/ui/file-icon"
44import { List } from "@opencode-ai/ui/list"
55import { getDirectory , getFilename } from "@opencode-ai/util/path"
6+ import fuzzysort from "fuzzysort"
67import { createMemo } from "solid-js"
78import { useGlobalSDK } from "@/context/global-sdk"
89import { useGlobalSync } from "@/context/global-sync"
@@ -21,63 +22,114 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
2122 const language = useLanguage ( )
2223
2324 const home = createMemo ( ( ) => sync . data . path . home )
24- const root = createMemo ( ( ) => sync . data . path . home || sync . data . path . directory )
25+
26+ const start = createMemo ( ( ) => sync . data . path . home || sync . data . path . directory )
27+
28+ function normalize ( input : string ) {
29+ const v = input . replaceAll ( "\\" , "/" )
30+ if ( v . startsWith ( "//" ) && ! v . startsWith ( "///" ) ) return "//" + v . slice ( 2 ) . replace ( / \/ + / g, "/" )
31+ return v . replace ( / \/ + / g, "/" )
32+ }
33+
34+ function normalizeDriveRoot ( input : string ) {
35+ const v = normalize ( input )
36+ if ( / ^ [ A - Z a - z ] : $ / . test ( v ) ) return v + "/"
37+ return v
38+ }
39+
40+ function trimTrailing ( input : string ) {
41+ const v = normalizeDriveRoot ( input )
42+ if ( v === "/" ) return v
43+ if ( / ^ [ A - Z a - z ] : \/ $ / . test ( v ) ) return v
44+ return v . replace ( / \/ + $ / , "" )
45+ }
2546
2647 function join ( base : string | undefined , rel : string ) {
27- const b = ( base ?? "" ) . replace ( / [ \\ / ] + $ / , "" )
28- const r = rel . replace ( / ^ [ \\ / ] + / , "" ) . replace ( / [ \\ / ] + $ / , "" )
48+ const b = trimTrailing ( base ?? "" )
49+ const r = trimTrailing ( rel ) . replace ( / ^ \/ + / , "" )
2950 if ( ! b ) return r
3051 if ( ! r ) return b
52+ if ( b . endsWith ( "/" ) ) return b + r
3153 return b + "/" + r
3254 }
3355
34- function display ( rel : string ) {
35- const full = join ( root ( ) , rel )
56+ function rootOf ( input : string ) {
57+ const v = normalizeDriveRoot ( input )
58+ if ( v . startsWith ( "//" ) ) return "//"
59+ if ( v . startsWith ( "/" ) ) return "/"
60+ if ( / ^ [ A - Z a - z ] : \/ / . test ( v ) ) return v . slice ( 0 , 3 )
61+ return ""
62+ }
63+
64+ function isRoot ( input : string ) {
65+ const v = trimTrailing ( input )
66+ if ( v === "/" ) return true
67+ return / ^ [ A - Z a - z ] : \/ $ / . test ( v )
68+ }
69+
70+ function display ( path : string ) {
71+ const full = trimTrailing ( path )
3672 const h = home ( )
3773 if ( ! h ) return full
38- if ( full === h ) return "~"
39- if ( full . startsWith ( h + "/" ) || full . startsWith ( h + "\\" ) ) {
40- return "~" + full . slice ( h . length )
41- }
74+
75+ const hn = trimTrailing ( h )
76+ const lc = full . toLowerCase ( )
77+ const hc = hn . toLowerCase ( )
78+ if ( lc === hc ) return "~"
79+ if ( lc . startsWith ( hc + "/" ) ) return "~" + full . slice ( hn . length )
4280 return full
4381 }
4482
45- function normalizeQuery ( query : string ) {
46- const h = home ( )
83+ function parse ( filter : string ) {
84+ const base = start ( )
85+ if ( ! base ) return
4786
48- if ( ! query ) return query
49- if ( query . startsWith ( "~/" ) ) return query . slice ( 2 )
87+ const raw = normalizeDriveRoot ( filter . trim ( ) )
88+ if ( ! raw ) return { directory : trimTrailing ( base ) , query : "" }
5089
51- if ( h ) {
52- const lc = query . toLowerCase ( )
53- const hc = h . toLowerCase ( )
54- if ( lc === hc || lc . startsWith ( hc + "/" ) || lc . startsWith ( hc + "\\" ) ) {
55- return query . slice ( h . length ) . replace ( / ^ [ \\ / ] + / , "" )
56- }
57- }
90+ const h = home ( )
91+ const expanded = raw === "~" ? h : raw . startsWith ( "~/" ) ? ( h ? join ( h , raw . slice ( 2 ) ) : raw . slice ( 2 ) ) : raw
92+ const absolute = rootOf ( expanded ) ? expanded : join ( base , expanded )
93+ const abs = normalizeDriveRoot ( absolute )
94+
95+ if ( abs . endsWith ( "/" ) ) return { directory : trimTrailing ( abs ) , query : "" }
96+ const i = abs . lastIndexOf ( "/" )
97+ if ( i === - 1 ) return { directory : trimTrailing ( base ) , query : abs }
5898
59- return query
99+ const dir = i === 0 ? "/" : / ^ [ A - Z a - z ] : $ / . test ( abs . slice ( 0 , i ) ) ? abs . slice ( 0 , i ) + "/" : abs . slice ( 0 , i )
100+ return { directory : trimTrailing ( dir ) , query : abs . slice ( i + 1 ) }
60101 }
61102
62- async function fetchDirs ( query : string ) {
63- const directory = root ( )
64- if ( ! directory ) return [ ] as string [ ]
103+ async function fetchDirs ( input : { directory : string ; query : string } ) {
104+ if ( isRoot ( input . directory ) ) {
105+ const nodes = await sdk . client . file
106+ . list ( { directory : input . directory , path : "" } )
107+ . then ( ( x ) => x . data ?? [ ] )
108+ . catch ( ( ) => [ ] )
109+
110+ const dirs = nodes . filter ( ( n ) => n . type === "directory" ) . map ( ( n ) => n . name )
111+ const sorted = input . query
112+ ? fuzzysort . go ( input . query , dirs , { limit : 50 } ) . map ( ( x ) => x . target )
113+ : dirs . slice ( ) . sort ( ( a , b ) => a . localeCompare ( b ) )
114+
115+ return sorted . slice ( 0 , 50 ) . map ( ( name ) => join ( input . directory , name ) )
116+ }
65117
66118 const results = await sdk . client . find
67- . files ( { directory, query, type : "directory" , limit : 50 } )
119+ . files ( { directory : input . directory , query : input . query , type : "directory" , limit : 50 } )
68120 . then ( ( x ) => x . data ?? [ ] )
69121 . catch ( ( ) => [ ] )
70122
71- return results . map ( ( x ) => x . replace ( / [ \\ / ] + $ / , "" ) )
123+ return results . map ( ( rel ) => join ( input . directory , rel ) )
72124 }
73125
74126 const directories = async ( filter : string ) => {
75- const query = normalizeQuery ( filter . trim ( ) )
76- return fetchDirs ( query )
127+ const input = parse ( filter )
128+ if ( ! input ) return [ ] as string [ ]
129+ return fetchDirs ( input )
77130 }
78131
79- function resolve ( rel : string ) {
80- const absolute = join ( root ( ) , rel )
132+ function resolve ( absolute : string ) {
81133 props . onSelect ( props . multiple ? [ absolute ] : absolute )
82134 dialog . close ( )
83135 }
@@ -95,12 +147,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
95147 resolve ( path )
96148 } }
97149 >
98- { ( rel ) => {
99- const path = display ( rel )
150+ { ( absolute ) => {
151+ const path = display ( absolute )
100152 return (
101153 < div class = "w-full flex items-center justify-between rounded-md" >
102154 < div class = "flex items-center gap-x-3 grow min-w-0" >
103- < FileIcon node = { { path : rel , type : "directory" } } class = "shrink-0 size-4" />
155+ < FileIcon node = { { path : absolute , type : "directory" } } class = "shrink-0 size-4" />
104156 < div class = "flex items-center text-14-regular min-w-0" >
105157 < span class = "text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0" >
106158 { getDirectory ( path ) }
0 commit comments