70 lines
4.3 KiB
TypeScript
70 lines
4.3 KiB
TypeScript
/**
|
||
* 问 AI 时要模型怎么写回答里的代码位置:写成和注释里一样的仓库链接,代码阅读预览里点了能跳过去。
|
||
* 扩展自己写进回答里的代码位置也按这个写法(见 [codeLink])。
|
||
*/
|
||
|
||
import type { NamedFolder } from '../editor/repoPath'
|
||
import { namesFolder } from '../editor/repoPath'
|
||
import { escapeMarkdown } from '../explorer/rowDecoration'
|
||
|
||
/** 链接打不开时(问的代码不在 git 仓库里)的写法:行内代码的绝对路径加行号。 */
|
||
const OUTSIDE_RULE = '问的代码不在 git 仓库里,提到代码位置时写行内代码:绝对路径加行号,如 `/path/to/file.py:12-20`。'
|
||
|
||
/**
|
||
* 要模型把代码位置写成 Markdown 链接的那段话,带一个例子。链接的写法同注释里的仓库链接([parseRepoLink](vscode/webview-ui/src/codeReader/repoLink.ts#parseRepoLink) 认的):
|
||
* 优先 `#符号`,几行写 `#L起-L止`。预览按 `base` 找链接里的路径,所以把它告诉模型。
|
||
*
|
||
* @param base 链接路径从哪儿算:字符串是被问的代码所在的 git 仓库根(绝对路径);数组是「了解一块代码」找代码用的工作区文件夹,
|
||
* 几个文件夹时路径以文件夹的名字开头(见 [namesFolder])。被问的代码不在 git 仓库里时没有,链接打不开,改要行内代码的绝对路径
|
||
*/
|
||
export function codeLinkRule(base: string | readonly NamedFolder[] | undefined): string {
|
||
if (base === undefined)
|
||
return OUTSIDE_RULE
|
||
const { from, prefix, outside } = linkFrom(base)
|
||
return [
|
||
`提到代码位置时写成 Markdown 链接,点了能跳过去。${from},开头不带 / 或 ./:`,
|
||
'- 指到函数、类、方法:[显示名](路径#符号),符号写函数名或「类名.方法名」。优先用这种,代码改了行号也不失效。',
|
||
'- 指到函数里的几行:[文件名:起-止](路径#L起-L止),只有一行时写 #L行号。',
|
||
`例如:[OrderService.cancel](${prefix}backend/app/orders/service.py#OrderService.cancel)、[service.py:357-364](${prefix}backend/app/orders/service.py#L357-L364)。`,
|
||
`${outside}的代码(比如依赖库)不写链接,写行内代码:绝对路径加行号,如 \`/path/to/file.py:12-20\`。`,
|
||
].join('\n')
|
||
}
|
||
|
||
/** [codeLinkRule] 里随 `base` 变的几处:路径从哪儿算起、例子里路径的开头、哪些代码算在外面。 */
|
||
function linkFrom(base: string | readonly NamedFolder[]): { from: string, prefix: string, outside: string } {
|
||
if (typeof base === 'string')
|
||
return { from: `路径从仓库根 ${base} 算起`, prefix: '', outside: '仓库外' }
|
||
if (!namesFolder(base))
|
||
return { from: `路径从工作区文件夹 ${base[0].path} 算起`, prefix: '', outside: '这个文件夹外' }
|
||
const names = base.map(folder => folder.name)
|
||
return { from: `路径的第一段是工作区文件夹的名字(${names.join('、')}),后面接相对这个文件夹的路径`, prefix: `${names[0]}/`, outside: '这几个文件夹外' }
|
||
}
|
||
|
||
/** 链接地址里 CommonMark 会断开、或页面解码时会认错的字:空白、尖括号、百分号(页面按 `decodeURIComponent` 解回来)。成对的括号不算,原样留着。 */
|
||
const UNSAFE_IN_HREF = /[%\s<>]/g
|
||
|
||
/** 括号是不是成对:成对的 CommonMark 认作链接地址的一部分,不成对的链接就断了。 */
|
||
function balanced(text: string): boolean {
|
||
let depth = 0
|
||
for (const char of text) {
|
||
depth += char === '(' ? 1 : char === ')' ? -1 : 0
|
||
if (depth < 0)
|
||
return false
|
||
}
|
||
return depth === 0
|
||
}
|
||
|
||
/**
|
||
* 一处代码写成的仓库链接,和 [codeLinkRule] 要模型写的一样:有符号时是 `[显示名](路径#符号)`,没有时只到文件。
|
||
* 显示名里的 Markdown 记号转义掉;路径里 [UNSAFE_IN_HREF] 的字和不成对的括号按百分号编码,页面照样认成链接。
|
||
*
|
||
* @param label 显示名
|
||
* @param target 链接路径(见 [codeLinkRule] 的 `base`)
|
||
* @param symbol 文件里的符号,函数名或「类名.方法名」
|
||
*/
|
||
export function codeLink(label: string, target: string, symbol?: string): string {
|
||
const safe = target.replace(UNSAFE_IN_HREF, encodeURIComponent)
|
||
const href = balanced(safe) ? safe : safe.replaceAll('(', '%28').replaceAll(')', '%29')
|
||
return `[${escapeMarkdown(label)}](${symbol === undefined ? href : `${href}#${symbol}`})`
|
||
}
|