✨ 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,134 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user