- api.ts:call 与 fetchUpload 里逐字相同的响应 envelope 解析 (json().catch(null) + ok/value 判断 + SidebarApiError 构造)收敛为 私有 readEnvelope<T>(),两处调用;判断顺序与错误码回退逻辑不变。 - 取扩展名双实现收敛:extOf 的实现迁至零依赖的 paths.ts(lang.ts 与 service.ts 均可达的基础模块),lang.ts re-export 保持既有导入面 (tests/file-types.spec.ts 从 lang.ts 导入 extOf);service.ts 删除 模块私有的同体 extOfPath,matchFileViewer 改用 extOf。方向说明: service.ts 不能反向 import lang.ts——lang.ts 携带全部 @codemirror/* 依赖且只经 editor 懒加载 chunk 进入浏览器,core bundle 静态引用它 会把 CodeMirror 拉进核心包(构建产物已验证:client.js / client-registry.js 中 0 处 codemirror,client-editor.js 不变)。
86 lines
3.7 KiB
TypeScript
86 lines
3.7 KiB
TypeScript
/**
|
|
* Path projection helpers shared by the explorer rows: a path relative to
|
|
* the session cwd (for the @-reference button and "copy relative path").
|
|
* The fs-tree joins with '/' even on Windows, so both separators normalize
|
|
* to '/' before comparison.
|
|
*
|
|
* This module is dependency-free (no node:path in the client bundle): the
|
|
* host is the authority for path semantics, so this mirror deliberately
|
|
* accepts a SUPERSET of absolute forms — anything a Windows host would emit
|
|
* (drive letters, UNC) plus POSIX roots. A form the host would reject
|
|
* (e.g. a backslash UNC path on a POSIX host) passes through here and then
|
|
* fails loudly in the host's requireAbsolute instead of being silently
|
|
* joined onto the cwd.
|
|
*/
|
|
|
|
/**
|
|
* Mirror of the host's absolute-path notion (see fs-tree.requireAbsolute):
|
|
* POSIX roots, Windows drive letters, and Windows UNC network shares in
|
|
* both backslash (`\\server\share\...`) and forward-slash
|
|
* (`//server/share/...`) form. Deliberately a superset — see the module
|
|
* comment — so a produced UNC path is never joined onto the cwd.
|
|
*/
|
|
export function isAbsolutePath(path: string): boolean {
|
|
return path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(path) || /^[\\/]{2}[^\\/]/.test(path)
|
|
}
|
|
|
|
/**
|
|
* The path relative to the session's working directory.
|
|
* @param cwd - the explorer root (absolute).
|
|
* @param path - an absolute entry path from the fs-tree.
|
|
* @returns the relative path with '/' separators ('.' for the cwd itself),
|
|
* or `path` unchanged when it lies outside the cwd.
|
|
*
|
|
* The prefix test is case-insensitive: Windows paths (and macOS's
|
|
* case-insensitive volumes) may arrive with different casing than the cwd
|
|
* row, and the containment decision must not depend on it. The returned
|
|
* relative text keeps the caller's own casing.
|
|
*/
|
|
export function relativeTo(cwd: string, path: string): string {
|
|
const base = cwd.replace(/[\\/]+$/, '')
|
|
const norm = (value: string): string => value.replace(/\\/g, '/')
|
|
const nBase = norm(base)
|
|
const nPath = norm(path)
|
|
if (nPath === nBase) return '.'
|
|
if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1)
|
|
return path
|
|
}
|
|
|
|
/**
|
|
* Whether `target` lies under `base` (or equals it), tolerant of separator
|
|
* style and — on Windows-style drive paths — of letter case. A client-side
|
|
* mirror of the host's `isWithin` (fs-tree.ts) used to decide whether a
|
|
* git-derived path can be opened in the editor (a linked worktree outside
|
|
* the session workspace cannot: the host's workspace fence would reject it).
|
|
*/
|
|
export function isWithinWorkspace(base: string, target: string): boolean {
|
|
const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '')
|
|
const b = norm(base)
|
|
const t = norm(target)
|
|
const lb = b.toLowerCase()
|
|
const lt = t.toLowerCase()
|
|
return lt === lb || lt.startsWith(`${lb}/`)
|
|
}
|
|
|
|
/**
|
|
* The last path segment of a '/'- or '\'-separated path (a diff tab title,
|
|
* a worktree label). Returns the whole string when no separator is present.
|
|
*/
|
|
export function baseName(path: string): string {
|
|
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
return at === -1 ? path : path.slice(at + 1)
|
|
}
|
|
|
|
/**
|
|
* The lowercased file extension of a path ('' when none). The dot must sit
|
|
* inside the last segment — a dot in a directory name is not an extension.
|
|
* Shared by the editor language mapping (lang.ts) and the viewer registry's
|
|
* extension matching (service.ts), which both live in the core bundle.
|
|
*/
|
|
export function extOf(path: string): string {
|
|
const at = path.lastIndexOf('.')
|
|
if (at === -1) return ''
|
|
const base = path.slice(at + 1).toLowerCase()
|
|
return base.includes('/') || base.includes('\\') ? '' : base
|
|
}
|