Skip to content

Commit 54e908b

Browse files
authored
Merge pull request #285 from xuhuanzy/upstream
update
2 parents f51f90e + 9ee9a3e commit 54e908b

File tree

4 files changed

+212
-28
lines changed

4 files changed

+212
-28
lines changed

src/configRenames.ts

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function get<T>(
3131
defaultValue?: T
3232
): T | undefined {
3333
const oldKey = CONFIG_RENAMES.get(key);
34-
34+
3535
// Check if old config exists and has a non-null value
3636
if (oldKey && config.has(oldKey)) {
3737
const oldValue = config.get<T>(oldKey);
@@ -44,7 +44,7 @@ export function get<T>(
4444
return oldValue;
4545
}
4646
}
47-
47+
4848
// Get from new config key
4949
return config.get<T>(key, defaultValue as T);
5050
}
@@ -54,7 +54,7 @@ export function get<T>(
5454
*/
5555
function showDeprecationWarning(oldKey: string, newKey: string): void {
5656
const message = `Configuration "${oldKey}" is deprecated. Please use "${newKey}" instead.`;
57-
57+
5858
vscode.window.showWarningMessage(
5959
message,
6060
'Update Now',
@@ -71,73 +71,60 @@ function showDeprecationWarning(oldKey: string, newKey: string): void {
7171
*/
7272
export class ConfigurationManager {
7373
private readonly config: vscode.WorkspaceConfiguration;
74-
74+
7575
constructor(scope?: vscode.ConfigurationScope) {
7676
this.config = vscode.workspace.getConfiguration('emmylua', scope);
7777
}
78-
78+
7979
/**
8080
* Get a configuration value with type safety
8181
*/
8282
get<T>(section: string, defaultValue?: T): T | undefined {
83-
return get<T>(this.config, `emmylua.${section}`, defaultValue);
84-
}
85-
86-
/**
87-
* Check if a configuration affects language server behavior
88-
*/
89-
isLanguageServerConfig(section: string): boolean {
90-
const lsConfigPrefixes = [
91-
'emmylua.ls.',
92-
'emmylua.misc.executablePath',
93-
'emmylua.misc.globalConfigPath',
94-
];
95-
96-
return lsConfigPrefixes.some(prefix => section.startsWith(prefix));
83+
return get<T>(this.config, `${section}`, defaultValue) || get<T>(this.config, `emmylua.${section}`, defaultValue);
9784
}
98-
85+
9986
/**
10087
* Get language server executable path
10188
*/
10289
getExecutablePath(): string | undefined {
10390
return this.get<string>('misc.executablePath');
10491
}
105-
92+
10693
/**
10794
* Get language server global config path
10895
*/
10996
getGlobalConfigPath(): string | undefined {
11097
return this.get<string>('misc.globalConfigPath');
11198
}
112-
99+
113100
/**
114101
* Get language server start parameters
115102
*/
116103
getStartParameters(): string[] {
117104
return this.get<string[]>('ls.startParameters', []) || [];
118105
}
119-
106+
120107
/**
121108
* Get language server debug port
122109
*/
123110
getDebugPort(): number | null {
124111
return this.get<number | null>('ls.debugPort', null) || null;
125112
}
126-
113+
127114
/**
128115
* Get color configuration
129116
*/
130117
getColor(colorType: string): string | undefined {
131118
return this.get<string>(`colors.${colorType}`);
132119
}
133-
120+
134121
/**
135122
* Check if mutable variables should have underline
136123
*/
137124
useMutableUnderline(): boolean {
138125
return this.get<boolean>('colors.mutableUnderline', false) || false;
139126
}
140-
127+
141128
/**
142129
* Check if auto-complete annotation is enabled
143130
*/

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+
}

src/luarocks/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,16 @@ export async function initializeLuaRocks(): Promise<void> {
3939
luaRocksTreeProvider
4040
);
4141

42-
// Check if LuaRocks is installed
4342
const isInstalled = await luaRocksManager.checkLuaRocksInstallation();
4443
if (isInstalled) {
4544
let workspace = await luaRocksManager.detectLuaRocksWorkspace();
4645
if (workspace) {
47-
vscode.window.showInformationMessage(`Found ${workspace.rockspecFiles.length} rockspec file(s) in workspace`);
46+
// 只有在第一次打开工作区时才显示提示
47+
const hasShownMessage = extensionContext.vscodeContext.workspaceState.get('luarocks.rockspecMessageShown', false);
48+
if (!hasShownMessage) {
49+
vscode.window.showInformationMessage(`Found ${workspace.rockspecFiles.length} rockspec file(s) in workspace`);
50+
await extensionContext.vscodeContext.workspaceState.update('luarocks.rockspecMessageShown', true);
51+
}
4852
}
4953
}
5054
}

0 commit comments

Comments
 (0)