├── .prettierrc ├── .eslintrc.json ├── res ├── icon.png └── logo.png ├── src ├── debugger │ ├── new_debugger │ │ ├── EmmyNewDebugAdapter.ts │ │ ├── EmmyNewDebuggerProvider.ts │ │ └── EmmyNewDebugSession.ts │ ├── attach │ │ ├── EmmyAttachDebugAdapter.ts │ │ ├── EmmyAttachDebuggerProvider.ts │ │ └── EmmyAttachDebugSession.ts │ ├── launch │ │ ├── EmmyLaunchDebugAdapter.ts │ │ ├── EmmyLaunchDebuggerProvider.ts │ │ └── EmmyLaunchDebugSession.ts │ ├── base │ │ ├── DebugConfigurationBase.ts │ │ ├── DebugSession.ts │ │ ├── EmmyDebugProto.ts │ │ ├── DebuggerProvider.ts │ │ ├── EmmyDebugData.ts │ │ └── EmmyDebugSession.ts │ ├── DebugFactory.ts │ └── index.ts ├── lspExtension.ts ├── emmyrcSchemaContentProvider.ts ├── configRenames.ts ├── languageConfiguration.ts ├── luarocks │ ├── LuaRocksTreeProvider.ts │ ├── index.ts │ └── LuaRocksManager.ts ├── annotator.ts ├── syntaxTreeProvider.ts ├── emmyContext.ts ├── extension.ts └── luaTerminalLinkProvider.ts ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .vscodeignore ├── .gitignore ├── tsconfig.json ├── language-configuration.json ├── .github └── workflows │ └── build.yml ├── package.nls.zh-cn.json ├── README.md ├── debugger ├── Emmy.lua └── emmy │ └── emmyHelper.lua ├── package.nls.json └── syntaxes └── schema.i18n.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules":{ 3 | "indent":"warn" 4 | } 5 | } -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmmyLua/VSCode-EmmyLua/HEAD/res/icon.png -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmmyLua/VSCode-EmmyLua/HEAD/res/logo.png -------------------------------------------------------------------------------- /src/debugger/new_debugger/EmmyNewDebugAdapter.ts: -------------------------------------------------------------------------------- 1 | import { EmmyNewDebugSession } from "./EmmyNewDebugSession"; 2 | EmmyNewDebugSession.run(EmmyNewDebugSession); -------------------------------------------------------------------------------- /src/debugger/attach/EmmyAttachDebugAdapter.ts: -------------------------------------------------------------------------------- 1 | import { EmmyAttachDebugSession } from "./EmmyAttachDebugSession"; 2 | 3 | EmmyAttachDebugSession.run(EmmyAttachDebugSession); -------------------------------------------------------------------------------- /src/debugger/launch/EmmyLaunchDebugAdapter.ts: -------------------------------------------------------------------------------- 1 | import { EmmyLaunchDebugSession } from "./EmmyLaunchDebugSession"; 2 | 3 | EmmyLaunchDebugSession.run(EmmyLaunchDebugSession); -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | build/** 7 | temp 8 | .gitignore 9 | tsconfig.json 10 | vsc-extension-quickstart.md 11 | tslint.json 12 | build.sh 13 | prepare-version.js -------------------------------------------------------------------------------- /src/debugger/base/DebugConfigurationBase.ts: -------------------------------------------------------------------------------- 1 | import { DebugConfiguration } from 'vscode'; 2 | 3 | export interface DebugConfigurationBase extends DebugConfiguration { 4 | extensionPath: string; 5 | sourcePaths: string[]; 6 | ext: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | *.vsix 5 | 6 | *.iml 7 | .gradle 8 | .idea 9 | temp 10 | /debugger/Emmy.lua 11 | /debugger/emmy/linux 12 | /debugger/emmy/mac 13 | /debugger/emmy/windows 14 | /server 15 | /package-lock.json 16 | /temp 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | /* Strict Type-Checking Option */ 13 | "strict": true, /* enable all strict type-checking options */ 14 | /* Additional Checks */ 15 | "noUnusedLocals": true /* Report errors on unused locals. */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/debugger/launch/EmmyLaunchDebuggerProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugConfigurationBase } from "../base/DebugConfigurationBase"; 3 | import { DebuggerProvider } from "../base/DebuggerProvider"; 4 | 5 | export interface EmmyLaunchDebugConfiguration extends DebugConfigurationBase { 6 | program: string; 7 | arguments: string[]; 8 | workingDir: string; 9 | blockOnExit: boolean; 10 | useWindowsTerminal: boolean; 11 | } 12 | 13 | 14 | export class EmmyLaunchDebuggerProvider extends DebuggerProvider { 15 | async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, configuration: EmmyLaunchDebugConfiguration, token?: vscode.CancellationToken): Promise { 16 | configuration.extensionPath = this.context.extensionPath; 17 | configuration.sourcePaths = this.getSourceRoots(); 18 | configuration.ext = this.getExt(); 19 | return configuration; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "--", 4 | "blockComment": ["--[[", "]]"] 5 | }, 6 | "brackets": [ 7 | ["[", "]"], 8 | ["(", ")"], 9 | ["{", "}"], 10 | ["`", "`"] 11 | ], 12 | "autoClosingPairs": [ 13 | { "open": "[", "close": "]" }, 14 | { "open": "(", "close": ")" }, 15 | { "open": "{", "close": "}", "notIn": ["string", "comment"] }, 16 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 17 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 18 | { "open": "`", "close": "`" } 19 | ], 20 | "surroundingPairs": [ 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["{", "}"], 24 | ["\"", "\""], 25 | ["'", "'"], 26 | ["`", "`"], 27 | ["<", ">"] 28 | ], 29 | "folding": { 30 | "markers": { 31 | "start": "^\\s*--region\\b", 32 | "end": "^\\s*--endregion\\b" 33 | } 34 | }, 35 | "indentationRules": { 36 | "increaseIndentPattern": "\\b(do|else|then|repeat|function)\\b((?!end).)*$", 37 | // "decreaseIndentPattern": "\\b(end|else|elseif|until)\\b" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/debugger/DebugFactory.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ProviderResult } from 'vscode'; 3 | import { EmmyNewDebugSession } from './new_debugger/EmmyNewDebugSession'; 4 | import { EmmyAttachDebugSession } from './attach/EmmyAttachDebugSession'; 5 | import { EmmyLaunchDebugSession } from './launch/EmmyLaunchDebugSession'; 6 | 7 | export class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { 8 | 9 | createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult { 10 | switch (_session.type) { 11 | case 'emmylua_attach': { 12 | return new vscode.DebugAdapterInlineImplementation(new EmmyAttachDebugSession()); 13 | } 14 | case 'emmylua_launch': { 15 | return new vscode.DebugAdapterInlineImplementation(new EmmyLaunchDebugSession()); 16 | } 17 | case 'emmylua_new': { 18 | return new vscode.DebugAdapterInlineImplementation(new EmmyNewDebugSession()); 19 | } 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lspExtension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export interface AnnotatorParams { 4 | uri: string; 5 | } 6 | 7 | export enum AnnotatorType { 8 | ReadOnlyParam, 9 | Global, 10 | ReadOnlyLocal, 11 | MutLocal, 12 | MutParam, 13 | DocEm, 14 | DocStrong, 15 | } 16 | 17 | export interface IAnnotator { 18 | ranges: vscode.Range[]; 19 | type: AnnotatorType; 20 | } 21 | 22 | export interface IProgressReport { 23 | text: string; 24 | percent: number; 25 | } 26 | 27 | export interface ServerStatusParams { 28 | health: string; 29 | message?: string; 30 | loading?: boolean; 31 | command?: string; 32 | } 33 | 34 | export interface IServerPosition { 35 | line: number; 36 | character: number; 37 | } 38 | 39 | export interface IServerRange { 40 | start: IServerPosition; 41 | end: IServerPosition; 42 | } 43 | 44 | export interface IServerLocation { 45 | uri: string; 46 | range: IServerRange; 47 | } 48 | 49 | /** 50 | * Syntax tree request parameters 51 | */ 52 | export interface SyntaxTreeParams { 53 | /** Document URI to get syntax tree for */ 54 | uri: string; 55 | /** Optional range to get syntax tree for (if not specified, entire document) */ 56 | range?: IServerRange; 57 | } 58 | 59 | /** 60 | * Syntax tree response 61 | */ 62 | export interface SyntaxTreeResponse { 63 | /** The syntax tree as text */ 64 | content: string; 65 | } 66 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | 9 | { 10 | "name": "Extension", 11 | "type": "extensionHost", 12 | "request": "launch", 13 | "runtimeExecutable": "${execPath}", 14 | "args": [ 15 | "--extensionDevelopmentPath=${workspaceFolder}" 16 | ], 17 | "outFiles": [ 18 | "${workspaceFolder}/out/**/*.js" 19 | ], 20 | "env": { 21 | "EMMY_DEV": "true" 22 | }, 23 | "preLaunchTask": "npm: watch" 24 | }, 25 | { 26 | "name": "Extension Tests", 27 | "type": "extensionHost", 28 | "request": "launch", 29 | "runtimeExecutable": "${execPath}", 30 | "args": [ 31 | "--extensionDevelopmentPath=${workspaceFolder}", 32 | "--extensionTestsPath=${workspaceFolder}/out/test" 33 | ], 34 | "outFiles": [ 35 | "${workspaceFolder}/out/test/**/*.js" 36 | ], 37 | "preLaunchTask": "npm: watch" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*.*.*" 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | include: 15 | - {target: win32-x64, file: emmylua_ls-win32-x64.zip} 16 | - {target: win32-arm64, file: emmylua_ls-win32-arm64.zip} 17 | - {target: linux-x64, file: emmylua_ls-linux-x64-glibc.2.17.tar.gz} 18 | - {target: linux-arm64, file: emmylua_ls-linux-aarch64-glibc.2.17.tar.gz} 19 | - {target: darwin-x64, file: emmylua_ls-darwin-x64.tar.gz} 20 | - {target: darwin-arm64, file: emmylua_ls-darwin-arm64.tar.gz} 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@master 26 | - name: Set node 20.x 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: 20.x 30 | - name: Install 31 | uses: borales/actions-yarn@v5 32 | with: 33 | cmd: install 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: build 37 | run: | 38 | node ./build/prepare.js ${{ matrix.file }} 39 | npx vsce package -o VSCode-EmmyLua-${{ matrix.target }}.vsix --target ${{ matrix.target }} 40 | - name: Upload 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: VSCode-EmmyLua-${{ matrix.target }} 44 | path: ${{ github.workspace }}/VSCode-EmmyLua*.vsix 45 | 46 | publish: 47 | runs-on: ubuntu-latest 48 | needs: [build] 49 | if: success() && startsWith(github.ref, 'refs/tags/') 50 | steps: 51 | - uses: actions/download-artifact@v4 52 | - run: npx vsce publish --packagePath $(find VSCode-EmmyLua* -iname "*.vsix") 53 | env: 54 | VSCE_PAT: ${{ secrets.VSCE_ACCESS_TOKEN }} 55 | -------------------------------------------------------------------------------- /src/emmyrcSchemaContentProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | /** 6 | * 提供`.emmyrc.json`的 i18n 7 | */ 8 | export class EmmyrcSchemaContentProvider implements vscode.TextDocumentContentProvider { 9 | private readonly schemaBaseDir: string; 10 | 11 | constructor(context: vscode.ExtensionContext) { 12 | this.schemaBaseDir = path.join(context.extensionPath, 'syntaxes'); 13 | } 14 | 15 | async provideTextDocumentContent(uri: vscode.Uri): Promise { 16 | const schemaIdentifier = path.posix.basename(uri.path); 17 | const locale = vscode.env.language; 18 | let schemaFileName: string; 19 | 20 | if (schemaIdentifier === 'emmyrc') { 21 | switch (locale) { 22 | case 'zh-cn': 23 | case 'zh-CN': 24 | case 'zh': 25 | schemaFileName = 'schema.zh-cn.json'; 26 | break; 27 | case 'en': 28 | case 'en-US': 29 | case 'en-GB': 30 | default: 31 | schemaFileName = 'schema.json'; 32 | break; 33 | } 34 | } else { 35 | return ''; 36 | } 37 | 38 | // 检查schema文件是否存在, 如果不存在则使用默认的 39 | let schemaFilePath = path.join(this.schemaBaseDir, schemaFileName); 40 | if (!fs.existsSync(schemaFilePath)) { 41 | schemaFilePath = path.join(this.schemaBaseDir, 'schema.json'); 42 | } 43 | 44 | try { 45 | return await fs.promises.readFile(schemaFilePath, 'utf8'); 46 | } catch (error: any) { 47 | return JSON.stringify({ 48 | "$schema": "https://json-schema.org/draft/2020-12/schema#", 49 | "title": "Error Loading Schema", 50 | "description": `Could not load schema: ${schemaFileName}. Error: ${error.message}.` 51 | }); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/debugger/base/DebugSession.ts: -------------------------------------------------------------------------------- 1 | import { LoggingDebugSession, Event, OutputEvent } from "@vscode/debugadapter"; 2 | import { DebugProtocol } from "@vscode/debugprotocol"; 3 | 4 | export abstract class DebugSession extends LoggingDebugSession { 5 | constructor() { 6 | super("emmy.debug.txt"); 7 | } 8 | 9 | protected ext: string[] = ['.lua']; 10 | private _findFileSeq = 0; 11 | private _fileCache = new Map(); 12 | 13 | log(obj: any) { 14 | this.sendEvent(new Event("log", obj)); 15 | } 16 | 17 | printConsole(msg: string, newLine: boolean = true) { 18 | if (newLine) { 19 | msg += "\n"; 20 | } 21 | this.sendEvent(new OutputEvent(msg)); 22 | } 23 | 24 | protected customRequest(command: string, response: DebugProtocol.Response, args: any): void { 25 | if (command === 'findFileRsp') { 26 | this.emit('findFileRsp', args); 27 | } 28 | else { 29 | this.emit(command); 30 | } 31 | } 32 | 33 | async findFile(file: string): Promise { 34 | const r = this._fileCache.get(file); 35 | if (r) { 36 | return r; 37 | } 38 | 39 | const seq = this._findFileSeq++; 40 | this.sendEvent(new Event('findFileReq', { file: file, ext: this.ext, seq: seq })); 41 | return new Promise((resolve, c) => { 42 | const listener = (body: any) => { 43 | if (seq === body.seq) { 44 | this.removeListener('findFileRsp', listener); 45 | const files: string[] = body.files; 46 | if (files.length > 0) { 47 | this._fileCache.set(file, files[0]); 48 | resolve(files[0]); 49 | } else { 50 | resolve(file); 51 | } 52 | } 53 | }; 54 | this.addListener('findFileRsp', listener); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/debugger/new_debugger/EmmyNewDebuggerProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { DebugConfigurationBase } from '../base/DebugConfigurationBase'; 5 | import { extensionContext } from '../../extension'; 6 | import { DebuggerProvider } from '../base/DebuggerProvider'; 7 | 8 | export interface EmmyDebugConfiguration extends DebugConfigurationBase { 9 | host: string; 10 | port: number; 11 | ideConnectDebugger: boolean; 12 | } 13 | 14 | export class EmmyNewDebuggerProvider extends DebuggerProvider { 15 | private showWaitConnectionToken = new vscode.CancellationTokenSource(); 16 | 17 | resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, debugConfiguration: EmmyDebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { 18 | debugConfiguration.extensionPath = extensionContext.vscodeContext.extensionPath; 19 | debugConfiguration.sourcePaths = this.getSourceRoots(); 20 | if (!debugConfiguration.request) { 21 | debugConfiguration.request = "launch"; 22 | debugConfiguration.type = "emmylua_new"; 23 | debugConfiguration.ideConnectDebugger = true; 24 | debugConfiguration.host = 'localhost'; 25 | debugConfiguration.port = 9966; 26 | } 27 | debugConfiguration.ext = this.getExt(); 28 | 29 | return debugConfiguration; 30 | } 31 | 32 | protected async onDebugCustomEvent(e: vscode.DebugSessionCustomEvent) { 33 | if (e.event === 'showWaitConnection') { 34 | this.showWaitConnectionToken.cancel(); 35 | this.showWaitConnectionToken = new vscode.CancellationTokenSource(); 36 | this.showWaitConnection(e.session, this.showWaitConnectionToken.token); 37 | } 38 | else if (e.event === 'onNewConnection') { 39 | this.showWaitConnectionToken.cancel(); 40 | } 41 | else { 42 | return super.onDebugCustomEvent(e); 43 | } 44 | } 45 | 46 | private async showWaitConnection(session: vscode.DebugSession, token: vscode.CancellationToken) { 47 | return vscode.window.withProgress({ 48 | location: vscode.ProgressLocation.Notification, 49 | title: 'Wait for connection.', 50 | cancellable: true 51 | }, 52 | async (progress, userCancelToken) => { 53 | userCancelToken.onCancellationRequested(e => { 54 | session.customRequest('stopWaitConnection'); 55 | }); 56 | await new Promise((r, e) => token.onCancellationRequested(r)); 57 | } 58 | ); 59 | } 60 | 61 | protected onTerminateDebugSession(session: vscode.DebugSession) { 62 | this.showWaitConnectionToken.cancel(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/debugger/base/EmmyDebugProto.ts: -------------------------------------------------------------------------------- 1 | // describe debugger proto 2 | export enum MessageCMD { 3 | Unknown, 4 | 5 | InitReq, 6 | InitRsp, 7 | 8 | ReadyReq, 9 | ReadyRsp, 10 | 11 | AddBreakPointReq, 12 | AddBreakPointRsp, 13 | 14 | RemoveBreakPointReq, 15 | RemoveBreakPointRsp, 16 | 17 | ActionReq, 18 | ActionRsp, 19 | 20 | EvalReq, 21 | EvalRsp, 22 | 23 | // lua -> ide 24 | BreakNotify, 25 | AttachedNotify, 26 | 27 | StartHookReq, 28 | StartHookRsp, 29 | 30 | LogNotify, 31 | } 32 | 33 | export interface IMessage { 34 | cmd: MessageCMD; 35 | } 36 | 37 | export enum ValueType { 38 | TNIL, 39 | TBOOLEAN, 40 | TLIGHTUSERDATA, 41 | TNUMBER, 42 | TSTRING, 43 | TTABLE, 44 | TFUNCTION, 45 | TUSERDATA, 46 | TTHREAD, 47 | 48 | GROUP, 49 | } 50 | 51 | export enum VariableNameType { 52 | NString, NNumber, NComplex, 53 | } 54 | 55 | export interface IVariable { 56 | name: string; 57 | nameType: ValueType; 58 | value: string; 59 | valueType: ValueType; 60 | valueTypeName: string; 61 | cacheId: number; 62 | children?: IVariable[]; 63 | } 64 | 65 | export interface IStack { 66 | file: string; 67 | line: number; 68 | functionName: string; 69 | level: number; 70 | localVariables: IVariable[]; 71 | upvalueVariables: IVariable[]; 72 | } 73 | 74 | export interface IBreakPoint { 75 | file: string; 76 | line: number; 77 | /** An optional expression for conditional breakpoints. */ 78 | condition?: string; 79 | /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ 80 | hitCondition?: string; 81 | /** If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. */ 82 | logMessage?: string; 83 | } 84 | 85 | export interface IInitReq extends IMessage { 86 | emmyHelper: string; 87 | ext: string[]; 88 | } 89 | export interface InitRsp { 90 | version: string; 91 | } 92 | 93 | // add breakpoint 94 | export interface IAddBreakPointReq extends IMessage { 95 | breakPoints: IBreakPoint[]; 96 | clear: boolean; 97 | } 98 | export interface IAddBreakPointRsp { 99 | } 100 | 101 | // remove breakpoint 102 | export interface IRemoveBreakPointReq { 103 | breakPoints: IBreakPoint[]; 104 | } 105 | export interface IRemoveBreakPointRsp { 106 | } 107 | 108 | export enum DebugAction { 109 | Break, 110 | Continue, 111 | StepOver, 112 | StepIn, 113 | StepOut, 114 | Stop, 115 | } 116 | 117 | // break, continue, step over, step into, step out, stop 118 | export interface IActionReq extends IMessage { 119 | action: DebugAction; 120 | } 121 | export interface IActionRsp { 122 | } 123 | 124 | // on break 125 | export interface IBreakNotify { 126 | stacks: IStack[]; 127 | } 128 | 129 | export interface IEvalReq extends IMessage { 130 | seq: number; 131 | expr: string; 132 | stackLevel: number; 133 | depth: number; 134 | cacheId: number; 135 | value?: string; 136 | setValue?: boolean; 137 | } 138 | 139 | export interface IEvalRsp { 140 | seq: number; 141 | success: boolean; 142 | error: string; 143 | value: IVariable; 144 | } -------------------------------------------------------------------------------- /src/debugger/new_debugger/EmmyNewDebugSession.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import { EmmyDebugSession } from "../base/EmmyDebugSession"; 3 | import { Event, OutputEvent, TerminatedEvent } from "@vscode/debugadapter"; 4 | import { DebugProtocol } from "@vscode/debugprotocol"; 5 | 6 | export interface EmmyNewDebugArguments extends DebugProtocol.AttachRequestArguments { 7 | extensionPath: string; 8 | sourcePaths: string[]; 9 | host: string; 10 | port: number; 11 | ext: string[]; 12 | ideConnectDebugger: boolean; 13 | } 14 | 15 | export class EmmyNewDebugSession extends EmmyDebugSession { 16 | private listenMode = false; 17 | private socket: net.Server | undefined; 18 | 19 | protected launchRequest(response: DebugProtocol.LaunchResponse, args: EmmyNewDebugArguments): void { 20 | this.ext = args.ext; 21 | this.extensionPath = args.extensionPath; 22 | if (!args.ideConnectDebugger) { 23 | this.listenMode = true; 24 | const socket = net.createServer(client => { 25 | this.client = client; 26 | this.sendResponse(response); 27 | this.onConnect(this.client); 28 | this.readClient(client); 29 | this.sendEvent(new Event('onNewConnection')); 30 | }) 31 | .listen(args.port, args.host) 32 | .on('listening', () => { 33 | this.sendEvent(new OutputEvent(`Server(${args.host}:${args.port}) open successfully, wait for connection...\n`)); 34 | }) 35 | .on('error', err => { 36 | this.sendEvent(new OutputEvent(`${err}`, 'stderr')); 37 | response.success = false; 38 | response.message = `${err}`; 39 | this.sendResponse(response); 40 | }); 41 | this.socket = socket; 42 | this.sendEvent(new Event('showWaitConnection')); 43 | } 44 | else { 45 | // send resp 46 | const client = net.connect(args.port, args.host) 47 | .on('connect', () => { 48 | this.sendResponse(response); 49 | this.onConnect(client); 50 | this.readClient(client); 51 | }) 52 | .on('error', err => { 53 | response.success = false; 54 | response.message = `${err}`; 55 | this.sendResponse(response); 56 | }); 57 | } 58 | } 59 | 60 | protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments) { 61 | super.disconnectRequest(response, args); 62 | setTimeout(() => { 63 | if (this.socket) { 64 | this.socket.close(); 65 | this.socket = undefined; 66 | } 67 | if (this.client) { 68 | this.client.end(); 69 | this.client = undefined; 70 | } 71 | }, 1000); 72 | } 73 | 74 | protected onSocketClose() { 75 | if (this.client) { 76 | this.client.removeAllListeners(); 77 | } 78 | this.sendEvent(new OutputEvent('Disconnected.\n')); 79 | if (this.listenMode) { 80 | this.client = undefined; 81 | } else { 82 | this.sendEvent(new TerminatedEvent()); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/debugger/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { extensionContext } from "../extension"; 4 | import { EmmyNewDebuggerProvider } from "./new_debugger/EmmyNewDebuggerProvider"; 5 | import { EmmyAttachDebuggerProvider } from "./attach/EmmyAttachDebuggerProvider"; 6 | import { EmmyLaunchDebuggerProvider } from "./launch/EmmyLaunchDebuggerProvider"; 7 | import { InlineDebugAdapterFactory } from "./DebugFactory"; 8 | 9 | 10 | /** 11 | * Debugger configuration interface 12 | */ 13 | interface DebuggerConfig { 14 | readonly type: string; 15 | readonly provider: vscode.DebugConfigurationProvider; 16 | } 17 | 18 | 19 | export async function insertEmmyDebugCode() { 20 | const context = extensionContext.vscodeContext; 21 | const activeEditor = vscode.window.activeTextEditor; 22 | if (!activeEditor) { 23 | return; 24 | } 25 | const document = activeEditor.document; 26 | if (document.languageId !== "lua") { 27 | return; 28 | } 29 | 30 | let dllPath = ""; 31 | const isWindows = process.platform === "win32"; 32 | const isMac = process.platform === "darwin"; 33 | const isLinux = process.platform === "linux"; 34 | if (isWindows) { 35 | const arch = await vscode.window.showQuickPick(["x64", "x86"]); 36 | if (!arch) { 37 | return; 38 | } 39 | dllPath = path.join( 40 | context.extensionPath, 41 | `debugger/emmy/windows/${arch}/?.dll` 42 | ); 43 | } else if (isMac) { 44 | const arch = await vscode.window.showQuickPick(["x64", "arm64"]); 45 | if (!arch) { 46 | return; 47 | } 48 | dllPath = path.join( 49 | context.extensionPath, 50 | `debugger/emmy/mac/${arch}/emmy_core.dylib` 51 | ); 52 | } else if (isLinux) { 53 | dllPath = path.join( 54 | context.extensionPath, 55 | `debugger/emmy/linux/emmy_core.so` 56 | ); 57 | } 58 | 59 | const host = "localhost"; 60 | const port = 9966; 61 | const ins = new vscode.SnippetString(); 62 | ins.appendText( 63 | `package.cpath = package.cpath .. ";${dllPath.replace(/\\/g, "/")}"\n` 64 | ); 65 | ins.appendText(`local dbg = require("emmy_core")\n`); 66 | ins.appendText(`dbg.tcpListen("${host}", ${port})`); 67 | activeEditor.insertSnippet(ins); 68 | } 69 | 70 | export function registerDebuggers(): void { 71 | const context = extensionContext.vscodeContext; 72 | 73 | const debuggerConfigs: DebuggerConfig[] = [ 74 | { type: 'emmylua_new', provider: new EmmyNewDebuggerProvider('emmylua_new', context) }, 75 | { type: 'emmylua_attach', provider: new EmmyAttachDebuggerProvider('emmylua_attach', context) }, 76 | { type: 'emmylua_launch', provider: new EmmyLaunchDebuggerProvider('emmylua_launch', context) }, 77 | ]; 78 | 79 | debuggerConfigs.forEach(({ type, provider }) => { 80 | context.subscriptions.push( 81 | vscode.debug.registerDebugConfigurationProvider(type, provider) 82 | ); 83 | 84 | context.subscriptions.push(provider as vscode.Disposable); 85 | }); 86 | 87 | if (extensionContext.debugMode) { 88 | const factory = new InlineDebugAdapterFactory(); 89 | debuggerConfigs.forEach(({ type }) => { 90 | context.subscriptions.push( 91 | vscode.debug.registerDebugAdapterDescriptorFactory(type, factory) 92 | ); 93 | }); 94 | } 95 | } -------------------------------------------------------------------------------- /src/debugger/attach/EmmyAttachDebuggerProvider.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'path'; 2 | import * as vscode from 'vscode'; 3 | import * as cp from "child_process"; 4 | import * as iconv from 'iconv-lite'; 5 | import { DebugConfigurationBase } from "../base/DebugConfigurationBase"; 6 | import { DebuggerProvider } from "../base/DebuggerProvider"; 7 | 8 | interface ProcessInfoItem extends vscode.QuickPickItem { 9 | pid: number; 10 | } 11 | 12 | export interface EmmyAttachDebugConfiguration extends DebugConfigurationBase { 13 | pid: number; 14 | processName: string; 15 | } 16 | 17 | 18 | export class EmmyAttachDebuggerProvider extends DebuggerProvider { 19 | async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, configuration: EmmyAttachDebugConfiguration, token?: vscode.CancellationToken): Promise { 20 | configuration.extensionPath = this.context.extensionPath; 21 | configuration.sourcePaths = this.getSourceRoots(); 22 | configuration.request = "attach"; 23 | configuration.type = "emmylua_attach"; 24 | configuration.ext = this.getExt(); 25 | configuration.processName = configuration.processName ?? "" 26 | if (configuration.pid > 0) { 27 | return configuration; 28 | } 29 | 30 | const pid = await this.pickPID(configuration.processName); 31 | configuration.pid = pid; 32 | return configuration; 33 | } 34 | 35 | private async pickPID(processName: string) { 36 | return new Promise((resolve, reject) => { 37 | const args = [`"${this.context.extensionPath}/debugger/emmy/windows/x86/emmy_tool.exe"`, "list_processes"]; 38 | cp.exec(args.join(" "), { encoding: 'buffer' }, (_err, stdout, _stderr) => { 39 | const str = iconv.decode(stdout, "cp936"); 40 | const arr = str.split('\r\n'); 41 | const size = Math.floor(arr.length / 4); 42 | const items: ProcessInfoItem[] = []; 43 | for (let i = 0; i < size; i++) { 44 | const pid = parseInt(arr[i * 4]); 45 | const title = arr[i * 4 + 1]; 46 | const path = arr[i * 4 + 2]; 47 | const name = basename(path); 48 | const item: ProcessInfoItem = { 49 | pid: pid, 50 | label: `${pid} : ${name}`, 51 | description: title, 52 | detail: path 53 | }; 54 | if (processName.length === 0 55 | || title.indexOf(processName) !== -1 56 | || name.indexOf(processName) !== -1) { 57 | items.push(item); 58 | } 59 | } 60 | if (items.length > 1) { 61 | vscode.window.showQuickPick(items, { 62 | matchOnDescription: true, 63 | matchOnDetail: true, 64 | placeHolder: "Select the process to attach" 65 | }).then((item: ProcessInfoItem | undefined) => { 66 | if (item) { 67 | resolve(item.pid); 68 | } else { 69 | reject(); 70 | } 71 | }); 72 | } else if (items.length == 1) { 73 | resolve(items[0].pid); 74 | } else { 75 | vscode.window.showErrorMessage("No process for attach") 76 | reject(); 77 | } 78 | 79 | }).on("error", error => reject); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.nls.zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "config.common.deprecated": "This option is deprecated", 3 | "config.common.enum.default.description": "使用 `.emmyrc.json` 中的值,或回退到默认值", 4 | "config.common.enum.off.description": "覆盖 `.emmyrc.json` 并禁用此选项", 5 | "config.common.enum.on.description": "覆盖 `.emmyrc.json` 并启用此选项", 6 | "config.emmylua.codeAction.insertSpace.description": "在插入 `@diagnostic disable-next-line` 时,在 `---` 注释后添加空格。", 7 | "config.emmylua.codeLens.enable.description": "启用代码透镜。", 8 | "config.emmylua.colors.global.description": "如果不为空,用指定颜色高亮全局变量", 9 | "config.emmylua.colors.local.description": "如果不为空,用指定颜色高亮局部变量", 10 | "config.emmylua.colors.mutableUnderline.description": "如果变量在作用域内被重新赋值,则为其添加下划线", 11 | "config.emmylua.colors.parameter.description": "如果不为空,用指定颜色高亮函数参数", 12 | "config.emmylua.completion.autoRequire.description": "自动插入 `require` 调用,当自动补全插入来自其他模块的对象时。", 13 | "config.emmylua.completion.baseFunctionIncludesName.description": "是否在基础函数补全中包含名称。效果:`function () end` -> `function name() end`。", 14 | "config.emmylua.completion.enable.description": "启用自动补全。", 15 | "config.emmylua.completion.postfix.description": "用于触发后缀自动补全的符号。", 16 | "config.emmylua.diagnostics.diagnosticInterval.description": "打开/更改文件与扫描错误之间的延迟(毫秒)。", 17 | "config.emmylua.documentColor.enable.description": "启用解析字符串中的颜色标签,并在旁边显示颜色选择器。", 18 | "config.emmylua.hint.enable.description": "启用内联提示。", 19 | "config.emmylua.hint.enumParamHint.description": "当向期望枚举的函数传递字面值时,显示枚举成员名称。\n\n示例:\n\n```lua\n--- @enum Level\nlocal Foo = {\n Info = 1,\n Error = 2,\n}\n\n--- @param l Level\nfunction print_level(l) end\n\nprint_level(1 --[[ 提示: Level.Info ]])\n```", 20 | "config.emmylua.hint.indexHint.description": "显示数组索引名称。\n\n示例:\n\n```lua\nlocal array = {\n [1] = 1, -- [name]\n}\n\nprint(array[1] --[[ 提示: name ]])\n```", 21 | "config.emmylua.hint.localHint.description": "显示局部变量类型。", 22 | "config.emmylua.hint.metaCallHint.description": "当调用对象会触发其元表的 `__call` 函数时显示提示。", 23 | "config.emmylua.hint.overrideHint.description": "显示重写基类函数的方法。", 24 | "config.emmylua.hint.paramHint.description": "在函数调用中显示参数名,在函数定义中显示参数类型。", 25 | "config.emmylua.hover.enable.description": "启用悬停显示文档。", 26 | "config.emmylua.inlineValues.enable.description": "调试时显示内联值。", 27 | "config.emmylua.language.completeAnnotation.description": "在注解后换行时自动插入 `\"---\"`", 28 | "config.emmylua.ls.executablePath.description": "VSCode中指定可执行文件路径", 29 | "config.emmylua.ls.globalConfigPath.description": "指定全局配置文件的路径", 30 | "config.emmylua.ls.startParameters.description": "传递给EmmyLua语言服务器的附加参数", 31 | "config.emmylua.references.enable.description": "启用符号用法搜索。", 32 | "config.emmylua.references.fuzzySearch.description": "当普通搜索未找到任何内容时,使用模糊搜索查找符号用法。", 33 | "config.emmylua.references.shortStringSearch.description": "也在字符串中搜索用法。", 34 | "config.emmylua.semanticTokens.enable.description": "启用语义标记。", 35 | "config.emmylua.semanticTokens.renderDocumentationMarkup.description": "Render Markdown/RST in documentation. Set `doc.syntax` for this option to have effect.", 36 | "config.emmylua.workspace.enableReindex.description": "更改文件后启用整个项目的重新索引。", 37 | "config.emmylua.workspace.reindexDuration.description": "更改文件与整个项目重新索引之间的延迟(毫秒)。", 38 | "config.lua.trace.server.description": "在输出视图中跟踪 VSCode 与 EmmyLua 语言服务器之间的通信。默认值为 `off`", 39 | "debug.attach.captureLog": "捕获进程输出,显示在windows terminal上,该特性可能引发进程崩溃", 40 | "debug.attach.desc": "附加进程调试", 41 | "debug.attach.label": "EmmyLua: 通过进程ID附加", 42 | "debug.attach.name": "通过进程ID附加", 43 | "debug.attach.target_pid": "目标进程ID", 44 | "debug.attach.target_process": "通过名称过滤进程,如果目标唯一则直接附加", 45 | "debug.launch.arguments": "传递给可执行文件的参数", 46 | "debug.launch.blockOnExit": "进程结束时是否阻塞进程保持窗口开启", 47 | "debug.launch.desc": "启动程序,并使用附加调试器附加到进程调试", 48 | "debug.launch.label": "EmmyLua: 启动并附加程序", 49 | "debug.launch.name": "启动并附加程序", 50 | "debug.launch.newWindow": "emmylua会创建一个新窗口展示这个进程", 51 | "debug.launch.program": "启动并附加的可执行文件", 52 | "debug.launch.workingDir": "设置工作区" 53 | } 54 | -------------------------------------------------------------------------------- /src/configRenames.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Configuration key rename mappings 5 | * Maps new keys to old keys for backward compatibility 6 | */ 7 | const CONFIG_RENAMES: ReadonlyMap = new Map([ 8 | ['emmylua.ls.executablePath', 'emmylua.misc.executablePath'], 9 | ['emmylua.ls.globalConfigPath', 'emmylua.misc.globalConfigPath'], 10 | ['emmylua.language.completeAnnotation', 'emmylua.misc.autoInsertTripleDash'], 11 | ]); 12 | 13 | /** 14 | * Track which deprecated configs have been warned about 15 | */ 16 | const warnedDeprecations = new Set(); 17 | 18 | /** 19 | * Get configuration value with support for renamed keys 20 | * Automatically falls back to old configuration keys for backward compatibility 21 | * 22 | * @param config - Workspace configuration 23 | * @param key - Configuration key to retrieve 24 | * @param defaultValue - Optional default value if config doesn't exist 25 | * @returns Configuration value or default 26 | */ 27 | export function get( 28 | config: vscode.WorkspaceConfiguration, 29 | key: string, 30 | defaultValue?: T 31 | ): T | undefined { 32 | const oldKey = CONFIG_RENAMES.get(key); 33 | 34 | // Check if old config exists and has a non-null value 35 | if (oldKey && config.has(oldKey)) { 36 | const oldValue = config.get(oldKey); 37 | if (oldValue !== undefined && oldValue !== null) { 38 | // Warn about deprecated config (only once per session) 39 | if (!warnedDeprecations.has(oldKey)) { 40 | warnedDeprecations.add(oldKey); 41 | showDeprecationWarning(oldKey, key); 42 | } 43 | return oldValue; 44 | } 45 | } 46 | 47 | // Get from new config key 48 | return config.get(key, defaultValue as T); 49 | } 50 | 51 | /** 52 | * Show a deprecation warning for old configuration keys 53 | */ 54 | function showDeprecationWarning(oldKey: string, newKey: string): void { 55 | const message = `Configuration "${oldKey}" is deprecated. Please use "${newKey}" instead.`; 56 | 57 | vscode.window.showWarningMessage( 58 | message, 59 | 'Update Now', 60 | 'Dismiss' 61 | ).then(action => { 62 | if (action === 'Update Now') { 63 | vscode.commands.executeCommand('workbench.action.openSettings', newKey); 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Get configuration with proper typing and defaults 70 | */ 71 | export class ConfigurationManager { 72 | private readonly config: vscode.WorkspaceConfiguration; 73 | 74 | constructor(scope?: vscode.ConfigurationScope) { 75 | this.config = vscode.workspace.getConfiguration('emmylua', scope); 76 | } 77 | 78 | /** 79 | * Get a configuration value with type safety 80 | */ 81 | get(section: string, defaultValue?: T): T | undefined { 82 | return get(this.config, `${section}`, defaultValue) || get(this.config, `emmylua.${section}`, defaultValue); 83 | } 84 | 85 | /** 86 | * Get language server executable path 87 | */ 88 | getExecutablePath(): string | undefined { 89 | return this.get('misc.executablePath'); 90 | } 91 | 92 | /** 93 | * Get language server global config path 94 | */ 95 | getGlobalConfigPath(): string | undefined { 96 | return this.get('misc.globalConfigPath'); 97 | } 98 | 99 | /** 100 | * Get language server start parameters 101 | */ 102 | getStartParameters(): string[] { 103 | return this.get('ls.startParameters', []) || []; 104 | } 105 | 106 | /** 107 | * Get language server debug port 108 | */ 109 | getDebugPort(): number | null { 110 | return this.get('ls.debugPort', null) || null; 111 | } 112 | 113 | /** 114 | * Get color configuration 115 | */ 116 | getColor(colorType: string): string | undefined { 117 | return this.get(`colors.${colorType}`); 118 | } 119 | 120 | /** 121 | * Check if mutable variables should have underline 122 | */ 123 | useMutableUnderline(): boolean { 124 | return this.get('colors.mutableUnderline', false) || false; 125 | } 126 | 127 | /** 128 | * Check if auto-complete annotation is enabled 129 | */ 130 | isCompleteAnnotationEnabled(): boolean { 131 | return this.get('language.completeAnnotation', true) ?? true; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/debugger/attach/EmmyAttachDebugSession.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import * as cp from "child_process"; 3 | import * as proto from "../base/EmmyDebugProto"; 4 | import { EmmyDebugSession } from "../base/EmmyDebugSession"; 5 | import { OutputEvent } from "@vscode/debugadapter"; 6 | import { DebugProtocol } from "@vscode/debugprotocol"; 7 | 8 | 9 | interface EmmyAttachDebugArguments extends DebugProtocol.AttachRequestArguments { 10 | extensionPath: string; 11 | sourcePaths: string[]; 12 | ext: string[]; 13 | pid: number; 14 | captureLog?: boolean; 15 | } 16 | 17 | enum WinArch { 18 | X86, X64 19 | } 20 | 21 | export class EmmyAttachDebugSession extends EmmyDebugSession { 22 | 23 | private pid = 0; 24 | private captureLog?: boolean; 25 | 26 | private getPort(pid: number): number { 27 | var port = pid; 28 | while (port > 0xffff) { port -= 0xffff; } 29 | while (port < 0x400) { port += 0x400; } 30 | return port; 31 | } 32 | 33 | async attachRequest(response: DebugProtocol.AttachResponse, args: EmmyAttachDebugArguments) { 34 | this.extensionPath = args.extensionPath; 35 | this.ext = args.ext; 36 | this.pid = args.pid; 37 | this.captureLog = args.captureLog; 38 | 39 | await this.attach(); 40 | 41 | // send resp 42 | const client = net.connect(this.getPort(this.pid), "localhost") 43 | .on('connect', () => { 44 | this.sendResponse(response); 45 | this.onConnect(client); 46 | this.readClient(client); 47 | this.sendMessage({ cmd: proto.MessageCMD.StartHookReq }); 48 | }) 49 | .on('error', err => { 50 | response.success = false; 51 | response.message = `${err}`; 52 | this.sendResponse(response); 53 | }); 54 | this.client = client; 55 | } 56 | 57 | private async detectArch(): Promise { 58 | const cwd = `${this.extensionPath}/debugger/emmy/windows/x86`; 59 | const args = [ 60 | 'emmy_tool.exe', 61 | 'arch_pid', 62 | `${this.pid}` 63 | ]; 64 | 65 | return new Promise((r, c) => { 66 | cp.exec(args.join(" "), { cwd: cwd }) 67 | .on('close', (code) => { 68 | r(code === 0 ? WinArch.X64 : WinArch.X86); 69 | }) 70 | .on('error', c); 71 | }); 72 | } 73 | 74 | private async attach(): Promise { 75 | const arch = await this.detectArch(); 76 | const archName = arch === WinArch.X64 ? 'x64' : 'x86'; 77 | const cwd = `${this.extensionPath}/debugger/emmy/windows/${archName}`; 78 | const args = [ 79 | 'emmy_tool.exe', 80 | 'attach', 81 | '-p', 82 | `${this.pid}`, 83 | '-dir', 84 | `"${cwd}"`, 85 | '-dll', 86 | 'emmy_hook.dll' 87 | ]; 88 | if (this.captureLog) { 89 | args.push("-capture-log"); 90 | } 91 | 92 | return new Promise((r, c) => { 93 | cp.exec(args.join(" "), { cwd: cwd }, (err, stdout, stderr) => { 94 | this.sendEvent(new OutputEvent(stdout)); 95 | }) 96 | .on('close', (code) => { 97 | if (code === 0) { 98 | if (this.captureLog) { 99 | const captureArgs = [ 100 | "emmy_tool.exe", 101 | "receive_log", 102 | "-p", 103 | `${this.pid}`, 104 | ] 105 | cp.spawn(`wt`, captureArgs, { 106 | cwd: cwd 107 | }); 108 | } 109 | r(); 110 | } 111 | else { 112 | c(`Exit code = ${code}`); 113 | } 114 | }); 115 | }); 116 | } 117 | 118 | protected handleDebugMessage(cmd: proto.MessageCMD, msg: any) { 119 | switch (cmd) { 120 | case proto.MessageCMD.AttachedNotify: 121 | const n: number = msg.state; 122 | this.sendEvent(new OutputEvent(`Attached to lua state 0x${n.toString(16)}\n`)); 123 | break; 124 | case proto.MessageCMD.LogNotify: 125 | this.sendEvent(new OutputEvent(`${msg.message}\n`)); 126 | break; 127 | } 128 | super.handleDebugMessage(cmd, msg); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/debugger/base/DebuggerProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | const isWin = process.platform === "win32"; 5 | 6 | function isAbsolutePath(strPath: string): boolean { 7 | if (isWin) { 8 | if ((strPath.startsWith("\\") && !strPath.startsWith("\\\\")) || (strPath.startsWith("/") && !strPath.startsWith("//"))) 9 | strPath = strPath.substring(1); 10 | } 11 | return path.isAbsolute(strPath); 12 | } 13 | 14 | export abstract class DebuggerProvider implements vscode.DebugConfigurationProvider, vscode.Disposable { 15 | constructor( 16 | protected type: string, 17 | protected context: vscode.ExtensionContext 18 | ) { 19 | context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent(e => { 20 | if (e.session.type === this.type) { 21 | this.onDebugCustomEvent(e); 22 | } 23 | })); 24 | context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(e => this.onTerminateDebugSession(e))); 25 | } 26 | 27 | protected isNullOrEmpty(s?: string): boolean { 28 | return !s || s.trim().length === 0; 29 | } 30 | 31 | protected getSourceRoots(): string[] { 32 | const workspaceFolders = vscode.workspace.workspaceFolders || []; 33 | const list = workspaceFolders.map(f => { return f.uri.fsPath; }); 34 | const config = >vscode.workspace.getConfiguration("emmylua").get("source.roots") || []; 35 | return list.concat(config.map(item => { return path.normalize(item); })); 36 | } 37 | 38 | protected getExt(): string[] { 39 | const ext = ['.lua']; 40 | const associations: any = vscode.workspace.getConfiguration("files").get("associations"); 41 | for (const key in associations) { 42 | if (associations.hasOwnProperty(key)) { 43 | const element = associations[key]; 44 | if (element === 'lua' && key.startsWith('*.')) { 45 | ext.push(key.substring(1)); 46 | } 47 | } 48 | } 49 | return ext; 50 | } 51 | 52 | abstract resolveDebugConfiguration?(folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult; 53 | 54 | protected async onDebugCustomEvent(e: vscode.DebugSessionCustomEvent) { 55 | if (e.event === 'findFileReq') { 56 | const file: string = e.body.file; 57 | const exts: string[] = e.body.ext; 58 | const seq = e.body.seq; 59 | let fileNames = []; 60 | for (const ext of exts) { 61 | if (file.endsWith(ext)) { 62 | fileNames.push(file); 63 | break; 64 | } 65 | } 66 | if (fileNames.length === 0) { 67 | fileNames = exts.map(it => `${file}${it}`); 68 | } 69 | 70 | let results: string[] = []; 71 | 72 | for (const fileName of fileNames) { 73 | if (isAbsolutePath(fileName)) { 74 | results = [fileName]; 75 | break; 76 | } 77 | 78 | // 在当前工作区下? 79 | let uris = await vscode.workspace.findFiles(fileName); 80 | 81 | // 在当前工作区的子目录下? 82 | if (uris.length === 0) { 83 | uris = await vscode.workspace.findFiles(path.join("**/*", fileName)); 84 | } 85 | 86 | // chunkname长度超过当前工作区 87 | if (uris.length === 0) { 88 | let parts = fileName.split(/\\|\//); 89 | while (parts.length >= 2) { 90 | parts = parts.slice(1); 91 | const matchFile = path.join("**", ...parts) 92 | uris = await vscode.workspace.findFiles(matchFile, null, 10); 93 | if (uris.length !== 0) { 94 | break; 95 | } 96 | } 97 | } 98 | 99 | if (uris.length !== 0) { 100 | results = uris.map(it => it.fsPath); 101 | break; 102 | } 103 | } 104 | 105 | if (results.length !== 0) { 106 | results = results.sort((a, b) => a.length - b.length) 107 | } 108 | 109 | e.session.customRequest('findFileRsp', { files: results, seq: seq }); 110 | } 111 | } 112 | 113 | protected onTerminateDebugSession(session: vscode.DebugSession) { 114 | } 115 | 116 | dispose() { } 117 | } 118 | -------------------------------------------------------------------------------- /src/languageConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { LanguageConfiguration, IndentAction, IndentationRule, OnEnterRule } from 'vscode'; 2 | import { ConfigurationManager } from './configRenames'; 3 | 4 | /** 5 | * Lua language configuration for VS Code 6 | * Provides indentation rules, on-enter rules, and word patterns for Lua 7 | */ 8 | export class LuaLanguageConfiguration implements LanguageConfiguration { 9 | public readonly onEnterRules: OnEnterRule[]; 10 | public readonly indentationRules: IndentationRule; 11 | public readonly wordPattern: RegExp; 12 | 13 | constructor() { 14 | // Configure annotation completion rules based on user settings 15 | const configManager = new ConfigurationManager(); 16 | const completeAnnotation = configManager.isCompleteAnnotationEnabled(); 17 | 18 | this.onEnterRules = this.buildOnEnterRules(completeAnnotation); 19 | this.indentationRules = this.buildIndentationRules(); 20 | this.wordPattern = this.buildWordPattern(); 21 | } 22 | 23 | /** 24 | * Build on-enter rules for auto-indentation and annotation completion 25 | */ 26 | private buildOnEnterRules(enableAnnotationCompletion: boolean): OnEnterRule[] { 27 | const baseRules: OnEnterRule[] = [ 28 | // Function with end block 29 | { 30 | beforeText: /^\s*function\s+\w*\s*\(.*\)\s*$/, 31 | afterText: /^\s*end\s*$/, 32 | action: { 33 | indentAction: IndentAction.IndentOutdent, 34 | appendText: '\t' 35 | } 36 | }, 37 | // Local function assignment with end block 38 | { 39 | beforeText: /^\s*local\s+\w+\s*=\s*function\s*\(.*\)\s*$/, 40 | afterText: /^\s*end\s*$/, 41 | action: { 42 | indentAction: IndentAction.IndentOutdent, 43 | appendText: '\t' 44 | } 45 | }, 46 | // Anonymous function assignment with end block 47 | { 48 | beforeText: /^\s*.*\s*=\s*function\s*\(.*\)\s*$/, 49 | afterText: /^\s*end\s*$/, 50 | action: { 51 | indentAction: IndentAction.IndentOutdent, 52 | appendText: '\t' 53 | } 54 | }, 55 | // Local function declaration with end block 56 | { 57 | beforeText: /^\s*local\s+function\s+\w*\s*\(.*\)\s*$/, 58 | afterText: /^\s*end\s*$/, 59 | action: { 60 | indentAction: IndentAction.IndentOutdent, 61 | appendText: '\t' 62 | } 63 | } 64 | ]; 65 | 66 | // Add annotation completion rules if enabled 67 | if (enableAnnotationCompletion) { 68 | const annotationRules: OnEnterRule[] = [ 69 | // Continue annotation with space (---) 70 | { 71 | beforeText: /^---\s+/, 72 | action: { 73 | indentAction: IndentAction.None, 74 | appendText: '--- ' 75 | } 76 | }, 77 | // Continue annotation without space (---) 78 | { 79 | beforeText: /^---$/, 80 | action: { 81 | indentAction: IndentAction.None, 82 | appendText: '---' 83 | } 84 | } 85 | ]; 86 | 87 | return [...annotationRules, ...baseRules]; 88 | } 89 | 90 | return baseRules; 91 | } 92 | 93 | /** 94 | * Build indentation rules for Lua syntax 95 | */ 96 | private buildIndentationRules(): IndentationRule { 97 | return { 98 | // Increase indent after these patterns (excluding single-line constructs) 99 | increaseIndentPattern: /^\s*((function\s+\w*\s*\(.*\)\s*$)|(local\s+function\s+\w*\s*\(.*\)\s*$)|(local\s+\w+\s*=\s*function\s*\(.*\)\s*$)|(.*\s*=\s*function\s*\(.*\)\s*$)|(if\s+.*\s+then\s*$)|(elseif\s+.*\s+then\s*$)|(else\s*$)|(for\s+.*\s+do\s*$)|(while\s+.*\s+do\s*$)|(repeat\s*$)|(do\s*$))/, 100 | 101 | // Decrease indent for these patterns 102 | decreaseIndentPattern: /^\s*(else|elseif|end|until)\b/, 103 | 104 | // Indent next line after these patterns 105 | indentNextLinePattern: /^\s*(local\s+\w+\s*=\s*function\s*\(.*\)\s*$|function\s+\w*\s*\(.*\)\s*$|.*\s*do\s*$|.*\s*=\s*function\s*\(.*\)\s*$)/, 106 | 107 | // Don't change indent for these lines (comments) 108 | unIndentedLinePattern: /^\s*(--.*|.*\*\/)$/ 109 | }; 110 | } 111 | 112 | /** 113 | * Build word pattern for Lua 114 | * Matches strings, numbers, and identifiers 115 | */ 116 | private buildWordPattern(): RegExp { 117 | return /((?<=')[^']+(?='))|((?<=")[^"]+(?="))|(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\-\s]+)/g; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EmmyLua for VSCode 2 | 3 | ![logo](/res/logo.png) 4 | 5 | EmmyLua is a powerful Lua language support extension for Visual Studio Code, providing intelligent code completion, debugging, and analysis capabilities. 6 | 7 | ## 📋 Quick Links 8 | 9 | - 📖 [Documentation](https://github.com/EmmyLuaLs/emmylua-analyzer-rust/blob/main/docs/config/emmyrc_json_EN.md) 10 | - 📝 [Changelog (English)](CHANGELOG.md) 11 | - 📝 [更新日志 (中文)](CHANGELOG_CN.md) 12 | - 🔧 [Language Server (Rust)](https://github.com/CppCXY/emmylua-analyzer-rust) 13 | - 💬 QQ Group: `29850775` 14 | 15 | [![Online EmmyLua Doc](https://img.shields.io/badge/emmy-doc-46BC99.svg?style=flat-square)](https://emmylua.github.io) 16 | [![donate](https://img.shields.io/badge/donate-emmy-FF69B4.svg?style=flat-square)](https://emmylua.github.io/donate.html) 17 | [![加入QQ群](https://img.shields.io/badge/chat-QQ群-46BC99.svg?style=flat-square)](//shang.qq.com/wpa/qunwpa?idkey=f1acce081c45fbb5670ed5f880f7578df7a8b84caa5d2acec230ac957f0c1716) 18 | 19 | ## 🚀 Features 20 | 21 | - **Smart Code Completion**: Intelligent auto-completion with type inference 22 | - **Real-time Diagnostics**: Error detection and warnings as you type 23 | - **Advanced Debugging**: Support for attach, launch, and remote debugging 24 | - **Cross-platform**: Works on Windows, macOS, and Linux 25 | - **LSP-based**: Built on Language Server Protocol for reliability 26 | 27 | ## 📦 Related Extensions 28 | 29 | Enhance your Lua development experience with these complementary extensions: 30 | 31 | - [EmmyLuaCodeStyle](https://marketplace.visualstudio.com/items?itemName=CppCXY.emmylua-codestyle) - Code formatting and style enforcement 32 | - [EmmyLuaUnity](https://marketplace.visualstudio.com/items?itemName=CppCXY.emmylua-unity) - Unity3D integration 33 | 34 | ## 🔧 Configuration 35 | 36 | ### Project Configuration 37 | 38 | Create a `.emmyrc.json` file in your project root to customize behavior: 39 | 40 | ```json 41 | { 42 | "diagnostics": { 43 | "undefined-global": false 44 | } 45 | } 46 | ``` 47 | 48 | For detailed configuration options, see: 49 | - [English Documentation](https://github.com/CppCXY/emmylua-analyzer-rust/blob/main/docs/config/emmyrc_json_EN.md) 50 | - [中文文档](https://github.com/CppCXY/emmylua-analyzer-rust/blob/main/docs/config/emmyrc_json_CN.md) 51 | 52 | ## 🐛 Debugging 53 | 54 | ### Remote Debug Setup 55 | 56 | 1. **Insert Debugger Code** 57 | - Use command: `EmmyLua: Insert Emmy Debugger Code` 58 | - Or manually add: 59 | ```lua 60 | package.cpath = package.cpath .. ";path/to/emmy/debugger/?.dll" 61 | local dbg = require('emmy_core') 62 | dbg.tcpListen('localhost', 9966) 63 | dbg.waitIDE() 64 | ``` 65 | 66 | 2. **Set Breakpoints** 67 | - Add `dbg.breakHere()` where you want to pause execution 68 | - Or use VSCode's built-in breakpoint system 69 | 70 | 3. **Start Debugging** 71 | - Run your Lua application 72 | - Launch "EmmyLua New Debug" configuration in VSCode 73 | - The debugger will connect automatically 74 | 75 | ### Debug Types 76 | 77 | - **EmmyLua New Debug**: Modern debugging with better performance 78 | - **EmmyLua Attach**: Attach to running processes (requires exported Lua symbols) 79 | - **EmmyLua Launch**: Direct launch debugging 80 | 81 | ## ❓ Frequently Asked Questions 82 | 83 |
84 | Why doesn't attach debugging work? 85 | 86 | **English**: The debugger needs access to Lua symbols from the target process. Ensure your executable exports Lua symbols. 87 | 88 | **中文**: 调试器需要获取进程中的 Lua 符号,因此需要进程导出 Lua 符号。 89 |
90 | 91 |
92 | Why do I see many "undefined variable" warnings? 93 | 94 | **English**: Create `.emmyrc.json` in your project root and disable the `undefined-global` diagnostic: 95 | ```json 96 | { 97 | "diagnostics": { 98 | "disable" : [ 99 | "undefined-global" 100 | ] 101 | } 102 | } 103 | ``` 104 | 105 | **中文**: 在项目根目录创建 `.emmyrc.json` 文件并禁用 `undefined-global` 诊断。 106 |
107 | 108 |
109 | Can I use EmmyLua analysis in other editors? 110 | 111 | **English**: Yes! EmmyLua uses a standard Language Server Protocol implementation. Any LSP-compatible editor can use it. 112 | 113 | **中文**: 可以!EmmyLua 基于标准的语言服务器协议,任何支持 LSP 的编辑器都可以使用。 114 |
115 | 116 |
117 | Why use .emmyrc.json instead of VSCode settings? 118 | 119 | **English**: Project-specific configuration files work across different editors and platforms without requiring IDE-specific setup. 120 | 121 | **中文**: 项目配置文件可以跨平台和编辑器使用,无需在每个 IDE 中重复配置。 122 |
123 | 124 |
125 | Why was the language server rewritten in Rust? 126 | 127 | **English**: The Rust implementation provides better performance, memory safety, and cross-platform compatibility compared to the previous .NET and Java versions. 128 | 129 | **中文**: Rust 实现提供了更好的性能、内存安全性和跨平台兼容性。(因为我想试试 rust 😄) 130 |
131 | 132 | ## 🤝 Contributing 133 | 134 | We welcome contributions! Please feel free to: 135 | - Report bugs and issues 136 | - Suggest new features 137 | - Submit pull requests 138 | - Join our QQ group for discussions 139 | 140 | ## 📄 License 141 | 142 | This project is licensed under the MIT License. 143 | -------------------------------------------------------------------------------- /debugger/Emmy.lua: -------------------------------------------------------------------------------- 1 | ---@meta 2 | 3 | -- Copyright (c) 2017. tangzx(love.tangzx@qq.com) 4 | -- 5 | -- Licensed under the Apache License, Version 2.0 (the "License"); 6 | -- you may not use this file except in compliance with the License. 7 | -- You may obtain a copy of the License at 8 | -- 9 | -- http://www.apache.org/licenses/LICENSE-2.0 10 | -- 11 | -- Unless required by applicable law or agreed to in writing, software 12 | -- distributed under the License is distributed on an "AS IS" BASIS, 13 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | -- See the License for the specific language governing permissions and 15 | -- limitations under the License. 16 | 17 | 18 | local toluaDebugger = {} 19 | 20 | function toluaDebugger.GetValueAsText(ty, obj, depth, typeNameOverride, displayAsKey) 21 | if ty == 'userdata' then 22 | if depth <= 1 then return nil end 23 | local mt = getmetatable(obj) 24 | if mt == nil then return nil end 25 | local tableNode = toluaDebugger.RawGetValueAsText(obj, depth, nil, false) 26 | if tableNode == nil then return nil end 27 | 28 | local propMap = {} 29 | while mt ~= nil do 30 | local getTab = mt[tolua.gettag] 31 | if getTab then 32 | for property, _ in pairs(getTab) do 33 | if not propMap[property] then 34 | propMap[property] = true 35 | local key = toluaDebugger.RawGetValueAsText(property, 0, nil, true) 36 | local value = toluaDebugger.RawGetValueAsText(obj[property], depth - 1, nil, false) 37 | toluaDebugger.AddChildNode(tableNode, key, value) 38 | end 39 | end 40 | end 41 | mt = getmetatable(mt) 42 | end 43 | return tableNode 44 | end 45 | end 46 | 47 | local xluaDebugger = {} 48 | function xluaDebugger.GetValueAsText(ty, obj, depth, typeNameOverride, displayAsKey) 49 | if ty == 'userdata' then 50 | local mt = getmetatable(obj) 51 | if mt == nil or depth <= 1 then return nil end 52 | 53 | local CSType = obj:GetType() 54 | if CSType then 55 | local tableNode = xluaDebugger.RawGetValueAsText(obj, depth, nil, false) 56 | 57 | local Type = CS.System.Type 58 | local ObsoleteType = Type.GetType('System.ObsoleteAttribute') 59 | local BindType = Type.GetType('System.Reflection.BindingFlags') 60 | local bindValue = CS.System.Enum.ToObject(BindType, 4157) -- 60 | 4096 61 | local properties = CSType:GetProperties(bindValue) 62 | for i = 1, properties.Length do 63 | local p = properties[i - 1] 64 | if CS.System.Attribute.GetCustomAttribute(p, ObsoleteType) == nil then 65 | local property = p.Name 66 | local value = obj[property] 67 | 68 | local key = xluaDebugger.RawGetValueAsText(property, 0, nil, true) 69 | local value = xluaDebugger.RawGetValueAsText(value, depth - 1, nil, false) 70 | xluaDebugger.AddChildNode(tableNode, key, value) 71 | end 72 | end 73 | 74 | return tableNode 75 | end 76 | end 77 | end 78 | 79 | local cocosLuaDebugger = {} 80 | function cocosLuaDebugger.GetValueAsText(ty, obj, depth, typeNameOverride, displayAsKey) 81 | if ty == 'userdata' then 82 | if depth <= 1 then return nil end 83 | local mt, tab = getmetatable(obj), tolua.getpeer(obj) 84 | if mt == nil then return nil end 85 | local tableNode = cocosLuaDebugger.RawGetValueAsText(obj, depth, nil, false) 86 | if tableNode == nil then return nil end 87 | 88 | local propMap = {} 89 | while mt ~= nil and tab ~= nil do 90 | for property, _ in pairs(tab) do 91 | if not propMap[property] then 92 | propMap[property] = true 93 | local key = cocosLuaDebugger.RawGetValueAsText(property, 0, nil, true) 94 | local value = cocosLuaDebugger.RawGetValueAsText(obj[property], depth - 1, nil, false) 95 | cocosLuaDebugger.AddChildNode(tableNode, key, value) 96 | end 97 | end 98 | mt, tab = getmetatable(mt), tolua.getpeer(mt) 99 | end 100 | return tableNode 101 | end 102 | end 103 | 104 | local emmy = {} 105 | 106 | if tolua then 107 | if tolua.gettag then 108 | emmy = toluaDebugger 109 | else 110 | emmy = cocosLuaDebugger 111 | end 112 | elseif xlua then 113 | emmy = xluaDebugger 114 | end 115 | 116 | function emmy.Reload(fileName) 117 | local a, b, c = string.find(fileName, '%.lua') 118 | if a then 119 | fileName = string.sub(fileName, 1, a - 1) 120 | end 121 | 122 | emmy.DebugLog('Try reload : ' .. fileName, 1) 123 | local searchers = package.searchers or package.loaders 124 | for _, load in ipairs(searchers) do 125 | local result = load(fileName) 126 | if type(result) == 'function' then 127 | break 128 | end 129 | end 130 | end 131 | 132 | rawset(_G, 'emmy', emmy) 133 | local emmy_init = rawget(_G, 'emmy_init') 134 | if emmy_init then 135 | emmy_init() 136 | end 137 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "config.common.deprecated": "This option is deprecated", 3 | "config.common.enum.default.description": "Use value from `.emmyrc.json` or fall back to default", 4 | "config.common.enum.off.description": "Override `.emmyrc.json` and disable this option", 5 | "config.common.enum.on.description": "Override `.emmyrc.json` and enable this option", 6 | "config.emmylua.codeAction.insertSpace.description": "Add space after `---` comments when inserting `@diagnostic disable-next-line`.", 7 | "config.emmylua.codeLens.enable.description": "Enable code lens.", 8 | "config.emmylua.colors.global.description": "If not empty, highlights global variables with the given color", 9 | "config.emmylua.colors.local.description": "If not empty, highlights local variables with the given color", 10 | "config.emmylua.colors.mutableUnderline.description": "Add underline to variables if they're reassigned within their scope", 11 | "config.emmylua.colors.parameter.description": "If not empty, highlights function parameters with the given color", 12 | "config.emmylua.completion.autoRequire.description": "Automatically insert call to `require` when autocompletion\ninserts objects from other modules.", 13 | "config.emmylua.completion.baseFunctionIncludesName.description": "Whether to include the name in the base function completion. Effect: `function () end` -> `function name() end`.", 14 | "config.emmylua.completion.enable.description": "Enable autocompletion.", 15 | "config.emmylua.completion.postfix.description": "Symbol that's used to trigger postfix autocompletion.", 16 | "config.emmylua.diagnostics.diagnosticInterval.description": "Delay between opening/changing a file and scanning it for errors, in milliseconds.", 17 | "config.emmylua.documentColor.enable.description": "Enable parsing strings for color tags and showing a color picker next to them.", 18 | "config.emmylua.hint.enable.description": "Enable inlay hints.", 19 | "config.emmylua.hint.enumParamHint.description": "Show name of enumerator when passing a literal value to a function\nthat expects an enum.\n\nExample:\n\n```lua\n--- @enum Level\nlocal Foo = {\n Info = 1,\n Error = 2,\n}\n\n--- @param l Level\nfunction print_level(l) end\n\nprint_level(1 --[[ Hint: Level.Info ]])\n```", 20 | "config.emmylua.hint.indexHint.description": "Show named array indexes.\n\nExample:\n\n```lua\nlocal array = {\n [1] = 1, -- [name]\n}\n\nprint(array[1] --[[ Hint: name ]])\n```", 21 | "config.emmylua.hint.localHint.description": "Show types of local variables.", 22 | "config.emmylua.hint.metaCallHint.description": "Show hint when calling an object results in a call to\nits meta table's `__call` function.", 23 | "config.emmylua.hint.overrideHint.description": "Show methods that override functions from base class.", 24 | "config.emmylua.hint.paramHint.description": "Show parameter names in function calls and parameter types in function definitions.", 25 | "config.emmylua.hover.enable.description": "Enable showing documentation on hover.", 26 | "config.emmylua.inlineValues.enable.description": "Show inline values during debug.", 27 | "config.emmylua.language.completeAnnotation.description": "Automatically insert `\"---\"` when line breaking after a comment", 28 | "config.emmylua.ls.executablePath.description": "Override path to the EmmyLua language server executable.\n\nUses bundled language server if not specified", 29 | "config.emmylua.ls.globalConfigPath.description": "Path to the global `.emmyrc.json`.\n\nSettings from this file will be used if they're not overridden by the project's `.emmyrc.json`", 30 | "config.emmylua.ls.startParameters.description": "Additional arguments to be passed to the EmmyLua language server", 31 | "config.emmylua.references.enable.description": "Enable searching for symbol usages.", 32 | "config.emmylua.references.fuzzySearch.description": "Use fuzzy search when searching for symbol usages\nand normal search didn't find anything.", 33 | "config.emmylua.references.shortStringSearch.description": "Also search for usages in strings.", 34 | "config.emmylua.semanticTokens.enable.description": "Enable semantic tokens.", 35 | "config.emmylua.semanticTokens.renderDocumentationMarkup.description": "Render Markdown/RST in documentation. Set `doc.syntax` for this option to have effect.", 36 | "config.emmylua.workspace.enableReindex.description": "Enable full project reindex after changing a file.", 37 | "config.emmylua.workspace.reindexDuration.description": "Delay between changing a file and full project reindex, in milliseconds.", 38 | "config.lua.trace.server.description": "Traces the communication between VSCode and the EmmyLua language server in the Output view. Default is `off`", 39 | "debug.attach.captureLog": "Capture process log with Windows Terminal", 40 | "debug.attach.desc": "Attach process debugger", 41 | "debug.attach.label": "EmmyLua: Attach by process id", 42 | "debug.attach.name": "Attach by process id", 43 | "debug.attach.target_pid": "Target pid", 44 | "debug.attach.target_process": "Attach process by name", 45 | "debug.launch.arguments": "arguments pass to executable file", 46 | "debug.launch.blockOnExit": "block process when exit", 47 | "debug.launch.desc": "Launch program and debug with attach debugger", 48 | "debug.launch.label": "EmmyLua: Attach by launch program", 49 | "debug.launch.name": "Attach by launch program", 50 | "debug.launch.newWindow": "create new windows", 51 | "debug.launch.program": "Launch program and debug executable file", 52 | "debug.launch.workingDir": "working directory" 53 | } 54 | -------------------------------------------------------------------------------- /src/debugger/base/EmmyDebugData.ts: -------------------------------------------------------------------------------- 1 | import * as proto from "./EmmyDebugProto"; 2 | import { DebugProtocol } from "@vscode/debugprotocol"; 3 | import { Handles } from "@vscode/debugadapter"; 4 | // import iconv = require('iconv-lite'); 5 | 6 | export interface IEmmyStackContext { 7 | handles: Handles; 8 | eval(expr: string, cacheId: number, depth: number): Promise; 9 | } 10 | 11 | export interface IEmmyStackNode { 12 | toVariable(ctx: IEmmyStackContext): DebugProtocol.Variable; 13 | computeChildren(ctx: IEmmyStackContext): Promise>; 14 | } 15 | 16 | export class EmmyStack implements IEmmyStackNode { 17 | constructor( 18 | private data: proto.IStack 19 | ) { 20 | } 21 | 22 | toVariable(ctx: IEmmyStackContext): DebugProtocol.Variable { 23 | throw new Error('Method not implemented.'); 24 | } 25 | 26 | async computeChildren(ctx: IEmmyStackContext): Promise> { 27 | const variables = this.data.localVariables.concat(this.data.upvalueVariables); 28 | return variables.map(v => { 29 | return new EmmyVariable(v); 30 | }); 31 | } 32 | } 33 | 34 | export class EmmyStackENV implements IEmmyStackNode { 35 | constructor( 36 | private data: proto.IStack 37 | ) { 38 | } 39 | 40 | toVariable(ctx: IEmmyStackContext): DebugProtocol.Variable { 41 | throw new Error('Method not implemented.'); 42 | } 43 | 44 | async computeChildren(ctx: IEmmyStackContext): Promise> { 45 | const variables = this.data.localVariables.concat(this.data.upvalueVariables); 46 | 47 | let variable = variables.find(variable => variable.name == "_ENV"); 48 | if (variable) { 49 | const _ENV = new EmmyVariable(variable); 50 | return await _ENV.computeChildren(ctx); 51 | } else { 52 | const _GVariable = await ctx.eval("_G", 0, 1); 53 | if (_GVariable.success) { 54 | const _G = new EmmyVariable(_GVariable.value) 55 | return await _G.computeChildren(ctx); 56 | } 57 | } 58 | return []; 59 | } 60 | } 61 | 62 | export class EmmyVariable implements IEmmyStackNode { 63 | private variable: DebugProtocol.Variable; 64 | constructor( 65 | private data: proto.IVariable, 66 | private parent?: EmmyVariable, 67 | ) { 68 | let value = this.data.value; 69 | // vscode not implement this feature 70 | // let presentationHint: DebugProtocol.VariablePresentationHint = { 71 | // kind: 'property', 72 | // attributes: [] 73 | // }; 74 | switch (this.data.valueType) { 75 | case proto.ValueType.TSTRING: 76 | value = `"${this.data.value}"`; 77 | break; 78 | // presentationHint.attributes?.push('rawString'); 79 | // break; 80 | // case proto.ValueType.TFUNCTION: 81 | // presentationHint.kind = 'method'; 82 | // break; 83 | } 84 | let name = this.data.name; 85 | switch (this.data.nameType) { 86 | case proto.ValueType.TSTRING: 87 | // if (name.startsWith("_")) { 88 | // presentationHint.attributes?.push('private'); 89 | // } 90 | // else { 91 | // presentationHint.attributes?.push('public'); 92 | // } 93 | // if (!/^[\x00-\x7F]*$/.test(name)) { 94 | 95 | // } 96 | break; 97 | case proto.ValueType.TNUMBER: 98 | name = `[${name}]`; 99 | // presentationHint.kind = 'data' 100 | break; 101 | default: 102 | name = `[${name}]`; 103 | break; 104 | } 105 | this.variable = { name, value, variablesReference: 0 }; 106 | } 107 | 108 | toVariable(ctx: IEmmyStackContext): DebugProtocol.Variable { 109 | const ref = ctx.handles.create(this); 110 | if (this.data.valueType === proto.ValueType.TTABLE || 111 | this.data.valueType === proto.ValueType.TUSERDATA || 112 | this.data.valueType === proto.ValueType.GROUP) { 113 | this.variable.variablesReference = ref; 114 | } 115 | return this.variable; 116 | } 117 | 118 | private getExpr(): string { 119 | let arr: proto.IVariable[] = []; 120 | let n: EmmyVariable | undefined = this; 121 | while (n) { 122 | if (n.data.valueType !== proto.ValueType.GROUP) { 123 | arr.push(n.data); 124 | } 125 | n = n.parent; 126 | } 127 | arr = arr.reverse(); 128 | return arr.map(it => it.name).join('.'); 129 | } 130 | 131 | sortVariables(a: proto.IVariable, b: proto.IVariable): number { 132 | if (a.nameType < b.nameType) { 133 | return -1; 134 | } else if (a.nameType > b.nameType) { 135 | return 1; 136 | } else { 137 | if (a.nameType == proto.ValueType.TNUMBER) { 138 | return Number(a.name) - Number(b.name); 139 | } 140 | else { 141 | return a.name.localeCompare(b.name); 142 | } 143 | } 144 | } 145 | 146 | async computeChildren(ctx: IEmmyStackContext): Promise { 147 | let children = this.data.children; 148 | if (this.data.valueType !== proto.ValueType.GROUP) { 149 | const evalResp = await ctx.eval(this.getExpr(), this.data.cacheId, 2); 150 | if (evalResp.success) { 151 | children = evalResp.value.children; 152 | } 153 | } 154 | if (children) { 155 | return children.sort(this.sortVariables).map(v => new EmmyVariable(v, this)); 156 | } 157 | return []; 158 | } 159 | } -------------------------------------------------------------------------------- /src/luarocks/LuaRocksTreeProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LuaRocksManager, LuaPackage } from './LuaRocksManager'; 3 | 4 | export class LuaRocksTreeProvider implements vscode.TreeDataProvider { 5 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 6 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 7 | 8 | private installedPackages: LuaPackage[] = []; 9 | private searchResults: LuaPackage[] = []; 10 | private isSearchMode = false; 11 | 12 | constructor(private readonly manager: LuaRocksManager) {} 13 | 14 | refresh(): void { 15 | this._onDidChangeTreeData.fire(); 16 | } 17 | 18 | async refreshInstalled(): Promise { 19 | this.installedPackages = await this.manager.getInstalledPackages(true); 20 | this.refresh(); 21 | } 22 | 23 | setSearchResults(packages: LuaPackage[]): void { 24 | this.searchResults = packages; 25 | this.isSearchMode = true; 26 | this.refresh(); 27 | } 28 | 29 | clearSearch(): void { 30 | this.isSearchMode = false; 31 | this.searchResults = []; 32 | this.refresh(); 33 | } 34 | 35 | getTreeItem(element: PackageTreeItem): vscode.TreeItem { 36 | return element; 37 | } 38 | 39 | async getChildren(element?: PackageTreeItem): Promise { 40 | if (!element) { 41 | // Root level 42 | if (this.isSearchMode) { 43 | return [ 44 | new PackageTreeItem('Search Results', vscode.TreeItemCollapsibleState.Expanded, 'category', undefined, this.searchResults.length) 45 | ]; 46 | } else { 47 | return [ 48 | new PackageTreeItem('Installed Packages', vscode.TreeItemCollapsibleState.Expanded, 'category', undefined, this.installedPackages.length), 49 | new PackageTreeItem('Search Packages', vscode.TreeItemCollapsibleState.None, 'search') 50 | ]; 51 | } 52 | } 53 | 54 | if (element.type === 'category') { 55 | if (element.label === 'Installed Packages') { 56 | if (this.installedPackages.length === 0) { 57 | this.installedPackages = await this.manager.getInstalledPackages(); 58 | } 59 | return this.installedPackages.map(pkg => 60 | new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'installed', pkg) 61 | ); 62 | } else if (element.label === 'Search Results') { 63 | return this.searchResults.map(pkg => 64 | new PackageTreeItem(pkg.name, vscode.TreeItemCollapsibleState.None, 'available', pkg) 65 | ); 66 | } 67 | } 68 | 69 | return []; 70 | } 71 | 72 | dispose(): void { 73 | // Clean up any resources if needed 74 | } 75 | } 76 | 77 | export class PackageTreeItem extends vscode.TreeItem { 78 | constructor( 79 | public readonly label: string, 80 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 81 | public readonly type: 'category' | 'installed' | 'available' | 'search', 82 | public readonly packageInfo?: LuaPackage, 83 | public readonly count?: number 84 | ) { 85 | super(label, collapsibleState); 86 | 87 | this.setupItem(); 88 | } 89 | 90 | private setupItem(): void { 91 | switch (this.type) { 92 | case 'category': 93 | this.iconPath = new vscode.ThemeIcon('folder'); 94 | this.description = this.count !== undefined ? `(${this.count})` : ''; 95 | this.contextValue = 'category'; 96 | break; 97 | 98 | case 'installed': 99 | this.iconPath = new vscode.ThemeIcon('package'); 100 | this.description = this.packageInfo?.version || ''; 101 | this.tooltip = this.createTooltip(); 102 | this.contextValue = 'installedPackage'; 103 | // 右键菜单中提供详细信息 104 | break; 105 | 106 | case 'available': 107 | this.iconPath = new vscode.ThemeIcon('cloud-download'); 108 | this.description = this.packageInfo?.version || ''; 109 | this.tooltip = this.createTooltip(); 110 | this.contextValue = 'availablePackage'; 111 | // 右键菜单中提供详细信息 112 | break; 113 | 114 | case 'search': 115 | this.iconPath = new vscode.ThemeIcon('search'); 116 | this.command = { 117 | command: 'emmylua.luarocks.searchPackages', 118 | title: 'Search Packages' 119 | }; 120 | this.contextValue = 'search'; 121 | break; 122 | } 123 | } 124 | 125 | private createTooltip(): string { 126 | if (!this.packageInfo) return this.label; 127 | 128 | const lines = [ 129 | `**${this.packageInfo.name}** ${this.packageInfo.version || ''}`, 130 | '' 131 | ]; 132 | 133 | if (this.packageInfo.description || this.packageInfo.summary) { 134 | lines.push(this.packageInfo.description || this.packageInfo.summary || ''); 135 | lines.push(''); 136 | } 137 | 138 | if (this.packageInfo.author) { 139 | lines.push(`**Author:** ${this.packageInfo.author}`); 140 | } 141 | 142 | if (this.packageInfo.license) { 143 | lines.push(`**License:** ${this.packageInfo.license}`); 144 | } 145 | 146 | if (this.packageInfo.homepage) { 147 | lines.push(`**Homepage:** ${this.packageInfo.homepage}`); 148 | } 149 | 150 | if (this.packageInfo.location && this.packageInfo.installed) { 151 | lines.push(`**Location:** ${this.packageInfo.location}`); 152 | } 153 | 154 | return lines.join('\n'); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/debugger/launch/EmmyLaunchDebugSession.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import * as cp from "child_process"; 3 | import * as proto from "../base/EmmyDebugProto"; 4 | import { EmmyDebugSession } from "../base/EmmyDebugSession"; 5 | import { OutputEvent, TerminatedEvent } from "@vscode/debugadapter"; 6 | import { DebugProtocol } from "@vscode/debugprotocol"; 7 | 8 | interface EmmyLaunchDebugArguments extends DebugProtocol.LaunchRequestArguments { 9 | extensionPath: string; 10 | sourcePaths: string[]; 11 | program: string; 12 | arguments: string[]; 13 | workingDir: string; 14 | blockOnExit?: boolean; 15 | newWindow?: boolean; 16 | ext: string[]; 17 | } 18 | 19 | enum WinArch { 20 | X86, X64 21 | } 22 | 23 | export class EmmyLaunchDebugSession extends EmmyDebugSession { 24 | 25 | private program: string = ""; 26 | private workingDir: string = ""; 27 | private arguments: string[] = []; 28 | private toolProcess?: cp.ChildProcess; 29 | private pid = 0; 30 | 31 | 32 | private getPort(pid: number): number { 33 | let port = pid; 34 | while (port > 0xffff) { port -= 0xffff; } 35 | while (port < 0x400) { port += 0x400; } 36 | return port; 37 | } 38 | 39 | protected async launchRequest(response: DebugProtocol.LaunchResponse, args: EmmyLaunchDebugArguments): Promise { 40 | this.extensionPath = args.extensionPath; 41 | this.ext = args.ext; 42 | 43 | this.program = args.program ?? ""; 44 | this.workingDir = args.workingDir ?? "" 45 | this.arguments = args.arguments ?? [] 46 | if (args.newWindow) { 47 | this.pid = await this.launchWithWindow(); 48 | } 49 | else { 50 | this.pid = await this.launchDebug(); 51 | } 52 | const client = net.connect(this.getPort(this.pid), "localhost") 53 | .on('connect', () => { 54 | 55 | this.sendResponse(response); 56 | this.onConnect(client); 57 | this.readClient(client); 58 | this.sendMessage({ cmd: proto.MessageCMD.StartHookReq }); 59 | //TODO 延时的原因是等待hook api完成,后续改成消息通知 60 | setTimeout(() => { 61 | this.toolProcess?.stdin?.write("connected\n"); 62 | }, 500); 63 | }) 64 | .on('error', err => { 65 | response.success = false; 66 | response.message = `${err}`; 67 | this.sendResponse(response); 68 | }); 69 | this.client = client; 70 | } 71 | 72 | private async detectArch(): Promise { 73 | const cwd = `${this.extensionPath}/debugger/emmy/windows/x64`; 74 | const args = [ 75 | 'emmy_tool.exe', 76 | 'arch_file', 77 | `\"${this.program}\"` 78 | ]; 79 | return new Promise((r, c) => { 80 | cp.exec(args.join(" "), { cwd: cwd }) 81 | .on('close', (code) => { 82 | r(code === 0 ? WinArch.X64 : WinArch.X86); 83 | }) 84 | .on('error', c); 85 | }); 86 | } 87 | 88 | protected onDisconnect(): void { 89 | this.toolProcess?.stdin?.write("close"); 90 | } 91 | 92 | private async launchWithWindow(): Promise { 93 | const arch = await this.detectArch(); 94 | const archName = arch === WinArch.X64 ? 'x64' : 'x86'; 95 | const cwd = `${this.extensionPath}/debugger/emmy/windows/${archName}`; 96 | // const mc = this.program.match(/[^/\\]+$/); 97 | const args = [ 98 | "launch", 99 | "-create-new-window", 100 | "-dll", 101 | "emmy_hook.dll", 102 | "-dir", 103 | `"${cwd}"`, 104 | "-work", 105 | `"${this.workingDir}"`, 106 | "-exe", 107 | `"${this.program}"`, 108 | "-args" 109 | ]; 110 | 111 | args.push(...(this.arguments)); 112 | return new Promise((r, c) => { 113 | const childProcess = cp.spawn(`emmy_tool.exe`, args, { 114 | cwd: cwd, 115 | windowsHide: false 116 | }); 117 | childProcess.stderr.on("data", (e) => { 118 | const s = e.toString(); 119 | this.sendEvent(new OutputEvent(s)); 120 | }); 121 | let forOut = false; 122 | childProcess.stdout.on("data", (e) => { 123 | if (!forOut) { 124 | forOut = true; 125 | r(Number(e.toString())); 126 | } 127 | else { 128 | this.sendEvent(new OutputEvent(e.toString())); 129 | } 130 | }); 131 | 132 | childProcess.on("exit", code => { 133 | this.sendEvent(new TerminatedEvent()); 134 | this.toolProcess = undefined; 135 | }); 136 | this.toolProcess = childProcess; 137 | }); 138 | } 139 | 140 | private async launchDebug(): Promise { 141 | const arch = await this.detectArch(); 142 | const archName = arch === WinArch.X64 ? 'x64' : 'x86'; 143 | const cwd = `${this.extensionPath}/debugger/emmy/windows/${archName}`; 144 | const args = [ 145 | "launch", 146 | "-dll", 147 | "emmy_hook.dll", 148 | "-dir", 149 | `"${cwd}"`, 150 | "-work", 151 | `"${this.workingDir}"`, 152 | "-exe", 153 | `"${this.program}"`, 154 | "-args", 155 | ]; 156 | 157 | args.push(...(this.arguments)); 158 | return new Promise((r, c) => { 159 | const childProcess = cp.spawn(`emmy_tool.exe`, args, { 160 | cwd: cwd, 161 | windowsHide: false 162 | }); 163 | childProcess.stderr.on("data", (e) => { 164 | const s = e.toString(); 165 | this.sendEvent(new OutputEvent(s)); 166 | }); 167 | let forOut = false; 168 | childProcess.stdout.on("data", (e) => { 169 | if (!forOut) { 170 | forOut = true; 171 | r(Number(e.toString())); 172 | } 173 | else { 174 | this.sendEvent(new OutputEvent(e.toString())); 175 | } 176 | }); 177 | 178 | childProcess.on("exit", code => { 179 | this.sendEvent(new TerminatedEvent()); 180 | this.toolProcess = undefined; 181 | }); 182 | 183 | this.toolProcess = childProcess; 184 | }); 185 | } 186 | 187 | protected handleDebugMessage(cmd: proto.MessageCMD, msg: any) { 188 | switch (cmd) { 189 | case proto.MessageCMD.AttachedNotify: 190 | const n: number = msg.state; 191 | this.sendEvent(new OutputEvent(`Attached to lua state 0x${n.toString(16)}\n`)); 192 | break; 193 | case proto.MessageCMD.LogNotify: 194 | this.sendEvent(new OutputEvent(`${msg.message}\n`)); 195 | break; 196 | } 197 | super.handleDebugMessage(cmd, msg); 198 | } 199 | 200 | protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments) { 201 | super.disconnectRequest(response, args); 202 | if (args.terminateDebuggee) { 203 | this.toolProcess?.stdin?.write("close"); 204 | } 205 | } 206 | 207 | protected onSocketClose() { 208 | if (this.client) { 209 | this.client.removeAllListeners(); 210 | } 211 | this.sendEvent(new OutputEvent('Disconnected.\n')); 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /debugger/emmy/emmyHelper.lua: -------------------------------------------------------------------------------- 1 | ---@meta 2 | 3 | -- Copyright (c) 2017. tangzx(love.tangzx@qq.com) 4 | -- 5 | -- Licensed under the Apache License, Version 2.0 (the "License"); 6 | -- you may not use this file except in compliance with the License. 7 | -- You may obtain a copy of the License at 8 | -- 9 | -- http://www.apache.org/licenses/LICENSE-2.0 10 | -- 11 | -- Unless required by applicable law or agreed to in writing, software 12 | -- distributed under the License is distributed on an "AS IS" BASIS, 13 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | -- See the License for the specific language governing permissions and 15 | -- limitations under the License. 16 | 17 | ---@class emmy 18 | ---@field createNode fun(): Variable 19 | local emmy = {} 20 | 21 | ---@class Variable 22 | ---@field query fun(self: Variable, obj: any, depth: number, queryHelper: boolean):void 23 | ---@field name string 24 | ---@field value string 25 | ---@field valueTypeName string 26 | 27 | local toluaHelper = { 28 | ---@param variable Variable 29 | queryVariable = function(variable, obj, typeName, depth) 30 | if typeName == 'table' then 31 | local cname = rawget(obj, '__cname') 32 | if cname then 33 | variable:query(obj, depth) 34 | variable.valueTypeName = cname 35 | return true 36 | end 37 | elseif typeName == 'userdata' then 38 | local mt = getmetatable(obj) 39 | if mt == nil then return false end 40 | 41 | variable.valueTypeName = 'C#' 42 | variable.value = tostring(obj) 43 | 44 | if depth > 1 then 45 | local parent = variable 46 | local propMap = {} 47 | while mt ~= nil do 48 | local getTab = mt[tolua.gettag] 49 | if getTab then 50 | for property, _ in pairs(getTab) do 51 | if not propMap[property] then 52 | propMap[property] = true 53 | local v = emmy.createNode() 54 | v.name = property 55 | v:query(obj[property], depth - 1, true) 56 | parent:addChild(v) 57 | end 58 | end 59 | end 60 | mt = getmetatable(mt) 61 | if mt then 62 | local super = emmy.createNode() 63 | super.name = "base" 64 | super.value = mt[".name"] 65 | super.valueType = 9 66 | super.valueTypeName = "C#" 67 | parent:addChild(super) 68 | parent = super 69 | end 70 | end 71 | end 72 | return true 73 | end 74 | end 75 | } 76 | 77 | local xluaDebugger = { 78 | queryVariable = function(variable, obj, typeName, depth) 79 | if typeName == 'userdata' then 80 | local mt = getmetatable(obj) 81 | if mt == nil then 82 | return false 83 | end 84 | 85 | local CSType = obj:GetType() 86 | if CSType then 87 | variable.valueTypeName = 'C#' 88 | variable.value = tostring(obj) --CSType.FullName 89 | 90 | if depth > 1 then 91 | local Type = CS.System.Type 92 | local ObsoleteType = Type.GetType('System.ObsoleteAttribute') 93 | local BindType = Type.GetType('System.Reflection.BindingFlags') 94 | local bindValue = CS.System.Enum.ToObject(BindType, 5174) -- Instance | Public | NonPublic | GetProperty | DeclaredOnly | GetField 95 | 96 | local parent = variable 97 | while CSType do 98 | local properties = CSType:GetProperties(bindValue) 99 | for i = 1, properties.Length do 100 | local p = properties[i - 1] 101 | if CS.System.Attribute.GetCustomAttribute(p, ObsoleteType) == nil then 102 | local property = p.Name 103 | local value = obj[property] 104 | 105 | local v = emmy.createNode() 106 | v.name = property 107 | v:query(value, depth - 1, true) 108 | parent:addChild(v) 109 | end 110 | end 111 | local fields = CSType:GetFields(bindValue) 112 | for i = 1, fields.Length do 113 | local p = fields[i - 1] 114 | if CS.System.Attribute.GetCustomAttribute(p, ObsoleteType) == nil then 115 | local property = p.Name 116 | local value = obj[property] 117 | 118 | local v = emmy.createNode() 119 | v.name = property 120 | v:query(value, depth - 1, true) 121 | parent:addChild(v) 122 | end 123 | end 124 | 125 | CSType = CSType.BaseType 126 | if CSType then 127 | local super = emmy.createNode() 128 | super.name = "base" 129 | super.value = CSType.FullName 130 | super.valueType = 9 131 | super.valueTypeName = "C#" 132 | parent:addChild(super) 133 | parent = super 134 | end 135 | end 136 | end 137 | 138 | return true 139 | end 140 | end 141 | end 142 | } 143 | 144 | local cocosLuaDebugger = { 145 | queryVariable = function(variable, obj, typeName, depth) 146 | if typeName == 'userdata' then 147 | local mt = getmetatable(obj) 148 | if mt == nil then return false end 149 | variable.valueTypeName = 'C++' 150 | variable.value = mt[".classname"] 151 | 152 | if depth > 1 then 153 | local parent = variable 154 | local propMap = {} 155 | while mt ~= nil do 156 | for property, _ in pairs(mt) do 157 | if not propMap[property] then 158 | propMap[property] = true 159 | local v = emmy.createNode() 160 | v.name = property 161 | v:query(obj[property], depth - 1, true) 162 | parent:addChild(v) 163 | end 164 | end 165 | mt = getmetatable(mt) 166 | if mt then 167 | local super = emmy.createNode() 168 | super.name = "base" 169 | super.value = mt[".classname"] 170 | super.valueType = 9 171 | super.valueTypeName = "C++" 172 | parent:addChild(super) 173 | parent = super 174 | end 175 | end 176 | end 177 | return true 178 | end 179 | end 180 | } 181 | 182 | if tolua then 183 | if tolua.gettag then 184 | emmy = toluaHelper 185 | else 186 | emmy = cocosLuaDebugger 187 | end 188 | elseif xlua then 189 | emmy = xluaDebugger 190 | end 191 | 192 | rawset(_G, 'emmyHelper', emmy) 193 | 194 | local emmyHelperInit = rawget(_G, 'emmyHelperInit') 195 | if emmyHelperInit then 196 | emmyHelperInit() 197 | end 198 | -------------------------------------------------------------------------------- /src/annotator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { AnnotatorType } from './lspExtension'; 3 | import { LanguageClient } from 'vscode-languageclient/node'; 4 | import * as notifications from "./lspExtension"; 5 | import { get } from './configRenames'; 6 | 7 | // 装饰器类型映射接口 8 | interface DecorationMap { 9 | [AnnotatorType.ReadOnlyParam]: vscode.TextEditorDecorationType; 10 | [AnnotatorType.Global]: vscode.TextEditorDecorationType; 11 | [AnnotatorType.ReadOnlyLocal]: vscode.TextEditorDecorationType; 12 | [AnnotatorType.MutLocal]: vscode.TextEditorDecorationType; 13 | [AnnotatorType.MutParam]: vscode.TextEditorDecorationType; 14 | [AnnotatorType.DocEm]: vscode.TextEditorDecorationType; 15 | [AnnotatorType.DocStrong]: vscode.TextEditorDecorationType; 16 | } 17 | 18 | // 装饰器缓存 19 | const decorationCache = new Map(); 20 | 21 | // 当前装饰器实例 22 | let decorations: Partial = {}; 23 | 24 | /** 25 | * 创建装饰器的工厂函数 26 | */ 27 | const createDecoration = (key: string): vscode.TextEditorDecorationType => { 28 | const cacheKey = `decoration:${key}`; 29 | if (decorationCache.has(cacheKey)) { 30 | return decorationCache.get(cacheKey)!; 31 | } 32 | 33 | const config: vscode.DecorationRenderOptions = {}; 34 | const color = vscode.workspace.getConfiguration("emmylua").get(key); 35 | 36 | if (color) { 37 | config.light = { color }; 38 | config.dark = { color }; 39 | } 40 | 41 | const decoration = vscode.window.createTextEditorDecorationType(config); 42 | decorationCache.set(cacheKey, decoration); 43 | return decoration; 44 | }; 45 | 46 | /** 47 | * 创建带下划线的装饰器 48 | */ 49 | const createDecorationUnderline = (key: string): vscode.TextEditorDecorationType => { 50 | const cacheKey = `underline:${key}`; 51 | if (decorationCache.has(cacheKey)) { 52 | return decorationCache.get(cacheKey)!; 53 | } 54 | 55 | const config: vscode.DecorationRenderOptions = {}; 56 | const color = vscode.workspace.getConfiguration("emmylua").get(key); 57 | 58 | const textDecoration = color 59 | ? `underline;text-decoration-color:${color};text-underline-offset: 4px;` 60 | : 'underline;text-underline-offset: 4px;'; 61 | 62 | if (color) { 63 | config.light = { color, textDecoration }; 64 | config.dark = { color, textDecoration }; 65 | } else { 66 | config.light = { textDecoration }; 67 | config.dark = { textDecoration }; 68 | } 69 | 70 | const decoration = vscode.window.createTextEditorDecorationType(config); 71 | decorationCache.set(cacheKey, decoration); 72 | return decoration; 73 | }; 74 | 75 | const createDecorationDocEm = (): vscode.TextEditorDecorationType => { 76 | const cacheKey = `decoration:doc.em`; 77 | if (decorationCache.has(cacheKey)) { 78 | return decorationCache.get(cacheKey)!; 79 | } 80 | 81 | const config: vscode.DecorationRenderOptions = { 82 | light: { 83 | fontStyle: "italic", 84 | }, 85 | dark: { 86 | fontStyle: "italic", 87 | }, 88 | }; 89 | const decoration = vscode.window.createTextEditorDecorationType(config); 90 | decorationCache.set(cacheKey, decoration); 91 | return decoration; 92 | }; 93 | 94 | const createDecorationDocStrong = (): vscode.TextEditorDecorationType => { 95 | const cacheKey = `decoration:doc.strong`; 96 | if (decorationCache.has(cacheKey)) { 97 | return decorationCache.get(cacheKey)!; 98 | } 99 | 100 | const config: vscode.DecorationRenderOptions = { 101 | light: { 102 | fontWeight: "bold", 103 | }, 104 | dark: { 105 | fontWeight: "bold", 106 | }, 107 | }; 108 | const decoration = vscode.window.createTextEditorDecorationType(config); 109 | decorationCache.set(cacheKey, decoration); 110 | return decoration; 111 | }; 112 | 113 | /** 114 | * 批量释放装饰器 115 | */ 116 | const disposeDecorations = (...decorationTypes: (vscode.TextEditorDecorationType | undefined)[]): void => { 117 | decorationTypes.forEach(decoration => decoration?.dispose()); 118 | }; 119 | 120 | /** 121 | * 更新所有装饰器实例 122 | */ 123 | const updateDecorations = (): void => { 124 | // 清理旧的装饰器 125 | if (Object.keys(decorations).length > 0) { 126 | disposeDecorations(...Object.values(decorations)); 127 | decorations = {}; 128 | } 129 | 130 | // 创建基础装饰器 131 | decorations[AnnotatorType.ReadOnlyParam] = createDecoration("colors.parameter"); 132 | decorations[AnnotatorType.Global] = createDecoration("colors.global"); 133 | decorations[AnnotatorType.ReadOnlyLocal] = createDecoration("colors.local"); 134 | 135 | // 获取配置以决定是否使用下划线 136 | const config = vscode.workspace.getConfiguration( 137 | undefined, 138 | vscode.workspace.workspaceFolders?.[0] 139 | ); 140 | const mutableUnderline = get(config, "emmylua.colors.mutableUnderline"); 141 | 142 | // 创建可变变量的装饰器 143 | if (mutableUnderline) { 144 | decorations[AnnotatorType.MutLocal] = createDecorationUnderline("colors.local"); 145 | decorations[AnnotatorType.MutParam] = createDecorationUnderline("colors.parameter"); 146 | } else { 147 | decorations[AnnotatorType.MutLocal] = createDecoration("colors.local"); 148 | decorations[AnnotatorType.MutParam] = createDecoration("colors.parameter"); 149 | } 150 | 151 | decorations[AnnotatorType.DocEm] = createDecorationDocEm(); 152 | decorations[AnnotatorType.DocStrong] = createDecorationDocStrong(); 153 | }; 154 | 155 | /** 156 | * 配置变化时的处理函数 157 | */ 158 | export const onDidChangeConfiguration = (): void => { 159 | // 清理缓存,强制重新创建装饰器 160 | decorationCache.clear(); 161 | updateDecorations(); 162 | }; 163 | 164 | // 防抖定时器 165 | let timeoutToReqAnn: NodeJS.Timer | undefined; 166 | 167 | /** 168 | * 请求注释器 - 带防抖功能 169 | */ 170 | export const requestAnnotators = (editor: vscode.TextEditor, client: LanguageClient): void => { 171 | if (timeoutToReqAnn) { 172 | clearTimeout(timeoutToReqAnn); 173 | } 174 | timeoutToReqAnn = setTimeout(() => { 175 | requestAnnotatorsImpl(editor, client); 176 | }, 150); 177 | }; 178 | 179 | /** 180 | * 异步请求注释器实现 181 | */ 182 | const requestAnnotatorsImpl = async (editor: vscode.TextEditor, client: LanguageClient): Promise => { 183 | // 确保装饰器已初始化 184 | if (Object.keys(decorations).length === 0) { 185 | updateDecorations(); 186 | } 187 | 188 | const params: notifications.AnnotatorParams = { 189 | uri: editor.document.uri.toString() 190 | }; 191 | 192 | try { 193 | const annotationList = await client.sendRequest("emmy/annotator", params); 194 | 195 | if (!annotationList) { 196 | return; 197 | } 198 | 199 | // 使用 Map 来优化数据收集 200 | const rangeMap = new Map([ 201 | [AnnotatorType.ReadOnlyParam, []], 202 | [AnnotatorType.Global, []], 203 | [AnnotatorType.ReadOnlyLocal, []], 204 | [AnnotatorType.MutLocal, []], 205 | [AnnotatorType.MutParam, []], 206 | [AnnotatorType.DocEm, []], 207 | [AnnotatorType.DocStrong, []], 208 | ]); 209 | 210 | // 批量处理注释 211 | for (const annotation of annotationList) { 212 | const ranges = rangeMap.get(annotation.type); 213 | if (ranges) { 214 | ranges.push(...annotation.ranges); 215 | } 216 | } 217 | 218 | // 批量更新装饰器 219 | rangeMap.forEach((ranges, type) => { 220 | updateAnnotators(editor, type, ranges); 221 | }); 222 | } catch (error) { 223 | console.error('Failed to get annotations from language server:', error); 224 | } 225 | }; 226 | 227 | /** 228 | * 更新编辑器中特定类型的注释器 229 | */ 230 | const updateAnnotators = ( 231 | editor: vscode.TextEditor, 232 | type: AnnotatorType, 233 | ranges: vscode.Range[] 234 | ): void => { 235 | const decoration = decorations[type]; 236 | if (decoration) { 237 | editor.setDecorations(decoration, ranges); 238 | } 239 | }; 240 | 241 | /** 242 | * 清理所有缓存和装饰器 - 用于扩展停用时清理 243 | */ 244 | export const dispose = (): void => { 245 | // 清理防抖定时器 246 | if (timeoutToReqAnn) { 247 | clearTimeout(timeoutToReqAnn); 248 | timeoutToReqAnn = undefined; 249 | } 250 | 251 | // 清理所有装饰器 252 | disposeDecorations(...Object.values(decorations)); 253 | decorations = {}; 254 | 255 | // 清理缓存中的装饰器 256 | decorationCache.forEach(decoration => decoration.dispose()); 257 | decorationCache.clear(); 258 | }; 259 | -------------------------------------------------------------------------------- /src/syntaxTreeProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { LanguageClient } from 'vscode-languageclient/node'; 4 | import { SyntaxTreeParams, SyntaxTreeResponse } from './lspExtension'; 5 | 6 | // Forward declaration - will be set by extension.ts 7 | let getClient: (() => LanguageClient | undefined) | undefined; 8 | 9 | /** 10 | * Set the function to get the language client 11 | * This is called by extension.ts during activation 12 | */ 13 | export function setClientGetter(getter: () => LanguageClient | undefined): void { 14 | getClient = getter; 15 | } 16 | 17 | /** 18 | * Provides syntax tree content as virtual documents 19 | * Similar to rust-analyzer's syntax tree viewer 20 | */ 21 | export class SyntaxTreeProvider implements vscode.TextDocumentContentProvider { 22 | static readonly SCHEME = 'emmylua-syntax-tree'; 23 | 24 | private readonly _onDidChange = new vscode.EventEmitter(); 25 | private cache = new Map(); 26 | 27 | readonly onDidChange = this._onDidChange.event; 28 | 29 | /** 30 | * Create a URI for viewing syntax tree of a document 31 | */ 32 | static createUri(sourceUri: vscode.Uri): vscode.Uri { 33 | const fileName = path.basename(sourceUri.fsPath); 34 | return vscode.Uri.parse( 35 | `${SyntaxTreeProvider.SCHEME}:Syntax Tree [${fileName}]?${encodeURIComponent(sourceUri.toString())}` 36 | ); 37 | } 38 | 39 | /** 40 | * Parse source URI from syntax tree URI 41 | */ 42 | private parseSourceUri(uri: vscode.Uri): vscode.Uri | undefined { 43 | const query = uri.query; 44 | if (!query) { 45 | return undefined; 46 | } 47 | try { 48 | return vscode.Uri.parse(decodeURIComponent(query)); 49 | } catch { 50 | return undefined; 51 | } 52 | } 53 | 54 | /** 55 | * Provide syntax tree content for a virtual document 56 | */ 57 | async provideTextDocumentContent( 58 | uri: vscode.Uri, 59 | token: vscode.CancellationToken 60 | ): Promise { 61 | const sourceUri = this.parseSourceUri(uri); 62 | if (!sourceUri) { 63 | return 'Error: Invalid syntax tree URI'; 64 | } 65 | 66 | // Check cache first 67 | const cacheKey = sourceUri.toString(); 68 | if (this.cache.has(cacheKey)) { 69 | return this.cache.get(cacheKey); 70 | } 71 | 72 | return this.generateSyntaxTree(sourceUri, token); 73 | } 74 | 75 | /** 76 | * Generate syntax tree from language server 77 | */ 78 | private async generateSyntaxTree( 79 | sourceUri: vscode.Uri, 80 | token: vscode.CancellationToken 81 | ): Promise { 82 | const client = await this.getLanguageClient(); 83 | 84 | if (!client) { 85 | return '// Language server is not running\n// Please ensure EmmyLua language server is started'; 86 | } 87 | 88 | try { 89 | const params: SyntaxTreeParams = { 90 | uri: sourceUri.toString() 91 | }; 92 | 93 | const result = await client.sendRequest( 94 | 'emmy/syntaxTree', 95 | params, 96 | token 97 | ); 98 | 99 | if (!result || !result.content) { 100 | return '// Failed to get syntax tree\n// The language server did not return any data'; 101 | } 102 | 103 | // Cache the result 104 | this.cache.set(sourceUri.toString(), result.content); 105 | 106 | return result.content; 107 | 108 | } catch (error) { 109 | const errorMessage = error instanceof Error ? error.message : String(error); 110 | console.error('Failed to generate syntax tree:', error); 111 | return `// Error generating syntax tree\n// ${errorMessage}`; 112 | } 113 | } 114 | 115 | /** 116 | * Get the language client from the extension context 117 | */ 118 | private async getLanguageClient(): Promise { 119 | if (getClient) { 120 | return getClient(); 121 | } 122 | return undefined; 123 | } 124 | 125 | /** 126 | * Refresh syntax tree for a source document 127 | */ 128 | refresh(sourceUri: vscode.Uri): void { 129 | const cacheKey = sourceUri.toString(); 130 | this.cache.delete(cacheKey); 131 | 132 | const syntaxTreeUri = SyntaxTreeProvider.createUri(sourceUri); 133 | this._onDidChange.fire(syntaxTreeUri); 134 | } 135 | 136 | /** 137 | * Clear all cached syntax trees 138 | */ 139 | clearCache(): void { 140 | this.cache.clear(); 141 | } 142 | 143 | /** 144 | * Dispose resources 145 | */ 146 | dispose(): void { 147 | this._onDidChange.dispose(); 148 | this.cache.clear(); 149 | } 150 | } 151 | 152 | /** 153 | * Manage syntax tree viewing and updates 154 | */ 155 | export class SyntaxTreeManager implements vscode.Disposable { 156 | private provider: SyntaxTreeProvider; 157 | private disposables: vscode.Disposable[] = []; 158 | private autoUpdateEnabled = true; 159 | 160 | constructor() { 161 | this.provider = new SyntaxTreeProvider(); 162 | 163 | // Register content provider 164 | this.disposables.push( 165 | vscode.workspace.registerTextDocumentContentProvider( 166 | SyntaxTreeProvider.SCHEME, 167 | this.provider 168 | ) 169 | ); 170 | 171 | // Auto-refresh on document changes 172 | this.disposables.push( 173 | vscode.workspace.onDidChangeTextDocument(e => { 174 | if (this.autoUpdateEnabled && this.isWatchingDocument(e.document.uri)) { 175 | // Debounce updates 176 | this.scheduleRefresh(e.document.uri); 177 | } 178 | }) 179 | ); 180 | 181 | // Refresh when switching editors 182 | this.disposables.push( 183 | vscode.window.onDidChangeActiveTextEditor(editor => { 184 | if (editor && this.autoUpdateEnabled) { 185 | this.provider.refresh(editor.document.uri); 186 | } 187 | }) 188 | ); 189 | } 190 | 191 | private updateTimeout?: NodeJS.Timeout; 192 | 193 | private scheduleRefresh(uri: vscode.Uri): void { 194 | if (this.updateTimeout) { 195 | clearTimeout(this.updateTimeout); 196 | } 197 | 198 | this.updateTimeout = setTimeout(() => { 199 | this.provider.refresh(uri); 200 | }, 500); // 500ms debounce 201 | } 202 | 203 | /** 204 | * Check if we're currently watching a document for syntax tree updates 205 | */ 206 | private isWatchingDocument(uri: vscode.Uri): boolean { 207 | // Check if there's an open syntax tree viewer for this document 208 | return vscode.window.visibleTextEditors.some(editor => { 209 | if (editor.document.uri.scheme !== SyntaxTreeProvider.SCHEME) { 210 | return false; 211 | } 212 | 213 | // Parse the source URI from the syntax tree document 214 | const query = editor.document.uri.query; 215 | if (!query) { 216 | return false; 217 | } 218 | 219 | try { 220 | const sourceUri = vscode.Uri.parse(decodeURIComponent(query)); 221 | return sourceUri.toString() === uri.toString(); 222 | } catch { 223 | return false; 224 | } 225 | }); 226 | } 227 | 228 | /** 229 | * Show syntax tree for a document 230 | */ 231 | async show(sourceUri: vscode.Uri, selection?: vscode.Selection): Promise { 232 | const syntaxTreeUri = SyntaxTreeProvider.createUri(sourceUri); 233 | 234 | try { 235 | const doc = await vscode.workspace.openTextDocument(syntaxTreeUri); 236 | await vscode.window.showTextDocument(doc, { 237 | viewColumn: vscode.ViewColumn.Beside, 238 | preview: false, 239 | preserveFocus: false 240 | }); 241 | } catch (error) { 242 | const errorMessage = error instanceof Error ? error.message : String(error); 243 | vscode.window.showErrorMessage(`Failed to show syntax tree: ${errorMessage}`); 244 | } 245 | } 246 | 247 | /** 248 | * Toggle auto-update for syntax trees 249 | */ 250 | toggleAutoUpdate(): void { 251 | this.autoUpdateEnabled = !this.autoUpdateEnabled; 252 | const status = this.autoUpdateEnabled ? 'enabled' : 'disabled'; 253 | vscode.window.showInformationMessage(`Syntax tree auto-update ${status}`); 254 | } 255 | 256 | /** 257 | * Manually refresh current syntax tree 258 | */ 259 | refresh(): void { 260 | const editor = vscode.window.activeTextEditor; 261 | if (editor) { 262 | this.provider.refresh(editor.document.uri); 263 | } 264 | } 265 | 266 | /** 267 | * Dispose all resources 268 | */ 269 | dispose(): void { 270 | if (this.updateTimeout) { 271 | clearTimeout(this.updateTimeout); 272 | } 273 | this.provider.dispose(); 274 | this.disposables.forEach(d => d.dispose()); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/luarocks/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LuaRocksManager } from "./LuaRocksManager"; 3 | import { LuaRocksTreeProvider, PackageTreeItem } from './LuaRocksTreeProvider'; 4 | import { extensionContext } from '../extension'; 5 | 6 | 7 | let luaRocksManager: LuaRocksManager | undefined; 8 | let luaRocksTreeProvider: LuaRocksTreeProvider | undefined; 9 | 10 | export async function initializeLuaRocks(): Promise { 11 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 12 | if (!workspaceFolder) { 13 | return; 14 | } 15 | 16 | const rockspecFiles = await vscode.workspace.findFiles( 17 | new vscode.RelativePattern(workspaceFolder, '*.rockspec'), 18 | null, 19 | 10 20 | ); 21 | 22 | if (rockspecFiles.length === 0) { 23 | return; 24 | } 25 | 26 | // Initialize LuaRocks manager 27 | luaRocksManager = new LuaRocksManager(workspaceFolder); 28 | luaRocksTreeProvider = new LuaRocksTreeProvider(luaRocksManager); 29 | 30 | // Register tree view 31 | const treeView = vscode.window.createTreeView('emmylua.luarocks', { 32 | treeDataProvider: luaRocksTreeProvider, 33 | showCollapseAll: true 34 | }); 35 | 36 | extensionContext.vscodeContext.subscriptions.push( 37 | treeView, 38 | luaRocksManager, 39 | luaRocksTreeProvider 40 | ); 41 | 42 | const isInstalled = await luaRocksManager.checkLuaRocksInstallation(); 43 | if (isInstalled) { 44 | let workspace = await luaRocksManager.detectLuaRocksWorkspace(); 45 | if (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 | } 52 | } 53 | } 54 | } 55 | 56 | export async function searchPackages(): Promise { 57 | if (!luaRocksManager || !luaRocksTreeProvider) { 58 | vscode.window.showErrorMessage('LuaRocks not initialized'); 59 | return; 60 | } 61 | 62 | const query = await vscode.window.showInputBox({ 63 | prompt: 'Enter package name or search term', 64 | placeHolder: 'e.g., lpeg, luasocket, etc.' 65 | }); 66 | 67 | if (!query) { 68 | return; 69 | } 70 | 71 | try { 72 | const packages = await luaRocksManager.searchPackages(query); 73 | if (packages.length === 0) { 74 | vscode.window.showInformationMessage(`No packages found for "${query}"`); 75 | } else { 76 | luaRocksTreeProvider.setSearchResults(packages); 77 | vscode.window.showInformationMessage(`Found ${packages.length} package(s) for "${query}"`); 78 | } 79 | } catch (error) { 80 | vscode.window.showErrorMessage(`Search failed: ${error}`); 81 | } 82 | } 83 | 84 | export async function installPackage(item?: PackageTreeItem): Promise { 85 | if (!luaRocksManager || !luaRocksTreeProvider) { 86 | vscode.window.showErrorMessage('LuaRocks not initialized'); 87 | return; 88 | } 89 | 90 | let packageName: string; 91 | let packageVersion: string | undefined; 92 | 93 | if (item && item.packageInfo) { 94 | packageName = item.packageInfo.name; 95 | packageVersion = item.packageInfo.version; 96 | } else { 97 | const input = await vscode.window.showInputBox({ 98 | prompt: 'Enter package name (optionally with version)', 99 | placeHolder: 'e.g., lpeg or lpeg 1.0.2' 100 | }); 101 | 102 | if (!input) { 103 | return; 104 | } 105 | 106 | const parts = input.trim().split(/\s+/); 107 | packageName = parts[0]; 108 | packageVersion = parts[1]; 109 | } 110 | 111 | const success = await luaRocksManager.installPackage(packageName, packageVersion); 112 | if (success) { 113 | luaRocksTreeProvider.refreshInstalled(); 114 | } 115 | } 116 | 117 | export async function uninstallPackage(item: PackageTreeItem): Promise { 118 | if (!luaRocksManager || !luaRocksTreeProvider) { 119 | vscode.window.showErrorMessage('LuaRocks not initialized'); 120 | return; 121 | } 122 | 123 | if (!item.packageInfo) { 124 | vscode.window.showErrorMessage('No package selected'); 125 | return; 126 | } 127 | 128 | const packageName = item.packageInfo.name; 129 | const confirm = await vscode.window.showWarningMessage( 130 | `Are you sure you want to uninstall "${packageName}"?`, 131 | 'Yes', 132 | 'No' 133 | ); 134 | 135 | if (confirm === 'Yes') { 136 | const success = await luaRocksManager.uninstallPackage(packageName); 137 | if (success) { 138 | luaRocksTreeProvider.refreshInstalled(); 139 | } 140 | } 141 | } 142 | 143 | export async function showPackageInfo(item: PackageTreeItem): Promise { 144 | if (!luaRocksManager) { 145 | vscode.window.showErrorMessage('LuaRocks not initialized'); 146 | return; 147 | } 148 | 149 | if (!item.packageInfo) { 150 | vscode.window.showErrorMessage('No package selected'); 151 | return; 152 | } 153 | 154 | const packageInfo = await luaRocksManager.getPackageInfo(item.packageInfo.name); 155 | if (!packageInfo) { 156 | vscode.window.showErrorMessage('Failed to get package information'); 157 | return; 158 | } 159 | 160 | // 使用QuickPick显示包信息,这样不会影响树视图 161 | const quickPick = vscode.window.createQuickPick(); 162 | quickPick.title = `Package: ${packageInfo.name}`; 163 | quickPick.placeholder = 'Package Information'; 164 | 165 | const items: vscode.QuickPickItem[] = [ 166 | { 167 | label: `$(package) ${packageInfo.name}`, 168 | description: `Version: ${packageInfo.version || 'Unknown'}`, 169 | detail: packageInfo.description || packageInfo.summary || 'No description available' 170 | } 171 | ]; 172 | 173 | if (packageInfo.author) { 174 | items.push({ 175 | label: `$(person) Author`, 176 | description: packageInfo.author 177 | }); 178 | } 179 | 180 | if (packageInfo.license) { 181 | items.push({ 182 | label: `$(law) License`, 183 | description: packageInfo.license 184 | }); 185 | } 186 | 187 | if (packageInfo.homepage) { 188 | items.push({ 189 | label: `$(link-external) Homepage`, 190 | description: packageInfo.homepage, 191 | detail: 'Click to open in browser' 192 | }); 193 | } 194 | 195 | if (packageInfo.location && packageInfo.installed) { 196 | items.push({ 197 | label: `$(folder) Location`, 198 | description: packageInfo.location 199 | }); 200 | } 201 | 202 | items.push({ 203 | label: `$(info) Status`, 204 | description: packageInfo.installed ? 'Installed' : 'Available for installation' 205 | }); 206 | 207 | // 添加操作按钮 208 | if (packageInfo.installed) { 209 | items.push({ 210 | label: `$(trash) Uninstall Package`, 211 | description: 'Remove this package', 212 | detail: 'Click to uninstall' 213 | }); 214 | } else { 215 | items.push({ 216 | label: `$(cloud-download) Install Package`, 217 | description: 'Install this package', 218 | detail: 'Click to install' 219 | }); 220 | } 221 | 222 | quickPick.items = items; 223 | 224 | quickPick.onDidAccept(() => { 225 | const selected = quickPick.selectedItems[0]; 226 | if (selected) { 227 | if (selected.label.includes('Homepage') && packageInfo.homepage) { 228 | vscode.env.openExternal(vscode.Uri.parse(packageInfo.homepage)); 229 | } else if (selected.label.includes('Uninstall')) { 230 | quickPick.hide(); 231 | uninstallPackage(item); 232 | } else if (selected.label.includes('Install')) { 233 | quickPick.hide(); 234 | installPackage(item); 235 | } 236 | } 237 | }); 238 | 239 | quickPick.show(); 240 | } 241 | 242 | export async function refreshPackages(): Promise { 243 | if (!luaRocksTreeProvider) { 244 | vscode.window.showErrorMessage('LuaRocks not initialized'); 245 | return; 246 | } 247 | 248 | await luaRocksTreeProvider.refreshInstalled(); 249 | vscode.window.showInformationMessage('Package list refreshed'); 250 | } 251 | 252 | export async function showPackagesView(): Promise { 253 | await vscode.commands.executeCommand('emmylua.luarocks.focus'); 254 | } 255 | 256 | export function clearSearch(): void { 257 | if (!luaRocksTreeProvider) { 258 | vscode.window.showErrorMessage('LuaRocks not initialized'); 259 | return; 260 | } 261 | 262 | luaRocksTreeProvider.clearSearch(); 263 | } 264 | 265 | export async function checkLuaRocksInstallation(): Promise { 266 | if (!luaRocksManager) { 267 | vscode.window.showErrorMessage('LuaRocks not initialized'); 268 | return; 269 | } 270 | 271 | const isInstalled = await luaRocksManager.checkLuaRocksInstallation(); 272 | if (isInstalled) { 273 | vscode.window.showInformationMessage('LuaRocks is installed and ready to use'); 274 | } else { 275 | const action = await vscode.window.showWarningMessage( 276 | 'LuaRocks is not installed or not in PATH', 277 | 'Install Guide', 278 | 'Dismiss' 279 | ); 280 | if (action === 'Install Guide') { 281 | vscode.env.openExternal(vscode.Uri.parse('https://luarocks.org/#quick-start')); 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /src/emmyContext.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LanguageClient } from 'vscode-languageclient/node'; 3 | 4 | /** 5 | * Server health status 6 | */ 7 | export enum ServerState { 8 | /** Server is starting up */ 9 | Starting = 'starting', 10 | /** Server is running normally */ 11 | Running = 'running', 12 | /** Server is stopping */ 13 | Stopping = 'stopping', 14 | /** Server is stopped */ 15 | Stopped = 'stopped', 16 | /** Server encountered a warning */ 17 | Warning = 'warning', 18 | /** Server encountered an error */ 19 | Error = 'error', 20 | } 21 | 22 | /** 23 | * Server status information 24 | */ 25 | export interface ServerStatus { 26 | /** Current server state */ 27 | state: ServerState; 28 | /** Optional status message */ 29 | message?: string; 30 | /** Additional details for tooltip */ 31 | details?: string; 32 | } 33 | 34 | /** 35 | * Status bar configuration 36 | */ 37 | interface StatusBarConfig { 38 | readonly icon: string; 39 | readonly color?: vscode.ThemeColor; 40 | readonly backgroundColor?: vscode.ThemeColor; 41 | } 42 | 43 | interface TooltipAction { 44 | readonly label: string; 45 | readonly command: string; 46 | readonly icon: string; 47 | readonly tooltip?: string; 48 | } 49 | 50 | /** 51 | * EmmyLua extension context manager 52 | * Manages language client, status bar, and extension state 53 | */ 54 | export class EmmyContext implements vscode.Disposable { 55 | public readonly LANGUAGE_ID = 'lua' as const; 56 | 57 | private _client?: LanguageClient; 58 | private _serverStatus: ServerStatus; 59 | private readonly _statusBar: vscode.StatusBarItem; 60 | private readonly _disposables: vscode.Disposable[] = []; 61 | 62 | constructor( 63 | public readonly debugMode: boolean, 64 | public readonly vscodeContext: vscode.ExtensionContext, 65 | ) { 66 | this._serverStatus = { state: ServerState.Stopped }; 67 | this._statusBar = vscode.window.createStatusBarItem( 68 | vscode.StatusBarAlignment.Left, 69 | 100 70 | ); 71 | 72 | // Status bar click shows quick pick menu instead of direct action 73 | this._statusBar.command = 'emmy.showServerMenu'; 74 | 75 | this._disposables.push(this._statusBar); 76 | this.updateStatusBar(); 77 | } 78 | 79 | // ==================== Public API ==================== 80 | 81 | /** 82 | * Get the language client instance 83 | */ 84 | get client(): LanguageClient | undefined { 85 | return this._client; 86 | } 87 | 88 | /** 89 | * Set the language client instance 90 | */ 91 | set client(value: LanguageClient | undefined) { 92 | this._client = value; 93 | } 94 | 95 | /** 96 | * Get current server status 97 | */ 98 | get serverStatus(): Readonly { 99 | return this._serverStatus; 100 | } 101 | 102 | /** 103 | * Check if server is running 104 | */ 105 | get isServerRunning(): boolean { 106 | return this._serverStatus.state === ServerState.Running; 107 | } 108 | 109 | /** 110 | * Check if server is starting 111 | */ 112 | get isServerStarting(): boolean { 113 | return this._serverStatus.state === ServerState.Starting; 114 | } 115 | 116 | /** 117 | * Update server status to starting 118 | */ 119 | setServerStarting(message?: string): void { 120 | this._serverStatus = { 121 | state: ServerState.Starting, 122 | message: message || 'Starting EmmyLua language server...', 123 | }; 124 | this.updateStatusBar(); 125 | } 126 | 127 | /** 128 | * Update server status to running 129 | */ 130 | setServerRunning(message?: string): void { 131 | this._serverStatus = { 132 | state: ServerState.Running, 133 | message: message || 'EmmyLua language server is running', 134 | }; 135 | this.updateStatusBar(); 136 | } 137 | 138 | /** 139 | * Update server status to stopping 140 | */ 141 | setServerStopping(message?: string): void { 142 | this._serverStatus = { 143 | state: ServerState.Stopping, 144 | message: message || 'Stopping EmmyLua language server...', 145 | }; 146 | this.updateStatusBar(); 147 | } 148 | 149 | /** 150 | * Update server status to stopped 151 | */ 152 | setServerStopped(message?: string): void { 153 | this._serverStatus = { 154 | state: ServerState.Stopped, 155 | message: message || 'EmmyLua language server is stopped', 156 | }; 157 | this.updateStatusBar(); 158 | } 159 | 160 | /** 161 | * Update server status to warning 162 | */ 163 | setServerWarning(message: string, details?: string): void { 164 | this._serverStatus = { 165 | state: ServerState.Warning, 166 | message, 167 | details, 168 | }; 169 | this.updateStatusBar(); 170 | } 171 | 172 | /** 173 | * Update server status to error 174 | */ 175 | setServerError(message: string, details?: string): void { 176 | this._serverStatus = { 177 | state: ServerState.Error, 178 | message, 179 | details, 180 | }; 181 | this.updateStatusBar(); 182 | } 183 | 184 | /** 185 | * Stop the language server 186 | */ 187 | async stopServer(): Promise { 188 | if (!this._client) { 189 | return; 190 | } 191 | 192 | this.setServerStopping(); 193 | try { 194 | await this._client.stop(); 195 | this.setServerStopped(); 196 | } catch (error) { 197 | this.setServerError('Failed to stop server', String(error)); 198 | } 199 | } 200 | 201 | /** 202 | * Show server control menu 203 | */ 204 | async showServerMenu(): Promise { 205 | const items: vscode.QuickPickItem[] = []; 206 | 207 | // Build menu based on current state 208 | if (this.isServerRunning) { 209 | items.push( 210 | { 211 | label: '$(debug-restart) Restart Server', 212 | description: 'Restart the language server', 213 | detail: 'Stop and restart the EmmyLua language server', 214 | }, 215 | { 216 | label: '$(stop-circle) Stop Server', 217 | description: 'Stop the language server', 218 | detail: 'Gracefully stop the EmmyLua language server', 219 | } 220 | ); 221 | } else { 222 | items.push({ 223 | label: '$(play) Start Server', 224 | description: 'Start the language server', 225 | detail: 'Start the EmmyLua language server', 226 | }); 227 | } 228 | 229 | items.push( 230 | { 231 | label: '$(info) Show Server Info', 232 | description: 'Display server information', 233 | detail: 'Show detailed server status and configuration', 234 | }, 235 | { 236 | label: '$(output) Show Output', 237 | description: 'Open output channel', 238 | detail: 'View server logs and output', 239 | }, 240 | { 241 | label: '$(symbol-structure) Show Syntax Tree', 242 | description: 'View syntax tree for current file', 243 | detail: 'Display the syntax tree of the active Lua file', 244 | } 245 | ); 246 | 247 | const selected = await vscode.window.showQuickPick(items, { 248 | placeHolder: 'EmmyLua Language Server', 249 | title: 'Server Control', 250 | }); 251 | 252 | if (!selected) { 253 | return; 254 | } 255 | 256 | // Execute selected action 257 | if (selected.label.includes('Restart')) { 258 | await vscode.commands.executeCommand('emmy.restartServer'); 259 | } else if (selected.label.includes('Stop')) { 260 | await vscode.commands.executeCommand('emmy.stopServer'); 261 | } else if (selected.label.includes('Start')) { 262 | await vscode.commands.executeCommand('emmy.startServer'); 263 | } else if (selected.label.includes('Server Info')) { 264 | this.showServerInfo(); 265 | } else if (selected.label.includes('Output')) { 266 | this._client?.outputChannel?.show(); 267 | } else if (selected.label.includes('Syntax Tree')) { 268 | await vscode.commands.executeCommand('emmy.showSyntaxTree'); 269 | } 270 | } 271 | 272 | /** 273 | * Show detailed server information 274 | */ 275 | private showServerInfo(): void { 276 | const info: string[] = [ 277 | '# EmmyLua Language Server', 278 | '', 279 | `**Status:** ${this._serverStatus.state}`, 280 | ]; 281 | 282 | if (this._serverStatus.message) { 283 | info.push(`**Message:** ${this._serverStatus.message}`); 284 | } 285 | 286 | if (this.debugMode) { 287 | info.push('', `**Debug Mode:** Enabled`); 288 | } 289 | 290 | if (this._serverStatus.details) { 291 | info.push('', '## Details', '', this._serverStatus.details); 292 | } 293 | 294 | const doc = vscode.workspace.openTextDocument({ 295 | content: info.join('\n'), 296 | language: 'markdown', 297 | }); 298 | 299 | doc.then((document) => { 300 | vscode.window.showTextDocument(document, { 301 | preview: true, 302 | viewColumn: vscode.ViewColumn.Beside, 303 | }); 304 | }); 305 | } 306 | 307 | // ==================== Private Methods ==================== 308 | 309 | /** 310 | * Update status bar display 311 | */ 312 | private updateStatusBar(): void { 313 | const config = this.getStatusBarConfig(); 314 | 315 | this._statusBar.text = `${config.icon}EmmyLua`; 316 | this._statusBar.color = config.color; 317 | this._statusBar.backgroundColor = config.backgroundColor; 318 | this._statusBar.tooltip = this.createTooltip(); 319 | this._statusBar.show(); 320 | } 321 | 322 | /** 323 | * Get status bar configuration based on current state 324 | */ 325 | private getStatusBarConfig(): StatusBarConfig { 326 | const configs: Record = { 327 | [ServerState.Starting]: { 328 | icon: '$(sync~spin) ', 329 | }, 330 | [ServerState.Running]: { 331 | icon: '$(check) ', 332 | }, 333 | [ServerState.Stopping]: { 334 | icon: '$(sync~spin) ', 335 | }, 336 | [ServerState.Stopped]: { 337 | icon: '$(circle-slash) ', 338 | }, 339 | [ServerState.Warning]: { 340 | icon: '$(warning) ', 341 | color: new vscode.ThemeColor('statusBarItem.warningForeground'), 342 | backgroundColor: new vscode.ThemeColor('statusBarItem.warningBackground'), 343 | }, 344 | [ServerState.Error]: { 345 | icon: '$(error) ', 346 | color: new vscode.ThemeColor('statusBarItem.errorForeground'), 347 | backgroundColor: new vscode.ThemeColor('statusBarItem.errorBackground'), 348 | }, 349 | }; 350 | 351 | return configs[this._serverStatus.state]; 352 | } 353 | 354 | /** 355 | * Create tooltip content 356 | */ 357 | private createTooltip(): vscode.MarkdownString { 358 | const tooltip = new vscode.MarkdownString('', true); 359 | tooltip.isTrusted = true; 360 | 361 | // Title 362 | tooltip.appendMarkdown('**EmmyLua Language Server**\n\n'); 363 | tooltip.appendMarkdown('---\n\n'); 364 | 365 | // Status 366 | tooltip.appendMarkdown(`Status: \`${this._serverStatus.state}\`\n\n`); 367 | 368 | const actions = this.getTooltipActions(); 369 | if (actions.length) { 370 | const links = actions.map((action) => { 371 | const tooltipText = action.tooltip 372 | ? ` "${action.tooltip.replace(/"/g, '\\"')}"` 373 | : ''; 374 | return `[${action.icon} ${action.label}](command:${action.command}${tooltipText})`; 375 | }); 376 | tooltip.appendMarkdown(links.join('\n\n')); 377 | tooltip.appendMarkdown('\n\n'); 378 | } 379 | 380 | return tooltip; 381 | } 382 | 383 | /** 384 | * Quick actions shown in the tooltip 385 | */ 386 | private getTooltipActions(): TooltipAction[] { 387 | const actions: TooltipAction[] = []; 388 | const state = this._serverStatus.state; 389 | 390 | if (state !== ServerState.Stopped && state !== ServerState.Stopping) { 391 | actions.push({ 392 | label: 'Stop server', 393 | command: 'emmy.stopServer', 394 | icon: '$(stop-circle)', 395 | tooltip: 'Stop the EmmyLua language server', 396 | }); 397 | } 398 | 399 | actions.push({ 400 | label: 'Restart server', 401 | command: 'emmy.restartServer', 402 | icon: '$(debug-restart)', 403 | tooltip: 'Restart the EmmyLua language server', 404 | }); 405 | return actions; 406 | } 407 | 408 | /** 409 | * Dispose all resources 410 | */ 411 | dispose(): void { 412 | this._client?.stop(); 413 | this._disposables.forEach((disposable) => disposable.dispose()); 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as net from 'net'; 4 | import * as process from 'process'; 5 | import * as os from 'os'; 6 | import * as fs from 'fs'; 7 | 8 | import { LanguageClient, LanguageClientOptions, ServerOptions, StreamInfo } from 'vscode-languageclient/node'; 9 | import { LuaLanguageConfiguration } from './languageConfiguration'; 10 | import { EmmyContext } from './emmyContext'; 11 | import { IServerLocation, IServerPosition } from './lspExtension'; 12 | import { onDidChangeConfiguration } from './annotator'; 13 | import { ConfigurationManager } from './configRenames'; 14 | import * as Annotator from './annotator'; 15 | import { EmmyrcSchemaContentProvider } from './emmyrcSchemaContentProvider'; 16 | import { SyntaxTreeManager, setClientGetter } from './syntaxTreeProvider'; 17 | import { registerTerminalLinkProvider } from './luaTerminalLinkProvider'; 18 | import { insertEmmyDebugCode, registerDebuggers } from './debugger'; 19 | import * as LuaRocks from './luarocks'; 20 | 21 | /** 22 | * Command registration entry 23 | */ 24 | interface CommandEntry { 25 | readonly id: string; 26 | readonly handler: (...args: any[]) => any; 27 | } 28 | 29 | // Global state 30 | export let extensionContext: EmmyContext; 31 | let activeEditor: vscode.TextEditor | undefined; 32 | 33 | let syntaxTreeManager: SyntaxTreeManager | undefined; 34 | 35 | /** 36 | * Extension activation entry point 37 | */ 38 | export async function activate(context: vscode.ExtensionContext): Promise { 39 | console.log('EmmyLua extension activated!'); 40 | 41 | // Provide `.emmyrc.json` schema with i18n support 42 | context.subscriptions.push( 43 | vscode.workspace.registerTextDocumentContentProvider( 44 | 'emmyrc-schema', 45 | new EmmyrcSchemaContentProvider(context) 46 | ) 47 | ); 48 | 49 | // Initialize extension context 50 | extensionContext = new EmmyContext( 51 | process.env['EMMY_DEV'] === 'true', 52 | context 53 | ); 54 | 55 | // Register all components 56 | registerCommands(context); 57 | registerEventListeners(context); 58 | registerLanguageConfiguration(context); 59 | registerTerminalLinkProvider(context); 60 | 61 | // Initialize features 62 | await initializeExtension(); 63 | } 64 | 65 | /** 66 | * Extension deactivation 67 | */ 68 | export function deactivate(): void { 69 | extensionContext?.dispose(); 70 | Annotator.dispose(); 71 | } 72 | 73 | /** 74 | * Register all commands 75 | */ 76 | function registerCommands(context: vscode.ExtensionContext): void { 77 | const commandEntries: CommandEntry[] = [ 78 | // Server commands 79 | { id: 'emmy.stopServer', handler: stopServer }, 80 | { id: 'emmy.restartServer', handler: restartServer }, 81 | { id: 'emmy.showServerMenu', handler: showServerMenu }, 82 | { id: 'emmy.showReferences', handler: showReferences }, 83 | { id: 'emmy.showSyntaxTree', handler: showSyntaxTree }, 84 | // debugger commands 85 | { id: 'emmy.insertEmmyDebugCode', handler: insertEmmyDebugCode }, 86 | // LuaRocks commands 87 | { id: 'emmylua.luarocks.searchPackages', handler: LuaRocks.searchPackages }, 88 | { id: 'emmylua.luarocks.installPackage', handler: LuaRocks.installPackage }, 89 | { id: 'emmylua.luarocks.uninstallPackage', handler: LuaRocks.uninstallPackage }, 90 | { id: 'emmylua.luarocks.showPackageInfo', handler: LuaRocks.showPackageInfo }, 91 | { id: 'emmylua.luarocks.refreshPackages', handler: LuaRocks.refreshPackages }, 92 | { id: 'emmylua.luarocks.showPackages', handler: LuaRocks.showPackagesView }, 93 | { id: 'emmylua.luarocks.clearSearch', handler: LuaRocks.clearSearch }, 94 | { id: 'emmylua.luarocks.checkInstallation', handler: LuaRocks.checkLuaRocksInstallation }, 95 | ]; 96 | 97 | // Register all commands 98 | const commands = commandEntries.map(({ id, handler }) => 99 | vscode.commands.registerCommand(id, handler) 100 | ); 101 | 102 | context.subscriptions.push(...commands); 103 | } 104 | 105 | /** 106 | * Register event listeners 107 | */ 108 | function registerEventListeners(context: vscode.ExtensionContext): void { 109 | const eventListeners = [ 110 | vscode.workspace.onDidChangeTextDocument(onDidChangeTextDocument), 111 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 112 | vscode.workspace.onDidChangeConfiguration(onConfigurationChanged), 113 | ]; 114 | 115 | context.subscriptions.push(...eventListeners); 116 | } 117 | 118 | /** 119 | * Register language configuration 120 | */ 121 | function registerLanguageConfiguration(context: vscode.ExtensionContext): void { 122 | const languageConfig = vscode.languages.setLanguageConfiguration( 123 | 'lua', 124 | new LuaLanguageConfiguration() 125 | ); 126 | 127 | context.subscriptions.push(languageConfig); 128 | } 129 | 130 | /** 131 | * Initialize all extension features 132 | */ 133 | async function initializeExtension(): Promise { 134 | // Initialize syntax tree manager 135 | syntaxTreeManager = new SyntaxTreeManager(); 136 | extensionContext.vscodeContext.subscriptions.push(syntaxTreeManager); 137 | 138 | // Set up client getter for syntax tree provider 139 | setClientGetter(() => extensionContext.client); 140 | 141 | await startServer(); 142 | registerDebuggers(); 143 | await LuaRocks.initializeLuaRocks(); 144 | } 145 | 146 | function onConfigurationChanged(e: vscode.ConfigurationChangeEvent): void { 147 | if (e.affectsConfiguration('emmylua')) { 148 | onDidChangeConfiguration(); 149 | } 150 | } 151 | 152 | function onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): void { 153 | if (activeEditor && 154 | activeEditor.document === event.document && 155 | activeEditor.document.languageId === extensionContext.LANGUAGE_ID && 156 | extensionContext.client 157 | ) { 158 | Annotator.requestAnnotators(activeEditor, extensionContext.client); 159 | } 160 | } 161 | 162 | function onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined): void { 163 | if (editor && 164 | editor.document.languageId === extensionContext.LANGUAGE_ID && 165 | extensionContext.client 166 | ) { 167 | activeEditor = editor; 168 | Annotator.requestAnnotators(activeEditor, extensionContext.client); 169 | } 170 | } 171 | 172 | 173 | async function startServer(): Promise { 174 | try { 175 | extensionContext.setServerStarting(); 176 | await doStartServer(); 177 | extensionContext.setServerRunning(); 178 | onDidChangeActiveTextEditor(vscode.window.activeTextEditor); 179 | } catch (reason) { 180 | const errorMessage = reason instanceof Error ? reason.message : String(reason); 181 | extensionContext.setServerError( 182 | 'Failed to start EmmyLua language server', 183 | errorMessage 184 | ); 185 | vscode.window.showErrorMessage( 186 | `Failed to start EmmyLua language server: ${errorMessage}`, 187 | 'Retry', 188 | 'Show Logs' 189 | ).then(action => { 190 | if (action === 'Retry') { 191 | restartServer(); 192 | } else if (action === 'Show Logs') { 193 | extensionContext.client?.outputChannel?.show(); 194 | } 195 | }); 196 | } 197 | } 198 | 199 | /** 200 | * Start the language server 201 | */ 202 | async function doStartServer(): Promise { 203 | const context = extensionContext.vscodeContext; 204 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 205 | const configManager = new ConfigurationManager(workspaceFolder); 206 | 207 | const clientOptions: LanguageClientOptions = { 208 | documentSelector: [{ scheme: 'file', language: extensionContext.LANGUAGE_ID }], 209 | initializationOptions: {}, 210 | }; 211 | 212 | let serverOptions: ServerOptions; 213 | const debugPort = configManager.getDebugPort(); 214 | 215 | if (debugPort || extensionContext.debugMode) { 216 | // Connect to language server via socket (debug mode) 217 | serverOptions = createDebugServerOptions(debugPort || 5007); 218 | } else { 219 | // Start language server as external process 220 | serverOptions = createProcessServerOptions(context, configManager); 221 | } 222 | 223 | extensionContext.client = new LanguageClient( 224 | extensionContext.LANGUAGE_ID, 225 | 'EmmyLua Language Server', 226 | serverOptions, 227 | clientOptions 228 | ); 229 | 230 | await extensionContext.client.start(); 231 | console.log('EmmyLua language server started successfully'); 232 | } 233 | 234 | /** 235 | * Create server options for debug mode (socket connection) 236 | */ 237 | function createDebugServerOptions(port: number): ServerOptions { 238 | return () => { 239 | const socket = net.connect({ port }); 240 | const result: StreamInfo = { 241 | writer: socket, 242 | reader: socket as NodeJS.ReadableStream 243 | }; 244 | 245 | socket.on('close', () => { 246 | console.error(`Language server connection closed (port ${port})`); 247 | }); 248 | 249 | socket.on('error', (error) => { 250 | console.error(`Language server connection error:`, error); 251 | }); 252 | 253 | return Promise.resolve(result); 254 | }; 255 | } 256 | 257 | /** 258 | * Create server options for process mode 259 | */ 260 | function createProcessServerOptions( 261 | context: vscode.ExtensionContext, 262 | configManager: ConfigurationManager 263 | ): ServerOptions { 264 | const executablePath = resolveExecutablePath(context, configManager); 265 | const startParameters = configManager.getStartParameters(); 266 | const globalConfigPath = configManager.getGlobalConfigPath(); 267 | 268 | const serverOptions: ServerOptions = { 269 | command: executablePath, 270 | args: startParameters, 271 | options: { env: { ...process.env } } 272 | }; 273 | 274 | // Set global config path if specified 275 | if (globalConfigPath?.trim()) { 276 | if (!serverOptions.options) { 277 | serverOptions.options = { env: {} }; 278 | } 279 | if (!serverOptions.options.env) { 280 | serverOptions.options.env = {}; 281 | } 282 | serverOptions.options.env['EMMYLUALS_CONFIG'] = globalConfigPath; 283 | } 284 | 285 | return serverOptions; 286 | } 287 | 288 | /** 289 | * Resolve the language server executable path 290 | */ 291 | function resolveExecutablePath( 292 | context: vscode.ExtensionContext, 293 | configManager: ConfigurationManager 294 | ): string { 295 | let executablePath = configManager.getExecutablePath()?.trim(); 296 | 297 | if (!executablePath) { 298 | // Use bundled language server 299 | const platform = os.platform(); 300 | const executableName = platform === 'win32' ? 'emmylua_ls.exe' : 'emmylua_ls'; 301 | executablePath = path.join(context.extensionPath, 'server', executableName); 302 | // Make executable on Unix-like systems 303 | if (platform !== 'win32') { 304 | try { 305 | fs.chmodSync(executablePath, '777'); 306 | } catch (error) { 307 | console.warn(`Failed to chmod language server:`, error); 308 | } 309 | } 310 | } 311 | 312 | return executablePath; 313 | } 314 | 315 | async function restartServer(): Promise { 316 | const client = extensionContext.client; 317 | if (!client) { 318 | await startServer(); 319 | } else { 320 | extensionContext.setServerStopping('Restarting server...'); 321 | try { 322 | if (client.isRunning()) { 323 | await client.stop(); 324 | } 325 | await startServer(); 326 | } catch (error) { 327 | const errorMessage = error instanceof Error ? error.message : String(error); 328 | extensionContext.setServerError('Failed to restart server', errorMessage); 329 | vscode.window.showErrorMessage(`Failed to restart server: ${errorMessage}`); 330 | } 331 | } 332 | } 333 | 334 | function showServerMenu(): void { 335 | extensionContext.showServerMenu(); 336 | } 337 | 338 | function showReferences(uri: string, pos: IServerPosition, locations: IServerLocation[]) { 339 | const u = vscode.Uri.parse(uri); 340 | const p = new vscode.Position(pos.line, pos.character); 341 | const vscodeLocations = locations.map(loc => 342 | new vscode.Location( 343 | vscode.Uri.parse(loc.uri), 344 | new vscode.Range( 345 | new vscode.Position(loc.range.start.line, loc.range.start.character), 346 | new vscode.Position(loc.range.end.line, loc.range.end.character) 347 | ))); 348 | vscode.commands.executeCommand("editor.action.showReferences", u, p, vscodeLocations); 349 | } 350 | 351 | async function stopServer(): Promise { 352 | try { 353 | await extensionContext.stopServer(); 354 | vscode.window.showInformationMessage('EmmyLua language server stopped'); 355 | } catch (error) { 356 | const errorMessage = error instanceof Error ? error.message : String(error); 357 | vscode.window.showErrorMessage(`Failed to stop server: ${errorMessage}`); 358 | } 359 | } 360 | 361 | 362 | /** 363 | * Show syntax tree for current document 364 | * Similar to rust-analyzer's "View Syntax Tree" feature 365 | */ 366 | async function showSyntaxTree(): Promise { 367 | const editor = vscode.window.activeTextEditor; 368 | 369 | if (!editor) { 370 | vscode.window.showWarningMessage('No active editor'); 371 | return; 372 | } 373 | 374 | const document = editor.document; 375 | 376 | if (document.languageId !== extensionContext.LANGUAGE_ID) { 377 | vscode.window.showWarningMessage('Current file is not a Lua file'); 378 | return; 379 | } 380 | 381 | if (!extensionContext.client) { 382 | vscode.window.showWarningMessage('Language server is not running'); 383 | return; 384 | } 385 | 386 | if (!syntaxTreeManager) { 387 | vscode.window.showErrorMessage('Syntax tree manager is not initialized'); 388 | return; 389 | } 390 | 391 | // Show syntax tree using the manager 392 | await syntaxTreeManager.show(document.uri, editor.selection); 393 | } 394 | -------------------------------------------------------------------------------- /src/luarocks/LuaRocksManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as cp from 'child_process'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import { promisify } from 'util'; 6 | 7 | const exec = promisify(cp.exec); 8 | const readFile = promisify(fs.readFile); 9 | const access = promisify(fs.access); 10 | 11 | export interface LuaPackage { 12 | name: string; 13 | version: string; 14 | description?: string; 15 | dependencies?: string[]; 16 | homepage?: string; 17 | license?: string; 18 | author?: string; 19 | installed?: boolean; 20 | location?: string; 21 | summary?: string; 22 | } 23 | 24 | export interface LuaRocksWorkspace { 25 | hasRockspec: boolean; 26 | rockspecFiles: string[]; 27 | hasLuarocksConfig: boolean; 28 | dependencies: LuaPackage[]; 29 | } 30 | 31 | export class LuaRocksManager { 32 | private readonly workspaceFolder: vscode.WorkspaceFolder; 33 | private readonly outputChannel: vscode.OutputChannel; 34 | private readonly statusBarItem: vscode.StatusBarItem; 35 | private isInstalling = false; 36 | private installedPackagesCache: LuaPackage[] = []; 37 | 38 | constructor(workspaceFolder: vscode.WorkspaceFolder) { 39 | this.workspaceFolder = workspaceFolder; 40 | this.outputChannel = vscode.window.createOutputChannel('LuaRocks'); 41 | this.statusBarItem = vscode.window.createStatusBarItem( 42 | vscode.StatusBarAlignment.Left, 43 | 100 44 | ); 45 | this.updateStatusBar(); 46 | } 47 | 48 | /** 49 | * 检查 LuaRocks 是否已安装 50 | */ 51 | async checkLuaRocksInstallation(): Promise { 52 | try { 53 | const { stdout } = await exec('luarocks --version'); 54 | return stdout.includes('LuaRocks'); 55 | } catch { 56 | return false; 57 | } 58 | } 59 | 60 | /** 61 | * 检测当前工作区是否为 LuaRocks 项目 62 | */ 63 | async detectLuaRocksWorkspace(): Promise { 64 | const workspace: LuaRocksWorkspace = { 65 | hasRockspec: false, 66 | rockspecFiles: [], 67 | hasLuarocksConfig: false, 68 | dependencies: [] 69 | }; 70 | 71 | try { 72 | // 查找 .rockspec 文件 73 | const rockspecFiles = await vscode.workspace.findFiles( 74 | new vscode.RelativePattern(this.workspaceFolder, '*.rockspec'), 75 | null, 76 | 10 77 | ); 78 | 79 | workspace.hasRockspec = rockspecFiles.length > 0; 80 | workspace.rockspecFiles = rockspecFiles.map(uri => uri.fsPath); 81 | 82 | // 检查 .luarocks 配置文件 83 | const luarocksConfigPath = path.join(this.workspaceFolder.uri.fsPath, '.luarocks'); 84 | try { 85 | await access(luarocksConfigPath); 86 | workspace.hasLuarocksConfig = true; 87 | } catch { 88 | workspace.hasLuarocksConfig = false; 89 | } 90 | 91 | // 如果有 rockspec 文件,解析依赖 92 | if (workspace.hasRockspec && workspace.rockspecFiles.length > 0) { 93 | workspace.dependencies = await this.parseRockspecDependencies(workspace.rockspecFiles[0]); 94 | } 95 | 96 | } catch (error) { 97 | console.error('Error detecting LuaRocks workspace:', error); 98 | } 99 | 100 | return workspace; 101 | } 102 | 103 | /** 104 | * 解析 rockspec 文件中的依赖 105 | */ 106 | private async parseRockspecDependencies(rockspecPath: string): Promise { 107 | try { 108 | const content = await readFile(rockspecPath, 'utf8'); 109 | const dependencies: LuaPackage[] = []; 110 | 111 | // 简单的正则解析依赖(实际应该使用 Lua 解析器) 112 | const depMatch = content.match(/dependencies\s*=\s*\{([^}]*)\}/); 113 | if (depMatch) { 114 | const depContent = depMatch[1]; 115 | const depLines = depContent.split(',').map(line => line.trim()); 116 | 117 | for (const line of depLines) { 118 | const match = line.match(/["']([^"']+)["']/); 119 | if (match) { 120 | const depString = match[1]; 121 | const [name, version] = depString.split(/\s+/); 122 | if (name && name !== 'lua') { 123 | dependencies.push({ 124 | name: name.trim(), 125 | version: version?.trim() || 'latest', 126 | installed: false 127 | }); 128 | } 129 | } 130 | } 131 | } 132 | 133 | return dependencies; 134 | } catch (error) { 135 | console.error('Error parsing rockspec dependencies:', error); 136 | return []; 137 | } 138 | } 139 | 140 | /** 141 | * 搜索包 142 | */ 143 | async searchPackages(query: string): Promise { 144 | try { 145 | this.showProgress('Searching packages...'); 146 | 147 | const { stdout } = await exec(`luarocks search ${query} --porcelain`); 148 | const packages = this.parseSearchResults(stdout); 149 | 150 | this.hideProgress(); 151 | return packages; 152 | } catch (error) { 153 | this.hideProgress(); 154 | this.showError('Failed to search packages', error); 155 | return []; 156 | } 157 | } 158 | 159 | /** 160 | * 获取已安装的包列表 161 | */ 162 | async getInstalledPackages(forceRefresh = false): Promise { 163 | if (!forceRefresh && this.installedPackagesCache.length > 0) { 164 | return this.installedPackagesCache; 165 | } 166 | 167 | try { 168 | const localFlag = await this.shouldUseLocal() ? '--local' : ''; 169 | const { stdout } = await exec(`luarocks list --porcelain ${localFlag}`.trim()); 170 | this.installedPackagesCache = this.parseInstalledPackages(stdout); 171 | return this.installedPackagesCache; 172 | } catch (error) { 173 | this.showError('Failed to get installed packages', error); 174 | return []; 175 | } 176 | } 177 | 178 | /** 179 | * 安装包 180 | */ 181 | async installPackage(packageName: string, version?: string): Promise { 182 | if (this.isInstalling) { 183 | vscode.window.showWarningMessage('Installation already in progress'); 184 | return false; 185 | } 186 | 187 | try { 188 | this.isInstalling = true; 189 | this.updateStatusBar('Installing...'); 190 | 191 | const versionSpec = version && version !== 'latest' ? `${packageName} ${version}` : packageName; 192 | const localFlag = await this.shouldUseLocal() ? '--local' : ''; 193 | 194 | const command = `luarocks install ${versionSpec} ${localFlag}`.trim(); 195 | 196 | this.outputChannel.show(); 197 | this.outputChannel.appendLine(`Installing: ${command}`); 198 | 199 | const { stdout, stderr } = await exec(command, { 200 | cwd: this.workspaceFolder.uri.fsPath 201 | }); 202 | 203 | this.outputChannel.appendLine(stdout); 204 | if (stderr) this.outputChannel.appendLine(stderr); 205 | 206 | // 清除缓存 207 | this.installedPackagesCache = []; 208 | 209 | vscode.window.showInformationMessage(`Successfully installed ${packageName}`); 210 | return true; 211 | } catch (error) { 212 | this.showError(`Failed to install ${packageName}`, error); 213 | return false; 214 | } finally { 215 | this.isInstalling = false; 216 | this.updateStatusBar(); 217 | } 218 | } 219 | 220 | /** 221 | * 卸载包 222 | */ 223 | async uninstallPackage(packageName: string): Promise { 224 | try { 225 | this.updateStatusBar('Uninstalling...'); 226 | 227 | const localFlag = await this.shouldUseLocal() ? '--local' : ''; 228 | const command = `luarocks remove ${packageName} ${localFlag}`.trim(); 229 | 230 | this.outputChannel.show(); 231 | this.outputChannel.appendLine(`Uninstalling: ${command}`); 232 | 233 | const { stdout, stderr } = await exec(command); 234 | 235 | this.outputChannel.appendLine(stdout); 236 | if (stderr) this.outputChannel.appendLine(stderr); 237 | 238 | // 清除缓存 239 | this.installedPackagesCache = []; 240 | 241 | vscode.window.showInformationMessage(`Successfully uninstalled ${packageName}`); 242 | return true; 243 | } catch (error) { 244 | this.showError(`Failed to uninstall ${packageName}`, error); 245 | return false; 246 | } finally { 247 | this.updateStatusBar(); 248 | } 249 | } 250 | 251 | /** 252 | * 获取包详细信息 253 | */ 254 | async getPackageInfo(packageName: string): Promise { 255 | try { 256 | const { stdout } = await exec(`luarocks show ${packageName}`); 257 | return this.parsePackageInfo(stdout, packageName); 258 | } catch (error) { 259 | // 尝试从搜索结果获取信息 260 | try { 261 | const searchResults = await this.searchPackages(packageName); 262 | return searchResults.find(pkg => pkg.name === packageName) || null; 263 | } catch { 264 | this.showError(`Failed to get package info for ${packageName}`, error); 265 | return null; 266 | } 267 | } 268 | } 269 | 270 | /** 271 | * 检查包是否已安装 272 | */ 273 | async isPackageInstalled(packageName: string): Promise { 274 | const installed = await this.getInstalledPackages(); 275 | return installed.some(pkg => pkg.name === packageName); 276 | } 277 | 278 | // Private helper methods 279 | 280 | private parseSearchResults(output: string): LuaPackage[] { 281 | const packages: LuaPackage[] = []; 282 | const lines = output.trim().split('\n').filter(line => line.trim()); 283 | 284 | for (const line of lines) { 285 | const parts = line.split('\t'); 286 | if (parts.length >= 2) { 287 | packages.push({ 288 | name: parts[0].trim(), 289 | version: parts[1].trim(), 290 | summary: parts[2]?.trim() || '', 291 | description: parts[2]?.trim() || '', 292 | installed: false 293 | }); 294 | } 295 | } 296 | 297 | return packages; 298 | } 299 | 300 | private parseInstalledPackages(output: string): LuaPackage[] { 301 | const packages: LuaPackage[] = []; 302 | const lines = output.trim().split('\n').filter(line => line.trim()); 303 | 304 | for (const line of lines) { 305 | const parts = line.split('\t'); 306 | if (parts.length >= 2) { 307 | packages.push({ 308 | name: parts[0].trim(), 309 | version: parts[1].trim(), 310 | location: parts[2]?.trim() || '', 311 | installed: true 312 | }); 313 | } 314 | } 315 | 316 | return packages; 317 | } 318 | 319 | private parsePackageInfo(output: string, packageName: string): LuaPackage { 320 | const lines = output.split('\n'); 321 | const info: Partial = { 322 | name: packageName, 323 | installed: true 324 | }; 325 | 326 | for (const line of lines) { 327 | const colonIndex = line.indexOf(':'); 328 | if (colonIndex === -1) continue; 329 | 330 | const key = line.substring(0, colonIndex).trim().toLowerCase(); 331 | const value = line.substring(colonIndex + 1).trim(); 332 | 333 | switch (key) { 334 | case 'version': 335 | info.version = value; 336 | break; 337 | case 'description': 338 | case 'summary': 339 | info.description = value; 340 | break; 341 | case 'homepage': 342 | info.homepage = value; 343 | break; 344 | case 'license': 345 | info.license = value; 346 | break; 347 | case 'maintainer': 348 | case 'author': 349 | info.author = value; 350 | break; 351 | } 352 | } 353 | 354 | return info as LuaPackage; 355 | } 356 | 357 | private async shouldUseLocal(): Promise { 358 | const config = vscode.workspace.getConfiguration('emmylua.luarocks'); 359 | return config.get('preferLocalInstall', true); 360 | } 361 | 362 | private showError(message: string, error: any): void { 363 | const errorMessage = error instanceof Error ? error.message : String(error); 364 | this.outputChannel.appendLine(`Error: ${message}`); 365 | this.outputChannel.appendLine(errorMessage); 366 | vscode.window.showErrorMessage(message); 367 | } 368 | 369 | private showProgress(message: string): void { 370 | this.updateStatusBar(message); 371 | } 372 | 373 | private hideProgress(): void { 374 | this.updateStatusBar(); 375 | } 376 | 377 | private updateStatusBar(text?: string): void { 378 | if (text) { 379 | this.statusBarItem.text = `$(sync~spin) ${text}`; 380 | this.statusBarItem.show(); 381 | } else { 382 | this.statusBarItem.text = '$(package) LuaRocks'; 383 | this.statusBarItem.command = 'emmylua.luarocks.showPackages'; 384 | this.statusBarItem.show(); 385 | } 386 | } 387 | 388 | dispose(): void { 389 | this.outputChannel.dispose(); 390 | this.statusBarItem.dispose(); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/debugger/base/EmmyDebugSession.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import * as readline from 'readline'; 3 | import * as proto from "./EmmyDebugProto"; 4 | import { DebugSession } from "./DebugSession"; 5 | import { DebugProtocol } from "@vscode/debugprotocol"; 6 | import { StoppedEvent, StackFrame, Thread, Source, Handles, TerminatedEvent, InitializedEvent, Breakpoint, OutputEvent } from "@vscode/debugadapter"; 7 | import { EmmyStack, IEmmyStackNode, EmmyVariable, IEmmyStackContext, EmmyStackENV } from "./EmmyDebugData"; 8 | import { readFileSync } from "fs"; 9 | import { join, normalize } from "path"; 10 | 11 | 12 | 13 | export class EmmyDebugSession extends DebugSession implements IEmmyStackContext { 14 | protected client?: net.Socket; 15 | private readHeader = true; 16 | private currentCmd: proto.MessageCMD = proto.MessageCMD.Unknown; 17 | private breakNotify?: proto.IBreakNotify; 18 | private currentFrameId = 0; 19 | private breakPointId = 0; 20 | private evalIdCount = 0; 21 | private breakpoints: proto.IBreakPoint[] = []; 22 | protected extensionPath: string = ''; 23 | 24 | handles = new Handles(); 25 | 26 | protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { 27 | response.body = { 28 | supportsEvaluateForHovers: true, 29 | supportTerminateDebuggee: true, 30 | supportsLogPoints: true, 31 | supportsHitConditionalBreakpoints: true, 32 | supportsSetExpression: true, 33 | // supportsDelayedStackTraceLoading: true, 34 | // supportsCompletionsRequest: true 35 | }; 36 | this.sendResponse(response); 37 | } 38 | 39 | protected customRequest(command: string, response: DebugProtocol.Response, args: any): void { 40 | if (command === 'stopWaitConnection') { 41 | this.sendEvent(new OutputEvent('---> stop')); 42 | this.sendEvent(new TerminatedEvent()); 43 | } 44 | else { 45 | super.customRequest(command, response, args); 46 | } 47 | } 48 | 49 | protected onConnect(client: net.Socket) { 50 | this.sendEvent(new OutputEvent(`Connected.\n`)); 51 | this.client = client; 52 | 53 | const extPath = this.extensionPath; 54 | const emmyHelperPath = join(extPath, 'debugger/emmy/emmyHelper.lua'); 55 | // send init event 56 | const emmyHelper = readFileSync(emmyHelperPath); 57 | const initReq: proto.IInitReq = { 58 | cmd: proto.MessageCMD.InitReq, 59 | emmyHelper: emmyHelper.toString(), 60 | ext: this.ext 61 | }; 62 | this.sendMessage(initReq); 63 | 64 | // add breakpoints 65 | this.sendBreakpoints(); 66 | 67 | // send ready 68 | this.sendMessage({ cmd: proto.MessageCMD.ReadyReq }); 69 | this.sendEvent(new InitializedEvent()); 70 | } 71 | 72 | protected readClient(client: net.Socket) { 73 | readline.createInterface({ 74 | input: client, 75 | output: client 76 | }).on("line", line => this.onReceiveLine(line)); 77 | 78 | client.on('close', hadErr => this.onSocketClose()) 79 | .on('error', err => this.onSocketClose()); 80 | } 81 | 82 | protected onSocketClose() { 83 | if (this.client) { 84 | this.client.removeAllListeners(); 85 | } 86 | this.sendEvent(new OutputEvent('Disconnected.\n')); 87 | this.sendEvent(new TerminatedEvent()); 88 | } 89 | 90 | protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { 91 | this.sendDebugAction(response, proto.DebugAction.Stop); 92 | this.onDisconnect(); 93 | } 94 | 95 | protected onDisconnect() { 96 | } 97 | 98 | private onReceiveLine(line: string) { 99 | if (this.readHeader) { 100 | this.currentCmd = parseInt(line); 101 | } 102 | else { 103 | const data = JSON.parse(line); 104 | this.handleDebugMessage(this.currentCmd, data); 105 | } 106 | this.readHeader = !this.readHeader; 107 | } 108 | 109 | protected handleDebugMessage(cmd: proto.MessageCMD, msg: any) { 110 | switch (cmd) { 111 | case proto.MessageCMD.BreakNotify: 112 | this.breakNotify = msg; 113 | this.sendEvent(new StoppedEvent("breakpoint", 1)); 114 | break; 115 | case proto.MessageCMD.EvalRsp: 116 | this.emit('onEvalRsp', msg); 117 | break; 118 | } 119 | } 120 | 121 | protected sendMessage(msg: { cmd: proto.MessageCMD }) { 122 | if (this.client) { 123 | this.client.write(`${msg.cmd}\n`); 124 | this.client.write(`${JSON.stringify(msg)}\n`); 125 | } 126 | } 127 | 128 | protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { 129 | response.body = { 130 | threads: [ 131 | new Thread(1, "thread 1") 132 | ] 133 | }; 134 | this.sendResponse(response); 135 | } 136 | 137 | protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): Promise { 138 | if (this.breakNotify) { 139 | const stackFrames: StackFrame[] = []; 140 | const stacks = this.breakNotify.stacks; 141 | for (let i = 0; i < stacks.length; i++) { 142 | const stack = stacks[i]; 143 | let file = stack.file; 144 | if (stack.line >= 0) { 145 | file = await this.findFile(stack.file); 146 | } 147 | else if (i < stacks.length - 1) { 148 | continue; 149 | } 150 | let source = new Source(stack.file, file); 151 | stackFrames.push(new StackFrame(stack.level, stack.functionName, source, stack.line)); 152 | } 153 | response.body = { 154 | stackFrames: stackFrames, 155 | totalFrames: stackFrames.length 156 | }; 157 | } 158 | this.sendResponse(response); 159 | } 160 | 161 | protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { 162 | this.currentFrameId = args.frameId; 163 | if (this.breakNotify) { 164 | const stackData = this.breakNotify.stacks[args.frameId]; 165 | const stack = new EmmyStack(stackData); 166 | const env = new EmmyStackENV(stackData); 167 | response.body = { 168 | scopes: [ 169 | { 170 | name: "Variables", 171 | presentationHint: "locals", 172 | variablesReference: this.handles.create(stack), 173 | expensive: false 174 | }, 175 | { 176 | name: "ENV", 177 | variablesReference: this.handles.create(env), 178 | expensive: false 179 | } 180 | ] 181 | }; 182 | } 183 | this.sendResponse(response); 184 | } 185 | 186 | protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { 187 | if (this.breakNotify) { 188 | const node = this.handles.get(args.variablesReference); 189 | const children = await node.computeChildren(this); 190 | response.body = { 191 | variables: children.map(v => v.toVariable(this)) 192 | }; 193 | } 194 | this.sendResponse(response); 195 | } 196 | 197 | protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise { 198 | const evalResp = await this.eval(args.expression, 0, 1, args.frameId); 199 | if (evalResp.success) { 200 | const emmyVar = new EmmyVariable(evalResp.value); 201 | const variable = emmyVar.toVariable(this); 202 | response.body = { 203 | result: variable.value, 204 | type: variable.type, 205 | variablesReference: variable.variablesReference 206 | }; 207 | } 208 | else { 209 | response.body = { 210 | result: evalResp.error, 211 | type: 'string', 212 | variablesReference: 0 213 | }; 214 | } 215 | this.sendResponse(response); 216 | } 217 | 218 | async eval(expr: string, cacheId: number, depth: number = 1, stackLevel = -1): Promise { 219 | const req: proto.IEvalReq = { 220 | cmd: proto.MessageCMD.EvalReq, 221 | seq: this.evalIdCount++, 222 | stackLevel: stackLevel >= 0 ? stackLevel: this.currentFrameId, 223 | expr, 224 | depth, 225 | cacheId 226 | }; 227 | this.sendMessage(req); 228 | return new Promise((resolve, reject) => { 229 | const listener = (msg: proto.IEvalRsp) => { 230 | if (msg.seq === req.seq) { 231 | this.removeListener('onEvalRsp', listener); 232 | resolve(msg); 233 | } 234 | }; 235 | this.on('onEvalRsp', listener); 236 | }); 237 | } 238 | 239 | async setEval(expr: string, value: string, cacheId: number, depth: number = 1, stackLevel = -1): Promise { 240 | const req: proto.IEvalReq = { 241 | cmd: proto.MessageCMD.EvalReq, 242 | seq: this.evalIdCount++, 243 | stackLevel: stackLevel >= 0 ? stackLevel : this.currentFrameId, 244 | expr, 245 | depth, 246 | cacheId, 247 | value, 248 | setValue: true, 249 | }; 250 | this.sendMessage(req); 251 | return new Promise((resolve, reject) => { 252 | const listener = (msg: proto.IEvalRsp) => { 253 | if (msg.seq === req.seq) { 254 | this.removeListener('onEvalRsp', listener); 255 | resolve(msg); 256 | } 257 | }; 258 | this.on('onEvalRsp', listener); 259 | }); 260 | } 261 | 262 | protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { 263 | const source = args.source; 264 | const bpsProto: proto.IBreakPoint[] = []; 265 | if (source && source.path) { 266 | const path = normalize(source.path); 267 | const bps = args.breakpoints || []; 268 | const bpsResp: DebugProtocol.Breakpoint[] = []; 269 | for (let i = 0; i < bps.length; i++) { 270 | const bp = bps[i]; 271 | bpsProto.push({ 272 | file: path, 273 | line: bp.line, 274 | condition: bp.condition, 275 | hitCondition: bp.hitCondition, 276 | logMessage: bp.logMessage 277 | }); 278 | 279 | const bpResp = new Breakpoint(true, bp.line); 280 | bpResp.id = this.breakPointId++; 281 | bpsResp.push(bpResp); 282 | } 283 | response.body = { breakpoints: bpsResp }; 284 | 285 | this.breakpoints = this.breakpoints.filter(v => v.file !== path); 286 | this.breakpoints = this.breakpoints.concat(bpsProto); 287 | } 288 | this.sendBreakpoints(); 289 | this.sendResponse(response); 290 | } 291 | 292 | protected async setExpressionRequest(response: DebugProtocol.SetExpressionResponse, args: DebugProtocol.SetExpressionArguments, request?: DebugProtocol.Request): Promise { 293 | const evalResp = await this.setEval(args.expression, args.value,0, 1, args.frameId); 294 | if (evalResp.success) { 295 | const emmyVar = new EmmyVariable(evalResp.value); 296 | const variable = emmyVar.toVariable(this); 297 | response.body = { 298 | value: variable.value, 299 | type: variable.type, 300 | variablesReference: variable.variablesReference 301 | }; 302 | } 303 | else { 304 | response.body = { 305 | value: evalResp.error, 306 | type: 'string', 307 | variablesReference: 0 308 | }; 309 | } 310 | this.sendResponse(response); 311 | } 312 | // protected completionsRequest(response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments, request?: DebugProtocol.Request): void { 313 | 314 | // } 315 | 316 | 317 | private sendBreakpoints() { 318 | const req: proto.IAddBreakPointReq = { 319 | breakPoints: this.breakpoints, 320 | clear: true, 321 | cmd: proto.MessageCMD.AddBreakPointReq 322 | }; 323 | this.sendMessage(req); 324 | } 325 | 326 | private sendDebugAction(response: DebugProtocol.Response, action: proto.DebugAction) { 327 | const req: proto.IActionReq = { cmd: proto.MessageCMD.ActionReq, action: action }; 328 | this.sendMessage(req); 329 | this.sendResponse(response); 330 | } 331 | 332 | protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): void { 333 | this.sendDebugAction(response, proto.DebugAction.Break); 334 | } 335 | 336 | protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { 337 | this.sendDebugAction(response, proto.DebugAction.Continue); 338 | } 339 | 340 | protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { 341 | this.sendDebugAction(response, proto.DebugAction.StepOver); 342 | } 343 | 344 | protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void { 345 | this.sendDebugAction(response, proto.DebugAction.StepIn); 346 | } 347 | 348 | protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void { 349 | this.sendDebugAction(response, proto.DebugAction.StepOut); 350 | } 351 | 352 | } 353 | -------------------------------------------------------------------------------- /src/luaTerminalLinkProvider.ts: -------------------------------------------------------------------------------- 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 columnNumber?: number, 16 | public readonly originalMatch?: string 17 | ) { 18 | super(startIndex, length, tooltip); 19 | } 20 | } 21 | 22 | /** 23 | * Lua 终端链接提供器 24 | * 支持多种常见的 Lua 错误输出格式,避免误匹配命令中的文件名 25 | */ 26 | class LuaTerminalLinkProvider implements vscode.TerminalLinkProvider { 27 | /** 28 | * 匹配模式定义 29 | * 每个模式包含:正则表达式、优先级、描述 30 | */ 31 | private readonly patterns: Array<{ 32 | regex: RegExp; 33 | priority: number; 34 | description: string; 35 | }> = [ 36 | // 1. Lua 标准错误格式: lua: path/to/file.lua:123: error message 37 | { 38 | regex: /\blua:\s+([^\s:]+\.lua):(\d+):/g, 39 | priority: 100, 40 | description: 'Lua standard error' 41 | }, 42 | 43 | // 2. 堆栈跟踪格式: \t at path/to/file.lua:123 44 | { 45 | regex: /\s+at\s+([^\s:]+\.lua):(\d+)/g, 46 | priority: 95, 47 | description: 'Stack trace with at' 48 | }, 49 | 50 | // 3. 堆栈跟踪格式(带制表符): \tpath/to/file.lua:123: in function 'name' 51 | { 52 | regex: /^\s+([^\s:]+\.lua):(\d+):\s+in\s+/gm, 53 | priority: 90, 54 | description: 'Stack trace in function' 55 | }, 56 | 57 | // 4. 文件和行号(通用): path/to/file.lua:123 58 | // 需要前面有空白、冒号、箭头或行首,避免误匹配命令参数 59 | { 60 | regex: /(?:^|[\s:→])((?:[A-Za-z]:)?(?:[\/\\][\w\s\-\.]+)+\.lua):(\d+)(?::(\d+))?/g, 61 | priority: 80, 62 | description: 'File path with line number' 63 | }, 64 | 65 | // 5. 截断路径格式: .../file.lua:123 66 | { 67 | regex: /(?:^|[\s:→])(\.\.\.[\/\\][\w\s\-\.\/\\]+\.lua):(\d+)(?::(\d+))?/g, 68 | priority: 85, 69 | description: 'Truncated path' 70 | }, 71 | 72 | // 6. 错误箭头格式: Error → path/to/file.lua:123 73 | { 74 | regex: /→\s+((?:[A-Za-z]:)?[\w\s\-\.\/\\]+\.lua):(\d+)(?::(\d+))?/g, 75 | priority: 90, 76 | description: 'Error with arrow' 77 | }, 78 | 79 | // 7. 括号内的文件路径: (path/to/file.lua:123) 80 | { 81 | regex: /\(((?:[A-Za-z]:)?[\w\s\-\.\/\\]+\.lua):(\d+)(?::(\d+))?\)/g, 82 | priority: 75, 83 | description: 'File in parentheses' 84 | }, 85 | 86 | // 8. 方括号内的文件路径: [path/to/file.lua:123] 87 | { 88 | regex: /\[((?:[A-Za-z]:)?[\w\s\-\.\/\\]+\.lua):(\d+)(?::(\d+))?\]/g, 89 | priority: 75, 90 | description: 'File in brackets' 91 | } 92 | ]; 93 | 94 | /** 95 | * 为 Lua 错误信息提供终端链接 96 | */ 97 | provideTerminalLinks( 98 | context: vscode.TerminalLinkContext, 99 | _token: vscode.CancellationToken 100 | ): vscode.ProviderResult { 101 | const line = context.line; 102 | const allMatches: Array<{ 103 | link: LuaTerminalLink; 104 | priority: number; 105 | }> = []; 106 | 107 | // 尝试所有模式 108 | for (const pattern of this.patterns) { 109 | pattern.regex.lastIndex = 0; 110 | 111 | let match: RegExpExecArray | null; 112 | while ((match = pattern.regex.exec(line)) !== null) { 113 | const fullMatch = match[0]; 114 | const filePath = match[1]; 115 | const lineNumber = match[2] ? parseInt(match[2], 10) : undefined; 116 | const columnNumber = match[3] ? parseInt(match[3], 10) : undefined; 117 | 118 | // 验证文件路径的合理性 119 | if (!this.isValidLuaPath(filePath, line, match.index)) { 120 | continue; 121 | } 122 | 123 | // 创建链接 124 | const tooltip = this.createTooltip(filePath, lineNumber, columnNumber); 125 | const link = new LuaTerminalLink( 126 | match.index, 127 | fullMatch.length, 128 | tooltip, 129 | filePath, 130 | lineNumber, 131 | columnNumber, 132 | fullMatch 133 | ); 134 | 135 | allMatches.push({ 136 | link, 137 | priority: pattern.priority 138 | }); 139 | } 140 | } 141 | 142 | if (allMatches.length === 0) { 143 | return []; 144 | } 145 | 146 | // 去重和优先级排序 147 | return this.deduplicateAndPrioritize(allMatches); 148 | } 149 | 150 | /** 151 | * 验证文件路径是否合理 152 | * 避免误匹配命令参数(如 echo "test.lua") 153 | */ 154 | private isValidLuaPath(filePath: string, line: string, matchIndex: number): boolean { 155 | // 1. 检查前面是否是引号(避免匹配字符串字面量) 156 | if (matchIndex > 0) { 157 | const charBefore = line[matchIndex - 1]; 158 | if (charBefore === '"' || charBefore === "'") { 159 | // 检查后面是否也有引号(完整的字符串字面量) 160 | const matchEnd = matchIndex + filePath.length; 161 | if (matchEnd < line.length) { 162 | const charAfter = line[matchEnd]; 163 | if (charAfter === charBefore) { 164 | return false; // 这是一个完整的字符串字面量 165 | } 166 | } 167 | } 168 | } 169 | 170 | // 2. 检查是否在命令参数中(简单启发式) 171 | const beforeMatch = line.substring(0, matchIndex); 172 | const shellCommands = ['echo', 'cat', 'type', 'more', 'less', 'tail', 'head']; 173 | 174 | for (const cmd of shellCommands) { 175 | // 检查前面是否有这些命令 176 | const cmdPattern = new RegExp(`\\b${cmd}\\s+[^\\n]*$`, 'i'); 177 | if (cmdPattern.test(beforeMatch)) { 178 | return false; 179 | } 180 | } 181 | 182 | // 3. 路径必须看起来合理 183 | // - 包含路径分隔符,或 184 | // - 以 ... 开头(截断路径),或 185 | // - 是绝对路径(Windows 盘符或 Unix 根路径) 186 | const hasPathSeparator = filePath.includes('/') || filePath.includes('\\'); 187 | const isTruncated = filePath.startsWith('...'); 188 | const isAbsolute = /^[A-Za-z]:/.test(filePath) || filePath.startsWith('/'); 189 | 190 | if (!hasPathSeparator && !isTruncated && !isAbsolute) { 191 | // 单个文件名(如 "test.lua")通常不是有效的错误路径 192 | // 除非它在特定的上下文中(如堆栈跟踪) 193 | return false; 194 | } 195 | 196 | return true; 197 | } 198 | 199 | /** 200 | * 创建工具提示文本 201 | */ 202 | private createTooltip( 203 | filePath: string, 204 | lineNumber?: number, 205 | columnNumber?: number 206 | ): string { 207 | let tooltip = `Open: ${filePath}`; 208 | 209 | if (lineNumber !== undefined) { 210 | tooltip += `:${lineNumber}`; 211 | 212 | if (columnNumber !== undefined) { 213 | tooltip += `:${columnNumber}`; 214 | } 215 | } 216 | 217 | return tooltip; 218 | } 219 | 220 | /** 221 | * 去重并按优先级排序链接 222 | * 处理重叠的匹配 223 | */ 224 | private deduplicateAndPrioritize( 225 | matches: Array<{ link: LuaTerminalLink; priority: number }> 226 | ): LuaTerminalLink[] { 227 | // 按起始位置和优先级排序 228 | matches.sort((a, b) => { 229 | const startDiff = a.link.startIndex - b.link.startIndex; 230 | if (startDiff !== 0) { 231 | return startDiff; 232 | } 233 | // 相同起始位置,优先级高的在前 234 | return b.priority - a.priority; 235 | }); 236 | 237 | const result: LuaTerminalLink[] = []; 238 | let lastEnd = -1; 239 | 240 | for (const { link } of matches) { 241 | const start = link.startIndex; 242 | const end = start + link.length; 243 | 244 | // 如果与前一个链接不重叠,添加 245 | if (start >= lastEnd) { 246 | result.push(link); 247 | lastEnd = end; 248 | } 249 | // 如果重叠,跳过(因为已经按优先级排序,前面的更优) 250 | } 251 | 252 | return result; 253 | } 254 | 255 | /** 256 | * 处理终端链接激活 257 | */ 258 | async handleTerminalLink(link: LuaTerminalLink): Promise { 259 | const { filePath, lineNumber, columnNumber, originalMatch } = link; 260 | 261 | // 尝试解析文件路径 262 | const resolvedPath = await this.resolveFilePath(filePath, originalMatch || filePath); 263 | 264 | if (!resolvedPath) { 265 | vscode.window.showWarningMessage( 266 | `File not found: ${filePath}` 267 | ); 268 | return; 269 | } 270 | 271 | // 打开文件 272 | try { 273 | const uri = vscode.Uri.file(resolvedPath); 274 | const document = await vscode.workspace.openTextDocument(uri); 275 | const editor = await vscode.window.showTextDocument(document, { 276 | preview: false, 277 | preserveFocus: false 278 | }); 279 | 280 | // 跳转到指定位置 281 | if (lineNumber !== undefined && lineNumber > 0) { 282 | const line = Math.min(lineNumber - 1, document.lineCount - 1); 283 | const column = columnNumber !== undefined && columnNumber > 0 284 | ? columnNumber - 1 285 | : 0; 286 | 287 | const position = new vscode.Position(line, column); 288 | editor.selection = new vscode.Selection(position, position); 289 | editor.revealRange( 290 | new vscode.Range(position, position), 291 | vscode.TextEditorRevealType.InCenter 292 | ); 293 | } 294 | } catch (error) { 295 | vscode.window.showErrorMessage( 296 | `Failed to open file: ${resolvedPath}` 297 | ); 298 | } 299 | } 300 | 301 | /** 302 | * 将文件路径解析为完整的绝对路径 303 | */ 304 | private async resolveFilePath( 305 | filePath: string, 306 | originalMatch: string 307 | ): Promise { 308 | const normalizedFilePath = path.normalize(filePath); 309 | const workspaceFolders = vscode.workspace.workspaceFolders ?? []; 310 | 311 | // 快速检查文件是否存在 312 | const exists = async (p: string): Promise => { 313 | try { 314 | const stat = await fs.promises.stat(p); 315 | return stat.isFile(); 316 | } catch { 317 | return false; 318 | } 319 | }; 320 | 321 | // 1. 绝对路径 - 直接验证 322 | if (path.isAbsolute(normalizedFilePath)) { 323 | if (await exists(normalizedFilePath)) { 324 | return normalizedFilePath; 325 | } 326 | return null; 327 | } 328 | 329 | // 2. 相对于工作区根目录的路径 330 | for (const folder of workspaceFolders) { 331 | const candidate = path.join(folder.uri.fsPath, normalizedFilePath); 332 | if (await exists(candidate)) { 333 | return candidate; 334 | } 335 | } 336 | 337 | // 3. 处理截断路径(.../path/to/file.lua) 338 | if (originalMatch.startsWith('...')) { 339 | const truncatedPath = normalizedFilePath.replace(/^\.\.\.[\\/]?/, ''); 340 | return await this.findTruncatedPath(truncatedPath); 341 | } 342 | 343 | // 4. 作为相对路径在工作区中搜索 344 | return await this.searchInWorkspace(normalizedFilePath); 345 | } 346 | 347 | /** 348 | * 查找被截断的路径 349 | */ 350 | private async findTruncatedPath(truncatedPath: string): Promise { 351 | const fileName = path.basename(truncatedPath); 352 | const normalizedTruncated = path.normalize(truncatedPath); 353 | 354 | // 搜索文件名匹配的文件 355 | const files = await vscode.workspace.findFiles( 356 | `**/${fileName}`, 357 | '**/node_modules/**', 358 | 100 359 | ); 360 | 361 | // 按路径后缀匹配度评分 362 | let bestMatch: { path: string; score: number } | null = null; 363 | 364 | for (const file of files) { 365 | const filePath = path.normalize(file.fsPath); 366 | 367 | // 检查是否以截断路径结尾 368 | if (filePath.endsWith(normalizedTruncated)) { 369 | const score = this.calculatePathMatchScore(filePath, normalizedTruncated); 370 | 371 | if (!bestMatch || score > bestMatch.score) { 372 | bestMatch = { path: filePath, score }; 373 | } 374 | } 375 | } 376 | 377 | return bestMatch?.path ?? null; 378 | } 379 | 380 | /** 381 | * 在工作区中搜索文件 382 | */ 383 | private async searchInWorkspace(relativePath: string): Promise { 384 | const fileName = path.basename(relativePath); 385 | const normalizedPath = path.normalize(relativePath); 386 | 387 | // 搜索所有匹配的文件 388 | const files = await vscode.workspace.findFiles( 389 | `**/${fileName}`, 390 | '**/node_modules/**', 391 | 50 392 | ); 393 | 394 | if (files.length === 0) { 395 | return null; 396 | } 397 | 398 | // 如果只有一个匹配,直接返回 399 | if (files.length === 1) { 400 | return files[0].fsPath; 401 | } 402 | 403 | // 多个匹配时,选择最佳匹配 404 | let bestMatch: { path: string; score: number } | null = null; 405 | 406 | for (const file of files) { 407 | const filePath = path.normalize(file.fsPath); 408 | 409 | // 计算路径相似度 410 | if (filePath.endsWith(normalizedPath)) { 411 | const score = this.calculatePathMatchScore(filePath, normalizedPath); 412 | 413 | if (!bestMatch || score > bestMatch.score) { 414 | bestMatch = { path: filePath, score }; 415 | } 416 | } 417 | } 418 | 419 | // 如果没有精确匹配,返回第一个 420 | return bestMatch?.path ?? files[0].fsPath; 421 | } 422 | 423 | /** 424 | * 计算路径匹配分数(从后往前匹配) 425 | */ 426 | private calculatePathMatchScore(fullPath: string, targetPath: string): number { 427 | const fullParts = fullPath.split(path.sep).reverse(); 428 | const targetParts = targetPath.split(path.sep).reverse(); 429 | 430 | let score = 0; 431 | const minLen = Math.min(fullParts.length, targetParts.length); 432 | 433 | for (let i = 0; i < minLen; i++) { 434 | if (fullParts[i] === targetParts[i]) { 435 | // 匹配的部分,权重随深度递增 436 | score += (i + 1) * 10; 437 | } else { 438 | break; 439 | } 440 | } 441 | 442 | return score; 443 | } 444 | } 445 | 446 | export function registerTerminalLinkProvider(context: vscode.ExtensionContext): void { 447 | const terminalLinkProvider = vscode.window.registerTerminalLinkProvider( 448 | new LuaTerminalLinkProvider() 449 | ); 450 | 451 | context.subscriptions.push(terminalLinkProvider); 452 | } 453 | -------------------------------------------------------------------------------- /syntaxes/schema.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "DiagnosticCode.access-invisible": { 3 | "en": "Access invisible", 4 | "zh-CN": "成员可见性检查。" 5 | }, 6 | "DiagnosticCode.annotation-usage-error": { 7 | "en": "Doc tag usage error", 8 | "zh-CN": "注解使用错误。" 9 | }, 10 | "DiagnosticCode.assign-type-mismatch": { 11 | "en": "Assign type mismatch", 12 | "zh-CN": "赋值类型不匹配。" 13 | }, 14 | "DiagnosticCode.attribute-missing-parameter": { 15 | "en": "attribute-missing-parameter" 16 | }, 17 | "DiagnosticCode.attribute-param-type-mismatch": { 18 | "en": "attribute-param-type-mismatch" 19 | }, 20 | "DiagnosticCode.attribute-redundant-parameter": { 21 | "en": "attribute-redundant-parameter" 22 | }, 23 | "DiagnosticCode.await-in-sync": { 24 | "en": "Await in sync", 25 | "zh-CN": "在同步上下文中使用 await。" 26 | }, 27 | "DiagnosticCode.cast-type-mismatch": { 28 | "en": "cast-type-mismatch" 29 | }, 30 | "DiagnosticCode.circle-doc-class": { 31 | "en": "Circle Doc Class", 32 | "zh-CN": "类型循环依赖。" 33 | }, 34 | "DiagnosticCode.code-style-check": { 35 | "en": "Code style check", 36 | "zh-CN": "代码风格检查。" 37 | }, 38 | "DiagnosticCode.deprecated": { 39 | "en": "Deprecated", 40 | "zh-CN": "已弃用。" 41 | }, 42 | "DiagnosticCode.discard-returns": { 43 | "en": "Discard return value", 44 | "zh-CN": "不可丢弃返回值。" 45 | }, 46 | "DiagnosticCode.doc-syntax-error": { 47 | "en": "Doc syntax error", 48 | "zh-CN": "注解语法错误。" 49 | }, 50 | "DiagnosticCode.duplicate-doc-field": { 51 | "en": "Duplicate doc field", 52 | "zh-CN": "重复定义的字段(注解)。" 53 | }, 54 | "DiagnosticCode.duplicate-index": { 55 | "en": "duplicate-index", 56 | "zh-CN": "重复的索引。" 57 | }, 58 | "DiagnosticCode.duplicate-require": { 59 | "en": "Duplicate require", 60 | "zh-CN": "重复的 require 导入。" 61 | }, 62 | "DiagnosticCode.duplicate-set-field": { 63 | "en": "duplicate-set-field", 64 | "zh-CN": "重复定义的字段(实际代码)。" 65 | }, 66 | "DiagnosticCode.duplicate-type": { 67 | "en": "Duplicate type", 68 | "zh-CN": "重复定义的类型。" 69 | }, 70 | "DiagnosticCode.enum-value-mismatch": { 71 | "en": "enum-value-mismatch", 72 | "zh-CN": "枚举值不匹配。" 73 | }, 74 | "DiagnosticCode.generic-constraint-mismatch": { 75 | "en": "generic-constraint-mismatch", 76 | "zh-CN": "泛型约束不匹配。" 77 | }, 78 | "DiagnosticCode.global-in-non-module": { 79 | "en": "Global variable defined in non-module scope" 80 | }, 81 | "DiagnosticCode.incomplete-signature-doc": { 82 | "en": "Incomplete signature doc", 83 | "zh-CN": "不完整的签名文档。" 84 | }, 85 | "DiagnosticCode.inject-field": { 86 | "en": "Inject Field", 87 | "zh-CN": "注入字段。" 88 | }, 89 | "DiagnosticCode.iter-variable-reassign": { 90 | "en": "Iter variable reassign", 91 | "zh-CN": "迭代变量重新赋值。" 92 | }, 93 | "DiagnosticCode.local-const-reassign": { 94 | "en": "Local const reassign", 95 | "zh-CN": "局部常量重新赋值。" 96 | }, 97 | "DiagnosticCode.missing-fields": { 98 | "en": "Missing fields", 99 | "zh-CN": "缺少字段。" 100 | }, 101 | "DiagnosticCode.missing-global-doc": { 102 | "en": "Missing global doc", 103 | "zh-CN": "全局变量缺少文档。" 104 | }, 105 | "DiagnosticCode.missing-parameter": { 106 | "en": "Missing parameter", 107 | "zh-CN": "缺少参数。" 108 | }, 109 | "DiagnosticCode.missing-return": { 110 | "en": "Missing return statement", 111 | "zh-CN": "缺少返回语句。" 112 | }, 113 | "DiagnosticCode.missing-return-value": { 114 | "en": "Missing return value", 115 | "zh-CN": "缺少返回值。" 116 | }, 117 | "DiagnosticCode.need-check-nil": { 118 | "en": "Need check nil", 119 | "zh-CN": "需要检查 nil。" 120 | }, 121 | "DiagnosticCode.non-literal-expressions-in-assert": { 122 | "en": "non-literal-expressions-in-assert", 123 | "zh-CN": "在 assert 中使用了非字面量表达式。" 124 | }, 125 | "DiagnosticCode.param-type-mismatch": { 126 | "en": "Param Type not match" 127 | }, 128 | "DiagnosticCode.preferred-local-alias": { 129 | "en": "preferred-local-alias" 130 | }, 131 | "DiagnosticCode.read-only": { 132 | "en": "readonly" 133 | }, 134 | "DiagnosticCode.redefined-label": { 135 | "en": "Redefined label", 136 | "zh-CN": "重复定义的标签。" 137 | }, 138 | "DiagnosticCode.redefined-local": { 139 | "en": "Redefined local", 140 | "zh-CN": "重复定义的局部变量。" 141 | }, 142 | "DiagnosticCode.redundant-parameter": { 143 | "en": "Redundant parameter", 144 | "zh-CN": "冗余参数。" 145 | }, 146 | "DiagnosticCode.redundant-return-value": { 147 | "en": "Redundant return value", 148 | "zh-CN": "冗余的返回值。" 149 | }, 150 | "DiagnosticCode.require-module-not-visible": { 151 | "en": "require-module-not-visible", 152 | "zh-CN": "require 模块不可见。" 153 | }, 154 | "DiagnosticCode.return-type-mismatch": { 155 | "en": "Return type mismatch", 156 | "zh-CN": "返回类型不匹配。" 157 | }, 158 | "DiagnosticCode.syntax-error": { 159 | "en": "Syntax error", 160 | "zh-CN": "语法错误。" 161 | }, 162 | "DiagnosticCode.type-not-found": { 163 | "en": "Type not found", 164 | "zh-CN": "类型未找到。" 165 | }, 166 | "DiagnosticCode.unbalanced-assignments": { 167 | "en": "Unbalanced assignments", 168 | "zh-CN": "不平衡的赋值。" 169 | }, 170 | "DiagnosticCode.undefined-doc-param": { 171 | "en": "Undefined Doc Param", 172 | "zh-CN": "未使用注解定义的参数。" 173 | }, 174 | "DiagnosticCode.undefined-field": { 175 | "en": "Undefined field", 176 | "zh-CN": "未定义的字段。" 177 | }, 178 | "DiagnosticCode.undefined-global": { 179 | "en": "Undefined global", 180 | "zh-CN": "未定义的全局变量。" 181 | }, 182 | "DiagnosticCode.unknown-doc-tag": { 183 | "en": "Unknown doc annotation", 184 | "zh-CN": "未知文档注解。" 185 | }, 186 | "DiagnosticCode.unnecessary-assert": { 187 | "en": "unnecessary-assert", 188 | "zh-CN": "不必要的 assert。" 189 | }, 190 | "DiagnosticCode.unnecessary-if": { 191 | "en": "unnecessary-if", 192 | "zh-CN": "不必要的 if。" 193 | }, 194 | "DiagnosticCode.unreachable-code": { 195 | "en": "Unreachable code", 196 | "zh-CN": "不可达的代码。" 197 | }, 198 | "DiagnosticCode.unused": { 199 | "en": "Unused", 200 | "zh-CN": "未使用的变量。" 201 | }, 202 | "DiagnosticSeveritySetting.error": { 203 | "en": "Represents an error diagnostic severity.", 204 | "zh-CN": "诊断严重性: 错误。" 205 | }, 206 | "DiagnosticSeveritySetting.hint": { 207 | "en": "Represents a hint diagnostic severity.", 208 | "zh-CN": "诊断严重性: 提示。" 209 | }, 210 | "DiagnosticSeveritySetting.information": { 211 | "en": "Represents an information diagnostic severity.", 212 | "zh-CN": "诊断严重性: 信息。" 213 | }, 214 | "DiagnosticSeveritySetting.warning": { 215 | "en": "Represents a warning diagnostic severity.", 216 | "zh-CN": "诊断严重性: 警告。" 217 | }, 218 | "EmmyrcCodeAction.insertSpace": { 219 | "en": "Add space after `---` comments when inserting `@diagnostic disable-next-line`.", 220 | "zh-CN": "是否在 '---' 后插入空格。" 221 | }, 222 | "EmmyrcCodeLens.enable": { 223 | "en": "Enable code lens." 224 | }, 225 | "EmmyrcCompletion": { 226 | "en": "Configuration for EmmyLua code completion.", 227 | "zh-CN": "EmmyLua 代码补全配置。" 228 | }, 229 | "EmmyrcCompletion.autoRequire": { 230 | "en": "Automatically insert call to `require` when autocompletion\ninserts objects from other modules.", 231 | "zh-CN": "自动补全 require 语句。" 232 | }, 233 | "EmmyrcCompletion.autoRequireFunction": { 234 | "en": "The function used for auto-requiring modules.", 235 | "zh-CN": "自动补全 require 语句时使用的函数名。" 236 | }, 237 | "EmmyrcCompletion.autoRequireNamingConvention": { 238 | "en": "The naming convention for auto-required filenames.", 239 | "zh-CN": "自动补全 require 语句时使用的命名规范。" 240 | }, 241 | "EmmyrcCompletion.autoRequireSeparator": { 242 | "en": "A separator used in auto-require paths.", 243 | "zh-CN": "自动补全 require 语句时使用的分隔符。" 244 | }, 245 | "EmmyrcCompletion.baseFunctionIncludesName": { 246 | "en": "Whether to include the name in the base function completion. Effect: `function () end` -> `function name() end`.", 247 | "zh-CN": "是否在基本函数补全中包含函数名。效果: `function () end` -> `function name() end`。" 248 | }, 249 | "EmmyrcCompletion.callSnippet": { 250 | "en": "Whether to use call snippets in completions.", 251 | "zh-CN": "是否使用代码片段补全函数调用。" 252 | }, 253 | "EmmyrcCompletion.enable": { 254 | "en": "Enable autocompletion.", 255 | "zh-CN": "是否启用代码补全。" 256 | }, 257 | "EmmyrcCompletion.postfix": { 258 | "en": "Symbol that's used to trigger postfix autocompletion.", 259 | "zh-CN": "后缀补全触发关键词。" 260 | }, 261 | "EmmyrcDiagnostic": { 262 | "en": "Represents the diagnostic configuration for Emmyrc.", 263 | "zh-CN": "Emmyrc 诊断配置。" 264 | }, 265 | "EmmyrcDiagnostic.diagnosticInterval": { 266 | "en": "Delay between opening/changing a file and scanning it for errors, in milliseconds.", 267 | "zh-CN": "诊断间隔时间(毫秒)。" 268 | }, 269 | "EmmyrcDiagnostic.disable": { 270 | "en": "A list of diagnostic codes that are disabled.", 271 | "zh-CN": "禁用的诊断代码列表。" 272 | }, 273 | "EmmyrcDiagnostic.enable": { 274 | "en": "A flag indicating whether diagnostics are enabled.", 275 | "zh-CN": "是否启用诊断。" 276 | }, 277 | "EmmyrcDiagnostic.enables": { 278 | "en": "A list of diagnostic codes that are enabled.", 279 | "zh-CN": "启用的诊断代码列表。" 280 | }, 281 | "EmmyrcDiagnostic.globals": { 282 | "en": "A list of global variables.", 283 | "zh-CN": "全局变量列表,在该列表中的全局变量不会被诊断为未定义。" 284 | }, 285 | "EmmyrcDiagnostic.globalsRegex": { 286 | "en": "A list of regular expressions for global variables.", 287 | "zh-CN": "全局变量正则表达式列表,符合正则表达式的全局变量不会被诊断为未定义。" 288 | }, 289 | "EmmyrcDiagnostic.severity": { 290 | "en": "A map of diagnostic codes to their severity settings.", 291 | "zh-CN": "诊断代码与严重性设置的映射。" 292 | }, 293 | "EmmyrcDoc.knownTags": { 294 | "en": "List of known documentation tags.", 295 | "zh-CN": "已知文档标签列表。" 296 | }, 297 | "EmmyrcDoc.privateName": { 298 | "en": "Treat specific field names as private, e.g. `m_*` means `XXX.m_id` and `XXX.m_type` are private, witch can only be accessed in the class where the definition is located.", 299 | "zh-CN": "将特定字段名视为私有,例如 `m_*` 表示 `XXX.m_id` 和 `XXX.m_type` 是私有字段,只能在定义所在的类中访问。" 300 | }, 301 | "EmmyrcDoc.rstDefaultRole": { 302 | "en": "When `syntax` is `Myst` or `Rst`, specifies default role used\nwith RST processor." 303 | }, 304 | "EmmyrcDoc.rstPrimaryDomain": { 305 | "en": "When `syntax` is `Myst` or `Rst`, specifies primary domain used\nwith RST processor." 306 | }, 307 | "EmmyrcDoc.syntax": { 308 | "en": "Syntax for highlighting documentation." 309 | }, 310 | "EmmyrcDocumentColor.enable": { 311 | "en": "Enable parsing strings for color tags and showing a color picker next to them.", 312 | "zh-CN": "是否启用文档颜色。" 313 | }, 314 | "EmmyrcExternalTool.args": { 315 | "en": "The arguments to pass to the external tool." 316 | }, 317 | "EmmyrcExternalTool.program": { 318 | "en": "The command to run the external tool." 319 | }, 320 | "EmmyrcExternalTool.timeout": { 321 | "en": "The timeout for the external tool in milliseconds." 322 | }, 323 | "EmmyrcFilenameConvention.camel-case": { 324 | "en": "Convert the filename to camelCase.", 325 | "zh-CN": "将文件名转换为驼峰(camelCase)命名。" 326 | }, 327 | "EmmyrcFilenameConvention.keep": { 328 | "en": "Keep the original filename.", 329 | "zh-CN": "保持原始文件名。" 330 | }, 331 | "EmmyrcFilenameConvention.keep-class": { 332 | "en": "When returning class definition, use class name, otherwise keep original name.", 333 | "zh-CN": "返回类定义时,使用类名,否则保持原始名称。" 334 | }, 335 | "EmmyrcFilenameConvention.pascal-case": { 336 | "en": "Convert the filename to PascalCase.", 337 | "zh-CN": "将文件名转换为帕斯卡(PascalCase)命名。" 338 | }, 339 | "EmmyrcFilenameConvention.snake-case": { 340 | "en": "Convert the filename to snake_case.", 341 | "zh-CN": "将文件名转换为蛇形(snake_case)命名。" 342 | }, 343 | "EmmyrcHover.customDetail": { 344 | "en": "The detail number of hover information.\nDefault is `None`, which means using the default detail level.\nYou can set it to a number between `1` and `255` to customize" 345 | }, 346 | "EmmyrcHover.enable": { 347 | "en": "Enable showing documentation on hover.", 348 | "zh-CN": "是否启用悬浮提示。" 349 | }, 350 | "EmmyrcInlayHint.enable": { 351 | "en": "Enable inlay hints.", 352 | "zh-CN": "是否启用内联提示。" 353 | }, 354 | "EmmyrcInlayHint.enumParamHint": { 355 | "en": "Show name of enumerator when passing a literal value to a function\nthat expects an enum.\n\nExample:\n\n```lua\n--- @enum Level\nlocal Foo = {\n Info = 1,\n Error = 2,\n}\n\n--- @param l Level\nfunction print_level(l) end\n\nprint_level(1 --[[ Hint: Level.Info ]])\n```", 356 | "zh-CN": "是否启用枚举参数提示。" 357 | }, 358 | "EmmyrcInlayHint.indexHint": { 359 | "en": "Show named array indexes.\n\nExample:\n\n```lua\nlocal array = {\n [1] = 1, -- [name]\n}\n\nprint(array[1] --[[ Hint: name ]])\n```", 360 | "zh-CN": "在索引表达式跨行时,显示提示。" 361 | }, 362 | "EmmyrcInlayHint.localHint": { 363 | "en": "Show types of local variables.", 364 | "zh-CN": "是否启用局部变量提示。" 365 | }, 366 | "EmmyrcInlayHint.metaCallHint": { 367 | "en": "Show hint when calling an object results in a call to\nits meta table's `__call` function.", 368 | "zh-CN": "是否启用 `__call` 调用提示。" 369 | }, 370 | "EmmyrcInlayHint.overrideHint": { 371 | "en": "Show methods that override functions from base class.", 372 | "zh-CN": "是否启用重写提示。" 373 | }, 374 | "EmmyrcInlayHint.paramHint": { 375 | "en": "Show parameter names in function calls and parameter types in function definitions.", 376 | "zh-CN": "是否启用参数提示。" 377 | }, 378 | "EmmyrcInlineValues.enable": { 379 | "en": "Show inline values during debug.", 380 | "zh-CN": "是否启用内联值。用于在调试断点时显示变量值。" 381 | }, 382 | "EmmyrcLuaVersion.Lua5.1": { 383 | "en": "Lua 5.1" 384 | }, 385 | "EmmyrcLuaVersion.Lua5.2": { 386 | "en": "Lua 5.2" 387 | }, 388 | "EmmyrcLuaVersion.Lua5.3": { 389 | "en": "Lua 5.3" 390 | }, 391 | "EmmyrcLuaVersion.Lua5.4": { 392 | "en": "Lua 5.4" 393 | }, 394 | "EmmyrcLuaVersion.Lua5.5": { 395 | "en": "Lua 5.5" 396 | }, 397 | "EmmyrcLuaVersion.LuaJIT": { 398 | "en": "LuaJIT" 399 | }, 400 | "EmmyrcLuaVersion.LuaLatest": { 401 | "en": "Lua Latest" 402 | }, 403 | "EmmyrcReference.enable": { 404 | "en": "Enable searching for symbol usages.", 405 | "zh-CN": "是否启用引用搜索。" 406 | }, 407 | "EmmyrcReference.fuzzySearch": { 408 | "en": "Use fuzzy search when searching for symbol usages\nand normal search didn't find anything.", 409 | "zh-CN": "是否启用模糊搜索。" 410 | }, 411 | "EmmyrcReference.shortStringSearch": { 412 | "en": "Also search for usages in strings.", 413 | "zh-CN": "缓存短字符串用于搜索。" 414 | }, 415 | "EmmyrcReformat.externalTool": { 416 | "en": "Whether to enable external tool formatting." 417 | }, 418 | "EmmyrcReformat.externalToolRangeFormat": { 419 | "en": "Whether to enable external tool range formatting." 420 | }, 421 | "EmmyrcReformat.useDiff": { 422 | "en": "Whether to use the diff algorithm for formatting." 423 | }, 424 | "EmmyrcRuntime.extensions": { 425 | "en": "file Extensions. eg: .lua, .lua.txt", 426 | "zh-CN": "文件扩展名。例如:.lua, .lua.txt" 427 | }, 428 | "EmmyrcRuntime.frameworkVersions": { 429 | "en": "Framework versions.", 430 | "zh-CN": "框架版本列表。" 431 | }, 432 | "EmmyrcRuntime.nonstandardSymbol": { 433 | "en": "Non-standard symbols." 434 | }, 435 | "EmmyrcRuntime.requireLikeFunction": { 436 | "en": "Functions that like require.", 437 | "zh-CN": "类似 require 的函数列表。" 438 | }, 439 | "EmmyrcRuntime.requirePattern": { 440 | "en": "Require pattern. eg. \"?.lua\", \"?/init.lua\"", 441 | "zh-CN": "require 模式。例如:\"?.lua\", \"?/init.lua\"" 442 | }, 443 | "EmmyrcRuntime.special": { 444 | "en": "Special symbols." 445 | }, 446 | "EmmyrcRuntime.version": { 447 | "en": "Lua version.", 448 | "zh-CN": "Lua 版本。" 449 | }, 450 | "EmmyrcSemanticToken.enable": { 451 | "en": "Enable semantic tokens.", 452 | "zh-CN": "是否启用语义标记。" 453 | }, 454 | "EmmyrcSemanticToken.renderDocumentationMarkup": { 455 | "en": "Render Markdown/RST in documentation. Set `doc.syntax` for this option to have effect." 456 | }, 457 | "EmmyrcSignature.detailSignatureHelper": { 458 | "en": "Whether to enable signature help.", 459 | "zh-CN": "是否启用签名帮助。" 460 | }, 461 | "EmmyrcStrict.arrayIndex": { 462 | "en": "Whether to enable strict mode array indexing.", 463 | "zh-CN": "是否启用严格模式数组索引,严格模式下数组取值返回将包含 nil。" 464 | }, 465 | "EmmyrcStrict.docBaseConstMatchBaseType": { 466 | "en": "Base constant types defined in doc can match base types, allowing int to match `---@alias id 1|2|3`, same for string.", 467 | "zh-CN": "doc定义的基础常量类型可以匹配基础类型,使 int 可以匹配 `---@alias id 1|2|3`,string 同理。" 468 | }, 469 | "EmmyrcStrict.metaOverrideFileDefine": { 470 | "en": "meta define overrides file define", 471 | "zh-CN": "`---@meta`文件的定义完全覆盖真实文件的定义。" 472 | }, 473 | "EmmyrcStrict.requirePath": { 474 | "en": "Whether to enable strict mode require path.", 475 | "zh-CN": "是否启用严格模式 require 路径。严格模式时 require 必须从指定的根目录开始。" 476 | }, 477 | "EmmyrcWorkspace.enableReindex": { 478 | "en": "Enable full project reindex after changing a file.", 479 | "zh-CN": "启用重新索引。" 480 | }, 481 | "EmmyrcWorkspace.encoding": { 482 | "en": "Encoding. eg: \"utf-8\"", 483 | "zh-CN": "编码。例如:\"utf-8\"" 484 | }, 485 | "EmmyrcWorkspace.ignoreDir": { 486 | "en": "Ignore directories.", 487 | "zh-CN": "忽略的目录。" 488 | }, 489 | "EmmyrcWorkspace.ignoreGlobs": { 490 | "en": "Ignore globs. eg: [\"**/*.lua\"]", 491 | "zh-CN": "忽略的文件。例如:[\"**/*.lua\"]" 492 | }, 493 | "EmmyrcWorkspace.library": { 494 | "en": "Library paths. eg: \"/usr/local/share/lua/5.1\"", 495 | "zh-CN": "库路径。例如:\"/usr/local/share/lua/5.1\"" 496 | }, 497 | "EmmyrcWorkspace.moduleMap": { 498 | "en": "Module map. key is regex, value is new module regex\neg: {\n \"^(.*)$\": \"module_$1\"\n \"^lib(.*)$\": \"script$1\"\n}", 499 | "zh-CN": "模块映射列表。key 是正则表达式,value 是新的模块正则表达式\n 例如:{\n \"^(.*)$\": \"module_$1\"\n \"^lib(.*)$\": \"script$1\"\n}" 500 | }, 501 | "EmmyrcWorkspace.reindexDuration": { 502 | "en": "Delay between changing a file and full project reindex, in milliseconds.", 503 | "zh-CN": "当保存文件时,ls 将在指定毫秒后重新索引工作区。" 504 | }, 505 | "EmmyrcWorkspace.workspaceRoots": { 506 | "en": "Workspace roots. eg: [\"src\", \"test\"]", 507 | "zh-CN": "工作区根目录列表。例如:[\"src\", \"test\"]" 508 | } 509 | } 510 | --------------------------------------------------------------------------------