From f6d38035a41195c7ef826cbb8c4d342bd219f3b0 Mon Sep 17 00:00:00 2001 From: xuhuanzy <501417909@qq.com> Date: Fri, 19 Dec 2025 09:23:36 +0800 Subject: [PATCH 1/4] Update configuration manager --- src/annotator.ts | 2 +- src/{configRenames.ts => configManager.ts} | 39 ++++++++++++---------- src/extension.ts | 2 +- src/languageConfiguration.ts | 18 +++++----- 4 files changed, 32 insertions(+), 29 deletions(-) rename src/{configRenames.ts => configManager.ts} (72%) diff --git a/src/annotator.ts b/src/annotator.ts index 1d111052..d55efc2d 100644 --- a/src/annotator.ts +++ b/src/annotator.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { AnnotatorType } from './lspExtension'; import { LanguageClient } from 'vscode-languageclient/node'; import * as notifications from "./lspExtension"; -import { get } from './configRenames'; +import { get } from './configManager'; // 装饰器类型映射接口 interface DecorationMap { diff --git a/src/configRenames.ts b/src/configManager.ts similarity index 72% rename from src/configRenames.ts rename to src/configManager.ts index c6bfcc7a..0359cb3d 100644 --- a/src/configRenames.ts +++ b/src/configManager.ts @@ -1,8 +1,9 @@ import * as vscode from 'vscode'; /** - * Configuration key rename mappings - * Maps new keys to old keys for backward compatibility + * 配置项重命名映射, 新键名 -> 旧键名. + * + * 用于将旧配置键名映射到新配置键名, 以确保向后兼容性 */ const CONFIG_RENAMES: ReadonlyMap = new Map([ ['emmylua.ls.executablePath', 'emmylua.misc.executablePath'], @@ -11,18 +12,19 @@ const CONFIG_RENAMES: ReadonlyMap = new Map([ ]); /** - * Track which deprecated configs have been warned about + * 已警告的废弃配置项 */ const warnedDeprecations = new Set(); /** - * Get configuration value with support for renamed keys - * Automatically falls back to old configuration keys for backward compatibility + * 获取配置值, 且支持重命名的键名. + * + * 会自动回退至旧配置键名, 确保向后兼容性. * - * @param config - Workspace configuration - * @param key - Configuration key to retrieve - * @param defaultValue - Optional default value if config doesn't exist - * @returns Configuration value or default + * @param config - 配置 + * @param key - 必须传入最新的配置键名以正确获取配置值 + * @param defaultValue - 可选的默认值 + * @returns 配置值或默认值 */ export function get( config: vscode.WorkspaceConfiguration, @@ -31,11 +33,11 @@ export function get( ): T | undefined { const oldKey = CONFIG_RENAMES.get(key); - // Check if old config exists and has a non-null value + // 如果旧键存在且有值, 那么我们需要使用旧键的值并警告用户该配置项已废弃 if (oldKey && config.has(oldKey)) { const oldValue = config.get(oldKey); if (oldValue !== undefined && oldValue !== null) { - // Warn about deprecated config (only once per session) + // 如果用户没有警告过该配置项, 那么我们需要警告用户 if (!warnedDeprecations.has(oldKey)) { warnedDeprecations.add(oldKey); showDeprecationWarning(oldKey, key); @@ -44,12 +46,12 @@ export function get( } } - // Get from new config key + // 如果旧键不存在, 那么我们直接使用新键的值 return config.get(key, defaultValue as T); } /** - * Show a deprecation warning for old configuration keys + * 显示配置项已废弃的警告 */ function showDeprecationWarning(oldKey: string, newKey: string): void { const message = `Configuration "${oldKey}" is deprecated. Please use "${newKey}" instead.`; @@ -66,7 +68,7 @@ function showDeprecationWarning(oldKey: string, newKey: string): void { } /** - * Get configuration with proper typing and defaults + * 配置管理器 */ export class ConfigurationManager { private readonly config: vscode.WorkspaceConfiguration; @@ -76,24 +78,25 @@ export class ConfigurationManager { } /** - * Get a configuration value with type safety + * 获取配置项 */ get(section: string, defaultValue?: T): T | undefined { - return get(this.config, `${section}`, defaultValue) || get(this.config, `emmylua.${section}`, defaultValue); + // `this.config`此时的值是所有`emmylua`配置项的集合(不包含`emmylua`前缀) + return get(this.config, `${section}`, defaultValue); } /** * Get language server executable path */ getExecutablePath(): string | undefined { - return this.get('misc.executablePath'); + return this.get('ls.executablePath'); } /** * Get language server global config path */ getGlobalConfigPath(): string | undefined { - return this.get('misc.globalConfigPath'); + return this.get('ls.globalConfigPath'); } /** diff --git a/src/extension.ts b/src/extension.ts index 2f375af3..a027b16b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import { LuaLanguageConfiguration } from './languageConfiguration'; import { EmmyContext } from './emmyContext'; import { IServerLocation, IServerPosition } from './lspExtension'; import { onDidChangeConfiguration } from './annotator'; -import { ConfigurationManager } from './configRenames'; +import { ConfigurationManager } from './configManager'; import * as Annotator from './annotator'; import { EmmyrcSchemaContentProvider } from './emmyrcSchemaContentProvider'; import { SyntaxTreeManager, setClientGetter } from './syntaxTreeProvider'; diff --git a/src/languageConfiguration.ts b/src/languageConfiguration.ts index bf3baf60..c2aabfb1 100644 --- a/src/languageConfiguration.ts +++ b/src/languageConfiguration.ts @@ -1,5 +1,5 @@ import { LanguageConfiguration, IndentAction, IndentationRule, OnEnterRule } from 'vscode'; -import { ConfigurationManager } from './configRenames'; +import { ConfigurationManager } from './configManager'; /** * Lua language configuration for VS Code @@ -14,7 +14,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { // Configure annotation completion rules based on user settings const configManager = new ConfigurationManager(); const completeAnnotation = configManager.isCompleteAnnotationEnabled(); - + this.onEnterRules = this.buildOnEnterRules(completeAnnotation); this.indentationRules = this.buildIndentationRules(); this.wordPattern = this.buildWordPattern(); @@ -29,7 +29,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { { beforeText: /^\s*function\s+\w*\s*\(.*\)\s*$/, afterText: /^\s*end\s*$/, - action: { + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } @@ -38,7 +38,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { { beforeText: /^\s*local\s+\w+\s*=\s*function\s*\(.*\)\s*$/, afterText: /^\s*end\s*$/, - action: { + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } @@ -47,7 +47,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { { beforeText: /^\s*.*\s*=\s*function\s*\(.*\)\s*$/, afterText: /^\s*end\s*$/, - action: { + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } @@ -56,7 +56,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { { beforeText: /^\s*local\s+function\s+\w*\s*\(.*\)\s*$/, afterText: /^\s*end\s*$/, - action: { + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } @@ -69,7 +69,7 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { // Continue annotation with space (---) { beforeText: /^---\s+/, - action: { + action: { indentAction: IndentAction.None, appendText: '--- ' } @@ -77,13 +77,13 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { // Continue annotation without space (---) { beforeText: /^---$/, - action: { + action: { indentAction: IndentAction.None, appendText: '---' } } ]; - + return [...annotationRules, ...baseRules]; } From 70eee8ab65779f077a0cc3cc28fa2085110eb30f Mon Sep 17 00:00:00 2001 From: xuhuanzy <501417909@qq.com> Date: Fri, 19 Dec 2025 07:03:39 +0800 Subject: [PATCH 2/4] chore: update LuaLanguageConfiguration --- src/languageConfiguration.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/languageConfiguration.ts b/src/languageConfiguration.ts index c2aabfb1..3cc0c648 100644 --- a/src/languageConfiguration.ts +++ b/src/languageConfiguration.ts @@ -63,10 +63,9 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { } ]; - // Add annotation completion rules if enabled if (enableAnnotationCompletion) { const annotationRules: OnEnterRule[] = [ - // Continue annotation with space (---) + // 当前行以`--- `开头时, 自动补全`--- ` { beforeText: /^---\s+/, action: { @@ -74,14 +73,38 @@ export class LuaLanguageConfiguration implements LanguageConfiguration { appendText: '--- ' } }, - // Continue annotation without space (---) + // 当前行以`---`开头时且后面没有任何内容时, 自动补全`---` { beforeText: /^---$/, action: { indentAction: IndentAction.None, appendText: '---' } - } + }, + // 当前行以`-- `开头时, 自动补全`-- ` + { + beforeText: /^--\s+/, + action: { + indentAction: IndentAction.None, + appendText: '-- ' + } + }, + // 当前行以一些注解标识符开头时, 自动补全`---@` + { + beforeText: /^---@(class|field|param|generic|overload)\b.*/, + action: { + indentAction: IndentAction.None, + appendText: '---@' + } + }, + // 对`---@alias`多行格式的处理, 我们认为以`---|`开头的行都是`---@alias`的续行 + { + beforeText: /^---\|.*/, + action: { + indentAction: IndentAction.None, + appendText: '---| ' + } + }, ]; return [...annotationRules, ...baseRules]; From 92fe9de7a0c4bd8587898be9f3809499e073b85e Mon Sep 17 00:00:00 2001 From: xuhuanzy <501417909@qq.com> Date: Fri, 19 Dec 2025 08:31:03 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20luarocks=20?= =?UTF-8?q?=E6=8A=A5=E9=94=99,=20=E7=A7=BB=E9=99=A4=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A0=8F=E5=9B=BE=E6=A0=87,=20=E6=B7=BB=E5=8A=A0=E5=9C=A8=20ro?= =?UTF-8?q?ckspec=20=E6=97=B6=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- src/luarocks/LuaRocksManager.ts | 154 ++++++++++++++------------- src/luarocks/LuaRocksTreeProvider.ts | 30 +++++- 3 files changed, 110 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index 41105334..32e28ae3 100644 --- a/package.json +++ b/package.json @@ -327,7 +327,7 @@ { "id": "emmylua.luarocks", "name": "LuaRocks", - "when": "resourceExtname == .lua", + "when": "resourceExtname == .lua || resourceExtname == .rockspec", "icon": "$(package)" } ] @@ -379,15 +379,15 @@ "commandPalette": [ { "command": "emmylua.luarocks.installPackage", - "when": "resourceExtname == .lua" + "when": "resourceExtname == .lua || resourceExtname == .rockspec" }, { "command": "emmylua.luarocks.searchPackages", - "when": "resourceExtname == .lua" + "when": "resourceExtname == .lua || resourceExtname == .rockspec" }, { "command": "emmylua.luarocks.checkInstallation", - "when": "resourceExtname == .lua" + "when": "resourceExtname == .lua || resourceExtname == .rockspec" } ] }, diff --git a/src/luarocks/LuaRocksManager.ts b/src/luarocks/LuaRocksManager.ts index 5e672d14..b21d385d 100644 --- a/src/luarocks/LuaRocksManager.ts +++ b/src/luarocks/LuaRocksManager.ts @@ -31,18 +31,12 @@ export interface LuaRocksWorkspace { export class LuaRocksManager { private readonly workspaceFolder: vscode.WorkspaceFolder; private readonly outputChannel: vscode.OutputChannel; - private readonly statusBarItem: vscode.StatusBarItem; private isInstalling = false; private installedPackagesCache: LuaPackage[] = []; constructor(workspaceFolder: vscode.WorkspaceFolder) { this.workspaceFolder = workspaceFolder; this.outputChannel = vscode.window.createOutputChannel('LuaRocks'); - this.statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, - 100 - ); - this.updateStatusBar(); } /** @@ -50,7 +44,7 @@ export class LuaRocksManager { */ async checkLuaRocksInstallation(): Promise { try { - const { stdout } = await exec('luarocks --version'); + const { stdout } = await exec('luarocks --version', { windowsHide: true }); return stdout.includes('LuaRocks'); } catch { return false; @@ -75,7 +69,7 @@ export class LuaRocksManager { null, 10 ); - + workspace.hasRockspec = rockspecFiles.length > 0; workspace.rockspecFiles = rockspecFiles.map(uri => uri.fsPath); @@ -107,13 +101,13 @@ export class LuaRocksManager { try { const content = await readFile(rockspecPath, 'utf8'); const dependencies: LuaPackage[] = []; - + // 简单的正则解析依赖(实际应该使用 Lua 解析器) const depMatch = content.match(/dependencies\s*=\s*\{([^}]*)\}/); if (depMatch) { const depContent = depMatch[1]; const depLines = depContent.split(',').map(line => line.trim()); - + for (const line of depLines) { const match = line.match(/["']([^"']+)["']/); if (match) { @@ -129,7 +123,7 @@ export class LuaRocksManager { } } } - + return dependencies; } catch (error) { console.error('Error parsing rockspec dependencies:', error); @@ -141,18 +135,25 @@ export class LuaRocksManager { * 搜索包 */ async searchPackages(query: string): Promise { + const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Searching packages...'); try { - this.showProgress('Searching packages...'); - - const { stdout } = await exec(`luarocks search ${query} --porcelain`); + const command = this.buildLuaRocksCommand([ + 'search', + this.quoteArg(query), + '--porcelain' + ]); + const { stdout } = await exec(command, { + cwd: this.workspaceFolder.uri.fsPath, + windowsHide: true + }); const packages = this.parseSearchResults(stdout); - this.hideProgress(); return packages; } catch (error) { - this.hideProgress(); this.showError('Failed to search packages', error); return []; + } finally { + status.dispose(); } } @@ -165,8 +166,14 @@ export class LuaRocksManager { } try { - const localFlag = await this.shouldUseLocal() ? '--local' : ''; - const { stdout } = await exec(`luarocks list --porcelain ${localFlag}`.trim()); + const command = this.buildLuaRocksCommand([ + 'list', + '--porcelain' + ]); + const { stdout } = await exec(command, { + cwd: this.workspaceFolder.uri.fsPath, + windowsHide: true + }); this.installedPackagesCache = this.parseInstalledPackages(stdout); return this.installedPackagesCache; } catch (error) { @@ -184,28 +191,31 @@ export class LuaRocksManager { return false; } + const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Installing...'); try { this.isInstalling = true; - this.updateStatusBar('Installing...'); - - const versionSpec = version && version !== 'latest' ? `${packageName} ${version}` : packageName; - const localFlag = await this.shouldUseLocal() ? '--local' : ''; - - const command = `luarocks install ${versionSpec} ${localFlag}`.trim(); + const installArgs = [ + 'install', + this.quoteArg(packageName), + ...(version && version !== 'latest' ? [this.quoteArg(version)] : []) + ]; + const command = this.buildLuaRocksCommand(installArgs); + this.outputChannel.show(); this.outputChannel.appendLine(`Installing: ${command}`); - + const { stdout, stderr } = await exec(command, { - cwd: this.workspaceFolder.uri.fsPath + cwd: this.workspaceFolder.uri.fsPath, + windowsHide: true }); - + this.outputChannel.appendLine(stdout); if (stderr) this.outputChannel.appendLine(stderr); - + // 清除缓存 this.installedPackagesCache = []; - + vscode.window.showInformationMessage(`Successfully installed ${packageName}`); return true; } catch (error) { @@ -213,7 +223,7 @@ export class LuaRocksManager { return false; } finally { this.isInstalling = false; - this.updateStatusBar(); + status.dispose(); } } @@ -221,30 +231,34 @@ export class LuaRocksManager { * 卸载包 */ async uninstallPackage(packageName: string): Promise { + const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Uninstalling...'); try { - this.updateStatusBar('Uninstalling...'); - - const localFlag = await this.shouldUseLocal() ? '--local' : ''; - const command = `luarocks remove ${packageName} ${localFlag}`.trim(); - + const command = this.buildLuaRocksCommand([ + 'remove', + this.quoteArg(packageName) + ]); + this.outputChannel.show(); this.outputChannel.appendLine(`Uninstalling: ${command}`); - - const { stdout, stderr } = await exec(command); - + + const { stdout, stderr } = await exec(command, { + cwd: this.workspaceFolder.uri.fsPath, + windowsHide: true + }); + this.outputChannel.appendLine(stdout); if (stderr) this.outputChannel.appendLine(stderr); - + // 清除缓存 this.installedPackagesCache = []; - + vscode.window.showInformationMessage(`Successfully uninstalled ${packageName}`); return true; } catch (error) { this.showError(`Failed to uninstall ${packageName}`, error); return false; } finally { - this.updateStatusBar(); + status.dispose(); } } @@ -253,7 +267,14 @@ export class LuaRocksManager { */ async getPackageInfo(packageName: string): Promise { try { - const { stdout } = await exec(`luarocks show ${packageName}`); + const command = this.buildLuaRocksCommand([ + 'show', + this.quoteArg(packageName) + ]); + const { stdout } = await exec(command, { + cwd: this.workspaceFolder.uri.fsPath, + windowsHide: true + }); return this.parsePackageInfo(stdout, packageName); } catch (error) { // 尝试从搜索结果获取信息 @@ -280,7 +301,7 @@ export class LuaRocksManager { private parseSearchResults(output: string): LuaPackage[] { const packages: LuaPackage[] = []; const lines = output.trim().split('\n').filter(line => line.trim()); - + for (const line of lines) { const parts = line.split('\t'); if (parts.length >= 2) { @@ -293,26 +314,26 @@ export class LuaRocksManager { }); } } - + return packages; } private parseInstalledPackages(output: string): LuaPackage[] { const packages: LuaPackage[] = []; const lines = output.trim().split('\n').filter(line => line.trim()); - + for (const line of lines) { const parts = line.split('\t'); if (parts.length >= 2) { packages.push({ name: parts[0].trim(), version: parts[1].trim(), - location: parts[2]?.trim() || '', + location: (parts[3] ?? parts[2] ?? '').trim(), installed: true }); } } - + return packages; } @@ -322,14 +343,14 @@ export class LuaRocksManager { name: packageName, installed: true }; - + for (const line of lines) { const colonIndex = line.indexOf(':'); if (colonIndex === -1) continue; - + const key = line.substring(0, colonIndex).trim().toLowerCase(); const value = line.substring(colonIndex + 1).trim(); - + switch (key) { case 'version': info.version = value; @@ -350,13 +371,18 @@ export class LuaRocksManager { break; } } - + return info as LuaPackage; } - private async shouldUseLocal(): Promise { - const config = vscode.workspace.getConfiguration('emmylua.luarocks'); - return config.get('preferLocalInstall', true); + private quoteArg(value: string): string { + const trimmed = value.trim().replace(/[\\/]+$/, ''); + const escapedQuotes = trimmed.replace(/"/g, '\\"'); + return `"${escapedQuotes}"`; + } + + private buildLuaRocksCommand(args: string[]): string { + return ['luarocks', ...args.filter(Boolean)].join(' ').trim(); } private showError(message: string, error: any): void { @@ -366,27 +392,7 @@ export class LuaRocksManager { vscode.window.showErrorMessage(message); } - private showProgress(message: string): void { - this.updateStatusBar(message); - } - - private hideProgress(): void { - this.updateStatusBar(); - } - - private updateStatusBar(text?: string): void { - if (text) { - this.statusBarItem.text = `$(sync~spin) ${text}`; - this.statusBarItem.show(); - } else { - this.statusBarItem.text = '$(package) LuaRocks'; - this.statusBarItem.command = 'emmylua.luarocks.showPackages'; - this.statusBarItem.show(); - } - } - dispose(): void { this.outputChannel.dispose(); - this.statusBarItem.dispose(); } } diff --git a/src/luarocks/LuaRocksTreeProvider.ts b/src/luarocks/LuaRocksTreeProvider.ts index db2ad82b..fc7442cc 100644 --- a/src/luarocks/LuaRocksTreeProvider.ts +++ b/src/luarocks/LuaRocksTreeProvider.ts @@ -6,6 +6,8 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; private installedPackages: LuaPackage[] = []; + private installedLoaded = false; + private installedLoading: Promise | undefined; private searchResults: LuaPackage[] = []; private isSearchMode = false; @@ -16,7 +18,7 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider { - this.installedPackages = await this.manager.getInstalledPackages(true); + await this.ensureInstalledLoaded(true); this.refresh(); } @@ -44,6 +46,7 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'installed', pkg) ); @@ -69,6 +70,27 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider { + if (!forceRefresh && this.installedLoaded) { + return; + } + + if (this.installedLoading) { + return this.installedLoading; + } + + this.installedLoading = (async () => { + this.installedPackages = await this.manager.getInstalledPackages(forceRefresh); + this.installedLoaded = true; + })(); + + try { + await this.installedLoading; + } finally { + this.installedLoading = undefined; + } + } + dispose(): void { // Clean up any resources if needed } From 365e74ea5e2d861abe355a2512a77dd72c55e855 Mon Sep 17 00:00:00 2001 From: xuhuanzy <501417909@qq.com> Date: Fri, 19 Dec 2025 09:18:31 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20luarocks=20=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E8=A7=86=E5=9B=BE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/luarocks/LuaRocksManager.ts | 13 ++- src/luarocks/LuaRocksTreeProvider.ts | 119 +++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/luarocks/LuaRocksManager.ts b/src/luarocks/LuaRocksManager.ts index b21d385d..1815dd8f 100644 --- a/src/luarocks/LuaRocksManager.ts +++ b/src/luarocks/LuaRocksManager.ts @@ -112,11 +112,12 @@ export class LuaRocksManager { const match = line.match(/["']([^"']+)["']/); if (match) { const depString = match[1]; - const [name, version] = depString.split(/\s+/); + const [name, ...versionParts] = depString.split(/\s+/); + const version = versionParts.join(' ').trim(); if (name && name !== 'lua') { dependencies.push({ name: name.trim(), - version: version?.trim() || 'latest', + version: version || 'latest', installed: false }); } @@ -135,6 +136,7 @@ export class LuaRocksManager { * 搜索包 */ async searchPackages(query: string): Promise { + // 使用临时状态栏消息显示进度(不再使用常驻的状态栏图标) const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Searching packages...'); try { const command = this.buildLuaRocksCommand([ @@ -147,7 +149,7 @@ export class LuaRocksManager { windowsHide: true }); const packages = this.parseSearchResults(stdout); - + return packages; } catch (error) { this.showError('Failed to search packages', error); @@ -191,10 +193,11 @@ export class LuaRocksManager { return false; } + // 使用临时状态栏消息显示进度 const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Installing...'); try { this.isInstalling = true; - + const installArgs = [ 'install', this.quoteArg(packageName), @@ -231,6 +234,7 @@ export class LuaRocksManager { * 卸载包 */ async uninstallPackage(packageName: string): Promise { + // 使用临时状态栏消息显示进度 const status = vscode.window.setStatusBarMessage('$(sync~spin) LuaRocks: Uninstalling...'); try { const command = this.buildLuaRocksCommand([ @@ -376,6 +380,7 @@ export class LuaRocksManager { } private quoteArg(value: string): string { + // 简单的引号封装,避免空格路径/参数导致命令解析错误 const trimmed = value.trim().replace(/[\\/]+$/, ''); const escapedQuotes = trimmed.replace(/"/g, '\\"'); return `"${escapedQuotes}"`; diff --git a/src/luarocks/LuaRocksTreeProvider.ts b/src/luarocks/LuaRocksTreeProvider.ts index fc7442cc..10f3e024 100644 --- a/src/luarocks/LuaRocksTreeProvider.ts +++ b/src/luarocks/LuaRocksTreeProvider.ts @@ -8,10 +8,15 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider | undefined; + + // 当前工作区 rockspec 声明的依赖 + private rockspecDependencies: { packageInfo: LuaPackage; requirement: string }[] = []; + private rockspecLoaded = false; + private rockspecLoading: Promise | undefined; private searchResults: LuaPackage[] = []; private isSearchMode = false; - constructor(private readonly manager: LuaRocksManager) {} + constructor(private readonly manager: LuaRocksManager) { } refresh(): void { this._onDidChangeTreeData.fire(); @@ -19,6 +24,7 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider { await this.ensureInstalledLoaded(true); + await this.ensureRockspecLoaded(true); this.refresh(); } @@ -43,27 +49,39 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider - new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'installed', pkg) - ); - } else if (element.label === 'Search Results') { - return this.searchResults.map(pkg => - new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'available', pkg) - ); + switch (element.categoryId) { + case 'installed': { + // 展开时再加载,避免扩展启动就执行外部命令 + await this.ensureInstalledLoaded(false); + return this.installedPackages.map(pkg => + new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'installed', pkg) + ); + } + case 'rockspecDependencies': { + // 依赖列表用于“缺什么装什么”,保持数据最新 + await this.ensureRockspecLoaded(false); + return this.rockspecDependencies.map(dep => + new PackageTreeItem(dep.packageInfo.name, vscode.TreeItemCollapsibleState.None, 'dependency', dep.packageInfo, undefined, undefined, dep.requirement) + ); + } + case 'searchResults': { + return this.searchResults.map(pkg => + new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'available', pkg) + ); + } } } @@ -80,6 +98,7 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider { + // 获取已安装包列表(内部有缓存,这里通过 forceRefresh 控制刷新) this.installedPackages = await this.manager.getInstalledPackages(forceRefresh); this.installedLoaded = true; })(); @@ -91,6 +110,60 @@ export class LuaRocksTreeProvider implements vscode.TreeDataProvider { + if (!forceRefresh && this.rockspecLoaded) { + return; + } + + if (this.rockspecLoading) { + return this.rockspecLoading; + } + + this.rockspecLoading = (async () => { + const workspace = await this.manager.detectLuaRocksWorkspace(); + const dependencies = workspace.dependencies ?? []; + + await this.ensureInstalledLoaded(false); + const installedByName = new Map(this.installedPackages.map(pkg => [pkg.name, pkg])); + + this.rockspecDependencies = dependencies.map(dep => { + const installed = installedByName.get(dep.name); + const requirement = dep.version || 'latest'; + + // 这里构造用于树视图的 packageInfo: + // - installed: 用于菜单/hover 行为(安装/卸载按钮) + // - summary: 用于 tooltip 展示依赖要求(Required: ...) + const packageInfo: LuaPackage = { + name: dep.name, + version: installed?.version || 'latest', + installed: Boolean(installed), + location: installed?.location, + summary: `Required: ${requirement}` + }; + + return { packageInfo, requirement }; + }); + + // 将未安装的依赖置顶 + this.rockspecDependencies.sort((a, b) => { + const aMissing = !a.packageInfo.installed; + const bMissing = !b.packageInfo.installed; + if (aMissing !== bMissing) { + return aMissing ? -1 : 1; + } + return a.packageInfo.name.localeCompare(b.packageInfo.name); + }); + + this.rockspecLoaded = true; + })(); + + try { + await this.rockspecLoading; + } finally { + this.rockspecLoading = undefined; + } + } + dispose(): void { // Clean up any resources if needed } @@ -100,9 +173,11 @@ export class PackageTreeItem extends vscode.TreeItem { constructor( public readonly label: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState, - public readonly type: 'category' | 'installed' | 'available' | 'search', + public readonly type: 'category' | 'installed' | 'available' | 'dependency' | 'search', public readonly packageInfo?: LuaPackage, - public readonly count?: number + public readonly count?: number, + public readonly categoryId?: 'installed' | 'rockspecDependencies' | 'searchResults', + public readonly requirement?: string ) { super(label, collapsibleState); @@ -113,6 +188,7 @@ export class PackageTreeItem extends vscode.TreeItem { switch (this.type) { case 'category': this.iconPath = new vscode.ThemeIcon('folder'); + // 右侧描述区域用来显示数量 this.description = this.count !== undefined ? `(${this.count})` : ''; this.contextValue = 'category'; break; @@ -133,6 +209,16 @@ export class PackageTreeItem extends vscode.TreeItem { // 右键菜单中提供详细信息 break; + case 'dependency': { + const installed = Boolean(this.packageInfo?.installed); + this.iconPath = new vscode.ThemeIcon(installed ? 'package' : 'cloud-download'); + // 安装状态状态指示 + this.description = installed ? '✓' : '✗'; + this.tooltip = this.createTooltip(); + this.contextValue = installed ? 'installedPackage' : 'availablePackage'; + break; + } + case 'search': this.iconPath = new vscode.ThemeIcon('search'); this.command = { @@ -147,8 +233,9 @@ export class PackageTreeItem extends vscode.TreeItem { private createTooltip(): string { if (!this.packageInfo) return this.label; + const displayVersion = this.packageInfo.version && this.packageInfo.version !== 'latest' ? this.packageInfo.version : ''; const lines = [ - `**${this.packageInfo.name}** ${this.packageInfo.version || ''}`, + `**${this.packageInfo.name}** ${displayVersion}`, '' ];