From a38accbe4c1e73af9406dddce227d82a1f6b0f68 Mon Sep 17 00:00:00 2001 From: dinhlongnguyen Date: Tue, 7 Nov 2023 17:08:34 +0700 Subject: [PATCH] Support multiple viselements (#33) (#47) * Support multiple viselements * Apply linter * per Fabien * fix descriptors file path --- l10n/bundle.l10n.json | 10 +-- package.json | 11 ++- package.nls.json | 4 +- src/assets/find_element_file.py | 15 +++- src/gui/command.ts | 18 ++-- src/gui/constant.ts | 5 +- src/gui/elementProvider.ts | 44 ++++++---- src/gui/utils.ts | 149 ++++++++++++++++++++------------ 8 files changed, 155 insertions(+), 101 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index c53ac32..78adca4 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -8,10 +8,10 @@ "Unmatched number of curly braces for expression": "Unmatched number of curly braces for expression", "Select properties for element '{0}'": "Select properties for element '{0}'", "Visual Element added": "Visual Element added", - "Find element descriptors file: ": "Find 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 selected environment": "Can't find visual element descriptors file with the selected environment", + "Find element descriptor files: ": "Find element descriptor files: ", + "Finding visual element descriptor files": "Finding visual element descriptor files", + "Visual element descriptor files was found and updated in workspace settings": "Visual element descriptor files was found and updated in workspace settings", + "Can't find visual element descriptor files with the selected environment": "Can't find visual element descriptor files with the selected 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}'", @@ -25,6 +25,6 @@ ". Do you mean '{0}'?": ". Do you mean '{0}'?", "Negated value of property '{0}' will be ignored": "Negated value of property '{0}' will be ignored", "Function '{0}' in property '{1}' is not available": "Function '{0}' in property '{1}' is not available", - "Parse element descriptors file: ": "Parse element descriptors file: ", + "Parse element descriptor files: ": "Parse element descriptor files: ", "Parse mock data file: ": "Parse mock data file: " } \ No newline at end of file diff --git a/package.json b/package.json index c7a4062..3a24c25 100644 --- a/package.json +++ b/package.json @@ -62,10 +62,13 @@ "configuration": { "title": "Taipy Studio GUI Helper", "properties": { - "taipyStudio.gUI.elementsFilePath": { - "type": "string", - "default": "", - "description": "%taipyStudio.gUI.elementsFilePath.description%" + "taipyStudio.gUI.elementsFilePaths": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "%taipyStudio.gUI.elementsFilePaths.description%" } } }, diff --git a/package.nls.json b/package.nls.json index 999f40f..07007dc 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,5 +1,5 @@ { "taipy.gui.md.generate.title": "Taipy: Generate visual element", - "taipy.gui.md.findElementFile.title": "Taipy: Locate visual element descriptors file in taipy-gui package", - "taipyStudio.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" + "taipy.gui.md.findElementFile.title": "Taipy: Locate visual element descriptor files in taipy-gui package", + "taipyStudio.gUI.elementsFilePaths.description": "Specifies the paths for visual element descriptor files. If this property is empty, the default visual element descriptor files will be used" } diff --git a/src/assets/find_element_file.py b/src/assets/find_element_file.py index e79bbb0..e545c5b 100644 --- a/src/assets/find_element_file.py +++ b/src/assets/find_element_file.py @@ -4,10 +4,17 @@ 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}") + + taipy_path = f"{os.path.dirname(os.path.dirname(inspect.getfile(Gui)))}" + potential_file_paths = [ + f"{taipy_path}{os.sep}gui{os.sep}viselements.json", + f"{taipy_path}{os.sep}gui_core{os.sep}viselements.json", + ] + if potential_file_paths := [ + path for path in potential_file_paths if os.path.exists(path) + ]: + print(f"Path: {';;;'.join(potential_file_paths)}") else: - print("Visual element descriptors file not found in taipy-gui package") + print("Visual element descriptor files not found in taipy-gui package") else: print("taipy-gui package is not installed within the selected python environment") diff --git a/src/gui/command.ts b/src/gui/command.ts index ca468a4..930f502 100644 --- a/src/gui/command.ts +++ b/src/gui/command.ts @@ -15,7 +15,7 @@ import { ExtensionContext, ProgressLocation, WorkspaceEdit, commands, l10n, wind import { ElementProvider } from "./elementProvider"; import { getLog } from "./logging"; -import { countChar, execShell, parseProperty, updateFilePath } from "./utils"; +import { countChar, execShell, parseProperty, updateFilePaths } from "./utils"; interface GuiElement { elementName: string; @@ -120,36 +120,36 @@ export class FindElementsFileCommand { workspace.workspaceFolders?.map(({ uri }) => uri.fsPath), ); } catch (error: any) { - getLog().info(l10n.t("Find element descriptors file: "), error.message || error); + getLog().info(l10n.t("Find element descriptor files: "), error.message || error); } pythonPath = pythonPath || workspace.getConfiguration("python").get("defaultInterpreterPath", "python"); window.withProgress( { location: ProgressLocation.Notification, cancellable: false, - title: l10n.t("Finding visual element descriptors file"), + title: l10n.t("Finding visual element descriptor files"), }, - async (progress) => { + async (_progress) => { try { const execResult = await execShell( `${pythonPath} ${join(__dirname, "assets", "find_element_file.py")}`, ); if (execResult.startsWith("Path: ")) { - updateFilePath(execResult.substring(6)); + updateFilePaths(execResult.trim().substring(6).split(";;;")); window.showInformationMessage( - l10n.t("Visual element descriptors file was found and updated in workspace settings"), + l10n.t("Visual element descriptor files 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 selected environment"), + l10n.t("Can't find visual element descriptor files with the selected environment"), ); } } catch (error: any) { - getLog().error(l10n.t("Find element descriptors file: "), error.message || error); + getLog().error(l10n.t("Find element descriptor files: "), error.message || error); window.showErrorMessage( - l10n.t("Can't find visual element descriptors file with the selected environment"), + l10n.t("Can't find visual element descriptor files with the selected environment"), ); } }, diff --git a/src/gui/constant.ts b/src/gui/constant.ts index 5fb693e..12ec0e3 100644 --- a/src/gui/constant.ts +++ b/src/gui/constant.ts @@ -15,14 +15,15 @@ import { join } from "path"; import { getBlockElementList, getControlElementList, - getElementFile, getElementList, getElementProperties, + getElementsFromFile, + getEmptyVisualElements, getOnFunctionList, getOnFunctionSignature, } from "./utils"; -const visualElements = getElementFile(join(__dirname, "assets", "viselements.json")) || {}; +const visualElements = getElementsFromFile(join(__dirname, "assets", "viselements.json")) || getEmptyVisualElements(); // object of all elements each with all of its properties export const defaultElementProperties = getElementProperties(visualElements); diff --git a/src/gui/elementProvider.ts b/src/gui/elementProvider.ts index 992ee19..9340eb7 100644 --- a/src/gui/elementProvider.ts +++ b/src/gui/elementProvider.ts @@ -20,18 +20,19 @@ import { } from "./constant"; import { ElementProperty, + VisualElements, getBlockElementList, getControlElementList, - getElementFile, - getElementFilePath, + getElementFilePaths, getElementList, getElementProperties, + getElementsFromFiles, getOnFunctionList, getOnFunctionSignature, } from "./utils"; export class ElementProvider { - private static elementCache: Record> = {}; + private static elementCache: Record = {}; public static getElementProperties(): Record> { return ElementProvider.resolve("elementProperties", defaultElementProperties, getElementProperties); @@ -50,41 +51,46 @@ export class ElementProvider { } public static getOnFunctionList(): string[] { - const filePath = getElementFilePath(); - if (filePath === null) { + const filePaths = getElementFilePaths(); + if (filePaths.length === 0) { return defaultOnFunctionList; } return getOnFunctionList(ElementProvider.getElementProperties()); } public static getOnFunctionSignature(): Record { - const filePath = getElementFilePath(); - if (filePath === null) { + const filePaths = getElementFilePaths(); + if (filePaths.length === 0) { return defaultOnFunctionSignature; } return getOnFunctionSignature(ElementProvider.getElementProperties()); } - private static resolve(propertyName: string, defaultValue: any, elementsAnalyzer: (viselements: object) => any) { - const filePath = getElementFilePath(); - if (filePath === null) { + public static invalidateCache() { + ElementProvider.elementCache = {}; + } + + private static resolve( + propertyName: string, + defaultValue: any, + elementsAnalyzer: (visualElements: VisualElements) => any, + ) { + const filePaths = getElementFilePaths(); + if (filePaths.length === 0) { return defaultValue; } - if (ElementProvider.elementCache[filePath] === undefined) { - ElementProvider.elementCache[filePath] = {}; - } - if (propertyName in ElementProvider.elementCache[filePath]) { - return ElementProvider.elementCache[filePath][propertyName]; + if (propertyName in ElementProvider.elementCache) { + return ElementProvider.elementCache[propertyName]; } - const visualElements = ElementProvider.elementCache[filePath]["visualElements"] || getElementFile(filePath); + const visualElements = ElementProvider.elementCache["visualElements"] || getElementsFromFiles(filePaths); if (!visualElements) { return defaultValue; } - if (!(propertyName in ElementProvider.elementCache[filePath])) { - ElementProvider.elementCache[filePath].visualElements = visualElements; + if (!(propertyName in ElementProvider.elementCache)) { + ElementProvider.elementCache.visualElements = visualElements; } const result = elementsAnalyzer(visualElements); - ElementProvider.elementCache[filePath][propertyName] = result; + ElementProvider.elementCache[propertyName] = result; return result; } } diff --git a/src/gui/utils.ts b/src/gui/utils.ts index e1f64d7..ec102a6 100644 --- a/src/gui/utils.ts +++ b/src/gui/utils.ts @@ -15,11 +15,24 @@ import { existsSync, readFileSync } from "fs"; import { DocumentFilter, l10n, workspace } from "vscode"; import { CONFIG_NAME } from "./constant"; +import { ElementProvider } from "./elementProvider"; import { getLog } from "./logging"; export const countChar = (str: string, char: string): number => { return str.split(char).length - 1; }; + +export interface VisualElements { + blocks?: [string, ElementDetail][]; + controls?: [string, ElementDetail][]; + undocumented?: [string, ElementDetail][]; +} + +interface ElementDetail { + properties?: ElementProperty[]; + inherits?: string[]; +} + export interface ElementProperty { name: string; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -29,31 +42,31 @@ export interface ElementProperty { signature?: [string, string][]; } -interface ElementDetail { - properties?: ElementProperty[]; - inherits?: string[]; -} - // visual elements parser -export const getElementProperties = (visualElements: object): Record> => { - const blocks: Record = ( - visualElements["blocks" as keyof typeof visualElements] as any - ).reduce((obj: Record, v: any) => { - obj[v[0]] = v[1]; - return obj; - }, {} as Record); - const controls: Record = ( - visualElements["controls" as keyof typeof visualElements] as any - ).reduce((obj: Record, v: any) => { - obj[v[0]] = v[1]; - return obj; - }, {} as Record); - const undocumented: Record = ( - visualElements["undocumented" as keyof typeof visualElements] as any - ).reduce((obj: Record, v: any) => { - obj[v[0]] = v[1]; - return obj; - }, {} as Record); +export const getElementProperties = ( + visualElements: VisualElements, +): Record> => { + const blocks: Record = (visualElements["blocks"] || []).reduce( + (obj: Record, v: any) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); + const controls: Record = (visualElements["controls"] || []).reduce( + (obj: Record, v: any) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); + const undocumented: Record = (visualElements["undocumented"] || []).reduce( + (obj: Record, v: any) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); const blocksProperties: Record> = {}; const controlsProperties: Record> = {}; // handle all blocks object @@ -68,19 +81,15 @@ export const getElementProperties = (visualElements: object): Record { - return (visualElements["blocks" as keyof typeof visualElements] as (typeof Object)[]).map( - (v: any) => v[0] as string, - ); +export const getBlockElementList = (visualElements: VisualElements): string[] => { + return (visualElements["blocks"] || []).map((v: any) => v[0] as string); }; -export const getControlElementList = (visualElements: object): string[] => { - return (visualElements["controls" as keyof typeof visualElements] as (typeof Object)[]).map( - (v: any) => v[0] as string, - ); +export const getControlElementList = (visualElements: VisualElements): string[] => { + return (visualElements["controls"] || []).map((v: any) => v[0] as string); }; -export const getElementList = (visualElements: object): string[] => { +export const getElementList = (visualElements: VisualElements): string[] => { return [...getControlElementList(visualElements), ...getBlockElementList(visualElements)]; }; @@ -176,46 +185,74 @@ export const execShell = (cmd: string) => new Promise((resolve, reject) => { exec(cmd, (err, out) => { if (err) { - getLog().error(l10n.t("Find element descriptors file: "), err.message || err); + getLog().error(l10n.t("Find element descriptor files: "), err.message || err); return reject(err); } return resolve(out); }); }); -export const getElementFilePath = (): string | null => { +export const getElementFilePaths = (): string[] => { const config = workspace.getConfiguration(CONFIG_NAME); - 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; + if (config.has("elementsFilePaths") && config.get("elementsFilePaths")) { + let filePaths = config.get("elementsFilePaths") as string[]; + filePaths = filePaths.filter((v) => existsSync(v)); + updateFilePaths(filePaths); + return filePaths; } - return null; + return []; }; -export const updateFilePath = (path: string) => { +export const updateFilePaths = (paths: string[]) => { const config = workspace.getConfiguration(CONFIG_NAME); - config.update("elementsFilePath", path); + config.update("elementsFilePaths", paths); + ElementProvider.invalidateCache(); }; -export const getElementFile = (path: string): object | undefined => { +export const getElementsFromFile = (path: string): VisualElements | 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; + return elements as VisualElements; } catch (error: any) { - getLog().error(l10n.t("Parse element descriptors file: "), error.message || error); + getLog().error(l10n.t("Parse element descriptor files: "), error.message || error); + return undefined; + } +}; + +export const getElementsFromFiles = (paths: string[]): VisualElements | undefined => { + let visualElements = getEmptyVisualElements(); + if (paths.length === 0) { + return undefined; + } + let registeredPathCount = 0; + for (const path of paths) { + const elements = getElementsFromFile(path); + if (elements) { + registeredPathCount++; + // Merge all elements with visual elements, checking for duplicates on the first item of each element + visualElements = mergeElements(visualElements, elements, "blocks"); + visualElements = mergeElements(visualElements, elements, "controls"); + visualElements = mergeElements(visualElements, elements, "undocumented"); + } + } + if (registeredPathCount === 0) { return undefined; } + return visualElements; +}; + +const mergeElements = ( + basedVisualElements: VisualElements, + newVisualElements: VisualElements, + section: keyof VisualElements, +): VisualElements => { + const basedElements = basedVisualElements[section] || []; + const newElements = newVisualElements[section] || []; + const mergedElements = newElements.filter((v) => !basedElements.map((v) => v[0]).includes(v[0])); + return { ...basedVisualElements, [section]: [...basedElements, ...mergedElements] }; +}; + +export const getEmptyVisualElements = (): VisualElements => { + return { blocks: [], controls: [], undocumented: [] }; };