diff --git a/package.json b/package.json index 098a25856..0d1270235 100644 --- a/package.json +++ b/package.json @@ -67,14 +67,6 @@ "dark": "#57cc99", "light": "#57cc99" } - }, - { - "id": "editorIcon.notCurrentProject", - "description": "Color for a TreeIteem label", - "defaults": { - "dark": "#fcbf49", - "light": "#fcbf49" - } } ], "semanticTokenScopes": [ @@ -266,7 +258,7 @@ "activitybar": [ { "id": "lf-lang", - "title": "Lingua Franca Projects", + "title": "Lingua Franca Package Explorer", "icon": "images/logo/lf-logo-dark.svg" } ] @@ -278,7 +270,13 @@ "name": "" } ] - } + }, + "viewsWelcome": [ + { + "view": "lf-lang-projects", + "contents": "No Lingua Franca project found. [Learn more](https://www.lf-lang.org/docs/) about setting up a Lingua Franca project structure.\n[Open Lingua Franca Project](command:linguafranca.openFolder)" + } + ] }, "devDependencies": { "@types/chai": "^4.3.1", diff --git a/src/extension.ts b/src/extension.ts index 983aa67ab..15c1e4d23 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import { registerCollapseAllCommand, registerGoToLingoTomlCommand, registerImportReactorCommand, registerIncludeProjectCommand, + registerOpenFolderCommand, registerOpenInSplitViewCommand, registerOpenInTerminalCommand, registerRefreshCommand} from './lfview/lf-data-provider-commands'; @@ -99,6 +100,7 @@ export async function activate(context: vscode.ExtensionContext) { registerGoToLingoTomlCommand(context, lfDataProvider); registerIncludeProjectCommand(context, lfDataProvider); registerOpenInTerminalCommand(context); + registerOpenFolderCommand(context); context.subscriptions.push(vscode.commands.registerCommand( "linguafranca.checkDocker", checkDependencies.checkDocker diff --git a/src/lfview/lf-data-provider-commands.ts b/src/lfview/lf-data-provider-commands.ts index af6c6ddc8..9f4805d4e 100644 --- a/src/lfview/lf-data-provider-commands.ts +++ b/src/lfview/lf-data-provider-commands.ts @@ -103,3 +103,22 @@ export function registerOpenInTerminalCommand(context: vscode.ExtensionContext) )); } +export function registerOpenFolderCommand(context: vscode.ExtensionContext) { + context.subscriptions.push(vscode.commands.registerCommand( + 'linguafranca.openFolder', async ( ) => { + // Prompt the user to select a folder + const folderUri = await vscode.window.showOpenDialog({ + canSelectFolders: true, // Allow folder selection + canSelectFiles: false, // Disallow file selection + canSelectMany: false, // Allow only a single folder + openLabel: 'Select Folder' + }); + + if (folderUri && folderUri[0]) { + // Use vscode.openFolder to open the selected folder + vscode.commands.executeCommand('vscode.openFolder', folderUri[0], false); + } + } + )); +} + diff --git a/src/lfview/lf-data-provider.ts b/src/lfview/lf-data-provider.ts index 96e6c960b..75cedabc9 100644 --- a/src/lfview/lf-data-provider.ts +++ b/src/lfview/lf-data-provider.ts @@ -30,7 +30,7 @@ export enum LFDataProviderNodeRole { */ export enum LFDataProviderNodeType { LOCAL = 1, - LIBRARY = 2, + LIBRARY = 2, SOURCE = 3 } @@ -58,12 +58,12 @@ export class LFDataProviderNode extends vscode.TreeItem { constructor(label: string, uri: string, role: string, type?: LFDataProviderNodeType | undefined, - children?: LFDataProviderNode[] | 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); + 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; @@ -71,7 +71,7 @@ export class LFDataProviderNode extends vscode.TreeItem { this.updateIcon(role, type); this.updateContextValue(role, type); if (position) { this.position = position; } - if(role === LFDataProviderNodeRole.FILE && type === LFDataProviderNodeType.SOURCE){ + if (role === LFDataProviderNodeRole.FILE && type === LFDataProviderNodeType.SOURCE) { this.command = { title: "Go to File", command: "vscode.open", @@ -90,7 +90,7 @@ export class LFDataProviderNode extends vscode.TreeItem { updateIcon(role: string, type?: LFDataProviderNodeType): void { const sameRootAsEditor = this.haveSameRootWithActiveEditor(); let newIcon = ''; - + switch (role) { case LFDataProviderNodeRole.PROJECT: newIcon = 'project'; @@ -122,15 +122,15 @@ export class LFDataProviderNode extends vscode.TreeItem { 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') + newIcon, + sameRootAsEditor + ? new vscode.ThemeColor('editorIcon.currentProject') + : undefined ); } - + /** * 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. @@ -140,9 +140,9 @@ export class LFDataProviderNode extends vscode.TreeItem { */ updateContextValue(role: string, type?: LFDataProviderNodeType): void { const sameRootAsEditor = this.haveSameRootWithActiveEditor(); - + let value: string = role; - + switch (role) { case LFDataProviderNodeRole.ROOT: value = sameRootAsEditor ? 'root-included' : 'root'; @@ -163,10 +163,10 @@ export class LFDataProviderNode extends vscode.TreeItem { 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. @@ -179,18 +179,20 @@ export class LFDataProviderNode extends vscode.TreeItem { if (!editor || !editor.document) { return false; } - if(this.role === LFDataProviderNodeRole.PROJECT){ + + 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'); - + + const pathSegments = this.uri.path.split('/'); + const srcOrBuildIndex = pathSegments.lastIndexOf(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); + return editor.document.uri.path.startsWith(rootPath); } } @@ -219,11 +221,11 @@ export class LFDataProvider implements vscode.TreeDataProvider = new vscode.EventEmitter(); @@ -245,7 +247,6 @@ export class LFDataProvider implements vscode.TreeDataProvider { this.refreshTree(); }), - this.watcher.onDidCreate(() => { - this.refreshTree(); - }), - this.watcher.onDidDelete(() => { - this.refreshTree(); - }) + this.watcher.onDidCreate(() => { + this.refreshTree(); + }), + this.watcher.onDidDelete(() => { + this.refreshTree(); + }) context.subscriptions.push(this.watcher); } @@ -355,16 +356,15 @@ export class LFDataProvider implements vscode.TreeDataProvider { - uris.forEach(uri => { + /** + * Finds files matching the provided search path and excludes files matching the exclude path. Processes the found files and adds them to the LFDataProvider tree. + * + * @param searchPath - The search path pattern to use for finding files. + * @param excludePath - The exclude path pattern to use for filtering out files. + * @param type - The type of the node (e.g., LOCAL, LIBRARY, SOURCE). + */ + findFiles( + searchPath: string | vscode.GlobPattern, + excludePath: vscode.GlobPattern | null, + type: LFDataProviderNodeType + ): void { + vscode.workspace.findFiles(searchPath, excludePath).then(uris => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + + if (!workspaceFolder) { + vscode.window.showErrorMessage('No workspace folder is open.'); + return; + } + + // Process each URI found and get relative path + const filteredUris = (type === LFDataProviderNodeType.SOURCE) + ? uris.filter(file => { + const relativePath = vscode.workspace.asRelativePath(file, false); + // Count occurrences of nested '/src/' + const srcCount = (relativePath.match(/\/src\//g) || []).length; + // Exclude paths with more than one 'src' + return srcCount == 0; + }) + : uris; + + // Process each URI found + filteredUris.forEach(uri => { this.client.sendRequest('generator/getLibraryReactors', uri.toString()).then(node => { - if(node){ + if (node) { this.addDataItem(node as LFDataProviderNode, type); - } - else if(node === null){ + } else { vscode.window.showErrorMessage('Error retrieving data from the Language Server'); return; } @@ -498,7 +527,7 @@ export class LFDataProvider implements vscode.TreeDataProvider n.label === node.label)) + if (!localNode.children?.some(n => n.label === node.label)) localNode.children!.push(node); } @@ -510,7 +539,7 @@ export class LFDataProvider implements vscode.TreeDataProvider n.label === node.label)) + if (!srcNode.children?.some(n => n.label === node.label)) srcNode.children!.push(node); } @@ -547,8 +576,12 @@ export class LFDataProvider implements vscode.TreeDataProvider { - const labelA = typeof a.label === 'string' ? a.label : a.uri.fsPath.split('/').pop() || ''; - const labelB = typeof b.label === 'string' ? b.label : b.uri.fsPath.split('/').pop() || ''; + const labelA = typeof a.label === 'string' + ? a.label + : a.uri.fsPath.split('/').pop() || ''; + const labelB = typeof b.label === 'string' + ? b.label + : b.uri.fsPath.split('/').pop() || ''; return labelA.localeCompare(labelB); }); this.data.forEach(n => this.sortNodes(n)); @@ -577,19 +610,19 @@ export class LFDataProvider implements vscode.TreeDataProvider 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; + const srcIdx = splittedUri.lastIndexOf(!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 or retrieves the root node for a library project based on the URI. * @param uri - The URI of the data node. @@ -598,10 +631,10 @@ export class LFDataProvider implements vscode.TreeDataProvider item.label === projectLabel); if (!existingProject) { const projectUri = splittedUri.slice(0, srcIdx).join('/') + '/'; @@ -648,7 +681,7 @@ export class LFDataProvider implements vscode.TreeDataProvider\n`; const position = await this.getTargetPosition(editor.document.uri); this.addTextOnActiveEditor(editor, position!.end, importText); @@ -692,18 +725,18 @@ export class LFDataProvider implements vscode.TreeDataProvider { return this.client.onReady().then(() => { @@ -758,17 +791,27 @@ export class LFDataProvider implements vscode.TreeDataProvider { - vscode.window.showTextDocument(doc); - }); + + vscode.workspace.openTextDocument(vscode.Uri.file(newUri)) + .then(doc => { + vscode.window.showTextDocument(doc); + }, (error: Error) => { + vscode.window.showErrorMessage(`Failed to open Lingo.toml: ${error.message}`); + }); } -} \ No newline at end of file +}