From 5ed9ab974876df1355b1f7c4826174abf999a0e8 Mon Sep 17 00:00:00 2001 From: dinhlongnguyen Date: Thu, 2 Feb 2023 22:17:28 +0700 Subject: [PATCH] add support for external json files (#7) * temp-update * add support for external json files * update categories * crlf * thanks Fred and Fab --- l10n/bundle.l10n.json | 14 ++++- package.json | 21 +++++++- package.nls.json | 4 +- src/assets/find_element_file.py | 13 +++++ src/gui/codeAction.ts | 4 +- src/gui/command.ts | 60 +++++++++++++++++++--- src/gui/completion.ts | 22 +++++--- src/gui/constant.ts | 6 ++- src/gui/context.ts | 3 +- src/gui/diagnostics.ts | 11 ++-- src/gui/elementProvider.ts | 91 +++++++++++++++++++++++++++++++++ src/gui/utils.ts | 54 ++++++++++++++++++- webpack.config.js | 15 ++---- 13 files changed, 276 insertions(+), 42 deletions(-) create mode 100644 src/assets/find_element_file.py create mode 100644 src/gui/elementProvider.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 65dd368..5b47755 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -1,16 +1,26 @@ { + "Add closing tag": "Add closing tag", + "Create function '{0}'": "Create function '{0}'", + "Replace with '{0}'": "Replace with '{0}'", + "Remove negated value": "Remove negated value", "Select an element type": "Select an element type", "Enter element value": "Enter element value", "Unmatched number of curly braces for expression": "Unmatched number of curly braces for expression", "Select properties for element '{0}'": "Select properties for element '{0}'", - "Element added": "Element added", + "Visual Element added": "Visual Element added", + "Enter Python executable path. `python` value will be used if the field is empty": "Enter Python executable path. `python` value will be used if the field is empty", + "This path is used to locate the visual element descriptors file": "This path is used to locate the visual element descriptors file", + "Finding visual element descriptors file": "Finding visual element descriptors file", + "Visual element descriptors file was found and updated in workspace settings": "Visual element descriptors file was found and updated in workspace settings", + "Can't find visual element descriptors file with the provided environment": "Can't find visual element descriptors file with the provided environment", "Missing closing syntax": "Missing closing syntax", "Missing opening tag": "Missing opening tag", "Missing matching opening tag identifier '{0}'": "Missing matching opening tag identifier '{0}'", "Missing matching closing tag identifier '{0}'": "Missing matching closing tag identifier '{0}'", "Unmatched opening tag identifier '{0}'": "Unmatched opening tag identifier '{0}'", "Unmatched closing tag identifier '{0}'": "Unmatched closing tag identifier '{0}'", - "Missing closing tag with tag identifier '{0}'": "Missing closing tag with tag identifier '{0}'", + "Missing closing tag for opened tag '{0}'": "Missing closing tag for opened tag '{0}'", + "Missing closing tag": "Missing closing tag", "Invalid property format": "Invalid property format", "Invalid property name '{0}'": "Invalid property name '{0}'", ". Do you mean '{0}'?": ". Do you mean '{0}'?", diff --git a/package.json b/package.json index 707dc75..36c7d52 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "icon": "assets/taipy-logo.png", "l10n": "./dist/l10n", "categories": [ + "Linters", + "Data Science", "Other" ], "activationEvents": [ @@ -25,7 +27,8 @@ "workspaceContains:**/*.py", "onLanguage:python", "onLanguage:markdown", - "onCommand:taipy.gui.md.generate" + "onCommand:taipy.gui.md.generate", + "onCommand:taipy.gui.md.findElementFile" ], "main": "./dist/taipy-studio-gui.js", "capabilities": { @@ -42,6 +45,10 @@ { "command": "taipy.gui.md.generate", "title": "%taipy.gui.md.generate.title%" + }, + { + "command": "taipy.gui.md.findElementFile", + "title": "%taipy.gui.md.findElementFile.title%" } ], "snippets": [ @@ -53,7 +60,17 @@ "language": "markdown", "path": "./dist/snippets.json" } - ] + ], + "configuration": { + "title": "Taipy Studio Gui Helper", + "properties": { + "taipy.gui.elementsFilePath": { + "type": "string", + "default": "", + "description": "%taipy.gui.elementsFilePath.description%" + } + } + } }, "scripts": { "compile": "webpack", diff --git a/package.nls.json b/package.nls.json index 5d636c8..8e49b35 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,3 +1,5 @@ { - "taipy.gui.md.generate.title": "Generate Taipy GUI element" + "taipy.gui.md.generate.title": "Taipy: Generate visual element", + "taipy.gui.md.findElementFile.title": "Taipy: Locate visual element descriptors file in taipy-gui package", + "taipy.gui.elementsFilePath.description": "Specifies the path for visual element descriptors file. If this property is empty, the default visual element descriptors file will be used" } diff --git a/src/assets/find_element_file.py b/src/assets/find_element_file.py new file mode 100644 index 0000000..b7f3823 --- /dev/null +++ b/src/assets/find_element_file.py @@ -0,0 +1,13 @@ +from importlib import util +import inspect +import os + +if util.find_spec("taipy") and util.find_spec("taipy.gui"): + from taipy.gui import Gui + element_file_path = f"{os.path.dirname(inspect.getfile(Gui))}{os.sep}webapp{os.sep}viselements.json" + if os.path.exists(element_file_path): + print(f"Path: {element_file_path}") + else: + print("Visual element descriptors file not found in taipy-gui package") +else: + print("taipy-gui python package is not installed") diff --git a/src/gui/codeAction.ts b/src/gui/codeAction.ts index 6bce864..e0dc1c7 100644 --- a/src/gui/codeAction.ts +++ b/src/gui/codeAction.ts @@ -27,9 +27,9 @@ import { TextDocument, WorkspaceEdit, } from "vscode"; -import { defaultOnFunctionSignature } from "./constant"; import { DiagnosticCode, PROPERTY_RE } from "./diagnostics"; +import { ElementProvider } from "./elementProvider"; import { generateOnFunction, markdownDocumentFilter, pythonDocumentFilter } from "./utils"; export class MarkdownActionProvider implements CodeActionProvider { @@ -118,7 +118,7 @@ export class MarkdownActionProvider implements CodeActionProvider { action.edit.insert( document.uri, quotePositions.length > 0 ? quotePositions[0].translate(0, 3) : new Position(document.lineCount - 1, 0), - "\n\n" + generateOnFunction(defaultOnFunctionSignature[onFunctionType] || [["state", "State"]], functionName) + "\n\n" + generateOnFunction(ElementProvider.getOnFunctionSignature()[onFunctionType] || [["state", "State"]], functionName) ); return action; } diff --git a/src/gui/command.ts b/src/gui/command.ts index 01652e5..c5c6f4a 100644 --- a/src/gui/command.ts +++ b/src/gui/command.ts @@ -11,9 +11,10 @@ * specific language governing permissions and limitations under the License. */ -import { commands, ExtensionContext, l10n, window, workspace, WorkspaceEdit } from "vscode"; -import { defaultElementList, defaultElementProperties } from "./constant"; -import { countChar, parseProperty } from "./utils"; +import { join } from "path"; +import { commands, ExtensionContext, l10n, ProgressLocation, window, workspace, WorkspaceEdit } from "vscode"; +import { ElementProvider } from "./elementProvider"; +import { countChar, execShell, parseProperty, updateFilePath } from "./utils"; interface GuiElement { elementName: string; @@ -33,7 +34,7 @@ export class GenerateGuiCommand { } private static async handleGenerateElementCommand() { - const result = await window.showQuickPick(defaultElementList, { + const result = await window.showQuickPick(ElementProvider.getElementList(), { placeHolder: l10n.t("Select an element type"), }); if (result === undefined) { @@ -62,7 +63,8 @@ export class GenerateGuiCommand { private static async handleElementPropertySelection(guiElement: GuiElement) { const quickPick = window.createQuickPick(); - const propertyObject = defaultElementProperties[guiElement.elementName as keyof typeof defaultElementProperties]; + const elementProperties = ElementProvider.getElementProperties(); + const propertyObject = elementProperties[guiElement.elementName as keyof typeof elementProperties]; if (Object.keys(propertyObject).length === 0) { GenerateGuiCommand.addGuiElement(guiElement); return; @@ -94,6 +96,52 @@ export class GenerateGuiCommand { } edit.insert(activeEditor?.document.uri, activeEditor?.selection.active, elementString); workspace.applyEdit(edit); - window.showInformationMessage(l10n.t("Element added")); + window.showInformationMessage(l10n.t("Visual Element added")); + } +} + +export class FindElementsFileCommand { + static register(vsContext: ExtensionContext): void { + new FindElementsFileCommand(vsContext); + } + + private constructor(readonly context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand("taipy.gui.md.findElementFile", FindElementsFileCommand.commandEntry) + ); + } + + private static async commandEntry() { + const result = await window.showInputBox({ + placeHolder: l10n.t("Enter Python executable path. `python` value will be used if the field is empty"), + prompt: l10n.t("This path is used to locate the visual element descriptors file"), + }); + if (result === undefined) { + return; + } + window.withProgress( + { + location: ProgressLocation.Notification, + cancellable: false, + title: l10n.t("Finding visual element descriptors file"), + }, + async (progress) => { + try { + let execResult = await execShell( + `${result || "python"} ${join(__dirname, "assets", "find_element_file.py")}` + ); + if (execResult.startsWith("Path: ")) { + updateFilePath(execResult.substring(6)); + window.showInformationMessage(l10n.t("Visual element descriptors file was found and updated in workspace settings")); + } else if (execResult) { + window.showErrorMessage(execResult); + } else { + window.showErrorMessage(l10n.t("Can't find visual element descriptors file with the provided environment")); + } + } catch (error) { + window.showErrorMessage(l10n.t("Can't find visual element descriptors file with the provided environment")); + } + } + ); } } diff --git a/src/gui/completion.ts b/src/gui/completion.ts index 456dc4a..3809828 100644 --- a/src/gui/completion.ts +++ b/src/gui/completion.ts @@ -28,7 +28,8 @@ import { TextDocument, Uri, } from "vscode"; -import { defaultElementList, defaultElementProperties, defaultOnFunctionList, LanguageId } from "./constant"; +import { LanguageId } from "./constant"; +import { ElementProvider } from "./elementProvider"; import { markdownDocumentFilter, parseProperty, pythonDocumentFilter } from "./utils"; const RE_LINE = /<(([\|]{1})([^\|]*)){1,2}/; @@ -83,7 +84,7 @@ export class GuiCompletionItemProvider implements CompletionItemProvider { return await this.getSymbols(Uri.file(potentialPythonFile), SymbolKind.Variable, CompletionItemKind.Variable); } // function name for 'on_*' properties - if (linePrefix.endsWith("=") && defaultOnFunctionList.some((v) => linePrefix.endsWith(v + "="))) { + if (linePrefix.endsWith("=") && ElementProvider.getOnFunctionList().some((v) => linePrefix.endsWith(v + "="))) { return await this.getSymbols(Uri.file(potentialPythonFile), SymbolKind.Function, CompletionItemKind.Function); } } @@ -96,7 +97,7 @@ export class GuiCompletionItemProvider implements CompletionItemProvider { return await this.getSymbols(document.uri, SymbolKind.Variable, CompletionItemKind.Variable); } // function name for 'on_*' properties - if (linePrefix.endsWith("=") && defaultOnFunctionList.some((v) => linePrefix.endsWith(v + "="))) { + if (linePrefix.endsWith("=") && ElementProvider.getOnFunctionList().some((v) => linePrefix.endsWith(v + "="))) { return await this.getSymbols(document.uri, SymbolKind.Function, CompletionItemKind.Function); } return this.getCommonCompletion(document, linePrefix); @@ -104,18 +105,19 @@ export class GuiCompletionItemProvider implements CompletionItemProvider { private async getCommonCompletion(document: TextDocument, linePrefix: string): Promise { if (linePrefix.endsWith("|")) { - const foundElements = defaultElementList.reduce((p: string[], c: string) => { + const foundElements = ElementProvider.getElementList().reduce((p: string[], c: string) => { linePrefix.includes(`|${c}`) && p.push(c); return p; }, []); // element type completion if (linePrefix.match(RE_LINE) && foundElements.length === 0) { - return defaultElementList.map((v) => new CompletionItem(v, CompletionItemKind.Keyword)); + return ElementProvider.getElementList().map((v) => new CompletionItem(v, CompletionItemKind.Keyword)); } // element property completion if (linePrefix.match(RE_LINE) && foundElements.length > 0) { const latestElement = foundElements[foundElements.length - 1]; - const properties = defaultElementProperties[latestElement as keyof typeof defaultElementProperties]; + const elementProperties = ElementProvider.getElementProperties(); + const properties = elementProperties[latestElement as keyof typeof elementProperties]; if (properties !== undefined) { return Object.keys(properties) .reduce((p: string[], c: string) => { @@ -124,7 +126,9 @@ export class GuiCompletionItemProvider implements CompletionItemProvider { }, []) .map((v) => { let completionItem = new CompletionItem(v, CompletionItemKind.Property); - completionItem.documentation = new MarkdownString(parseProperty(properties[v as keyof typeof properties])); + completionItem.documentation = new MarkdownString( + parseProperty(properties[v as keyof typeof properties]) + ); return completionItem; }); } @@ -139,6 +143,8 @@ export class GuiCompletionItemProvider implements CompletionItemProvider { completionItemKind: CompletionItemKind ): Promise { const symbols = (await commands.executeCommand("vscode.executeDocumentSymbolProvider", uri)) as SymbolInformation[]; - return !symbols ? [] : symbols.filter((v) => v.kind === symbolKind).map((v) => new CompletionItem(v.name, completionItemKind)); + return !symbols + ? [] + : symbols.filter((v) => v.kind === symbolKind).map((v) => new CompletionItem(v.name, completionItemKind)); } } diff --git a/src/gui/constant.ts b/src/gui/constant.ts index 40ac2fe..0a21039 100644 --- a/src/gui/constant.ts +++ b/src/gui/constant.ts @@ -11,8 +11,10 @@ * specific language governing permissions and limitations under the License. */ -import visualElements from "../assets/viselements.json"; -import { getBlockElementList, getControlElementList, getElementList, getElementProperties, getOnFunctionList, getOnFunctionSignature } from "./utils"; +import { join } from "path"; +import { getBlockElementList, getControlElementList, getElementFile, getElementList, getElementProperties, getOnFunctionList, getOnFunctionSignature } from "./utils"; + +const visualElements = getElementFile(join(__dirname, "assets", "viselements.json")) || {}; // object of all elements each with all of its properties export const defaultElementProperties = getElementProperties(visualElements); diff --git a/src/gui/context.ts b/src/gui/context.ts index 18ef5e3..2acabd6 100644 --- a/src/gui/context.ts +++ b/src/gui/context.ts @@ -13,7 +13,7 @@ import { ExtensionContext } from "vscode"; import { MarkdownActionProvider } from "./codeAction"; -import { GenerateGuiCommand } from "./command"; +import { GenerateGuiCommand, FindElementsFileCommand } from "./command"; import { GuiCompletionItemProvider } from "./completion"; import { registerDiagnostics } from "./diagnostics"; @@ -26,6 +26,7 @@ export class GuiContext { registerDiagnostics(context); GuiCompletionItemProvider.register(context); GenerateGuiCommand.register(context); + FindElementsFileCommand.register(context); MarkdownActionProvider.register(context); } } diff --git a/src/gui/diagnostics.ts b/src/gui/diagnostics.ts index 6bfcda3..204af30 100644 --- a/src/gui/diagnostics.ts +++ b/src/gui/diagnostics.ts @@ -28,7 +28,8 @@ import { workspace, } from "vscode"; import { findBestMatch } from "string-similarity"; -import { defaultElementList, defaultBlockElementList, defaultElementProperties, LanguageId } from "./constant"; +import { LanguageId } from "./constant"; +import { ElementProvider } from "./elementProvider"; const CONTROL_RE = /<\|(.*?)\|>/; const OPENING_TAG_RE = /<([0-9a-zA-Z\_\.]*)\|((?:(?!\|>).)*)\s*$/; @@ -142,7 +143,7 @@ const getSectionDiagnostics = (diagnosticSection: DiagnosticSection): Diagnostic element = e; diagnostics.push(...d); } - if (defaultBlockElementList.includes(element.type)) { + if (ElementProvider.getBlockElementList().includes(element.type)) { tagQueue.push([ element, getRangeOfStringInline(line, openingTagSearch[0], new Position(lineCount, 0)), @@ -238,7 +239,7 @@ const getSectionDiagnostics = (diagnosticSection: DiagnosticSection): Diagnostic if (tagId) { diagnostics.push( createWarningDiagnostic( - l10n.t("Missing closing tag with tag identifier '{0}'", tagId), + l10n.t("Missing closing tag for opened tag '{0}'", tagId), DiagnosticCode.missCTagId, getRangeFromPosition(p, inlineP) ) @@ -266,7 +267,7 @@ const processElement = ( const fragments = s.split(SPLIT_RE).filter((v) => !!v); const e = buildEmptyTaipyElement(); fragments.forEach((fragment) => { - if (!e.type && defaultElementList.includes(fragment)) { + if (!e.type && ElementProvider.getElementList().includes(fragment)) { e.type = fragment; return; } @@ -290,7 +291,7 @@ const processElement = ( const propNameMatch = PROPERTY_NAME_RE.exec(propMatch[2]); const propName = propNameMatch ? propNameMatch[1] : propMatch[2]; const val = propMatch[3]; - const validPropertyList = Object.keys(defaultElementProperties[e.type] || []); + const validPropertyList = Object.keys(ElementProvider.getElementProperties()[e.type] || []); if (validPropertyList.length !== 0 && !validPropertyList.includes(propName)) { const bestMatch = findBestMatch(propName, validPropertyList).bestMatch; let dS = l10n.t("Invalid property name '{0}'", propName); diff --git a/src/gui/elementProvider.ts b/src/gui/elementProvider.ts new file mode 100644 index 0000000..c03b51e --- /dev/null +++ b/src/gui/elementProvider.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2023 Avaiga Private Limited + * + * 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 { + defaultBlockElementList, + defaultControlElementList, + defaultElementList, + defaultElementProperties, + defaultOnFunctionList, + defaultOnFunctionSignature, +} from "./constant"; +import { + ElementProperty, + getBlockElementList, + getControlElementList, + getElementFile, + getElementFilePath, + getElementList, + getElementProperties, + getOnFunctionList, + getOnFunctionSignature, +} from "./utils"; + +export class ElementProvider { + private static elementCache: Record> = {}; + + public static getElementProperties(): Record> { + return ElementProvider.resolve("elementProperties", defaultElementProperties, getElementProperties); + } + + public static getElementList(): string[] { + return ElementProvider.resolve("elementList", defaultElementList, getElementList); + } + + public static getControlElementList(): string[] { + return ElementProvider.resolve("controlElementList", defaultControlElementList, getControlElementList); + } + + public static getBlockElementList(): string[] { + return ElementProvider.resolve("blockElementList", defaultBlockElementList, getBlockElementList); + } + + public static getOnFunctionList(): string[] { + const filePath = getElementFilePath(); + if (filePath === null) { + return defaultOnFunctionList; + } + return getOnFunctionList(ElementProvider.getElementProperties()); + } + + public static getOnFunctionSignature(): Record { + const filePath = getElementFilePath(); + if (filePath === null) { + return defaultOnFunctionSignature; + } + return getOnFunctionSignature(ElementProvider.getElementProperties()); + } + + private static resolve(propertyName: string, defaultValue: any, elementsAnalyzer: (viselements: object) => any) { + const filePath = getElementFilePath(); + if (filePath === null) { + return defaultValue; + } + if (ElementProvider.elementCache[filePath] === undefined) { + ElementProvider.elementCache[filePath] = {}; + } + if (propertyName in ElementProvider.elementCache[filePath]) { + return ElementProvider.elementCache[filePath][propertyName]; + } + const visualElements = ElementProvider.elementCache[filePath]["visualElements"] || getElementFile(filePath); + if (!visualElements) { + return defaultValue; + } + if (!(propertyName in ElementProvider.elementCache[filePath])) { + ElementProvider.elementCache[filePath].visualElements = visualElements; + } + const result = elementsAnalyzer(visualElements); + ElementProvider.elementCache[filePath][propertyName] = result; + return result; + } +} diff --git a/src/gui/utils.ts b/src/gui/utils.ts index 9ae7c56..c30f6bb 100644 --- a/src/gui/utils.ts +++ b/src/gui/utils.ts @@ -11,12 +11,14 @@ * specific language governing permissions and limitations under the License. */ -import { DocumentFilter } from "vscode"; +import { exec } from "child_process"; +import { existsSync, readFileSync } from "fs"; +import { DocumentFilter, workspace } from "vscode"; export const countChar = (str: string, char: string): number => { return str.split(char).length - 1; }; -interface ElementProperty { +export interface ElementProperty { name: string; // eslint-disable-next-line @typescript-eslint/naming-convention default_property?: any; @@ -165,3 +167,51 @@ export const generateOnFunction = (signature: [string, string][], functionName: export const markdownDocumentFilter: DocumentFilter = { language: "markdown" }; export const pythonDocumentFilter: DocumentFilter = { language: "python" }; + +export const execShell = (cmd: string) => + new Promise((resolve, reject) => { + exec(cmd, (err, out) => { + if (err) { + console.log(err); + return reject(err); + } + return resolve(out); + }); + }); + +export const getElementFilePath = (): string | null => { + const config = workspace.getConfiguration("taipy.gui"); + if (config.has("elementsFilePath") && config.get("elementsFilePath")) { + const filePath = config.get("elementsFilePath") as string; + if (existsSync(filePath)) { + return filePath; + } + // Reset if filepath is not valid + updateFilePath(""); + return null; + } + return null; +}; + +export const updateFilePath = (path: string) => { + const config = workspace.getConfiguration("taipy.gui"); + config.update("elementsFilePath", path); +}; + +export const getElementFile = (path: string): object | undefined => { + try { + const content = readFileSync(path, { encoding: "utf8", flag: "r" }); + const elements = JSON.parse(content); + if ("controls" in elements && "blocks" in elements && "undocumented" in elements) { + return elements; + } + // Reset if filepath is not valid + if (path === getElementFilePath()) { + updateFilePath(""); + } + return undefined; + } catch (error) { + console.info(error); + return undefined; + } +}; diff --git a/webpack.config.js b/webpack.config.js index ff3e62d..8278205 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,6 +52,10 @@ const extensionConfig = { { from: path.resolve(__dirname, "snippets.json"), }, + { + from: path.resolve(__dirname, "src", "assets"), + to: "assets", + }, ] }) ], @@ -66,17 +70,6 @@ const extensionConfig = { } ] }, - { - type: 'asset/resource', - test: /\.json$/, - generator: { - filename: 'assets/[name][ext]' - } - }, - { - test: /\.json$/, - type: 'json', - } ], }, devtool: 'nosources-source-map',