Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949d72c394 | ||
|
|
dcd1bf25e7 | ||
|
|
45c988c989 | ||
|
|
2815c3e652 | ||
|
|
4b311f8b4c | ||
|
|
4712330faa |
Vendored
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
|
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
|
||||||
"${workspaceFolder}/compiler/dist/**/*.js"
|
"${workspaceFolder}/compiler/dist/**/*.js"
|
||||||
],
|
],
|
||||||
"preLaunchTask": "watch"
|
"timeout": 5000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -13,5 +13,6 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"dutchie",
|
"dutchie",
|
||||||
"dutchies"
|
"dutchies"
|
||||||
]
|
],
|
||||||
|
"Lua.workspace.library": [],
|
||||||
}
|
}
|
||||||
+41
-20
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { CompilationError } from './CompilationError';
|
import { CompilationError, CompilationErrorType } from './CompilationError';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Block types.
|
Block types.
|
||||||
@@ -68,10 +68,21 @@ export abstract class CodeBlock {
|
|||||||
const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
|
const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
|
||||||
|
|
||||||
let leftCursor = 0;
|
let leftCursor = 0;
|
||||||
|
let lineCursor = 0;
|
||||||
let lineCounter = 0;
|
let lineCounter = 0;
|
||||||
const file: LuaFile = new LuaFile();
|
const file: LuaFile = new LuaFile();
|
||||||
let currentBlock: CodeBlock = file;
|
let currentBlock: CodeBlock = file;
|
||||||
|
|
||||||
|
function advanceCursor(number: number = 1) {
|
||||||
|
leftCursor += number;
|
||||||
|
lineCursor += number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newLine() {
|
||||||
|
lineCounter++;
|
||||||
|
lineCursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let currentWord = '';
|
let currentWord = '';
|
||||||
let currentBlockString = '';
|
let currentBlockString = '';
|
||||||
|
|
||||||
@@ -85,10 +96,6 @@ export abstract class CodeBlock {
|
|||||||
currentBlockString += currentChar;
|
currentBlockString += currentChar;
|
||||||
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
|
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
|
||||||
|
|
||||||
if (currentChar === '\n') {
|
|
||||||
lineCounter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentChar === '-' && nextChar === '-') {
|
if (currentChar === '-' && nextChar === '-') {
|
||||||
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
|
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
|
||||||
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
|
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
|
||||||
@@ -97,19 +104,20 @@ export abstract class CodeBlock {
|
|||||||
leftCursor += 3; // Skip the --[
|
leftCursor += 3; // Skip the --[
|
||||||
while (leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')) {
|
while (leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')) {
|
||||||
if (fileContent[leftCursor] === '\n') {
|
if (fileContent[leftCursor] === '\n') {
|
||||||
lineCounter++;
|
newLine();
|
||||||
}
|
}
|
||||||
leftCursor++;
|
advanceCursor()
|
||||||
}
|
}
|
||||||
leftCursor += 2; // Skip the closing ]]
|
advanceCursor(2); // Skip the closing ]]
|
||||||
} else {
|
} else {
|
||||||
// Comment line, skip to end of line
|
// Comment line, skip to end of line
|
||||||
while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
|
while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
}
|
}
|
||||||
if (fileContent[leftCursor] === '\n') {
|
if (fileContent[leftCursor] === '\n') {
|
||||||
lineCounter++;
|
newLine();
|
||||||
}
|
}
|
||||||
|
advanceCursor();
|
||||||
}
|
}
|
||||||
currentWord = '';
|
currentWord = '';
|
||||||
continue;
|
continue;
|
||||||
@@ -117,14 +125,14 @@ export abstract class CodeBlock {
|
|||||||
else if (currentChar === '"' || currentChar === "'") {
|
else if (currentChar === '"' || currentChar === "'") {
|
||||||
// String literal, skip to closing quote
|
// String literal, skip to closing quote
|
||||||
const quoteType = currentChar;
|
const quoteType = currentChar;
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
|
while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
|
||||||
// Add string but don't process it for keywords
|
// Add string but don't process it for keywords
|
||||||
currentBlockString += fileContent[leftCursor];
|
currentBlockString += fileContent[leftCursor];
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
}
|
}
|
||||||
currentBlockString += quoteType; // Add the closing quote
|
currentBlockString += quoteType; // Add the closing quote
|
||||||
leftCursor++; // Skip the closing quote
|
advanceCursor(); // Skip the closing quote
|
||||||
currentWord = '';
|
currentWord = '';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -144,6 +152,9 @@ export abstract class CodeBlock {
|
|||||||
currentBlockString = '';
|
currentBlockString = '';
|
||||||
currentWord = '';
|
currentWord = '';
|
||||||
}
|
}
|
||||||
|
newLine();
|
||||||
|
advanceCursor();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
|
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
|
||||||
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
|
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
|
||||||
@@ -152,6 +163,9 @@ export abstract class CodeBlock {
|
|||||||
currentBlock.childBlocks.push(lineBlock);
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
currentBlockString = '';
|
currentBlockString = '';
|
||||||
|
|
||||||
|
// Update charEnd to include the closing parenthesis
|
||||||
|
(currentBlock as RequireBlock).charEnd = lineCursor + 1;
|
||||||
|
|
||||||
currentBlock = currentBlock.getParentBlock()!;
|
currentBlock = currentBlock.getParentBlock()!;
|
||||||
}
|
}
|
||||||
//Table blocks
|
//Table blocks
|
||||||
@@ -178,7 +192,7 @@ export abstract class CodeBlock {
|
|||||||
currentBlock = tableBlock;
|
currentBlock = tableBlock;
|
||||||
|
|
||||||
let braceCounter = 1;
|
let braceCounter = 1;
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
while (leftCursor < fileContent.length && braceCounter > 0) {
|
while (leftCursor < fileContent.length && braceCounter > 0) {
|
||||||
const char = fileContent[leftCursor];
|
const char = fileContent[leftCursor];
|
||||||
currentBlockString += char;
|
currentBlockString += char;
|
||||||
@@ -194,23 +208,29 @@ export abstract class CodeBlock {
|
|||||||
currentBlock.childBlocks.push(lineBlock);
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
currentBlockString = '';
|
currentBlockString = '';
|
||||||
lineCounter++;
|
lineCounter++;
|
||||||
|
lineCursor = 0;
|
||||||
}
|
}
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find newline or other character
|
// Find newline or other character
|
||||||
let tempCursor = leftCursor;
|
let tempCursor = leftCursor;
|
||||||
|
let tempLineCursor = lineCursor;
|
||||||
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
|
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
|
||||||
currentBlockString += fileContent[tempCursor];
|
currentBlockString += fileContent[tempCursor];
|
||||||
tempCursor++;
|
tempCursor++;
|
||||||
|
tempLineCursor++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
|
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
|
||||||
currentBlockString += '\n';
|
currentBlockString += '\n';
|
||||||
lineCounter++;
|
lineCounter++;
|
||||||
|
lineCursor = 0;
|
||||||
|
leftCursor = tempCursor + 1;
|
||||||
} else if (tempCursor > leftCursor) {
|
} else if (tempCursor > leftCursor) {
|
||||||
// We found whitespace but no newline, so position cursor at last whitespace
|
// We found whitespace but no newline, so position cursor at last whitespace
|
||||||
leftCursor = tempCursor - 1;
|
leftCursor = tempCursor;
|
||||||
|
lineCursor = tempLineCursor;
|
||||||
}
|
}
|
||||||
// else: no whitespace after table, leave leftCursor where it is
|
// else: no whitespace after table, leave leftCursor where it is
|
||||||
|
|
||||||
@@ -279,7 +299,7 @@ export abstract class CodeBlock {
|
|||||||
currentBlock.childBlocks.push(line);
|
currentBlock.childBlocks.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
|
blockToAdd = new RequireBlock(lineCounter, currentBlock, Math.max(0, lineCursor - trimmedWord.length), lineCursor);
|
||||||
currentBlockString = trimmedWord; // Start the require block content with 'require' keyword
|
currentBlockString = trimmedWord; // Start the require block content with 'require' keyword
|
||||||
}
|
}
|
||||||
else if (trimmedWord === 'end') {
|
else if (trimmedWord === 'end') {
|
||||||
@@ -288,7 +308,8 @@ export abstract class CodeBlock {
|
|||||||
onError?.({
|
onError?.({
|
||||||
filePath: luaFilePath,
|
filePath: luaFilePath,
|
||||||
line: fileContent.substring(0, leftCursor).split('\n').length - 1,
|
line: fileContent.substring(0, leftCursor).split('\n').length - 1,
|
||||||
message: "Unexpected 'end' without matching block start"
|
message: "Unexpected 'end' without matching block start",
|
||||||
|
type: CompilationErrorType.Syntax
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
currentBlock = parent;
|
currentBlock = parent;
|
||||||
@@ -302,7 +323,7 @@ export abstract class CodeBlock {
|
|||||||
|
|
||||||
currentWord = '';
|
currentWord = '';
|
||||||
}
|
}
|
||||||
leftCursor++;
|
advanceCursor();
|
||||||
}
|
}
|
||||||
// Handle case where file ends while in a ReturnBlock
|
// Handle case where file ends while in a ReturnBlock
|
||||||
if (currentBlock.blockType === BlockType.Return) {
|
if (currentBlock.blockType === BlockType.Return) {
|
||||||
@@ -548,7 +569,7 @@ export class FunctionBlock extends CodeBlock {
|
|||||||
|
|
||||||
export class RequireBlock extends CodeBlock {
|
export class RequireBlock extends CodeBlock {
|
||||||
|
|
||||||
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number) {
|
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public charEnd?: number) {
|
||||||
super(sourceLineNumber, BlockType.Require, parent);
|
super(sourceLineNumber, BlockType.Require, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,14 @@ export interface CompilationError {
|
|||||||
charStart?: number;
|
charStart?: number;
|
||||||
charEnd?: number;
|
charEnd?: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
type: CompilationErrorType;
|
||||||
|
metaData?: Map<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CompilationErrorType {
|
||||||
|
Syntax = "Syntax",
|
||||||
|
Semantic = "Semantic",
|
||||||
|
Runtime = "Runtime",
|
||||||
|
DependencyCircular = "DependencyCircular",
|
||||||
|
DependencyNotFound = "DependencyNotFound"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks';
|
import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks';
|
||||||
import { CompilationError } from './CompilationError';
|
import { CompilationError, CompilationErrorType } from './CompilationError';
|
||||||
|
|
||||||
export interface ScriptCompilerOptions {
|
export interface ScriptCompilerOptions {
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
@@ -17,7 +17,7 @@ export interface ICompilationLogger {
|
|||||||
writeLine(message: string): void;
|
writeLine(message: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CompilationError };
|
export { CompilationError, CompilationErrorType };
|
||||||
|
|
||||||
class Metrics {
|
class Metrics {
|
||||||
public totalLinesRead : number = 0;
|
public totalLinesRead : number = 0;
|
||||||
@@ -70,7 +70,8 @@ export class ScriptCompiler {
|
|||||||
this.options.onError?.({
|
this.options.onError?.({
|
||||||
filePath: fullPath,
|
filePath: fullPath,
|
||||||
line: 0,
|
line: 0,
|
||||||
message: `Duplicate file key detected: ${key}. This can happen if two files have different capitalization. Lua is case sensitive, but the compiler treats file keys as case insensitive.`
|
message: `Duplicate file key detected: ${key}. This can happen if two files have different capitalization. Lua is case sensitive, but the compiler treats file keys as case insensitive.`,
|
||||||
|
type: CompilationErrorType.Semantic
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -305,7 +306,8 @@ class Writer {
|
|||||||
this.onError({
|
this.onError({
|
||||||
filePath: file.fullPath,
|
filePath: file.fullPath,
|
||||||
line: 0,
|
line: 0,
|
||||||
message: `Circular dependency detected: ${cycle.map(x => x.split('.').pop()).join(' -> ')}`
|
message: `Circular dependency detected: ${cycle.map(x => x.split('.').pop()).join(' -> ')}`,
|
||||||
|
type: CompilationErrorType.DependencyCircular
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -348,7 +350,13 @@ class Writer {
|
|||||||
line: dep.requiredAtLine,
|
line: dep.requiredAtLine,
|
||||||
charStart: dep.charStart,
|
charStart: dep.charStart,
|
||||||
charEnd: dep.charEnd,
|
charEnd: dep.charEnd,
|
||||||
message: `Missing dependency: ${dep.fileKey}`
|
message: `Missing dependency: ${dep.fileKey}`,
|
||||||
|
type: CompilationErrorType.DependencyNotFound,
|
||||||
|
metaData: new Map(
|
||||||
|
[
|
||||||
|
["dependency", dep.fileKey],
|
||||||
|
]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function main() {
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: !production,
|
sourcemap: !production,
|
||||||
sourcesContent: false,
|
sourcesContent: true,
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
outfile: 'dist/extension.js',
|
outfile: 'dist/extension.js',
|
||||||
external: ['vscode'],
|
external: ['vscode'],
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ do --mission table
|
|||||||
---@field properties table
|
---@field properties table
|
||||||
|
|
||||||
---@class Country
|
---@class Country
|
||||||
---@field id string
|
---@field id number
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field vehicle Groups
|
---@field vehicle Groups
|
||||||
---@field plane Groups
|
---@field plane Groups
|
||||||
@@ -123,14 +123,18 @@ do --mission table
|
|||||||
---@class MissionTriggerZone
|
---@class MissionTriggerZone
|
||||||
---@field radius number
|
---@field radius number
|
||||||
---@field zoneId number
|
---@field zoneId number
|
||||||
---@field properties table<string, string>
|
---@field properties Array<TriggerZoneProperty>
|
||||||
---@field hidden boolean
|
---@field hidden boolean
|
||||||
---@field x number
|
---@field x number
|
||||||
---@field y number
|
---@field y number
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field type number
|
---@field type number
|
||||||
---@field heading number
|
---@field heading number
|
||||||
---@field vertices Array<Vec2>
|
---@field verticies Array<Vec2>
|
||||||
|
|
||||||
|
---@class TriggerZoneProperty
|
||||||
|
---@field key string
|
||||||
|
---@field value string
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---@meta LfsTypes
|
||||||
|
|
||||||
|
---@alias lfs.AttributeName
|
||||||
|
---|'dev' -- on Unix systems, this represents the device that the inode resides on. On Windows systems, represents the drive number of the disk containing the file
|
||||||
|
---|'ino' -- on Unix systems, this represents the inode number. On Windows systems this has no meaning
|
||||||
|
---|'mode' -- string representing the associated protection mode (the values could be file, directory, link, socket, named pipe, char device, block device or other)
|
||||||
|
---|'nlink' -- number of hard links to the file
|
||||||
|
---|'uid' -- user-id of owner (Unix only, always 0 on Windows)
|
||||||
|
---|'gid' -- group-id of owner (Unix only, always 0 on Windows)
|
||||||
|
---|'rdev' -- on Unix systems, represents the device type, for special file inodes. On Windows systems represents the same as dev
|
||||||
|
---|'access' -- time of last access
|
||||||
|
---|'modification' -- time of last data modification
|
||||||
|
---|'change' -- time of last file status change
|
||||||
|
---|'size' -- file size, in bytes
|
||||||
|
---|'permissions' -- file permissions string
|
||||||
|
---|'blocks' -- block allocated for file; (Unix only)
|
||||||
|
---|'blksize' -- optimal file system I/O blocksize; (Unix only)
|
||||||
|
|
||||||
|
---@alias lfs.AttributeMode
|
||||||
|
---|'file'
|
||||||
|
---|'directory'
|
||||||
|
---|'link'
|
||||||
|
---|'socket'
|
||||||
|
---|'char device'
|
||||||
|
---|"block device"
|
||||||
|
---|"named pipe"
|
||||||
|
|
||||||
|
---@class lfs.Attributes
|
||||||
|
---@field [lfs.AttributeName] any
|
||||||
|
---@field ['mode'] lfs.AttributeMode
|
||||||
|
|
||||||
|
---@alias lfs.FileMode
|
||||||
|
---| "binary"
|
||||||
|
---| "text"
|
||||||
|
|
||||||
|
---@class lfs.Lock
|
||||||
|
---@field free fun() Releases the lock on the file/directory.
|
||||||
|
|
||||||
|
---@class lfs.DirObject
|
||||||
|
---@field next fun(self: lfs.DirObject): string? Returns a directory entry's name as a string, or `nil` if there are no more entries.
|
||||||
|
---@field close fun(self: lfs.DirObject) Explicitly closes the directory before iteration finishes.
|
||||||
|
|
||||||
|
---@class lfs
|
||||||
|
---@field attributes fun(path:string, result_param: lfs.AttributeName | lfs.Attributes | table): lfs.Attributes? Returns a table with the file attributes corresponding to filepath (or `nil` followed by an error message and a system-dependent error code in case of error). If the second optional argument is given and is a string, then only the value of the named attribute is returned (this use is equivalent to lfs.attributes(filepath)[request_name], but the table is not created and only one attribute is retrieved from the O.S.). if a table is passed as the second argument, it (result_table) is filled with attributes and returned instead of a new table
|
||||||
|
---@field chdir fun(path:string) : boolean?, string? Changes the current working directory to the given path. <br> Returns true in case of success or `nil` plus an error string.
|
||||||
|
---@field lock_dir fun(path:string, seconds_stale: number?) : lfs.Lock?, string? Creates a lockfile (called lockfile.lfs) in path if it does not exist and returns the lock. If the lock already exists checks if it's stale, using the second parameter (default for the second parameter is `INT_MAX`, which in practice means the lock will never be stale. To free the the lock call `lock:free()`. <br>In case of any errors it returns `nil` and the error message. In particular, if the lock exists and is not stale it returns the "File exists" message.
|
||||||
|
---@field currentdir fun(): string?, string? Returns a string with the current working directory or `nil` plus an error string.
|
||||||
|
---@field dir fun(path: string): fun(): string?, lfs.DirObject Lua iterator over the entries of a given directory. Each time the iterator is called with `dir_obj` it returns a directory entry's name as a string, or `nil` if there are no more entries. You can also iterate by calling `dir_obj:next()`, and explicitly close the directory before the iteration finished with `dir_obj:close()`. Raises an error if `path` is not a directory.
|
||||||
|
---@field lock fun(filehandle, mode: string, start?: number, length?: number): boolean?, string? Locks a file or a part of it. The mode can be `'r'` (read/shared lock) or `'w'` (write/exclusive lock). Returns `true` if successful, or `nil` plus an error string in case of error.
|
||||||
|
---@field link fun(old: string, new: string, symlink?: boolean): boolean?, string?, number? Creates a link. If the optional third argument is true, creates a symbolic link; otherwise creates a hard link.
|
||||||
|
---@field mkdir fun(dirname: string): boolean?, string?, number? Creates a new directory. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||||
|
---@field rmdir fun(dirname: string): boolean?, string?, number? Removes an existing directory. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||||
|
---@field setmode fun(file, mode: lfs.FileMode): boolean?, string? Sets the writing mode for a file. The mode can be `'binary'` or `'text'`. Returns `true` followed by the previous mode string, or `nil` followed by an error string in case of error. On non-Windows platforms, setting the mode has no effect and is always returned as `'binary'`.
|
||||||
|
---@field symlinkattributes fun(filepath: string, request_name?: lfs.AttributeName | string): lfs.Attributes? | any Gets information about a symlink itself (not the file it refers to). Identical to `lfs.attributes` but also adds a `target` field containing the filename the symlink points to. On Windows, this is identical to `lfs.attributes`.
|
||||||
|
---@field touch fun(filepath: string, atime?: number, mtime?: number): boolean?, string?, number? Sets access and modification times of a file. Times are in seconds (from `os.time()`). If `mtime` is omitted, `atime` is used; if both are omitted, the current time is used. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||||
|
---@field unlock fun(filehandle, start?: number, length?: number): boolean?, string? Unlocks a file or a part of it. Returns `true` if successful, or `nil` plus an error string in case of error.
|
||||||
|
|
||||||
|
---@class DcsLfs : lfs
|
||||||
|
---@field tempdir fun(): string Returns the DCS temporary directory.
|
||||||
|
---@field writedir fun(): string Returns the Saved Games directory.
|
||||||
|
---@field realpath fun(path: string): string Returns the absolute path of a file.
|
||||||
|
---@field normpath fun(path: string): string Returns the normalized path.
|
||||||
|
---@field md5sum fun(path: string): string Returns the MD5 checksum of the file at the given path.
|
||||||
|
---@field locations fun(): table Returns available drives.
|
||||||
|
|
||||||
|
-- In DCS lfs can be removed, hence the option of it being `nil`.
|
||||||
|
---@type DcsLfs|nil
|
||||||
|
lfs = lfs or nil
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "dutchies-dcs-scripting-tools",
|
"name": "dutchies-dcs-scripting-tools",
|
||||||
"displayName": "Dutchies Dcs Scripting Tools",
|
"displayName": "Dutchies Dcs Scripting Tools",
|
||||||
"description": "Scripting tools to create DCS script and frameworks easier",
|
"description": "Scripting tools to create DCS script and frameworks easier",
|
||||||
"version": "0.1.4",
|
"version": "0.2.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "dutchie031",
|
"name": "dutchie031",
|
||||||
"email": "54616262+dutchie031@users.noreply.github.com"
|
"email": "54616262+dutchie031@users.noreply.github.com"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"dutchies-dcs-scripting-tools.dcsTypes": {
|
"dutchies-dcs-scripting-tools.dcsTypes": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": false,
|
||||||
"description": "Enable or disable DCS Types (adds types for DCS functions and objects)"
|
"description": "Enable or disable DCS Types (adds types for DCS functions and objects)"
|
||||||
},
|
},
|
||||||
"dutchies-dcs-scripting-tools.spearheadTypes": {
|
"dutchies-dcs-scripting-tools.spearheadTypes": {
|
||||||
@@ -64,6 +64,14 @@
|
|||||||
"type":"string",
|
"type":"string",
|
||||||
"default": "${workspaceFolder}/dist",
|
"default": "${workspaceFolder}/dist",
|
||||||
"description": "Where the compiled Lua files will be output. default: ${workspaceFolder}/dist"
|
"description": "Where the compiled Lua files will be output. default: ${workspaceFolder}/dist"
|
||||||
|
},
|
||||||
|
"dutchies-dcs-scripting-tools.globalRequirables": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": [],
|
||||||
|
"description": "List of global requirable Lua scripts that will be included automatically."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,7 +122,6 @@
|
|||||||
"watch": "npm-run-all -p watch:*",
|
"watch": "npm-run-all -p watch:*",
|
||||||
"watch:esbuild": "node esbuild.js --watch",
|
"watch:esbuild": "node esbuild.js --watch",
|
||||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||||
"watch:compiler": "cd ../compiler && npm run watch",
|
|
||||||
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
||||||
"compile-tests": "tsc -p . --outDir out",
|
"compile-tests": "tsc -p . --outDir out",
|
||||||
"watch-tests": "tsc -p . -w --outDir out",
|
"watch-tests": "tsc -p . -w --outDir out",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// The module 'vscode' contains the VS Code extensibility API
|
// The module 'vscode' contains the VS Code extensibility API
|
||||||
// Import the module and reference it with the alias vscode in your code below
|
// Import the module and reference it with the alias vscode in your code below
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
|
import * as path from 'path';
|
||||||
|
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger, CompilationErrorType } from 'dcs-script-compiler';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
import * as luaAddonsManager from './lua-addons-manager';
|
import * as luaAddonsManager from './lua-addons-manager';
|
||||||
|
|
||||||
@@ -9,17 +10,24 @@ const luaWorkSpaceSettingKey = "Lua.workspace";
|
|||||||
const librarySettingsKey = "library";
|
const librarySettingsKey = "library";
|
||||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
||||||
|
|
||||||
|
// Diagnostic codes
|
||||||
|
const DIAGNOSTIC_CODE_MISSING_GLOBAL = 'lua-missing-global-dependency';
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
|
const luaAddonNames: string[] = [ "dcs-types" ];
|
||||||
|
|
||||||
const extensionName = 'dutchies-dcs-scripting-tools';
|
const extensionName = 'dutchies-dcs-scripting-tools';
|
||||||
const publisherName = 'dutchie031';
|
const publisherName = 'dutchie031';
|
||||||
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
||||||
|
|
||||||
|
let luaAddonsManagerInstance: luaAddonsManager.LuaAddonsManager | undefined;
|
||||||
|
|
||||||
// State tracking for addon updates
|
// State tracking for addon updates
|
||||||
let extensionPath: string | undefined;
|
let extensionPath: string | undefined;
|
||||||
let workspaceRoot: string | undefined;
|
let workspaceRoot: string | undefined;
|
||||||
|
let luaAddonsTargetPath: string | undefined;
|
||||||
let currentExtensionVersion: string | undefined;
|
let currentExtensionVersion: string | undefined;
|
||||||
let installedAddonPaths: Map<string, string> = new Map();
|
|
||||||
let isUpdatingAddons = false;
|
let isUpdatingAddons = false;
|
||||||
|
|
||||||
//TODO:
|
//TODO:
|
||||||
@@ -32,9 +40,22 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
extensionPath = context.extensionPath;
|
extensionPath = context.extensionPath;
|
||||||
workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (workspaceRoot === undefined) {
|
||||||
|
vscode.window.showErrorMessage('Workspace root not found. Lua addons manager cannot be initialized.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
luaAddonsTargetPath = path.join(workspaceRoot, '.vscode' , 'lua-addons');
|
||||||
|
luaAddonsManagerInstance = new luaAddonsManager.LuaAddonsManager(luaAddonsTargetPath, context.extensionPath);
|
||||||
|
|
||||||
|
if (!luaAddonsManagerInstance) {
|
||||||
|
vscode.window.showErrorMessage('Failed to initialize Lua addons manager.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize extension version
|
// Initialize extension version
|
||||||
try {
|
try {
|
||||||
currentExtensionVersion = await luaAddonsManager.getExtensionVersion(extensionPath);
|
currentExtensionVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||||
logger.debug(`Extension version: ${currentExtensionVersion}`);
|
logger.debug(`Extension version: ${currentExtensionVersion}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
|
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
@@ -70,6 +91,23 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register code actions provider for quick fixes
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.languages.registerCodeActionsProvider('lua', new LuaQuickFixProvider(), {
|
||||||
|
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register command to add global requirable
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand(
|
||||||
|
'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||||
|
async (dependency: string) => {
|
||||||
|
await addGlobalRequirable(dependency);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
vscode.workspace.onDidSaveTextDocument(async(document) => {
|
vscode.workspace.onDidSaveTextDocument(async(document) => {
|
||||||
logger.info(`Document saved: ${document.uri.fsPath} | Language: ${document.languageId}`);
|
logger.info(`Document saved: ${document.uri.fsPath} | Language: ${document.languageId}`);
|
||||||
if (document.languageId === 'lua') {
|
if (document.languageId === 'lua') {
|
||||||
@@ -164,16 +202,40 @@ async function compileLuaScripts() {
|
|||||||
logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack);
|
logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalRequirables: string[] = config.get<string[]>('globalRequirables') || [];
|
||||||
|
|
||||||
// Update diagnostics
|
// Update diagnostics
|
||||||
diagnosticCollection.clear();
|
diagnosticCollection.clear();
|
||||||
for (const [filePath, errors] of errorsByFile) {
|
for (const [filePath, errors] of errorsByFile) {
|
||||||
const uri = vscode.Uri.file(filePath);
|
const uri = vscode.Uri.file(filePath);
|
||||||
const diagnostics = errors.map(error => {
|
const diagnostics = errors.map(error => {
|
||||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
if (error.type === CompilationErrorType.DependencyNotFound) {
|
||||||
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
const dependencyStr = error.metaData?.get("dependency") ?? undefined;
|
||||||
diagnostic.source = 'DCS Lua Transpiler';
|
if (dependencyStr) {
|
||||||
return diagnostic;
|
if (globalRequirables.includes(dependencyStr)) {
|
||||||
});
|
// Do nothing, it is a globally requirable dependency
|
||||||
|
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||||
|
const diagnostic = new vscode.Diagnostic(range, "Unchecked: Globally marked dependency", vscode.DiagnosticSeverity.Hint);
|
||||||
|
diagnostic.source = 'DCS Lua Transpiler';
|
||||||
|
return diagnostic;
|
||||||
|
} else {
|
||||||
|
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||||
|
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
||||||
|
diagnostic.code = { value: DIAGNOSTIC_CODE_MISSING_GLOBAL, target: vscode.Uri.parse('https://example.com') };
|
||||||
|
diagnostic.source = 'DCS Lua Transpiler';
|
||||||
|
// Store the dependency name for the quick fix to access
|
||||||
|
(diagnostic as any).dependency = dependencyStr;
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined; //Something weird happened, let's ignore it for now.
|
||||||
|
} else {
|
||||||
|
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||||
|
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
||||||
|
diagnostic.source = 'DCS Lua Transpiler';
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
}).filter(diagnostic => diagnostic !== undefined);
|
||||||
diagnosticCollection.set(uri, diagnostics);
|
diagnosticCollection.set(uri, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +248,7 @@ async function enableIntellisense() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await updateLuaAddons();
|
await updateLuaAddons();
|
||||||
|
await addPluginPathsToSettings();
|
||||||
await vscode.commands.executeCommand(
|
await vscode.commands.executeCommand(
|
||||||
"lua.startServer"
|
"lua.startServer"
|
||||||
);
|
);
|
||||||
@@ -199,18 +262,8 @@ async function disableIntellisense() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await removeVersionedPluginPathsFromSettings();
|
await removeVersionedPluginPathsFromSettings();
|
||||||
|
if(luaAddonsManagerInstance){
|
||||||
// Clean up all addon versions from workspace
|
await luaAddonsManagerInstance.removeAllExtensions(luaAddonNames);
|
||||||
if (workspaceRoot) {
|
|
||||||
try {
|
|
||||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath || '');
|
|
||||||
for (const addonName of discoveredAddons.keys()) {
|
|
||||||
await luaAddonsManager.deleteAllVersionsOfAddon(workspaceRoot, addonName);
|
|
||||||
logger.debug(`Deleted all versions of ${addonName} from workspace.`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Failed to clean up addons: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await vscode.commands.executeCommand(
|
await vscode.commands.executeCommand(
|
||||||
@@ -237,41 +290,28 @@ async function updateLuaAddons(): Promise<void> {
|
|||||||
|
|
||||||
isUpdatingAddons = true;
|
isUpdatingAddons = true;
|
||||||
|
|
||||||
|
if (luaAddonsManagerInstance === undefined) {
|
||||||
|
logger.warn('Lua Addons Manager instance is not available.');
|
||||||
|
isUpdatingAddons = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info('Starting lua-addons update...');
|
logger.info('Starting lua-addons update...');
|
||||||
|
|
||||||
// Discover bundled addons
|
for (const addonName of luaAddonNames) {
|
||||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||||
|
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||||
|
|
||||||
if (discoveredAddons.size === 0) {
|
if (packageVersion && packageVersion !== addonVersion) {
|
||||||
logger.info('No lua-addons found in extension bundle.');
|
logger.warn(
|
||||||
isUpdatingAddons = false;
|
`Version mismatch for addon ${addonName}: package=${packageVersion}, installed=${addonVersion}`
|
||||||
return;
|
);
|
||||||
|
|
||||||
|
luaAddonsManagerInstance.removeExtension(addonName);
|
||||||
|
luaAddonsManagerInstance.installExtension(addonName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Discovered ${discoveredAddons.size} lua-addon(s): ${Array.from(discoveredAddons.keys()).join(', ')}`);
|
|
||||||
|
|
||||||
// Copy addons with versioned names
|
|
||||||
const copiedAddons = await luaAddonsManager.copyLuaAddons(
|
|
||||||
extensionPath,
|
|
||||||
workspaceRoot,
|
|
||||||
currentExtensionVersion,
|
|
||||||
discoveredAddons
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store paths for settings management
|
|
||||||
installedAddonPaths = copiedAddons;
|
|
||||||
logger.debug(`Copied ${copiedAddons.size} addon(s) to workspace.`);
|
|
||||||
|
|
||||||
// Clean up old versions
|
|
||||||
for (const addonName of discoveredAddons.keys()) {
|
|
||||||
await luaAddonsManager.deleteOldVersions(workspaceRoot, addonName, currentExtensionVersion);
|
|
||||||
logger.debug(`Cleaned old versions of ${addonName}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Lua settings with new paths
|
|
||||||
await addVersionedPluginPathsToSettings(copiedAddons);
|
|
||||||
|
|
||||||
logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`);
|
logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -304,27 +344,22 @@ async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
|||||||
|
|
||||||
logger.debug('Checking lua-addons versions on startup...');
|
logger.debug('Checking lua-addons versions on startup...');
|
||||||
|
|
||||||
// Get installed addons
|
if (!luaAddonsManagerInstance) {
|
||||||
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
logger.debug('Lua Addons Manager instance is not available, skipping addon version check.');
|
||||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check each discovered addon
|
|
||||||
let updateNeeded = false;
|
let updateNeeded = false;
|
||||||
for (const addonName of discoveredAddons.keys()) {
|
for (const addonName of luaAddonNames) {
|
||||||
const installed = installedAddons.get(addonName);
|
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||||
const installedVersion = installed?.version;
|
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||||
|
if (packageVersion && packageVersion !== addonVersion) {
|
||||||
if (luaAddonsManager.requiresUpdate(installedVersion, currentExtensionVersion)) {
|
|
||||||
logger.info(
|
|
||||||
`Version mismatch for ${addonName}: installed=${installedVersion || 'none'}, current=${currentExtensionVersion}`
|
|
||||||
);
|
|
||||||
updateNeeded = true;
|
updateNeeded = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateNeeded) {
|
if (updateNeeded) {
|
||||||
logger.info('Lua-addons update needed, prompting user...');
|
|
||||||
const userChoice = await vscode.window.showInformationMessage(
|
const userChoice = await vscode.window.showInformationMessage(
|
||||||
'DCS-Types are not up to date with the current extension version. Do you want to update them?',
|
'DCS-Types are not up to date with the current extension version. Do you want to update them?',
|
||||||
{ modal: false },
|
{ modal: false },
|
||||||
@@ -337,17 +372,7 @@ async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
logger.info('User declined DCS-Types update.');
|
logger.info('User declined DCS-Types update.');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.debug('Lua-addons versions are current, no update needed.');
|
|
||||||
// Ensure paths are in settings even if no update needed
|
|
||||||
const pathsMap = new Map<string, string>();
|
|
||||||
for (const [name, info] of installedAddons.entries()) {
|
|
||||||
pathsMap.set(name, info.path);
|
|
||||||
}
|
|
||||||
installedAddonPaths = pathsMap;
|
|
||||||
await addVersionedPluginPathsToSettings(installedAddonPaths);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}`
|
`Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}`
|
||||||
@@ -377,14 +402,12 @@ function convertToWorkspaceFolderPath(absolutePath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds versioned addon paths to the Lua workspace library settings
|
* Adds the lua-addons directory to the Lua workspace library settings.
|
||||||
* @param addonPaths - Map of addon name to installed path
|
* Lua Language Server automatically discovers versioned addons within this directory.
|
||||||
*/
|
*/
|
||||||
async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string>): Promise<void> {
|
async function addPluginPathsToSettings(): Promise<void> {
|
||||||
const pathsToAdd = addonPaths || installedAddonPaths;
|
if (!luaAddonsTargetPath) {
|
||||||
|
logger.warn('Lua addons target path not available.');
|
||||||
if (pathsToAdd.size === 0) {
|
|
||||||
logger.debug('No addon paths to add to settings.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,58 +415,110 @@ async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string
|
|||||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||||
|
|
||||||
// Add each addon path if not already present
|
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||||
for (const addonPath of pathsToAdd.values()) {
|
|
||||||
const workspaceFolderPath = convertToWorkspaceFolderPath(addonPath);
|
|
||||||
if (!librarySettings.includes(workspaceFolderPath)) {
|
|
||||||
librarySettings.push(workspaceFolderPath);
|
|
||||||
logger.debug(`Added addon path to settings: ${workspaceFolderPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
if (!librarySettings.includes(workspaceFolderPath)) {
|
||||||
|
librarySettings.push(workspaceFolderPath);
|
||||||
|
logger.debug(`Added lua-addons path to Lua settings: ${workspaceFolderPath}`);
|
||||||
|
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||||
|
} else {
|
||||||
|
logger.debug('Lua-addons path already in settings.');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to add addon paths to settings: ${err instanceof Error ? err.message : String(err)}`);
|
logger.error(`Failed to add lua-addons path to settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all addon paths from the Lua workspace library settings
|
* Removes the lua-addons directory from the Lua workspace library settings
|
||||||
*/
|
*/
|
||||||
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
|
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
|
||||||
|
if (!luaAddonsTargetPath) {
|
||||||
|
logger.debug('Lua addons target path not available, skipping removal.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||||
|
|
||||||
if (workspaceRoot) {
|
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||||
// Get all installed addons to know what paths to remove
|
const filteredSettings = librarySettings.filter(path => path !== workspaceFolderPath);
|
||||||
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
|
||||||
|
|
||||||
for (const addonInfo of installedAddons.values()) {
|
if (filteredSettings.length !== librarySettings.length) {
|
||||||
// Try both absolute and workspace-relative paths for compatibility
|
await luaSettings.update(librarySettingsKey, filteredSettings, vscode.ConfigurationTarget.Workspace);
|
||||||
const absolutePath = addonInfo.path;
|
logger.debug(`Removed lua-addons path from Lua settings: ${workspaceFolderPath}`);
|
||||||
const relativePath = convertToWorkspaceFolderPath(absolutePath);
|
} else {
|
||||||
|
logger.debug('Lua-addons path not found in settings.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to remove lua-addons path from settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove absolute path if present
|
/**
|
||||||
let index = librarySettings.indexOf(absolutePath);
|
* Code actions provider for Lua quick fixes
|
||||||
if (index !== -1) {
|
*/
|
||||||
librarySettings.splice(index, 1);
|
class LuaQuickFixProvider implements vscode.CodeActionProvider {
|
||||||
logger.debug(`Removed addon path from settings: ${absolutePath}`);
|
provideCodeActions(
|
||||||
}
|
document: vscode.TextDocument,
|
||||||
|
range: vscode.Range | vscode.Selection,
|
||||||
|
context: vscode.CodeActionContext,
|
||||||
|
): vscode.CodeAction[] {
|
||||||
|
const codeActions: vscode.CodeAction[] = [];
|
||||||
|
|
||||||
// Remove relative path if present
|
// Check for missing global dependency diagnostics
|
||||||
index = librarySettings.indexOf(relativePath);
|
for (const diagnostic of context.diagnostics) {
|
||||||
if (index !== -1) {
|
const codeValue = typeof diagnostic.code === 'object' && diagnostic.code !== null
|
||||||
librarySettings.splice(index, 1);
|
? (diagnostic.code as any).value
|
||||||
logger.debug(`Removed addon path from settings: ${relativePath}`);
|
: diagnostic.code;
|
||||||
|
|
||||||
|
if (codeValue === DIAGNOSTIC_CODE_MISSING_GLOBAL) {
|
||||||
|
const dependency = (diagnostic as any).dependency;
|
||||||
|
if (dependency) {
|
||||||
|
const action = new vscode.CodeAction(
|
||||||
|
`Mark '${dependency}' as globally available`,
|
||||||
|
vscode.CodeActionKind.QuickFix
|
||||||
|
);
|
||||||
|
action.command = {
|
||||||
|
title: `Add '${dependency}' to global requireables`,
|
||||||
|
command: 'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||||
|
arguments: [dependency]
|
||||||
|
};
|
||||||
|
action.diagnostics = [diagnostic];
|
||||||
|
codeActions.push(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
return codeActions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a dependency to the global requireables list in workspace settings
|
||||||
|
* @param dependency - The dependency name to add
|
||||||
|
*/
|
||||||
|
async function addGlobalRequirable(dependency: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = vscode.workspace.getConfiguration(extensionName);
|
||||||
|
const globalRequirables = config.get<string[]>('globalRequirables') || [];
|
||||||
|
|
||||||
|
if (!globalRequirables.includes(dependency)) {
|
||||||
|
globalRequirables.push(dependency);
|
||||||
|
await config.update('globalRequirables', globalRequirables, vscode.ConfigurationTarget.Workspace);
|
||||||
|
vscode.window.showInformationMessage(`Added '${dependency}' to global requireables.`);
|
||||||
|
|
||||||
|
// Recompile to update diagnostics
|
||||||
|
await compileLuaScripts();
|
||||||
|
} else {
|
||||||
|
vscode.window.showInformationMessage(`'${dependency}' is already in global requireables.`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to remove addon paths from settings: ${err instanceof Error ? err.message : String(err)}`);
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
throw err;
|
vscode.window.showErrorMessage(`Failed to add global requirable: ${errorMsg}`);
|
||||||
|
logger.error(`Error adding global requirable: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,354 +1,149 @@
|
|||||||
|
import path from "path";
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Manages lua-addons: discovery, versioning, copying, and cleanup.
|
* Lua addon naming conventions:
|
||||||
* All operations assume semver versioning (X.Y.Z format).
|
* <addon-name>.<major>.<minor>.<patch>
|
||||||
* Addon folder names follow the pattern: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
*/
|
||||||
*/
|
|
||||||
|
|
||||||
interface LuaAddonInfo {
|
export class LuaAddonsManager {
|
||||||
name: string;
|
|
||||||
sourceDir: string;
|
/** Path where addons are installed */
|
||||||
version: string;
|
private targetPath: string;
|
||||||
}
|
|
||||||
|
/** Path where addon sources are located */
|
||||||
|
private sourceExtensionPath: string;
|
||||||
|
|
||||||
|
private extensionContextPath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new LuaAddonsManager instance.
|
||||||
|
*
|
||||||
|
* @param extensionPath - Directory where addons are installed
|
||||||
|
* @param sourceExtensionPath - Directory where addon sources are located
|
||||||
|
*/
|
||||||
|
public constructor(luaTargetPath: string, extensionPath: string) {
|
||||||
|
this.targetPath = luaTargetPath;
|
||||||
|
this.sourceExtensionPath = path.join(extensionPath, "lua-addons");
|
||||||
|
this.extensionContextPath = extensionPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the version of an installed addon by name.
|
||||||
|
*
|
||||||
|
* Searches for a folder matching the naming pattern `<name>.<major>.<minor>.<patch>`
|
||||||
|
* and returns the version string.
|
||||||
|
*
|
||||||
|
* @param name - The addon name to search for (without version suffix)
|
||||||
|
* @returns The version string in format "major.minor.patch", or undefined if not found
|
||||||
|
* @throws Never throws, returns undefined if addon not found
|
||||||
|
*/
|
||||||
|
async getExtensionVersion(name: string): Promise<string | undefined> {
|
||||||
|
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||||
|
for (const folder of folders) {
|
||||||
|
if (!folder.isDirectory()) { continue; }
|
||||||
|
|
||||||
|
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (!versionMatch) { continue; }
|
||||||
|
|
||||||
|
const addonName = versionMatch[1];
|
||||||
|
if (addonName === name) {
|
||||||
|
return `${versionMatch[2]}.${versionMatch[3]}.${versionMatch[4]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the version from the package.json file in the extension path.
|
||||||
|
*
|
||||||
|
* @returns The version string from package.json
|
||||||
|
* @throws Error if package.json cannot be read or is missing the version field
|
||||||
|
*/
|
||||||
|
async getPackageVersion(): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.join(this.extensionContextPath,'package.json');
|
||||||
|
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
|
||||||
|
const version = packageJson.version as string | undefined;
|
||||||
|
if (!version) {
|
||||||
|
throw new Error('Version field not found in package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to read extension version: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a single addon by name.
|
||||||
|
*
|
||||||
|
* Deletes the addon directory matching the given name, regardless of version.
|
||||||
|
*
|
||||||
|
* @param name - The addon name to remove (without version suffix)
|
||||||
|
* @throws Error if the removal operation fails
|
||||||
|
*/
|
||||||
|
async removeExtension(name: string): Promise<void> {
|
||||||
|
return this.removeAllExtensions([name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes multiple addons by name.
|
||||||
|
*
|
||||||
|
* Scans the extension directory for folders matching the addon naming pattern
|
||||||
|
* `<name>.<major>.<minor>.<patch>` and removes those whose base name is in the
|
||||||
|
* provided list. Removes all versions of matched addons.
|
||||||
|
*
|
||||||
|
* @param names - Array of addon names to remove (without version suffix)
|
||||||
|
* @throws Error if the removal operation fails
|
||||||
|
*/
|
||||||
|
async removeAllExtensions(names: string[]): Promise<void> {
|
||||||
|
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||||
|
for (const folder of folders) {
|
||||||
|
if (!folder.isDirectory()) { continue; }
|
||||||
|
|
||||||
|
// Check if folder name matches versioning pattern: name.major.minor.patch
|
||||||
|
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (!versionMatch) { continue; }
|
||||||
|
|
||||||
|
// Extract addon name from the versioned folder name
|
||||||
|
const addonName = versionMatch[1];
|
||||||
|
|
||||||
|
if (names.includes(addonName)) {
|
||||||
|
const folderPath = path.join(this.targetPath, folder.name);
|
||||||
|
await fs.rm(folderPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs an addon from source to the target installation directory.
|
||||||
|
*
|
||||||
|
* Copies the addon directory from sourceExtensionPath to extensionPath.
|
||||||
|
* The source directory name should match the addon name.
|
||||||
|
*
|
||||||
|
* @param name - The addon name/directory to install (without version suffix)
|
||||||
|
* @throws Error if source directory not found or copy operation fails
|
||||||
|
*/
|
||||||
|
async installExtension(name: string): Promise<void> {
|
||||||
|
const sourceFolderPath = path.join(this.sourceExtensionPath, name);
|
||||||
|
|
||||||
|
const version = await this.getPackageVersion();
|
||||||
|
const versionedName = `${name}.${version}`;
|
||||||
|
const targetFolderPath = path.join(this.targetPath, versionedName);
|
||||||
|
|
||||||
|
if (!existsSync(sourceFolderPath)) {
|
||||||
|
throw new Error(`Source extension folder not found: ${sourceFolderPath}`);
|
||||||
|
}
|
||||||
|
await fs.mkdir(targetFolderPath, { recursive: true });
|
||||||
|
await fs.cp(sourceFolderPath, targetFolderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
interface InstalledAddonInfo {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the extension's version from package.json
|
|
||||||
* @param extensionRoot - Absolute path to the extension root directory
|
|
||||||
* @returns The semantic version string (e.g., "0.0.5")
|
|
||||||
* @throws Error if package.json cannot be read or version is not found
|
|
||||||
*/
|
|
||||||
export async function getExtensionVersion(extensionRoot: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const packageJsonPath = path.join(extensionRoot, 'package.json');
|
|
||||||
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
||||||
const packageJson = JSON.parse(content);
|
|
||||||
|
|
||||||
const version = packageJson.version as string | undefined;
|
|
||||||
if (!version) {
|
|
||||||
throw new Error('Version field not found in package.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to read extension version: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers all lua-addons in the bundled lua-addons directory
|
|
||||||
* @param extensionPath - Absolute path to the extension installation directory
|
|
||||||
* @returns Map of addon name to source directory path
|
|
||||||
* @throws Error if lua-addons directory cannot be read
|
|
||||||
*/
|
|
||||||
export async function discoverLuaAddons(extensionPath: string): Promise<Map<string, string>> {
|
|
||||||
const luaAddonsDir = path.join(extensionPath, 'lua-addons');
|
|
||||||
const addons = new Map<string, string>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if lua-addons directory exists
|
|
||||||
if (!existsSync(luaAddonsDir)) {
|
|
||||||
return addons; // Empty map if no lua-addons
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const addonName = entry.name;
|
|
||||||
const addonPath = path.join(luaAddonsDir, addonName);
|
|
||||||
addons.set(addonName, addonPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addons;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to discover lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the workspace lua-addons directory path (.vscode/lua-addons)
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @returns Absolute path to .vscode/lua-addons
|
|
||||||
*/
|
|
||||||
export function getWorkspaceLuaAddonsDir(workspaceRoot: string): string {
|
|
||||||
return path.join(workspaceRoot, '.vscode', 'lua-addons');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses installed addon versions from the workspace lua-addons directory
|
|
||||||
* Expects folder names in format: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @returns Map of addon name to version string; returns empty map if directory doesn't exist
|
|
||||||
* @throws Error if directory cannot be read
|
|
||||||
*/
|
|
||||||
export async function getInstalledVersions(
|
|
||||||
workspaceRoot: string
|
|
||||||
): Promise<Map<string, InstalledAddonInfo>> {
|
|
||||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
|
||||||
const installed = new Map<string, InstalledAddonInfo>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Return empty map if directory doesn't exist yet
|
|
||||||
if (!existsSync(luaAddonsDir)) {
|
|
||||||
return installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const folderName = entry.name;
|
|
||||||
const parsed = parseAddonFolderName(folderName);
|
|
||||||
|
|
||||||
if (parsed) {
|
|
||||||
installed.set(parsed.name, {
|
|
||||||
name: parsed.name,
|
|
||||||
version: parsed.version,
|
|
||||||
path: path.join(luaAddonsDir, folderName)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return installed;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to read installed versions: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies lua-addons from the extension bundle to the workspace with versioned folder names
|
|
||||||
* @param extensionPath - Absolute path to the extension installation directory
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @param extensionVersion - The current extension version string
|
|
||||||
* @param addonsToCopy - Map of addon name to source directory (from discoverLuaAddons)
|
|
||||||
* @returns Map of addon name to installed path in workspace
|
|
||||||
* @throws Error if copy fails or directory creation fails
|
|
||||||
*/
|
|
||||||
export async function copyLuaAddons(
|
|
||||||
extensionPath: string,
|
|
||||||
workspaceRoot: string,
|
|
||||||
extensionVersion: string,
|
|
||||||
addonsToCopy: Map<string, string>
|
|
||||||
): Promise<Map<string, string>> {
|
|
||||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
|
||||||
const results = new Map<string, string>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure .vscode directory exists
|
|
||||||
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
|
||||||
await ensureDirectoryExists(vscodeDir);
|
|
||||||
|
|
||||||
// Ensure lua-addons directory exists
|
|
||||||
await ensureDirectoryExists(luaAddonsDir);
|
|
||||||
|
|
||||||
// Copy each addon
|
|
||||||
for (const [addonName, sourceDir] of addonsToCopy) {
|
|
||||||
const versionedFolderName = `${addonName}.${extensionVersion}`;
|
|
||||||
const destDir = path.join(luaAddonsDir, versionedFolderName);
|
|
||||||
|
|
||||||
// Remove destination if it already exists (shouldn't happen, but be safe)
|
|
||||||
if (existsSync(destDir)) {
|
|
||||||
await fs.rm(destDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the addon
|
|
||||||
await copyDirectory(sourceDir, destDir);
|
|
||||||
results.set(addonName, destDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to copy lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all old versions of a specific addon
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @param addonName - Name of the addon (e.g., "dcs-types")
|
|
||||||
* @param currentVersion - Version to keep (e.g., "0.0.5")
|
|
||||||
* @throws Error if directory operations fail
|
|
||||||
*/
|
|
||||||
export async function deleteOldVersions(
|
|
||||||
workspaceRoot: string,
|
|
||||||
addonName: string,
|
|
||||||
currentVersion: string
|
|
||||||
): Promise<void> {
|
|
||||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Return silently if directory doesn't exist
|
|
||||||
if (!existsSync(luaAddonsDir)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const parsed = parseAddonFolderName(entry.name);
|
|
||||||
|
|
||||||
// Delete if it's an old version of this addon
|
|
||||||
if (parsed && parsed.name === addonName && parsed.version !== currentVersion) {
|
|
||||||
const oldPath = path.join(luaAddonsDir, entry.name);
|
|
||||||
await fs.rm(oldPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to delete old versions of ${addonName}: ${
|
|
||||||
err instanceof Error ? err.message : String(err)
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an addon needs to be updated (version mismatch)
|
|
||||||
* @param installedVersion - Currently installed version or undefined if not installed
|
|
||||||
* @param currentExtensionVersion - Current extension version to compare against
|
|
||||||
* @returns true if no version is installed or version doesn't match
|
|
||||||
*/
|
|
||||||
export function requiresUpdate(
|
|
||||||
installedVersion: string | undefined,
|
|
||||||
currentExtensionVersion: string
|
|
||||||
): boolean {
|
|
||||||
return !installedVersion || installedVersion !== currentExtensionVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all versioned addon folders for a specific addon name
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @param addonName - Name of the addon to clean up completely
|
|
||||||
* @throws Error if directory operations fail
|
|
||||||
*/
|
|
||||||
export async function deleteAllVersionsOfAddon(
|
|
||||||
workspaceRoot: string,
|
|
||||||
addonName: string
|
|
||||||
): Promise<void> {
|
|
||||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Return silently if directory doesn't exist
|
|
||||||
if (!existsSync(luaAddonsDir)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const parsed = parseAddonFolderName(entry.name);
|
|
||||||
|
|
||||||
// Delete all versions of this addon
|
|
||||||
if (parsed && parsed.name === addonName) {
|
|
||||||
const addonPath = path.join(luaAddonsDir, entry.name);
|
|
||||||
await fs.rm(addonPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to delete all versions of ${addonName}: ${
|
|
||||||
err instanceof Error ? err.message : String(err)
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the currently installed addon info for all addons
|
|
||||||
* @param workspaceRoot - Absolute path to the workspace root
|
|
||||||
* @returns Map of addon name to its latest installed version info
|
|
||||||
*/
|
|
||||||
export async function getInstalledAddons(
|
|
||||||
workspaceRoot: string
|
|
||||||
): Promise<Map<string, InstalledAddonInfo>> {
|
|
||||||
return getInstalledVersions(workspaceRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Private Helper Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses an addon folder name in format `<name>.<version>`
|
|
||||||
* @param folderName - The folder name to parse
|
|
||||||
* @returns Object with name and version, or null if format doesn't match
|
|
||||||
*/
|
|
||||||
function parseAddonFolderName(
|
|
||||||
folderName: string
|
|
||||||
): { name: string; version: string } | null {
|
|
||||||
// Split only on the last dot to handle addon names with dots (unlikely, but safe)
|
|
||||||
const lastDotIndex = folderName.lastIndexOf('.');
|
|
||||||
if (lastDotIndex === -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = folderName.substring(0, lastDotIndex);
|
|
||||||
const version = folderName.substring(lastDotIndex + 1);
|
|
||||||
|
|
||||||
// Validate semver format (basic check)
|
|
||||||
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name, version };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures a directory exists, creating it if necessary
|
|
||||||
* @param dirPath - Absolute path to the directory
|
|
||||||
*/
|
|
||||||
async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await fs.mkdir(dirPath, { recursive: true });
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore EEXIST errors
|
|
||||||
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively copies a directory and its contents
|
|
||||||
* @param srcDir - Source directory path
|
|
||||||
* @param destDir - Destination directory path
|
|
||||||
*/
|
|
||||||
async function copyDirectory(srcDir: string, destDir: string): Promise<void> {
|
|
||||||
await ensureDirectoryExists(destDir);
|
|
||||||
|
|
||||||
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const srcPath = path.join(srcDir, entry.name);
|
|
||||||
const destPath = path.join(destDir, entry.name);
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
await copyDirectory(srcPath, destPath);
|
|
||||||
} else {
|
|
||||||
await fs.copyFile(srcPath, destPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,14 @@ import * as fs from 'fs/promises';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import * as luaAddonsManager from '../lua-addons-manager';
|
import { LuaAddonsManager } from '../lua-addons-manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for lua-addons-manager
|
* Integration tests for LuaAddonsManager
|
||||||
* Uses temporary directories to avoid polluting the file system
|
* Uses temporary directories to avoid polluting the file system
|
||||||
*/
|
*/
|
||||||
|
|
||||||
suite('lua-addons-manager', () => {
|
suite('LuaAddonsManager', () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
||||||
suiteSetup(async () => {
|
suiteSetup(async () => {
|
||||||
@@ -24,287 +24,232 @@ suite('lua-addons-manager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
suite('getExtensionVersion', () => {
|
suite('getExtensionVersion', () => {
|
||||||
test('should read version from package.json', async () => {
|
test('should return undefined if addon not found', async () => {
|
||||||
const testDir = path.join(tempDir, 'getExtensionVersion-1');
|
const extensionDir = path.join(tempDir, 'getExtensionVersion-1');
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
const sourceDir = path.join(tempDir, 'getExtensionVersion-1-source');
|
||||||
const packageJson = path.join(testDir, 'package.json');
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
await fs.writeFile(packageJson, JSON.stringify({ version: '1.2.3' }));
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
|
||||||
const version = await luaAddonsManager.getExtensionVersion(testDir);
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
const version = await manager.getExtensionVersion('nonexistent');
|
||||||
|
|
||||||
|
assert.strictEqual(version, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return version string for installed addon', async () => {
|
||||||
|
const extensionDir = path.join(tempDir, 'getExtensionVersion-2');
|
||||||
|
const sourceDir = path.join(tempDir, 'getExtensionVersion-2-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'dcs-types.1.2.3'), { recursive: true });
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
const version = await manager.getExtensionVersion('dcs-types');
|
||||||
|
|
||||||
assert.strictEqual(version, '1.2.3');
|
assert.strictEqual(version, '1.2.3');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error if package.json not found', async () => {
|
test('should return first matching version if multiple versions exist', async () => {
|
||||||
const testDir = path.join(tempDir, 'getExtensionVersion-2');
|
const extensionDir = path.join(tempDir, 'getExtensionVersion-3');
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
const sourceDir = path.join(tempDir, 'getExtensionVersion-3-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon.1.0.0'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon.2.0.0'), { recursive: true });
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
const version = await manager.getExtensionVersion('addon');
|
||||||
|
|
||||||
|
// Should return one of the versions (first found)
|
||||||
|
assert.match(version!, /^[12]\.0\.0$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getPackageVersion', () => {
|
||||||
|
test('should read version from package.json', async () => {
|
||||||
|
const extensionDir = path.join(tempDir, 'getPackageVersion-1');
|
||||||
|
const sourceDir = path.join(tempDir, 'getPackageVersion-1-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
const packageJson = path.join(extensionDir, 'package.json');
|
||||||
|
await fs.writeFile(packageJson, JSON.stringify({ version: '2.1.0' }));
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
const version = await manager.getPackageVersion();
|
||||||
|
|
||||||
|
assert.strictEqual(version, '2.1.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error if package.json not found', async () => {
|
||||||
|
const extensionDir = path.join(tempDir, 'getPackageVersion-2');
|
||||||
|
const sourceDir = path.join(tempDir, 'getPackageVersion-2-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => luaAddonsManager.getExtensionVersion(testDir),
|
() => manager.getPackageVersion(),
|
||||||
/Failed to read extension version/
|
/Failed to read extension version/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error if version field missing', async () => {
|
test('should throw error if version field missing', async () => {
|
||||||
const testDir = path.join(tempDir, 'getExtensionVersion-3');
|
const extensionDir = path.join(tempDir, 'getPackageVersion-3');
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
const sourceDir = path.join(tempDir, 'getPackageVersion-3-source');
|
||||||
const packageJson = path.join(testDir, 'package.json');
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
const packageJson = path.join(extensionDir, 'package.json');
|
||||||
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
|
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => luaAddonsManager.getExtensionVersion(testDir),
|
() => manager.getPackageVersion(),
|
||||||
/Version field not found/
|
/Version field not found/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('discoverLuaAddons', () => {
|
suite('removeExtension', () => {
|
||||||
test('should discover lua-addons directories', async () => {
|
test('should remove addon by name', async () => {
|
||||||
const testDir = path.join(tempDir, 'discoverLuaAddons-1');
|
const extensionDir = path.join(tempDir, 'removeExtension-1');
|
||||||
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
const sourceDir = path.join(tempDir, 'removeExtension-1-source');
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2'), { recursive: true });
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||||
|
|
||||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
await manager.removeExtension('addon1');
|
||||||
|
|
||||||
assert.strictEqual(addons.size, 2);
|
const remaining = await fs.readdir(extensionDir);
|
||||||
assert.strictEqual(addons.has('addon1'), true);
|
assert.strictEqual(remaining.length, 0);
|
||||||
assert.strictEqual(addons.has('addon2'), true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return empty map if lua-addons directory does not exist', async () => {
|
test('should not affect other addons', async () => {
|
||||||
const testDir = path.join(tempDir, 'discoverLuaAddons-2');
|
const extensionDir = path.join(tempDir, 'removeExtension-2');
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
const sourceDir = path.join(tempDir, 'removeExtension-2-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||||
|
|
||||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
await manager.removeExtension('addon1');
|
||||||
|
|
||||||
assert.strictEqual(addons.size, 0);
|
const remaining = await fs.readdir(extensionDir);
|
||||||
});
|
assert.strictEqual(remaining.length, 1);
|
||||||
|
assert.strictEqual(remaining[0], 'addon2.1.0.0');
|
||||||
test('should ignore non-directory entries', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'discoverLuaAddons-3');
|
|
||||||
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
|
||||||
await fs.mkdir(luaAddonsDir, { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
|
||||||
await fs.writeFile(path.join(luaAddonsDir, 'file.txt'), 'test');
|
|
||||||
|
|
||||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
|
||||||
|
|
||||||
assert.strictEqual(addons.size, 1);
|
|
||||||
assert.strictEqual(addons.has('addon1'), true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('getWorkspaceLuaAddonsDir', () => {
|
suite('removeAllExtensions', () => {
|
||||||
test('should return correct .vscode/lua-addons path', () => {
|
test('should remove multiple addons by name', async () => {
|
||||||
const workspaceRoot = '/path/to/workspace';
|
const extensionDir = path.join(tempDir, 'removeAllExtensions-1');
|
||||||
const result = luaAddonsManager.getWorkspaceLuaAddonsDir(workspaceRoot);
|
const sourceDir = path.join(tempDir, 'removeAllExtensions-1-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon3.1.0.0'), { recursive: true });
|
||||||
|
|
||||||
assert.strictEqual(result, path.join(workspaceRoot, '.vscode', 'lua-addons'));
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
});
|
await manager.removeAllExtensions(['addon1', 'addon2']);
|
||||||
});
|
|
||||||
|
|
||||||
suite('getInstalledVersions', () => {
|
const remaining = await fs.readdir(extensionDir);
|
||||||
test('should parse installed addon versions from folder names', async () => {
|
assert.deepStrictEqual(remaining.sort(), ['addon3.1.0.0']);
|
||||||
const testDir = path.join(tempDir, 'getInstalledVersions-1');
|
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'mission-utils.1.2.3'), { recursive: true });
|
|
||||||
|
|
||||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
|
||||||
|
|
||||||
assert.strictEqual(installed.size, 2);
|
|
||||||
assert.strictEqual(installed.has('dcs-types'), true);
|
|
||||||
assert.strictEqual(installed.has('mission-utils'), true);
|
|
||||||
assert.strictEqual(installed.get('dcs-types')?.version, '0.0.5');
|
|
||||||
assert.strictEqual(installed.get('mission-utils')?.version, '1.2.3');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return empty map if directory does not exist', async () => {
|
test('should delete all versions of matching addons', async () => {
|
||||||
const testDir = path.join(tempDir, 'getInstalledVersions-2');
|
const extensionDir = path.join(tempDir, 'removeAllExtensions-2');
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
const sourceDir = path.join(tempDir, 'removeAllExtensions-2-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.1'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.2'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.3'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||||
|
|
||||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
await manager.removeAllExtensions(['addon1']);
|
||||||
|
|
||||||
assert.strictEqual(installed.size, 0);
|
const remaining = await fs.readdir(extensionDir);
|
||||||
|
assert.deepStrictEqual(remaining.sort(), ['addon2.1.0.0']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should ignore folders with invalid version format', async () => {
|
test('should ignore folders with invalid version format', async () => {
|
||||||
const testDir = path.join(tempDir, 'getInstalledVersions-3');
|
const extensionDir = path.join(tempDir, 'removeAllExtensions-3');
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
const sourceDir = path.join(tempDir, 'removeAllExtensions-3-source');
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'invalid-addon'), { recursive: true });
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'bad-format.v1'), { recursive: true });
|
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(extensionDir, 'invalid-folder'), { recursive: true });
|
||||||
|
|
||||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
await manager.removeAllExtensions(['addon1']);
|
||||||
|
|
||||||
assert.strictEqual(installed.size, 1);
|
const remaining = await fs.readdir(extensionDir);
|
||||||
assert.strictEqual(installed.has('dcs-types'), true);
|
assert.deepStrictEqual(remaining.sort(), ['invalid-folder']);
|
||||||
assert.strictEqual(installed.has('invalid-addon'), false);
|
});
|
||||||
assert.strictEqual(installed.has('bad-format'), false);
|
|
||||||
|
test('should not throw if addon does not exist', async () => {
|
||||||
|
const extensionDir = path.join(tempDir, 'removeAllExtensions-4');
|
||||||
|
const sourceDir = path.join(tempDir, 'removeAllExtensions-4-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
|
||||||
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
|
// Should not throw
|
||||||
|
await manager.removeAllExtensions(['nonexistent']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('copyLuaAddons', () => {
|
suite('installExtension', () => {
|
||||||
test('should copy addons with versioned folder names', async () => {
|
test('should copy addon from source to target', async () => {
|
||||||
const testDir = path.join(tempDir, 'copyLuaAddons-1');
|
const extensionDir = path.join(tempDir, 'installExtension-1');
|
||||||
const sourceAddon1 = path.join(testDir, 'source-addons', 'addon1');
|
const sourceDir = path.join(tempDir, 'installExtension-1-source');
|
||||||
await fs.mkdir(sourceAddon1, { recursive: true });
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
await fs.writeFile(path.join(sourceAddon1, 'file1.lua'), 'content1');
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||||
|
await fs.mkdir(sourceAddon, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'content');
|
||||||
|
|
||||||
const addonsToCopy = new Map<string, string>([
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
['addon1', sourceAddon1]
|
await manager.installExtension('addon1');
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await luaAddonsManager.copyLuaAddons(
|
const targetAddon = path.join(extensionDir, 'addon1');
|
||||||
testDir,
|
assert.strictEqual(existsSync(targetAddon), true);
|
||||||
testDir,
|
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||||
'1.0.0',
|
|
||||||
addonsToCopy
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(result.size, 1);
|
|
||||||
const copiedPath = result.get('addon1');
|
|
||||||
assert.strictEqual(copiedPath !== undefined, true);
|
|
||||||
if (copiedPath) {
|
|
||||||
assert.strictEqual(copiedPath.includes('addon1.1.0.0'), true);
|
|
||||||
assert.strictEqual(existsSync(path.join(copiedPath, 'file1.lua')), true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create .vscode directory if it does not exist', async () => {
|
test('should throw error if source directory does not exist', async () => {
|
||||||
const testDir = path.join(tempDir, 'copyLuaAddons-2');
|
const extensionDir = path.join(tempDir, 'installExtension-2');
|
||||||
const sourceAddon = path.join(testDir, 'source', 'addon1');
|
const sourceDir = path.join(tempDir, 'installExtension-2-source');
|
||||||
await fs.mkdir(sourceAddon, { recursive: true });
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
|
||||||
const addonsToCopy = new Map<string, string>([
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
['addon1', sourceAddon]
|
await assert.rejects(
|
||||||
]);
|
() => manager.installExtension('nonexistent'),
|
||||||
|
/Source extension folder not found/
|
||||||
await luaAddonsManager.copyLuaAddons(
|
|
||||||
testDir,
|
|
||||||
testDir,
|
|
||||||
'1.0.0',
|
|
||||||
addonsToCopy
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const vscodeDir = path.join(testDir, '.vscode');
|
|
||||||
assert.strictEqual(existsSync(vscodeDir), true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should copy nested directories recursively', async () => {
|
test('should copy nested directories recursively', async () => {
|
||||||
const testDir = path.join(tempDir, 'copyLuaAddons-3');
|
const extensionDir = path.join(tempDir, 'installExtension-3');
|
||||||
const sourceAddon = path.join(testDir, 'source', 'addon1');
|
const sourceDir = path.join(tempDir, 'installExtension-3-source');
|
||||||
|
await fs.mkdir(extensionDir, { recursive: true });
|
||||||
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
|
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||||
await fs.mkdir(path.join(sourceAddon, 'subdir'), { recursive: true });
|
await fs.mkdir(path.join(sourceAddon, 'subdir'), { recursive: true });
|
||||||
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'root');
|
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'root');
|
||||||
await fs.writeFile(path.join(sourceAddon, 'subdir', 'file.lua'), 'nested');
|
await fs.writeFile(path.join(sourceAddon, 'subdir', 'file.lua'), 'nested');
|
||||||
|
|
||||||
const addonsToCopy = new Map<string, string>([
|
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||||
['addon1', sourceAddon]
|
await manager.installExtension('addon1');
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await luaAddonsManager.copyLuaAddons(
|
const targetAddon = path.join(extensionDir, 'addon1');
|
||||||
testDir,
|
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||||
testDir,
|
assert.strictEqual(existsSync(path.join(targetAddon, 'subdir', 'file.lua')), true);
|
||||||
'1.0.0',
|
|
||||||
addonsToCopy
|
|
||||||
);
|
|
||||||
|
|
||||||
const copiedPath = result.get('addon1')!;
|
|
||||||
assert.strictEqual(existsSync(path.join(copiedPath, 'file.lua')), true);
|
|
||||||
assert.strictEqual(existsSync(path.join(copiedPath, 'subdir', 'file.lua')), true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('deleteOldVersions', () => {
|
|
||||||
test('should delete old versions of an addon', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'deleteOldVersions-1');
|
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.3'), { recursive: true });
|
|
||||||
|
|
||||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.3');
|
|
||||||
|
|
||||||
const remaining = await fs.readdir(luaAddonsDir);
|
|
||||||
assert.deepStrictEqual(remaining.sort(), ['addon1.0.0.3']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not delete other addons', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'deleteOldVersions-2');
|
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
|
||||||
|
|
||||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.2');
|
|
||||||
|
|
||||||
const remaining = await fs.readdir(luaAddonsDir);
|
|
||||||
assert.strictEqual(remaining.includes('addon1.0.0.2'), true);
|
|
||||||
assert.strictEqual(remaining.includes('addon2.0.0.1'), true);
|
|
||||||
assert.strictEqual(remaining.includes('addon1.0.0.1'), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle missing directory gracefully', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'deleteOldVersions-3');
|
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
|
||||||
|
|
||||||
// Should not throw
|
|
||||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('deleteAllVersionsOfAddon', () => {
|
|
||||||
test('should delete all versions of an addon', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'deleteAllVersionsOfAddon-1');
|
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
|
||||||
|
|
||||||
await luaAddonsManager.deleteAllVersionsOfAddon(testDir, 'addon1');
|
|
||||||
|
|
||||||
const remaining = await fs.readdir(luaAddonsDir);
|
|
||||||
assert.deepStrictEqual(remaining.sort(), ['addon2.0.0.1']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('requiresUpdate', () => {
|
|
||||||
test('should return true if no version installed', () => {
|
|
||||||
const result = luaAddonsManager.requiresUpdate(undefined, '1.0.0');
|
|
||||||
|
|
||||||
assert.strictEqual(result, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return true if versions do not match', () => {
|
|
||||||
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.1');
|
|
||||||
|
|
||||||
assert.strictEqual(result, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false if versions match', () => {
|
|
||||||
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.0');
|
|
||||||
|
|
||||||
assert.strictEqual(result, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('getInstalledAddons', () => {
|
|
||||||
test('should return installed addon information', async () => {
|
|
||||||
const testDir = path.join(tempDir, 'getInstalledAddons-1');
|
|
||||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
|
||||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
|
||||||
|
|
||||||
const installed = await luaAddonsManager.getInstalledAddons(testDir);
|
|
||||||
|
|
||||||
assert.strictEqual(installed.size, 1);
|
|
||||||
const addonInfo = installed.get('dcs-types');
|
|
||||||
assert.strictEqual(addonInfo !== undefined, true);
|
|
||||||
if (addonInfo) {
|
|
||||||
assert.strictEqual(addonInfo.name, 'dcs-types');
|
|
||||||
assert.strictEqual(addonInfo.version, '0.0.5');
|
|
||||||
assert.strictEqual(addonInfo.path.includes('dcs-types.0.0.5'), true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gh-scripting-compiler",
|
"name": "gh-scripting-compiler",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node esbuild.js",
|
"build": "node esbuild.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gh-lua-addon-installer",
|
"name": "gh-lua-addon-installer",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node esbuild.js",
|
"build": "node esbuild.js",
|
||||||
|
|||||||
Generated
+1
-6
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dutchies-dcs-scripting-tools": {
|
"dutchies-dcs-scripting-tools": {
|
||||||
"version": "0.0.5",
|
"version": "0.1.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "10.0.10",
|
"@types/mocha": "10.0.10",
|
||||||
@@ -1449,7 +1449,6 @@
|
|||||||
"integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==",
|
"integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.70.0",
|
"@typescript-eslint/scope-manager": "8.70.0",
|
||||||
"@typescript-eslint/types": "8.70.0",
|
"@typescript-eslint/types": "8.70.0",
|
||||||
@@ -1734,7 +1733,6 @@
|
|||||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2671,7 +2669,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5771,7 +5768,6 @@
|
|||||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5902,7 +5898,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
Reference in New Issue
Block a user