✨ feat(vscode): 注释里的仓库路径链接能点开并跳到符号
This commit is contained in:
1 parent
5528a405cc
commit
c4113748e1
13 files changed
+662
-18
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import type { Stats } from 'node:fs'
|
||||
import { realpath, stat } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { runGit } from '../../git/worktree'
|
||||
|
||||
/** 文档符号树上的一个名字。只用来按名字找,不解析源码。 */
|
||||
export interface NamedSymbol {
|
||||
name: string
|
||||
children?: readonly NamedSymbol[]
|
||||
}
|
||||
|
||||
/** 仓库里的一个文件或目录。 */
|
||||
export interface RepoEntry {
|
||||
fsPath: string
|
||||
isDirectory: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 从仓库根起的相对路径。空段、`.`、`..`、绝对路径、盘符、反斜杠都不是。
|
||||
* 页面解析过一次,这里再查一遍:消息里的路径不能跑出仓库。
|
||||
*/
|
||||
export function isSafeRepoPath(rel: string): boolean {
|
||||
if (rel === '' || rel.startsWith('/') || rel.startsWith('./') || /^[a-z]:/i.test(rel) || hasControl(rel))
|
||||
return false
|
||||
if (rel.includes('\\') || rel.includes('?') || rel.includes('#'))
|
||||
return false
|
||||
const segments = rel.split('/')
|
||||
if (segments.at(-1) === '')
|
||||
segments.pop()
|
||||
return segments.length > 0 && segments.every(segment => segment !== '' && segment !== '.' && segment !== '..')
|
||||
}
|
||||
|
||||
/** target 解析之后还在 root 里面(root 自己不算)。 */
|
||||
export function staysInside(root: string, target: string): boolean {
|
||||
const back = path.relative(path.resolve(root), path.resolve(target))
|
||||
return back !== '' && back !== '..' && !back.startsWith(`..${path.sep}`) && !path.isAbsolute(back)
|
||||
}
|
||||
|
||||
/** 把相对路径拼到仓库根上。跑出去了就没有。不看磁盘。 */
|
||||
export function pathInsideRoot(root: string, rel: string): string | undefined {
|
||||
if (!isSafeRepoPath(rel))
|
||||
return undefined
|
||||
const resolved = path.resolve(root, rel)
|
||||
return staysInside(root, resolved) ? resolved : undefined
|
||||
}
|
||||
|
||||
/** 被预览的文件所在的 git 仓库根。不在仓库里就没有。 */
|
||||
export async function repoRootOf(filePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
const root = (await runGit(path.dirname(filePath), ['rev-parse', '--show-toplevel'])).trim()
|
||||
return root === '' ? undefined : root
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库相对路径在磁盘上的文件或目录。
|
||||
* 解析之后(含符号链接的真实位置)必须仍在仓库根里面;没有这个路径就没有。
|
||||
*/
|
||||
export async function resolveRepoEntry(root: string, rel: string): Promise<RepoEntry | undefined> {
|
||||
const lexical = pathInsideRoot(root, rel)
|
||||
if (!lexical)
|
||||
return undefined
|
||||
let rootReal: string
|
||||
let info: Stats
|
||||
let fileReal: string
|
||||
try {
|
||||
rootReal = await realpath(root)
|
||||
info = await stat(lexical)
|
||||
fileReal = await realpath(lexical)
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
if (!staysInside(rootReal, fileReal))
|
||||
return undefined
|
||||
return { fsPath: fileReal, isDirectory: info.isDirectory() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 在文档符号里按名字找。`Agent` 找这个类或函数;`Agent.owner_id` 先找 `Agent`,再在它里面找 `owner_id`。
|
||||
* 名字对的是符号自己的 name,不看源码怎么写。
|
||||
*/
|
||||
export function findRepoSymbol<T extends NamedSymbol>(symbols: readonly T[], symbol: string): T | undefined {
|
||||
const dot = symbol.indexOf('.')
|
||||
const head = dot === -1 ? symbol : symbol.slice(0, dot)
|
||||
const member = dot === -1 ? undefined : symbol.slice(dot + 1)
|
||||
if (head === '' || member === '')
|
||||
return undefined
|
||||
const found = findNamed(symbols, head)
|
||||
if (!found || member === undefined)
|
||||
return found
|
||||
return findNamed((found.children ?? []) as readonly T[], member)
|
||||
}
|
||||
|
||||
function findNamed<T extends NamedSymbol>(symbols: readonly T[], name: string): T | undefined {
|
||||
for (const symbol of symbols) {
|
||||
if (symbol.name === name)
|
||||
return symbol
|
||||
const nested = findNamed((symbol.children ?? []) as readonly T[], name)
|
||||
if (nested)
|
||||
return nested
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function hasControl(text: string): boolean {
|
||||
for (const char of text) {
|
||||
if (char.charCodeAt(0) <= 31)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user