From ecf139af799260ea72543d39979f4ae3aff3ded8 Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:57:06 +0700 Subject: [PATCH] Browse enhancements + white-space fix, #100 Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com> --- toolbar/actions.ts | 21 ++++++++++- toolbar/engine.ts | 91 +++++++++++++++++++++++++++++++++++++++------- toolbar/types.ts | 1 + 3 files changed, 97 insertions(+), 16 deletions(-) diff --git a/toolbar/actions.ts b/toolbar/actions.ts index 9f85f93..c35b687 100644 --- a/toolbar/actions.ts +++ b/toolbar/actions.ts @@ -730,7 +730,7 @@ const actions: ToolbarActionDefinition[] = [ keywords: COMMAND_KEYWORDS.browse, getDefaultResult: () => createBaseResult('Browse', 'Specify a remote to browse its files', {}, 37), - getResults: ({ query, paths }) => { + getResults: ({ query, paths, remotes }) => { if (query && !matchesKeyword(query, COMMAND_KEYWORDS.browse)) { return [] } @@ -738,7 +738,24 @@ const actions: ToolbarActionDefinition[] = [ const remotePaths = paths.filter((path) => !path.isLocal) if (remotePaths.length === 0) { - return [createBaseResult('Browse', 'Specify a remote to browse its files', {}, 37)] + if (remotes.length === 0) { + return [ + createBaseResult('Browse', 'Specify a remote to browse its files', {}, 37), + ] + } + + const results: ToolbarActionResult[] = [] + for (const remote of remotes) { + results.push( + createBaseResult( + `Browse ${remote}`, + COMMAND_DESCRIPTIONS.browse, + { remote }, + 140 + ) + ) + } + return results } const results: ToolbarActionResult[] = [] diff --git a/toolbar/engine.ts b/toolbar/engine.ts index 96309aa..e0a4623 100644 --- a/toolbar/engine.ts +++ b/toolbar/engine.ts @@ -37,6 +37,7 @@ export function runToolbarEngine( query: cleanedQuery, fullQuery: trimmed, paths: parsedPaths, + remotes, }) : buildDefaultResults(actions, remotes) @@ -107,42 +108,57 @@ function serializeResult(actionId: ToolbarCommandId, args: ToolbarActionArgs) { return `${actionId}:${JSON.stringify(args)}` } -const WHITESPACE_SPLIT = /\s+/ const TOKEN_TRIM_REGEX = /^[\"'`]+|[\"'`.,;!?]+$/g const REMOTE_PATH_REGEX = /^([^:\s]+):(.*)$/ const WINDOWS_DRIVE_REGEX = /^[a-zA-Z]:[\\/]/ const ALPHA_REGEX = /[a-zA-Z]/ const WINDOWS_PREFIX_REGEX = /^([a-zA-Z]:)(.*)$/ +const QUOTED_STRING_REGEX = /^(["'`])(.+?)\1/ +const WHITESPACE_OR_COLON_REGEX = /[\s:]/ +const NON_WHITESPACE_TOKEN_REGEX = /^(\S+)/ function extractPaths( input: string, remotes: string[], remoteTypes?: Record ): ToolbarActionPath[] { - const matches = input.split(WHITESPACE_SPLIT).filter(Boolean) const seen = new Set() const results: ToolbarActionPath[] = [] const separator = sep() + const remoteLowerToOriginal = new Map() + for (const remote of remotes) { + remoteLowerToOriginal.set(remote.toLowerCase(), remote) + } + console.log('remotes', remotes) - for (const raw of matches) { + const tokens = tokenizeInput(input, remotes) + + for (const raw of tokens) { const cleaned = stripToken(raw) if (!cleaned) continue let remoteName: string | undefined let remoteType: string | undefined let isLocal: boolean = false - // eagerly match remotes - if (remotes.includes(cleaned)) { - remoteName = cleaned - remoteType = remoteTypes?.[cleaned] + const matchedRemote = remoteLowerToOriginal.get(cleaned.toLowerCase()) + + // eagerly match remotes (case-insensitive) + if (matchedRemote) { + remoteName = matchedRemote + remoteType = remoteTypes?.[matchedRemote] } else if (isRemotePath(cleaned)) { // remote path match (e.g., "rct:/path/to/file") const match = REMOTE_PATH_REGEX.exec(cleaned) - if (match && remotes.includes(match[1])) { - remoteName = match[1] - remoteType = remoteTypes?.[match[1]] + if (match) { + const matchedPathRemote = remoteLowerToOriginal.get(match[1].toLowerCase()) + if (matchedPathRemote) { + remoteName = matchedPathRemote + remoteType = remoteTypes?.[matchedPathRemote] + } else { + continue + } } else { continue } @@ -151,11 +167,14 @@ function extractPaths( } else { continue } - if (!seen.has(cleaned)) { - seen.add(cleaned) + + // Use the original remote name for the full path if matched + const fullPath = remoteName && !isRemotePath(cleaned) ? remoteName : cleaned + if (!seen.has(fullPath.toLowerCase())) { + seen.add(fullPath.toLowerCase()) results.push({ - full: cleaned, - readable: createReadablePath(cleaned, isLocal, separator), + full: fullPath, + readable: createReadablePath(fullPath, isLocal, separator), isLocal, remoteName, remoteType, @@ -257,6 +276,50 @@ function stripToken(token: string): string { return token.replace(TOKEN_TRIM_REGEX, '') } +function tokenizeInput(input: string, remotes: string[]): string[] { + const tokens: string[] = [] + let remaining = input.trim() + + const sortedRemotes = [...remotes].sort((a, b) => b.length - a.length) + const remotesWithSpaces = sortedRemotes.filter((r) => r.includes(' ')) + + while (remaining.length > 0) { + remaining = remaining.trimStart() + if (!remaining) break + + //quoted strings first + const quoteMatch = QUOTED_STRING_REGEX.exec(remaining) + if (quoteMatch) { + tokens.push(quoteMatch[2]) + remaining = remaining.slice(quoteMatch[0].length) + continue + } + + let matchedRemoteWithSpace = false + for (const remote of remotesWithSpaces) { + if (remaining.toLowerCase().startsWith(remote.toLowerCase())) { + const nextChar = remaining[remote.length] + if (!nextChar || WHITESPACE_OR_COLON_REGEX.test(nextChar)) { + tokens.push(remote) + remaining = remaining.slice(remote.length) + matchedRemoteWithSpace = true + break + } + } + } + if (matchedRemoteWithSpace) continue + + // fallback + const wsMatch = NON_WHITESPACE_TOKEN_REGEX.exec(remaining) + if (wsMatch) { + tokens.push(wsMatch[1]) + remaining = remaining.slice(wsMatch[0].length) + } + } + + return tokens +} + function isRemotePath(token: string): boolean { if (!token.includes(':')) return false if (token.includes('://')) return false diff --git a/toolbar/types.ts b/toolbar/types.ts index 8c78367..60d39ef 100644 --- a/toolbar/types.ts +++ b/toolbar/types.ts @@ -46,6 +46,7 @@ export interface ToolbarActionContext { query: string fullQuery: string paths: ToolbarActionPath[] + remotes: string[] } export interface ToolbarActionDefaultContext {