Skip to content

Commit ed6d677

Browse files
committed
添加终端链接适配器
1 parent e0d4a08 commit ed6d677

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ConfigurationManager } from './configRenames';
1414
import * as Annotator from './annotator';
1515
import { EmmyrcSchemaContentProvider } from './emmyrcSchemaContentProvider';
1616
import { SyntaxTreeManager, setClientGetter } from './syntaxTreeProvider';
17+
import { registerTerminalLinkProvider } from './luaTerminalLinkProvider';
1718
import { insertEmmyDebugCode, registerDebuggers } from './debugger';
1819
import * as LuaRocks from './luarocks';
1920

@@ -55,6 +56,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
5556
registerCommands(context);
5657
registerEventListeners(context);
5758
registerLanguageConfiguration(context);
59+
registerTerminalLinkProvider(context);
5860

5961
// Initialize features
6062
await initializeExtension();

src/luaTerminalLinkProvider.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
5+
/**
6+
* 扩展的终端链接,包含额外数据
7+
*/
8+
class LuaTerminalLink extends vscode.TerminalLink {
9+
constructor(
10+
startIndex: number,
11+
length: number,
12+
tooltip: string | undefined,
13+
public readonly filePath: string,
14+
public readonly lineNumber?: number,
15+
public readonly originalMatch?: string
16+
) {
17+
super(startIndex, length, tooltip);
18+
}
19+
}
20+
21+
/**
22+
* Lua 终端链接提供器
23+
*/
24+
class LuaTerminalLinkProvider implements vscode.TerminalLinkProvider {
25+
/**
26+
* 匹配 Lua 文件路径的正则表达式
27+
* 匹配内容:
28+
* - 可选的任意前缀和 '→ ' (用于 busted 测试框架输出)
29+
* - 可选的 '...' 前缀. `error` 函数会截断路径为`...`
30+
* - 带反斜杠或正斜杠的路径段
31+
* - .lua 文件扩展名
32+
* - 可选的 :行号 后缀
33+
*/
34+
// 支持: "Error → .../a.lua:3", ".../a.lua:3", "C:\path with space\a.lua:3", "src/a.lua:3"
35+
private readonly linkPattern =
36+
/(?:.*?\s*)?(?:\.\.\.)?((?:[A-Za-z]:)?[^:\r\n]*?\.lua)(?::(\d+))?/g;
37+
38+
/**
39+
* 为 Lua 错误信息提供终端链接
40+
*/
41+
provideTerminalLinks(
42+
context: vscode.TerminalLinkContext,
43+
_token: vscode.CancellationToken
44+
): vscode.ProviderResult<LuaTerminalLink[]> {
45+
const line = context.line;
46+
const links: LuaTerminalLink[] = [];
47+
48+
// 重置正则表达式状态
49+
this.linkPattern.lastIndex = 0;
50+
51+
let match: RegExpExecArray | null;
52+
while ((match = this.linkPattern.exec(line)) !== null) {
53+
const fullMatch = match[0];
54+
const filePath = match[1];
55+
const lineNumber = match[2] ? parseInt(match[2], 10) : undefined;
56+
57+
// 创建带工具提示的链接
58+
const link = new LuaTerminalLink(
59+
match.index,
60+
fullMatch.length,
61+
undefined,
62+
filePath,
63+
lineNumber,
64+
fullMatch
65+
);
66+
67+
links.push(link);
68+
}
69+
70+
return links;
71+
}
72+
73+
/**
74+
* 处理终端链接激活
75+
*/
76+
async handleTerminalLink(link: LuaTerminalLink): Promise<void> {
77+
const { filePath, lineNumber, originalMatch } = link;
78+
79+
// 尝试解析文件路径
80+
const resolvedPath = await this.resolveFilePath(filePath, originalMatch || filePath);
81+
82+
if (!resolvedPath) {
83+
vscode.window.showWarningMessage(
84+
`not found: ${filePath}`
85+
);
86+
return;
87+
}
88+
89+
// 打开文件
90+
try {
91+
const uri = vscode.Uri.file(resolvedPath);
92+
const document = await vscode.workspace.openTextDocument(uri);
93+
const editor = await vscode.window.showTextDocument(document);
94+
95+
// 如果指定了行号则跳转
96+
if (lineNumber !== undefined && lineNumber > 0) {
97+
const position = new vscode.Position(lineNumber - 1, 0);
98+
editor.selection = new vscode.Selection(position, position);
99+
editor.revealRange(
100+
new vscode.Range(position, position),
101+
vscode.TextEditorRevealType.InCenter
102+
);
103+
}
104+
} catch (error) {
105+
vscode.window.showErrorMessage(
106+
`open file error: ${resolvedPath}`
107+
);
108+
}
109+
}
110+
111+
/**
112+
* 将截断的文件路径解析为完整路径
113+
*/
114+
private async resolveFilePath(
115+
filePath: string,
116+
originalMatch: string
117+
): Promise<string | null> {
118+
const normalizedFilePath = path.normalize(filePath);
119+
const workspaceFolders = vscode.workspace.workspaceFolders ?? [];
120+
const exists = async (p: string) => {
121+
try {
122+
await fs.promises.access(p, fs.constants.F_OK);
123+
return true;
124+
} catch {
125+
return false;
126+
}
127+
};
128+
129+
// 1) 绝对路径直接返回
130+
if (path.isAbsolute(normalizedFilePath) && (await exists(normalizedFilePath))) {
131+
return normalizedFilePath;
132+
}
133+
134+
// 2) 尝试工作区相对路径
135+
for (const folder of workspaceFolders) {
136+
const candidate = path.join(folder.uri.fsPath, normalizedFilePath);
137+
if (await exists(candidate)) {
138+
return candidate;
139+
}
140+
}
141+
142+
// 3) 处理被 "..." 截断的路径,提取后缀再搜索
143+
const suffixMatch = originalMatch.match(/^\.\.\.(.+\.lua)/);
144+
const suffix = suffixMatch ? suffixMatch[1] : normalizedFilePath;
145+
146+
// 基于文件名的快速搜索,再按后缀评分
147+
const fileName = path.basename(suffix);
148+
const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/.git/**', 200);
149+
const normalizedSuffix = path.normalize(suffix);
150+
151+
let best: { path: string; score: number; len: number } | null = null;
152+
153+
const suffixScore = (candidate: string): number => {
154+
const a = candidate.split(path.sep).reverse();
155+
const b = normalizedSuffix.split(path.sep).reverse();
156+
let matched = 0;
157+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
158+
if (a[i] === b[i]) {
159+
matched += a[i].length + 1;
160+
} else {
161+
break;
162+
}
163+
}
164+
return matched;
165+
};
166+
167+
for (const file of files) {
168+
const candidate = path.normalize(file.fsPath);
169+
if (!candidate.endsWith(normalizedSuffix)) {
170+
continue;
171+
}
172+
const score = suffixScore(candidate);
173+
const len = candidate.length;
174+
175+
if (!best || score > best.score || (score === best.score && len < best.len)) {
176+
best = { path: candidate, score, len };
177+
}
178+
}
179+
180+
return best?.path ?? null;
181+
}
182+
}
183+
184+
185+
export function registerTerminalLinkProvider(context: vscode.ExtensionContext): void {
186+
const terminalLinkProvider = vscode.window.registerTerminalLinkProvider(
187+
new LuaTerminalLinkProvider()
188+
);
189+
190+
context.subscriptions.push(terminalLinkProvider);
191+
}

0 commit comments

Comments
 (0)