diff --git a/package.json b/package.json index 9db3a46..1f7a978 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,40 @@ "command": "extension.refreshRTThread", "title": "Refresh", "icon": "$(sync)" + }, + { + "command": "extension.showWorkspaceSettings", + "title": "Show Workspace Settings", + "icon": "$(settings-gear)" + }, + { + "command": "extension.switchProject", + "title": "Switch project to this bsp", + "icon": "$(pass-filled)" + }, + { + "command": "extension.fastBuildProject", + "title": "Build (-j CPU)...", + "icon": "$(github-action)" + }, + { + "command": "extension.configProject", + "title": "Menuconfig", + "icon": "$(gear)" + }, + { + "command": "extension.openTerminalProject", + "title": "Open RT-Thread Terminal", + "icon": "$(console)" } ], "menus": { "view/title": [ + { + "command": "extension.showWorkspaceSettings", + "when": "(view == projectFilesId || view == groupsId) && isRTThreadWorksapce", + "group": "navigation" + }, { "command": "extension.refreshRTThread", "when": "view == projectFilesId || view == groupsId", @@ -87,6 +117,28 @@ "when": "(view == projectFilesId || view == groupsId) && isRTThread", "group": "navigation" } + ], + "view/item/context": [ + { + "command": "extension.switchProject", + "when": "view == projectFilesId && viewItem == project_bsp", + "group": "inline" + }, + { + "command": "extension.fastBuildProject", + "when": "view == projectFilesId && viewItem == project_bsp", + "group": "inline" + }, + { + "command": "extension.configProject", + "when": "view == projectFilesId && viewItem == project_bsp", + "group": "inline" + }, + { + "command": "extension.openTerminalProject", + "when": "view == projectFilesId && viewItem == project_bsp", + "group": "inline" + } ] }, "configuration": { diff --git a/src/api.ts b/src/api.ts index e7ef196..7b21b99 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,16 +6,20 @@ import * as path from 'path'; import { executeCommand } from './terminal'; let _context: vscode.ExtensionContext; -let _isRTThread = false; let _bi: any[] = []; export function isRTThreadProject() { - return _isRTThread; + let status = _context.workspaceState.get('isRTThread') || false; + return status; } -export function initAPI(context:vscode.ExtensionContext, isRTThreadEnv: boolean) { +export function isRTThreadWorksapce() { + let status = _context.workspaceState.get('isRTThreadWorksapce') || false; + return status; +} + +export function initAPI(context:vscode.ExtensionContext) { _context = context; - _isRTThread = isRTThreadEnv; // read resources/bi.json for boards information _bi = readJsonObject(path.join(getExtensionPath(), "resources", "bi.json")); @@ -149,7 +153,7 @@ export async function openFolder(uri?: string) { canSelectFolders: true, canSelectFiles: false, canSelectMany: false, - }); + }); } else { selectedFolder = await vscode.window.showOpenDialog({ diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 4b11237..1cadeef 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -50,6 +50,16 @@ export let cmds: Object = { ] } }, + { + name : "fix config", + iconId : "layers-active", + cmd : { + title : "fix-config", + arguments : [ + "scons --pyconfig-silent" + ] + } + }, { name : "vscode settings", iconId : "compare-changes", diff --git a/src/dock.ts b/src/dock.ts index ca02b6e..30a999b 100644 --- a/src/dock.ts +++ b/src/dock.ts @@ -2,8 +2,8 @@ import path from 'path'; import * as vscode from 'vscode'; import os from 'os'; import fs from 'fs'; -import { getWorkspaceFolder, isRTThreadProject } from './api'; -import { buildGroupsTree, buildProjectTree, buildEmptyProjectTree, ProjectTreeItem, listFolderTreeItem } from './project/tree'; +import { getWorkspaceFolder, isRTThreadProject, isRTThreadWorksapce } from './api'; +import { buildGroupsTree, buildProjectTree, buildEmptyProjectTree, ProjectTreeItem, listFolderTreeItem, buildBSPTree } from './project/tree'; import { cmds } from './cmds/index'; class CmdTreeDataProvider implements vscode.TreeDataProvider { @@ -12,7 +12,13 @@ class CmdTreeDataProvider implements vscode.TreeDataProvider { } getChildren(element?: vscode.TreeItem): vscode.ProviderResult { - if (isRTThreadProject() != true) { + const isRTT = isRTThreadProject(); + const isRTTWorksapce = isRTThreadWorksapce(); + if (isRTT != true && isRTTWorksapce != true) { + console.log("not RT-Thread project or workspace, return empty tree item."); + } + + if (isRTThreadProject() != true && isRTThreadWorksapce() != true) { // only show Home command let home = new vscode.TreeItem("Home", vscode.TreeItemCollapsibleState.None); home.iconPath = new vscode.ThemeIcon("home"); @@ -114,6 +120,22 @@ class GroupsDataProvider implements vscode.TreeDataProvider { return buildEmptyProjectTree(); } } + else { + jsonPath = getWorkspaceFolder() + "/.vscode/workspace.json"; + if (fs.existsSync(jsonPath)) { + try { + const json = fs.readFileSync(jsonPath, 'utf8'); + const jsonNode = JSON.parse(json); + + if (jsonNode.hasOwnProperty("bsps")) { + return buildBSPTree(jsonNode); + } + } + catch (err) { + return buildEmptyProjectTree(); + } + } + } /* build empty project tree */ return buildEmptyProjectTree(); @@ -212,6 +234,21 @@ class ProjectFilesDataProvider implements vscode.TreeDataProvider refreshProjectFilesAndGroups()); diff --git a/src/extension.ts b/src/extension.ts index 24a97b6..aca29c0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,24 +12,55 @@ import { getMenuItems, getParallelBuildNumber } from './smart'; import { initDockView } from './dock'; import { setupVEnv } from './venv'; import { initAPI } from './api'; +import { openWorkspaceProjectsWebview } from './webviews/project'; +import { initProjectTree } from './project/tree'; let _context: vscode.ExtensionContext; +// 有两种模式 +// isRTThreadWorksapce - workspace模式,会定位.vscode/workspace.json文件是否存在,是否启用 +// isRTThread - 项目模式,rtconfig.h文件是否存在 + export async function activate(context: vscode.ExtensionContext) { let isRTThread: boolean = false; - const workspaceFolders = vscode.workspace.workspaceFolders; + let isRTThreadWorksapce: boolean = false; _context = context; + + // init context for isRTThread, isRTThreadWorksapce + vscode.commands.executeCommand('setContext', 'isRTThread', isRTThread); + context.workspaceState.update('isRTThread', isRTThread); + vscode.commands.executeCommand('setContext', 'isRTThreadWorksapce', isRTThreadWorksapce); + context.workspaceState.update('isRTThreadWorksapce', isRTThreadWorksapce); + initAPI(context); + + const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders) { const workspacePath = workspaceFolders[0].uri.fsPath; - // check rtconfig.h exists - const rtconfigPath = path.join(workspacePath, 'rtconfig.h'); - if (fs.existsSync(rtconfigPath)) { - /* The workspace is a RT-Thread Project*/ - isRTThread = true; - initAPI(context, isRTThread); - vscode.commands.executeCommand('setContext', 'isRTThread', true); + const rtthreadWorkspace = path.join(workspacePath, '.vscode', 'workspace.json'); + if (fs.existsSync(rtthreadWorkspace)) { + const json = fs.readFileSync(rtthreadWorkspace, 'utf8'); + const jsonNode = JSON.parse(json); + + if (jsonNode.hasOwnProperty("bsps")) { + isRTThreadWorksapce = true; + vscode.commands.executeCommand('setContext', 'isRTThreadWorksapce', true); + context.workspaceState.update('isRTThreadWorksapce', isRTThreadWorksapce); + } + } + else { + // check rtconfig.h exists + const rtconfigPath = path.join(workspacePath, 'rtconfig.h'); + if (fs.existsSync(rtconfigPath)) { + /* The workspace is a RT-Thread Project*/ + isRTThread = true; + vscode.commands.executeCommand('setContext', 'isRTThread', true); + context.workspaceState.update('isRTThread', isRTThread); + } + } + + if (isRTThread || isRTThreadWorksapce) { // if it's Windows system if (os.platform() === 'win32') { await setupVEnv(); @@ -42,7 +73,7 @@ export async function activate(context: vscode.ExtensionContext) { // register commands vscode.commands.registerCommand('extension.showAbout', () => { openAboutWebview(context); - }); + }); vscode.commands.registerCommand('extension.executeCommand', (arg1, arg2) => { if (arg1) { @@ -60,21 +91,35 @@ export async function activate(context: vscode.ExtensionContext) { } }) } - else { - isRTThread = false; - vscode.commands.executeCommand('setContext', 'isRTThread', false); - initAPI(context, isRTThread); - } - } - else { - initAPI(context, isRTThread); } vscode.commands.registerCommand('extension.showHome', () => { openHomeWebview(context); }); + if (isRTThreadWorksapce) { + vscode.commands.registerCommand('extension.showWorkspaceSettings', () => { + openWorkspaceProjectsWebview(context); + }); + initProjectTree(context); + } + /* initialize dock view always */ initDockView(context); + initExperimentStatusBarItem(context) +} + +function initExperimentStatusBarItem(context: vscode.ExtensionContext) { + if (false){ + const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 5); + statusItem.text = '$(beaker) 实验性功能'; + statusItem.tooltip = 'Experimental features'; + statusItem.command = 'extension.Experimental'; + statusItem.show(); + + vscode.commands.registerCommand('extension.Experimental', () => { + console.log('Experimental features are not available yet.'); + }); + } } function setupStatusBarItems(context: vscode.ExtensionContext) { diff --git a/src/project/cmd.ts b/src/project/cmd.ts new file mode 100644 index 0000000..85297d8 --- /dev/null +++ b/src/project/cmd.ts @@ -0,0 +1,56 @@ +import * as os from 'os'; +import * as fs from 'fs'; + +import { getWorkspaceFolder } from '../api'; +import { executeCommand } from '../terminal'; + +let _currentProject: string = ''; + +export function fastBuildProject(arg: any) { + if (arg) { + const cpus = os.cpus().length; + let cmd = 'scons -C ' + arg.fn + ' -j ' + cpus.toString(); + + executeCommand(cmd); + } + + return; +} + +export function configProject(arg: any) { + if (arg) { + let cmd = 'scons -C ' + arg.fn + ' --menuconfig'; + + executeCommand(cmd); + } + + return; +} + +export function openTerminalProject(arg: any) { + if (arg) { + let cmd = 'cd ' + arg.fn; + + executeCommand(cmd); + } + + return; +} + +export function setCurrentProject(arg: any) { + if (arg) { + _currentProject = arg.fn; + } + + return; +} + +export function getCurrentProject() { + let rtconfig = getWorkspaceFolder() + '/' + 'rtconfig.h'; + + if (fs.existsSync(rtconfig)) { + return getWorkspaceFolder(); + } + + return _currentProject; +} diff --git a/src/project/tree.ts b/src/project/tree.ts index 723585f..32413a7 100644 --- a/src/project/tree.ts +++ b/src/project/tree.ts @@ -1,6 +1,5 @@ import path from 'path'; import * as vscode from 'vscode'; -import os, { getPriority } from 'os'; import fs from 'fs'; import { getWorkspaceFolder, getExtensionPath } from '../api'; @@ -8,7 +7,8 @@ import { getWorkspaceFolder, getExtensionPath } from '../api'; * contexType -> contextValue as following value: * project_root, * project_group, - * project_file + * project_file, + * project_bsp */ export class ProjectTreeItem extends vscode.TreeItem { @@ -53,7 +53,17 @@ export class ProjectTreeItem extends vscode.TreeItem { arguments: [ this ] - }; + }; + } + else if (contextType == 'project_bsp') { + this.command = { + title: this.name, + command: 'extension.switchProject', + tooltip: this.name, + arguments: [ + this + ] + }; } } } @@ -78,7 +88,10 @@ export function getTreeIcon(isDir: boolean, value: string): string { if (isDir) { icon = "default_folder.svg"; } else { - if (value.endsWith(".c")) { + if (value == "project") { + icon = "chip"; + } + else if (value.endsWith(".c")) { icon = "file_type_c.svg"; } else if (value.endsWith(".cpp") || value.endsWith(".cc") || value.endsWith(".cxx")) { icon = "file_type_cpp.svg"; @@ -119,7 +132,6 @@ export function getTreeIcon(isDir: boolean, value: string): string { return icon; } - export function buildGroupsTree(node: any): ProjectTreeItem[] { const projectItems: ProjectTreeItem[] = []; let extensionPath:string = getExtensionPath() || "."; @@ -172,6 +184,13 @@ export function buildProjectTree(node: any): ProjectTreeItem[] { return projectItems; } +export function buildBSPTree(node: any) { + let workspacePath = getWorkspaceFolder() || ""; + let bspFolder = path.join(workspacePath, node['bsps'].folder); + + return listStarsTreeItem(bspFolder, node['bsps']); +} + export function buildEmptyProjectTree() { const projectItems: ProjectTreeItem[] = []; @@ -260,3 +279,61 @@ export function listFolderTreeItem(treeItem: ProjectTreeItem) { treeItem.children = children; } + +export function listStarsTreeItem(bspFolder:string, node: any) { + let children: ProjectTreeItem[] = []; + + node.stars.forEach(function (item: string) { + let parentPath = path.join(bspFolder, item); + let fPath = path.join(parentPath, "rtconfig.h"); + if (fs.existsSync(fPath)) { + let parent = children; + let value = String(item); + + let items = item.split(/[\/\\]/); + if (items.length >= 2) { + for (let i = 0; i < children.length; i++) { + if (children[i].label == items[0]) { + parent = children[i].children; + break; + } + } + + // not found, create a new parent node + if (parent === children) { + const pnode: ProjectTreeItem = new ProjectTreeItem(items[0], vscode.TreeItemCollapsibleState.Collapsed, "project_folder", path.join(bspFolder, items[0])); + + children.push(pnode); + parent = pnode.children; + } + + // rename the node name + value = items.slice(1, items.length).join(path.sep); + } + + let fn = parentPath; + let childItem = new ProjectTreeItem(value, vscode.TreeItemCollapsibleState.None, "project_bsp", fn); + childItem.iconPath = new vscode.ThemeIcon("chip"); + childItem.tooltip = fn; + parent.push(childItem); + } + }); + + return children; +} + +import {fastBuildProject, configProject, openTerminalProject, setCurrentProject} from './cmd'; +export function initProjectTree(context:vscode.ExtensionContext) { + vscode.commands.registerCommand('extension.fastBuildProject', (arg) => { + fastBuildProject(arg); + }); + vscode.commands.registerCommand('extension.configProject', (arg) => { + configProject(arg); + }); + vscode.commands.registerCommand('extension.openTerminalProject', (arg) => { + openTerminalProject(arg); + }); + vscode.commands.registerCommand('extension.switchProject', (arg) => { + setCurrentProject(arg); + }); +} diff --git a/src/terminal.ts b/src/terminal.ts index 424f5cf..21b2de5 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1,43 +1,51 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { getExtensionPath, getWorkspaceFolder } from './api'; +import { getExtensionPath, isRTThreadWorksapce } from './api'; import { Constants } from './constants'; +import { getCurrentProject } from './project/cmd'; let _terminal: vscode.Terminal | undefined; export function initTerminal() { - let terminal = vscode.window.activeTerminal; - if (terminal) { - _terminal = terminal; - // change terminal name to RT-Thread - let name = Constants.TERMINAL_LABLE; - vscode.commands.executeCommand("workbench.action.terminal.renameWithArg", { name }); - }; + let terminal = vscode.window.activeTerminal; + if (terminal) { + _terminal = terminal; + // change terminal name to RT-Thread + let name = Constants.TERMINAL_LABLE; + vscode.commands.executeCommand("workbench.action.terminal.renameWithArg", { name }); + }; } export function getTerminal(): vscode.Terminal | undefined { - if (!_terminal) { - let extensionPath = getExtensionPath(); - if (extensionPath) { - let iconPath : vscode.Uri | vscode.ThemeIcon = vscode.Uri.file(path.join(extensionPath, 'resources', "images", "rt-thread.png")); - const options: vscode.TerminalOptions = { - name: Constants.TERMINAL_LABLE, - iconPath: iconPath, - message: Constants.TERMINAL_LOGO, - }; - - _terminal = vscode.window.createTerminal(options); + if (!_terminal) { + let extensionPath = getExtensionPath(); + if (extensionPath) { + let iconPath: vscode.Uri | vscode.ThemeIcon = vscode.Uri.file(path.join(extensionPath, 'resources', "images", "rt-thread.png")); + const options: vscode.TerminalOptions = { + name: Constants.TERMINAL_LABLE, + iconPath: iconPath, + message: Constants.TERMINAL_LOGO, + }; + + _terminal = vscode.window.createTerminal(options); + } } - } - return _terminal; + return _terminal; } export function executeCommand(command: string) { let terminal = getTerminal(); if (terminal) { - terminal.show(); - terminal.sendText(command, true); + terminal.show(); + + if (command.includes('scons') && isRTThreadWorksapce()) { + if (!command.includes('-C')) { + command = command.replace('scons', 'scons -C ' + getCurrentProject() + ' '); + } + } + + terminal.sendText(command, true); } } diff --git a/src/vue/projects/App.vue b/src/vue/projects/App.vue new file mode 100644 index 0000000..269172e --- /dev/null +++ b/src/vue/projects/App.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/vue/projects/index.html b/src/vue/projects/index.html new file mode 100644 index 0000000..ef94d09 --- /dev/null +++ b/src/vue/projects/index.html @@ -0,0 +1,12 @@ + + + + + + About RT-Thread + + +
+ + + diff --git a/src/vue/projects/main.ts b/src/vue/projects/main.ts new file mode 100644 index 0000000..8e206d3 --- /dev/null +++ b/src/vue/projects/main.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' + +const app = createApp(App) +app.use(ElementPlus) +app.mount('#app') \ No newline at end of file diff --git a/src/vue/vite.config.ts b/src/vue/vite.config.ts index 4b683f4..2d8d6d2 100644 --- a/src/vue/vite.config.ts +++ b/src/vue/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ input: { about: resolve(__dirname, 'about/index.html'), home: resolve(__dirname, 'home/index.html'), + projects: resolve(__dirname, 'projects/index.html'), } }, outDir: '../../out', diff --git a/src/webviews/project.ts b/src/webviews/project.ts new file mode 100644 index 0000000..c780125 --- /dev/null +++ b/src/webviews/project.ts @@ -0,0 +1,170 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { getWorkspaceFolder } from '../api'; + +let workspaceViewPanel: vscode.WebviewPanel | null = null; +const name = "projects"; +const title = "RT-Thread Workspace"; + +interface TreeNode { + id: string, + name: string; + path: string; + children: TreeNode[]; +} + +/** + * Enumerates all subdirectories containing rtconfig.h file under the specified BSP directory. + * @param bspDir The root BSP directory to search. + * @returns An array of subdirectory paths containing rtconfig.h. + */ +async function findRtconfigDirectories(bspDir: string): Promise { + const root:TreeNode[] = []; + + async function searchDir(dir: string): Promise { + let parent:any = undefined + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name.includes('template')) + continue; // Skip directories containing 'template' + + if (entry.isDirectory()) { + const fullPath = path.join(dir, entry.name); + + if (fs.existsSync(path.join(fullPath, 'rtconfig.h'))) { + const node : TreeNode = { + id: "" + Math.random().toString(36).substring(2, 15), + name: path.basename(fullPath), + path: path.relative(bspDir, fullPath), + children: [] + }; + + let items = node.path.split(path.sep); + if (items.length >= 2) { + // rename the node name + node.name = items.slice(1, items.length).join(path.sep); + + if (parent == undefined) { + // search for the parent node in the root array + for (let i = 0; i < root.length; i++) { + if (root[i].name == items[0]) { + parent = root[i]; + break; + } + } + + // not found, create a new parent node + if (parent == undefined) { + const parent_node : TreeNode = { + id: "" + Math.random().toString(36).substring(2, 15), + name: items[0], + path: items[0], + children: [] + }; + root.push(parent_node); + + parent = parent_node; + } + + parent.children.push(node); + } + else { + parent.children.push(node); + } + } + else { + root.push(node); + } + } else { + // If rtconfig.h is not found, continue searching in subdirectories + await searchDir(fullPath); + } + } + } + } + + await searchDir(bspDir); + return root; +} + +export function openWorkspaceProjectsWebview(context: vscode.ExtensionContext) { + if (workspaceViewPanel) { + workspaceViewPanel.reveal(vscode.ViewColumn.One); + } + else { + const rootDir = path.join(context.extensionPath, 'out'); + const panel = vscode.window.createWebviewPanel('webview', title, vscode.ViewColumn.One, { + enableScripts: true, // Enable javascript in the webview + localResourceRoots: [vscode.Uri.file(rootDir)] // Only allow resources from vue view + }); + const iconPath = path.join(context.extensionPath, 'resources', 'images', 'rt-thread.png'); + panel.iconPath = vscode.Uri.file(iconPath); + + // handle close webview event + panel.onDidDispose(() => { + workspaceViewPanel = null; + }); + + // read out/${name}/index.html + const indexHtmlPath = vscode.Uri.file(context.asAbsolutePath(`out/${name}/index.html`)); + const htmlFolder = vscode.Uri.file(context.asAbsolutePath(`out`)); + const indexHtmlContent = vscode.workspace.fs.readFile(indexHtmlPath).then(buffer => buffer.toString()); + + // set html + indexHtmlContent.then(content => { + panel.webview.html = content.replace(/"[\w\-\.\/]+?\.(?:css|js)"/ig, (str) => { + const fileName = str.substr(1, str.length - 2); // remove '"' + const absPath = htmlFolder.path + '/' + fileName; + + return `"${panel.webview.asWebviewUri(vscode.Uri.file(absPath)).toString()}"`; + }); + }); + panel.webview.onDidReceiveMessage(message => { + switch (message.command) { + case 'searchBSPProjects': + let workspaceJson = path.join(getWorkspaceFolder() + '/' + '.vscode', 'workspace.json'); + if (fs.existsSync(workspaceJson)) { + let j = JSON.parse(fs.readFileSync(workspaceJson, 'utf8')); + if (j.hasOwnProperty("bsps")) { + let bsps = j["bsps"]; + if (bsps.hasOwnProperty("folder")) { + let bspFolder = getWorkspaceFolder() + '/' + bsps.folder; + + findRtconfigDirectories(bspFolder).then((dirs) => { + let stars:string[] = []; + if (bsps.hasOwnProperty("stars")) { + stars = bsps.stars; + } + + panel.webview.postMessage({command: 'updateProjects', data: {dirs: dirs, stars: stars}}); + }); + } + } + } + + break; + + case 'saveBSPProjects': + let stars = message.args[0]; + // save the stars to the workspace.json file + let workspaceFile = path.join(getWorkspaceFolder() + '/' + '.vscode', 'workspace.json'); + if (fs.existsSync(workspaceFile)) { + let j = JSON.parse(fs.readFileSync(workspaceFile, 'utf8')); + if (j.hasOwnProperty("bsps")) { + let bsps = j["bsps"]; + bsps.stars = stars; + fs.writeFileSync(workspaceFile, JSON.stringify(j, null, 4), 'utf8'); + } + } + break; + }}, + undefined + ); + + workspaceViewPanel = panel; + } + + return workspaceViewPanel; +}