Files
orca/src/shared/terminal-path-normalization.ts
T
Kelvin Amoaba f719d0fb10 fix(mobile): open terminal urls and file paths with desktop's link detection (#33964)
* fix(mobile): open wrapped terminal urls and paths with desktop's link detection

Desktop's terminal URL and file-link detectors move to src/shared and the phone's
WebView document calls them, replacing its single-row copies. File taps carry every
reading of the tapped text so the phone opens the longest one the host finds, as
desktop does.

* fix(mobile): keep the unc host of osc 8 file links for the host to resolve

* fix(mobile): resolve terminal file-tap readings a few at a time

A tap inside a run of path rows yields hundreds of hard-wrap guesses; firing them all at once overflowed the page bridge's pending-request cap and the real path was never checked.

* refactor(terminal): share the osc 8 link classifier and trim file-tap plumbing

Desktop and the phone now read OSC 8 targets through one shared classifier. Terminal file taps carry only their candidate list, deduped by path, and desktop checks OSC overlap only for candidates it can resolve.

* refactor(terminal): keep shared link files under the line cap

The xterm buffer subset gets its own module, and the text-only OSC resolver goes now that the router uses findExactTerminalFileLink.
2026-10-27 05:30:05 +00:00

131 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
type NormalizedAbsolutePath = {
normalized: string
comparisonKey: string
rootKind: 'posix' | 'windows' | 'unc'
}
function normalizeSegments(pathValue: string): string[] {
const segments = pathValue.split(/[\\/]+/)
const stack: string[] = []
for (const segment of segments) {
if (!segment || segment === '.') {
continue
}
if (segment === '..') {
if (stack.length > 0) {
stack.pop()
}
continue
}
stack.push(segment)
}
return stack
}
export function normalizeAbsolutePath(pathValue: string): NormalizedAbsolutePath | null {
const windowsDriveMatch = /^([A-Za-z]):[\\/]*(.*)$/.exec(pathValue)
if (windowsDriveMatch) {
const driveLetter = windowsDriveMatch[1].toUpperCase()
const suffix = normalizeSegments(windowsDriveMatch[2]).join('/')
const normalized = suffix ? `${driveLetter}:/${suffix}` : `${driveLetter}:/`
return {
normalized,
comparisonKey: normalized.toLowerCase(),
rootKind: 'windows'
}
}
const uncMatch = /^(?:\\\\|\/\/)([^\\/]+)[\\/]+([^\\/]+)(?:[\\/]*(.*))?$/.exec(pathValue)
if (uncMatch) {
const server = uncMatch[1]
const share = uncMatch[2]
const suffix = normalizeSegments(uncMatch[3] ?? '').join('/')
const normalizedRoot = `//${server}/${share}`
const normalized = suffix ? `${normalizedRoot}/${suffix}` : normalizedRoot
return {
normalized,
comparisonKey: normalized.toLowerCase(),
rootKind: 'unc'
}
}
if (pathValue.startsWith('/')) {
const normalized = `/${normalizeSegments(pathValue).join('/')}`.replace(/\/+$/, '') || '/'
return {
normalized,
comparisonKey: normalized,
rootKind: 'posix'
}
}
return null
}
function inferHomePathFromCwd(cwd: string): string | null {
const normalizedCwd = normalizeAbsolutePath(cwd)
if (!normalizedCwd) {
return null
}
const segments = normalizeSegments(normalizedCwd.normalized)
if (normalizedCwd.rootKind === 'windows') {
const [drive, usersSegment, userSegment] = segments
if (!drive || !usersSegment || !userSegment || usersSegment.toLowerCase() !== 'users') {
return null
}
return `${drive}/${usersSegment}/${userSegment}`
}
if (normalizedCwd.rootKind === 'posix') {
const [rootParent, userSegment] = segments
if ((rootParent === 'Users' || rootParent === 'home') && userSegment) {
return `/${rootParent}/${userSegment}`
}
if (rootParent === 'root') {
return '/root'
}
}
return null
}
function normalizeExplicitHomePath(homePath: string | null | undefined): string | null {
const trimmedHomePath = homePath?.trim()
if (!trimmedHomePath) {
return null
}
return normalizeAbsolutePath(trimmedHomePath)?.normalized ?? null
}
export function resolveTildePath(
pathValue: string,
cwd: string,
homePath?: string | null
): string | null {
if (!/^~[\\/]/.test(pathValue)) {
return null
}
// Prefer explicit terminal home; remote/devcontainer cwd need not sit under the user’s home.
const resolvedHomePath = normalizeExplicitHomePath(homePath) ?? inferHomePathFromCwd(cwd)
if (!resolvedHomePath) {
return null
}
return joinAbsolutePath(resolvedHomePath, pathValue.slice(2))
}
export function joinAbsolutePath(basePath: string, relativePath: string): string | null {
const normalizedBase = normalizeAbsolutePath(basePath)
if (!normalizedBase) {
return null
}
const base = normalizedBase.normalized.replace(/\/$/, '')
const suffix = relativePath.replace(/^[\\/]+/, '')
// Resolve parent segments against the base while its drive or UNC share remains the root.
return normalizeAbsolutePath(`${base}/${suffix}`)?.normalized ?? null
}