From 351f2a8c443173b8d62a4b900038297ab20f299c Mon Sep 17 00:00:00 2001 From: Roman <22231294+Lutymane@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:15:04 +0500 Subject: [PATCH 1/5] format --- .prettierrc.mjs | 16 ++ src/extension.ts | 655 ++++++++++++++++++++++++----------------------- 2 files changed, 350 insertions(+), 321 deletions(-) create mode 100644 .prettierrc.mjs diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 0000000..5f842ca --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,16 @@ +/** + * @type {import('prettier').Options} + */ +export default { + htmlWhitespaceSensitivity: "css", + printWidth: 80, + arrowParens: "avoid", + bracketSameLine: true, + semi: true, + tabWidth: 2, + useTabs: false, + trailingComma: "es5", + vueIndentScriptAndStyle: false, + + // https://prettier.github.io/plugin-pug/guide/pug-specific-options.html +}; diff --git a/src/extension.ts b/src/extension.ts index 7a3ad40..39e2286 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,332 +2,345 @@ import { Luraph, type LuraphOptionList } from "luraph"; import * as vscode from "vscode"; const TIER_ICONS = { - CUSTOMER_ONLY: undefined, - PREMIUM_ONLY: new vscode.ThemeIcon("star"), - ADMIN_ONLY: new vscode.ThemeIcon("lock") + CUSTOMER_ONLY: undefined, + PREMIUM_ONLY: new vscode.ThemeIcon("star"), + ADMIN_ONLY: new vscode.ThemeIcon("lock"), }; const TIER_TEXT = { - CUSTOMER_ONLY: "", - PREMIUM_ONLY: "Premium feature", - ADMIN_ONLY: "Administrator-only feature" -} + CUSTOMER_ONLY: "", + PREMIUM_ONLY: "Premium feature", + ADMIN_ONLY: "Administrator-only feature", +}; export function activate(context: vscode.ExtensionContext) { - const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - - statusBarItem.name = "Luraph"; - statusBarItem.text = "$(terminal) Obfuscate with Luraph"; - statusBarItem.tooltip = new vscode.MarkdownString(`Obfuscate the currently opened file with [Luraph](https://lura.ph/ "Luraph - Online Lua Obfuscator").`); - statusBarItem.command = "luraph.obfuscate"; - - statusBarItem.show(); - - const logOutput = vscode.window.createOutputChannel("Luraph", { - log: true - }); - const log = (msg: string) => logOutput.info(msg); - - context.subscriptions.push(statusBarItem, logOutput); //auto dispose on extension deactivation - - log("Luraph VS Code extension has started."); - - const command = vscode.commands.registerCommand('luraph.obfuscate', async () => { - const settings = vscode.workspace.getConfiguration("luraph"); - let apiKey: string | undefined = settings.get("API Key"); - - if(!apiKey?.length){ - const action = await vscode.window.showErrorMessage("An API key must be configured to use the Luraph API.", - { - title: "Set API Key" - } - ); - - if(!action){ - return; - } - - const input = await vscode.window.showInputBox({ - title: "Luraph - Set API Key", - prompt: "Please enter your Luraph API key.", - placeHolder: "Luraph API Key", - - ignoreFocusOut: true, - validateInput: (value) => { - if(!value.length){ - return { - message: "API key must not be empty.", - severity: vscode.InputBoxValidationSeverity.Error - }; - } - - return null; - } - }); - - if(!input){ - return; - } - - apiKey = input; - settings.update("API Key", input, true); //update globally - } - - const textEditor = vscode.window.activeTextEditor; - const document = textEditor?.document; - - if(!document){ - return vscode.window.showErrorMessage("Please open an file to obfuscate."); - } - - const fileName = document.fileName; - const contents = document.getText(); - - if(!contents.length){ - return vscode.window.showErrorMessage("Cannot obfuscate an empty file."); - } - - log(`Performing Luraph obfuscation for ${fileName}...`); - const luraphApi = new Luraph(apiKey); - - const availableNodes: vscode.QuickPickItem[] = []; - - try{ - log("> Fetching nodes..."); - const nodes = await luraphApi.getNodes(); - - const recommendedId = nodes.recommendedId; - log(`> Recommended node: ${recommendedId || "[none]"}`); - - log("> Available nodes:"); - for(const [nodeId, nodeInfo] of Object.entries(nodes.nodes)){ - const recommended = nodeId === recommendedId; - const description = (recommended ? " (recommended)" : undefined); - const details = `${Math.floor(nodeInfo.cpuUsage)}% CPU usage, ${Object.keys(nodeInfo.options).length} options`; - - log(`> - ${nodeId}: [${details}]${description ?? ""}`); - - const quickPickItem = { - iconPath: recommended ? new vscode.ThemeIcon("heart") : undefined, - label: nodeId, - description: description, - detail: details, - picked: recommended - }; - - if(recommended){ - availableNodes.unshift(quickPickItem); - }else{ - availableNodes.push(quickPickItem); - } - } - - const selectedNode = await vscode.window.showQuickPick(availableNodes, { - title: "Luraph - Select Node", - placeHolder: "Node ID", - - ignoreFocusOut: true - }); - - if(!selectedNode){ - return; - } - - const nodeId = selectedNode.label; - const nodeInfo = nodes.nodes[nodeId]; - - log(`> Selected node: ${nodeId}`); - log("> Available options:"); - - const optionValues: LuraphOptionList = {}; - const checkboxes: (vscode.QuickPickItem & {id: string})[] = []; - const dropdowns = []; - const textFields = []; - - for(const [optionId, { name, description, tier, type, choices }] of Object.entries(nodeInfo.options)){ - const tierIcon = TIER_ICONS[tier]; - const tierText = TIER_TEXT[tier]; - const tierTextParen = tierText ? ` (${tierText})` : ""; - - log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); - - switch(type){ - case "CHECKBOX": { - optionValues[optionId] = false; - - checkboxes.push({ - id: optionId, - label: name, - description: optionId + tierTextParen, - detail: description, - iconPath: tierIcon - }); - break; - } - case "DROPDOWN": { - optionValues[optionId] = choices[0]; - - log(` Choices: [${choices.join(", ")}]`); - - dropdowns.push({ - id: optionId, - title: `${name}${tierTextParen} - ${description} [${optionId}]`, - placeHolder: `Value for ${name}`, - items: choices.map((choice, index) => ({ - label: choice, - description: tierText, - iconPath: index !== 0 ? tierIcon : undefined, - picked: index === 0 - })) as vscode.QuickPickItem[] - }); - break; - } - case "TEXT": { - optionValues[optionId] = ""; - - textFields.push({ - id: optionId, - title: `${name}${tierTextParen} [${optionId}]`, - prompt: description, - placeHolder: `Value for ${name} (leave empty to use default value)`, - }) - break; - } - default: - throw new Error(`Received invalid option type: ${type}`); - } - } - - const selectedValues = await vscode.window.showQuickPick(checkboxes, { - title: "Luraph - Select Options (checkbox)", - placeHolder: "Option name/ID", - - ignoreFocusOut: true, - canPickMany: true, - matchOnDescription: true - }); - - if(!selectedValues){ - return; - } - - for(const checkboxInfo of selectedValues){ - optionValues[checkboxInfo.id] = true; - } - - for(const { id, title, placeHolder, items } of dropdowns){ - const selectedValue = await vscode.window.showQuickPick(items, { - title: `Luraph - Select Option: ${title}`, - placeHolder, - - ignoreFocusOut: true, - canPickMany: false, - matchOnDetail: true - }); - - if(!selectedValue){ - return; - } - - optionValues[id] = selectedValue.label; - } - - for(const { id, title, prompt, placeHolder } of textFields){ - const selectedValue = await vscode.window.showInputBox({ - title: `Luraph - Select Option: ${title}`, - prompt, - placeHolder, - - ignoreFocusOut: true - }); - - if(!selectedValue){ - return; - } - - optionValues[id] = selectedValue; - } - - statusBarItem.text = "$(gear~spin) Obfuscating..."; - const { jobId } = await luraphApi.createNewJob(nodeId, contents, `[luraph-vscode] ${fileName}`, optionValues); - - log(`> Job ID: ${jobId}`); - statusBarItem.text = `$(gear~spin) Obfuscating... (Job ID: ${jobId})`; - - const status = await luraphApi.getJobStatus(jobId); - if(!status.success){ - const error = status.error; - log(`> Obfuscation failed: ${error}`); - return vscode.window.showErrorMessage(`Obfuscation Error: ${error}`); - } - - const result = await luraphApi.downloadResult(jobId); - log(`> Obfuscation succeeded! (${result.data.length} bytes)`); - - let directory = vscode.workspace.workspaceFolders?.[0].uri.path || ""; - let resultName = document.uri.path; - if(document.uri.scheme === "file" || document.uri.scheme === "untitled"){ - const lastSlash = resultName.lastIndexOf("/"); - - if(lastSlash !== -1){ - directory = resultName.substring(0, lastSlash); - resultName = resultName.substring(lastSlash + 1); - } - } - - const filePart = resultName.split(".")[0]; - resultName = `${filePart}-obfuscated.lua`; - - let resultUri; - let tries = 0; - while(true){ - resultUri = vscode.Uri.from({ - path: `${directory}/${filePart}-obfuscated${tries > 0 ? `-${tries}` : ""}.lua`, - scheme: "untitled" - }); - - try{ - await vscode.workspace.fs.stat(resultUri.with({ scheme: "file" })); - }catch(err){ - if(err instanceof vscode.FileSystemError && err.code === "FileNotFound"){ - break; //file doesn't exist, save here - } - - throw err; - } - - tries++; - } - - log(`> Saving to file: ${resultUri.fsPath}`); - - const newDoc = await vscode.workspace.openTextDocument(resultUri); - const textEditor = await vscode.window.showTextDocument(newDoc); - - const editsApplied = await textEditor.edit((editBuilder) => { - const fullRange = new vscode.Range( - newDoc.lineAt(0).range.start, - newDoc.lineAt(newDoc.lineCount - 1).range.end - ); - - editBuilder.replace(fullRange, result.data); - }); - - if(!editsApplied){ - throw new Error("VS Code Extension Error: Could not apply edits to created TextEditor"); - } - }catch(err){ - if(err instanceof Error && err.name === "LuraphException"){ //TODO: use instanceof LuraphException - return vscode.window.showErrorMessage(`Luraph API Error: ${err.message}`); - } - - throw err; - }finally{ - statusBarItem.text = "$(terminal) Obfuscate with Luraph"; - } - }); - - context.subscriptions.push(command); + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + + statusBarItem.name = "Luraph"; + statusBarItem.text = "$(terminal) Obfuscate with Luraph"; + statusBarItem.tooltip = new vscode.MarkdownString( + `Obfuscate the currently opened file with [Luraph](https://lura.ph/ "Luraph - Online Lua Obfuscator").` + ); + statusBarItem.command = "luraph.obfuscate"; + + statusBarItem.show(); + + const logOutput = vscode.window.createOutputChannel("Luraph", { + log: true, + }); + const log = (msg: string) => logOutput.info(msg); + + context.subscriptions.push(statusBarItem, logOutput); //auto dispose on extension deactivation + + log("Luraph VS Code extension has started."); + + const command = vscode.commands.registerCommand("luraph.obfuscate", async () => { + const settings = vscode.workspace.getConfiguration("luraph"); + let apiKey: string | undefined = settings.get("API Key"); + + if (!apiKey?.length) { + const action = await vscode.window.showErrorMessage( + "An API key must be configured to use the Luraph API.", + { + title: "Set API Key", + } + ); + + if (!action) { + return; + } + + const input = await vscode.window.showInputBox({ + title: "Luraph - Set API Key", + prompt: "Please enter your Luraph API key.", + placeHolder: "Luraph API Key", + + ignoreFocusOut: true, + validateInput: value => { + if (!value.length) { + return { + message: "API key must not be empty.", + severity: vscode.InputBoxValidationSeverity.Error, + }; + } + + return null; + }, + }); + + if (!input) { + return; + } + + apiKey = input; + settings.update("API Key", input, true); //update globally + } + + const textEditor = vscode.window.activeTextEditor; + const document = textEditor?.document; + + if (!document) { + return vscode.window.showErrorMessage("Please open an file to obfuscate."); + } + + const fileName = document.fileName; + const contents = document.getText(); + + if (!contents.length) { + return vscode.window.showErrorMessage("Cannot obfuscate an empty file."); + } + + log(`Performing Luraph obfuscation for ${fileName}...`); + const luraphApi = new Luraph(apiKey); + + const availableNodes: vscode.QuickPickItem[] = []; + + try { + log("> Fetching nodes..."); + const nodes = await luraphApi.getNodes(); + + const recommendedId = nodes.recommendedId; + log(`> Recommended node: ${recommendedId || "[none]"}`); + + log("> Available nodes:"); + for (const [nodeId, nodeInfo] of Object.entries(nodes.nodes)) { + const recommended = nodeId === recommendedId; + const description = recommended ? " (recommended)" : undefined; + const details = `${Math.floor(nodeInfo.cpuUsage)}% CPU usage, ${ + Object.keys(nodeInfo.options).length + } options`; + + log(`> - ${nodeId}: [${details}]${description ?? ""}`); + + const quickPickItem = { + iconPath: recommended ? new vscode.ThemeIcon("heart") : undefined, + label: nodeId, + description: description, + detail: details, + picked: recommended, + }; + + if (recommended) { + availableNodes.unshift(quickPickItem); + } else { + availableNodes.push(quickPickItem); + } + } + + const selectedNode = await vscode.window.showQuickPick(availableNodes, { + title: "Luraph - Select Node", + placeHolder: "Node ID", + + ignoreFocusOut: true, + }); + + if (!selectedNode) { + return; + } + + const nodeId = selectedNode.label; + const nodeInfo = nodes.nodes[nodeId]; + + log(`> Selected node: ${nodeId}`); + log("> Available options:"); + + const optionValues: LuraphOptionList = {}; + const checkboxes: (vscode.QuickPickItem & { id: string })[] = []; + const dropdowns = []; + const textFields = []; + + for (const [optionId, { name, description, tier, type, choices }] of Object.entries( + nodeInfo.options + )) { + const tierIcon = TIER_ICONS[tier]; + const tierText = TIER_TEXT[tier]; + const tierTextParen = tierText ? ` (${tierText})` : ""; + + log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); + + switch (type) { + case "CHECKBOX": { + optionValues[optionId] = false; + + checkboxes.push({ + id: optionId, + label: name, + description: optionId + tierTextParen, + detail: description, + iconPath: tierIcon, + }); + break; + } + case "DROPDOWN": { + optionValues[optionId] = choices[0]; + + log(` Choices: [${choices.join(", ")}]`); + + dropdowns.push({ + id: optionId, + title: `${name}${tierTextParen} - ${description} [${optionId}]`, + placeHolder: `Value for ${name}`, + items: choices.map((choice, index) => ({ + label: choice, + description: tierText, + iconPath: index !== 0 ? tierIcon : undefined, + picked: index === 0, + })) as vscode.QuickPickItem[], + }); + break; + } + case "TEXT": { + optionValues[optionId] = ""; + + textFields.push({ + id: optionId, + title: `${name}${tierTextParen} [${optionId}]`, + prompt: description, + placeHolder: `Value for ${name} (leave empty to use default value)`, + }); + break; + } + default: + throw new Error(`Received invalid option type: ${type}`); + } + } + + const selectedValues = await vscode.window.showQuickPick(checkboxes, { + title: "Luraph - Select Options (checkbox)", + placeHolder: "Option name/ID", + + ignoreFocusOut: true, + canPickMany: true, + matchOnDescription: true, + }); + + if (!selectedValues) { + return; + } + + for (const checkboxInfo of selectedValues) { + optionValues[checkboxInfo.id] = true; + } + + for (const { id, title, placeHolder, items } of dropdowns) { + const selectedValue = await vscode.window.showQuickPick(items, { + title: `Luraph - Select Option: ${title}`, + placeHolder, + + ignoreFocusOut: true, + canPickMany: false, + matchOnDetail: true, + }); + + if (!selectedValue) { + return; + } + + optionValues[id] = selectedValue.label; + } + + for (const { id, title, prompt, placeHolder } of textFields) { + const selectedValue = await vscode.window.showInputBox({ + title: `Luraph - Select Option: ${title}`, + prompt, + placeHolder, + + ignoreFocusOut: true, + }); + + if (!selectedValue) { + return; + } + + optionValues[id] = selectedValue; + } + + statusBarItem.text = "$(gear~spin) Obfuscating..."; + const { jobId } = await luraphApi.createNewJob( + nodeId, + contents, + `[luraph-vscode] ${fileName}`, + optionValues + ); + + log(`> Job ID: ${jobId}`); + statusBarItem.text = `$(gear~spin) Obfuscating... (Job ID: ${jobId})`; + + const status = await luraphApi.getJobStatus(jobId); + if (!status.success) { + const error = status.error; + log(`> Obfuscation failed: ${error}`); + return vscode.window.showErrorMessage(`Obfuscation Error: ${error}`); + } + + const result = await luraphApi.downloadResult(jobId); + log(`> Obfuscation succeeded! (${result.data.length} bytes)`); + + let directory = vscode.workspace.workspaceFolders?.[0].uri.path || ""; + let resultName = document.uri.path; + if (document.uri.scheme === "file" || document.uri.scheme === "untitled") { + const lastSlash = resultName.lastIndexOf("/"); + + if (lastSlash !== -1) { + directory = resultName.substring(0, lastSlash); + resultName = resultName.substring(lastSlash + 1); + } + } + + const filePart = resultName.split(".")[0]; + resultName = `${filePart}-obfuscated.lua`; + + let resultUri; + let tries = 0; + while (true) { + resultUri = vscode.Uri.from({ + path: `${directory}/${filePart}-obfuscated${tries > 0 ? `-${tries}` : ""}.lua`, + scheme: "untitled", + }); + + try { + await vscode.workspace.fs.stat(resultUri.with({ scheme: "file" })); + } catch (err) { + if (err instanceof vscode.FileSystemError && err.code === "FileNotFound") { + break; //file doesn't exist, save here + } + + throw err; + } + + tries++; + } + + log(`> Saving to file: ${resultUri.fsPath}`); + + const newDoc = await vscode.workspace.openTextDocument(resultUri); + const textEditor = await vscode.window.showTextDocument(newDoc); + + const editsApplied = await textEditor.edit(editBuilder => { + const fullRange = new vscode.Range( + newDoc.lineAt(0).range.start, + newDoc.lineAt(newDoc.lineCount - 1).range.end + ); + + editBuilder.replace(fullRange, result.data); + }); + + if (!editsApplied) { + throw new Error("VS Code Extension Error: Could not apply edits to created TextEditor"); + } + } catch (err) { + if (err instanceof Error && err.name === "LuraphException") { + //TODO: use instanceof LuraphException + return vscode.window.showErrorMessage(`Luraph API Error: ${err.message}`); + } + + throw err; + } finally { + statusBarItem.text = "$(terminal) Obfuscate with Luraph"; + } + }); + + context.subscriptions.push(command); } export function deactivate() {} //no-op From f6ef23cf89f941edeca467a8b6e60ed97ed43805 Mon Sep 17 00:00:00 2001 From: Roman <22231294+Lutymane@users.noreply.github.com> Date: Tue, 23 Apr 2024 18:08:02 +0500 Subject: [PATCH 2/5] feat: filter feature options based on its dependencies --- src/extension.ts | 127 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 39e2286..200d6a6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -141,8 +141,8 @@ export function activate(context: vscode.ExtensionContext) { const nodeId = selectedNode.label; const nodeInfo = nodes.nodes[nodeId]; - log(`> Selected node: ${nodeId}`); - log("> Available options:"); + // log(`> Selected node: ${nodeId}`); + // log("> Available options:"); const optionValues: LuraphOptionList = {}; const checkboxes: (vscode.QuickPickItem & { id: string })[] = []; @@ -156,7 +156,7 @@ export function activate(context: vscode.ExtensionContext) { const tierText = TIER_TEXT[tier]; const tierTextParen = tierText ? ` (${tierText})` : ""; - log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); + // log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); switch (type) { case "CHECKBOX": { @@ -174,7 +174,7 @@ export function activate(context: vscode.ExtensionContext) { case "DROPDOWN": { optionValues[optionId] = choices[0]; - log(` Choices: [${choices.join(", ")}]`); + // log(` Choices: [${choices.join(", ")}]`); dropdowns.push({ id: optionId, @@ -205,15 +205,122 @@ export function activate(context: vscode.ExtensionContext) { } } - const selectedValues = await vscode.window.showQuickPick(checkboxes, { - title: "Luraph - Select Options (checkbox)", - placeHolder: "Option name/ID", + // @note show target before options, because options can depend on selected target + { + const [targetVersionDropdown] = dropdowns.splice( + dropdowns.findIndex(d => d.id === "TARGET_VERSION"), + 1 + ); - ignoreFocusOut: true, - canPickMany: true, - matchOnDescription: true, + const { id, title, placeHolder, items } = targetVersionDropdown; + + const selectedTarget = await vscode.window.showQuickPick(items, { + title: `Luraph - Select Option: ${title}`, + placeHolder, + + ignoreFocusOut: true, + canPickMany: false, + matchOnDetail: true, + }); + + if (!selectedTarget) { + return; + } + + optionValues[id] = selectedTarget.label; + } + + const selectedValues = await new Promise(resolve => { + const disposables: vscode.Disposable[] = []; + + const dispose = () => { + disposables.forEach(d => d.dispose()); + }; + + const optionsPick = vscode.window.createQuickPick<(typeof checkboxes)[number]>(); + disposables.push(optionsPick); + + // @note initial visible items + const getVisibleOptions = () => + checkboxes.filter(({ id }) => { + let option = nodeInfo.options[id]; + + if (!option.dependencies) return true; + + for (let [depId, depVals] of Object.entries(option.dependencies)) { + if ( + !depVals.includes(optionValues[depId]) && + !depVals.includes(!!optionsPick.selectedItems.find(i => i.id === depId)) + ) { + return false; + } + } + + return true; + }); + + optionsPick.title = "Luraph - Select Options (checkbox)"; + optionsPick.placeholder = "Option name/ID"; + + optionsPick.items = getVisibleOptions(); + + optionsPick.ignoreFocusOut = true; + optionsPick.canSelectMany = true; + optionsPick.matchOnDescription = true; + + /** + * @note onDidChangeSelection is also triggered when you edit `.selectedItems` property, which gets reset when changing `.items` for some reason, so we need to reapply selected items without creating infinite loop, where event is triggered inside its handler + */ + let ignoreSelectionUpdate = false; + + disposables.push( + optionsPick.onDidAccept(() => { + // @note call resolve first so items are not empty + resolve(optionsPick.selectedItems); + dispose(); + }), + + optionsPick.onDidChangeSelection(items => { + // log( + // `onDidChangeSelection ${JSON.stringify(items.map(i => i.id))} / ${JSON.stringify( + // optionsPick.selectedItems.map(i => i.id) + // )} :: ignored: ${ignoreSelectionUpdate}` + // ); + + if (ignoreSelectionUpdate) { + ignoreSelectionUpdate = false; + return; + } + + const visibleOptions = getVisibleOptions(); + // @note if different set of options, then update, tbh we could remove this check and update every time + if (!visibleOptions.every((o, i) => o.id === optionsPick.items[i].id)) { + optionsPick.items = visibleOptions; + optionsPick.selectedItems = items; + ignoreSelectionUpdate = true; + } + }), + + optionsPick.onDidHide(() => { + resolve(undefined); + dispose(); + }) + ); + + optionsPick.show(); }); + // log( + // `selectedValues: ${ + // selectedValues && + // JSON.stringify( + // selectedValues.map(i => i.id), + // null, + // 4 + // ) + // }` + // ); + if (!selectedValues) { return; } From 411ce4caa7e4791b338076bd9c68174a878bed63 Mon Sep 17 00:00:00 2001 From: Roman <22231294+Lutymane@users.noreply.github.com> Date: Tue, 23 Apr 2024 18:30:46 +0500 Subject: [PATCH 3/5] feat: show progress popup when loading nodes --- src/extension.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 200d6a6..20bc9f1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -97,7 +97,19 @@ export function activate(context: vscode.ExtensionContext) { try { log("> Fetching nodes..."); - const nodes = await luraphApi.getNodes(); + + const nodesPromise = luraphApi.getNodes(); + + vscode.window.withProgress( + { + title: "Fetching Luraph Nodes...", + location: vscode.ProgressLocation.Notification, + cancellable: true, + }, + progress => nodesPromise + ); + + const nodes = await nodesPromise; const recommendedId = nodes.recommendedId; log(`> Recommended node: ${recommendedId || "[none]"}`); From 1d0f6fb77e860258234f52e4481c12148029cc90 Mon Sep 17 00:00:00 2001 From: Roman <22231294+Lutymane@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:28:15 +0500 Subject: [PATCH 4/5] feat: topological sorting --- src/extension.ts | 405 ++++++++++++++++++++++++++--------------------- 1 file changed, 226 insertions(+), 179 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 20bc9f1..a4f3d5b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,7 +28,7 @@ export function activate(context: vscode.ExtensionContext) { const logOutput = vscode.window.createOutputChannel("Luraph", { log: true, }); - const log = (msg: string) => logOutput.info(msg); + const log = logOutput.info; context.subscriptions.push(statusBarItem, logOutput); //auto dispose on extension deactivation @@ -96,7 +96,7 @@ export function activate(context: vscode.ExtensionContext) { const availableNodes: vscode.QuickPickItem[] = []; try { - log("> Fetching nodes..."); + // log("> Fetching nodes..."); const nodesPromise = luraphApi.getNodes(); @@ -112,9 +112,9 @@ export function activate(context: vscode.ExtensionContext) { const nodes = await nodesPromise; const recommendedId = nodes.recommendedId; - log(`> Recommended node: ${recommendedId || "[none]"}`); + // log(`> Recommended node: ${recommendedId || "[none]"}`); - log("> Available nodes:"); + // log("> Available nodes:"); for (const [nodeId, nodeInfo] of Object.entries(nodes.nodes)) { const recommended = nodeId === recommendedId; const description = recommended ? " (recommended)" : undefined; @@ -122,7 +122,7 @@ export function activate(context: vscode.ExtensionContext) { Object.keys(nodeInfo.options).length } options`; - log(`> - ${nodeId}: [${details}]${description ?? ""}`); + // log(`> - ${nodeId}: [${details}]${description ?? ""}`); const quickPickItem = { iconPath: recommended ? new vscode.ThemeIcon("heart") : undefined, @@ -156,222 +156,269 @@ export function activate(context: vscode.ExtensionContext) { // log(`> Selected node: ${nodeId}`); // log("> Available options:"); - const optionValues: LuraphOptionList = {}; - const checkboxes: (vscode.QuickPickItem & { id: string })[] = []; - const dropdowns = []; - const textFields = []; + const userOptionValues: LuraphOptionList = {}; - for (const [optionId, { name, description, tier, type, choices }] of Object.entries( - nodeInfo.options - )) { - const tierIcon = TIER_ICONS[tier]; - const tierText = TIER_TEXT[tier]; - const tierTextParen = tierText ? ` (${tierText})` : ""; - - // log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); - - switch (type) { - case "CHECKBOX": { - optionValues[optionId] = false; - - checkboxes.push({ - id: optionId, - label: name, - description: optionId + tierTextParen, - detail: description, - iconPath: tierIcon, - }); - break; + // @note topologically sort options + let idOrder: string[]; + { + /** + * @note options that depend on each other + */ + let linkedOptions: string[] = []; + Object.entries(nodeInfo.options).forEach(([id, option]) => { + if (option.dependencies) { + linkedOptions.push(id); + Object.keys(option.dependencies).forEach(id => linkedOptions.push(id)); } - case "DROPDOWN": { - optionValues[optionId] = choices[0]; + }); - // log(` Choices: [${choices.join(", ")}]`); + linkedOptions = linkedOptions + // @note group dropdowns + .sort((a, b) => (nodeInfo.options[a].type === "DROPDOWN" ? -1 : 0)); - dropdowns.push({ - id: optionId, - title: `${name}${tierTextParen} - ${description} [${optionId}]`, - placeHolder: `Value for ${name}`, - items: choices.map((choice, index) => ({ - label: choice, - description: tierText, - iconPath: index !== 0 ? tierIcon : undefined, - picked: index === 0, - })) as vscode.QuickPickItem[], - }); - break; - } - case "TEXT": { - optionValues[optionId] = ""; + const topologicalSort = (arr: string[]) => { + const visited = new Map(); + const result: string[] = []; - textFields.push({ - id: optionId, - title: `${name}${tierTextParen} [${optionId}]`, - prompt: description, - placeHolder: `Value for ${name} (leave empty to use default value)`, - }); - break; - } - default: - throw new Error(`Received invalid option type: ${type}`); - } - } + const visit = (id: string) => { + if (visited.get(id)) return; + visited.set(id, true); - // @note show target before options, because options can depend on selected target - { - const [targetVersionDropdown] = dropdowns.splice( - dropdowns.findIndex(d => d.id === "TARGET_VERSION"), - 1 - ); - - const { id, title, placeHolder, items } = targetVersionDropdown; + const { dependencies } = nodeInfo.options[id]; + // @note put dependencies before dependent options + if (dependencies) { + Object.keys(dependencies).forEach(visit); + } - const selectedTarget = await vscode.window.showQuickPick(items, { - title: `Luraph - Select Option: ${title}`, - placeHolder, + result.push(id); + }; - ignoreFocusOut: true, - canPickMany: false, - matchOnDetail: true, - }); + arr.forEach(visit); - if (!selectedTarget) { - return; - } + return result; + }; - optionValues[id] = selectedTarget.label; + linkedOptions = topologicalSort(linkedOptions); + + // @note put linked before + idOrder = [ + ...linkedOptions, + ...Object.keys(nodeInfo.options).sort(a => + nodeInfo.options[a].type === "CHECKBOX" ? -1 : 0 + ), + ] + // @note remove dupes + .filter((v, i, arr) => arr.indexOf(v) === i); + + log( + JSON.stringify( + idOrder.map(id => `${id} : ${nodeInfo.options[id].type}`), + null, + 4 + ) + ); } - const selectedValues = await new Promise(resolve => { - const disposables: vscode.Disposable[] = []; + for (let i = 0; i < idOrder.length; ) { + let optionId = idOrder[i]; + let { name, description, tier, type, choices, dependencies } = nodeInfo.options[optionId]; - const dispose = () => { - disposables.forEach(d => d.dispose()); - }; + let tierIcon = TIER_ICONS[tier]; + let tierText = TIER_TEXT[tier]; + let tierTextParen = tierText ? ` (${tierText})` : ""; - const optionsPick = vscode.window.createQuickPick<(typeof checkboxes)[number]>(); - disposables.push(optionsPick); + // log(`> - [${optionId}] ${name}${tierTextParen} - ${description} (${type})`); - // @note initial visible items - const getVisibleOptions = () => - checkboxes.filter(({ id }) => { - let option = nodeInfo.options[id]; + switch (type) { + case "CHECKBOX": { + type TCheckbox = vscode.QuickPickItem & { id: string }; + let checkboxCluster: TCheckbox[] = []; - if (!option.dependencies) return true; + while (true) { + userOptionValues[optionId] = false; - for (let [depId, depVals] of Object.entries(option.dependencies)) { - if ( - !depVals.includes(optionValues[depId]) && - !depVals.includes(!!optionsPick.selectedItems.find(i => i.id === depId)) - ) { - return false; - } - } + // @note put options with dependencies at the end + checkboxCluster.push({ + id: optionId, + label: name, + description: optionId + tierTextParen, + detail: description, + iconPath: tierIcon, + }); - return true; - }); + i += 1; - optionsPick.title = "Luraph - Select Options (checkbox)"; - optionsPick.placeholder = "Option name/ID"; + optionId = idOrder[i]; - optionsPick.items = getVisibleOptions(); + if (nodeInfo.options[optionId]?.type !== "CHECKBOX") break; - optionsPick.ignoreFocusOut = true; - optionsPick.canSelectMany = true; - optionsPick.matchOnDescription = true; + ({ name, description, tier, type, choices, dependencies } = + nodeInfo.options[optionId]); - /** - * @note onDidChangeSelection is also triggered when you edit `.selectedItems` property, which gets reset when changing `.items` for some reason, so we need to reapply selected items without creating infinite loop, where event is triggered inside its handler - */ - let ignoreSelectionUpdate = false; + tierIcon = TIER_ICONS[tier]; + tierText = TIER_TEXT[tier]; + tierTextParen = tierText ? ` (${tierText})` : ""; + } - disposables.push( - optionsPick.onDidAccept(() => { - // @note call resolve first so items are not empty - resolve(optionsPick.selectedItems); - dispose(); - }), + const selectedValues = await new Promise(resolve => { + const disposables: vscode.Disposable[] = []; + + const dispose = () => { + disposables.forEach(d => d.dispose()); + }; + + const optionsPick = vscode.window.createQuickPick<(typeof checkboxCluster)[number]>(); + disposables.push(optionsPick); + + // @note initial visible items + const getVisibleOptions = () => + checkboxCluster.filter(({ id }) => { + let option = nodeInfo.options[id]; + + if (!option.dependencies) return true; + + for (let [depId, depVals] of Object.entries(option.dependencies)) { + if ( + // @note check previously selected options + !depVals.includes(userOptionValues[depId]) && + // @note check current selected cluster dependencies + !depVals.includes(!!optionsPick.selectedItems.find(i => i.id === depId)) + ) { + return false; + } + } + + return true; + }); + + optionsPick.title = "Luraph - Select Options (checkbox)"; + optionsPick.placeholder = "Option name/ID"; + + optionsPick.items = getVisibleOptions(); + + optionsPick.ignoreFocusOut = true; + optionsPick.canSelectMany = true; + optionsPick.matchOnDescription = true; + + /** + * @note onDidChangeSelection is also triggered when you edit `.selectedItems` property, which gets reset when changing `.items` for some reason, so we need to reapply selected items without creating infinite loop, where event is triggered inside its handler + */ + let ignoreSelectionUpdate = false; + + disposables.push( + optionsPick.onDidAccept(() => { + // @note call resolve first so items are not empty + resolve(optionsPick.selectedItems); + dispose(); + }), + + optionsPick.onDidChangeSelection(items => { + // log( + // `onDidChangeSelection ${JSON.stringify(items.map(i => i.id))} / ${JSON.stringify( + // optionsPick.selectedItems.map(i => i.id) + // )} :: ignored: ${ignoreSelectionUpdate}` + // ); + + if (ignoreSelectionUpdate) { + ignoreSelectionUpdate = false; + return; + } + + const visibleOptions = getVisibleOptions(); + // @note if different set of options, then update, tbh we could remove this check and update every time + if (!visibleOptions.every((o, i) => o.id === optionsPick.items[i].id)) { + optionsPick.items = visibleOptions; + optionsPick.selectedItems = items; + ignoreSelectionUpdate = true; + } + }), + + optionsPick.onDidHide(() => { + resolve(undefined); + dispose(); + }) + ); + + optionsPick.show(); + }); - optionsPick.onDidChangeSelection(items => { // log( - // `onDidChangeSelection ${JSON.stringify(items.map(i => i.id))} / ${JSON.stringify( - // optionsPick.selectedItems.map(i => i.id) - // )} :: ignored: ${ignoreSelectionUpdate}` + // `selectedValues: ${ + // selectedValues && + // JSON.stringify( + // selectedValues.map(i => i.id), + // null, + // 4 + // ) + // }` // ); - if (ignoreSelectionUpdate) { - ignoreSelectionUpdate = false; + if (!selectedValues) { return; } - const visibleOptions = getVisibleOptions(); - // @note if different set of options, then update, tbh we could remove this check and update every time - if (!visibleOptions.every((o, i) => o.id === optionsPick.items[i].id)) { - optionsPick.items = visibleOptions; - optionsPick.selectedItems = items; - ignoreSelectionUpdate = true; + for (const checkboxInfo of selectedValues) { + userOptionValues[checkboxInfo.id] = true; } - }), - optionsPick.onDidHide(() => { - resolve(undefined); - dispose(); - }) - ); + break; + } + case "DROPDOWN": { + userOptionValues[optionId] = choices[0]; - optionsPick.show(); - }); + // log(` Choices: [${choices.join(", ")}]`); - // log( - // `selectedValues: ${ - // selectedValues && - // JSON.stringify( - // selectedValues.map(i => i.id), - // null, - // 4 - // ) - // }` - // ); - - if (!selectedValues) { - return; - } + const selectedValue = await vscode.window.showQuickPick( + choices.map((choice, index) => ({ + label: choice, + description: tierText, + iconPath: index !== 0 ? tierIcon : undefined, + picked: index === 0, + })) as vscode.QuickPickItem[], + { + title: `Luraph - Select Option: ${name}${tierTextParen} - ${description} [${optionId}]`, + placeHolder: `Value for ${name}`, - for (const checkboxInfo of selectedValues) { - optionValues[checkboxInfo.id] = true; - } + ignoreFocusOut: true, + canPickMany: false, + matchOnDetail: true, + } + ); - for (const { id, title, placeHolder, items } of dropdowns) { - const selectedValue = await vscode.window.showQuickPick(items, { - title: `Luraph - Select Option: ${title}`, - placeHolder, + if (!selectedValue) { + return; + } - ignoreFocusOut: true, - canPickMany: false, - matchOnDetail: true, - }); + userOptionValues[optionId] = selectedValue.label; - if (!selectedValue) { - return; - } + i += 1; - optionValues[id] = selectedValue.label; - } + break; + } + case "TEXT": { + userOptionValues[optionId] = ""; - for (const { id, title, prompt, placeHolder } of textFields) { - const selectedValue = await vscode.window.showInputBox({ - title: `Luraph - Select Option: ${title}`, - prompt, - placeHolder, + const selectedValue = await vscode.window.showInputBox({ + title: `Luraph - Select Option: ${name}${tierTextParen} [${optionId}]`, + prompt: description, + placeHolder: `Value for ${name} (leave empty to use default value)`, - ignoreFocusOut: true, - }); + ignoreFocusOut: true, + }); - if (!selectedValue) { - return; - } + if (!selectedValue) { + return; + } + + userOptionValues[optionId] = selectedValue; + + i += 1; - optionValues[id] = selectedValue; + break; + } + default: + throw new Error(`Received invalid option type: ${type}`); + } } statusBarItem.text = "$(gear~spin) Obfuscating..."; @@ -379,7 +426,7 @@ export function activate(context: vscode.ExtensionContext) { nodeId, contents, `[luraph-vscode] ${fileName}`, - optionValues + userOptionValues ); log(`> Job ID: ${jobId}`); From 5d0bb1d52c0e7e76fdefe52c52873ead3aa2f7be Mon Sep 17 00:00:00 2001 From: Roman <22231294+Lutymane@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:38:11 +0500 Subject: [PATCH 5/5] feat: settings confirmation --- src/extension.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index a4f3d5b..7bc9bda 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -421,6 +421,24 @@ export function activate(context: vscode.ExtensionContext) { } } + const confirm = await vscode.window.showInformationMessage( + "Confirm options", + { + modal: true, + detail: Object.entries(userOptionValues) + .map( + ([id, val]) => + `${nodeInfo.options[id].name}: ${ + typeof val === "boolean" ? (val ? "✅" : "❌") : val + }` + ) + .join("\n"), + }, + "Obfuscate" + ); + + if (confirm !== "Obfuscate") return; + statusBarItem.text = "$(gear~spin) Obfuscating..."; const { jobId } = await luraphApi.createNewJob( nodeId,