Adds a new section "Scripting" in the server UI where PowerShell modules can be defined. These can be used for defining a library of functions that can be used in any script. These modules are automatically synced to the launcher and imported upon script execution.
594 lines
26 KiB
TypeScript
594 lines
26 KiB
TypeScript
import { cmdlets, builtinCmdlets, CmdletDefinition, variableTypes, scriptTypeValues } from "./PowerShellCompletions.g";
|
|
|
|
// Merge LANCommander cmdlets (shown first) with built-in PowerShell cmdlets
|
|
const allCmdlets: CmdletDefinition[] = [...cmdlets, ...builtinCmdlets];
|
|
|
|
declare const monaco: any;
|
|
|
|
// Build a lookup from type name to its properties
|
|
const typeMap = new Map(variableTypes.map((t) => [t.name, t]));
|
|
|
|
interface VariableDefinition {
|
|
name: string;
|
|
type: string;
|
|
description: string;
|
|
scriptTypes?: string[];
|
|
}
|
|
|
|
// Variables available across all client-side game script types
|
|
const gameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"];
|
|
|
|
const variables: VariableDefinition[] = [
|
|
{ name: "$InstallDirectory", type: "string", description: "Root install directory for the game", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$WorkingDirectory", type: "string", description: "Current working directory for the script" },
|
|
{ name: "$ServerAddress", type: "string", description: "Address of the LANCommander server", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$DefaultInstallDirectory", type: "string", description: "Default installation directory", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$GameManifest", type: "GameManifest", description: "The game's manifest object containing metadata such as title, ID, sort title, description, notes, and related collections", scriptTypes: ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"] },
|
|
{ name: "$PlayerAlias", type: "string", description: "Current player's alias/display name", scriptTypes: ["BeforeStart", "AfterStop"] },
|
|
{ name: "$NewPlayerAlias", type: "string", description: "New player alias (name change scripts)", scriptTypes: ["NameChange"] },
|
|
{ name: "$OldPlayerAlias", type: "string", description: "Previous player alias (name change scripts)", scriptTypes: ["NameChange"] },
|
|
{ name: "$AllocatedKey", type: "string", description: "The product/serial key allocated to the current player for this game (key allocation scripts)", scriptTypes: ["KeyChange"] },
|
|
{ name: "$ScriptType", type: "ScriptType", description: "Type of the script being executed" },
|
|
{ name: "$Server", type: "Server", description: "Server model object with properties: Id, Name, Host, Port, Game, and WorkingDirectory", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$Game", type: "Game", description: "Game model object with properties: Id, Title, SortTitle, Description, and Notes", scriptTypes: ["Package", "GameStarted", "GameStopped"] },
|
|
{ name: "$User", type: "User", description: "User model object with properties: Id, UserName, and Alias", scriptTypes: ["GameStarted", "GameStopped", "UserRegistration", "UserLogin"] },
|
|
{ name: "$ToolManifest", type: "ToolManifest", description: "Tool manifest object containing metadata for the current tool", scriptTypes: ["Install", "BeforeStart", "AfterStop", "DetectInstall"] },
|
|
{ name: "$Tool", type: "Tool", description: "Tool model object with properties: Id, Name, and Description", scriptTypes: ["Package"] },
|
|
{ name: "$RedistributableManifest", type: "RedistributableManifest", description: "Redistributable manifest object containing metadata for the current redistributable", scriptTypes: ["Install", "BeforeStart", "AfterStop", "NameChange", "DetectInstall", "RunWrapper"] },
|
|
{ name: "$ExecutablePath", type: "string", description: "Resolved path to the game executable", scriptTypes: ["RunWrapper"] },
|
|
{ name: "$Arguments", type: "string", description: "Resolved command-line arguments for the executable", scriptTypes: ["RunWrapper"] },
|
|
{ name: "$Redistributable", type: "Redistributable", description: "Redistributable model object with properties: Id, Name, and Description", scriptTypes: ["Package"] },
|
|
{ name: "$DisplayWidth", type: "string", description: "Primary display width in pixels", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$DisplayHeight", type: "string", description: "Primary display height in pixels", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$DisplayRefreshRate", type: "string", description: "Primary display refresh rate in Hz", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$DisplayBitDepth", type: "string", description: "Primary display bit depth (bits per pixel)", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$IPXRelayHost", type: "string", description: "IPX relay server hostname (when IPX relay is configured)", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$IPXRelayPort", type: "string", description: "IPX relay server port (when IPX relay is configured)", scriptTypes: gameClientScriptTypes },
|
|
{ name: "$ServerId", type: "string", description: "Server GUID identifier", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$ServerName", type: "string", description: "Server display name", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$ServerHost", type: "string", description: "Server hostname", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$ServerPort", type: "string", description: "Server port number", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$GameTitle", type: "string", description: "Game title", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
{ name: "$GameId", type: "string", description: "Game GUID identifier", scriptTypes: ["BeforeStart", "AfterStop", "GameStarted", "GameStopped"] },
|
|
];
|
|
|
|
interface ModuleFunctionDefinition {
|
|
name: string;
|
|
synopsis: string | null;
|
|
module: string | null;
|
|
}
|
|
|
|
let registered = false;
|
|
let currentScriptType: string | null = null;
|
|
let moduleFunctions: ModuleFunctionDefinition[] = [];
|
|
|
|
export function registerPowerShellCompletions(): void {
|
|
if (registered || typeof monaco === "undefined") return;
|
|
registered = true;
|
|
|
|
registerCompletionProvider();
|
|
registerHoverProvider();
|
|
}
|
|
|
|
export function setScriptType(scriptType: string | null): void {
|
|
currentScriptType = scriptType;
|
|
}
|
|
|
|
export function setModuleFunctions(functions: ModuleFunctionDefinition[]): void {
|
|
moduleFunctions = functions ?? [];
|
|
}
|
|
|
|
function getFilteredVariables(): VariableDefinition[] {
|
|
if (!currentScriptType) return variables;
|
|
|
|
return variables.filter((v) => {
|
|
// Variables without scriptTypes are always available (e.g. $WorkingDirectory, $ScriptType)
|
|
if (!v.scriptTypes) return true;
|
|
return v.scriptTypes.includes(currentScriptType!);
|
|
});
|
|
}
|
|
|
|
function registerCompletionProvider(): void {
|
|
monaco.languages.registerCompletionItemProvider("powershell", {
|
|
triggerCharacters: ["-", "$", "."],
|
|
provideCompletionItems(model: any, position: any) {
|
|
const word = model.getWordUntilPosition(position);
|
|
const range = {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: word.startColumn,
|
|
endColumn: word.endColumn,
|
|
};
|
|
|
|
const lineContent: string = model.getLineContent(position.lineNumber);
|
|
const textBefore = lineContent.substring(0, position.column - 1);
|
|
|
|
const suggestions: any[] = [];
|
|
const filtered = getFilteredVariables();
|
|
|
|
// Member access completions: triggered by . after a variable
|
|
const memberMatch = textBefore.match(/\$(\w+)\.(\w*)$/);
|
|
if (memberMatch) {
|
|
const varName = "$" + memberMatch[1];
|
|
const variable = filtered.find((v) => v.name === varName);
|
|
if (variable) {
|
|
const typeDef = typeMap.get(variable.type);
|
|
if (typeDef) {
|
|
for (const prop of typeDef.properties) {
|
|
suggestions.push({
|
|
label: prop.name,
|
|
kind: monaco.languages.CompletionItemKind.Field,
|
|
insertText: prop.name,
|
|
detail: `[${prop.type}]`,
|
|
documentation: `Property of ${variable.type}`,
|
|
range,
|
|
});
|
|
}
|
|
return { suggestions };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Variable completions: triggered by $
|
|
if (/\$\w*$/.test(textBefore)) {
|
|
const varRange = {
|
|
...range,
|
|
startColumn: textBefore.lastIndexOf("$") + 1,
|
|
};
|
|
|
|
for (const v of filtered) {
|
|
suggestions.push({
|
|
label: v.name,
|
|
kind: monaco.languages.CompletionItemKind.Variable,
|
|
insertText: v.name.substring(1),
|
|
detail: `[${v.type}]`,
|
|
documentation: v.description,
|
|
range: varRange,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Parameter completions: triggered by - after a cmdlet name
|
|
const paramMatch = textBefore.match(/-(\w*)$/);
|
|
if (paramMatch) {
|
|
const cmdlet = findCmdletOnLine(textBefore);
|
|
if (cmdlet) {
|
|
const dashCol = textBefore.lastIndexOf("-") + 1;
|
|
const paramRange = {
|
|
...range,
|
|
startColumn: dashCol,
|
|
};
|
|
|
|
for (const p of cmdlet.parameters) {
|
|
const detail = `[${p.type}]${p.mandatory ? " (Required)" : ""}`;
|
|
suggestions.push({
|
|
label: `-${p.name}`,
|
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
insertText: `-${p.name} `,
|
|
detail,
|
|
documentation: p.helpMessage || undefined,
|
|
range: paramRange,
|
|
sortText: p.mandatory ? `0_${p.name}` : `1_${p.name}`,
|
|
});
|
|
|
|
for (const alias of p.aliases) {
|
|
suggestions.push({
|
|
label: `-${alias}`,
|
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
insertText: `-${alias} `,
|
|
detail: `${detail} (alias for -${p.name})`,
|
|
documentation: p.helpMessage || undefined,
|
|
range: paramRange,
|
|
sortText: `2_${alias}`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cmdlet name completions
|
|
if (!paramMatch && !memberMatch) {
|
|
for (const fn of moduleFunctions) {
|
|
suggestions.push({
|
|
label: fn.name,
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
insertText: fn.name,
|
|
detail: fn.module ? `Module: ${fn.module}` : "Module function",
|
|
documentation: fn.synopsis || undefined,
|
|
range,
|
|
sortText: `0_${fn.name}`,
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < allCmdlets.length; i++) {
|
|
const c = allCmdlets[i];
|
|
const isBuiltin = i >= cmdlets.length;
|
|
suggestions.push({
|
|
label: c.name,
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
insertText: c.name,
|
|
detail: c.outputType ? `-> ${c.outputType}` : isBuiltin ? "PowerShell Cmdlet" : "Cmdlet",
|
|
documentation: buildCmdletDocumentation(c),
|
|
range,
|
|
sortText: isBuiltin ? `1_${c.name}` : `0_${c.name}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { suggestions };
|
|
},
|
|
});
|
|
}
|
|
|
|
function registerHoverProvider(): void {
|
|
monaco.languages.registerHoverProvider("powershell", {
|
|
provideHover(model: any, position: any) {
|
|
const word = model.getWordAtPosition(position);
|
|
if (!word) return null;
|
|
|
|
const lineContent: string = model.getLineContent(position.lineNumber);
|
|
const filtered = getFilteredVariables();
|
|
|
|
// Check for cmdlet hover (Verb-Noun pattern)
|
|
const cmdletPattern = /[\w]+-[\w]+/g;
|
|
let match;
|
|
while ((match = cmdletPattern.exec(lineContent)) !== null) {
|
|
const start = match.index + 1;
|
|
const end = start + match[0].length;
|
|
if (position.column >= start && position.column <= end) {
|
|
const cmdlet = allCmdlets.find((c) => c.name === match![0]);
|
|
if (cmdlet) {
|
|
return {
|
|
range: {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: start,
|
|
endColumn: end,
|
|
},
|
|
contents: [
|
|
{ value: `**${cmdlet.name}**` },
|
|
{ value: buildCmdletHoverMarkdown(cmdlet) },
|
|
],
|
|
};
|
|
}
|
|
|
|
const fn = moduleFunctions.find((f) => f.name === match![0]);
|
|
if (fn) {
|
|
const body = (fn.module ? `**Module:** ${fn.module}` : "Module function") +
|
|
(fn.synopsis ? `\n\n${fn.synopsis}` : "");
|
|
return {
|
|
range: {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: start,
|
|
endColumn: end,
|
|
},
|
|
contents: [
|
|
{ value: `**${fn.name}**` },
|
|
{ value: body },
|
|
],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for variable property hover ($Var.Property)
|
|
const varPropPattern = /\$(\w+)\.(\w+)/g;
|
|
while ((match = varPropPattern.exec(lineContent)) !== null) {
|
|
const fullStart = match.index + 1;
|
|
const fullEnd = fullStart + match[0].length;
|
|
if (position.column >= fullStart && position.column <= fullEnd) {
|
|
const varName = "$" + match[1];
|
|
const propName = match[2];
|
|
const variable = filtered.find((v) => v.name === varName);
|
|
if (variable) {
|
|
const typeDef = typeMap.get(variable.type);
|
|
if (typeDef) {
|
|
const prop = typeDef.properties.find((p) => p.name === propName);
|
|
if (prop) {
|
|
return {
|
|
range: {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: fullStart,
|
|
endColumn: fullEnd,
|
|
},
|
|
contents: [
|
|
{ value: `**${varName}.${prop.name}** [${prop.type}]` },
|
|
{ value: `Property of ${variable.type}` },
|
|
],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for variable hover
|
|
const varPattern = /\$[\w]+/g;
|
|
while ((match = varPattern.exec(lineContent)) !== null) {
|
|
const start = match.index + 1;
|
|
const end = start + match[0].length;
|
|
if (position.column >= start && position.column <= end) {
|
|
const variable = filtered.find((v) => v.name === match![0]);
|
|
if (variable) {
|
|
const typeDef = typeMap.get(variable.type);
|
|
const contents: { value: string }[] = [
|
|
{ value: `**${variable.name}** [${variable.type}]` },
|
|
{ value: variable.description },
|
|
];
|
|
if (typeDef) {
|
|
const propList = typeDef.properties
|
|
.map((p) => `- \`${p.name}\` [${p.type}]`)
|
|
.join("\n");
|
|
contents.push({ value: `**Properties:**\n${propList}` });
|
|
}
|
|
return {
|
|
range: {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: start,
|
|
endColumn: end,
|
|
},
|
|
contents,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for parameter hover (-ParameterName)
|
|
const paramPattern = /-(\w+)/g;
|
|
while ((match = paramPattern.exec(lineContent)) !== null) {
|
|
const start = match.index + 1;
|
|
const end = start + match[0].length;
|
|
if (position.column >= start && position.column <= end) {
|
|
const paramName = match[1];
|
|
const cmdlet = findCmdletOnLine(
|
|
lineContent.substring(0, match.index)
|
|
);
|
|
if (cmdlet) {
|
|
const param = cmdlet.parameters.find(
|
|
(p) =>
|
|
p.name === paramName ||
|
|
p.aliases.includes(paramName)
|
|
);
|
|
if (param) {
|
|
const parts = [
|
|
{ value: `**-${param.name}** [${param.type}]${param.mandatory ? " *(Required)*" : ""}` },
|
|
];
|
|
if (param.helpMessage) {
|
|
parts.push({ value: param.helpMessage });
|
|
}
|
|
if (param.aliases.length > 0) {
|
|
parts.push({
|
|
value: `Aliases: ${param.aliases.map((a) => `-${a}`).join(", ")}`,
|
|
});
|
|
}
|
|
return {
|
|
range: {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: start,
|
|
endColumn: end,
|
|
},
|
|
contents: parts,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
});
|
|
}
|
|
|
|
function findCmdletOnLine(text: string): CmdletDefinition | null {
|
|
const matches = text.match(/(\w+-\w+)/g);
|
|
if (!matches) return null;
|
|
|
|
// Return the last cmdlet found on the line (closest to cursor)
|
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
const cmdlet = allCmdlets.find((c) => c.name === matches[i]);
|
|
if (cmdlet) return cmdlet;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function buildCmdletDocumentation(c: CmdletDefinition): string {
|
|
const parts: string[] = [];
|
|
if (c.description) parts.push(c.description);
|
|
|
|
const requiredParams = c.parameters.filter((p) => p.mandatory);
|
|
if (requiredParams.length > 0) {
|
|
parts.push(
|
|
"Required: " +
|
|
requiredParams.map((p) => `-${p.name}`).join(", ")
|
|
);
|
|
}
|
|
|
|
return parts.join("\n\n");
|
|
}
|
|
|
|
function buildCmdletHoverMarkdown(c: CmdletDefinition): string {
|
|
const lines: string[] = [];
|
|
|
|
if (c.description) lines.push(c.description);
|
|
if (c.outputType) lines.push(`**Output:** ${c.outputType}`);
|
|
|
|
if (c.parameters.length > 0) {
|
|
lines.push("");
|
|
lines.push("**Parameters:**");
|
|
for (const p of c.parameters) {
|
|
const req = p.mandatory ? " *(Required)*" : "";
|
|
lines.push(`- \`-${p.name}\` [${p.type}]${req}`);
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// --- Snippet insertion ---
|
|
|
|
export function insertSnippet(snippetText: string): void {
|
|
if (typeof monaco === "undefined") return;
|
|
|
|
// Use the last focused editor (there's typically only one in a dialog)
|
|
const editors = monaco.editor.getEditors();
|
|
const editor = editors[editors.length - 1];
|
|
|
|
if (!editor) return;
|
|
|
|
const controller = editor.getContribution("snippetController");
|
|
if (controller) {
|
|
editor.focus();
|
|
controller.insert(snippetText);
|
|
} else {
|
|
// Fallback: insert as plain text
|
|
const selection = editor.getSelection();
|
|
editor.executeEdits("snippet", [{
|
|
range: selection,
|
|
text: snippetText,
|
|
forceMoveMarkers: true,
|
|
}]);
|
|
}
|
|
}
|
|
|
|
// --- Script templates ---
|
|
|
|
const scriptTemplates: Record<string, string> = {
|
|
Package: [
|
|
"# Package Script",
|
|
"# Prepare the game files and create a package for distribution.",
|
|
"",
|
|
"$StagingPath = \"\"",
|
|
"",
|
|
"# Copy or prepare files in $StagingPath",
|
|
"",
|
|
"New-Package -Path $StagingPath -Version \"1.0.0\" -Changelog \"Initial release\"",
|
|
].join("\n"),
|
|
Install: [
|
|
"# Install Script",
|
|
"# Runs after the game archive has been extracted.",
|
|
"",
|
|
].join("\n"),
|
|
Uninstall: [
|
|
"# Uninstall Script",
|
|
"# Runs when the game is being uninstalled.",
|
|
"",
|
|
].join("\n"),
|
|
NameChange: [
|
|
"# Name Change Script",
|
|
"# Runs when a player changes their alias.",
|
|
"",
|
|
].join("\n"),
|
|
KeyChange: [
|
|
"# Key Change Script",
|
|
"# Runs when a product key is allocated or changed.",
|
|
"",
|
|
].join("\n"),
|
|
BeforeStart: [
|
|
"# Before Start Script",
|
|
"# Runs before the game process starts.",
|
|
"",
|
|
].join("\n"),
|
|
AfterStop: [
|
|
"# After Stop Script",
|
|
"# Runs after the game process stops.",
|
|
"",
|
|
].join("\n"),
|
|
RunWrapper: [
|
|
"# Run Wrapper Script",
|
|
"# Controls how the game executable is launched.",
|
|
"# This script runs instead of the normal process launch.",
|
|
"",
|
|
"# Launch the game",
|
|
"Start-Process -FilePath $ExecutablePath -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory -Wait",
|
|
].join("\n"),
|
|
};
|
|
|
|
export function getScriptTemplate(scriptType: string): string | null {
|
|
return scriptTemplates[scriptType] ?? null;
|
|
}
|
|
|
|
// --- Script validation / diagnostics ---
|
|
|
|
const allKnownVarNames = new Set(variables.map((v) => v.name));
|
|
|
|
export function validateScript(): void {
|
|
if (typeof monaco === "undefined") return;
|
|
|
|
// Validate all PowerShell models
|
|
const models = monaco.editor.getModels().filter((m: any) => {
|
|
const lang = m.getLanguageId?.() ?? m.getModeId?.();
|
|
return lang === "powershell";
|
|
});
|
|
|
|
for (const model of models) {
|
|
validateModel(model);
|
|
}
|
|
}
|
|
|
|
function validateModel(model: any): void {
|
|
|
|
const markers: any[] = [];
|
|
const content = model.getValue();
|
|
const lines = content.split("\n");
|
|
const filtered = getFilteredVariables();
|
|
const filteredNames = new Set(filtered.map((v) => v.name));
|
|
|
|
// Check for missing New-Package in Package scripts
|
|
if (currentScriptType === "Package") {
|
|
const hasNewPackage = /New-Package\b/.test(content);
|
|
const hasReturnAssignment = /\$Return\s*=/.test(content);
|
|
|
|
if (!hasNewPackage && !hasReturnAssignment) {
|
|
markers.push({
|
|
severity: monaco.MarkerSeverity.Warning,
|
|
message: "Package scripts should call New-Package to set the return value with Path, Version, and Changelog.",
|
|
startLineNumber: 1,
|
|
startColumn: 1,
|
|
endLineNumber: 1,
|
|
endColumn: 1,
|
|
source: "LANCommander",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for variables not available in current script type
|
|
if (currentScriptType) {
|
|
const varPattern = /\$(\w+)/g;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
let match;
|
|
|
|
// Skip comment lines
|
|
if (line.trimStart().startsWith("#")) continue;
|
|
|
|
varPattern.lastIndex = 0;
|
|
while ((match = varPattern.exec(line)) !== null) {
|
|
const varName = "$" + match[1];
|
|
|
|
// Skip PowerShell built-in/common variables and user-assigned variables
|
|
if (!allKnownVarNames.has(varName)) continue;
|
|
|
|
// It's a known LANCommander variable but not available for this script type
|
|
if (!filteredNames.has(varName)) {
|
|
const col = match.index + 1;
|
|
markers.push({
|
|
severity: monaco.MarkerSeverity.Warning,
|
|
message: `${varName} is not available in ${currentScriptType} scripts.`,
|
|
startLineNumber: i + 1,
|
|
startColumn: col,
|
|
endLineNumber: i + 1,
|
|
endColumn: col + match[0].length,
|
|
source: "LANCommander",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
monaco.editor.setModelMarkers(model, "LANCommander", markers);
|
|
}
|