Files
superwork/vscode/webview-ui/src/codeReader/repoLink.ts
T

135 lines
4.4 KiB
TypeScript

import type { WebviewToExtension } from './types'
/** 注释里指向仓库代码的链接:从仓库根起的路径,可选一个符号。 */
export interface RepoLink {
/** 从仓库根算起,用 / 分隔,开头没有 / 或 ./ */
path: string
/** 类名、函数名,或「类名.成员名」。指向目录或整个文件时没有 */
symbol?: string
}
const SCHEME = /^[a-z][a-z0-9+.-]*:/i
const IDENT = /^[\p{L}_$][\p{L}\p{N}_$]*$/u
/**
* 从链接地址拆出仓库路径和可选符号。
* 不是这种写法就没有:http / mailto、行号(`#123`)、带 `..`、绝对路径、盘符、以 `/` 或 `./` 开头。
*/
export function parseRepoLink(url: string): RepoLink | undefined {
let text: string
try {
text = decodeURIComponent(url.trim())
}
catch {
return undefined
}
if (text === '' || SCHEME.test(text) || text.startsWith('/') || text.startsWith('./') || text.startsWith('../') || text.includes('\\') || hasControl(text))
return undefined
const hash = text.indexOf('#')
const rawPath = hash === -1 ? text : text.slice(0, hash)
const symbol = hash === -1 ? undefined : text.slice(hash + 1)
const path = rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath
if (!isRepoPath(path))
return undefined
if (symbol === undefined)
return { path }
if (!isSymbol(symbol))
return undefined
return { path, symbol }
}
function hasControl(text: string): boolean {
for (const char of text) {
if (char.charCodeAt(0) <= 31)
return true
}
return false
}
function isRepoPath(path: string): boolean {
if (path === '' || path.includes('?'))
return false
return path.split('/').every(segment => segment !== '' && segment !== '.' && segment !== '..')
}
function isSymbol(symbol: string): boolean {
const parts = symbol.split('.')
return (parts.length === 1 || parts.length === 2) && parts.every(part => IDENT.test(part))
}
function symbolKey(path: string, symbol: string): string {
return `${path}#${symbol}`
}
/**
* 页面上的仓库链接:点了发给扩展端打开;预览出来后把路径发去问哪些文件没有;
* 扩展端说没有的,标成坏的。符号在不在要点了才知道。
*/
export class RepoLinks {
private request = 0
/** 最近一次问过的路径,没变就不再问 */
private sent = ''
private missingPaths = new Set<string>()
private missingSymbols = new Set<string>()
constructor(private readonly post: (msg: WebviewToExtension) => void) {}
/** 点在仓库链接上:发给扩展端打开,返回 true。不是仓库链接就不管。 */
click(href: string | null): boolean {
const link = href === null ? undefined : parseRepoLink(href)
if (!link)
return false
this.post({ type: 'openRepoLink', path: link.path, ...(link.symbol !== undefined && { symbol: link.symbol }) })
return true
}
/** 页面上现有的仓库链接。路径变了才问扩展端哪些没有。 */
scan(hrefs: readonly string[]): void {
const paths = [...new Set(hrefs.flatMap((href) => {
const link = parseRepoLink(href)
return link ? [link.path] : []
}))].sort()
const key = paths.join('\n')
if (key === this.sent)
return
this.sent = key
if (paths.length === 0)
return
this.request += 1
this.post({ type: 'checkRepoLinks', id: this.request, paths })
}
/** id 那次询问的回答。不是最近一次的丢掉,返回这次有没有用上。 */
checked(id: number, missing: readonly string[]): boolean {
if (id !== this.request)
return false
this.missingPaths = new Set(missing)
return true
}
/** 扩展端打开时发现文件或符号没有。 */
broken(path: string, symbol?: string): void {
if (symbol !== undefined)
this.missingSymbols.add(symbolKey(path, symbol))
else
this.missingPaths.add(path)
}
/** 打开成功:这个路径在,这个符号也在。 */
opened(path: string, symbol?: string): void {
this.missingPaths.delete(path)
if (symbol !== undefined)
this.missingSymbols.delete(symbolKey(path, symbol))
}
/** 这个地址现在是不是坏的(文件没有,或这个符号没有)。还没问到的不算。 */
isBroken(href: string): boolean {
const link = parseRepoLink(href)
if (!link)
return false
if (this.missingPaths.has(link.path))
return true
return link.symbol !== undefined && this.missingSymbols.has(symbolKey(link.path, link.symbol))
}
}