diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0fc71f..57b21dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - # TypeScript compilation check + # Repository validation checks - repo: local hooks: - - id: typescript-compile - name: TypeScript Compile Check - entry: npm run compile + - id: npm-precommit + name: Repository Precommit Check + entry: npm run precommit language: system - files: \.(ts|tsx)$ pass_filenames: false + always_run: true # Pre-commit hooks for general file checks - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/doc/Message_Interfaces.md b/doc/Message_Interfaces.md index 93686a9..52cb53f 100644 --- a/doc/Message_Interfaces.md +++ b/doc/Message_Interfaces.md @@ -1,6 +1,6 @@ # Viewer to External Editor JSON-RPC
Message Interfaces Documentation -This document describes all the message interfaces defined in for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. +This document describes all the message interfaces defined for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. ## Table of Contents @@ -15,10 +15,13 @@ This document describes all the message interfaces defined in for WebSocket comm - [SyntaxChange](#syntaxchange) - [Language Syntax ID Request](#language-syntax-id-request) - [Language Syntax Request](#language-syntax-request) + - [Language Syntax Cache List](#language-syntax-cache-list) + - [Language Syntax Cache Get](#language-syntax-cache-get) - [Script Subscription Interfaces](#script-subscription-interfaces) - [ScriptSubscribe](#scriptsubscribe) - [ScriptSubscribeResponse](#scriptsubscriberesponse) - [ScriptUnsubscribe](#scriptunsubscribe) + - [ScriptList](#scriptlist) - [Compilation Interfaces](#compilation-interfaces) - [CompilationError](#compilationerror) - [CompilationResult](#compilationresult) @@ -33,7 +36,7 @@ This document describes all the message interfaces defined in for WebSocket comm 1. **Connection Establishment:** - - Viewer sends `session.handshake` notification with `SessionHandshake` data + - Viewer sends `session.handshake` call with `SessionHandshake` data - Extension responds with `SessionHandshakeResponse` - Viewer confirms with `session.ok` notification @@ -41,7 +44,7 @@ This document describes all the message interfaces defined in for WebSocket comm - Extension makes `language.syntax.id` call to get current syntax version - Extension makes `language.syntax` calls with different `kind` parameters to get specific language data - - Viewer responds with `LanguageInfo` data containing the requested information + - Viewer responds with a `LanguageInfo` object containing the requested definitions 3. **Script Subscription Management:** @@ -65,17 +68,23 @@ This document describes all the message interfaces defined in for WebSocket comm | Method | Direction | Type | Interface/Parameters | | ------------------------------- | ------------------ | ------------ | -------------------------- | -| `session.handshake` | Viewer → Extension | Notification | `SessionHandshake` | +| `session.handshake` | Viewer → Extension | Call | `SessionHandshake` | | `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | | `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | | `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | | `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | | `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | +| `script.list` | Extension → Viewer | Call | _(no parameters)_ | +| `script.list` (response) | Viewer → Extension | Response | `ScriptList` | | `language.syntax.id` | Extension → Viewer | Call | _(no parameters)_ | | `language.syntax.id` (response) | Viewer → Extension | Response | `{ id: string }` | | `language.syntax` | Extension → Viewer | Call | `{ kind: string }` | | `language.syntax` (response) | Viewer → Extension | Response | `LanguageInfo` | +| `language.syntax.cache` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.cache` (response) | Viewer → Extension | Response | `SyntaxCacheList` | +| `language.syntax.get` | Extension → Viewer | Call | `{ filename: string, as_json?: boolean }` | +| `language.syntax.get` (response) | Viewer → Extension | Response | `SyntaxCacheFile` | | `language.syntax.change` | Viewer → Extension | Notification | `SyntaxChange` | | `script.compiled` | Viewer → Extension | Notification | `CompilationResult` | | `runtime.debug` | Viewer → Extension | Notification | `RuntimeDebug` | @@ -85,9 +94,9 @@ This document describes all the message interfaces defined in for WebSocket comm ### SessionHandshake -**JSON-RPC Method:** `session.handshake` (notification from viewer) +**JSON-RPC Method:** `session.handshake` (call from viewer) -The initial handshake message sent by the viewer to establish a connection. +The initial handshake call sent by the viewer to establish a session. ```typescript interface SessionHandshake { @@ -112,10 +121,13 @@ interface SessionHandshake { - `viewer_version`: Version string of the viewer - `agent_id`: Unique identifier for the user/agent - `agent_name`: Human-readable name of the agent -- `challenge` (optional): Security challenge string for authentication -- `languages`: Array of supported scripting languages (e.g., ["lsl", "luau"]) -- `syntax_id`: Current active syntax/language identifier -- `features`: Dictionary of feature flags indicating viewer capabilities +- `challenge` (optional): Path to a temporary file on the local filesystem containing a UUID. The client must read this file and return the UUID as `challenge_response` to authenticate the connection. +- `languages`: Array of supported scripting languages (e.g., `["lsl", "luau"]`) +- `syntax_id`: Current active syntax identifier as a UUID string +- `features`: Dictionary of feature flags indicating viewer capabilities. Known flags: + - `live_sync`: Viewer supports live script synchronisation with the external editor + - `compilation`: Viewer will forward compilation results via `script.compiled` + - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files ### SessionHandshakeResponse @@ -131,6 +143,8 @@ interface SessionHandshakeResponse { challenge_response?: string; languages: string[]; features: { [feature: string]: boolean }; + script_name?: string; + script_language?: string; } ``` @@ -139,15 +153,17 @@ interface SessionHandshakeResponse { - `client_name`: Name of the client (VS Code extension) - `client_version`: Fixed version "1.0" of the client - `protocol_version`: Protocol version the client supports -- `challenge_response` (optional): Response to the security challenge if provided +- `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed. - `languages`: Array of languages supported by the client - `features`: Dictionary of features supported by the client +- `script_name` (optional): Name of the script currently open in the editor +- `script_language` (optional): Language of the script currently open in the editor (e.g. `"lsl"`, `"luau"`) ### Session OK **JSON-RPC Method:** `session.ok` (notification from viewer) -Confirmation notification sent by the viewer after successful handshake completion. This interface has no defined structure as it appears to be a simple confirmation message. +Confirmation notification sent by the viewer after successful handshake completion. No parameters are sent with this notification. ### SessionDisconnect @@ -164,7 +180,12 @@ interface SessionDisconnect { **Fields:** -- `reason`: Numeric code indicating the reason for disconnection +- `reason`: Numeric code indicating the reason for disconnection: + - `0`: Normal closure + - `1`: Editor closed + - `2`: Protocol error + - `3`: Connection timeout + - `4`: Internal server error - `message`: Human-readable description of the disconnect reason ## Language and Syntax Interfaces @@ -183,7 +204,7 @@ interface SyntaxChange { **Fields:** -- `id`: Identifier for the new syntax/language +- `id`: UUID string identifying the new syntax version ### Language Syntax ID Request @@ -191,63 +212,130 @@ interface SyntaxChange { Requests the current active language syntax identifier from the viewer. This method takes no parameters. -**Response:** Returns an object with an `id` field containing the current syntax identifier. +**Response:** Returns `{ id: string }` where `id` is the current syntax version as a UUID string. ### Language Syntax Request **JSON-RPC Method:** `language.syntax` (call from extension to viewer) -Requests detailed syntax information for a specific language kind. +Requests the in-memory keyword definitions for a specific language. These definitions are the deserialized, viewer-processed form of the syntax data for the current region. **Parameters:** ```typescript { - kind: string; // The type of syntax information requested + kind: string; // The language whose definitions to retrieve } ``` -**Fields:** +**Valid `kind` values:** -- `kind`: The type of syntax information to retrieve (e.g., "functions", "constants", "events", "types.luau") +| Value | Description | +| ----------- | ----------------------------------------- | +| `"defs.lsl"` | Returns the LSL keyword definitions | +| `"defs.lua"` | Returns the Luau keyword definitions | -**Response:** Returns `LanguageInfo` data containing the requested syntax information: +**Response:** ```typescript interface LanguageInfo { id: string; - lslDefs?: { - controls?: any; - types?: any; - constants?: { [name: string]: ConstantDef }; - events?: { [name: string]: FunctionDef }; - functions?: { [name: string]: FunctionDef }; - }; - luaDefs?: { - modules?: { [name: string]: TypeDef }; - classes?: { [name: string]: TypeDef }; - aliases?: { [name: string]: TypeDef }; - functions?: { [name: string]: FunctionDef }; - }; + defs?: object; // Present only on success + success: boolean; + error?: string; // Present only on failure +} +``` + +**Response Fields:** + +- `id`: The current syntax version identifier +- `defs` (optional): The keyword definitions object. Only present when `success` is `true`. Structure varies by language. +- `success`: Whether the definitions were found and returned successfully +- `error` (optional): Human-readable error description. Only present when `success` is `false` + +**Error cases:** + +- No `kind` parameter supplied: `success: false`, `error: "No syntax category specified"` +- Unknown `kind` value: `success: false`, `error: "Unknown syntax category requested"` + +### Language Syntax Cache List + +**JSON-RPC Method:** `language.syntax.cache` (call from extension to viewer) + +Requests the list of file names currently held in the `LLSyntaxDefCache`. This provides the extension with the available syntax definition file names that can subsequently be retrieved with `language.syntax.get`. This method takes no parameters. + +**Response:** + +```typescript +interface SyntaxCacheList { + files: string[]; // Array of file names (e.g. ["lsl_keywords.xml", "slua_definitions.yaml"]) + success: boolean; } ``` **Response Fields:** -- `id`: Version identifier for the language syntax -- `lslDefs` (optional): LSL-specific language definitions containing: - - `controls` (optional): Control flow and language constructs - - `types` (optional): LSL type definitions - - `constants` (optional): Object containing constant definitions keyed by constant name - - `events` (optional): Object containing event definitions keyed by event name - - `functions` (optional): Object containing function definitions keyed by function name -- `luaDefs` (optional): Lua-specific language definitions containing: - - `modules` (optional): Module type definitions keyed by module name - - `classes` (optional): Class type definitions keyed by class name - - `aliases` (optional): Type alias definitions keyed by alias name - - `functions` (optional): Function definitions keyed by function name - -The specific sections returned depend on the `kind` parameter and the active language context. +- `files`: Array of file name strings, each of which can be passed as the `filename` parameter to `language.syntax.get` +- `success`: Whether the request was handled successfully + +**Known cache files:** + +| File name | Description | +| -------------------------------- | ---------------------------------------------------- | +| `builtins.txt` | LSL built-in keyword list in plain text format | +| `lsl_definitions.yaml` | LSL language definitions in YAML format | +| `lsl_keywords.xml` | LSL keyword definitions in LLSD XML format | +| `lsl_keywords_pretty.xml` | LSL keyword definitions in formatted LLSD XML format | +| `slua_default.d.luau` | Luau type definition file for editor tooling | +| `slua_default.docs.json` | Luau documentation data in JSON format | +| `slua_definitions.yaml` | Luau language definitions in YAML format | +| `slua_keywords.xml` | Luau keyword definitions in LLSD XML format | +| `slua_keywords_pretty.xml` | Luau keyword definitions in formatted LLSD XML format | +| `slua_selene.yml` | Luau Selene linter configuration in YAML format | + +Not all files may be present in every cache — the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. + +### Language Syntax Cache Get + +**JSON-RPC Method:** `language.syntax.get` (call from extension to viewer) + +Requests the content of a specific file from the syntax definition cache. The file name must be one of the names returned by a prior `language.syntax.cache` call. Content is returned either as a raw text string or as a parsed JSON/LLSD object depending on the `as_json` parameter. + +**Parameters:** + +```typescript +{ + filename: string; // The file name to retrieve, as returned by language.syntax.cache + as_json?: boolean; // Optional. If true, content is returned as a parsed object rather than raw text +} +``` + +**Fields:** + +- `filename`: The file name to retrieve (e.g. `"lsl_keywords.xml"`, `"slua_definitions.yaml"`) +- `as_json` (optional): When `true`, the file is deserialized and returned as a structured object in `content`. When omitted or `false`, `content` is the raw text of the file. + +**Response:** + +```typescript +interface SyntaxCacheFile { + content?: string | object; // Present only on success. String if as_json is false/omitted, object if as_json is true + success: boolean; + error?: string; // Present only on failure +} +``` + +**Response Fields:** + +- `content`: The file content. Only present when `success` is `true`. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. +- `success`: Whether the file was found and read successfully +- `error` (optional): Human-readable error description. Only present when `success` is `false` + +**Error cases:** + +- No `filename` parameter supplied: `success: false`, `error: "No filename specified"` +- Name not found in cache: `success: false`, `error: "Requested syntax cache file not found"` +- File could not be loaded: `success: false`, `error: "Failed to load syntax cache file"` (or `"Failed to load and format syntax cache file."` when `as_json` is `true`) ## Script Subscription Interfaces @@ -283,7 +371,6 @@ interface ScriptSubscribeResponse { success: boolean; status: number; object_id?: string; - object_name?: string; item_id?: string; message?: string; } @@ -293,9 +380,14 @@ interface ScriptSubscribeResponse { - `script_id`: The script identifier that was subscribed to - `success`: Whether the subscription was successful -- `status`: Numeric status code indicating the result -- `object_id` (optional): The in-world ID of the object containing the script -- `object_name` (optional): The name of the object containing the script. +- `status`: Numeric status code indicating the result: + - `0`: Success + - `1`: Invalid editor — the script editor panel is no longer open + - `2`: Invalid subscription — no subscription found for the given `script_id` + - `3`: Already subscribed — another connection is already subscribed to this script + - `4`: Internal server error +- `object_id` (optional): The in-world UUID of the object containing the script +- `item_id` (optional): The inventory item UUID of the script within the object - `message` (optional): Additional information about the subscription result ### ScriptUnsubscribe @@ -314,6 +406,28 @@ interface ScriptUnsubscribe { - `script_id`: Unique identifier for the script to unsubscribe from +### ScriptList + +**JSON-RPC Method:** `script.list` (call from extension to viewer) + +Requests the list of all scripts currently open and tracked by the viewer, along with the viewer's temp directory. This is intended for use by a file watcher tool that needs to discover which script temp files are active without going through the full `script.subscribe` flow. This method takes no parameters. + +**Response:** + +```typescript +interface ScriptList { + temp_dir: string; + script_ids: string[]; + success: boolean; +} +``` + +**Response Fields:** + +- `temp_dir`: The absolute path to the viewer's temp directory where live-sync script files are written. Combined with a `script_id`, the caller can locate the corresponding temp file on disk. +- `script_ids`: Array of script ID strings for all currently subscribed scripts, across all active connections. +- `success`: Always `true`. + ## Compilation Interfaces ### CompilationError @@ -324,17 +438,19 @@ Individual compilation error record. interface CompilationError { row: number; column: number; - level: "ERROR"; + level: string; message: string; + format?: "lsl"; // Present only for LSL compilation errors } ``` **Fields:** -- `row`: Line number where the error occurred (0-based or 1-based depending on context) -- `column`: Column position of the error -- `level`: Severity level (currently only "ERROR" is defined) +- `row`: Line number where the error occurred (1-based for both LSL and Luau) +- `column`: Column position of the error (1-based for LSL; always `0` for Luau as the compiler does not provide column information) +- `level`: Compiler severity string (e.g. `"ERROR"`, `"WARNING"`) - `message`: Error description +- `format` (optional): Present and set to `"lsl"` for LSL compilation errors; absent for Luau errors ### CompilationResult @@ -405,10 +521,10 @@ interface RuntimeError { - `script_id`: Unique identifier for the script that encountered the error - `object_id`: Unique identifier for the object containing the script - `object_name`: Human-readable name of the object -- `message`: Error message description -- `error`: Specific error type or code -- `line`: Line number where the error occurred -- `stack` (optional): Stack trace information if available +- `message`: The full raw chat text of the runtime error message as received from the simulator +- `error`: Extracted error description. Currently always an empty string — runtime error extraction from the simulator's multi-message format is not yet fully implemented. +- `line`: Line number where the error occurred. Currently always `0` for the same reason. +- `stack` (optional): Stack trace lines if they could be extracted from the error message ## Handler and Configuration Interfaces @@ -436,7 +552,7 @@ interface WebSocketHandlers { - `onHandshake`: Handler for initial handshake message, returns handshake response - `onHandshakeOk`: Handler called when handshake is successfully completed - `onDisconnect`: Handler for disconnect notifications -- `onSubscribe`: Handler for script subscription requests from viewer, returns subscription response +- `onSubscribe`: Handler called when the extension sends a `script.subscribe` request, returns subscription response - `onUnsubscribe`: Handler for script unsubscription notifications from viewer - `onSyntaxChange`: Handler for syntax change notifications - `onConnectionClosed`: Handler called when connection is closed diff --git a/src/pluginsupport.ts b/src/pluginsupport.ts index 7deb61d..35a0e98 100644 --- a/src/pluginsupport.ts +++ b/src/pluginsupport.ts @@ -82,6 +82,32 @@ export class SelenePlugin extends BasePlugin { return true; } + public async configureFromViewerCache( + version: any, + viewerSeleneYml: string, + ): Promise { + if (!SelenePlugin.isEnabledHost(this.host)) { + console.warn("Selene plugin not active - skipping configuration"); + return false; + } + + const basename = `slua_${version}`; + const configPath = await this.host.config.getWorkspaceConfigPath(); + + const saved = await SelenePlugin.saveSLuaSeleneConfig( + configPath, + basename + `.yml`, + viewerSeleneYml, + this.host, + ); + + if (saved) { + await SelenePlugin.updateSeleneConfig(configPath, basename, this.host); + } + + return saved; + } + private buildSeleneConfig(version: any, defs: LuaTypeDefinitions): string { const generator = new SeleneYamlGenerator(); const config = { @@ -142,6 +168,11 @@ export class LuaLSPPlugin extends BasePlugin { version: any, defs: LuaTypeDefinitions, ): Promise { + if (!LuaLSPPlugin.isEnabledHost(this.host)) { + console.warn("Lua LSP plugin not active - skipping configuration"); + return false; + } + // Implementation for configuring the Lua LSP plugin let configs = this.buildLuauLSPConfig(defs); @@ -264,6 +295,35 @@ export class LuaLSPPlugin extends BasePlugin { return fullPath; } + public async configureFromViewerCache( + version: any, + viewerDLuau: string, + viewerDocsJson: string, + ): Promise { + const configPath = await this.host.config.getWorkspaceConfigPath(); + + const defsFiles: { [k: string]: string } = {}; + + defsFiles["sl-slua"] = await this.saveLuauLSPDefs( + configPath, + version, + viewerDLuau, + ); + + if (this.host.config.getConfig(ConfigKey.PreprocessorConstantsInSLua, false)) { + defsFiles["sl-slua-consts"] = await this.saveLuauLSPConstantDefs(configPath); + } + + const docsFileName = await this.saveLuauLSPDocs( + configPath, + version, + viewerDocsJson, + ); + + await this.restartLuauLSP(defsFiles, docsFileName, this.host); + return true; + } + public buildLuauLSPConfig( defs: LuaTypeDefinitions, ): [string, string] { diff --git a/src/shared/languagerepository.ts b/src/shared/languagerepository.ts index 561a2fd..a245c51 100644 --- a/src/shared/languagerepository.ts +++ b/src/shared/languagerepository.ts @@ -5,6 +5,7 @@ import { HostInterface, NormalizedPath, normalizeJoinPath } from '../interfaces/hostinterface'; import { LanguageTransformer } from './languagetransformer'; import { JSONRPCInterface } from '../websockclient'; +import { SyntaxCacheFile, SyntaxCacheGetRequest, SyntaxCacheList } from '../viewereditwsclient'; import { LSLKeywords } from "./lslkeywords"; import { LuaTypeDefinitions } from "./luadefsinterface"; import { sortObjectKeysRecursive } from '../utils'; @@ -18,9 +19,12 @@ export interface LanguageInfo { export interface FetchOptions { force?: boolean; // bypass cache when true socket?: JSONRPCInterface; // viewer connection for remote fetch + syntaxCacheSupported?: boolean; } export class LanguageRepository { + public syntaxCacheFiles: string[] = []; + constructor(private readonly host: HostInterface) {} public async getSyntax(version: string, opts: FetchOptions = {}): Promise { @@ -114,6 +118,44 @@ export class LanguageRepository { } } + public async requestSyntaxCacheList(socket: JSONRPCInterface): Promise { + try { + const result = await socket.call('language.syntax.cache') as SyntaxCacheList; + if (result && result.success === true && Array.isArray(result.files)) { + this.syntaxCacheFiles = result.files; + return this.syntaxCacheFiles; + } + this.syntaxCacheFiles = []; + return null; + } catch (error) { + console.error('Error calling language.syntax.cache:', error); + this.syntaxCacheFiles = []; + return null; + } + } + + public async requestSyntaxCacheFile( + socket: JSONRPCInterface, + filename: string, + asJson?: boolean, + ): Promise { + const params: SyntaxCacheGetRequest = { + filename, + ...(asJson !== undefined ? { as_json: asJson } : {}), + }; + + try { + const result = await socket.call('language.syntax.get', params) as SyntaxCacheFile; + if (result && result.success === true && result.content !== undefined) { + return result.content; + } + return null; + } catch (error) { + console.error(`Error calling language.syntax.get for ${filename}:`, error); + return null; + } + } + private async requestLanguageSyntax(socket: JSONRPCInterface, kind: string): Promise { const params = { kind }; try { diff --git a/src/shared/languageservice.ts b/src/shared/languageservice.ts index 3088fda..2d389a4 100644 --- a/src/shared/languageservice.ts +++ b/src/shared/languageservice.ts @@ -106,9 +106,21 @@ export class LanguageService implements DisposableLike { } //#region Language Info Fetching - public async changeSyntaxVersion(syntaxId: string, - socket?: JSONRPCInterface, force?: boolean): Promise { - const syntax = await this.repository.getSyntax(syntaxId, { force, socket }); + public async changeSyntaxVersion( + syntaxId: string, + socket?: JSONRPCInterface, + force?: boolean, + syntaxCacheSupported?: boolean, + ): Promise { + if (syntaxCacheSupported && socket) { + return await this.configureSyntaxFromViewerCache(syntaxId, socket); + } + + const syntax = await this.repository.getSyntax(syntaxId, { + force, + socket, + syntaxCacheSupported, + }); if (!syntax) { console.warn(`No language syntax found for version ${syntaxId}`); @@ -134,9 +146,59 @@ export class LanguageService implements DisposableLike { return true; } + private async configureSyntaxFromViewerCache( + syntaxId: string, + socket: JSONRPCInterface, + ): Promise { + const cacheFiles = this.repository.syntaxCacheFiles; + + const selene = new SelenePlugin(this.host); + if (cacheFiles.includes("slua_selene.yml")) { + const content = await this.repository.requestSyntaxCacheFile(socket, "slua_selene.yml"); + if (typeof content === "string") { + await selene.configureFromViewerCache(syntaxId, content); + } else { + console.warn("syntax_cache: slua_selene.yml missing or invalid, skipping Selene configuration"); + } + } else { + console.warn("syntax_cache: slua_selene.yml not in viewer cache, skipping Selene configuration"); + } + + const luauLSP = new LuaLSPPlugin(this.host); + const hasDLuau = cacheFiles.includes("slua_default.d.luau"); + const hasDocs = cacheFiles.includes("slua_default.docs.json"); + if (hasDLuau && hasDocs) { + const dLuau = await this.repository.requestSyntaxCacheFile(socket, "slua_default.d.luau"); + const docs = await this.repository.requestSyntaxCacheFile(socket, "slua_default.docs.json"); + if (typeof dLuau === "string" && typeof docs === "string") { + await luauLSP.configureFromViewerCache(syntaxId, dLuau, docs); + } else { + console.warn("syntax_cache: slua_default.d.luau or slua_default.docs.json missing or invalid, skipping Luau-LSP configuration"); + } + } else { + console.warn("syntax_cache: Luau-LSP files not in viewer cache, skipping Luau-LSP configuration"); + } + + this.languageVersion = syntaxId; + await ConfigService.getInstance().setConfig(ConfigKey.LastSyntaxID, syntaxId, { target: "global" }); + return true; + } + public async requestSyntaxId(socket: JSONRPCInterface): Promise { return await this.repository.requestLanguageSyntaxId(socket); } + + public async requestSyntaxCacheList(socket: JSONRPCInterface): Promise { + return await this.repository.requestSyntaxCacheList(socket); + } + + public async requestSyntaxCacheFile( + socket: JSONRPCInterface, + filename: string, + asJson?: boolean, + ): Promise { + return await this.repository.requestSyntaxCacheFile(socket, filename, asJson); + } //#endregion //#region Language definition massaging diff --git a/src/synchservice.ts b/src/synchservice.ts index a562a2c..ef1f9f0 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -54,6 +54,7 @@ export class SynchService implements vscode.Disposable { public viewerVersion?: string; public viewerLanguages?: string[]; public viewerFeatures?: { [feature: string]: boolean }; + public syntaxCacheSupported: boolean = false; public syntaxId?: string; public agentId?: string; public agentName?: string; @@ -295,7 +296,7 @@ export class SynchService implements vscode.Disposable { onHandshake: (message: SessionHandshake): any => this.onHandshake(message), onHandshakeOk: (): any => this.onHandshakeOk(), onDisconnect: (message: SessionDisconnect): any => this.onDisconnect(message), - onScriptUnsubscribe: (message: ScriptUnsubscribe): any => + onUnsubscribe: (message: ScriptUnsubscribe): any => this.onScriptUnsubscribe(message), onSyntaxChange: (message: SyntaxChange): any => this.onSyntaxChange(message), onCompilationResult: (message: CompilationResult): any => this.onCompilationResult(message), @@ -348,6 +349,7 @@ export class SynchService implements vscode.Disposable { this.viewerLanguages = message.languages; this.syntaxId = message.syntax_id; this.viewerFeatures = message.features; + this.syntaxCacheSupported = message.features?.["syntax_cache"] === true; let challengeResponse: string | undefined = undefined; if (message.challenge) { @@ -359,11 +361,17 @@ export class SynchService implements vscode.Disposable { }); } + const firstSync = this.activeSync ?? [...this.activeSyncs.values()][0]; + const scriptName = firstSync ? path.basename(firstSync.getMasterFilePath()) : undefined; + const scriptLanguage = firstSync ? firstSync.getLanguage() : undefined; + const response: SessionHandshakeResponse = { client_name: ConfigService.getInstance().getConfig(ConfigKey.ClientName) || "sl-vscode-plugin", client_version: "1.0", protocol_version: "1.0", ...maybe("challenge_response", challengeResponse), + ...maybe("script_name", scriptName), + ...maybe("script_language", scriptLanguage), languages: ["lsl", "luau"], features: { live_sync: true, @@ -375,7 +383,7 @@ export class SynchService implements vscode.Disposable { return response; } - private onHandshakeOk(): void { + private async onHandshakeOk(): Promise { // Session established successfully console.log( `Session established with viewer ${this.viewerName} v${this.viewerVersion}`, @@ -385,10 +393,16 @@ export class SynchService implements vscode.Disposable { ); const service = LanguageService.getInstance(); + await this.refreshSyntaxCacheListIfSupported(service); if (!this.checkLanguageVersion()) { const socket = this.getWebSocket(); if (socket && this.syntaxId) { - const promise = service.changeSyntaxVersion(this.syntaxId, socket); + const promise = service.changeSyntaxVersion( + this.syntaxId, + socket, + false, + this.syntaxCacheSupported, + ); showStatusMessage("Updating to latest language definitions...", promise); } } @@ -424,20 +438,39 @@ export class SynchService implements vscode.Disposable { } } - private onSyntaxChange(params: SyntaxChange): void { + private async onSyntaxChange(params: SyntaxChange): Promise { if (this.syntaxId !== params.id) { this.syntaxId = params.id; + const service = LanguageService.getInstance(); + await this.refreshSyntaxCacheListIfSupported(service); if (!this.checkLanguageVersion()) { - const service = LanguageService.getInstance(); const socket = this.getWebSocket(); if (socket) { - const promise = service.changeSyntaxVersion(params.id, socket); + const promise = service.changeSyntaxVersion( + params.id, + socket, + false, + this.syntaxCacheSupported, + ); showStatusMessage("Updating to latest language definitions...", promise); } } } } + private async refreshSyntaxCacheListIfSupported(service: LanguageService): Promise { + if (!this.syntaxCacheSupported) { + return; + } + + const socket = this.getWebSocket(); + if (!socket) { + return; + } + + await service.requestSyntaxCacheList(socket); + } + private onCompilationResult(message: CompilationResult): void { const scriptId = message.script_id; const sync = this.findSyncByScriptId(scriptId); diff --git a/src/viewereditwsclient.ts b/src/viewereditwsclient.ts index 98b2d2a..90722b2 100644 --- a/src/viewereditwsclient.ts +++ b/src/viewereditwsclient.ts @@ -30,6 +30,8 @@ export interface SessionHandshakeResponse { languages: string[]; features: { [feature: string]: boolean }; challenge_response?: string; + script_name?: string; + script_language?: string; } export interface SessionDisconnect { @@ -47,6 +49,8 @@ export interface ScriptSubscribeResponse { script_id: string; success: boolean; status: number; + object_id?: string; + item_id?: string; message?: string; } @@ -58,11 +62,28 @@ export interface SyntaxChange { id: string; } +export interface SyntaxCacheList { + files: string[]; + success: boolean; +} + +export interface SyntaxCacheGetRequest { + filename: string; + as_json?: boolean; +} + +export interface SyntaxCacheFile { + content?: string | object; + success: boolean; + error?: string; +} + export interface CompilationError { row: number; column: number; level: string; message: string; + format?: "lsl"; } export interface CompilationResult { diff --git a/src/websockclient.ts b/src/websockclient.ts index b160f0e..c2974e8 100644 --- a/src/websockclient.ts +++ b/src/websockclient.ts @@ -542,7 +542,15 @@ export class JSONRPCClient extends WebsockClient implements JSONRPCInterface { const handler = this.methodHandlers.get(notification.method); if (handler) { try { - handler(notification.params); + const result = handler(notification.params); + if (result && typeof (result as PromiseLike).then === "function") { + Promise.resolve(result).catch((error) => { + console.error( + `Error in async notification handler for ${notification.method}:`, + error, + ); + }); + } } catch (error) { console.error( `Error in notification handler for ${notification.method}:`,