From ade500c9f5b4b54e0ea7d628ab4b2c3114023638 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Tue, 14 Apr 2026 13:11:59 +0100 Subject: [PATCH 01/19] Basic notecard support --- .gitignore | 1 + package.json | 5 + src/configservice.ts | 2 + src/interfaces/configinterface.ts | 1 + src/scriptsync.ts | 35 +++--- src/shared/languageservice.ts | 2 +- src/shared/lexer.ts | 197 ++++++++++++++++-------------- src/synchservice.ts | 64 +++++++--- 8 files changed, 187 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index 5dc9bec..3945be4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.vsix *.code-workspace .vscode/settings.json +.vscode/lsl-lsp-server.log diff --git a/package.json b/package.json index 88a3cb1..08c1066 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,11 @@ "type": "boolean", "default": false, "description": "Whether the current sl user info should be included in the meta info" + }, + "slVscodeEdit.sync.notecardComment": { + "type": "string", + "default" : null, + "description": "String to use as a comment for notecards to allow syncing to external files of notecards with a file comment" } } }, diff --git a/src/configservice.ts b/src/configservice.ts index 7b628be..842f213 100644 --- a/src/configservice.ts +++ b/src/configservice.ts @@ -11,6 +11,8 @@ import { normalizePath, NormalizedPath } from "./interfaces/hostinterface"; export const STATUS_BAR_TIMEOUT_SECONDS = 3; export const SCRIPT_FILE_PATTERN = /^sl_script_(.+)_([a-fA-F0-9]{32}|[a-fA-F0-9-]{36})\.(luau|lsl)$/; +export const NOTECARD_FILE_PATTERN = + /^sl_notecard_(.+)_([a-fA-F0-9]{32}|[a-fA-F0-9-]{36})\.txt$/; export const configPrefix = "slVscodeEdit"; diff --git a/src/interfaces/configinterface.ts b/src/interfaces/configinterface.ts index c4c032a..692f402 100644 --- a/src/interfaces/configinterface.ts +++ b/src/interfaces/configinterface.ts @@ -33,6 +33,7 @@ export enum ConfigKey { LastSyntaxID = 'syntax.lastID', AskIfViewerScriptMismatchesMaster = 'sync.askIfViewerScriptMismatchesMaster', CompareHashBeforeSync = 'sync.compareHashBeforeSync', + NotecardSyncComment = 'sync.notecardComment', FileMetaInfoInOutput ='sync.includeFileMetaInOutput', FileMetaInfoIncludeCreator ='sync.includeCreatorInFileMeta', diff --git a/src/scriptsync.ts b/src/scriptsync.ts index c03b79f..2a6ff28 100644 --- a/src/scriptsync.ts +++ b/src/scriptsync.ts @@ -29,7 +29,7 @@ import { HostInterface, normalizePath } from "./interfaces/hostinterface"; import { SynchService } from "./synchservice"; import { IncludeInfo } from "./shared/parser"; import { sha256 } from "js-sha256"; -import { getLanguageConfig, LanguageLexerConfig } from "./shared/lexer"; +import { getLanguageConfig, isProccessedLanguage, LanguageLexerConfig } from "./shared/lexer"; //==================================================================== interface TrackedDocument { @@ -74,8 +74,8 @@ export class ScriptSync implements vscode.Disposable { this.host = host ?? new VSCodeHost(); // Initialize preprocessor with macro processor - const enabled = config.getConfig(ConfigKey.PreprocessorEnable) ?? true; - if (enabled) { + const enabled = config.getConfig(ConfigKey.PreprocessorEnable, true); + if (enabled && isProccessedLanguage(this.language)) { this.preprocessor = new LexingPreprocessor(this.host, config, this.macros); } @@ -441,7 +441,7 @@ export class ScriptSync implements vscode.Disposable { } private getLanguageConfig(): LanguageLexerConfig { - const config = getLanguageConfig(this.language); + const config = getLanguageConfig(this.language, this.config); if(config.name === "lsl" && this.config.getConfig(ConfigKey.PreprocessorLSLSwitchStatements, false)) { config.directiveKeywords.push("switch"); } @@ -504,14 +504,19 @@ export class ScriptSync implements vscode.Disposable { const path = vscode.workspace.asRelativePath(this.masterDocument.uri.fsPath); const comment = this.getLanguageConfig().lineCommentPrefix; + + if(comment.length < 1) return content; + meta.push(`${comment} ================ sl-vscode-plugin meta ================`); meta.push(`${comment} @file ${path}`); meta.push(`${comment} @hash ${hash}`); meta.push(`${comment} @date ${date[0]} ${date[1].split(".")[0]}`); // console.error("PREFIX CREATOR", this.config.getConfig(ConfigKey.FileMetaInfoIncludeCreator,false)); if(this.config.getConfig(ConfigKey.FileMetaInfoIncludeCreator, false)) { - meta.push(`${comment} @creator ${ScriptSync.getCurrentAgentName()}`); - meta.push(`${comment} @creatorID ${ScriptSync.getCurrentAgentId()}`); + const agentName = ScriptSync.getCurrentAgentName(); + if(agentName) meta.push(`${comment} @creator ${agentName}`); + const agentID = ScriptSync.getCurrentAgentId(); + if(agentID) meta.push(`${comment} @creatorID ${agentID}`); } meta.push(`${comment} =======================================================`); meta.push(content) @@ -526,12 +531,12 @@ export class ScriptSync implements vscode.Disposable { return this.fileMappings.filter(mapping => mapping.hash !== hash); } - private static getCurrentAgentId(): string { - return SynchService.getInstance().agentId || "unknown-agent-id"; + private static getCurrentAgentId(): string | null { + return SynchService.getInstance().agentId ?? null; } - private static getCurrentAgentName(): string { - return SynchService.getInstance().agentName || "unknown-agent-name"; + private static getCurrentAgentName(): string | null { + return SynchService.getInstance().agentName ?? null; } private initializeSystemMacros(language: ScriptLanguage): void { @@ -545,16 +550,16 @@ export class ScriptSync implements vscode.Disposable { this.macros.clear(); if (language === "lsl") { - this.macros.defineSystemMacro("__AGENTKEY__", (_context) => `"${ScriptSync.getCurrentAgentId()}"`); - this.macros.defineSystemMacro("__AGENTIDRAW__", (_context) => ScriptSync.getCurrentAgentId()); + this.macros.defineSystemMacro("__AGENTKEY__", (_context) => `"${ScriptSync.getCurrentAgentId() ?? "unkown-agent-id"}"`); + this.macros.defineSystemMacro("__AGENTIDRAW__", (_context) => ScriptSync.getCurrentAgentId() ?? "unkown-agent-id"); } else if(language === "luau") { - this.macros.defineSystemMacro("__AGENTKEY__", (_context) => `uuid("${ScriptSync.getCurrentAgentId()}")`); + this.macros.defineSystemMacro("__AGENTKEY__", (_context) => `uuid("${ScriptSync.getCurrentAgentId() ?? "unkown-agent-id"}")`); } this.macros.defineSystemMacro("__LINE__", (context) => context.line.toString()); this.macros.defineSystemMacro("__FILE__", (context) => `"${path.normalize(context.sourceFile)}"`); this.macros.defineSystemMacro("__SHORTFILE__", (context) => `"${path.basename(path.normalize(context.sourceFile))}"`); - this.macros.defineSystemMacro("__AGENTID__", (_context) => `"${ScriptSync.getCurrentAgentId()}"`); - this.macros.defineSystemMacro("__AGENTNAME__", (_context) => `"${ScriptSync.getCurrentAgentName()}"`); + this.macros.defineSystemMacro("__AGENTID__", (_context) => `"${ScriptSync.getCurrentAgentId() ?? "unknown-agent-id"}"`); + this.macros.defineSystemMacro("__AGENTNAME__", (_context) => `"${ScriptSync.getCurrentAgentName() ?? "unknown-agent-name"}"`); //this.macros.defineSystemMacro("__ASSETID__", (_context) => `"${getCurrentAssetId()}"`); this.macros.defineSystemMacro("__DATE__", (_context) => { let date = new Date(); diff --git a/src/shared/languageservice.ts b/src/shared/languageservice.ts index 3088fda..37e0548 100644 --- a/src/shared/languageservice.ts +++ b/src/shared/languageservice.ts @@ -18,7 +18,7 @@ import { SelenePlugin, LuaLSPPlugin } from "../pluginsupport"; import { ConfigService } from "../configservice"; // TODO: migrate to ConfigInterface injection -export type ScriptLanguage = "lsl" | "luau"; +export type ScriptLanguage = "lsl" | "luau" | "txt"; //----------------------------------------- diff --git a/src/shared/lexer.ts b/src/shared/lexer.ts index 2da9b9e..5db706e 100644 --- a/src/shared/lexer.ts +++ b/src/shared/lexer.ts @@ -9,6 +9,7 @@ import { ScriptLanguage } from "./languageservice"; import { NormalizedPath } from "../interfaces/hostinterface"; import { DiagnosticCollector, ErrorCodes } from "./diagnostics"; +import { ConfigKey, FullConfigInterface } from "../interfaces/configinterface"; //#region Language Configuration @@ -60,98 +61,114 @@ export interface LanguageLexerConfig extends BaseLanguageLexerConfig { /** * Predefined language configurations - */ -const LANGUAGE_CONFIGS: Record = { - lsl: { - name: "lsl", - lineCommentPrefix: "//", - blockCommentDelimiters: [ - ["/*","*/"] - ], - logicalOperators: { - and: "&&", - or: "||", - not: "!" - }, - useLongBracketSyntax: false, - supportsVectorLiterals: true, - directivePrefix: "#", - directiveKeywords: ["defined"], - operators: [ - // Multi-character operators - "==", "!=", "<=", ">=", "&&", "||", "<<", ">>", - "+=", "-=", "*=", "/=", "%=", "++", "--", - // Single-character operators - "+", "-", "*", "/", "%", "=", "!", "<", ">", "&", "|", "^", "~", - // Punctuation (brackets handled separately as distinct token types) - "?", ":", ";", ",", ".", - ], - brackets: [ - ["{", "}"], // Braces for code blocks - ["(", ")"], // Parentheses for expressions and function calls - ["[", "]"], // Brackets for lists - ], - stringDelimiters: ['"', "'"], // Double and single quotes - }, - luau: { - name: "luau", - lineCommentPrefix: "--", - blockCommentDelimiters: [ - { - startingChar: "-", - endingChar: "]", - startingMatch: /^--\[[=]*\[$/, // Regex for `--[=[` with 0-n `=` characters - endingMatch: /^\][=]*\]$/, // Regex for `]=]` with 0-n `=` characters - lengthDifference: -2, // ending sequence is 2 chars shorter - } - ], - logicalOperators: { - and: "and", - or: "or", - not: "not" - }, - useLongBracketSyntax: true, - directivePrefix: null, - directiveKeywords: ["require"], - operators: [ - // Arithmetic operators - "+", "-", "*", "/", "%", "^", - // Relational operators - "==", "~=", "<=", ">=", "<", ">", - // Logical operators - "and", "or", "not", - // Other & punctuation (brackets handled separately as distinct token types) - "..", "#", "?", ":", ";", ",", ".", - ], - brackets: [ - ["{", "}"], // Braces for code blocks (do...end in Lua, but braces for tables) - ["(", ")"], // Parentheses for expressions and function calls - ["[", "]"], // Brackets for table indexing - ], - stringDelimiters: ['"', "'"], // Double quotes, single quotes - multiLineStringDelimiters: [ - { - startingChar: "[", - endingChar: "]", - startingMatch: /^\[[=]*\[$/, // Regex for `[=[` with 0-n `=` characters - endingMatch: /^\][=]*\]$/, // Regex for `]=]` with 0-n `=` characters - // allowsNewLines: true, - } - ], // Double quotes, single quotes, and backticks - stringInterpDelimiter: { - char: "`", - open: "{", - close: "}", - escape: "\\", - } - }, -}; - -/** * Get language configuration for a script language */ -export function getLanguageConfig(language: ScriptLanguage): LanguageLexerConfig { - return structuredClone(LANGUAGE_CONFIGS[language]); +export function isProccessedLanguage(language: ScriptLanguage): boolean { + return language == "lsl" || language == "luau"; +} +export function getLanguageConfig(language: ScriptLanguage, config: FullConfigInterface|null = null): LanguageLexerConfig { + switch (language) { + case "lsl": + return { + name: "lsl", + lineCommentPrefix: "//", + blockCommentDelimiters: [ + ["/*", "*/"] + ], + logicalOperators: { + and: "&&", + or: "||", + not: "!" + }, + useLongBracketSyntax: false, + supportsVectorLiterals: true, + directivePrefix: "#", + directiveKeywords: ["defined"], + operators: [ + // Multi-character operators + "==", "!=", "<=", ">=", "&&", "||", "<<", ">>", + "+=", "-=", "*=", "/=", "%=", "++", "--", + // Single-character operators + "+", "-", "*", "/", "%", "=", "!", "<", ">", "&", "|", "^", "~", + // Punctuation (brackets handled separately as distinct token types) + "?", ":", ";", ",", ".", + ], + brackets: [ + ["{", "}"], // Braces for code blocks + ["(", ")"], // Parentheses for expressions and function calls + ["[", "]"], // Brackets for lists + ], + stringDelimiters: ['"', "'"], // Double and single quotes + }; + case "luau": + return { + name: "luau", + lineCommentPrefix: "--", + blockCommentDelimiters: [ + { + startingChar: "-", + endingChar: "]", + startingMatch: /^--\[[=]*\[$/, // Regex for `--[=[` with 0-n `=` characters + endingMatch: /^\][=]*\]$/, // Regex for `]=]` with 0-n `=` characters + lengthDifference: -2, // ending sequence is 2 chars shorter + } + ], + logicalOperators: { + and: "and", + or: "or", + not: "not" + }, + useLongBracketSyntax: true, + directivePrefix: null, + directiveKeywords: ["require"], + operators: [ + // Arithmetic operators + "+", "-", "*", "/", "%", "^", + // Relational operators + "==", "~=", "<=", ">=", "<", ">", + // Logical operators + "and", "or", "not", + // Other & punctuation (brackets handled separately as distinct token types) + "..", "#", "?", ":", ";", ",", ".", + ], + brackets: [ + ["{", "}"], // Braces for code blocks (do...end in Lua, but braces for tables) + ["(", ")"], // Parentheses for expressions and function calls + ["[", "]"], // Brackets for table indexing + ], + stringDelimiters: ['"', "'"], // Double quotes, single quotes + multiLineStringDelimiters: [ + { + startingChar: "[", + endingChar: "]", + startingMatch: /^\[[=]*\[$/, // Regex for `[=[` with 0-n `=` characters + endingMatch: /^\][=]*\]$/, // Regex for `]=]` with 0-n `=` characters + // allowsNewLines: true, + } + ], // Double quotes, single quotes, and backticks + stringInterpDelimiter: { + char: "`", + open: "{", + close: "}", + escape: "\\", + } + }; + case "txt": { + const prefix = config ? config.getConfig(ConfigKey.NotecardSyncComment, null) : null; + return { + name: "txt", + lineCommentPrefix: prefix ?? "", + blockCommentDelimiters: [], + logicalOperators: { + and: "", + or: "", + not: "" + }, + directivePrefix: prefix, + directiveKeywords: [] + }; + } + } } //#endregion diff --git a/src/synchservice.ts b/src/synchservice.ts index a562a2c..787f7e9 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode"; import * as path from "path"; import * as fs from "fs"; -import { SCRIPT_FILE_PATTERN, ConfigService } from "./configservice"; +import { SCRIPT_FILE_PATTERN, ConfigService, NOTECARD_FILE_PATTERN } from "./configservice"; import { ConfigKey } from "./interfaces/configinterface"; import { ViewerEditWSClient, @@ -577,6 +577,12 @@ export class SynchService implements vscode.Disposable { // Break up the temp file name into its components private static parseTempFile( viewerFilePath: string, + ): ParsedTempFile | null { + return SynchService.parseTempScriptFile(viewerFilePath) ?? SynchService.parseTempNotecardFile(viewerFilePath); + } + + private static parseTempScriptFile( + viewerFilePath: string, ): ParsedTempFile | null { const openedBase = path.basename(viewerFilePath); const match = openedBase.match(SCRIPT_FILE_PATTERN); @@ -591,6 +597,22 @@ export class SynchService implements vscode.Disposable { : null; } + private static parseTempNotecardFile( + viewerFilePath: string, + ): ParsedTempFile | null { + const openedBase = path.basename(viewerFilePath); + const match = openedBase.match(NOTECARD_FILE_PATTERN); + + return match + ? { + scriptName: match[1], + scriptId: match[2], + extension: "txt", + language: "txt", + } + : null; + } + public findSyncByScriptId(scriptId: string): ScriptSync | undefined { return [...this.activeSyncs.values()].find((sync) => sync.isTrackingId(scriptId), @@ -627,19 +649,8 @@ export class SynchService implements vscode.Disposable { viewerFile: vscode.TextDocument ): Promise { // Attempt to match by file meta info - if (ConfigService.getInstance().getConfig(ConfigKey.FileMetaInfoInOutput, false)) { - const cmt = getLanguageConfig(script.language).lineCommentPrefix; - const lineRegExp = new RegExp(`^[\\s]*${cmt}[\\s]*@file[\\s]*[A-z0-9-_/.]*[\\s]*$`, "i"); - const range = new vscode.Range(0, 0, 10, 0); - const start = viewerFile.getText(range).split("\n").filter(line => line.match(lineRegExp))[0] ?? null; - if (start) { - const files = await vscode.workspace.findFiles(start.split("@file")[1].trim()); - if (files.length == 1) { - console.warn("Match on meta info"); - return files[0]; - } - } - } + const metaMatch = SynchService.findMasterFileByMetaComment(script, viewerFile); + if(metaMatch) return metaMatch; let files = await vscode.workspace.findFiles(`**/${script.scriptName}.${script.extension}`); if (files.length > 0) { @@ -671,6 +682,31 @@ export class SynchService implements vscode.Disposable { } } + private static async findMasterFileByMetaComment( + script: ParsedTempFile, + viewerFile: vscode.TextDocument + ) : Promise { + const config = ConfigService.getInstance() + + if (!config.getConfig(ConfigKey.FileMetaInfoInOutput, false)) return null; + + const cmt = getLanguageConfig(script.language,config).lineCommentPrefix; + + if(cmt.length < 1) return null; + + const lineRegExp = new RegExp(`^[\\s]*${cmt}[\\s]*@file[\\s]*[A-z0-9-_/.]*[\\s]*$`, "i"); + const range = new vscode.Range(0, 0, 10, 0); + const start = viewerFile.getText(range).split("\n").filter(line => line.match(lineRegExp))[0] ?? null; + if (start) { + const files = await vscode.workspace.findFiles(start.split("@file")[1].trim()); + if (files.length == 1) { + console.log("Match on meta info", start); + return files[0]; + } + } + return null; + } + private static async openMasterScript( masterUri: vscode.Uri, ): Promise { From d0175773879e72adb412accd00195a281ea8d941 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Wed, 15 Apr 2026 01:30:24 +0100 Subject: [PATCH 02/19] Fix file meta --- src/synchservice.ts | 65 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 787f7e9..3bb2926 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -682,6 +682,58 @@ export class SynchService implements vscode.Disposable { } } + // Resolve file by checking actual paths, not searching with globs as, filenames may contain glob special characters + private static async resolveUriFromMetaFilePath( + pathPart: string, + ): Promise { + const trimmed = pathPart.trim(); + if (!trimmed) { + return null; + } + + const isWindowsDrive = /^[a-zA-Z]:[\\/]/.test(trimmed); + const isAbs = path.isAbsolute(trimmed) || isWindowsDrive; + + const tryUriInWorkspace = async (uri: vscode.Uri): Promise => { + if (!vscode.workspace.getWorkspaceFolder(uri)) { + return null; + } + try { + const st = await vscode.workspace.fs.stat(uri); + if (st.type === vscode.FileType.File) { + return uri; + } + } catch { + // Miss + } + return null; + }; + + if (isAbs) { + const uri = vscode.Uri.file(path.normalize(trimmed)); + return tryUriInWorkspace(uri); + } + + const folders = vscode.workspace.workspaceFolders; + if (!folders?.length) { + return null; + } + // Split path windows or unix style + const segments = trimmed + .replace(/\\/g, "/") + .split("/") + .filter((s) => s.length > 0); + for (const folder of folders) { + const joined = path.join(folder.uri.fsPath, ...segments); + const candidate = vscode.Uri.file(path.normalize(joined)); + const hit = await tryUriInWorkspace(candidate); + if (hit) { + return hit; + } + } + return null; + } + private static async findMasterFileByMetaComment( script: ParsedTempFile, viewerFile: vscode.TextDocument @@ -694,14 +746,15 @@ export class SynchService implements vscode.Disposable { if(cmt.length < 1) return null; - const lineRegExp = new RegExp(`^[\\s]*${cmt}[\\s]*@file[\\s]*[A-z0-9-_/.]*[\\s]*$`, "i"); + const lineRegExp = new RegExp(`^[\\s]*${cmt}[\\s]*@file[\\s]+.*$`, "i"); const range = new vscode.Range(0, 0, 10, 0); - const start = viewerFile.getText(range).split("\n").filter(line => line.match(lineRegExp))[0] ?? null; + const lines = viewerFile.getText(range).split("\n"); + const start = lines.filter(line => line.match(lineRegExp))[0] ?? null; if (start) { - const files = await vscode.workspace.findFiles(start.split("@file")[1].trim()); - if (files.length == 1) { - console.log("Match on meta info", start); - return files[0]; + const pathPart = start.split("@file")[1]?.trim() ?? ""; + const resolved = await SynchService.resolveUriFromMetaFilePath(pathPart); + if (resolved) { + return resolved; } } return null; From d30cda513518055f651ca2b6b784a7077e610f51 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Wed, 15 Apr 2026 01:54:58 +0100 Subject: [PATCH 03/19] Add startup check --- src/synchservice.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/synchservice.ts b/src/synchservice.ts index 3bb2926..7a17108 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -136,6 +136,12 @@ export class SynchService implements vscode.Disposable { this.disposables.push(onDidSaveListener); this.disposables.push(onDidChangeWindowState); this.disposables.push(onDidChangeActiveTextEditor); + + const launchDoc = vscode.window.activeTextEditor?.document + + if(launchDoc) { + this.onOpenTextDocument(launchDoc); + } } private async initializeSyntax(): Promise { From e09dc1b0886eca4a4ffc61527bab9fa65ac37838 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Wed, 15 Apr 2026 02:19:03 +0100 Subject: [PATCH 04/19] Remove file meta output check --- src/synchservice.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 7a17108..1d53aa5 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -746,8 +746,6 @@ export class SynchService implements vscode.Disposable { ) : Promise { const config = ConfigService.getInstance() - if (!config.getConfig(ConfigKey.FileMetaInfoInOutput, false)) return null; - const cmt = getLanguageConfig(script.language,config).lineCommentPrefix; if(cmt.length < 1) return null; From fb08c33ada3395573a40345fcdaa4fbc432d62d2 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sat, 18 Apr 2026 03:27:11 +0100 Subject: [PATCH 05/19] First pass of new sync setup --- package.json | 39 +++++++++++ src/extension.ts | 68 +++++++++++------- src/interfaces/configinterface.ts | 1 + src/scriptsync.ts | 27 +++++--- src/synchservice.ts | 111 +++++++++++++++++++----------- src/vscode/SyncedFileDecorator.ts | 35 ++++++++++ 6 files changed, 208 insertions(+), 73 deletions(-) create mode 100644 src/vscode/SyncedFileDecorator.ts diff --git a/package.json b/package.json index 88a3cb1..96e5904 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,40 @@ "command": "second-life-scripting.forceLanguageUpdate", "title": "Force Language Update", "category": "Second Life" + }, + { + "command": "second-life-scripting.stopFileSync", + "shortTitle": "Stop File Sync", + "title": "Stop file sync with SL Viewer", + "category": "Second Life" + } + ], + "menus": { + "explorer/context": [ + { + "command": "second-life-scripting.stopFileSync", + "group": "navigation@99", + "when": "resourceScheme == file && slVscodeEdit:syncsActive" + } + ], + "editor/title/context": [ + { + "command": "second-life-scripting.stopFileSync", + "group": "1_close@99", + "when": "resourceScheme == file && slVscodeEdit:syncsActive" + } + ] + }, + "colors": [ + { + "id": "secondlife.syncedfile", + "description": "Color for files that are being synced with the viewer", + "defaults": { + "dark": "tab.activeBorderTop", + "light": "tab.activeBorderTop", + "highContrast": "tab.activeBorderTop", + "highContrastLight": "tab.activeBorderTop" + } } ], "configuration": [ @@ -105,6 +139,11 @@ "type": "boolean", "default": false, "description": "Whether the current sl user info should be included in the meta info" + }, + "slVscodeEdit.sync.keepViewerFileOpen": { + "type": "boolean", + "default": true, + "description": "Should the viewers tempfile be kept open while editing" } } }, diff --git a/src/extension.ts b/src/extension.ts index b469d8a..925d701 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { showErrorMessage } from "./utils"; import { ConfigKey } from "./interfaces/configinterface"; +import path from "path"; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -34,7 +35,35 @@ export function activate(context: vscode.ExtensionContext): void { showErrorMessage("Second Life Scripting Extension: No workspace is opened.\nPlease open a folder in VSCode to enable full functionality."); } + setupCommands(context); + configService.on(ConfigKey.Enabled, (configService) => { + if(configService.isEnabled()) { + synchService.activate(); + logInfo("Second Life Scripting Extension activated"); + } else { + synchService.deactivate(); + logInfo("Second Life Scripting Extension deactivated"); + } + }); + + if(configService.isEnabled()) { + synchService.activate(); + logInfo("Second Life Scripting Extension activated"); + } + + context.subscriptions.push(configService); + context.subscriptions.push(languageService); + context.subscriptions.push(synchService); +} + +// This method is called when your extension is deactivated +export function deactivate(): void { + const synchService = SynchService.getInstance(); + synchService.deactivate(); +} + +function setupCommands(context: vscode.ExtensionContext) { // Register commands context.subscriptions.push( vscode.commands.registerCommand( @@ -89,28 +118,19 @@ export function activate(context: vscode.ExtensionContext): void { ) ); - configService.on(ConfigKey.Enabled, (configService) => { - if(configService.isEnabled()) { - synchService.activate(); - logInfo("Second Life Scripting Extension activated"); - } else { - synchService.deactivate(); - logInfo("Second Life Scripting Extension deactivated"); - } - }); - - if(configService.isEnabled()) { - synchService.activate(); - logInfo("Second Life Scripting Extension activated"); - } - - context.subscriptions.push(configService); - context.subscriptions.push(languageService); - context.subscriptions.push(synchService); -} - -// This method is called when your extension is deactivated -export function deactivate(): void { - const synchService = SynchService.getInstance(); - synchService.deactivate(); + context.subscriptions.push( + vscode.commands.registerCommand( + "second-life-scripting.stopFileSync", + (uri?: vscode.Uri) => { + console.error("CLOSE SYNC COMMAND", uri); + if(!uri) { + uri = vscode.window.activeTextEditor?.document.uri; + } + if(!uri) { + return; + } + SynchService.getInstance().removeSync(path.normalize(uri.fsPath), false); + } + ) + ); } diff --git a/src/interfaces/configinterface.ts b/src/interfaces/configinterface.ts index c4c032a..b3ff02d 100644 --- a/src/interfaces/configinterface.ts +++ b/src/interfaces/configinterface.ts @@ -33,6 +33,7 @@ export enum ConfigKey { LastSyntaxID = 'syntax.lastID', AskIfViewerScriptMismatchesMaster = 'sync.askIfViewerScriptMismatchesMaster', CompareHashBeforeSync = 'sync.compareHashBeforeSync', + KeepViewerFileOpen = 'sync.keepViewerFileOpen', FileMetaInfoInOutput ='sync.includeFileMetaInOutput', FileMetaInfoIncludeCreator ='sync.includeCreatorInFileMeta', diff --git a/src/scriptsync.ts b/src/scriptsync.ts index c03b79f..4bc3459 100644 --- a/src/scriptsync.ts +++ b/src/scriptsync.ts @@ -51,18 +51,19 @@ export class ScriptSync implements vscode.Disposable { private diagnosticSources: Set = new Set(); private lineMappings?: LineMapping[]; private config: ConfigService; - private host: HostInterface; + // private host: HostInterface; private includedFiles : IncludeInfo[] = []; + private syncService: SynchService; //==================================================================== public constructor( masterDocument: vscode.TextDocument, language: ScriptLanguage, config: ConfigService, - scriptId?: string, - viewerDocument?: vscode.TextDocument, - host?: HostInterface, + scriptId: string, + viewerDocument: vscode.TextDocument, + syncService: SynchService, ) { this.config = config; @@ -71,12 +72,12 @@ export class ScriptSync implements vscode.Disposable { this.macros = new MacroProcessor(); this.initializeSystemMacros(language); - this.host = host ?? new VSCodeHost(); + this.syncService = syncService; // Initialize preprocessor with macro processor const enabled = config.getConfig(ConfigKey.PreprocessorEnable) ?? true; if (enabled) { - this.preprocessor = new LexingPreprocessor(this.host, config, this.macros); + this.preprocessor = new LexingPreprocessor(this.syncService.getHost(), config, this.macros); } this.masterDocument = masterDocument; @@ -146,6 +147,10 @@ export class ScriptSync implements vscode.Disposable { this.fileMappings = this.fileMappings.filter((m) => m !== mapping); if (close) { closeTextDocument(mapping.viewerDocument); + mapping.watcher?.dispose(); + } + if(!this.hasFilesToTrack()) { + this.syncService.clearEmptySyncs(); } } return this.fileMappings.length; @@ -157,10 +162,7 @@ export class ScriptSync implements vscode.Disposable { (m) => path.normalize(m.viewerDocument.fileName) === viewerFile, ); if (mapping) { - this.fileMappings = this.fileMappings.filter((m) => m !== mapping); - if (close) { - closeTextDocument(mapping.viewerDocument); - } + this.unsubscribeById(mapping.id, close); } return this.fileMappings.length; } @@ -178,6 +180,10 @@ export class ScriptSync implements vscode.Disposable { ); } + public hasFilesToTrack() : boolean { + return this.fileMappings.length > 0; + } + public getMasterDocument(): vscode.TextDocument { return this.masterDocument; } @@ -580,6 +586,7 @@ export class ScriptSync implements vscode.Disposable { try { this.diagnosticCollection.dispose(); + this.fileMappings.forEach(map => map.watcher?.dispose()); } catch (error) { // Log but don't throw during disposal console.warn("Error during ScriptSync disposal:", error); diff --git a/src/synchservice.ts b/src/synchservice.ts index a562a2c..0a3b3a2 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -28,12 +28,14 @@ import { closeEditor, logInfo, VSCodeHost, + closeTextDocument, } from "./utils"; import { maybe } from "./shared/sharedutils"; // TODO: migrate needed utilities from sharedutils if required import { ScriptLanguage, LanguageService } from "./shared/languageservice"; import { ScriptSync } from "./scriptsync"; import { getLanguageConfig } from "./shared/lexer"; import { HostInterface } from "./interfaces/hostinterface"; +import { SyncedFileDecorator } from "./vscode/SyncedFileDecorator"; type ParsedTempFile = { scriptName: string; scriptId: string; extension: string, language: ScriptLanguage }; @@ -58,11 +60,14 @@ export class SynchService implements vscode.Disposable { public agentId?: string; public agentName?: string; + private syncedFileDecorator : SyncedFileDecorator; + private disposables: vscode.Disposable[] = []; private constructor(context: vscode.ExtensionContext) { this.context = context; this.host = new VSCodeHost(); + this.syncedFileDecorator = new SyncedFileDecorator(this); } public static getInstance(context?: vscode.ExtensionContext): SynchService { @@ -93,20 +98,29 @@ export class SynchService implements vscode.Disposable { this.disposables = []; } + public getHost() : HostInterface + { + return this.host; + } + public initialize(): void { const onDidOpenListener = vscode.workspace.onDidOpenTextDocument( async (document) => this.onOpenTextDocument(document), ); - const onDidCloseListener = vscode.workspace.onDidCloseTextDocument( - (document: vscode.TextDocument) => this.onCloseTextDocument(document), - ); + // const onDidCloseListener = vscode.workspace.onDidCloseTextDocument( + // (document: vscode.TextDocument) => this.onCloseTextDocument(document), + // ); const onDidDeleteListener = vscode.workspace.onDidDeleteFiles( (event: vscode.FileDeleteEvent) => this.onDeleteFiles(event), ); + const onDidCloseWorkspace = vscode.workspace.onDidChangeWorkspaceFolders((e) => { + e.removed.forEach(folder => this.onCloseWorkspace(folder)); + }); + const onDidSaveListener = vscode.workspace.onDidSaveTextDocument( (document: vscode.TextDocument) => this.onSaveTextDocument(document), ); @@ -131,11 +145,13 @@ export class SynchService implements vscode.Disposable { // showStatusMessage("Initializing syntax...", syntaxInit); this.disposables.push(onDidOpenListener); - this.disposables.push(onDidCloseListener); + // this.disposables.push(onDidCloseListener); + this.disposables.push(onDidCloseWorkspace); this.disposables.push(onDidDeleteListener); this.disposables.push(onDidSaveListener); this.disposables.push(onDidChangeWindowState); this.disposables.push(onDidChangeActiveTextEditor); + this.disposables.push(vscode.window.registerFileDecorationProvider(this.syncedFileDecorator)); } private async initializeSyntax(): Promise { @@ -165,7 +181,7 @@ export class SynchService implements vscode.Disposable { private async setupSync( viewerDocument: vscode.TextDocument, ): Promise { - const viewerFilePath = path.normalize(viewerDocument.fileName); + const viewerFilePath = path.normalize(viewerDocument.uri.fsPath); const openedBase = path.basename(viewerFilePath); if (!hasWorkspace()) { @@ -196,7 +212,7 @@ export class SynchService implements vscode.Disposable { showInfoMessage(`Opening master script: ${path.basename(masterPath)}`); let masterEditor = await SynchService.openMasterScript(masterUri); let masterDoc = masterEditor.document - SynchService.checkAndUpdateMasterDocumentInBackground(masterEditor, viewerDocument) + SynchService.checkAndUpdateMasterDocumentInBackground(masterEditor, viewerDocument); // Connection goes on in the background let viewerConnecting: Promise = this.setupConnection(); @@ -217,62 +233,75 @@ export class SynchService implements vscode.Disposable { } }); - let sync = this.findSyncByTempFilePath(viewerFilePath) ?? - this.findSyncByMasterFilePath(masterPath); - if (sync) { + const masterSync = this.findSyncByMasterFilePath(masterPath); + const syncs : ScriptSync[] = []; + + if(masterSync) { + syncs.push(masterSync); + } else { + syncs.push(...this.findSyncsByTempFilePath(viewerFilePath)); + } + + if(!this.host.config.getConfig(ConfigKey.KeepViewerFileOpen, true)) { + closeTextDocument(viewerDocument); + } + + if(syncs.length) { // Already syncing the master, add another id and viewer file - sync.subscribe(parsed.scriptId, viewerDocument); + syncs.forEach(sync => sync.subscribe(parsed.scriptId, viewerDocument)); } else { const config = ConfigService.getInstance(); - sync = new ScriptSync( + const sync = new ScriptSync( masterDoc, parsed.extension as ScriptLanguage, config, parsed.scriptId, viewerDocument, - this.host, + this, ); await sync.initialize(); this.activeSyncs.set(masterPath, sync); + syncs.push(sync); } + syncs.forEach(sync => this.syncedFileDecorator.refresh(sync.getMasterDocument().uri)); + if (this.websocket && this.websocket.isConnected()) { - this.sendSyncSubscription(sync); + syncs.forEach(sync => this.sendSyncSubscription(sync)); } else { viewerConnecting.then((connected) => { if (connected) { - this.sendSyncSubscription(sync); + syncs.forEach(sync=>this.sendSyncSubscription(sync)); } }); } + this.clearEmptySyncs(); + return true; } public removeSync(filePath: string, close: boolean): void { // seeing if we closed a temp file or a master file - let sync = - this.findSyncByTempFilePath(filePath) ?? - this.findSyncByMasterFilePath(filePath); + let sync = this.findSyncByMasterFilePath(filePath); if (!sync) { // No sync found for this file, we are not tracking it return; } - if (sync.getMasterFilePath() !== filePath) { - // We only destroy the sync if the master file is closed - // This is so we can continue to handle preprocessor directives while editing. - this.activeSyncs.delete(sync.getMasterFilePath()); - sync.dispose(); - } else { - // This is not the master file, just remove the tracking links. - const parsed = SynchService.parseTempFile(filePath); - if (parsed) { - // Remove the tracking subscription, if there are no more tracked files we will dispose the sync - sync.unsubscribeById(parsed.scriptId); - if (close) { - closeEditor(filePath); - } + this.activeSyncs.delete(sync.getMasterFilePath()); + this.syncedFileDecorator.refresh(sync.getMasterDocument().uri); + sync.dispose(); + + this.clearEmptySyncs(); + } + + public clearEmptySyncs() { + for(const [key,sync] of this.activeSyncs) { + if(!sync.hasFilesToTrack()) { + this.activeSyncs.delete(key); + this.syncedFileDecorator.refresh(sync.getMasterDocument().uri); + sync.dispose(); } } @@ -286,6 +315,11 @@ export class SynchService implements vscode.Disposable { this.websocket = undefined; } } + vscode.commands.executeCommand( + "setContext", + "slVscodeEdit:syncsActive", + this.activeSyncs.size > 0 + ); } //==================================================================== @@ -597,9 +631,9 @@ export class SynchService implements vscode.Disposable { ); } - public findSyncByTempFilePath(filePath: string): ScriptSync | undefined { + public findSyncsByTempFilePath(filePath: string): ScriptSync[] { filePath = path.normalize(filePath); - return [...this.activeSyncs.values()].find((sync) => + return [...this.activeSyncs.values()].filter((sync) => sync.isTrackingFile(filePath), ); } @@ -723,9 +757,8 @@ export class SynchService implements vscode.Disposable { this.initializeSyntax(); } - private onCloseTextDocument(document: vscode.TextDocument): void { - const filePath = path.normalize(document.fileName); - this.removeSync(filePath, false); + private onCloseWorkspace(workspace: vscode.WorkspaceFolder) : void { + console.error("WORKSPACE CLOSE",workspace.name,workspace.uri.fsPath); } private onDeleteFiles(event: vscode.FileDeleteEvent): void { @@ -768,13 +801,13 @@ export class SynchService implements vscode.Disposable { // if the viewer launched it we will soon get a foucus event (onChangeWindowState) // Find the sync for this file, if any and then record the time. const filePath = path.normalize(editor.document.fileName); - const sync = this.findSyncByTempFilePath(filePath); - if (sync) { + const syncs = this.findSyncsByTempFilePath(filePath); + if (syncs.length) { // We have a sync for this file, record the time // We'll use this to see if a focus event happens very soon after // this event, if so we can assume the viewer launched us this.lastActiveChange = Date.now(); - this.activeSync = sync; + this.activeSync = syncs.pop(); } } //#endregion diff --git a/src/vscode/SyncedFileDecorator.ts b/src/vscode/SyncedFileDecorator.ts new file mode 100644 index 0000000..21ce5a0 --- /dev/null +++ b/src/vscode/SyncedFileDecorator.ts @@ -0,0 +1,35 @@ +import { + FileDecorationProvider, + Uri, + FileDecoration, + EventEmitter, + ProviderResult, + CancellationToken, + ThemeColor +} from "vscode"; + +import { SynchService } from "../synchservice"; + +export class SyncedFileDecorator implements FileDecorationProvider { + private syncService: SynchService; + private _onDidChangeFileDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations = this._onDidChangeFileDecorations.event; + + constructor(syncService:SynchService) { + this.syncService = syncService; + } + + provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult { + if(this.syncService.findSyncByMasterFilePath(uri.fsPath)) { + return { + badge: '🔗', + tooltip: 'Synchronized with secondlife viewer', + color: new ThemeColor('tab.activeBorderTop'), + }; + } + } + + public refresh(uri?: Uri | Uri[]) : void { + this._onDidChangeFileDecorations.fire(uri); + } +} From e0d3e1b10b2277fd6fd6cd88e166c51e5e390594 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sat, 18 Apr 2026 03:40:10 +0100 Subject: [PATCH 06/19] Dont close file if no master found --- src/synchservice.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 0a3b3a2..8b7e227 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -199,7 +199,9 @@ export class SynchService implements vscode.Disposable { // Look for a file in the workspace with the same name as the master script let masterUri = await SynchService.findMasterFile(parsed, viewerDocument); + let masterFound = true; if (!masterUri) { + masterFound = false; // There was no master file found, we are our own master showInfoMessage( `No master script found for: ${parsed.scriptName}.${parsed.extension}`, @@ -242,7 +244,7 @@ export class SynchService implements vscode.Disposable { syncs.push(...this.findSyncsByTempFilePath(viewerFilePath)); } - if(!this.host.config.getConfig(ConfigKey.KeepViewerFileOpen, true)) { + if(!this.host.config.getConfig(ConfigKey.KeepViewerFileOpen, true) && masterFound) { closeTextDocument(viewerDocument); } From 243e01000251dcd2d21599e346747118626f7dab Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Sat, 18 Apr 2026 03:46:25 +0100 Subject: [PATCH 07/19] linting --- src/extension.ts | 4 ++-- src/scriptsync.ts | 2 +- src/synchservice.ts | 7 +++---- src/vscode/SyncedFileDecorator.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 925d701..755f024 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -63,7 +63,7 @@ export function deactivate(): void { synchService.deactivate(); } -function setupCommands(context: vscode.ExtensionContext) { +function setupCommands(context: vscode.ExtensionContext) : void { // Register commands context.subscriptions.push( vscode.commands.registerCommand( @@ -129,7 +129,7 @@ function setupCommands(context: vscode.ExtensionContext) { if(!uri) { return; } - SynchService.getInstance().removeSync(path.normalize(uri.fsPath), false); + SynchService.getInstance().removeSync(path.normalize(uri.fsPath)); } ) ); diff --git a/src/scriptsync.ts b/src/scriptsync.ts index 4bc3459..1ee37cd 100644 --- a/src/scriptsync.ts +++ b/src/scriptsync.ts @@ -25,7 +25,7 @@ import { } from "./utils"; import { ScriptLanguage } from "./shared/languageservice"; import { CompilationResult, RuntimeDebug, RuntimeError } from "./viewereditwsclient"; -import { HostInterface, normalizePath } from "./interfaces/hostinterface"; +import { normalizePath } from "./interfaces/hostinterface"; import { SynchService } from "./synchservice"; import { IncludeInfo } from "./shared/parser"; import { sha256 } from "js-sha256"; diff --git a/src/synchservice.ts b/src/synchservice.ts index 8b7e227..0026fcb 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -25,7 +25,6 @@ import { showInfoMessage, showStatusMessage, showWarningMessage, - closeEditor, logInfo, VSCodeHost, closeTextDocument, @@ -283,7 +282,7 @@ export class SynchService implements vscode.Disposable { return true; } - public removeSync(filePath: string, close: boolean): void { + public removeSync(filePath: string): void { // seeing if we closed a temp file or a master file let sync = this.findSyncByMasterFilePath(filePath); if (!sync) { @@ -298,7 +297,7 @@ export class SynchService implements vscode.Disposable { this.clearEmptySyncs(); } - public clearEmptySyncs() { + public clearEmptySyncs() : void { for(const [key,sync] of this.activeSyncs) { if(!sync.hasFilesToTrack()) { this.activeSyncs.delete(key); @@ -767,7 +766,7 @@ export class SynchService implements vscode.Disposable { const uris = event.files; uris.forEach((uri) => { const filePath = path.normalize(uri.fsPath); - this.removeSync(filePath, false); + this.removeSync(filePath); }); } diff --git a/src/vscode/SyncedFileDecorator.ts b/src/vscode/SyncedFileDecorator.ts index 21ce5a0..816b9e5 100644 --- a/src/vscode/SyncedFileDecorator.ts +++ b/src/vscode/SyncedFileDecorator.ts @@ -19,7 +19,7 @@ export class SyncedFileDecorator implements FileDecorationProvider { this.syncService = syncService; } - provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult { + provideFileDecoration(uri: Uri, _token: CancellationToken): ProviderResult { if(this.syncService.findSyncByMasterFilePath(uri.fsPath)) { return { badge: '🔗', From abd070cea1c57cf51db8649b5e4cf51c7dbf8923 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 11:29:31 -0700 Subject: [PATCH 08/19] I had seen a couple issues when checking against the documentation. --- .pre-commit-config.yaml | 10 +- doc/Message_Interfaces.md | 236 ++++++++++++++++++++++++++++---------- src/synchservice.ts | 8 +- src/viewereditwsclient.ts | 5 + 4 files changed, 193 insertions(+), 66 deletions(-) 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..2dd8c98 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` + - `defcache`: 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/synchservice.ts b/src/synchservice.ts index a562a2c..5ab8d31 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -295,7 +295,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), @@ -359,11 +359,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, diff --git a/src/viewereditwsclient.ts b/src/viewereditwsclient.ts index 98b2d2a..c4dfd38 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; } @@ -63,6 +67,7 @@ export interface CompilationError { column: number; level: string; message: string; + format?: "lsl"; } export interface CompilationResult { From 711e10b59d5a7e9244eaf03e06711f21bca7a204 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 14:49:40 -0700 Subject: [PATCH 09/19] Consume the pregenerated selene and LUALSP config files delivered through the JSON RPC interface to the viewer. IF syntax.cache is not implemented on the viewer continue using the old method. --- doc/Message_Interfaces.md | 2 +- src/pluginsupport.ts | 55 ++++++++++++++++++++++++++ src/shared/languagerepository.ts | 42 ++++++++++++++++++++ src/shared/languageservice.ts | 68 ++++++++++++++++++++++++++++++-- src/synchservice.ts | 37 ++++++++++++++--- src/viewereditwsclient.ts | 16 ++++++++ 6 files changed, 211 insertions(+), 9 deletions(-) diff --git a/doc/Message_Interfaces.md b/doc/Message_Interfaces.md index 2dd8c98..52cb53f 100644 --- a/doc/Message_Interfaces.md +++ b/doc/Message_Interfaces.md @@ -127,7 +127,7 @@ interface SessionHandshake { - `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` - - `defcache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files + - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files ### SessionHandshakeResponse diff --git a/src/pluginsupport.ts b/src/pluginsupport.ts index 7deb61d..bb12995 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 = { @@ -264,6 +290,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 5ab8d31..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; @@ -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) { @@ -381,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}`, @@ -391,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); } } @@ -430,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 c4dfd38..90722b2 100644 --- a/src/viewereditwsclient.ts +++ b/src/viewereditwsclient.ts @@ -62,6 +62,22 @@ 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; From fc07cfed264bc43a1f5f045728c04806801bc71d Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:49:47 +0100 Subject: [PATCH 10/19] Remove Debug Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 755f024..4ad5f35 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -122,7 +122,6 @@ function setupCommands(context: vscode.ExtensionContext) : void { vscode.commands.registerCommand( "second-life-scripting.stopFileSync", (uri?: vscode.Uri) => { - console.error("CLOSE SYNC COMMAND", uri); if(!uri) { uri = vscode.window.activeTextEditor?.document.uri; } From e50c8b57174ae7d7c42ddeb49aa2eb3403d202e2 Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:50:24 +0100 Subject: [PATCH 11/19] Handle close textDocument errors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/synchservice.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 7527c20..78a7fe8 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -250,7 +250,11 @@ export class SynchService implements vscode.Disposable { } if(!this.host.config.getConfig(ConfigKey.KeepViewerFileOpen, true) && masterFound) { - closeTextDocument(viewerDocument); + void closeTextDocument(viewerDocument).catch((error) => { + logInfo( + `Failed to auto-close viewer document ${viewerDocument.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + }); } if(syncs.length) { From dfc90199a1b2ae8cf4960eff429ef086b1085812 Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:50:47 +0100 Subject: [PATCH 12/19] Normalize file path before setting in activeSyncs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/synchservice.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 78a7fe8..657d2d2 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -271,7 +271,8 @@ export class SynchService implements vscode.Disposable { this, ); await sync.initialize(); - this.activeSyncs.set(masterPath, sync); + const normalizedMasterPath = path.normalize(masterPath); + this.activeSyncs.set(normalizedMasterPath, sync); syncs.push(sync); } From e0386b48830bfbfb2d9955bf207391512259f85c Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:52:09 +0100 Subject: [PATCH 13/19] Update src/vscode/SyncedFileDecorator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vscode/SyncedFileDecorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode/SyncedFileDecorator.ts b/src/vscode/SyncedFileDecorator.ts index 816b9e5..e5f818e 100644 --- a/src/vscode/SyncedFileDecorator.ts +++ b/src/vscode/SyncedFileDecorator.ts @@ -24,7 +24,7 @@ export class SyncedFileDecorator implements FileDecorationProvider { return { badge: '🔗', tooltip: 'Synchronized with secondlife viewer', - color: new ThemeColor('tab.activeBorderTop'), + color: new ThemeColor('secondlife.syncedfile'), }; } } From e868284c4eb2caa32cbe1994cb09fbc82a04142c Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:53:22 +0100 Subject: [PATCH 14/19] Update src/vscode/SyncedFileDecorator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vscode/SyncedFileDecorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode/SyncedFileDecorator.ts b/src/vscode/SyncedFileDecorator.ts index e5f818e..3f175fb 100644 --- a/src/vscode/SyncedFileDecorator.ts +++ b/src/vscode/SyncedFileDecorator.ts @@ -8,7 +8,7 @@ import { ThemeColor } from "vscode"; -import { SynchService } from "../synchservice"; +import type { SynchService } from "../synchservice"; export class SyncedFileDecorator implements FileDecorationProvider { private syncService: SynchService; From f10443b4bbbd5c5cdc4ce7c79dd25a268357b1ca Mon Sep 17 00:00:00 2001 From: WolfGang Date: Thu, 30 Apr 2026 17:56:18 +0100 Subject: [PATCH 15/19] Update src/synchservice.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/synchservice.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/synchservice.ts b/src/synchservice.ts index 657d2d2..3a12474 100644 --- a/src/synchservice.ts +++ b/src/synchservice.ts @@ -857,7 +857,17 @@ export class SynchService implements vscode.Disposable { } private onCloseWorkspace(workspace: vscode.WorkspaceFolder) : void { - console.error("WORKSPACE CLOSE",workspace.name,workspace.uri.fsPath); + const workspacePath = path.normalize(workspace.uri.fsPath); + const workspacePrefix = workspacePath.endsWith(path.sep) + ? workspacePath + : workspacePath + path.sep; + + for (const document of vscode.workspace.textDocuments) { + const filePath = path.normalize(document.fileName); + if (filePath === workspacePath || filePath.startsWith(workspacePrefix)) { + this.removeSync(filePath); + } + } } private onDeleteFiles(event: vscode.FileDeleteEvent): void { From 4e00e8154202c9f4f40d5761450839e0ea7998a8 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Thu, 30 Apr 2026 11:56:36 -0700 Subject: [PATCH 16/19] Check that Luau-LSP is active before configuring. --- src/pluginsupport.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pluginsupport.ts b/src/pluginsupport.ts index bb12995..35a0e98 100644 --- a/src/pluginsupport.ts +++ b/src/pluginsupport.ts @@ -168,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); From 12947fcb30d4e904304bcdc3d598af3a1f78f0d0 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Thu, 30 Apr 2026 12:12:38 -0700 Subject: [PATCH 17/19] Correctly handle possibly async message handlers. --- src/websockclient.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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}:`, From 05aa0318d31e644179b55f8b4be5041bc55df179 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 27 May 2026 11:40:23 -0700 Subject: [PATCH 18/19] Rider test (#73) Revise workflow for publishing to VSCode marketplace. --- .github/workflows/ci.yml | 76 +-- .github/workflows/generate-release-notes.yml | 48 ++ .github/workflows/pre-commit.yaml | 10 +- .github/workflows/release.yml | 482 +++++++++++++++++++ package-lock.json | 146 +----- package.json | 1 - 6 files changed, 541 insertions(+), 222 deletions(-) create mode 100644 .github/workflows/generate-release-notes.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6f3ff1..f284e80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,26 +3,22 @@ name: CI on: push: branches: [ "*" ] - tags: [ "v*" ] + tags-ignore: [ "**" ] pull_request: branches: [ main, develop ] - workflow_dispatch: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [24.x] steps: - name: Checkout code uses: actions/checkout@v5 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: ${{ matrix.node-version }} + node-version: '24.x' cache: 'npm' - name: Install dependencies @@ -41,7 +37,6 @@ jobs: - name: Run full test suite run: xvfb-run -a npm test - continue-on-error: false build: needs: test @@ -66,26 +61,9 @@ jobs: - name: Compile extension run: npm run vscode:prepublish - - name: Determine version suffix + - name: Get build SHA id: version - run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "suffix=" >> $GITHUB_OUTPUT - echo "release_type=release" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" = "refs/heads/debug" ]; then - echo "suffix=-debug" >> $GITHUB_OUTPUT - echo "release_type=debug" >> $GITHUB_OUTPUT - fi - echo "sha_short=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_OUTPUT - - - name: Update package.json version for non-main builds - if: github.ref != 'refs/heads/main' - run: | - # Get current version and append build info - CURRENT_VERSION=$(node -p "require('./package.json').version") - NEW_VERSION="${CURRENT_VERSION}${{ steps.version.outputs.suffix }}-${GITHUB_RUN_NUMBER}+${{ steps.version.outputs.sha_short }}" - npm version "$NEW_VERSION" --no-git-tag-version - echo "Updated version to: $NEW_VERSION" + run: echo "sha_short=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_OUTPUT - name: Package extension run: vsce package @@ -95,52 +73,10 @@ jobs: run: | PACKAGE_FILE=$(ls *.vsix | head -1) echo "filename=$PACKAGE_FILE" >> $GITHUB_OUTPUT - echo "name=$(basename "$PACKAGE_FILE" .vsix)" >> $GITHUB_OUTPUT - name: Upload VSIX artifact uses: actions/upload-artifact@v4 with: - name: vscode-extension-${{ steps.version.outputs.release_type }}-${{ steps.version.outputs.sha_short }} + name: vscode-extension-${{ steps.version.outputs.sha_short }} path: ${{ steps.package.outputs.filename }} retention-days: 30 - - release: - name: Update release info - needs: build - runs-on: ubuntu-latest - timeout-minutes: 10 - if: startsWith(github.ref, 'refs/tags/v') - permissions: - contents: write - - steps: - - name: Get version from tag - id: version - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Download VSIX artifact - uses: actions/download-artifact@v4 - with: - pattern: vscode-extension-* - merge-multiple: true - - - name: Get package filename - id: package - run: | - PACKAGE_FILE=$(ls *.vsix | head -1) - echo "filename=$PACKAGE_FILE" >> $GITHUB_OUTPUT - - - name: Create release - id: release - uses: secondlife-3p/action-gh-release@v1 - with: - # name the release page for the branch - name: "SL-VScode Plugin ${{ steps.version.outputs.version }} Release" - prerelease: true - generate_release_notes: true - target_commitish: ${{ github.sha }} - append_body: true - files: ${{ steps.package.outputs.filename }} diff --git a/.github/workflows/generate-release-notes.yml b/.github/workflows/generate-release-notes.yml new file mode 100644 index 0000000..ed5a7c4 --- /dev/null +++ b/.github/workflows/generate-release-notes.yml @@ -0,0 +1,48 @@ +name: Generate Release Notes + +on: + workflow_call: + inputs: + tag_name: + description: Tag name to generate release notes for + required: true + type: string + target_commitish: + description: Branch or commit SHA used as target for note generation + required: true + type: string + outputs: + release_notes: + description: Generated release notes body + value: ${{ jobs.generate.outputs.release_notes }} + +permissions: + contents: read + +jobs: + generate: + runs-on: ubuntu-latest + outputs: + release_notes: ${{ steps.generate.outputs.release_notes }} + + steps: + - name: Generate release notes body + id: generate + uses: actions/github-script@v7 + env: + TAG_NAME: ${{ inputs.tag_name }} + TARGET_COMMITISH: ${{ inputs.target_commitish }} + with: + script: | + const { owner, repo } = context.repo; + const tag_name = process.env.TAG_NAME; + const target_commitish = process.env.TARGET_COMMITISH; + + const response = await github.rest.repos.generateReleaseNotes({ + owner, + repo, + tag_name, + target_commitish, + }); + + core.setOutput('release_notes', response.data.body || ''); diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 18a488f..debd5c3 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -7,7 +7,6 @@ on: branches: [main, develop] permissions: - id-token: write contents: read jobs: @@ -20,18 +19,15 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 'latest' + node-version: '24.x' cache: 'npm' - name: Install dependencies - run: npm install + run: npm ci - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.x' - - name: Install pre-commit - run: pip install pre-commit - - name: Run pre-commit checks - run: pre-commit run --all-files + uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..055859b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,482 @@ +name: Cut Release + +on: + workflow_dispatch: + inputs: + tag_name: + description: Release tag to create, for example v1.0.3 + required: true + type: string + target_commitish: + description: Branch or commit SHA the tag should point to + required: true + default: develop + type: string + prerelease: + description: Mark the GitHub release as a prerelease + required: true + default: true + type: boolean + draft: + description: Create the GitHub release as a draft + required: true + default: false + type: boolean + publish_marketplace: + description: Publish the extension to the VS Code Marketplace + required: true + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + authorize_actor: + name: Authorize triggering user + runs-on: ubuntu-latest + steps: + - name: Ensure actor has access + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const username = context.actor; + + const response = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username, + }); + + const allowed = ['admin', 'maintain']; + + if (!allowed.includes(response.data.permission)) { + core.setFailed( + `${username} must have maintain or admin access to run this workflow. Current permission: ${response.data.permission}` + ); + return; + } + + core.info(`${username} is authorized with permission: ${response.data.permission}`); + + validate: + name: Validate release inputs + needs: authorize_actor + runs-on: ubuntu-latest + outputs: + version: ${{ steps.set_variables.outputs.version }} + steps: + - name: Validate + shell: bash + run: | + if [ -z "${{ inputs.tag_name }}" ]; then + echo "tag_name is required and cannot be empty" >&2 + exit 1 + fi + + # Enforce plain vX.Y.Z — no pre-release suffixes or build metadata. + # The VS Code Marketplace does not support SemVer pre-release identifiers in + # extension versions. Use an odd minor number (e.g. v1.3.0) with the + # prerelease input flag instead of a suffix like v1.2.3-rc.1. + if [[ ! "${{ inputs.tag_name }}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "tag_name must be plain vX.Y.Z (for example v1.0.3)." >&2 + echo "The VS Code Marketplace does not support pre-release suffixes in version numbers." >&2 + echo "Use an odd minor version (e.g. v1.3.0) with the prerelease flag for pre-release builds." >&2 + exit 1 + fi + + # Enforce odd/even minor version convention: + # even minor (0, 2, 4, ...) → stable release (prerelease must be false) + # odd minor (1, 3, 5, ...) → pre-release (prerelease must be true) + MINOR=$(echo "${{ inputs.tag_name }}" | cut -d. -f2) + IS_PRERELEASE="${{ inputs.prerelease }}" + if (( MINOR % 2 == 0 )) && [[ "$IS_PRERELEASE" == "true" ]]; then + echo "Even minor version (${{ inputs.tag_name }}) must not be marked as prerelease." >&2 + echo "Even minor = stable release. Use an odd minor version for pre-release builds." >&2 + exit 1 + fi + if (( MINOR % 2 == 1 )) && [[ "$IS_PRERELEASE" == "false" ]]; then + echo "Odd minor version (${{ inputs.tag_name }}) must be marked as prerelease." >&2 + echo "Odd minor = pre-release. Use an even minor version for stable releases." >&2 + exit 1 + fi + + echo "tag_name=${{ inputs.tag_name }}" >&2 + echo "target_commitish=${{ inputs.target_commitish }}" >&2 + echo "prerelease=${{ inputs.prerelease }}" >&2 + echo "draft=${{ inputs.draft }}" >&2 + + - name: Set Variables + id: set_variables + shell: bash + run: | + TAG_NAME="${{ inputs.tag_name }}" + SEMANTIC_VERSION="${TAG_NAME#v}" + echo "version=$SEMANTIC_VERSION" >> "$GITHUB_OUTPUT" + echo "version=$SEMANTIC_VERSION" >&2 + + release_notes: + needs: validate + name: Generate release notes + runs-on: ubuntu-latest + outputs: + release_notes: ${{ steps.generate.outputs.release_notes }} + permissions: + contents: write + + steps: + - name: Generate release notes body + id: generate + uses: actions/github-script@v7 + env: + TAG_NAME: ${{ inputs.tag_name }} + TARGET_COMMITISH: ${{ inputs.target_commitish }} + with: + script: | + const { owner, repo } = context.repo; + const tag_name = process.env.TAG_NAME; + const target_commitish = process.env.TARGET_COMMITISH; + + const response = await github.rest.repos.generateReleaseNotes({ + owner, + repo, + tag_name, + target_commitish, + }); + + core.setOutput('release_notes', response.data.body || ''); + + - name: Checkout target branch + uses: actions/checkout@v5 + with: + ref: ${{ inputs.target_commitish }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.x' + cache: 'npm' + + - name: Update version + shell: bash + run: | + VERSION="${{ needs.validate.outputs.version }}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Write release notes file + shell: bash + env: + RELEASE_NOTES: ${{ steps.generate.outputs.release_notes }} + run: | + printf '%s\n' "$RELEASE_NOTES" > release_notes.md + + - name: Update CHANGELOG.md + shell: bash + run: | + VERSION="${{ needs.validate.outputs.version }}" + RELEASE_DATE=$(date -u +%Y-%m-%d) + + { + echo "## [${VERSION}] - ${RELEASE_DATE}" + echo + cat release_notes.md + echo + } > changelog_entry.md + + awk ' + BEGIN { inserted = 0 } + /^## \[/ && inserted == 0 { + while ((getline line < "changelog_entry.md") > 0) print line + close("changelog_entry.md") + print "" + inserted = 1 + } + { print } + END { + if (inserted == 0) { + print "" + while ((getline line < "changelog_entry.md") > 0) print line + close("changelog_entry.md") + } + } + ' CHANGELOG.md > CHANGELOG.new.md + + mv CHANGELOG.new.md CHANGELOG.md + cat CHANGELOG.md >&2 + + - name: Update README badges from package.json + shell: bash + run: | + node - <<'NODE' + const fs = require('fs'); + + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + let vscodeVersion = pkg.engines && pkg.engines.vscode ? String(pkg.engines.vscode) : ''; + + if (vscodeVersion.startsWith('^')) { + vscodeVersion = `${vscodeVersion.slice(1)}+`; + } + + const versionBadge = `[![Version](https://img.shields.io/badge/version-${pkg.version}-blue.svg)](https://github.com/secondlife/sl-vscode-plugin)`; + const licenseBadge = `[![License](https://img.shields.io/badge/license-${pkg.license}-green.svg)](LICENSE)`; + const vscodeBadge = `[![VS Code](https://img.shields.io/badge/VS%20Code-${encodeURIComponent(vscodeVersion)}-red.svg)](https://code.visualstudio.com/)`; + + let readme = fs.readFileSync('README.md', 'utf8'); + + readme = readme.replace(/^\[!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-[^)]+\)\]\(https:\/\/github\.com\/secondlife\/sl-vscode-plugin\)$/m, versionBadge); + readme = readme.replace(/^\[!\[License\]\(https:\/\/img\.shields\.io\/badge\/license-[^)]+\)\]\(LICENSE\)$/m, licenseBadge); + readme = readme.replace(/^\[!\[VS Code\]\(https:\/\/img\.shields\.io\/badge\/VS%20Code-[^)]+\)\]\(https:\/\/code\.visualstudio\.com\/\)$/m, vscodeBadge); + + fs.writeFileSync('README.md', readme); + NODE + + - name: Package modified files for downstream jobs + shell: bash + run: | + rm -rf release-notes-files + mkdir -p release-notes-files + + git ls-files -m > modified-files.txt + git ls-files --others --exclude-standard >> modified-files.txt + + if [ ! -s modified-files.txt ]; then + echo "No modified files detected" >&2 + else + while IFS= read -r file; do + if [ -n "$file" ] && [ -e "$file" ]; then + mkdir -p "release-notes-files/$(dirname "$file")" + cp "$file" "release-notes-files/$file" + fi + done < modified-files.txt + fi + + - name: Upload modified files artifact + uses: actions/upload-artifact@v4 + with: + name: release-notes-modified-files-${{ github.run_id }} + path: | + release-notes-files + modified-files.txt + retention-days: 1 + + # - name: Commit and push changelog update + # shell: bash + # run: | + # TARGET_BRANCH="${{ inputs.target_commitish }}" + # TARGET_BRANCH="${TARGET_BRANCH#refs/heads/}" + + # if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null; then + # echo "target_commitish must be a branch name (or refs/heads/) to update CHANGELOG.md" >&2 + # echo "Received: ${{ inputs.target_commitish }}" >&2 + # exit 1 + # fi + + # git config user.name "github-actions[bot]" + # git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # git add CHANGELOG.md + + # if git diff --cached --quiet; then + # echo "No changelog changes to commit" + # exit 0 + # fi + + # git commit -m "docs: update changelog for ${{ inputs.tag_name }}" + # git push origin HEAD:$TARGET_BRANCH + + build: + needs: release_notes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ inputs.target_commitish }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.x' + cache: 'npm' + + - name: Download modified files artifact + uses: actions/download-artifact@v4 + with: + name: release-notes-modified-files-${{ github.run_id }} + path: . + + - name: Apply modified files from release_notes job + shell: bash + run: | + if [ -d release-notes-files ]; then + cp -R release-notes-files/. . + fi + + - name: Create release branch, commit, and push tag + shell: bash + run: | + TARGET_BRANCH="${{ inputs.target_commitish }}" + TARGET_BRANCH="${TARGET_BRANCH#refs/heads/}" + RELEASE_BRANCH="release/${{ inputs.tag_name }}" + + if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null; then + echo "target_commitish must be a branch name (or refs/heads/) to create a release branch" >&2 + echo "Received: ${{ inputs.target_commitish }}" >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git checkout -b "$RELEASE_BRANCH" + git add -u + + if git diff --cached --quiet; then + echo "No repository changes to commit" + else + git commit -m "chore: prepare release ${{ inputs.tag_name }}" + fi + + git push origin "$RELEASE_BRANCH" + + git tag -a "${{ inputs.tag_name }}" -m "Release ${{ inputs.tag_name }}" + git push origin "refs/tags/${{ inputs.tag_name }}" + + - name: Create release PRs + uses: actions/github-script@v7 + env: + RELEASE_NOTES: ${{ needs.release_notes.outputs.release_notes }} + with: + script: | + const { owner, repo } = context.repo; + const releaseBranch = `release/${{ inputs.tag_name }}`; + const tagName = '${{ inputs.tag_name }}'; + const prTitle = `chore: prepare release ${tagName}`; + const releaseNotes = process.env.RELEASE_NOTES || ''; + const prBody = `Automated release PR for ${tagName}.\n\nUpdates CHANGELOG.md, package.json version, and README badges.\n\n## Release Notes\n\n${releaseNotes}`; + + for (const base of ['develop', 'main']) { + try { + const pr = await github.rest.pulls.create({ + owner, + repo, + title: prTitle, + head: releaseBranch, + base, + body: prBody, + }); + core.info(`Created PR into ${base}: ${pr.data.html_url}`); + } catch (err) { + core.warning(`Failed to create PR into ${base}: ${err.message}`); + } + } + + - name: Install dependencies + run: npm install + + - name: Compile extension + run: npm run vscode:prepublish + + - name: Install vsce + run: npm install -g @vscode/vsce + + - name: Package extension + run: vsce package + + - name: Get package filename + id: package + shell: bash + run: | + PACKAGE_FILE=$(ls *.vsix | head -1) + echo "filename=$PACKAGE_FILE" >> "$GITHUB_OUTPUT" + + - name: Upload VSIX artifact + uses: actions/upload-artifact@v4 + with: + name: vscode-extension-release-${{ github.run_id }} + path: ${{ steps.package.outputs.filename }} + retention-days: 30 + + release: + name: Publish release + needs: [build, release_notes, validate] + runs-on: ubuntu-latest + + steps: + - name: Download VSIX artifact + uses: actions/download-artifact@v4 + with: + name: vscode-extension-release-${{ github.run_id }} + path: vsix/ + + - name: Download modified files artifact + uses: actions/download-artifact@v4 + with: + name: release-notes-modified-files-${{ github.run_id }} + path: . + + - name: Apply modified files from release_notes job + shell: bash + run: | + if [ -d release-notes-files ]; then + cp -R release-notes-files/. . + fi + + - name: Get package filename + id: package + shell: bash + run: | + PACKAGE_FILE=$(ls vsix/*.vsix | head -1) + echo "filename=$PACKAGE_FILE" >> "$GITHUB_OUTPUT" + + - name: Create release page + id: release + uses: secondlife-3p/action-gh-release@v1 + with: + # name the release page for the branch + tag_name: ${{ inputs.tag_name }} + target_commitish: ${{ inputs.target_commitish }} + name: "SL-VScode Plugin ${{ needs.validate.outputs.version }} Release" + prerelease: ${{ inputs.prerelease }} + draft: ${{ inputs.draft }} + body_path: release_notes.md + files: ${{ steps.package.outputs.filename }} + + publish: + name: Publish to VS Code Marketplace + needs: [release, build, validate] + runs-on: ubuntu-latest + # Skip if publish not requested or if this is a draft release + if: ${{ inputs.publish_marketplace && !inputs.draft }} + + steps: + - name: Download VSIX artifact + uses: actions/download-artifact@v4 + with: + name: vscode-extension-release-${{ github.run_id }} + path: vsix/ + + - name: Get package filename + id: package + shell: bash + run: | + PACKAGE_FILE=$(ls vsix/*.vsix | head -1) + echo "filename=$PACKAGE_FILE" >> "$GITHUB_OUTPUT" + + - name: Publish to Marketplace + shell: bash + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + npm install -g @vscode/vsce + + if [[ "${{ inputs.prerelease }}" == "true" ]]; then + echo "Publishing pre-release to Marketplace..." >&2 + vsce publish --pre-release --packagePath "${{ steps.package.outputs.filename }}" --pat "$VSCE_PAT" + else + echo "Publishing stable release to Marketplace..." >&2 + vsce publish --packagePath "${{ steps.package.outputs.filename }}" --pat "$VSCE_PAT" + fi diff --git a/package-lock.json b/package-lock.json index befb184..a4cc9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sl-vscode-plugin", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sl-vscode-plugin", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -29,7 +29,6 @@ "eslint": "^9.34.0", "glob": "^11.1.0", "mocha": "^10.0.0", - "pre-commit": "^1.2.2", "typescript": "^5.9.2" }, "engines": { @@ -525,7 +524,6 @@ "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", @@ -932,7 +930,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1084,13 +1081,6 @@ "dev": true, "license": "ISC" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/c8": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", @@ -1329,22 +1319,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1479,7 +1453,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2849,15 +2822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2984,78 +2948,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pre-commit": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^5.0.1", - "spawn-sync": "^1.0.15", - "which": "1.2.x" - } - }, - "node_modules/pre-commit/node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/pre-commit/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/pre-commit/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pre-commit/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pre-commit/node_modules/which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3073,13 +2965,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true, - "license": "ISC" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3294,18 +3179,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -3569,20 +3442,12 @@ "node": ">= 0.8.0" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3846,13 +3711,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 06c90ad..6323b74 100644 --- a/package.json +++ b/package.json @@ -248,7 +248,6 @@ "eslint": "^9.34.0", "glob": "^11.1.0", "mocha": "^10.0.0", - "pre-commit": "^1.2.2", "typescript": "^5.9.2" }, "dependencies": { From f223244f24cb0a1f53f4bd79b30545c5fe238a20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 18:48:16 +0000 Subject: [PATCH 19/19] chore: prepare release v1.0.4 --- CHANGELOG.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++-- package-lock.json | 4 ++-- package.json | 4 ++-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ddada..c7fcae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,65 @@ All notable changes to the Second Life External Scripting Extension will be docu The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.4] - 2026-05-27 + +## What's Changed +* Fix for incorrect luau-lsp config use by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/2 +* Bump the npm_and_yarn group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/secondlife/sl-vscode-plugin/pull/1 +* Switch from class to extern syntax for luau-lsp defs and move DetecteEvent by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/4 +* Fix selene yaml gen and toml config by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/5 +* Fix for selene yaml self on `:` calls by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/6 +* Fix default data to have eventname for LLEvents:off by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/7 +* Make saves of included/required files trigger 'saves' on actively sync'd file by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/12 +* Fix precomp require generating invalid code for files with no trailing line ending by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/10 +* Add fallback lookup for matching file, if the default glob finds nothing by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/9 +* Add config to enable/disable the extension, and a command to do it quickly by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/8 +* Update Package Name Across Project to fix README Links and be Consistent with Repo Name by @GalaxyLittlepaws in https://github.com/secondlife/sl-vscode-plugin/pull/11 +* Implement a check to prevent saving over a file with identicle content by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/14 +* Add __UNIXTIME__ macro for lsl by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/15 +* Add LLEvents:once do data for default generation by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/18 +* Add support to lexer for luau [[ style strings by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/24 +* Selene warnign about list type in ll funcs by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/21 +* Alter path in selene.toml to be relative by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/26 +* Add support to slua require for aliases and default init files by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/29 +* Add extra filemeta to the output and support using it to match files by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/30 +* LSL Preproc drop comments after defines by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/35 +* Add support to lsl preprocessing for <> style includes by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/34 +* Fix #define #undef and #include being executed inside false conditional blocks by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/36 +* Perform a dry run of the preprocessor when initializing a sync by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/31 +* Add slencode and sldecode functions for lljson by @mikelittman in https://github.com/secondlife/sl-vscode-plugin/pull/40 +* Delay initial definition generation to opening of first luau file by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/42 +* Switch require to use new `dangerouslyexecuterequiredmodule` function by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/45 +* Add support for alternative LLEvents event subscription style by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/41 +* Add support for preproc style macro's as system constants by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/46 +* Fix non relative paths in @line and @module comments by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/47 +* Fix casing of learn more links in luau-lsp docs by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/48 +* Reduce config switches for meta output by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/50 +* Linux-specific instructions by @tapple in https://github.com/secondlife/sl-vscode-plugin/pull/52 +* Add the option to treat the viewer file as master by @tapple in https://github.com/secondlife/sl-vscode-plugin/pull/55 +* Spelling corrections by @FelixWolf in https://github.com/secondlife/sl-vscode-plugin/pull/53 +* Fix default config, and support for custom port by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/51 +* Luau type fixes for luau-lsp by @tapple in https://github.com/secondlife/sl-vscode-plugin/pull/57 +* Always try to match a master file by `@file` meta by @tapple in https://github.com/secondlife/sl-vscode-plugin/pull/61 +* Disable-auto-language-update by @tapple in https://github.com/secondlife/sl-vscode-plugin/pull/62 +* Switch support for lsl preproc by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/59 +* Notecard link and `@file` meta linking improvements by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/64 +* Change how file sync's are ended, and add icons to indicate files that are synced by @WolfGangS in https://github.com/secondlife/sl-vscode-plugin/pull/65 +* Retrieve pregenerated tool config files from cache on viewer. by @Rider-Linden in https://github.com/secondlife/sl-vscode-plugin/pull/66 +* Rider test by @Rider-Linden in https://github.com/secondlife/sl-vscode-plugin/pull/73 + +## New Contributors +* @WolfGangS made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/2 +* @dependabot[bot] made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/1 +* @GalaxyLittlepaws made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/11 +* @mikelittman made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/40 +* @tapple made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/52 +* @FelixWolf made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/53 +* @Rider-Linden made their first contribution in https://github.com/secondlife/sl-vscode-plugin/pull/66 + +**Full Changelog**: https://github.com/secondlife/sl-vscode-plugin/commits/v1.0.4 + + ## [1.0.0] - 2025-11-18 ### Added diff --git a/README.md b/README.md index eb64e02..6bb85b5 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Enhance your Second Life scripting workflow with advanced preprocessing and external editing capabilities!** -[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/secondlife/sl-vscode-plugin) +[![Version](https://img.shields.io/badge/version-1.0.4-blue.svg)](https://github.com/secondlife/sl-vscode-plugin) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![VS Code](https://img.shields.io/badge/VS%20Code-1.85.0+-red.svg)](https://code.visualstudio.com/) +[![VS Code](https://img.shields.io/badge/VS%20Code-1.85.0%2B-red.svg)](https://code.visualstudio.com/) The Second Life External Scripting Extension transforms VS Code into a development environment for Second Life scripts, supporting both **LSL (Linden Scripting Language)** and **SLua (Second Life Lua)** with preprocessing capabilities and viewer integration. diff --git a/package-lock.json b/package-lock.json index a4cc9ea..860a333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sl-vscode-plugin", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sl-vscode-plugin", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 6323b74..a0b48cc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sl-vscode-plugin", "displayName": "Second Life VSCode Plugin", "description": "Binding extension for external LSL and SLua editing from the Second Life viewer.", - "version": "1.0.3", + "version": "1.0.4", "publisher": "lindenlab", "icon": "sl-logo-vscode.png", "extensionDependencies": [], @@ -147,7 +147,7 @@ }, "slVscodeEdit.sync.notecardComment": { "type": "string", - "default" : null, + "default": null, "description": "String to use as a comment for notecards to allow syncing to external files of notecards with a file comment" } }