From 9295b1e325d1ff8806890703da6af97377daf7c8 Mon Sep 17 00:00:00 2001 From: vinzbarbuto Date: Wed, 2 Oct 2024 17:57:46 -0700 Subject: [PATCH] User interface redesign --- package.json | 120 +++--- src/extension.ts | 62 ++- src/extension_version.ts | 2 +- src/lfview/lf-data-provider-commands.ts | 110 ++--- src/lfview/lf-data-provider.ts | 526 ++++++++++++++++++------ 5 files changed, 513 insertions(+), 307 deletions(-) diff --git a/package.json b/package.json index ca815b8c1..098a25856 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,24 @@ } } ], + "colors": [ + { + "id": "editorIcon.currentProject", + "description": "Color for a TreeIteem label", + "defaults": { + "dark": "#57cc99", + "light": "#57cc99" + } + }, + { + "id": "editorIcon.notCurrentProject", + "description": "Color for a TreeIteem label", + "defaults": { + "dark": "#fcbf49", + "light": "#fcbf49" + } + } + ], "semanticTokenScopes": [ { "scopes": { @@ -100,54 +118,44 @@ "title": "Refresh Tree View", "icon": "$(refresh)" }, - { - "command": "linguafranca.refreshLibraryEntries", - "title": "Refresh Tree View", - "icon": "$(refresh)" - }, { "command": "linguafranca.goToFile", "title": "Go To Selected File", "icon": "$(go-to-file)" }, - { - "command": "linguafranca.goToLibraryFile", - "title": "Go To Selected File", - "icon": "$(go-to-file)" - }, { "command": "linguafranca.importReactor", "title": "Import Selected Reactor", "icon": "$(insert)" }, - { - "command": "linguafranca.importLibraryReactor", - "title": "Import Selected Reactor", - "icon": "$(insert)" - }, { "command": "linguafranca.openInSplitView", "title": "Open in Split View", "icon": "$(split-horizontal)" }, - { - "command": "linguafranca.openLibraryInSplitView", - "title": "Open in Split View", - "icon": "$(split-horizontal)" - }, { "command": "linguafranca.collapseAll", "title": "Collapse All", "icon": "$(collapse-all)" }, - { - "command": "linguafranca.collapseAllLibrary", - "title": "Collapse All", - "icon": "$(collapse-all)" - }, { "command": "linguafranca.getVersion", "title": "Lingua Franca: Get Version" + }, + { + "command": "linguafranca.includeProject", + "title": "Include in current project", + "icon": "$(desktop-download)" + }, + { + "command": "linguafranca.goToLingoToml", + "title": "Go to Lingo.toml", + "icon": "$(edit)" + }, + { + "command": "linguafranca.openInTerminal", + "title": "Open in Terminal", + "icon": "$(terminal)" } ], @@ -208,63 +216,49 @@ { "command": "linguafranca.refreshEntries", "group": "navigation@1", - "when": "view == lf-lang-local" - }, - { - "command": "linguafranca.refreshLibraryEntries", - "group": "navigation@1", - "when": "view == lf-lang-library" + "when": "view == lf-lang-projects" }, { "command": "linguafranca.collapseAll", "group": "navigation@2", - "when": "view == lf-lang-local" - }, - { - "command": "linguafranca.collapseAllLibrary", - "group": "navigation@2", - "when": "view == lf-lang-library" + "when": "view == lf-lang-projects" } ], "view/item/context" : [ { - "command": "linguafranca.goToFile", - "group": "inline", - "when": "view == lf-lang-local && viewItem != root" + "command": "linguafranca.openInTerminal", + "group": "inline@1", + "when": "viewItem == project" }, { - "command": "linguafranca.goToLibraryFile", - "group": "inline", - "when": "view == lf-lang-library && viewItem != root" - }, - { - "command": "linguafranca.openInSplitView", - "group": "inline", - "when": "view == lf-lang-local && viewItem == file" + "command": "linguafranca.includeProject", + "group": "inline@1", + "when": "viewItem == root || viewItem == file-local" }, { - "command": "linguafranca.openLibraryInSplitView", - "group": "inline", - "when": "view == lf-lang-library && viewItem == file" + "command": "linguafranca.goToFile", + "group": "inline@2", + "when": "viewItem == file-local || viewItem == file-lingo || viewItem == file-local-included || viewItem == reactor || viewItem == reactor-included" }, { "command": "linguafranca.openInSplitView", - "when": "view == lf-lang-local && viewItem == reactor" + "group": "inline@3", + "when": "viewItem == file-local || viewItem == file-lingo || viewItem == file-local-included" }, { - "command": "linguafranca.openLibraryInSplitView", - "when": "view == lf-lang-library && viewItem == reactor" + "command": "linguafranca.openInSplitView", + "when": "viewItem == reactor || viewItem == reactor-included" }, { "command": "linguafranca.importReactor", - "group": "inline", - "when": "view == lf-lang-local && viewItem == reactor" + "group": "inline@3", + "when": "viewItem == reactor-included" }, { - "command": "linguafranca.importLibraryReactor", + "command": "linguafranca.goToLingoToml", "group": "inline", - "when": "view == lf-lang-library && viewItem == reactor" + "when": "viewItem == lingo" } ] }, @@ -272,7 +266,7 @@ "activitybar": [ { "id": "lf-lang", - "title": "Lingua Franca Package Explorer", + "title": "Lingua Franca Projects", "icon": "images/logo/lf-logo-dark.svg" } ] @@ -280,12 +274,8 @@ "views": { "lf-lang": [ { - "id": "lf-lang-local", - "name": "Local Libraries" - }, - { - "id": "lf-lang-library", - "name": "Lingo Libraries" + "id": "lf-lang-projects", + "name": "" } ] } diff --git a/src/extension.ts b/src/extension.ts index d00d99d51..983aa67ab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,15 +13,13 @@ import { registerBuildCommands, registerNewFileCommand } from './build_commands' import * as checkDependencies from './check_dependencies'; import { LFDataProvider, LFDataProviderNode, LFDataProviderNodeType} from './lfview/lf-data-provider'; import { registerCollapseAllCommand, - registerCollapseAllLibraryCommand, registerGoToFileCommand, - registerGoToLibraryFileCommand, + registerGoToLingoTomlCommand, registerImportReactorCommand, - registerImportLibraryReactorCommand, + registerIncludeProjectCommand, registerOpenInSplitViewCommand, - registerOpenLibraryInSplitViewCommand, - registerRefreshCommand, - registerRefreshLibraryCommand } from './lfview/lf-data-provider-commands'; + registerOpenInTerminalCommand, + registerRefreshCommand} from './lfview/lf-data-provider-commands'; import * as extensionVersion from './extension_version'; let client: LanguageClient; @@ -81,40 +79,26 @@ export async function activate(context: vscode.ExtensionContext) { registerNewFileCommand(context); // Registers a tree data provider and creates a tree view for the 'lf-lang-local' view - const lfDataProviderLocal = new LFDataProvider(LFDataProviderNodeType.LOCAL, client, context); - context.subscriptions.push(vscode.window.registerTreeDataProvider('lf-lang-local', lfDataProviderLocal)); - const localTreeView = vscode.window.createTreeView('lf-lang-local', { treeDataProvider: lfDataProviderLocal }); - context.subscriptions.push(localTreeView); - localTreeView.onDidExpandElement(element => { - lfDataProviderLocal.onExpandEvent(element.element); + const lfDataProvider = new LFDataProvider(client, context); + context.subscriptions.push(vscode.window.registerTreeDataProvider('lf-lang-projects', lfDataProvider)); + const projectsTreeView = vscode.window.createTreeView('lf-lang-projects', { treeDataProvider: lfDataProvider }); + context.subscriptions.push(projectsTreeView); + projectsTreeView.onDidExpandElement(element => { + lfDataProvider.onExpandEvent(element.element); }); - localTreeView.onDidCollapseElement(element => { - lfDataProviderLocal.onCollapseEvent(element.element); + projectsTreeView.onDidCollapseElement(element => { + lfDataProvider.onCollapseEvent(element.element); }); - // Registers a tree data provider and creates a tree view for the 'lf-lang-library' view - const lfDataProviderLibrary = new LFDataProvider(LFDataProviderNodeType.LIBRARY, client, context); - context.subscriptions.push(vscode.window.registerTreeDataProvider('lf-lang-library', lfDataProviderLibrary)); - const libraryTreeView = vscode.window.createTreeView('lf-lang-library', { treeDataProvider: lfDataProviderLibrary }); - context.subscriptions.push(libraryTreeView); - libraryTreeView.onDidExpandElement(element => { - lfDataProviderLibrary.onExpandEvent(element.element); - }); - libraryTreeView.onDidCollapseElement(element => { - lfDataProviderLibrary.onCollapseEvent(element.element); - }); - - // Register all the commands - registerRefreshCommand(context, lfDataProviderLocal); - registerRefreshLibraryCommand(context, lfDataProviderLibrary); - registerGoToFileCommand(context, lfDataProviderLocal); - registerGoToLibraryFileCommand(context, lfDataProviderLibrary); - registerOpenInSplitViewCommand(context, lfDataProviderLocal); - registerOpenLibraryInSplitViewCommand(context, lfDataProviderLibrary); - registerImportReactorCommand(context, lfDataProviderLocal); - registerImportLibraryReactorCommand(context, lfDataProviderLibrary); + // // Register all the commands + registerRefreshCommand(context, lfDataProvider); + registerGoToFileCommand(context, lfDataProvider); + registerOpenInSplitViewCommand(context, lfDataProvider); + registerImportReactorCommand(context, lfDataProvider); registerCollapseAllCommand(context); - registerCollapseAllLibraryCommand(context); + registerGoToLingoTomlCommand(context, lfDataProvider); + registerIncludeProjectCommand(context, lfDataProvider); + registerOpenInTerminalCommand(context); context.subscriptions.push(vscode.commands.registerCommand( "linguafranca.checkDocker", checkDependencies.checkDocker @@ -122,6 +106,12 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand( "linguafranca.getVersion", () => extensionVersion.version )); + + vscode.window.onDidChangeActiveTextEditor( editor => { + if (editor){ + lfDataProvider.onChangeActiveEditor(); + } + }) } /** diff --git a/src/extension_version.ts b/src/extension_version.ts index 66e1d8dd3..72bec68db 100644 --- a/src/extension_version.ts +++ b/src/extension_version.ts @@ -1,3 +1,3 @@ 'use strict'; // This is a generated file. Do not edit. -export const version = "2e65200594b16d40b836b81aed3a244a488df151"; +export const version = "0aa241b1a6750887e50c4ae04a1a4b74826a7f31"; diff --git a/src/lfview/lf-data-provider-commands.ts b/src/lfview/lf-data-provider-commands.ts index 6d1f541f1..af6c6ddc8 100644 --- a/src/lfview/lf-data-provider-commands.ts +++ b/src/lfview/lf-data-provider-commands.ts @@ -1,29 +1,16 @@ import * as vscode from 'vscode'; import { LFDataProvider, LFDataProviderNode, LFDataProviderNodeType } from './lf-data-provider'; +import { getTerminal } from '../utils'; /** * Registers a command to refresh the local LF libraries tree view. * @param context - The extension context. - * @param local - The LFDataProvider instance managing local LF libraries. + * @param provider - The LFDataProvider instance. */ -export function registerRefreshCommand(context: vscode.ExtensionContext, local: LFDataProvider) { +export function registerRefreshCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( 'linguafranca.refreshEntries', () => { - local.refreshTree(); - } - )); -} - - -/** -* Registers a command to refresh the Lingo downloaded LF libraries tree view. -* @param context - The extension context. -* @param library - The LFDataProvider instance managing Lingo downloaded LF libraries. -*/ -export function registerRefreshLibraryCommand(context: vscode.ExtensionContext, library: LFDataProvider) { - context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.refreshLibraryEntries', () => { - library.refreshTree(); + provider.refreshTree(); } )); } @@ -31,25 +18,12 @@ export function registerRefreshLibraryCommand(context: vscode.ExtensionContext, /** * Registers a command to navigate to a file in the local LF libraries tree view. * @param context - The extension context. - * @param local - The LFDataProvider instance managing local LF libraries. + * @param provider - The LFDataProvider instance. */ -export function registerGoToFileCommand(context: vscode.ExtensionContext, local: LFDataProvider) { +export function registerGoToFileCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( 'linguafranca.goToFile', (node: LFDataProviderNode) => { - local.goToFileCommand(node, false); - } - )); -} - -/** - * Registers a command to navigate to a file in the Lingo downloaded LF libraries tree view. - * @param context - The extension context. - * @param library - The LFDataProvider instance managing Lingo downloaded LF libraries. - */ -export function registerGoToLibraryFileCommand(context: vscode.ExtensionContext, library: LFDataProvider) { - context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.goToLibraryFile', (node: LFDataProviderNode) => { - library.goToFileCommand(node, false); + provider.goToFileCommand(node, false); } )); } @@ -57,76 +31,74 @@ export function registerGoToLibraryFileCommand(context: vscode.ExtensionContext, /** * Registers a command to open a file in split view in the local LF libraries tree view. * @param context - The extension context. - * @param local - The LFDataProvider instance managing local LF libraries. + * @param provider - The LFDataProvider instance. */ -export function registerOpenInSplitViewCommand(context: vscode.ExtensionContext, local: LFDataProvider) { +export function registerOpenInSplitViewCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( 'linguafranca.openInSplitView', (node: LFDataProviderNode) => { - local.goToFileCommand(node, true); + provider.goToFileCommand(node, true); } )); } /** - * Registers a command to open a file in split view in the Lingo downloaded LF libraries tree view. + * Registers a command to import a reactor from the local LF libraries into the active LF program. * @param context - The extension context. - * @param library - The LFDataProvider instance managing Lingo downloaded LF libraries. + * @param provider - The LFDataProvider instance. */ -export function registerOpenLibraryInSplitViewCommand(context: vscode.ExtensionContext, library: LFDataProvider) { +export function registerImportReactorCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.openLibraryInSplitView', (node: LFDataProviderNode) => { - library.goToFileCommand(node, true); + 'linguafranca.importReactor', async (node: LFDataProviderNode) => { + if(node.type === LFDataProviderNodeType.LOCAL) { + await provider.importReactorCommand(node); + } + else { + await provider.importLibraryReactorCommand(node); + } } )); } /** - * Registers a command to import a reactor from the local LF libraries into the active LF program. + * Registers a command to collapse all nodes in the local LF libraries tree view. * @param context - The extension context. - * @param local - The LFDataProvider instance managing local LF libraries. */ -export function registerImportReactorCommand(context: vscode.ExtensionContext, local: LFDataProvider) { +export function registerCollapseAllCommand(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.importReactor', async (node: LFDataProviderNode) => { - await local.importReactorCommand(node); + 'linguafranca.collapseAll', () => { + vscode.commands.executeCommand('workbench.actions.treeView.lf-lang-projects.collapseAll'); } )); } -/** - * Registers a command to import a reactor from the Lingo downloaded LF libraries into the active LF program. - * @param context - The extension context. - * @param library - The LFDataProvider instance managing Lingo downloaded LF libraries. - */ -export function registerImportLibraryReactorCommand(context: vscode.ExtensionContext, library: LFDataProvider) { +export function registerGoToLingoTomlCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.importLibraryReactor', async (node: LFDataProviderNode) => { - await library.importLibraryReactorCommand(node); + 'linguafranca.goToLingoToml', (node: LFDataProviderNode) => { + provider.goToLingoTomlCommand(node); } )); } -/** - * Registers a command to collapse all nodes in the local LF libraries tree view. - * @param context - The extension context. - * @param local - The LFDataProvider instance managing local LF libraries. - */ -export function registerCollapseAllCommand(context: vscode.ExtensionContext) { +export function registerIncludeProjectCommand(context: vscode.ExtensionContext, provider: LFDataProvider) { context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.collapseAll', () => { - vscode.commands.executeCommand('workbench.actions.treeView.lf-lang-local.collapseAll'); + 'linguafranca.includeProject', (node: LFDataProviderNode) => { + vscode.window.showInformationMessage('The "Include Project" feature is not implemented yet.', 'Details').then(selection => { + if (selection === "Details") { + vscode.window.showInformationMessage('Please use the Lingo command line to include the selected library in your current project. Once included, the library will appear under the "Lingo Packages" section.'); + } + }); } )); } -/** - * Registers a command to collapse all nodes in the Lingo downloaded LF libraries tree view. - * @param context - The extension context. - */ -export function registerCollapseAllLibraryCommand(context: vscode.ExtensionContext) { +export function registerOpenInTerminalCommand(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand( - 'linguafranca.collapseAllLibrary', () => { - vscode.commands.executeCommand('workbench.actions.treeView.lf-lang-library.collapseAll'); + 'linguafranca.openInTerminal', (node: LFDataProviderNode) => { + const terminal = getTerminal("Lingua Franca") + if (terminal) { + terminal.show(true); + terminal.sendText(`cd ${node.uri.fsPath} && clear`); + } } )); } diff --git a/src/lfview/lf-data-provider.ts b/src/lfview/lf-data-provider.ts index 05f4b05f4..274bb0276 100644 --- a/src/lfview/lf-data-provider.ts +++ b/src/lfview/lf-data-provider.ts @@ -4,25 +4,34 @@ import { LanguageClient } from 'vscode-languageclient'; /** * Defines the different roles of nodes that can be displayed in the LFDataProvider tree view. - * {@code ROOT}: Represents the root node of the tree view. - * {@code FILE}: Represents a file node in the tree view. - * {@code REACTOR}: Represents a reactor node in the tree view. + * + * @property PROJECT - Represents the project node of the tree view. + * @property ROOT - Represents the root node of a lingo library + * @property SUB - Represents a sub-directory within the project, which may contain local libraries, Lingo packages, or source files. + * @property SRC - Represents a Lingua Franca file located in the project’s `src` directory. + * @property FILE - Represents a file node in the tree view. + * @property REACTOR - Represents a reactor node in the tree view. */ export enum LFDataProviderNodeRole { + PROJECT = 'project', ROOT = 'root', + SUB = 'sub', + SRC = 'src', FILE = 'file', REACTOR = 'reactor' } /** - * Defines the types of the displayed data provider tree view. + * Defines the different types of nodes that can be displayed in the LFDataProvider tree view. * - * {@code LOCAL} represents nodes for local LF libraries. - * {@code LIBRARY} represents nodes for LF libraries downloaded using Lingo. + * @property LOCAL - Represents a local library node. + * @property LIBRARY - Represents a library node. + * @property SOURCE - Represents a source file node. */ export enum LFDataProviderNodeType { LOCAL = 1, - LIBRARY = 2 + LIBRARY = 2, + SOURCE = 3 } /** @@ -39,7 +48,7 @@ export class LFDataProviderNode extends vscode.TreeItem { children: LFDataProviderNode[] | undefined; role: string; position: NodePosition | undefined; - type: LFDataProviderNodeType; + type: LFDataProviderNodeType | undefined; /** * Represents the URI of the data provider node. @@ -47,18 +56,141 @@ export class LFDataProviderNode extends vscode.TreeItem { */ uri: vscode.Uri; - constructor(label: string, uri: string, role: string, type: LFDataProviderNodeType, - children?: LFDataProviderNode[] | undefined, position?: NodePosition | undefined) { - let newLabel = label.replace('.lf', ''); - super(newLabel, role === LFDataProviderNodeRole.REACTOR ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed); + constructor(label: string, uri: string, role: string, + type?: LFDataProviderNodeType | undefined, + children?: LFDataProviderNode[] | undefined, + position?: NodePosition | undefined) { + let newLabel = type === LFDataProviderNodeType.SOURCE ? label : label.replace('.lf', ''); + super(newLabel, role === LFDataProviderNodeRole.REACTOR || + (role === LFDataProviderNodeRole.FILE && type === LFDataProviderNodeType.SOURCE) + ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed); this.uri = vscode.Uri.parse(uri); this.children = children; this.role = role; this.type = type; - let icon = role === LFDataProviderNodeRole.ROOT ? 'root-folder' : role === LFDataProviderNodeRole.FILE ? 'file-code' : 'json'; - this.iconPath = new vscode.ThemeIcon(icon); - this.contextValue = role; + this.updateIcon(role, type); + this.updateContextValue(role, type); if (position) { this.position = position; } + if(role === LFDataProviderNodeRole.FILE && type === LFDataProviderNodeType.SOURCE){ + this.command = { + title: "Go to File", + command: "vscode.open", + arguments: [this.uri] + } + } + } + + /** + * Determines the appropriate icon to display for a node in the LFDataProvider tree based on its role and type. + * + * @param role - The role of the node (e.g. project, root, file, reactor, sub). + * @param type - The type of the node (e.g. local, library, source). + * @returns The name of the icon to display for the node. + */ + updateIcon(role: string, type?: LFDataProviderNodeType): void { + const sameRootAsEditor = this.haveSameRootWithActiveEditor(); + let newIcon = ''; + + switch (role) { + case LFDataProviderNodeRole.PROJECT: + newIcon = 'project'; + break; + case LFDataProviderNodeRole.ROOT: + newIcon = 'root-folder'; + break; + case LFDataProviderNodeRole.FILE: + newIcon = 'file-code'; + break; + case LFDataProviderNodeRole.REACTOR: + newIcon = 'json'; + break; + case LFDataProviderNodeRole.SUB: + switch (type) { + case LFDataProviderNodeType.LOCAL: + newIcon = 'book'; + break; + case LFDataProviderNodeType.LIBRARY: + newIcon = 'library'; + break; + case LFDataProviderNodeType.SOURCE: + newIcon = 'circuit-board'; + break; + default: + newIcon = 'default-icon'; // fallback for unknown types + } + break; + default: + newIcon = 'default-icon'; // fallback for unknown roles + } + + this.iconPath = new vscode.ThemeIcon( + newIcon, + sameRootAsEditor + ? new vscode.ThemeColor('editorIcon.currentProject') + : new vscode.ThemeColor('editorIcon.notCurrentProject') + ); + } + + /** + * Updates the context value of the LFDataProviderNode based on its role and type. + * The context value is used to determine the appropriate visual representation of the node in the tree view. + * + * @param role - The role of the node (e.g. project, root, file, reactor, sub). + * @param type - The type of the node (e.g. local, library, source). + */ + updateContextValue(role: string, type?: LFDataProviderNodeType): void { + const sameRootAsEditor = this.haveSameRootWithActiveEditor(); + + let value: string = role; + + switch (role) { + case LFDataProviderNodeRole.ROOT: + value = sameRootAsEditor ? 'root-included' : 'root'; + break; + case LFDataProviderNodeRole.SUB: + if (type === LFDataProviderNodeType.LIBRARY) { + value = 'lingo'; + } + break; + case LFDataProviderNodeRole.FILE: + if (type === LFDataProviderNodeType.LOCAL) { + value = sameRootAsEditor ? 'file-local-included' : 'file-local'; + } else if (type === LFDataProviderNodeType.LIBRARY) { + value = 'file-lingo'; + } + break; + case LFDataProviderNodeRole.REACTOR: + value = sameRootAsEditor ? 'reactor-included' : 'reactor'; + break; + } + + this.contextValue = value; + } + + /** + * Determines whether the current node's root path is the same as the active editor's root path. + * This is used to determine the appropriate context value for the node in the tree view. + * + * @returns `true` if the current node's root path is the same as the active editor's root path, `false` otherwise. + */ + haveSameRootWithActiveEditor(): boolean { + const editor = vscode.window.activeTextEditor; + + if (!editor || !editor.document) { + return false; + } + if(this.role === LFDataProviderNodeRole.PROJECT){ + return editor.document.uri.fsPath.startsWith(this.uri.fsPath); + } + const pathSegments = this.uri.fsPath.split('/'); + const srcOrBuildIndex = pathSegments.indexOf(this.type === LFDataProviderNodeType.LIBRARY ? 'build' : 'src'); + + if (srcOrBuildIndex === -1) { + return false; + } + + const rootPath = pathSegments.slice(0, srcOrBuildIndex).join('/'); + return editor.document.uri.fsPath.startsWith(rootPath); } } @@ -83,25 +215,22 @@ export class NodePosition { */ export class LFDataProvider implements vscode.TreeDataProvider { - // Offset used to highlight text in goToFile Command - private HIGHLIGHT_OFFSET = 100; - // LF libraries data private data: LFDataProviderNode[] = []; - // Type of the data provider - private type: LFDataProviderNodeType; // Utility properties - private searchPath: string; - private path_offset: number; - private exclude_path: vscode.GlobPattern | null = null; + private searchSourceFiles: string = '**/src/*.lf'; + private searchPathLocal: string = '**/src/lib/*.lf'; + private searchPathLibrary: vscode.GlobPattern = '**/build/lfc_include/**/src/lib/*.lf'; + private exclude_path_local: vscode.GlobPattern = '**/build/**'; // only for local LF libraries + private exclude_path_src: vscode.GlobPattern = `{${this.exclude_path_local},**/fed-gen/**,**/src-gen/***}` // Event emitter for tree data change private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; // Watches for changes to .lf files - private watcher: vscode.FileSystemWatcher; + private watcher: vscode.FileSystemWatcher | undefined; /** * Constructs a new LFDataProvider instance with the given type, client, and extension context. @@ -109,24 +238,16 @@ export class LFDataProvider implements vscode.TreeDataProvider { - this.refreshTree(); - }), - this.watcher.onDidCreate(() => { - this.refreshTree(); - }), - this.watcher.onDidDelete(() => { - this.refreshTree(); - }) - ); + watchFileChanges(context: vscode.ExtensionContext): void { + this.watcher = vscode.workspace.createFileSystemWatcher(`**/*.lf`, false, false, false); + this.watcher.onDidChange(() => { + this.refreshTree(); + }), + this.watcher.onDidCreate(() => { + this.refreshTree(); + }), + this.watcher.onDidDelete(() => { + this.refreshTree(); + }) + context.subscriptions.push(this.watcher); } /** @@ -195,11 +309,14 @@ export class LFDataProvider implements vscode.TreeDataProvider { + if (root.children?.length) { + root.updateIcon(root.role, root.type); + const updateIfExists = (type: LFDataProviderNodeType) => { + const node = root.children?.find(n => n.role === LFDataProviderNodeRole.SUB && n.type === type); + node?.updateIcon(node.role, node.type); + if (node?.children?.length) { + this.updateContextValueAndIcon(node.children); + } + }; + + updateIfExists(LFDataProviderNodeType.LOCAL); + updateIfExists(LFDataProviderNodeType.LIBRARY); + updateIfExists(LFDataProviderNodeType.SOURCE); + } + }); + + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Recursively updates the context value for the given LFDataProviderNode elements. + * This method iterates through the provided elements and updates the context value + * for each node based on its role and type. If a node has children, the method + * is called recursively to update the context value for the child nodes as well. + * + * @param elements - An array of LFDataProviderNode elements to update. + */ + updateContextValueAndIcon(elements: LFDataProviderNode[]): void { + elements.forEach((node: LFDataProviderNode) => { + node.updateContextValue(node.role, node.type); + node.updateIcon(node.role, node.type); + if (node.children?.length) { + this.updateContextValueAndIcon(node.children); + } + }); + } + /** * Refreshes the LF libraries tree view by fetching the latest library reactor information from the Language Server. */ @@ -224,83 +391,153 @@ export class LFDataProvider implements vscode.TreeDataProvider { - vscode.workspace.findFiles(this.searchPath, this.exclude_path ? this.exclude_path : null).then(uris => { - uris.forEach(uri => { - this.client.sendRequest('generator/getLibraryReactors', uri.toString()).then(node => { - if(node){ - this.addDataItem(node as LFDataProviderNode); - } - else if(node === null){ - vscode.window.showErrorMessage('Error retrieving data from the Language Server', ...['Try again', 'Cancel']).then(selection => { - if(selection === 'Try again'){ - this.refreshTree(); - } - }); - return; - } - }); - }); - }); + // Find all source files + this.findFiles(this.searchSourceFiles, this.exclude_path_src, LFDataProviderNodeType.SOURCE); + // Find all local reusable reactor libraries + this.findFiles(this.searchPathLocal, this.exclude_path_local, LFDataProviderNodeType.LOCAL); + // Find all lingo downloaded reactor libraries + this.findFiles(this.searchPathLibrary, null, LFDataProviderNodeType.LIBRARY); }); } this._onDidChangeTreeData.fire(undefined); } + findFiles(searchPath: string | vscode.GlobPattern, exclude_path: vscode.GlobPattern | null, type: LFDataProviderNodeType): void { + vscode.workspace.findFiles(searchPath, exclude_path ? exclude_path : null).then(uris => { + uris.forEach(uri => { + this.client.sendRequest('generator/getLibraryReactors', uri.toString()).then(node => { + if(node){ + this.addDataItem(node as LFDataProviderNode, type); + } + else if(node === null){ + vscode.window.showErrorMessage('Error retrieving data from the Language Server'); + return; + } + }); + }); + }); + } + /** * Adds a new data item to the LFDataProvider tree. * @param dataNode - The data node to add to the tree. + * @param type - The type of the node (e.g., LOCAL, LIBRARY, SOURCE). */ - addDataItem(dataNode: LFDataProviderNode) { - if (this.type === LFDataProviderNodeType.LOCAL) { - this.addDataItemLocal(dataNode); - } else { - this.addDataItemLibrary(dataNode); + addDataItem(dataNode: LFDataProviderNode, type: LFDataProviderNodeType) { + const root = this.buildRoot(dataNode.uri.toString(), type); + const node = this.createNode(dataNode, type, LFDataProviderNodeRole.FILE); + + // Add child nodes if applicable + this.addChildNodes(dataNode, node, type); + + switch (type) { + case LFDataProviderNodeType.LIBRARY: + this.handleLibraryNode(root, node, dataNode); + break; + case LFDataProviderNodeType.LOCAL: + this.handleLocalNode(root, node, dataNode); + break; + case LFDataProviderNodeType.SOURCE: + this.handleSourceNode(root, node, dataNode); + break; } + + // Sort data after adding the new node + this.sortData(); } /** - * Adds a data item to the Local Libraries view. - * @param dataNode - The data node to add. + * Creates a new LFDataProviderNode. + * @param dataNode - The data node to create. + * @param type - The type of the node. + * @param role - The role of the node (e.g., FILE, REACTOR). */ - addDataItemLocal(dataNode: LFDataProviderNode) { - const root = this.buildRoot(dataNode.uri.toString()); - let node = new LFDataProviderNode(dataNode.label!.toString(), dataNode.uri.toString(), LFDataProviderNodeRole.FILE, this.type, []); - root.children!.push(node); - if (dataNode.children!.length > 0) { - dataNode.children!.forEach((child: LFDataProviderNode) => { - node.children!.push(new LFDataProviderNode(child.label!.toString(), - child.uri.toString(), - LFDataProviderNodeRole.REACTOR, - this.type, [], - child.position - )); + createNode(dataNode: LFDataProviderNode, type: LFDataProviderNodeType, role: LFDataProviderNodeRole): LFDataProviderNode { + return new LFDataProviderNode( + dataNode.label!.toString(), + dataNode.uri.toString(), + role, + type, + [], + dataNode.position + ); + } + + /** + * Adds child nodes to a given node if applicable. + * @param dataNode - The parent data node. + * @param node - The node to which children will be added. + * @param type - The type of the parent node. + */ + addChildNodes(dataNode: LFDataProviderNode, node: LFDataProviderNode, type: LFDataProviderNodeType) { + if (type !== LFDataProviderNodeType.SOURCE && dataNode.children?.length) { + dataNode.children.forEach((child: LFDataProviderNode) => { + node.children!.push(this.createNode(child, type, LFDataProviderNodeRole.REACTOR)); }); } - this.sortData(); } /** - * Adds a data item to the Lingo Libraries view. - * @param dataNode - The data node to add. + * Handles the addition of a LIBRARY type node. + * @param root - The root node of the tree. + * @param node - The node to add to the tree. + * @param dataNode - The data node being added. */ - addDataItemLibrary(dataNode: LFDataProviderNode) { - const root = this.buildRoot(dataNode.uri.toString()); - const library_root = this.buildLibraryRoot(dataNode.uri.toString(), root); - let node = new LFDataProviderNode(dataNode.label!.toString(), dataNode.uri.toString(), LFDataProviderNodeRole.FILE, this.type, []); - if (dataNode.children!.length > 0) { - dataNode.children!.forEach((child: LFDataProviderNode) => { - node.children!.push(new LFDataProviderNode(child.label!.toString(), - child.uri.toString(), - LFDataProviderNodeRole.REACTOR, - this.type, [], - child.position - )); - }); + handleLibraryNode(root: LFDataProviderNode, node: LFDataProviderNode, dataNode: LFDataProviderNode) { + const libraryRoot = this.buildLibraryRoot(dataNode.uri.toString(), root, dataNode); + if (!libraryRoot.children?.some(n => n.label === node.label)) { + libraryRoot.children!.push(node); } - if (library_root.children!.find(n => n.label === node.label) === undefined) { - library_root.children!.push(node); + } + + /** + * Handles the addition of a LOCAL type node. + * @param root - The root node of the tree. + * @param node - The node to add to the tree. + * @param dataNode - The data node being added. + */ + handleLocalNode(root: LFDataProviderNode, node: LFDataProviderNode, dataNode: LFDataProviderNode) { + let localNode = this.findOrCreateSubNode(root, "Local Libraries", LFDataProviderNodeRole.SUB, LFDataProviderNodeType.LOCAL, dataNode); + if(!localNode.children?.some(n => n.label === node.label)) + localNode.children!.push(node); + } + + /** + * Handles the addition of a SOURCE type node. + * @param root - The root node of the tree. + * @param node - The node to add to the tree. + * @param dataNode - The data node being added. + */ + handleSourceNode(root: LFDataProviderNode, node: LFDataProviderNode, dataNode: LFDataProviderNode) { + let srcNode = this.findOrCreateSubNode(root, "Source Files", LFDataProviderNodeRole.SUB, LFDataProviderNodeType.SOURCE, dataNode); + if(!srcNode.children?.some(n => n.label === node.label)) + srcNode.children!.push(node); + } + + /** + * Finds or creates a sub-node with a given label, role, and type. + * @param root - The root node to search in. + * @param label - The label of the sub-node to find or create. + * @param role - The role of the sub-node. + * @param type - The type of the sub-node. + * @param dataNode - The data node associated with the sub-node. + * @returns The found or newly created sub-node. + */ + findOrCreateSubNode(root: LFDataProviderNode, label: string, role: LFDataProviderNodeRole, type: LFDataProviderNodeType, dataNode: LFDataProviderNode): LFDataProviderNode { + let subNode = root.children?.find(n => n.role === role && n.type === type); + + if (!subNode) { + subNode = new LFDataProviderNode( + label, + dataNode.uri.toString(), + role, + type, + [] + ); + root.children!.push(subNode); } - this.sortData(); + + return subNode; } /** @@ -338,38 +575,41 @@ export class LFDataProvider implements vscode.TreeDataProvider item.label === projectLabel); - if (!existingProject) { - const projectUri = splittedUri.slice(0, - this.path_offset).join('/') + '/'; - const root = new LFDataProviderNode(projectLabel, projectUri, LFDataProviderNodeRole.ROOT, this.type, []); - this.data.push(root); - return root; - } - return existingProject; + const srcIdx = splittedUri.indexOf(!type || type == LFDataProviderNodeType.LIBRARY ? 'build' : 'src'); + const projectLabel = splittedUri[srcIdx - 1]; + + const existingProject = this.data.find(item => item.label === projectLabel); + if (!existingProject) { + const projectUri = splittedUri.slice(0, srcIdx).join('/') + '/'; + const root = new LFDataProviderNode(projectLabel, projectUri, LFDataProviderNodeRole.PROJECT, type!, []); + this.data.push(root); + return root; + } + return existingProject; } - + /** - * Builds the root node of the Lingo Library tree for the given URI. - * @param uri - The URI of the item to build. - * @param root - The root node of the library tree. - * @returns The root node of the library tree, either a new node or an existing one. + * Builds or retrieves the root node for a library project based on the URI. + * @param uri - The URI of the data node. + * @param root - The root node of the tree. + * @returns The root node for the library project. */ - buildLibraryRoot(uri: string, root: LFDataProviderNode): LFDataProviderNode { + buildLibraryRoot(uri: string, root: LFDataProviderNode, dataNode: LFDataProviderNode): LFDataProviderNode { const splittedUri = uri.split('/'); - const projectLabel = splittedUri[splittedUri.length - this.path_offset + 3]; - - const existingLibraryRoot = root.children!.find(item => item.label === projectLabel); - if (!existingLibraryRoot) { - const projectUri = splittedUri.slice(0, - this.path_offset + 3).join('/') + '/'; - const library_root = new LFDataProviderNode(projectLabel, projectUri, LFDataProviderNodeRole.ROOT, this.type, []); - root.children!.push(library_root); - return library_root; + const srcIdx = splittedUri.indexOf('src'); + const projectLabel = splittedUri[srcIdx - 1]; + + let lingo = this.findOrCreateSubNode(root, "Lingo Packages", LFDataProviderNodeRole.SUB, LFDataProviderNodeType.LIBRARY, dataNode); + const existingProject = lingo.children!.find(item => item.label === projectLabel); + if (!existingProject) { + const projectUri = splittedUri.slice(0, srcIdx).join('/') + '/'; + const root = new LFDataProviderNode(projectLabel, projectUri, LFDataProviderNodeRole.ROOT, LFDataProviderNodeType.LIBRARY, []); + lingo.children!.push(root); + return root; } - return existingLibraryRoot; + return existingProject; } /** @@ -522,4 +762,18 @@ export class LFDataProvider implements vscode.TreeDataProvider { + vscode.window.showTextDocument(doc); + }); + } + } \ No newline at end of file