diff --git a/docs/MPQEditor.md b/docs/MPQEditor.md new file mode 100644 index 00000000..eba70cb7 --- /dev/null +++ b/docs/MPQEditor.md @@ -0,0 +1,65 @@ +# MixedPrecisionQantizationEditor (MPQEditor for short) + +## About + +MPQEditor povides easy way of creation/editing MPQ json files, suitable for +Quantization step in CfgEditor. Also it gives the ability to select the most +sensitive to quantization layers using `visq` data. + +## How to use + +Currently MPQEditor is integrated into EXPLORER view by context menu item +"Create MPQ json". + +In order to use it: +1) call context menu for a `.circle` model in EXPLORER view +2) call "Create MPQ json" +3) give a name to MPQ json +4) edit MPQ json using MPQEditor +5) all layers from 'layers' section in json are shown in grid-table, so +that their options for quantization can be edited using their respective +dropdowns. Layers which don't belong to 'layers' section will be quantized +using default parameters. +6) to make 'model-graph-view' more informative the user may use 'visq' +information, it shows which layers are more sensitive to quantization than +others + +## Screen layout +Layout is divided into three main parts: +1) Default Quantization Parameters + + + +2) 'Layers' section + + + +3) Model graph view + + + +### Layers section + + +1. `Add more layers` + - use it to add more layers for editing their quantization properties +2. `VISQ control` + - info + - actual path + - 'load visq' button + - 'clear visq' button +3. `Model graph is shown/hidden` + - use this to enable selection on `Model graph view` +4. `Individual layer` + - it consists of quantization properties set for each layer +5. `Quantization properties of layer` + - quantization and granularity fields for each layer +6. `Restore layer` + - use it to restore the layer to default state + + +For the whole workflow see: + + + + diff --git a/docs/images/MpqDefaultSection.png b/docs/images/MpqDefaultSection.png new file mode 100644 index 00000000..7c75fbd7 Binary files /dev/null and b/docs/images/MpqDefaultSection.png differ diff --git a/docs/images/MpqLayersSection.png b/docs/images/MpqLayersSection.png new file mode 100644 index 00000000..8eea2331 Binary files /dev/null and b/docs/images/MpqLayersSection.png differ diff --git a/docs/images/MpqLayersSectionGen.png b/docs/images/MpqLayersSectionGen.png new file mode 100644 index 00000000..7125ed0d Binary files /dev/null and b/docs/images/MpqLayersSectionGen.png differ diff --git a/docs/images/MpqModelGraphView.png b/docs/images/MpqModelGraphView.png new file mode 100644 index 00000000..6e1a3a5e Binary files /dev/null and b/docs/images/MpqModelGraphView.png differ diff --git a/docs/images/MpqWorkflow.gif b/docs/images/MpqWorkflow.gif new file mode 100644 index 00000000..91a096fc Binary files /dev/null and b/docs/images/MpqWorkflow.gif differ diff --git a/media/CircleGraph/index.js b/media/CircleGraph/index.js index 96a6bb21..7c77e62f 100644 --- a/media/CircleGraph/index.js +++ b/media/CircleGraph/index.js @@ -52,6 +52,7 @@ const viewMode = { viewer: 0, // default circle viewer selector: 1, // circle partition editor node selector visq: 2, // quantization error viewer + visqselector: 3, //quantization error viewer which is able to select nodes // refer https://github.com/Samsung/ONE-vscode/issues/1350 }; @@ -97,6 +98,8 @@ host.BrowserHost = class { this._mode = viewMode.selector; } else if (__viewMode === "visq") { this._mode = viewMode.visq; + } else if (__viewMode === "visqselector") { + this._mode = viewMode.visqselector; } } @@ -167,6 +170,9 @@ host.BrowserHost = class { case "visq": this._msgVisq(message); break; + case "scrollToSelected": + this._view.setScrollToSelected(message.value); + break; } }); @@ -263,7 +269,7 @@ host.BrowserHost = class { }); this._view.show("welcome spinner"); - if (this._mode === viewMode.visq) { + if (this._mode === viewMode.visq || this._mode === viewMode.visqselector) { // request visq data prior to model // model is inside visq data vscode.postMessage({ command: "visq" }); diff --git a/media/CircleGraph/view.js b/media/CircleGraph/view.js index 58f50ab8..ed837306 100644 --- a/media/CircleGraph/view.js +++ b/media/CircleGraph/view.js @@ -542,13 +542,21 @@ view.View = class { } } + setScrollToSelected(value) { + this._scrollToSelected = value; + } + /** * @brief toggleSelect will select or toggle select with CtrlKey down * @param viewNode view.Node instance * @note works on host mode is viewMode.selector */ toggleSelect(viewNode) { - if (viewNode && this._host._mode === viewMode.selector) { + if ( + viewNode && + (this._host._mode === viewMode.selector || + this._host._mode === viewMode.visqselector) + ) { if (this._keyCtrl) { // toggle selection let index = this._selectionNodes.indexOf(viewNode); @@ -585,7 +593,10 @@ view.View = class { clearSelection() { this._clearSelection(); - if (host._mode === viewMode.selector) { + if ( + host._mode === viewMode.selector || + this._host._mode === viewMode.visqselector + ) { this._host.onView("selection"); } } @@ -1098,7 +1109,10 @@ view.View = class { } applyStyleSheetVisq(element) { - if (this._host._mode === viewMode.visq) { + if ( + this._host._mode === viewMode.visq || + this._host._mode === viewMode.visqselector + ) { let rules = []; for (const styleSheet of this._host.document.styleSheets) { if (styleSheet.title === "visq_style") { @@ -1503,7 +1517,7 @@ view.Node = class extends grapher.Node { _visq(node) { const host = this.context.view._host; - if (host._mode !== viewMode.visq) { + if (host._mode !== viewMode.visq && host._mode !== viewMode.visqselector) { return; } @@ -1535,7 +1549,7 @@ view.Node = class extends grapher.Node { } } let visqSuffix = undefined; - if (host._mode === viewMode.visq) { + if (host._mode === viewMode.visq || host._mode === viewMode.visqselector) { if (node.visq_index) { let qstyle = `node-item-type-visq-${node.visq_index}`; styles.push(qstyle); @@ -1571,7 +1585,10 @@ view.Node = class extends grapher.Node { if (host._mode === viewMode.viewer || host._mode === viewMode.visq) { title.on("click", () => this.context.view.showNodeProperties(node, null)); - } else if (host._mode === viewMode.selector) { + } else if ( + host._mode === viewMode.selector || + host._mode === viewMode.visqselector + ) { // toggle select with click title.on("click", () => { this.context.view.toggleSelect(this); diff --git a/media/MPQEditor/index.html b/media/MPQEditor/index.html new file mode 100644 index 00000000..4b68f953 --- /dev/null +++ b/media/MPQEditor/index.html @@ -0,0 +1,87 @@ + + + + + + + + Mixed Precision Quantization editor + + + + + + +
+ Edit mixed precision quantization json file +
+
+
+
+ + + uint8 + int16 + +
+
+ + + layer + channel + +
+
+
+
+ +
+
+
+ + + +
+
+ + + + visq.json file with layer-wise quantization errors + + + + + + Model Graph +
+
+
+ + + Name + Quantization + Granularity +   + + +
+ + diff --git a/media/MPQEditor/index.js b/media/MPQEditor/index.js new file mode 100644 index 00000000..efefd2a1 --- /dev/null +++ b/media/MPQEditor/index.js @@ -0,0 +1,416 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const vscode = acquireVsCodeApi(); + +// Just like a regular webpage we need to wait for the webview +// DOM to load before we can reference any of the HTML elements +// or toolkit components +window.addEventListener("load", main); + +function main() { + register(); + + window.addEventListener("message", (event) => { + const message = event.data; + switch (message.type) { + case "displayMPQ": + displayMPQToEditor(message.content); + break; + //case "selectionChanged": + // handleSelectionChanged(message.names); + // break; + case "modelNodesChanged": + handleModelNodesChanged(message.names); + break; + case "VisqFileLoaded": + handleVisqFileLoaded(message.visqFile); + break; + case "modelGraphIsShown": + handleModelGraphIsShown(message.shown); + break; + default: + break; + } + }); + + vscode.postMessage({ type: "requestModelNodes" }); + vscode.postMessage({ type: "requestDisplayMPQ" }); + vscode.postMessage({ type: "showModelNodes" }); +} + +function register() { + registerMainControls(); +} + +function handleModelGraphIsShown(shown) { + document.getElementById("circle-graph").checked = shown; +} + +function handleVisqFileLoaded(visqFile) { + document.getElementById("VisqInputPath").value = visqFile; +} + +//function handleSelectionChanged(names) { +// document.getElementById("RemoveSelected").disabled = names.length < 1; +//} + +function handleModelNodesChanged(names) { + document.getElementById("AddSpecificLayer").disabled = names.length < 1; +} + +function registerMainControls() { + document + .getElementById("DefaultDtype") + .addEventListener("click", function () { + updateDefaultQuantization(); + applyUpdates(); + }); + + document + .getElementById("DefaultGranularity") + .addEventListener("click", function () { + updateGranularity(); + applyUpdates(); + }); + + document + .getElementById("AddSpecificLayer") + .addEventListener("click", function () { + vscode.postMessage({ + type: "addSpecificLayerFromDialog", + }); + }); + + //document + // .getElementById("RemoveSelected") + // .addEventListener("click", function () { + // vscode.postMessage({ + // type: "removeSelectedFromLayers", + // }); + // }); + + // show model graph on openening by default + document.getElementById("circle-graph").checked = true; + document + .getElementById("circle-graph") + .addEventListener("click", function () { + vscode.postMessage({ + type: "toggleCircleGraphIsShown", + show: document.getElementById("circle-graph").checked, + }); + }); + + document + .getElementById("VisqInputPath") + .addEventListener("input", function () { + vscode.postMessage({ + type: "VisqInputPathChanged", + path: document.getElementById("VisqInputPath").value, + }); + }); + + document.getElementById("visq-file").addEventListener("click", function () { + vscode.postMessage({ + type: "loadVisqFile", + }); + }); + + document.getElementById("visq-delete").addEventListener("click", function () { + document.getElementById("VisqInputPath").value = ""; + vscode.postMessage({ + type: "removeVisqFile", + }); + }); + + //document.getElementById("RemoveSelected").disabled = true; + document.getElementById("AddSpecificLayer").disabled = true; +} + +function displayMPQToEditor(mpqCfg) { + document.getElementById("DefaultDtype").value = + mpqCfg?.["default_quantization_dtype"]; + document.getElementById("DefaultGranularity").value = + mpqCfg?.["default_granularity"]; + + const length = mpqCfg && mpqCfg["layers"] ? mpqCfg["layers"].length : 0; + + let names = Array(length); + let quantization = Array(length); + let granularity = Array(length); + for (let i = 0; i < length; i++) { + names[i] = mpqCfg["layers"][i]["name"]; + quantization[i] = mpqCfg["layers"][i]["dtype"]; + granularity[i] = mpqCfg["layers"][i]["granularity"]; + } + + const layersTable = document.getElementById("LayersTable"); + layersTable.replaceChildren(); + addQuantizedNodes(names, quantization, granularity, false); +} + +function addQuantizedNodes(names, quantization, granularity, update) { + const layersTable = document.getElementById("LayersTable"); + // for (let idx = 0; idx < names.length; idx++) { + // if (names[idx].length < 1) { + // continue; + // } + // + // let row = document.createElement("vscode-data-grid-row"); + // + // const name = names[idx]; + // // selection check + // let cellSwitch = document.createElement("vscode-data-grid-cell"); + // let checkbox = document.createElement("vscode-checkbox"); + // checkbox.setAttribute("id", "checkboxSelect" + name); + // cellSwitch.appendChild(checkbox); + // cellSwitch.setAttribute("grid-column", "1"); + // row.appendChild(cellSwitch); + // + // // name + // let cellName = document.createElement("vscode-data-grid-cell"); + // cellName.textContent = name; + // cellName.setAttribute("grid-column", "2"); + // row.appendChild(cellName); + // + // // quantization + // let cellQuantization = document.createElement("vscode-data-grid-cell"); + // let quantDropdown = document.createElement("vscode-dropdown"); + // { + // let uint8Opt = document.createElement("vscode-option"); + // uint8Opt.innerText = "uint8"; + // uint8Opt.value = 0; + // quantDropdown.appendChild(uint8Opt); + // + // let int16Opt = document.createElement("vscode-option"); + // int16Opt.innerText = "int16"; + // int16Opt.value = 1; + // quantDropdown.appendChild(int16Opt); + // } + // quantDropdown.setAttribute("id", "dropdownQuantization" + name); + // cellQuantization.appendChild(quantDropdown); + // cellQuantization.setAttribute("grid-column", "3"); + // row.appendChild(cellQuantization); + // + // // granularity + // let cellGranularity = document.createElement("vscode-data-grid-cell"); + // let granularityDropdown = document.createElement("vscode-dropdown"); + // { + // let layerOpt = document.createElement("vscode-option"); + // layerOpt.innerText = "layer"; + // layerOpt.value = 0; + // granularityDropdown.appendChild(layerOpt); + // + // let channelOpt = document.createElement("vscode-option"); + // channelOpt.innerText = "channel"; + // channelOpt.value = 1; + // granularityDropdown.appendChild(channelOpt); + // } + // granularityDropdown.setAttribute("id", "dropdownGranularity" + name); + // cellGranularity.appendChild(granularityDropdown); + // cellGranularity.setAttribute("grid-column", "4"); + // row.appendChild(cellGranularity); + // + // layersTable.appendChild(row); + // } + + for (let idx = 0; idx < names.length; idx++) { + if (names[idx].length < 1) { + continue; + } + + let row = document.createElement("vscode-data-grid-row"); + const name = names[idx]; + + // name + let cellName = document.createElement("vscode-data-grid-cell"); + cellName.textContent = name; + cellName.setAttribute("grid-column", "1"); + row.appendChild(cellName); + // quantization + let cellQuantization = document.createElement("vscode-data-grid-cell"); + let quantDropdown = document.createElement("vscode-dropdown"); + { + let uint8Opt = document.createElement("vscode-option"); + uint8Opt.innerText = "uint8"; + uint8Opt.value = 0; + quantDropdown.appendChild(uint8Opt); + let int16Opt = document.createElement("vscode-option"); + int16Opt.innerText = "int16"; + int16Opt.value = 1; + quantDropdown.appendChild(int16Opt); + } + quantDropdown.setAttribute("id", "dropdownQuantization" + name); + cellQuantization.appendChild(quantDropdown); + cellQuantization.setAttribute("grid-column", "2"); + row.appendChild(cellQuantization); + // granularity + let cellGranularity = document.createElement("vscode-data-grid-cell"); + let granularityDropdown = document.createElement("vscode-dropdown"); + { + let layerOpt = document.createElement("vscode-option"); + layerOpt.innerText = "layer"; + layerOpt.value = 0; + granularityDropdown.appendChild(layerOpt); + let channelOpt = document.createElement("vscode-option"); + channelOpt.innerText = "channel"; + channelOpt.value = 1; + granularityDropdown.appendChild(channelOpt); + } + granularityDropdown.setAttribute("id", "dropdownGranularity" + name); + cellGranularity.appendChild(granularityDropdown); + cellGranularity.setAttribute("grid-column", "3"); + row.appendChild(cellGranularity); + + // remove button + let cellRemoveBtn = document.createElement("vscode-data-grid-cell"); + let remBtn = document.createElement("vscode-button"); + remBtn.setAttribute("id", "removeButton" + name); + remBtn.appearance = "icon"; + { + let iconSpan = document.createElement("span"); + iconSpan.className = "codicon codicon-chrome-close"; + iconSpan.slot = "start"; + remBtn.appendChild(iconSpan); + } + cellRemoveBtn.appendChild(remBtn); + cellRemoveBtn.setAttribute("grid-column", "4"); + row.appendChild(cellRemoveBtn); + + layersTable.appendChild(row); + } + + // set quantization and granularity attributes + for (let idx = 0; idx < names.length; idx++) { + if (names[idx].length < 1) { + continue; + } + + const name = names[idx]; + const qValue = quantization[idx] === "uint8" ? 0 : 1; + document.getElementById("dropdownQuantization" + name).value = qValue; + + const gValue = granularity[idx] === "layer" ? 0 : 1; + document.getElementById("dropdownGranularity" + name).value = gValue; + } + + // set change values + for (let idx = 0; idx < names.length; idx++) { + if (names[idx].length < 1) { + continue; + } + + const name = names[idx]; + document + .getElementById("dropdownQuantization" + name) + .addEventListener("change", function () { + updateSpecificQuantization(name); + applyUpdates(); + }); + + document + .getElementById("dropdownGranularity" + name) + .addEventListener("change", function () { + updateSpecificGranularity(name); + applyUpdates(); + }); + + // document + // .getElementById("checkboxSelect" + name) + // .addEventListener("click", function () { + // updateLayersSelection(name); + // }); + document + .getElementById("removeButton" + name) + .addEventListener("click", function () { + removeLayer(name); + }); + } + + if (update) { + updateLayers(names, quantization, granularity); + applyUpdates(); + } +} + +//function updateLayersSelection(name) { +// vscode.postMessage({ +// type: "toggleSelectedNode", +// name: name, +// }); +//} + +function updateLayers(names, quantization, granularity) { + vscode.postMessage({ + type: "updateLayers", + names: names, + quantization: quantization, + granularity: granularity, + }); +} + +function removeLayer(name) { + vscode.postMessage({ + type: "removeLayer", + name: name, + }); +} + +function updateSpecificQuantization(name) { + const value = + document.getElementById("dropdownQuantization" + name).value === 0 + ? "uint8" + : "int16"; + vscode.postMessage({ + type: "updateSpecificQuantization", + name: name, + value: value, + }); +} + +function updateSpecificGranularity(name) { + const value = + document.getElementById("dropdownGranularity" + name).value === 0 + ? "layer" + : "channel"; + vscode.postMessage({ + type: "updateSpecificGranularity", + name: name, + value: value, + }); +} + +function updateDefaultQuantization() { + let value = document.getElementById("DefaultDtype").value; + vscode.postMessage({ + type: "updateSection", + section: "default_quantization_dtype", + value: value, + }); +} + +function updateGranularity() { + let value = document.getElementById("DefaultGranularity").value; + vscode.postMessage({ + type: "updateSection", + section: "default_granularity", + value: value, + }); +} + +function applyUpdates() { + vscode.postMessage({ type: "updateDocument" }); +} diff --git a/media/MPQEditor/style.css b/media/MPQEditor/style.css new file mode 100644 index 00000000..285b093f --- /dev/null +++ b/media/MPQEditor/style.css @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +label { +white-space: pre-wrap; +} + +body { +min-width: 500px; +width: auto !important; +} + +.maintitle { +width: 100%; +display: flex; +font-size: medium; +} + +.maintitle span { +float: left; +display: block; +padding-left: 5px; +} + +.leftbtns { +float: left; +} + +.rightbtns { +float: right; +} + +.codicon.codicon-question .help { +visibility: hidden; +width: auto; +background-color: var(--vscode-editorSuggestWidget-background); +color: var(--vscode-editorSuggestWidget-foreground); +border: 2px solid var(--vscode-editorSuggestWidget-border); +font-size: small; +text-align: left; +position: absolute; +z-index: 1; +margin-left: 10px; +padding: 6px 6px 10px 8px; +font-family: var(--vscode-font-family); +} + +.codicon.codicon-question:hover .help { +visibility: visible; +} diff --git a/package.json b/package.json index ff229e69..71fc4191 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,16 @@ } ], "priority": "default" + }, + { + "viewType": "one.editor.mpq", + "displayName": "MPQ Editor", + "selector": [ + { + "filenamePattern": "*.mpq.json" + } + ], + "priority": "default" } ], "commands": [ @@ -256,6 +266,16 @@ "command": "one.editor.cfg.setDefaultValues", "title": "ONE: Set Default Values", "category": "ONE" + }, + { + "command": "one.editor.mpq.showFromDefaultExplorer", + "title": "Create MPQ json", + "category": "ONE" + }, + { + "command": "one.editor.mpq.showFromOneExplorer", + "title": "Create MPQ json", + "category": "ONE" } ], "keybindings": [ @@ -363,6 +383,11 @@ "command": "one.viewer.metadata.showFromOneExplorer", "when": "view == OneExplorerView && viewItem =~ /baseModel|product/", "group": "7_metadata@1" + }, + { + "command": "one.editor.mpq.showFromOneExplorer", + "when": "view == OneExplorerView && viewItem == product && !one.job:running", + "group": "6_tools" } ], "explorer/context": [ @@ -370,6 +395,11 @@ "command": "one.viewer.metadata.showFromDefaultExplorer", "when": "resourceExtname in one.metadata.supportedFiles && !explorerResourceIsFolder", "group": "7_metadata@1" + }, + { + "command": "one.editor.mpq.showFromDefaultExplorer", + "when": "resourceExtname == .circle", + "group": "6_tools" } ], "commandPalette": [ @@ -452,6 +482,10 @@ { "command": "one.viewer.metadata.showFromOneExplorer", "when": "false" + }, + { + "command": "one.editor.mpq.showFromDefaultExplorer", + "when": "false" } ] }, diff --git a/src/CircleGraph/CircleGraphCtrl.ts b/src/CircleGraph/CircleGraphCtrl.ts index c8152c05..556aedd0 100644 --- a/src/CircleGraph/CircleGraphCtrl.ts +++ b/src/CircleGraph/CircleGraphCtrl.ts @@ -55,6 +55,7 @@ export class MessageDefs { public static readonly backendColor = "backendColor"; public static readonly error = "error"; public static readonly colorTheme = "colorTheme"; + public static readonly scrollToSelected = "scrollToSelected"; // loadmodel type public static readonly modelpath = "modelpath"; public static readonly uint8array = "uint8array"; diff --git a/src/MPQEditor/MPQCircleSelector.ts b/src/MPQEditor/MPQCircleSelector.ts new file mode 100644 index 00000000..7901a494 --- /dev/null +++ b/src/MPQEditor/MPQCircleSelector.ts @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from "path"; +import * as vscode from "vscode"; + +import { + CircleGraphCtrl, + CircleGraphEvent, + MessageDefs, +} from "../CircleGraph/CircleGraphCtrl"; + +export interface MPQSelectionEvent { + onSelection(names: string[], document: vscode.TextDocument): void; + onClosed(panel: vscode.WebviewPanel): void; + onOpened(panel: vscode.WebviewPanel): void; + onFailedToLoadVISQFile( + visqPath: string, + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ): void; +} + +export type MPQSelectionCmdOpenArgs = { + modelPath: string; + document: vscode.TextDocument; + names: any; + panel: vscode.WebviewPanel; +}; + +export type MPQVisqData = { + visqPath: string; +}; + +export type MPQSelectionCmdCloseArgs = { + modelPath: string; + document: vscode.TextDocument; +}; + +export type MPQSelectionCmdLayersChangedArgs = { + modelPath: string; + document: vscode.TextDocument; + names: any; +}; + +export class MPQSelectionPanel + extends CircleGraphCtrl + implements CircleGraphEvent +{ + public static readonly viewType = "one.viewer.mpq"; + public static readonly cmdOpen = "one.viewer.mpq.openGraphSelector"; + public static readonly cmdClose = "one.viewer.mpq.closeGraphSelector"; + public static readonly cmdChanged = "one.viewer.mpq.layersChangedByOwner"; + public static readonly cmdOpenVisq = "one.viewer.mpq.loadVisq"; + + public static panels: MPQSelectionPanel[] = []; + + private _panel: vscode.WebviewPanel; + private _disposables: vscode.Disposable[] = []; + private _document: vscode.TextDocument; + private _ownerPanel: vscode.WebviewPanel; + private _ownerId: string; // stringified uri of the owner document + private _modelPath: string; // circle file path + private _mpqEventHandler?: MPQSelectionEvent; + private _lastSelected: string[]; + private _visqData: string[]; + private _closedByOwner: boolean = false; + + public static register(context: vscode.ExtensionContext): void { + const registrations = [ + vscode.commands.registerCommand( + MPQSelectionPanel.cmdOpen, + (args: MPQSelectionCmdOpenArgs, handler: MPQSelectionEvent) => { + MPQSelectionPanel.createOrShow( + context.extensionUri, + args, + "", + handler + ); + } + ), + vscode.commands.registerCommand( + MPQSelectionPanel.cmdClose, + (args: MPQSelectionCmdCloseArgs) => { + MPQSelectionPanel.closeByOwner(context.extensionUri, args); + } + ), + vscode.commands.registerCommand( + MPQSelectionPanel.cmdChanged, + (args: MPQSelectionCmdLayersChangedArgs) => { + MPQSelectionPanel.forwardSelectionByOwner(context.extensionUri, args); + } + ), + vscode.commands.registerCommand( + MPQSelectionPanel.cmdOpenVisq, + ( + args: MPQSelectionCmdOpenArgs, + visqData: MPQVisqData, + handler: MPQSelectionEvent + ) => { + MPQSelectionPanel.createOrShow( + context.extensionUri, + args, + visqData.visqPath, + handler + ); + } + ), + // TODO add more commands + ]; + + registrations.forEach((disposable) => + context.subscriptions.push(disposable) + ); + } + + public static createOrShow( + extensionUri: vscode.Uri, + args: MPQSelectionCmdOpenArgs, + visqPath: string, + handler: MPQSelectionEvent | undefined + ) { + let column = args.panel.viewColumn; + if (column) { + if (column >= vscode.ViewColumn.One) { + column = column + 1; + } + } + + // search for existing panel + const oldPanel = MPQSelectionPanel.findSelPanel( + args.modelPath, + args.document.uri.toString() + ); + if (oldPanel) { + oldPanel._panel.reveal(column); + return; + } + + // Otherwise, create a new panel. + const lastSlash = args.modelPath.lastIndexOf(path.sep) + 1; + const fileNameExt = args.modelPath.substring(lastSlash); + const panel = vscode.window.createWebviewPanel( + MPQSelectionPanel.viewType, + fileNameExt, + column || vscode.ViewColumn.Two, + { retainContextWhenHidden: true } + ); + + const graphSelPanel = new MPQSelectionPanel( + panel, + extensionUri, + args, + visqPath, + handler + ); + + MPQSelectionPanel.panels.push(graphSelPanel); + graphSelPanel.loadContent(); + graphSelPanel.onForwardSelection(args.names); + } + + private static findSelPanel( + docPath: string, + id: string + ): MPQSelectionPanel | undefined { + let result = undefined; + MPQSelectionPanel.panels.forEach((selpan) => { + if (docPath === selpan._modelPath && id === selpan._ownerId) { + result = selpan; + } + }); + return result; + } + + /** + * @brief called when owner is closing + */ + public static closeByOwner( + extensionUri: vscode.Uri, + args: MPQSelectionCmdCloseArgs + ) { + let selPanel = MPQSelectionPanel.findSelPanel( + args.modelPath, + args.document.uri.toString() + ); + if (selPanel) { + selPanel._closedByOwner = true; + selPanel.dispose(); + } + } + + /** + * @brief called when owner selection state of nodes has changed + */ + public static forwardSelectionByOwner( + extensionUri: vscode.Uri, + args: MPQSelectionCmdLayersChangedArgs + ) { + let selPanel = MPQSelectionPanel.findSelPanel( + args.modelPath, + args.document.uri.toString() + ); + if (selPanel) { + selPanel.onForwardSelection(args.names); + } + } + + private constructor( + panel: vscode.WebviewPanel, + extensionUri: vscode.Uri, + args: MPQSelectionCmdOpenArgs, + visqPath: string, + handler: MPQSelectionEvent | undefined + ) { + super(extensionUri, panel.webview); + + this._panel = panel; + this._ownerId = args.document.uri.toString(); + this._document = args.document; + this._ownerPanel = args.panel; + this._modelPath = args.modelPath; + this._mpqEventHandler = handler; + this._lastSelected = args.names; + this._visqData = []; + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programmatically + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + this.initGraphCtrl(this._modelPath, this); + if (visqPath.length < 1) { + this.setMode("selector"); + } else { + this.setMode("visqselector"); + const visqUri = vscode.Uri.file(visqPath); + vscode.workspace.fs.readFile(visqUri).then((visqData) => { + try { + this._visqData = JSON.parse(visqData.toString()); + } catch (error) { + this.onInvalidVISQData(visqPath); + } + + // check whether _visqData pretend to be valid + if (!("error" in this._visqData) || !("meta" in this._visqData)) { + this.onInvalidVISQData(visqPath); + } + }); + } + + if (this._mpqEventHandler) { + this._mpqEventHandler.onOpened(this._ownerPanel); + } + + this.sendScrollToSelected(); + } + + private onInvalidVISQData(visqPath: string) { + this._visqData = []; + if (this._mpqEventHandler) { + this._mpqEventHandler.onFailedToLoadVISQFile( + visqPath, + this._document, + this._ownerPanel + ); + } + } + + public dispose() { + if (!this._closedByOwner && this._mpqEventHandler) { + this._mpqEventHandler.onClosed(this._ownerPanel); + } + + this.disposeGraphCtrl(); + + MPQSelectionPanel.panels.forEach((selPan, index) => { + if ( + this._ownerId === selPan._ownerId && + this._modelPath === selPan._modelPath + ) { + MPQSelectionPanel.panels.splice(index, 1); + } + }); + + // Clean up our resources + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + if (x) { + x.dispose(); + } + } + } + + /** + * CircleGraphEvent interface implementations + */ + public onViewMessage(message: any) { + switch (message.command) { + case MessageDefs.selection: + this._lastSelected = message.names; + // we need to update the document, but not save to file. + // pass to owner to handle this. + if (this._mpqEventHandler) { + this._mpqEventHandler.onSelection(message.names, this._document); + } + break; + case MessageDefs.finishload: + this.onForwardSelection(this._lastSelected); + break; + case MessageDefs.visq: + this.sendVisq(this._visqData); + break; + } + } + + public onForwardSelection(selection: any) { + let selections: string[] = []; + let items = selection as Array; + for (let i = 0; i < items.length; i++) { + if (items[i].length > 0) { + selections.push(items[i]); + } + } + this.setSelection(selections); + } + + public setTitle(title: string) { + this._panel.title = title; + } + + public sendScrollToSelected() { + this._webview.postMessage({ + command: MessageDefs.scrollToSelected, + value: false, + }); + } +} diff --git a/src/MPQEditor/MPQEditor.ts b/src/MPQEditor/MPQEditor.ts new file mode 100644 index 00000000..9e209680 --- /dev/null +++ b/src/MPQEditor/MPQEditor.ts @@ -0,0 +1,865 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as cp from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { Balloon } from "../Utils/Balloon"; +import { Logger } from "../Utils/Logger"; +import { getNonce } from "../Utils/external/Nonce"; +import { getUri } from "../Utils/external/Uri"; + +import { MPQSelectionEvent, MPQSelectionPanel } from "./MPQCircleSelector"; +import { + MPQSelectionCmdCloseArgs, + MPQSelectionCmdOpenArgs, + MPQSelectionCmdLayersChangedArgs, + MPQVisqData, +} from "./MPQCircleSelector"; + +class MPQData { + private _content: any; + private _allModelNodes: string[] | undefined; + private _modelNodes?: string[]; + private _selectedNodes: Set; + private _visqPath: string = ""; // empty means no visqData is provided + + static _layersKey: string = "layers"; + static _nameKey: string = "name"; + static _defQuantizationKey: string = "default_quantization_dtype"; + static _defGranularityKey: string = "default_granularity"; + + constructor() { + this._selectedNodes = new Set(); + } + + getAsString(): string { + return JSON.stringify(this._content, null, " "); + } + + getValue(key: string): string { + return this._content[key].toString(); + } + + setWithString(text: string) { + this._content = JSON.parse(text); + } + + updateSection(section: string, value: string) { + this._content[section] = value; + } + + setAllModelNodes(modelNodes: string[]) { + this._allModelNodes = modelNodes.filter((name) => name.length > 0); + //filter by content + this.filterModelNodesbyContent(); + } + + filterModelNodesbyContent() { + let curLayers = this.getLayers(); + this.filterModelNodesBy(curLayers); + } + + getLayers() { + return this._content[MPQData._layersKey].map( + (item: any) => item[MPQData._nameKey] + ); + } + + setLayers(names: string[]): string[] { + let layersToAdd = Array(); + let layersToDelete = Array(); + this._content[MPQData._layersKey].forEach((layer: any) => { + let foundIndex = names.findIndex( + (name: string) => name === layer[MPQData._nameKey] + ); + if (foundIndex < 0) { + // name to delete + layersToDelete.push(layer["name"]); + } + }); + names.forEach((name: any) => { + let foundIndex = this._content[MPQData._layersKey].findIndex( + (x: any) => x[MPQData._nameKey] === name + ); + if (foundIndex < 0) { + // name to add + layersToAdd.push(name); + } + }); + this.removeModelNodesFromLayers(layersToDelete); + this.addLayers(layersToAdd); + + return layersToAdd; + } + + addLayers(names: string[]): void { + if (names.length < 1) { + return; + } + + const otherQuantization = + this._content[MPQData._defQuantizationKey] === "uint8" + ? "int16" + : "uint8"; + let quantization = Array(names.length); + quantization.fill(otherQuantization); + let granularity = Array(names.length); + granularity.fill(this._content[MPQData._defGranularityKey]); + this.updateLayers(names, quantization, granularity); + } + + filterModelNodesBy(filter: string[]) { + this._modelNodes = this._allModelNodes?.filter( + (name) => filter.find((filterName) => name === filterName) === undefined + ); + } + + getAllModelNodes(): string[] | undefined { + return this._modelNodes; + } + + updateLayers(names: string[], quantization: string[], granularity: string[]) { + if (!(MPQData._layersKey in this._content)) { + this._content[MPQData._layersKey] = []; + } + for (let i = 0; i < names.length; i++) { + let layer = { + name: names[i], + dtype: quantization[i], + granularity: granularity[i], + }; + this._content[MPQData._layersKey].push(layer); + } + this.filterModelNodesbyContent(); + } + + updateSectionOfLayer(name: string, section: string, value: string) { + let layer = this._content[MPQData._layersKey].find( + (x: any) => x["name"] === name + ); + if (layer) { + layer[section] = value; + } + } + + toggleSelectedNode(name: string) { + if (this._selectedNodes.has(name)) { + this._selectedNodes.delete(name); + } else { + this._selectedNodes.add(name); + } + } + + getSelected(): Set { + return this._selectedNodes; + } + + clearSelection() { + this._selectedNodes.clear(); + } + + removeModelNodesFromLayers(names: any) { + names.forEach((name: any) => { + let foundIndex = this._content[MPQData._layersKey].findIndex( + (x: any) => x["name"] === name + ); + if (foundIndex > -1) { + this._content[MPQData._layersKey].splice(foundIndex, 1); + } + }); + this.filterModelNodesbyContent(); + } + + getVisqPath(): string { + return this._visqPath; + } + + setVisqPath(path: string): void { + this._visqPath = path; + } +} + +export class MPQEditorProvider + implements vscode.CustomTextEditorProvider, MPQSelectionEvent +{ + public static readonly viewType = "one.editor.mpq"; + public static readonly fileExtension = ".mpq.json"; + + private _disposables: vscode.Disposable[] = []; + private _mpqDataMap: any = {}; + + public static register(context: vscode.ExtensionContext): void { + const provider = new MPQEditorProvider(context); + const registrations = [ + vscode.window.registerCustomEditorProvider( + MPQEditorProvider.viewType, + provider, + { + webviewOptions: { retainContextWhenHidden: true }, + } + ), + // Add command registration here + vscode.commands.registerCommand( + "one.editor.mpq.showFromDefaultExplorer", + (uri) => { + MPQEditorProvider.createMPQConfig(uri); + } + ), + vscode.commands.registerCommand( + "one.editor.mpq.showFromOneExplorer", + async (uri) => { + MPQEditorProvider.createMPQConfig(uri); + }), + ]; + + registrations.forEach((disposable) => + context.subscriptions.push(disposable) + ); + } + + public static createMPQConfig(uri: vscode.Uri) { + const extName = path.parse(uri.path).ext.slice(1); + if (extName !== 'circle') { + return; + } + + const dirPath = path.parse(uri.path).dir; + const modelName = path.parse(uri.path).name; + const circleName = path.parse(uri.path).base; + + const content = + '{"default_quantization_dtype": "uint8",' + + '"default_granularity": "channel",' + + '"layers": [],' + + '"model_path": ' + + '"' + + circleName + + '"' + + "}"; + + const findInputPath = (mpqName: string): string => { + const maxIters = 5; + for (let i = 0; i < maxIters; i++) { + const mpqPath: string = path.join( + dirPath, + mpqName + MPQEditorProvider.fileExtension + ); + if (!fs.existsSync(mpqPath)) { + return mpqName + MPQEditorProvider.fileExtension; + } + mpqName = mpqName + "(1)"; + } + return ""; + }; + + let mpqName = findInputPath(modelName); + if (mpqName.length < 1) { + // failed to find valid name, just revert to initial version + mpqName = modelName + MPQEditorProvider.fileExtension; + } + + const validateInputPath = (mpqName: string): string | undefined => { + const mpqPath: string = path.join(dirPath, mpqName); + + if (!mpqPath.endsWith(MPQEditorProvider.fileExtension)) { + return ( + "A file extension must be " + MPQEditorProvider.fileExtension + ); + } + + if (fs.existsSync(mpqPath)) { + return `A file or folder ${mpqPath} already exists at this location. Please choose a different name.`; + } + }; + + vscode.window + .showInputBox({ + title: `Create mixed precision quantization configuration for '${modelName}.${extName}' :`, + placeHolder: `Enter a file name`, + value: mpqName, + valueSelection: [ + 0, + mpqName.length - `${MPQEditorProvider.fileExtension}`.length, + ], + validateInput: validateInputPath, + }) + .then((value) => { + if (!value) { + Logger.debug("MPQEditor", "User hit the escape key!"); + return; + } + + // 'uri' path is not occupied, assured by validateInputPath + const uri = vscode.Uri.file(`${dirPath}/${value}`); + + const edit = new vscode.WorkspaceEdit(); + edit.createFile(uri); + edit.insert(uri, new vscode.Position(0, 0), content); + + vscode.workspace.applyEdit(edit).then((isSuccess) => { + if (isSuccess) { + vscode.workspace.openTextDocument(uri).then((document) => { + document.save(); + vscode.commands.executeCommand( + "vscode.openWith", + uri, + MPQEditorProvider.viewType + ); + }); + } else { + Logger.error( + "MPQEditor", + "CreateMPQ", + `Failed to create the file ${uri}` + ); + } + }); + }); + } + + constructor(private readonly context: vscode.ExtensionContext) {} + + public async resolveCustomTextEditor( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): Promise { + this._mpqDataMap[document.uri.toString()] = new MPQData(); + await this.initWebview(document, webviewPanel); + this.initWebviewPanel(document, webviewPanel); + this.updateWebview(document, webviewPanel.webview); + } + + private async initWebview( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ): Promise { + let webview: vscode.Webview = webviewPanel.webview; + + webview.options = { + enableScripts: true, + }; + + const nonce = getNonce(); + const scriptUri = getUri(webview, this.context.extensionUri, [ + "media", + "MPQEditor", + "index.js", + ]); + const styleUri = getUri(webview, this.context.extensionUri, [ + "media", + "MPQEditor", + "style.css", + ]); + const codiconUri = getUri(webview, this.context.extensionUri, [ + "node_modules", + "@vscode", + "codicons", + "dist", + "codicon.css", + ]); + const toolkitUri = getUri(webview, this.context.extensionUri, [ + "node_modules", + "@vscode", + "webview-ui-toolkit", + "dist", + "toolkit.js", + ]); + const htmlUri = vscode.Uri.joinPath( + this.context.extensionUri, + "media/MPQEditor/index.html" + ); + let html = Buffer.from( + await vscode.workspace.fs.readFile(htmlUri) + ).toString(); + html = html.replace(/\${nonce}/g, `${nonce}`); + html = html.replace(/\${webview.cspSource}/g, `${webview.cspSource}`); + html = html.replace(/\${scriptUri}/g, `${scriptUri}`); + html = html.replace(/\${toolkitUri}/g, `${toolkitUri}`); + html = html.replace(/\${cssUri}/g, `${styleUri}`); + html = html.replace(/\${codiconUri}/g, `${codiconUri}`); + webview.html = html; + + // Receive message from the webview. + webview.onDidReceiveMessage((e) => { + switch (e.type) { + case "requestDisplayMPQ": + this.updateWebview(document, webview); + break; + case "addSpecificLayerFromDialog": + this.hadleAddSpecificLayerFromDialog(document); + break; + case "setModelNodesToDefault": + this._mpqDataMap[document.uri.toString()].removeModelNodesFromLayers( + e.names + ); + break; + case "toggleSelectedNode": + this._mpqDataMap[document.uri.toString()].toggleSelectedNode(e.name); + webview.postMessage({ + type: "selectionChanged", + names: Array.from( + this._mpqDataMap[document.uri.toString()].getSelected() + ), + }); + break; + case "removeSelectedFromLayers": + this.handleRemoveSelectedFromLayers(document, webview); + break; + case "updateLayers": + this._mpqDataMap[document.uri.toString()].updateLayers( + e.names, + e.quantization, + e.granularity + ); + break; + case "updateSpecificQuantization": + this._mpqDataMap[document.uri.toString()].updateSectionOfLayer( + e.name, + "dtype", + e.value + ); + break; + case "updateSpecificGranularity": + this._mpqDataMap[document.uri.toString()].updateSectionOfLayer( + e.name, + "granularity", + e.value + ); + break; + case "requestModelNodes": + this.handleRequestModelNodes(document, webview); + break; + case "updateSection": + this._mpqDataMap[document.uri.toString()].updateSection( + e.section, + e.value + ); + break; + case "updateDocument": + this.updateDocument(document); + break; + case "toggleCircleGraphIsShown": + this.toggleCircleGraphIsShown(e.show, document, webviewPanel); + break; + case "loadVisqFile": + this.loadVisqFile(document, webviewPanel); + break; + case "removeVisqFile": + this.removeVisqFile(document, webviewPanel); + break; + case "VisqInputPathChanged": + this.hadleVisqInputPathChanged(e.path, document, webviewPanel); + break; + case "showModelNodes": + this.handleShowModelNodes(document, webviewPanel); + break; + case "removeLayer": + this.handleRemoveLayerFromLayers(e.name, document, webview); + break; + default: + break; + } + }); + } + + private updateDocument(document: vscode.TextDocument) { + if ( + this._mpqDataMap[document.uri.toString()].getAsString() !== + document.getText() + ) { + // TODO Optimize this to modify only changed lines + const edit = new vscode.WorkspaceEdit(); + edit.replace( + document.uri, + new vscode.Range(0, 0, document.lineCount, 0), + this._mpqDataMap[document.uri.toString()].getAsString() + ); + vscode.workspace.applyEdit(edit); + } + } + + private initWebviewPanel( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ): void { + vscode.commands.executeCommand( + "setContext", + MPQEditorProvider.viewType, + true + ); + + const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument( + (e) => { + if ( + e.contentChanges.length > 0 && + e.document.uri.toString() === document.uri.toString() + ) { + this.updateWebview(document, webviewPanel.webview); + + { + // synchronize circle view + const args: MPQSelectionCmdLayersChangedArgs = { + modelPath: this.getModelFilePath(document), + document: document, + names: this._mpqDataMap[document.uri.toString()].getLayers(), + }; + vscode.commands.executeCommand( + MPQSelectionPanel.cmdChanged, + args, + this + ); + } + } + } + ); + + webviewPanel.onDidChangeViewState( + () => { + vscode.commands.executeCommand( + "setContext", + MPQEditorProvider.viewType, + webviewPanel.visible + ); + }, + null, + this._disposables + ); + + webviewPanel.onDidDispose(() => { + this.closeModelGraphView(document); + + changeDocumentSubscription.dispose(); + while (this._disposables.length) { + const x = this._disposables.pop(); + if (x) { + x.dispose(); + } + } + vscode.commands.executeCommand( + "setContext", + MPQEditorProvider.viewType, + false + ); + }); + } + + private closeModelGraphView(document: vscode.TextDocument): void { + const args: MPQSelectionCmdCloseArgs = { + modelPath: this.getModelFilePath(document), + document: document, + }; + vscode.commands.executeCommand(MPQSelectionPanel.cmdClose, args); + } + + private updateWebview( + document: vscode.TextDocument, + webview: vscode.Webview + ): void { + this._mpqDataMap[document.uri.toString()].setWithString(document.getText()); + const content = JSON.parse(document.getText()); + if (content !== undefined) { + webview.postMessage({ + type: "displayMPQ", + content: content, + }); + } + } + + private hadleAddSpecificLayerFromDialog(document: vscode.TextDocument) { + const nodes = this._mpqDataMap[document.uri.toString()].getAllModelNodes(); + const pickOptions = { + canPickMany: true, + }; + + vscode.window + .showQuickPick(nodes, pickOptions) + .then((values: string | undefined) => { + if (!values) { + return; + } + + this._mpqDataMap[document.uri.toString()].addLayers(values); + this.updateDocument(document); + }); + } + + private handleRemoveSelectedFromLayers( + document: vscode.TextDocument, + webview: vscode.Webview + ) { + let curConf = this._mpqDataMap[document.uri.toString()]; + let selection = curConf.getSelected(); + + curConf.removeModelNodesFromLayers(selection); + curConf.clearSelection(); + + this.updateDocument(document); + webview.postMessage({ + type: "selectionChanged", + names: Array(), + }); + } + + private handleRemoveLayerFromLayers( + name: string, + document: vscode.TextDocument, + webview: vscode.Webview + ) { + let curConf = this._mpqDataMap[document.uri.toString()]; + + curConf.removeModelNodesFromLayers([name]); + curConf.clearSelection(); + + this.updateDocument(document); + webview.postMessage({ + type: "selectionChanged", + names: Array(), + }); + } + + private getModelFilePath(document: vscode.TextDocument): string { + const dirPath = path.parse(document.uri.path).dir; + let fileName = + this._mpqDataMap[document.uri.toString()].getValue("model_path"); + return path.join(dirPath, fileName); + } + + private handleRequestModelNodes( + document: vscode.TextDocument, + webview: vscode.Webview + ): void { + const K_DATA: string = "data"; + const K_EXIT: string = "exit"; + const K_ERROR: string = "error"; + let modelFilePath = this.getModelFilePath(document); + + // TODO integrate with Toolchain + const tool = "/usr/share/one/bin/circle-operator"; + if (!fs.existsSync(tool)) { + // check whether it is installed + Balloon.info( + "To add more layers for editing please install ONE-toolchain" + ); + return; + } + + const toolargs = ["--name", modelFilePath]; + let result: string = ""; + let error: string = ""; + + let runPromise = new Promise((resolve, reject) => { + let cmd = cp.spawn(tool, toolargs, { shell: false }); + + cmd.stdout.on(K_DATA, (data: any) => { + let str = data.toString(); + if (str.length > 0) { + result = result + str; + } + }); + + cmd.stderr.on(K_DATA, (data: any) => { + error = result + data.toString(); + Logger.error("MPQEditor", error); + }); + + cmd.on(K_EXIT, (code: any) => { + let codestr = code.toString(); + if (codestr === "0") { + resolve(result); + } else { + let msg = "Failed to load model: " + modelFilePath; + Balloon.error(msg); + reject(msg); + } + }); + + cmd.on(K_ERROR, () => { + let msg = "Failed to run circle-operator: " + modelFilePath; + Balloon.error(msg); + reject(msg); + }); + }); + + runPromise + .then((names) => { + const nodesNames = names.split(/\r?\n/); + this._mpqDataMap[document.uri.toString()].setAllModelNodes(nodesNames); + webview.postMessage({ + type: "modelNodesChanged", + names: nodesNames, + }); + }) + .catch((error) => { + Logger.error("MPQEditor", error); + }); + } + + private showCircleModelGraph( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + const args: MPQSelectionCmdOpenArgs = { + modelPath: this.getModelFilePath(document), + document: document, + names: this._mpqDataMap[document.uri.toString()].getLayers(), + panel: webviewPanel, + }; + vscode.commands.executeCommand(MPQSelectionPanel.cmdOpen, args, this); + } + + private showVisqCircleModelGraph( + visqPath: string, + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + const args: MPQSelectionCmdOpenArgs = { + modelPath: this.getModelFilePath(document), + document: document, + names: this._mpqDataMap[document.uri.toString()].getLayers(), + panel: webviewPanel, + }; + + const visqData: MPQVisqData = { + visqPath: visqPath, + }; + vscode.commands.executeCommand( + MPQSelectionPanel.cmdOpenVisq, + args, + visqData, + this + ); + + webviewPanel.webview.postMessage({ + type: "VisqFileLoaded", + visqFile: visqPath, + }); + } + + private toggleCircleGraphIsShown( + show: boolean, + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + if (show) { + const docUri = document.uri.toString(); + const visqPath = this._mpqDataMap[docUri].getVisqPath(); + if (visqPath.length < 1) { + this.showCircleModelGraph(document, webviewPanel); + } else { + this.showVisqCircleModelGraph(visqPath, document, webviewPanel); + } + } else { + this.closeModelGraphView(document); + } + } + + private handleShowModelNodes( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + this.showCircleModelGraph(document, webviewPanel); + } + + private hadleVisqInputPathChanged( + path: string, + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ): void { + if ( + (path === "" || !path.endsWith(".visq.json")) && + this._mpqDataMap[document.uri.toString()].getVisqPath().length > 0 + ) { + // remove invalid path + this.removeVisqFile(document, webviewPanel); + } else if (path.endsWith(".visq.json")) { + // reload visq + this._mpqDataMap[document.uri.toString()].setVisqPath(path); + this.closeModelGraphView(document); + this.showVisqCircleModelGraph(path, document, webviewPanel); + } + } + + private removeVisqFile( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + this._mpqDataMap[document.uri.toString()].setVisqPath(""); // clear visqPath + + this.closeModelGraphView(document); + this.showCircleModelGraph(document, webviewPanel); + } + + private loadVisqFile( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ) { + const dialogOptions = { + canSelectMany: false, + canSelectFolders: false, + openLabel: "Open", + filters: { "target files": ["visq.json"], "all files": ["*"] }, + }; + + vscode.window.showOpenDialog(dialogOptions).then((fileUri) => { + if (fileUri && fileUri[0]) { + const visqPath = fileUri[0].fsPath.toString(); + + let docUri = document.uri.toString(); + this._mpqDataMap[docUri].setVisqPath(visqPath); + // close previous view if any + this.closeModelGraphView(document); + // open new view + this.showVisqCircleModelGraph(visqPath, document, webviewPanel); + } + }); + } + + onSelection(names: string[], document: vscode.TextDocument): void { + let docUri = document.uri.toString(); + this._mpqDataMap[docUri].setLayers(names); + this.updateDocument(document); + } + + onClosed(panel: vscode.WebviewPanel): void { + panel.webview.postMessage({ + type: "modelGraphIsShown", + shown: false, + }); + } + + onOpened(panel: vscode.WebviewPanel): void { + panel.webview.postMessage({ + type: "modelGraphIsShown", + shown: true, + }); + } + + onFailedToLoadVISQFile( + visqPath: string, + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel + ): void { + Balloon.error("Invalid visq file " + visqPath); + this._mpqDataMap[document.uri.toString()].setVisqPath(""); + this.closeModelGraphView(document); + // revert to slector mode + this.showCircleModelGraph(document, webviewPanel); + } +} diff --git a/src/extension.ts b/src/extension.ts index ad6c476e..9614db93 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,8 @@ import { PartGraphSelPanel } from "./PartEditor/PartGraphSelector"; import { ToolchainProvider } from "./Toolchain/ToolchainProvider"; import { Logger } from "./Utils/Logger"; import { VisqViewerProvider } from "./Visquv/VisqViewer"; +import { MPQEditorProvider } from "./MPQEditor/MPQEditor"; +import { MPQSelectionPanel } from "./MPQEditor/MPQCircleSelector"; /* istanbul ignore next */ export function activate(context: vscode.ExtensionContext) { @@ -77,6 +79,9 @@ export function activate(context: vscode.ExtensionContext) { MetadataViewerProvider.register(context); + MPQEditorProvider.register(context); + MPQSelectionPanel.register(context); + // returning backend registration function that will be called by backend extensions return backendRegistrationApi(); }