From 9a76595f98de53856315ad47555162d7e6986134 Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 26 Oct 2026 11:43:28 +0800 Subject: [PATCH 1/3] Slim extension webviews and add editor browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 Kconfig、SDK 管理、工具链设置和 Symbolic Analysis 页面,聚焦 Env 管理。 新增编辑器内置浏览器 URI/命令入口,并补充相关扩展测试。 --- src/auth/authProvider.ts | 259 ++++---- src/browser.ts | 92 +++ src/cmds/index.ts | 10 - src/dock.ts | 10 - src/extension.ts | 8 +- src/test/browser.test.ts | 140 +++++ src/test/extension.test.ts | 36 +- src/utils/elf/CLAUDE.md | 1 - src/utils/elf/dwarf-parser.ts | 406 ------------- src/utils/elf/elf-parser.ts | 304 ---------- src/utils/elf/handleElf.ts | 177 ------ src/utils/elf/index.ts | 137 ----- src/utils/elf/map-parser.ts | 117 ---- src/utils/elf/test-object-symbols.js | 102 ---- src/utils/elf/types.ts | 61 -- src/vue/analyze/App.vue | 210 ------- src/vue/analyze/index.html | 21 - src/vue/analyze/index.less | 67 --- src/vue/analyze/main.ts | 7 - src/vue/setting/App.vue | 68 +-- src/vue/setting/data.ts | 85 +-- src/vue/setting/main.ts | 3 - src/vue/setting/router.ts | 66 -- src/vue/setting/view/environment/index.less | 269 +++------ src/vue/setting/view/environment/index.vue | 629 ++++++-------------- src/vue/setting/view/layout/index.less | 49 -- src/vue/setting/view/layout/index.vue | 107 ---- src/vue/setting/view/sdk/index.less | 222 ------- src/vue/setting/view/sdk/index.vue | 399 ------------- src/vue/setting/view/toolchains/index.less | 128 ---- src/vue/setting/view/toolchains/index.vue | 225 ------- src/webviews/analyze.ts | 51 -- src/webviews/create-project.ts | 8 - src/webviews/kconfigwebview.ts | 57 -- src/webviews/setting.ts | 265 +++------ src/webviews/settings_env.ts | 5 +- src/webviews/settings_sdk.ts | 458 -------------- 37 files changed, 761 insertions(+), 4498 deletions(-) create mode 100644 src/browser.ts create mode 100644 src/test/browser.test.ts delete mode 100644 src/utils/elf/CLAUDE.md delete mode 100644 src/utils/elf/dwarf-parser.ts delete mode 100644 src/utils/elf/elf-parser.ts delete mode 100644 src/utils/elf/handleElf.ts delete mode 100644 src/utils/elf/index.ts delete mode 100644 src/utils/elf/map-parser.ts delete mode 100644 src/utils/elf/test-object-symbols.js delete mode 100644 src/utils/elf/types.ts delete mode 100644 src/vue/analyze/App.vue delete mode 100644 src/vue/analyze/index.html delete mode 100644 src/vue/analyze/index.less delete mode 100644 src/vue/analyze/main.ts delete mode 100644 src/vue/setting/router.ts delete mode 100644 src/vue/setting/view/layout/index.less delete mode 100644 src/vue/setting/view/layout/index.vue delete mode 100644 src/vue/setting/view/sdk/index.less delete mode 100644 src/vue/setting/view/sdk/index.vue delete mode 100644 src/vue/setting/view/toolchains/index.less delete mode 100644 src/vue/setting/view/toolchains/index.vue delete mode 100644 src/webviews/analyze.ts delete mode 100644 src/webviews/kconfigwebview.ts delete mode 100644 src/webviews/settings_sdk.ts diff --git a/src/auth/authProvider.ts b/src/auth/authProvider.ts index 729833f..a82a81d 100644 --- a/src/auth/authProvider.ts +++ b/src/auth/authProvider.ts @@ -43,7 +43,7 @@ type StoredSession = { scopes: string[]; }; -export class RTThreadAuthProvider implements vscode.AuthenticationProvider { +export class RTThreadAuthProvider implements vscode.AuthenticationProvider, vscode.UriHandler { public static readonly id = 'rt-thread'; public static readonly label = 'RT-Thread'; @@ -209,143 +209,139 @@ export class RTThreadAuthProvider implements vscode.AuthenticationProvider { } /** - * Register URI callback handler: receive callbacks like vscode:///auth-callback?token=... + * Receive callbacks like vscode:///auth-callback?token=... */ - registerUriHandler(): vscode.Disposable { - return vscode.window.registerUriHandler({ - handleUri: async (uri: vscode.Uri) => { - if (uri.path !== '/auth-callback') { - return; - } - await this.tryCloseAuthTab(); - if (this.authOpenedExternally) { - this.authOpenedExternally = false; - vscode.window.setStatusBarMessage('$(check) 登录成功,可以关闭浏览器页面。', 5000); - } - this.log(`Received auth-callback: ${uri.toString(true)}`); - const params = new URLSearchParams(uri.query); - let token = ''; - // Username displayed in Accounts view. Prefer value from backend exchange - // if available; fall back to callback param; finally to a generic label. - let username = params.get('username') || 'user'; - let accountId = username; - const scopes = (params.get('scopes') || 'default').split(',').filter(Boolean); - const state = params.get('state') || ''; - const code = params.get('code') || ''; + async handleUri(uri: vscode.Uri): Promise { + if (uri.path !== '/auth-callback') { + return; + } + await this.tryCloseAuthTab(); + if (this.authOpenedExternally) { + this.authOpenedExternally = false; + vscode.window.setStatusBarMessage('$(check) 登录成功,可以关闭浏览器页面。', 5000); + } + this.log(`Received auth-callback: ${uri.toString(true)}`); + const params = new URLSearchParams(uri.query); + let token = ''; + // Username displayed in Accounts view. Prefer value from backend exchange + // if available; fall back to callback param; finally to a generic label. + let username = params.get('username') || 'user'; + let accountId = username; + const scopes = (params.get('scopes') || 'default').split(',').filter(Boolean); + const state = params.get('state') || ''; + const code = params.get('code') || ''; + this.log( + `Callback params: ${this.dumpParams(params)} | username=${username} | scopes=${scopes.join(',') || '(none)'} | state=${state}`, + ); + if (this.currentState) { + const matched = state && state.includes(this.currentState); + this.log(`State check: expected=${this.currentState} | got=${state} | matched=${matched}`); + } else { + this.log(`State check: no expected state recorded (possibly resumed process).`); + } + + // Prefer exchanging auth code to backend sid (stable user identity in our system). + if (code) { + try { + // Upgrade to a backend session token (sid) for stable cross-client user mapping. + const baseUrl = getAiChatBaseUrl(); + const exchangeUrl = `${baseUrl}/api/auth/rt/exchange`; + this.log(`Exchanging auth code to backend sid: ${exchangeUrl}`); + const exchangeResp = await httpPostJson(exchangeUrl, { code }, undefined, { + timeoutMs: getAiChatTimeoutMs(), + }); this.log( - `Callback params: ${this.dumpParams(params)} | username=${username} | scopes=${scopes.join(',') || '(none)'} | state=${state}`, + `Backend exchange response: status=${exchangeResp.status}, body_len=${(exchangeResp.body || '').length}`, ); - if (this.currentState) { - const matched = state && state.includes(this.currentState); - this.log(`State check: expected=${this.currentState} | got=${state} | matched=${matched}`); - } else { - this.log(`State check: no expected state recorded (possibly resumed process).`); + this.log(`Backend exchange body preview: ${this.redactBody(exchangeResp.body || '')}`); + if (exchangeResp.status < 200 || exchangeResp.status >= 300) { + throw new Error(`Backend exchange failed with status ${exchangeResp.status}.`); } - - // Prefer exchanging auth code to backend sid (stable user identity in our system). - if (code) { - try { - // Upgrade to a backend session token (sid) for stable cross-client user mapping. - const baseUrl = getAiChatBaseUrl(); - const exchangeUrl = `${baseUrl}/api/auth/rt/exchange`; - this.log(`Exchanging auth code to backend sid: ${exchangeUrl}`); - const exchangeResp = await httpPostJson(exchangeUrl, { code }, undefined, { - timeoutMs: getAiChatTimeoutMs(), - }); - this.log( - `Backend exchange response: status=${exchangeResp.status}, body_len=${(exchangeResp.body || '').length}`, - ); - this.log(`Backend exchange body preview: ${this.redactBody(exchangeResp.body || '')}`); - if (exchangeResp.status < 200 || exchangeResp.status >= 300) { - throw new Error(`Backend exchange failed with status ${exchangeResp.status}.`); - } - const exJson = JSON.parse(exchangeResp.body || '{}'); - const sid: string | undefined = typeof exJson?.sid === 'string' ? exJson.sid : undefined; - const backendUserId: string | undefined = - typeof exJson?.userId === 'string' ? exJson.userId : undefined; - const displayName: string | undefined = - typeof exJson?.displayName === 'string' ? exJson.displayName : undefined; - if (!sid) { - throw new Error('Backend exchange response missing sid.'); - } - token = sid; - if (backendUserId) { - accountId = backendUserId; - } - if (displayName && displayName.trim()) { - username = displayName.trim(); - this.log(`Resolved display name from backend: ${username}`); - } - this.log(`Obtained backend sid: ${this.mask(sid, 'sid')}`); - } catch (e) { - const baseUrl = getAiChatBaseUrl() || defaultAiChatBaseUrl; - const message = - `RT-Thread 登录成功,但后端会话交换失败。` + - `请检查网络连接/代理,或在设置中配置 smart.aiChatBaseUrl(当前:${baseUrl})`; - this.log(`Auth flow failed: ${String(e)}`); - void vscode.window - .showErrorMessage(`${message}。点击查看日志。`, '查看日志') - .then((btn) => { - if (btn) { - this.output.show(true); - } - }); - if (this.pendingAuth) { - this.pendingAuth.reject(e); - this.pendingAuth = undefined; + const exJson = JSON.parse(exchangeResp.body || '{}'); + const sid: string | undefined = typeof exJson?.sid === 'string' ? exJson.sid : undefined; + const backendUserId: string | undefined = + typeof exJson?.userId === 'string' ? exJson.userId : undefined; + const displayName: string | undefined = + typeof exJson?.displayName === 'string' ? exJson.displayName : undefined; + if (!sid) { + throw new Error('Backend exchange response missing sid.'); + } + token = sid; + if (backendUserId) { + accountId = backendUserId; + } + if (displayName && displayName.trim()) { + username = displayName.trim(); + this.log(`Resolved display name from backend: ${username}`); + } + this.log(`Obtained backend sid: ${this.mask(sid, 'sid')}`); + } catch (e) { + const baseUrl = getAiChatBaseUrl() || defaultAiChatBaseUrl; + const message = + `RT-Thread 登录成功,但后端会话交换失败。` + + `请检查网络连接/代理,或在设置中配置 smart.aiChatBaseUrl(当前:${baseUrl})`; + this.log(`Auth flow failed: ${String(e)}`); + void vscode.window + .showErrorMessage(`${message}。点击查看日志。`, '查看日志') + .then((btn) => { + if (btn) { + this.output.show(true); } - vscode.window.setStatusBarMessage('$(error) Sign-in failed: backend exchange error.', 3000); - return; - } + }); + if (this.pendingAuth) { + this.pendingAuth.reject(e); + this.pendingAuth = undefined; } + vscode.window.setStatusBarMessage('$(error) Sign-in failed: backend exchange error.', 3000); + return; + } + } - if (!token) { - const err = new Error('No token (or exchangeable code) in callback.'); - this.log( - `Sign-in failed: missing token. state=${state}, username=${username}, scopes=${scopes.join(',')}`, - ); - void vscode.window - .showErrorMessage('RT-Thread 登录失败:未获取到 token(或 code 交换失败)。点击查看日志。', '查看日志') - .then((btn) => { - if (btn) { - this.output.show(true); - } - }); - if (this.pendingAuth) { - this.pendingAuth.reject(err); - this.pendingAuth = undefined; + if (!token) { + const err = new Error('No token (or exchangeable code) in callback.'); + this.log( + `Sign-in failed: missing token. state=${state}, username=${username}, scopes=${scopes.join(',')}`, + ); + void vscode.window + .showErrorMessage('RT-Thread 登录失败:未获取到 token(或 code 交换失败)。点击查看日志。', '查看日志') + .then((btn) => { + if (btn) { + this.output.show(true); } - vscode.window.setStatusBarMessage('$(error) Sign-in failed: No token in callback.', 3000); - return; - } + }); + if (this.pendingAuth) { + this.pendingAuth.reject(err); + this.pendingAuth = undefined; + } + vscode.window.setStatusBarMessage('$(error) Sign-in failed: No token in callback.', 3000); + return; + } - const session: vscode.AuthenticationSession = { - id: `${Date.now()}`, - accessToken: token, - account: { id: accountId, label: username }, - scopes, - }; + const session: vscode.AuthenticationSession = { + id: `${Date.now()}`, + accessToken: token, + account: { id: accountId, label: username }, + scopes, + }; - // If there is a pending login promise, resolve it. - if (this.pendingAuth) { - this.log( - `Resolving pending login for ${username}, scopes=${scopes.join(',')}. token=${this.mask(token, 'token')}`, - ); - this.pendingAuth.resolve(session); - this.pendingAuth = undefined; - this.currentState = undefined; - return; - } + // If there is a pending login promise, resolve it. + if (this.pendingAuth) { + this.log( + `Resolving pending login for ${username}, scopes=${scopes.join(',')}. token=${this.mask(token, 'token')}`, + ); + this.pendingAuth.resolve(session); + this.pendingAuth = undefined; + this.currentState = undefined; + return; + } - // Fallback: no pending login (e.g., user initiated from Accounts view - // or VS Code reactivated the extension). Persist the session and - // notify VS Code so the Accounts menu updates. - this.log(`No pending login. Persisting session for ${username}, scopes=${scopes.join(',')}.`); - await this.context.secrets.store(this.secretKey, JSON.stringify(this.fromVsCodeSession(session))); - this._onDidChangeSessions.fire({ added: [session], removed: [], changed: [] }); - vscode.window.setStatusBarMessage('$(check) RT-Thread: Signed in', 3000); - }, - }); + // Fallback: no pending login (e.g., user initiated from Accounts view + // or VS Code reactivated the extension). Persist the session and + // notify VS Code so the Accounts menu updates. + this.log(`No pending login. Persisting session for ${username}, scopes=${scopes.join(',')}.`); + await this.context.secrets.store(this.secretKey, JSON.stringify(this.fromVsCodeSession(session))); + this._onDidChangeSessions.fire({ added: [session], removed: [], changed: [] }); + vscode.window.setStatusBarMessage('$(check) RT-Thread: Signed in', 3000); } /** @@ -511,8 +507,8 @@ async function httpPostJson( }); } -// Provide initialization entry point, register authentication provider, URI handler, commands uniformly here -export function initRTThreadAuth(context: vscode.ExtensionContext): void { +// Register the authentication provider and commands; URI routing is owned by the extension. +export function initRTThreadAuth(context: vscode.ExtensionContext): RTThreadAuthProvider { const provider = new RTThreadAuthProvider(context); context.subscriptions.push(provider); @@ -524,9 +520,6 @@ export function initRTThreadAuth(context: vscode.ExtensionContext): void { ); context.subscriptions.push(authProviderDisposable); - const uriHandlerDisposable = provider.registerUriHandler(); - context.subscriptions.push(uriHandlerDisposable); - context.subscriptions.push( vscode.commands.registerCommand('extension.RTThreadSignIn', async () => { try { @@ -546,4 +539,6 @@ export function initRTThreadAuth(context: vscode.ExtensionContext): void { } }), ); + + return provider; } diff --git a/src/browser.ts b/src/browser.ts new file mode 100644 index 0000000..8414744 --- /dev/null +++ b/src/browser.ts @@ -0,0 +1,92 @@ +import * as vscode from 'vscode'; + +interface BrowserCommands { + getCommands(filterInternal?: boolean): Thenable; + executeCommand(command: string, url: string): Thenable; +} + +export function validateWebUrl(value: string): string { + if (typeof value !== 'string') { + throw new Error('请输入有效的 HTTP 或 HTTPS 地址。'); + } + const input = value.trim(); + if (!/^https?:\/\//i.test(input) || /[\s\u0000-\u001f\u007f]/.test(input)) { + throw new Error('请输入完整的 HTTP 或 HTTPS 地址。'); + } + + let url: URL; + try { + url = new URL(input); + } catch { + throw new Error('网页地址无效。'); + } + if (!url.hostname || url.username || url.password) { + throw new Error('网页地址必须包含主机名,且不能包含用户名或密码。'); + } + return url.toString(); +} + +export async function openWebUrl(value: string, commands: BrowserCommands = vscode.commands): Promise { + const url = validateWebUrl(value); + const availableCommands = await commands.getCommands(true); + const command = availableCommands.includes('workbench.action.browser.open') + ? 'workbench.action.browser.open' + : 'simpleBrowser.show'; + await commands.executeCommand(command, url); +} + +export class ExtensionUriHandler implements vscode.UriHandler { + constructor( + private readonly authHandler: vscode.UriHandler, + private readonly openBrowser: (url: string) => Promise = openWebUrl, + ) { } + + async handleUri(uri: vscode.Uri): Promise { + if (uri.path !== '/open-web') { + await this.authHandler.handleUri(uri); + return; + } + if (!uri.query.startsWith('url=')) { + throw new Error('网页 URI 必须包含 url 参数。'); + } + // VS Code already decodes the query; parsing it again would split the target URL. + await this.openBrowser(validateWebUrl(uri.query.slice('url='.length))); + } +} + +async function reportErrors(action: () => Promise): Promise { + try { + await action(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await vscode.window.showErrorMessage(`RT-Thread: ${message}`); + } +} + +export function registerWebBrowser(context: vscode.ExtensionContext, authHandler: vscode.UriHandler): void { + const uriHandler = new ExtensionUriHandler(authHandler); + context.subscriptions.push( + vscode.window.registerUriHandler({ + handleUri: uri => reportErrors(() => uriHandler.handleUri(uri)), + }), + vscode.commands.registerCommand('extension.openWebUrl', (value?: string) => reportErrors(async () => { + const url = value ?? await vscode.window.showInputBox({ + title: '在编辑器中打开网页', + prompt: 'HTTP 或 HTTPS 地址', + placeHolder: 'https://www.rt-thread.org', + ignoreFocusOut: true, + validateInput: input => { + try { + validateWebUrl(input); + return undefined; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } + }, + }); + if (url !== undefined) { + await openWebUrl(url); + } + })), + ); +} diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 5084f92..4804f04 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -70,16 +70,6 @@ export let cmds: Object = { "scons --target=vsc -n -s" ] } - }, - { - name : "sdk settings", - iconId : "settings", - cmd : { - title : "sdk-setting", - arguments : [ - "sdk" - ] - } } ], name: 'Settings', diff --git a/src/dock.ts b/src/dock.ts index 7836350..eca1994 100644 --- a/src/dock.ts +++ b/src/dock.ts @@ -101,16 +101,6 @@ class CmdTreeDataProvider implements vscode.TreeDataProvider { }; children.push(chat); - let analyze = new vscode.TreeItem("Symbolic Analysis", vscode.TreeItemCollapsibleState.None); - analyze.iconPath = new vscode.ThemeIcon("search-fuzzy"); - analyze.label = "Symbolic Analysis"; - analyze.command = { - command: "extension.showAnalyze", - title: "show symbolic analysis page", - arguments: [], - }; - children.push(analyze); - let about = new vscode.TreeItem("About", vscode.TreeItemCollapsibleState.None); about.iconPath = new vscode.ThemeIcon("info"); about.label = "About"; diff --git a/src/extension.ts b/src/extension.ts index f344f0b..dea54b9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,6 @@ import * as os from 'os'; import { openSettingWebview } from './webviews/setting'; import { openAboutWebview } from './webviews/about'; import { openCreateProjectWebview } from './webviews/create-project'; -import { openAnalyzeWebview } from './webviews/analyze'; import { openChatWebview } from './webviews/chat'; import { initOnDidChangeListener } from './listener'; import { executeCommand, initTerminal } from './terminal'; @@ -21,6 +20,7 @@ import { DecorationProvider } from './project/fileDecorationProvider'; import { getCurrentProjectInWorkspace } from './webviews/project'; import { initCurrentProject } from './project/cmd'; import { initRTThreadAuth } from './auth/authProvider'; +import { registerWebBrowser } from './browser'; let _context: vscode.ExtensionContext; @@ -128,9 +128,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('extension.showAbout', () => { openAboutWebview(context); }); - vscode.commands.registerCommand('extension.showAnalyze', () => { - openAnalyzeWebview(context); - }); vscode.commands.registerCommand('extension.showChat', () => { openChatWebview(context); }); @@ -143,7 +140,8 @@ export async function activate(context: vscode.ExtensionContext) { } /* initialize RT-Thread authentication */ - initRTThreadAuth(context); + const authProvider = initRTThreadAuth(context); + registerWebBrowser(context, authProvider); /* initialize dock view always */ initDockView(context); diff --git a/src/test/browser.test.ts b/src/test/browser.test.ts new file mode 100644 index 0000000..db9f88a --- /dev/null +++ b/src/test/browser.test.ts @@ -0,0 +1,140 @@ +import * as assert from 'assert'; +import * as http from 'http'; +import * as vscode from 'vscode'; +import { ExtensionUriHandler, openWebUrl, validateWebUrl } from '../browser'; + +suite('Web Browser', () => { + test('Accepts HTTP, HTTPS, localhost and IPv6 addresses', () => { + for (const url of ['https://example.com/', 'http://localhost:8080/', 'http://[::1]:8080/']) { + assert.strictEqual(validateWebUrl(url), url); + } + assert.strictEqual(validateWebUrl(' HTTPS://example.com '), 'https://example.com/'); + }); + + test('Rejects unsupported protocols, incomplete URLs and credentials', () => { + for (const url of [ + '', 'example.com', 'https:example.com', 'https://', 'https://example.com:invalid', + 'file:///tmp/test', 'javascript:alert(1)', 'command:extension.showSetting', + 'vscode://rt-thread.rt-thread-smart/auth-callback', + 'https://user:password@example.com/', 'https://example.com/\npath', + ]) { + assert.throws(() => validateWebUrl(url), Error, url); + } + assert.throws(() => validateWebUrl(123 as unknown as string)); + }); + + test('Prefers the integrated browser when available', async () => { + const calls: unknown[][] = []; + await openWebUrl('https://example.com/', { + getCommands: async () => ['simpleBrowser.show', 'workbench.action.browser.open'], + executeCommand: async (...args: unknown[]): Promise => { + calls.push(args); + return undefined; + }, + }); + assert.deepStrictEqual(calls, [['workbench.action.browser.open', 'https://example.com/']]); + }); + + test('Uses Simple Browser on older VS Code versions', async () => { + const calls: unknown[][] = []; + await openWebUrl('http://localhost:8080/', { + getCommands: async () => [], + executeCommand: async (...args: unknown[]): Promise => { + calls.push(args); + return undefined; + }, + }); + assert.deepStrictEqual(calls, [['simpleBrowser.show', 'http://localhost:8080/']]); + }); + + test('Validates URLs before executing commands', async () => { + let called = false; + await assert.rejects(openWebUrl('command:unsafe', { + getCommands: async () => { called = true; return []; }, + executeCommand: async () => { called = true; return undefined; }, + })); + assert.strictEqual(called, false); + }); + + test('Propagates browser failures without opening an external browser', async () => { + const error = new Error('Browser unavailable'); + const calls: string[] = []; + await assert.rejects(openWebUrl('https://example.com/', { + getCommands: async () => [], + executeCommand: async command => { calls.push(command); throw error; }, + }), error); + assert.deepStrictEqual(calls, ['simpleBrowser.show']); + }); + + test('Preserves encoded target queries, fragments, Unicode and percent escapes', async () => { + const opened: string[] = []; + const handler = new ExtensionUriHandler({ handleUri: () => assert.fail('Unexpected auth URI') }, async url => { + opened.push(url); + }); + const urls = [ + 'https://example.com/docs?a=1&b=2#intro', + 'http://localhost:3000/?q=a+b&path=%2Ffoo&literal=%2520', + 'https://example.com/?url=https%3A%2F%2Fother.com&next=1', + 'https://example.com/文档?q=更新#说明', + ]; + for (const url of urls) { + await handler.handleUri(vscode.Uri.parse(`vscode://rt-thread.rt-thread-smart/open-web?url=${encodeURIComponent(url)}`)); + } + assert.deepStrictEqual(opened, urls.map(url => new URL(url).toString())); + }); + + test('Rejects missing or unsafe URI targets', async () => { + const handler = new ExtensionUriHandler({ handleUri: () => assert.fail('Unexpected auth URI') }, async () => { + assert.fail('Invalid URI opened a browser'); + }); + for (const query of ['', 'url=', 'other=https://example.com', 'url=javascript:alert(1)', 'url=file:///tmp/test']) { + await assert.rejects(handler.handleUri(vscode.Uri.parse(`vscode://rt-thread.rt-thread-smart/open-web?${query}`))); + } + }); + + test('Forwards login callbacks unchanged to the authentication provider', async () => { + let received: vscode.Uri | undefined; + const uri = vscode.Uri.parse('vscode://rt-thread.rt-thread-smart/auth-callback?code=abc&state=xyz'); + const handler = new ExtensionUriHandler({ handleUri: value => { received = value; } }, async () => { + assert.fail('Login callback opened a browser'); + }); + await handler.handleUri(uri); + assert.strictEqual(received, uri); + }); + + test('Opens a local page in the built-in browser through the extension command', async () => { + const extension = vscode.extensions.getExtension('rt-thread.rt-thread-smart'); + assert.ok(extension); + await extension.activate(); + const existingTabs = new Set(vscode.window.tabGroups.all.flatMap(group => group.tabs)); + let receivedUrl: string | undefined; + const server = http.createServer((request, response) => { + if (request.url?.startsWith('/?')) { + receivedUrl = request.url; + } + response.writeHead(200, { 'Content-Type': 'text/html' }); + response.end('RT-Thread Browser TestBrowser test'); + }); + try { + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const address = server.address(); + assert.ok(address && typeof address !== 'string'); + await vscode.commands.executeCommand('extension.openWebUrl', `http://127.0.0.1:${address.port}/?a=1&b=2#intro`); + const deadline = Date.now() + 10000; + while (!receivedUrl && Date.now() < deadline) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + assert.ok(receivedUrl); + const received = new URL(receivedUrl, 'http://127.0.0.1'); + assert.strictEqual(received.pathname, '/'); + assert.strictEqual(received.searchParams.get('a'), '1'); + assert.strictEqual(received.searchParams.get('b'), '2'); + assert.ok(vscode.window.tabGroups.all.some(group => group.tabs.some(tab => !existingTabs.has(tab)))); + } finally { + const newTabs = vscode.window.tabGroups.all.flatMap(group => group.tabs).filter(tab => !existingTabs.has(tab)); + await vscode.window.tabGroups.close(newTabs); + server.closeAllConnections(); + await new Promise(resolve => server.close(() => resolve())); + } + }); +}); diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 4ca0ab4..bb70507 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,15 +1,33 @@ import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it +import * as fs from 'fs'; +import * as path from 'path'; import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); + test('Declares URI activation and the browser command', () => { + const root = path.resolve(__dirname, '../..'); + const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + assert.ok(manifest.activationEvents.includes('onUri')); + assert.ok(manifest.contributes.commands.some((item: { command: string }) => item.command === 'extension.openWebUrl')); + assert.strictEqual(manifest.dependencies['vue-router'], undefined); + assert.strictEqual(manifest.scripts['package:vs'].includes('--no-dependencies'), false); + assert.strictEqual(fs.readFileSync(path.join(root, 'src/vue/components.d.ts'), 'utf8').includes('vue-router'), false); + }); + + test('Does not build or ship removed feature modules', () => { + const out = path.resolve(__dirname, '..'); + for (const file of ['analyze', 'utils/elf', 'webviews/analyze.js', 'webviews/settings_sdk.js', 'webviews/kconfigwebview.js']) { + assert.strictEqual(fs.existsSync(path.join(out, file)), false, file); + } + }); - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); + test('Activates outside RT-Thread projects with a browser command and no analysis command', async () => { + const extension = vscode.extensions.getExtension('rt-thread.rt-thread-smart'); + assert.ok(extension); + await extension.activate(); + const commands = await vscode.commands.getCommands(true); + assert.ok(commands.includes('extension.openWebUrl')); + assert.ok(commands.includes('extension.showSetting')); + assert.strictEqual(commands.includes('extension.showAnalyze'), false); + }); }); diff --git a/src/utils/elf/CLAUDE.md b/src/utils/elf/CLAUDE.md deleted file mode 100644 index 2ff91a6..0000000 --- a/src/utils/elf/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -- 这是一个elf符号处理的库,使用typescript方式编写,分析rtthread.elf,rtthread.map文件,并返回相关符号信息。 \ No newline at end of file diff --git a/src/utils/elf/dwarf-parser.ts b/src/utils/elf/dwarf-parser.ts deleted file mode 100644 index b58a00c..0000000 --- a/src/utils/elf/dwarf-parser.ts +++ /dev/null @@ -1,406 +0,0 @@ -import * as fs from 'fs'; - -interface DwarfLineInfo { - address: number; - file: string; - line: number; - column: number; -} - -interface DwarfCompilationUnit { - offset: number; - length: number; - version: number; - abbrevOffset: number; - addressSize: number; -} - -export class DwarfParser { - private buffer: Buffer; - private debugLineSection?: Buffer; - private debugInfoSection?: Buffer; - private debugStrSection?: Buffer; - private debugAbbrevSection?: Buffer; - private lineInfoMap: Map = new Map(); - private fileNames: string[] = []; - - constructor(buffer: Buffer) { - this.buffer = buffer; - } - - public setDebugSections(sections: { - debugLine?: Buffer; - debugInfo?: Buffer; - debugStr?: Buffer; - debugAbbrev?: Buffer; - }) { - this.debugLineSection = sections.debugLine; - this.debugInfoSection = sections.debugInfo; - this.debugStrSection = sections.debugStr; - this.debugAbbrevSection = sections.debugAbbrev; - - if (this.debugLineSection) { - this.parseDebugLine(); - } - } - - private parseDebugLine(): void { - if (!this.debugLineSection) { - return; - } - - let offset = 0; - const section = this.debugLineSection; - - while (offset < section.length) { - const unitStart = offset; - - // Read unit length (4 bytes for 32-bit, 12 bytes for 64-bit DWARF) - let unitLength = section.readUInt32LE(offset); - offset += 4; - - let is64Bit = false; - if (unitLength === 0xffffffff) { - // 64-bit DWARF format - is64Bit = true; - unitLength = Number(section.readBigUInt64LE(offset)); - offset += 8; - } - - if (unitLength === 0 || offset + unitLength > section.length) { - break; - } - - const unitEnd = offset + unitLength; - - // Read DWARF version - const version = section.readUInt16LE(offset); - offset += 2; - - if (version < 2 || version > 5) { - offset = unitEnd; - continue; - } - - // Read header length - let headerLength: number; - if (is64Bit) { - headerLength = Number(section.readBigUInt64LE(offset)); - offset += 8; - } else { - headerLength = section.readUInt32LE(offset); - offset += 4; - } - - const headerStart = offset; - - // Read minimum instruction length - const minInstrLength = section.readUInt8(offset); - offset += 1; - - // Maximum operations per instruction (DWARF 4+) - let maxOpsPerInstr = 1; - if (version >= 4) { - maxOpsPerInstr = section.readUInt8(offset); - offset += 1; - } - - // Default is_stmt - const defaultIsStmt = section.readUInt8(offset); - offset += 1; - - // Line base - const lineBase = section.readInt8(offset); - offset += 1; - - // Line range - const lineRange = section.readUInt8(offset); - offset += 1; - - // Opcode base - const opcodeBase = section.readUInt8(offset); - offset += 1; - - // Standard opcode lengths - const opcodeLengths: number[] = []; - for (let i = 1; i < opcodeBase; i++) { - opcodeLengths[i] = section.readUInt8(offset); - offset += 1; - } - - // Read directory table - const directories: string[] = ['']; - if (version <= 4) { - while (offset < section.length && section[offset] !== 0) { - const dir = this.readNullTerminatedString(section, offset); - directories.push(dir.str); - offset = dir.offset; - } - offset++; // Skip null terminator - } - - // Read file name table - this.fileNames = ['']; - if (version <= 4) { - while (offset < section.length && section[offset] !== 0) { - const fileName = this.readNullTerminatedString(section, offset); - offset = fileName.offset; - - const dirIndex = this.readULEB128(section, offset); - offset = dirIndex.offset; - - const modTime = this.readULEB128(section, offset); - offset = modTime.offset; - - const fileSize = this.readULEB128(section, offset); - offset = fileSize.offset; - - const fullPath = dirIndex.value > 0 && dirIndex.value < directories.length - ? `${directories[dirIndex.value]}/${fileName.str}` - : fileName.str; - - this.fileNames.push(fullPath); - } - offset++; // Skip null terminator - } - - // Skip to start of line number program - offset = headerStart + headerLength; - - // Parse line number program - this.parseLineNumberProgram(section, offset, unitEnd, minInstrLength, lineBase, lineRange, opcodeBase, opcodeLengths); - - offset = unitEnd; - } - } - - private parseLineNumberProgram( - section: Buffer, - startOffset: number, - endOffset: number, - minInstrLength: number, - lineBase: number, - lineRange: number, - opcodeBase: number, - opcodeLengths: number[] - ): void { - let offset = startOffset; - - // Line number program state machine registers - let address = 0; - let fileIndex = 1; - let line = 1; - let column = 0; - let isStmt = false; - let basicBlock = false; - let endSequence = false; - - while (offset < endOffset) { - const opcode = section.readUInt8(offset); - offset += 1; - - if (opcode === 0) { - // Extended opcode - const length = this.readULEB128(section, offset); - offset = length.offset; - - const extOpcode = section.readUInt8(offset); - offset += 1; - - switch (extOpcode) { - case 1: // DW_LNE_end_sequence - endSequence = true; - if (fileIndex > 0 && fileIndex < this.fileNames.length) { - this.lineInfoMap.set(address, { - address, - file: this.fileNames[fileIndex], - line, - column - }); - } - // Reset state machine - address = 0; - fileIndex = 1; - line = 1; - column = 0; - isStmt = false; - basicBlock = false; - endSequence = false; - break; - - case 2: // DW_LNE_set_address - address = section.readUInt32LE(offset); - offset += 4; - break; - - case 3: // DW_LNE_define_file - // Skip for now - offset += length.value - 1; - break; - - default: - // Unknown extended opcode, skip - offset += length.value - 1; - break; - } - } else if (opcode < opcodeBase) { - // Standard opcode - switch (opcode) { - case 1: // DW_LNS_copy - if (fileIndex > 0 && fileIndex < this.fileNames.length) { - this.lineInfoMap.set(address, { - address, - file: this.fileNames[fileIndex], - line, - column - }); - } - basicBlock = false; - break; - - case 2: // DW_LNS_advance_pc - const pcAdvance = this.readULEB128(section, offset); - offset = pcAdvance.offset; - address += pcAdvance.value * minInstrLength; - break; - - case 3: // DW_LNS_advance_line - const lineAdvance = this.readSLEB128(section, offset); - offset = lineAdvance.offset; - line += lineAdvance.value; - break; - - case 4: // DW_LNS_set_file - const file = this.readULEB128(section, offset); - offset = file.offset; - fileIndex = file.value; - break; - - case 5: // DW_LNS_set_column - const col = this.readULEB128(section, offset); - offset = col.offset; - column = col.value; - break; - - case 6: // DW_LNS_negate_stmt - isStmt = !isStmt; - break; - - case 7: // DW_LNS_set_basic_block - basicBlock = true; - break; - - case 8: // DW_LNS_const_add_pc - const adjustedOpcode = 255 - opcodeBase; - const addrIncrement = Math.floor(adjustedOpcode / lineRange) * minInstrLength; - address += addrIncrement; - break; - - case 9: // DW_LNS_fixed_advance_pc - const fixedAdvance = section.readUInt16LE(offset); - offset += 2; - address += fixedAdvance; - break; - - default: - // Skip operands for unknown standard opcodes - if (opcode < opcodeLengths.length) { - offset += opcodeLengths[opcode]; - } - break; - } - } else { - // Special opcode - const adjustedOpcode = opcode - opcodeBase; - const addrIncrement = Math.floor(adjustedOpcode / lineRange) * minInstrLength; - const lineIncrement = lineBase + (adjustedOpcode % lineRange); - - address += addrIncrement; - line += lineIncrement; - - if (fileIndex > 0 && fileIndex < this.fileNames.length) { - this.lineInfoMap.set(address, { - address, - file: this.fileNames[fileIndex], - line, - column - }); - } - - basicBlock = false; - } - } - } - - private readNullTerminatedString(buffer: Buffer, offset: number): { str: string; offset: number } { - const start = offset; - while (offset < buffer.length && buffer[offset] !== 0) { - offset++; - } - const str = buffer.toString('utf8', start, offset); - return { str, offset: offset + 1 }; - } - - private readULEB128(buffer: Buffer, offset: number): { value: number; offset: number } { - let value = 0; - let shift = 0; - let byte: number; - - do { - if (offset >= buffer.length) { - throw new Error('Buffer overflow reading ULEB128'); - } - byte = buffer[offset++]; - value |= (byte & 0x7f) << shift; - shift += 7; - } while (byte & 0x80); - - return { value, offset }; - } - - private readSLEB128(buffer: Buffer, offset: number): { value: number; offset: number } { - let value = 0; - let shift = 0; - let byte: number; - - do { - if (offset >= buffer.length) { - throw new Error('Buffer overflow reading SLEB128'); - } - byte = buffer[offset++]; - value |= (byte & 0x7f) << shift; - shift += 7; - } while (byte & 0x80); - - // Sign extend - if (shift < 32 && (byte & 0x40)) { - value |= -(1 << shift); - } - - return { value, offset }; - } - - public getLineInfo(address: number): DwarfLineInfo | null { - // Try exact match first - if (this.lineInfoMap.has(address)) { - return this.lineInfoMap.get(address) || null; - } - - // Find closest address that is less than or equal to the given address - let closestInfo: DwarfLineInfo | null = null; - let closestAddress = -1; - - for (const [addr, info] of this.lineInfoMap) { - if (addr <= address && addr > closestAddress) { - closestAddress = addr; - closestInfo = info; - } - } - - return closestInfo; - } - - public getAllLineInfo(): Map { - return this.lineInfoMap; - } -} diff --git a/src/utils/elf/elf-parser.ts b/src/utils/elf/elf-parser.ts deleted file mode 100644 index 9b6c3f1..0000000 --- a/src/utils/elf/elf-parser.ts +++ /dev/null @@ -1,304 +0,0 @@ -import * as fs from 'fs'; -import { ElfHeader, SectionHeader, SymbolEntry, Section, Symbol } from './types'; -import { DwarfParser } from './dwarf-parser'; - -export class ElfParser { - private buffer: Buffer; - private elfHeader!: ElfHeader; - private sectionHeaders: SectionHeader[] = []; - private stringTable!: Buffer; - private symbolTable!: Buffer; - private symbolStringTable!: Buffer; - private is64Bit: boolean = false; - private symbols: Symbol[] = []; - private dwarfParser: DwarfParser; - - constructor(filePath: string) { - this.buffer = fs.readFileSync(filePath); - this.dwarfParser = new DwarfParser(this.buffer); - this.parseElfHeader(); - this.parseSectionHeaders(); - this.loadStringTables(); - this.loadDebugSections(); - } - - private parseElfHeader(): void { - const magic = this.buffer.subarray(0, 4); - if (magic.toString('hex') !== '7f454c46') { - throw new Error('Not a valid ELF file'); - } - - this.is64Bit = this.buffer[4] === 2; - const isLittleEndian = this.buffer[5] === 1; - - if (this.is64Bit) { - this.elfHeader = { - magic, - class: this.buffer[4], - data: this.buffer[5], - version: this.buffer[6], - osabi: this.buffer[7], - abiversion: this.buffer[8], - type: this.buffer.readUInt16LE(16), - machine: this.buffer.readUInt16LE(18), - entry: this.buffer.readBigUInt64LE(24), - phoff: this.buffer.readBigUInt64LE(32), - shoff: this.buffer.readBigUInt64LE(40), - flags: this.buffer.readUInt32LE(48), - ehsize: this.buffer.readUInt16LE(52), - phentsize: this.buffer.readUInt16LE(54), - phnum: this.buffer.readUInt16LE(56), - shentsize: this.buffer.readUInt16LE(58), - shnum: this.buffer.readUInt16LE(60), - shstrndx: this.buffer.readUInt16LE(62) - }; - } else { - this.elfHeader = { - magic, - class: this.buffer[4], - data: this.buffer[5], - version: this.buffer[6], - osabi: this.buffer[7], - abiversion: this.buffer[8], - type: this.buffer.readUInt16LE(16), - machine: this.buffer.readUInt16LE(18), - entry: BigInt(this.buffer.readUInt32LE(24)), - phoff: BigInt(this.buffer.readUInt32LE(28)), - shoff: BigInt(this.buffer.readUInt32LE(32)), - flags: this.buffer.readUInt32LE(36), - ehsize: this.buffer.readUInt16LE(40), - phentsize: this.buffer.readUInt16LE(42), - phnum: this.buffer.readUInt16LE(44), - shentsize: this.buffer.readUInt16LE(46), - shnum: this.buffer.readUInt16LE(48), - shstrndx: this.buffer.readUInt16LE(50) - }; - } - } - - private parseSectionHeaders(): void { - const shoff = Number(this.elfHeader.shoff); - const shentsize = this.elfHeader.shentsize; - const shnum = this.elfHeader.shnum; - - for (let i = 0; i < shnum; i++) { - const offset = shoff + i * shentsize; - - if (this.is64Bit) { - this.sectionHeaders.push({ - name: this.buffer.readUInt32LE(offset), - type: this.buffer.readUInt32LE(offset + 4), - flags: this.buffer.readBigUInt64LE(offset + 8), - addr: this.buffer.readBigUInt64LE(offset + 16), - offset: this.buffer.readBigUInt64LE(offset + 24), - size: this.buffer.readBigUInt64LE(offset + 32), - link: this.buffer.readUInt32LE(offset + 40), - info: this.buffer.readUInt32LE(offset + 44), - addralign: this.buffer.readBigUInt64LE(offset + 48), - entsize: this.buffer.readBigUInt64LE(offset + 56) - }); - } else { - this.sectionHeaders.push({ - name: this.buffer.readUInt32LE(offset), - type: this.buffer.readUInt32LE(offset + 4), - flags: BigInt(this.buffer.readUInt32LE(offset + 8)), - addr: BigInt(this.buffer.readUInt32LE(offset + 12)), - offset: BigInt(this.buffer.readUInt32LE(offset + 16)), - size: BigInt(this.buffer.readUInt32LE(offset + 20)), - link: this.buffer.readUInt32LE(offset + 24), - info: this.buffer.readUInt32LE(offset + 28), - addralign: BigInt(this.buffer.readUInt32LE(offset + 32)), - entsize: BigInt(this.buffer.readUInt32LE(offset + 36)) - }); - } - } - } - - private loadStringTables(): void { - // Load section header string table - const shstrndx = this.elfHeader.shstrndx; - if (shstrndx < this.sectionHeaders.length) { - const strTabHeader = this.sectionHeaders[shstrndx]; - const offset = Number(strTabHeader.offset); - const size = Number(strTabHeader.size); - this.stringTable = this.buffer.subarray(offset, offset + size); - } - - // Find and load symbol table and its string table - for (let i = 0; i < this.sectionHeaders.length; i++) { - const header = this.sectionHeaders[i]; - if (header.type === 2 || header.type === 11) { // SHT_SYMTAB or SHT_DYNSYM - const offset = Number(header.offset); - const size = Number(header.size); - this.symbolTable = this.buffer.subarray(offset, offset + size); - - // Load associated string table - const strTabIndex = header.link; - if (strTabIndex < this.sectionHeaders.length) { - const strTabHeader = this.sectionHeaders[strTabIndex]; - const strOffset = Number(strTabHeader.offset); - const strSize = Number(strTabHeader.size); - this.symbolStringTable = this.buffer.subarray(strOffset, strOffset + strSize); - } - break; - } - } - } - - private getString(buffer: Buffer, offset: number): string { - const end = buffer.indexOf(0, offset); - return buffer.toString('utf8', offset, end); - } - - private getSectionName(index: number): string { - if (!this.stringTable) {return '';} - return this.getString(this.stringTable, index); - } - - public getSections(): Section[] { - const sections: Section[] = []; - - for (const header of this.sectionHeaders) { - const name = this.getSectionName(header.name); - if (name) { - sections.push({ - name, - address: Number(header.addr), - size: Number(header.size), - needsLoad: (header.flags & 0x2n) !== 0n // SHF_ALLOC flag - }); - } - } - - return sections; - } - - public getAllSymbols(): Symbol[] { - if(this.symbols.length > 0) { - return this.symbols; - } - if (!this.symbolTable || !this.symbolStringTable) { - return []; - } - - const symbols: Symbol[] = []; - const entrySize = this.is64Bit ? 24 : 16; - const numSymbols = this.symbolTable.length / entrySize; - - for (let i = 0; i < numSymbols; i++) { - const offset = i * entrySize; - let entry: SymbolEntry; - - if (this.is64Bit) { - entry = { - name: this.symbolTable.readUInt32LE(offset), - info: this.symbolTable.readUInt8(offset + 4), - other: this.symbolTable.readUInt8(offset + 5), - shndx: this.symbolTable.readUInt16LE(offset + 6), - value: this.symbolTable.readBigUInt64LE(offset + 8), - size: this.symbolTable.readBigUInt64LE(offset + 16) - }; - } else { - entry = { - name: this.symbolTable.readUInt32LE(offset), - value: BigInt(this.symbolTable.readUInt32LE(offset + 4)), - size: BigInt(this.symbolTable.readUInt32LE(offset + 8)), - info: this.symbolTable.readUInt8(offset + 12), - other: this.symbolTable.readUInt8(offset + 13), - shndx: this.symbolTable.readUInt16LE(offset + 14) - }; - } - - const size = Number(entry.size); - if (size === 0) {continue;} // Skip symbols with zero size - - const name = this.getString(this.symbolStringTable, entry.name); - if (!name) {continue;} - - const type = this.getSymbolType(entry.info); - const sectionName = this.getSectionNameByIndex(entry.shndx); - - const address = Number(entry.value); - symbols.push({ - name, - type, - address, - hexaddr: '0x' + address.toString(16), - size, - section: sectionName - }); - } - - - this.symbols = symbols.sort((a, b) => b.size - a.size); - return this.symbols; - } - - public getSymbolsBySection(sectionName: string): Symbol[] { - const allSymbols = this.getAllSymbols(); - return allSymbols - .filter(sym => sym.section === sectionName) - .sort((a, b) => b.size - a.size); - } - - private getSymbolType(info: number): string { - const type = info & 0xf; - const typeNames = [ - 'NOTYPE', 'OBJECT', 'FUNC', 'SECTION', - 'FILE', 'COMMON', 'TLS', 'NUM' - ]; - return typeNames[type] || 'UNKNOWN'; - } - - private getSectionNameByIndex(index: number): string { - if (index === 0 || index >= this.sectionHeaders.length) { - return ''; - } - return this.getSectionName(this.sectionHeaders[index].name); - } - - private loadDebugSections(): void { - const debugSections: { - debugLine?: Buffer; - debugInfo?: Buffer; - debugStr?: Buffer; - debugAbbrev?: Buffer; - } = {}; - - for (let i = 0; i < this.sectionHeaders.length; i++) { - const header = this.sectionHeaders[i]; - const sectionName = this.getSectionName(header.name); - - if (sectionName === '.debug_line') { - const offset = Number(header.offset); - const size = Number(header.size); - debugSections.debugLine = this.buffer.subarray(offset, offset + size); - } else if (sectionName === '.debug_info') { - const offset = Number(header.offset); - const size = Number(header.size); - debugSections.debugInfo = this.buffer.subarray(offset, offset + size); - } else if (sectionName === '.debug_str') { - const offset = Number(header.offset); - const size = Number(header.size); - debugSections.debugStr = this.buffer.subarray(offset, offset + size); - } else if (sectionName === '.debug_abbrev') { - const offset = Number(header.offset); - const size = Number(header.size); - debugSections.debugAbbrev = this.buffer.subarray(offset, offset + size); - } - } - - this.dwarfParser.setDebugSections(debugSections); - } - - public getSymbolDebugInfo(address: number): { file?: string; line?: number } | null { - const lineInfo = this.dwarfParser.getLineInfo(address); - if (lineInfo) { - return { - file: lineInfo.file, - line: lineInfo.line - }; - } - return null; - } -} \ No newline at end of file diff --git a/src/utils/elf/handleElf.ts b/src/utils/elf/handleElf.ts deleted file mode 100644 index 9af3744..0000000 --- a/src/utils/elf/handleElf.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ElfAnalyzer } from './index'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as vscode from 'vscode'; - -const SECTIONS = 'sections'; -const SYMBOLS_BY_SECTION = 'symbolsBySection'; -const SYMBOLS_BY_SECTION_FROM_ELF = "symbolsBySectionFromElf"; -const GET_SYMBOL_INFO = 'getSymbolInfo'; -const SYMBOL_INFO_RESPONSE = 'symbolInfoResponse'; -const OPEN_SYMBOL_SOURCE = 'openSymbolSource'; - -// Test the analyzer with local files -export async function handleElf(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) { - - // 1. 先判断是否存在工作区 - if (!vscode.workspace.workspaceFolders) { - // 提示用户打开工作区 - vscode.window.showErrorMessage('请先打开一个项目文件夹或工作区!'); - return; - } - const projectPath = vscode.workspace.workspaceFolders![0].uri.fsPath; - - // Support both naming conventions: rtthread.* and rt-thread.* - let elfPath = path.join(projectPath, 'rtthread.elf'); - let mapPath = path.join(projectPath, 'rtthread.map'); - - // Check for rtthread.* first - let elfExists = fs.existsSync(elfPath); - let mapExists = fs.existsSync(mapPath); - - // If not found, try rt-thread.* - if (!elfExists) { - elfPath = path.join(projectPath, 'rt-thread.elf'); - elfExists = fs.existsSync(elfPath); - } - - if (!mapExists) { - mapPath = path.join(projectPath, 'rt-thread.map'); - mapExists = fs.existsSync(mapPath); - } - - if (!elfExists && !mapExists) { - // console.log('\nPlease place rtthread.elf and/or rtthread.map in the current directory to test.'); - vscode.window.showInformationMessage('ELF 或 MAP 文件不存在!请检查当前目录下是否存在 rtthread.elf/rt-thread.elf 或 rtthread.map/rt-thread.map 文件'); - return; - } - - try { - const analyzer = new ElfAnalyzer( - elfExists ? elfPath : undefined, - mapExists ? mapPath : undefined - ); - - // Test getSections - const sections = analyzer.getSections(); - const postSections = []; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const symbols = analyzer.getSymbolsBySection(section.name); - if (symbols.length > 0) { - postSections.push(section); - } - } - console.log('\n=== Sections (Top 5) ===', postSections); - panel.webview.postMessage({ eventName: SECTIONS, data: postSections, from: 'extension' }); - - // 监听 Webview 发送的消息 - panel.webview.onDidReceiveMessage( - async (message) => { - console.log(message.eventName); - if (message.eventName === GET_SYMBOL_INFO) { - console.log('GET_SYMBOL_INFO'); - } - if (message.eventName === OPEN_SYMBOL_SOURCE) { - console.log('OPEN_SYMBOL_SOURCE'); - } - - // 根据消息中的 command 处理不同逻辑 - switch (message.eventName) { - case SYMBOLS_BY_SECTION: - const symbols = analyzer.getSymbolsBySection(message.sectionName); - panel.webview.postMessage({ eventName: SYMBOLS_BY_SECTION_FROM_ELF, data: symbols, from: 'extension' }); - break; - - case GET_SYMBOL_INFO: - // 获取符号的调试信息(源文件和行号) - const symbolInfo = analyzer.getSymbolWithDebugInfo(message.symbolName); - panel.webview.postMessage({ - eventName: SYMBOL_INFO_RESPONSE, - data: symbolInfo, - from: 'extension' - }); - break; - - case OPEN_SYMBOL_SOURCE: - // 打开符号对应的源代码文件 - const symbol = analyzer.getSymbolWithDebugInfo(message.symbolName); - if (symbol) { - // Check if symbol is a FUNCTION type - if (symbol.type !== 'FUNC') { - vscode.window.showWarningMessage(`符号 ${message.symbolName} 是 ${symbol.type} 类型,暂不支持定位源代码位置`); - break; - } - - if (symbol.sourceFile) { - try { - // 处理相对路径和绝对路径 - let filePath = symbol.sourceFile; - if (!path.isAbsolute(filePath)) { - filePath = path.join(projectPath, filePath); - } - - // 检查文件是否存在 - if (fs.existsSync(filePath)) { - const document = await vscode.workspace.openTextDocument(filePath); - const editor = await vscode.window.showTextDocument(document); - - // 跳转到指定行号 - if (symbol.sourceLine && symbol.sourceLine > 0) { - const position = new vscode.Position(symbol.sourceLine - 1, 0); - const range = new vscode.Range(position, position); - editor.selection = new vscode.Selection(range.start, range.end); - editor.revealRange(range, vscode.TextEditorRevealType.InCenter); - } - - // vscode.window.showInformationMessage(`已打开 ${symbol.name} 的源代码位置`); - } else { - vscode.window.showWarningMessage(`源文件不存在: ${filePath}`); - } - } catch (error) { - vscode.window.showErrorMessage(`无法打开源文件: ${error}`); - } - } else { - vscode.window.showWarningMessage(`无法找到函数 ${message.symbolName} 的源代码位置`); - } - } else { - vscode.window.showWarningMessage(`未找到符号: ${message.symbolName}`); - } - break; - - default: - break; - } - }, - undefined, // 可选的错误处理 - ); - - // Test getSymbolsBySection - if (elfExists) { - const sections = analyzer.getSections(); - const textSection = sections.find(s => s.name === '.text'); - if (textSection) { - const textSymbols = analyzer.getSymbolsBySection('.text'); - } - } - - // Test getSymbols by object (if MAP file available) - if (mapExists) { - const allSymbols = analyzer.getAllSymbols(); - const uniqueObjects = new Set(allSymbols.map(s => s.object).filter(o => o)); - const firstObject = Array.from(uniqueObjects)[0]; - - if (firstObject) { - console.log(`\n=== Symbols in ${firstObject} (Top 5) ===`); - const objectSymbols = analyzer.getSymbols(firstObject); - console.log(`Found ${objectSymbols.length} symbols:`); - objectSymbols.slice(0, 5).forEach(symbol => { - console.log(` ${symbol.name}: size=${symbol.size}, addr=${symbol.hexaddr}`); - }); - } - } - - } catch (error) { - console.error('Error during analysis:', error); - } -} \ No newline at end of file diff --git a/src/utils/elf/index.ts b/src/utils/elf/index.ts deleted file mode 100644 index 6ba29f0..0000000 --- a/src/utils/elf/index.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ElfParser } from './elf-parser'; -import { MapParser } from './map-parser'; -import { Section, Symbol } from './types'; - -export class ElfAnalyzer { - private elfParser: ElfParser | null = null; - private mapParser: MapParser | null = null; - - constructor(elfPath?: string, mapPath?: string) { - if (elfPath) { - this.elfParser = new ElfParser(elfPath); - } - if (mapPath) { - this.mapParser = new MapParser(mapPath); - } - } - - /** - * Get all sections from the ELF file - */ - public getSections(): Section[] { - if (!this.elfParser) { - throw new Error('ELF file not loaded'); - } - return this.elfParser.getSections(); - } - - /** - * Get symbols by section name - */ - public getSymbolsBySection(section: string): Symbol[] { - if (!this.elfParser) { - throw new Error('ELF file not loaded'); - } - return this.elfParser.getSymbolsBySection(section); - } - - /** - * Get all symbols (merged from ELF and MAP if both available) - */ - public getAllSymbols(): Symbol[] { - if (!this.elfParser && !this.mapParser) { - throw new Error('No ELF or MAP file loaded'); - } - - if (this.elfParser && this.mapParser) { - const elfSymbols = this.elfParser.getAllSymbols(); - return this.mapParser.mergeWithElfSymbols(elfSymbols); - } else if (this.elfParser) { - return this.elfParser.getAllSymbols(); - } else if (this.mapParser) { - return this.mapParser.getAllSymbols(); - } - - return []; - } - - /** - * Get symbols from a specific object file - */ - public getSymbols(object: string): Symbol[] { - if (!this.mapParser) { - // If no MAP file, try to filter by object name from all symbols - const allSymbols = this.getAllSymbols(); - return allSymbols - .filter(sym => sym.object === object) - .sort((a, b) => b.size - a.size); - } - return this.mapParser.getSymbolsByObject(object); - } - - /** - * Get debug information for a symbol (source file and line number) - */ - public getSymbolDebugInfo(symbolName: string): { sourceFile?: string; sourceLine?: number } | null { - if (!this.elfParser) { - return null; - } - - try { - // First find the symbol to get its address - const allSymbols = this.getAllSymbols(); - const symbol = allSymbols.find(s => s.name === symbolName); - - if (!symbol) { - return null; - } - - // Use internal DWARF parser to get source file and line number - const debugInfo = this.elfParser.getSymbolDebugInfo(symbol.address); - - if (debugInfo && debugInfo.file) { - return { - sourceFile: debugInfo.file, - sourceLine: debugInfo.line - }; - } - } catch (error) { - console.error('Error getting debug info:', error); - } - - return null; - } - - /** - * Get symbol with debug information - */ - public getSymbolWithDebugInfo(symbolName: string): Symbol | null { - const allSymbols = this.getAllSymbols(); - const symbol = allSymbols.find(s => s.name === symbolName); - - if (!symbol) { - return null; - } - - // Only provide debug info for FUNCTION symbols - // OBJECT symbols (variables) don't have accurate debug info - if (symbol.type === 'FUNC') { - const debugInfo = this.getSymbolDebugInfo(symbolName); - if (debugInfo) { - return { - ...symbol, - sourceFile: debugInfo.sourceFile, - sourceLine: debugInfo.sourceLine - }; - } - } - - // For OBJECT and other types, return without source info - return symbol; - } -} - -// Export types and classes -export { Section, Symbol } from './types'; -export { ElfParser } from './elf-parser'; -export { MapParser } from './map-parser'; \ No newline at end of file diff --git a/src/utils/elf/map-parser.ts b/src/utils/elf/map-parser.ts deleted file mode 100644 index ba8319e..0000000 --- a/src/utils/elf/map-parser.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as fs from 'fs'; -import { Symbol } from './types'; - -export class MapParser { - private content: string; - private symbols: Map = new Map(); - - constructor(filePath: string) { - this.content = fs.readFileSync(filePath, 'utf-8'); - this.parse(); - } - - private parse(): void { - const lines = this.content.split('\n'); - let inSymbolSection = false; - let currentObject = ''; - - for (const line of lines) { - // Detect symbol section start - if (line.includes('Symbol') && line.includes('File')) { - inSymbolSection = true; - continue; - } - - if (!inSymbolSection) { - continue; - } - - // Parse object file name - const objectMatch = line.match(/^\..*?\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+(.+\.o)/i); - if (objectMatch) { - currentObject = objectMatch[1]; - } - - // Parse symbol entries - // Format: symbol_name 0xaddress 0xsize object_file - const symbolMatch = line.match(/^\s+(\S+)\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)/i); - if (symbolMatch) { - const name = symbolMatch[1]; - const address = parseInt(symbolMatch[2], 16); - const size = parseInt(symbolMatch[3], 16); - - if (size > 0) { // Only store symbols with non-zero size - const key = `${name}_${address}`; - if (!this.symbols.has(key) || this.symbols.get(key)!.size < size) { - this.symbols.set(key, { - name, - type: 'UNKNOWN', - address, - hexaddr: '0x' + address.toString(16), - size, - object: currentObject || undefined - }); - } - } - } - - // Alternative format handling - const altMatch = line.match(/^([.\w]+)\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)/i); - if (altMatch && !symbolMatch) { - const name = altMatch[1]; - const address = parseInt(altMatch[2], 16); - const size = parseInt(altMatch[3], 16); - - if (size > 0 && !name.startsWith('.')) { - const key = `${name}_${address}`; - if (!this.symbols.has(key) || this.symbols.get(key)!.size < size) { - this.symbols.set(key, { - name, - type: 'UNKNOWN', - address, - hexaddr: '0x' + address.toString(16), - size, - object: currentObject || undefined - }); - } - } - } - } - } - - public getAllSymbols(): Symbol[] { - return Array.from(this.symbols.values()).sort((a, b) => b.size - a.size); - } - - public getSymbolsByObject(objectFile: string): Symbol[] { - return Array.from(this.symbols.values()) - .filter(sym => sym.object === objectFile) - .sort((a, b) => b.size - a.size); - } - - public mergeWithElfSymbols(elfSymbols: Symbol[]): Symbol[] { - const mergedMap = new Map(); - - // Add ELF symbols first - for (const sym of elfSymbols) { - const key = `${sym.name}_${sym.address}`; - mergedMap.set(key, sym); - } - - // Merge MAP symbols (may provide additional object info) - for (const sym of this.symbols.values()) { - const key = `${sym.name}_${sym.address}`; - const existing = mergedMap.get(key); - if (existing) { - // Merge object information from MAP file - if (sym.object && !existing.object) { - existing.object = sym.object; - } - } else { - mergedMap.set(key, sym); - } - } - - return Array.from(mergedMap.values()).sort((a, b) => b.size - a.size); - } -} diff --git a/src/utils/elf/test-object-symbols.js b/src/utils/elf/test-object-symbols.js deleted file mode 100644 index d934283..0000000 --- a/src/utils/elf/test-object-symbols.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script to verify OBJECT symbols don't have source info - */ - -const path = require('path'); -const fs = require('fs'); - -// Load the compiled module -const { ElfAnalyzer } = require('../../../out/utils/elf/index'); - -console.log('=== Testing OBJECT Symbol Source Info Removal ===\n'); - -// Find a test ELF file -const testDir = path.join(__dirname, 'test/fixtures'); -const elfPath = path.join(testDir, 'test.elf'); - -if (!fs.existsSync(elfPath)) { - console.log('Test ELF not found, creating a simple analyzer...'); - const analyzer = new ElfAnalyzer(); - console.log('✓ ElfAnalyzer created without files'); - process.exit(0); -} - -try { - const analyzer = new ElfAnalyzer(elfPath); - console.log('✓ Loaded ELF file:', elfPath); - - // Get all symbols - const allSymbols = analyzer.getAllSymbols(); - console.log(`\nFound ${allSymbols.length} total symbols`); - - // Separate by type - const functions = allSymbols.filter(s => s.type === 'FUNC'); - const objects = allSymbols.filter(s => s.type === 'OBJECT'); - - console.log(` - ${functions.length} FUNCTION symbols`); - console.log(` - ${objects.length} OBJECT symbols`); - - // Test a function symbol - console.log('\n--- Testing FUNCTION Symbol ---'); - if (functions.length > 0) { - const func = functions[0]; - console.log(`Testing: ${func.name}`); - const funcWithDebug = analyzer.getSymbolWithDebugInfo(func.name); - if (funcWithDebug) { - console.log(` Type: ${funcWithDebug.type}`); - console.log(` Has sourceFile: ${funcWithDebug.sourceFile ? 'YES' : 'NO'}`); - console.log(` Has sourceLine: ${funcWithDebug.sourceLine ? 'YES' : 'NO'}`); - if (funcWithDebug.sourceFile) { - console.log(` Source: ${funcWithDebug.sourceFile}:${funcWithDebug.sourceLine}`); - } - } - } - - // Test an object symbol - console.log('\n--- Testing OBJECT Symbol ---'); - if (objects.length > 0) { - const obj = objects[0]; - console.log(`Testing: ${obj.name}`); - const objWithDebug = analyzer.getSymbolWithDebugInfo(obj.name); - if (objWithDebug) { - console.log(` Type: ${objWithDebug.type}`); - console.log(` Has sourceFile: ${objWithDebug.sourceFile ? 'YES' : 'NO'}`); - console.log(` Has sourceLine: ${objWithDebug.sourceLine ? 'YES' : 'NO'}`); - if (objWithDebug.sourceFile) { - console.log(` ⚠️ WARNING: OBJECT should not have source info!`); - console.log(` Source: ${objWithDebug.sourceFile}:${objWithDebug.sourceLine}`); - } else { - console.log(` ✓ Correctly has no source info (as expected for OBJECT)`); - } - } - } - - // Test specific known symbols if they exist - console.log('\n--- Testing Known Symbols ---'); - const testSymbols = [ - { name: 'main', expectedType: 'FUNC', shouldHaveSource: true }, - { name: 'global_variable', expectedType: 'OBJECT', shouldHaveSource: false }, - { name: 'global_array', expectedType: 'OBJECT', shouldHaveSource: false } - ]; - - testSymbols.forEach(test => { - const symbol = analyzer.getSymbolWithDebugInfo(test.name); - if (symbol) { - const hasSource = !!(symbol.sourceFile); - const correct = hasSource === test.shouldHaveSource; - const status = correct ? '✓' : '✗'; - console.log(`${status} ${test.name} (${symbol.type}): source info = ${hasSource ? 'YES' : 'NO'}`); - } - }); - - console.log('\n✅ Test completed successfully!'); - -} catch (error) { - console.error('Error:', error.message); - if (error.message.includes('out of range')) { - console.log('Note: DWARF parsing error, but the main functionality works'); - } - process.exit(1); -} \ No newline at end of file diff --git a/src/utils/elf/types.ts b/src/utils/elf/types.ts deleted file mode 100644 index ba25483..0000000 --- a/src/utils/elf/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -export interface Section { - name: string; - address: number; - size: number; - needsLoad: boolean; -} - -export interface Symbol { - name: string; - type: string; - address: number; - hexaddr: string; - size: number; - section?: string; - object?: string; - sourceFile?: string; - sourceLine?: number; -} - -export interface ElfHeader { - magic: Buffer; - class: number; - data: number; - version: number; - osabi: number; - abiversion: number; - type: number; - machine: number; - entry: bigint; - phoff: bigint; - shoff: bigint; - flags: number; - ehsize: number; - phentsize: number; - phnum: number; - shentsize: number; - shnum: number; - shstrndx: number; -} - -export interface SectionHeader { - name: number; - type: number; - flags: bigint; - addr: bigint; - offset: bigint; - size: bigint; - link: number; - info: number; - addralign: bigint; - entsize: bigint; -} - -export interface SymbolEntry { - name: number; - info: number; - other: number; - shndx: number; - value: bigint; - size: bigint; -} \ No newline at end of file diff --git a/src/vue/analyze/App.vue b/src/vue/analyze/App.vue deleted file mode 100644 index f7d551c..0000000 --- a/src/vue/analyze/App.vue +++ /dev/null @@ -1,210 +0,0 @@ - - - - - diff --git a/src/vue/analyze/index.html b/src/vue/analyze/index.html deleted file mode 100644 index ac696db..0000000 --- a/src/vue/analyze/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Analyze RT-Thread - - - -
- - - diff --git a/src/vue/analyze/index.less b/src/vue/analyze/index.less deleted file mode 100644 index 29cc963..0000000 --- a/src/vue/analyze/index.less +++ /dev/null @@ -1,67 +0,0 @@ -// Analyze page styles -@import '../styles/mixins.less'; -@import '../styles/variables.less'; - -.container { - .container-base(); - padding: 0; - min-height: 100vh; -} - -.content_area { - // Use Element Plus default styles -} - -// Tabs styles -.section-tabs { - // Use Element Plus default tab styles - - > .el-tabs__content { - padding: @spacing-xxl; - font-size: @font-size-base; - font-weight: 400; - } -} - -// Table styles - using Element Plus theme -:deep(.el-table) { - font-size: @font-size-base; -} - -// Symbol info panel -.symbol-info-panel { - margin-top: @spacing-base; - border-radius: @border-radius-base; - overflow: hidden; - - .symbol-info-header { - .flex-between(); - padding: @spacing-sm @spacing-base; - - span { - .text-title(); - font-size: @font-size-medium; - } - } - - .symbol-info-content { - padding: @spacing-base; - max-height: 300px; - overflow-y: auto; - .scrollbar-style(); - - pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; - font-family: 'Courier New', monospace; - font-size: @font-size-small; - line-height: @line-height-normal; - } - } -} - -// Icon styles -:deep(.el-icon) { - font-size: 16px; -} \ No newline at end of file diff --git a/src/vue/analyze/main.ts b/src/vue/analyze/main.ts deleted file mode 100644 index 17c0cff..0000000 --- a/src/vue/analyze/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '../styles/global.less'; -import { createApp } from 'vue'; -import App from './App.vue'; - -const app = createApp(App); -app.mount('#app'); - diff --git a/src/vue/setting/App.vue b/src/vue/setting/App.vue index 0f89edf..36efb64 100644 --- a/src/vue/setting/App.vue +++ b/src/vue/setting/App.vue @@ -1,64 +1,32 @@ -onUpdated(() => { - console.log('App updated'); -}); - -onMounted(() => { - sendCommand('getExtensionInfo'); - window.addEventListener('message', event => { - const message = event.data; - // console.log(message); - - switch (message.command) { - case 'extensionInfo': - extensionInfo.value.version = message.data.version; - - envInfo.value.version = message.data.env.version; - envInfo.value.path = message.data.env.path; - - // 设置RT-Thread配置数据 - if (message.data.configInfo && message.data.configInfo.length > 0) { - envInfo.value.rtConfig.path = message.data.configInfo[0].path || ''; - } - break; - - case 'setToolchainFolder': - // message like {command: 'setToolchainFolder', data: 'path'} - // 这个消息现在由SDK管理器处理 - break; - - case 'setItemFolder': - // message like {command: 'setItemFolder', data: 'path'} - // 这个消息现在由environment页面处理 - break; - - case 'setSDKConfig': - // SDK配置现在由SDK管理器页面处理 - break; + diff --git a/src/vue/setting/data.ts b/src/vue/setting/data.ts index 4562238..5aff697 100644 --- a/src/vue/setting/data.ts +++ b/src/vue/setting/data.ts @@ -1,74 +1,23 @@ import { ref } from 'vue'; -// data model -export let extensionInfo = ref({ - envRoot: "~/.env", - version: "0.4.11" -}); - -export const envInfo = ref({ - progressNum: 0, - selectRow: null, +export interface EnvStatus { + installed: boolean; + path: string; + envVersion?: string; + envGitRev?: string; +} - // Env 安装状态相关 - envStatus: { - installed: false, - path: "~/.env", - envVersion: undefined, - envGitRev: undefined - }, - isInstalling: false, - installProgress: [], - showProgressLog: false, - showForceCloseButton: false, // 控制强制关闭按钮的显示 +export interface RootConfig { + name: string; + path: string; + description: string; +} - version : "v2.0.1", - path: "~/.env", - - // RT-Thread 根目录配置 +export const envInfo = ref({ + envStatus: { installed: false, path: '~/.env' } as EnvStatus, rtConfig: { - name: "RT-Thread", - path: "d:/workspace/rt-thread", - description: "RT-Thread 主干版本" - }, - rtConfigDialogVisible: false, - editRtConfig: { - name: "", - path: "", - description: "" - }, - - radioChange: 0, -}); - -export const sdkInfo = ref({ - selectRow: null, - sdkTitleList: [ - { - title: "名称", - field: "name", - }, - { - title: "路径", - field: "path", - }, - { - title: "描述", - field: "description", - }, - ], - sdkData: [ - { - name: "gcc", - path: "d:/tools/toolchains/arm-none-eabi-gcc", - description: "ARM GNU GCC", - } - ], - dialogVisible: false, - editMode: false, - addToolchain: { - name: "", - path: "", - description: "" - } + name: 'RT-Thread', + path: '', + description: 'RT-Thread 主干版本', + } as RootConfig, }); diff --git a/src/vue/setting/main.ts b/src/vue/setting/main.ts index 7429d17..94e56f4 100644 --- a/src/vue/setting/main.ts +++ b/src/vue/setting/main.ts @@ -1,12 +1,9 @@ import { createApp } from 'vue'; import App from './App.vue'; -import router from './router'; import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; import '../styles/global.less'; const app = createApp(App); -app.use(router); app.use(ElementPlus); app.mount('#app'); - diff --git a/src/vue/setting/router.ts b/src/vue/setting/router.ts deleted file mode 100644 index 4a8ffa2..0000000 --- a/src/vue/setting/router.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createRouter, createWebHashHistory } from "vue-router"; -import Environment from './view/environment/index.vue'; -import Sdk from './view/sdk/index.vue'; -import Toolchains from './view/toolchains/index.vue'; -import LayoutView from './view/layout/index.vue'; - -export const routes = [ - { - path: '/', - component: LayoutView, - meta: { - title: '环境工具' - }, - redirect: '/environment', - children: [ - { - path: '/environment', - component: Environment, - meta: { - title: '环境工具' - } - }, - ] - }, - { - path: '/sdk', - component: LayoutView, - meta: { - title: 'SDK管理器' - }, - redirect: '/sdk', - children: [ - { - path: '/sdk', - component: Sdk, - meta: { - title: 'SDK管理器' - }, - }, - ] - }, - { - path: '/toolchains', - component: LayoutView, - meta: { - title: '工具链设置' - }, - redirect: '/toolchains', - children: [ - { - path: '/toolchains', - component: Toolchains, - meta: { - title: '工具链设置' - }, - }, - ] - } -]; - -const router = createRouter({ - history: createWebHashHistory(), - routes -}); - -export default router; diff --git a/src/vue/setting/view/environment/index.less b/src/vue/setting/view/environment/index.less index c049c4e..b43ff22 100644 --- a/src/vue/setting/view/environment/index.less +++ b/src/vue/setting/view/environment/index.less @@ -1,234 +1,103 @@ -@import '../../../styles/mixins.less'; +@import '../../../styles/variables.less'; .content { - width: 100%; - height: 100%; - padding-bottom: @spacing-xl; - - .body-box { width: 100%; - min-width: 800px; - display: flex; - flex-direction: column; - row-gap: @spacing-xl; - } + min-width: 0; } -.rt-config-box { - .config-card { - .card-container(); - - .card-header { - .card-header(); - margin-bottom: @spacing-base; - - .card-title { - .text-title(); - padding-top: @spacing-sm; - } - - .el-button { - min-width: 100px; - width: 100px; - } - } - - .config-content { - display: flex; - flex-direction: column; - row-gap: @spacing-sm; - - .config-item { - display: flex; - align-items: center; - column-gap: @spacing-sm; - font-size: @font-size-base; - color: @color-text-primary; - - label { - width: 80px; - font-weight: 500; - color: @color-text-secondary; - } - - span { - flex: 1; - } - } - } - } +section { + padding: @spacing-lg 0; } -.env-status-box { - .card-container(); - display: flex; - flex-direction: column; - row-gap: @spacing-lg; +h2, h3 { + margin: 0; + font-weight: 600; + font-size: 16px; +} - .status-header { +.status-header, .config-header, .log-header { display: flex; justify-content: space-between; align-items: flex-start; - gap: @spacing-xl; - font-size: @font-size-base; - color: @color-text-secondary; - - .info-content { - flex: 1; - padding-top: @spacing-sm; - - .status-title { - margin: 0 0 @spacing-xs; - font-weight: 600; - color: @color-text-primary; - } - - .info-text { - .tag-list(); - margin-left: 0; - - li { - color: @color-text-secondary; - } - } - } - - .env-actions { - display: flex; - align-items: center; - - .el-button { - min-width: 120px; - } + gap: @spacing-base; +} - .env-actions-group { - display: flex; - gap: @spacing-sm; +.info-content { + min-width: 0; + flex: 1; - .el-button { - min-width: 100px; - } - } + h2 { + margin-bottom: @spacing-sm; } - } +} - .progress-box { - display: flex; - flex-direction: column; - row-gap: @spacing-sm; +.info-text { + display: grid; + grid-template-columns: 70px minmax(0, 1fr); + gap: @spacing-sm @spacing-base; + margin: @spacing-base 0 0; + font-size: @font-size-base; - .progress-status { - font-weight: 600; - color: @color-text-primary; + dt { + color: @color-text-secondary; } - .progress-log { - .surface-muted(@spacing-sm); - display: flex; - flex-direction: column; - row-gap: @spacing-sm; - font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; - font-size: @font-size-extra-small; - - .log-header { - .flex-between(); - align-items: center; - - .log-title { - margin: 0; - font-weight: 600; - color: @color-text-primary; - } - - .log-actions { - display: flex; - column-gap: @spacing-sm; - align-items: center; - } - } - - .log-content { - max-height: 200px; - overflow-y: auto; - padding: @spacing-sm; - border-radius: @border-radius-small; - background-color: @color-surface; - .scrollbar-style(); - } - - .log-message { + dd { margin: 0; - white-space: pre-wrap; - line-height: 1.4; - } - - .terminal-container { - height: 400px; - border-radius: @border-radius-base; - overflow: hidden; - background-color: @color-surface; - - :deep(.xterm) { - padding: @spacing-sm; - height: 100%; - } - - :deep(.xterm-viewport) { - overflow-y: auto; - } - - :deep(.xterm-screen) { - height: 100%; - } - } + overflow-wrap: anywhere; } - } } -.form-box { - .card-container(); - margin-bottom: @spacing-xl; - padding: @spacing-xl; - display: flex; - flex-direction: column; - row-gap: @spacing-base; - - .row-box { +.env-actions { display: flex; - align-items: center; - column-gap: @spacing-base; - width: 100%; - - .el-input { - flex: 1; - } + flex-wrap: wrap; + gap: @spacing-sm; .el-button { - min-width: 80px; + margin: 0; + min-width: 124px; } - } +} - :deep(.el-input) { - height: 40px; - } +.rt-config-box { + border-top: 1px solid @color-border; +} - :deep(.el-form-item__label) { - justify-content: flex-start; - font-weight: 500; - color: @color-text-secondary; - } +.progress-box { + margin-top: @spacing-lg; +} - :deep(.el-form-item__content) { - flex: 1; - } +.log-header { + align-items: center; + margin-bottom: @spacing-sm; } -.dialog-footer { - display: flex; - justify-content: flex-end; - column-gap: @spacing-base; +.terminal-container { + height: 360px; + width: 100%; + overflow: hidden; + border: 1px solid @color-border; + border-radius: 4px; } -.el-button { - min-width: 120px; +.folder-input { + display: flex; + gap: @spacing-sm; + width: 100%; + min-width: 0; + + .el-input { + min-width: 0; + flex: 1; + } +} + +@media (max-width: 600px) { + .status-header { + flex-direction: column; + } + + .info-content { + width: 100%; + } } diff --git a/src/vue/setting/view/environment/index.vue b/src/vue/setting/view/environment/index.vue index ed069d3..daf3363 100644 --- a/src/vue/setting/view/environment/index.vue +++ b/src/vue/setting/view/environment/index.vue @@ -1,479 +1,234 @@ - diff --git a/src/vue/setting/view/layout/index.less b/src/vue/setting/view/layout/index.less deleted file mode 100644 index 794069f..0000000 --- a/src/vue/setting/view/layout/index.less +++ /dev/null @@ -1,49 +0,0 @@ -@import '../../../styles/mixins.less'; - -.common-layout { - min-height: 100vh; - background-color: @color-surface; - color: @color-text-primary; - - .el-aside { - width: 100%; - padding: 0 @spacing-xxl; - transition: @transition-all; - - .el-menu { - background-color: transparent; - border: none !important; - color: @color-text-secondary; - } - - .el-menu-item { - transition: @transition-all; - border-radius: @border-radius-base; - color: @color-text-secondary; - - .title { - font-size: @font-size-base; - } - - &:hover { - background-color: @color-surface-muted; - color: @color-text-primary; - } - - &.is-active { - font-weight: 600; - color: @color-accent; - background-color: color-mix(in srgb, @color-accent 12%, transparent); - } - } - } - - .container_main { - width: 100%; - - .main_box { - width: 100%; - padding: @spacing-lg @spacing-xxl; - } - } -} diff --git a/src/vue/setting/view/layout/index.vue b/src/vue/setting/view/layout/index.vue deleted file mode 100644 index 4cabea9..0000000 --- a/src/vue/setting/view/layout/index.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - diff --git a/src/vue/setting/view/sdk/index.less b/src/vue/setting/view/sdk/index.less deleted file mode 100644 index 8840596..0000000 --- a/src/vue/setting/view/sdk/index.less +++ /dev/null @@ -1,222 +0,0 @@ -@import '../../../styles/mixins.less'; - -.sdk-container { - width: 100%; - height: 100%; - padding: @spacing-xl; - box-sizing: border-box; - display: flex; - flex-direction: column; - row-gap: @spacing-lg; -} - -.sdk-header { - .card-container(); - display: flex; - align-items: center; - justify-content: space-between; - column-gap: @spacing-lg; - padding: @spacing-lg @spacing-xl; - - .sdk-title { - .text-title(); - margin: 0; - } - - .sdk-actions { - display: flex; - column-gap: @spacing-sm; - } -} - -.sdk-content { - .table-container(); - padding: 0; - - .el-table { - background-color: transparent; - - .el-table__expand-icon { - display: none; - } - } -} - -.sdk-name { - display: flex; - align-items: center; - column-gap: @spacing-sm; - cursor: pointer; - color: @color-text-primary; - - .expand-icon { - font-size: @font-size-small; - transition: transform @transition-duration ease; - - &.el-icon-arrow-down { - transform: rotate(90deg); - } - } - - span { - font-weight: 500; - } -} - -.install-status { - display: flex; - align-items: center; - column-gap: @spacing-sm; - - .el-tag { - min-width: 60px; - } - - .installed-version, - .selected-version { - font-weight: 500; - color: @color-text-secondary; - } -} - -.sdk-expand-content { - padding: 1px; - background-color: @color-surface; -} - -.sdk-versions { - display: flex; - flex-direction: column; - row-gap: @spacing-sm; - margin-left: 50px; - - .version-radio-item { - display: flex; - align-items: center; - column-gap: @spacing-sm; - cursor: pointer; - padding: @spacing-xs @spacing-sm; - border-radius: @border-radius-base; - transition: background-color @transition-duration-fast ease; - - input[type='radio'] { - width: 16px; - height: 16px; - margin: 0; - cursor: pointer; - } - - label { - margin-left: @spacing-xs; - cursor: pointer; - user-select: none; - } - - input[type='radio']:checked + label { - font-weight: 600; - color: @color-accent; - } - } -} - -.el-table__empty-text { - font-size: @font-size-base; - padding: @spacing-xxl 0; - color: @color-text-secondary; -} - -.sdk-log-dialog { - max-width: 1200px; -} - -.log-dialog-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: @spacing-lg; - - .dialog-title { - .text-title(); - font-size: @font-size-large; - } -} - -.log-container { - border-radius: @border-radius-base; - height: 60vh; - max-height: 600px; - min-height: 400px; - overflow: hidden; - position: relative; - background-color: @color-surface; -} - -.terminal-content { - height: 100%; - width: 100%; - - .xterm { - height: 100%; - padding: @spacing-sm; - } - - .xterm-viewport { - overflow-y: auto !important; - } -} - -.log-dialog-footer { - display: flex; - align-items: center; - justify-content: center; - column-gap: @spacing-base; - padding: @spacing-lg; - - .update-status { - padding: @spacing-sm @spacing-base; - font-size: @font-size-base; - - i { - margin-right: @spacing-xs; - animation: rotate 1s linear infinite; - } - } -} - -@media (max-width: 768px) { - .sdk-container { - padding: @spacing-lg; - } - - .sdk-header { - flex-direction: column; - align-items: flex-start; - row-gap: @spacing-sm; - padding: @spacing-lg; - } - - .sdk-title { - font-size: @font-size-large; - } - - .sdk-actions { - width: 100%; - - .el-button { - flex: 1; - } - } - - .sdk-expand-content { - padding: @spacing-lg; - } -} - -@keyframes rotate { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/vue/setting/view/sdk/index.vue b/src/vue/setting/view/sdk/index.vue deleted file mode 100644 index d482f48..0000000 --- a/src/vue/setting/view/sdk/index.vue +++ /dev/null @@ -1,399 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/vue/setting/view/toolchains/index.less b/src/vue/setting/view/toolchains/index.less deleted file mode 100644 index 11715b0..0000000 --- a/src/vue/setting/view/toolchains/index.less +++ /dev/null @@ -1,128 +0,0 @@ -@import '../../../styles/mixins.less'; - -.content { - padding: @spacing-xl; - height: 100%; - box-sizing: border-box; -} - -.body-box { - .card-container(); - height: 100%; - display: flex; - flex-direction: column; - row-gap: @spacing-lg; -} - -.header-section { - .card-header(); - margin-bottom: 0; - padding-bottom: @spacing-base; - border-bottom: 1px solid @color-border; -} - -.page-title { - .text-title(); - margin: 0; -} - -.header-actions { - display: flex; - column-gap: @spacing-sm; -} - -.table-container { - .table-container(); - flex: 1; - overflow: auto; -} - -.el-table { - font-size: @font-size-base; - background-color: transparent; -} - -.toolchain-form { - padding: @spacing-base 0; - - .el-form-item { - margin-bottom: @spacing-xl; - - &:last-child { - margin-bottom: 0; - } - } - - .el-form-item__label { - font-weight: 500; - color: @color-text-secondary; - } - - .el-input__inner { - height: 36px; - } - - .el-textarea__inner { - font-family: inherit; - line-height: @line-height-normal; - } -} - -.path-input-group { - display: flex; - column-gap: @spacing-sm; - align-items: center; - width: 100%; - - // Make the path input flexibly take available width - .el-input { - flex: 1 1 0; - min-width: 0; // allow shrinking in flex container - } - - .el-button { - min-width: 80px; - flex: 0 0 auto; // keep button size stable - } -} - -.dialog-footer { - display: flex; - justify-content: flex-end; - column-gap: @spacing-sm; -} - -.el-table__empty-text { - font-size: @font-size-base; - padding: @spacing-xxl 0; - color: @color-text-secondary; -} - -@media (max-width: 768px) { - .content { - padding: @spacing-lg; - } - - .body-box { - padding: @spacing-lg; - } - - .header-section { - flex-direction: column; - align-items: flex-start; - row-gap: @spacing-sm; - } - - .page-title { - font-size: @font-size-large; - } - - .header-actions { - width: 100%; - flex-direction: column; - - .el-button { - width: 100%; - } - } -} diff --git a/src/vue/setting/view/toolchains/index.vue b/src/vue/setting/view/toolchains/index.vue deleted file mode 100644 index 12d26d4..0000000 --- a/src/vue/setting/view/toolchains/index.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/webviews/analyze.ts b/src/webviews/analyze.ts deleted file mode 100644 index c5ffc5f..0000000 --- a/src/webviews/analyze.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import { postMessageExtensionData } from '../extension'; -import { handleElf } from '../utils/elf/handleElf'; - -let analyzeViewPanel: vscode.WebviewPanel | null = null; -const name = "analyze"; -const title = "Analyze RT-Thread"; - -export function openAnalyzeWebview(context: vscode.ExtensionContext) { - if (analyzeViewPanel) { - analyzeViewPanel.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 - retainContextWhenHidden: true, // Keep the webview's context when it is hidden - 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(() => { - analyzeViewPanel = 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()}"`; - }); - }); - - analyzeViewPanel = panel; - } - - postMessageExtensionData(context, analyzeViewPanel); - handleElf(context, analyzeViewPanel); - - return analyzeViewPanel; -} diff --git a/src/webviews/create-project.ts b/src/webviews/create-project.ts index 80d9bf4..8f8bfae 100644 --- a/src/webviews/create-project.ts +++ b/src/webviews/create-project.ts @@ -10,7 +10,6 @@ const name = "create-project"; const title = "RT-Thread Create Project"; const cfgFn = path.join(os.homedir(), '.env/cfg.json'); -const sdkCfgFn = path.join(os.homedir(), '.env/tools/scripts/sdk_cfg.json'); let extensionInfo = { version: "0.1.1", @@ -35,7 +34,6 @@ let extensionInfo = { ] } ], - SDKConfig: {}, configInfo: [{ name: "RT-Thread", path: "d:/workspace/rt-thread", description: "RT-Thread主干路径" }] }; @@ -69,12 +67,6 @@ export function openCreateProjectWebview(context: vscode.ExtensionContext) { extensionInfo.configInfo = cfgObj.RTThreadConfig; } - // 更新SDK相关信息 - let sdkCfgObj = readJsonObject(sdkCfgFn); - if (sdkCfgObj) { - extensionInfo.SDKConfig = sdkCfgObj; - } - // And set its HTML content // read out/${name}/index.html const indexHtmlPath = vscode.Uri.file(context.asAbsolutePath(`out/${name}/index.html`)); diff --git a/src/webviews/kconfigwebview.ts b/src/webviews/kconfigwebview.ts deleted file mode 100644 index b4652db..0000000 --- a/src/webviews/kconfigwebview.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as vscode from 'vscode'; - -let kconfigViewPanel: vscode.WebviewPanel | null = null; - -export function openKconfigWebView(title: string, url: string, context: vscode.ExtensionContext) { - if (kconfigViewPanel) { - kconfigViewPanel.reveal(vscode.ViewColumn.One); - } else { - kconfigViewPanel = vscode.window.createWebviewPanel( - 'RT-Thread Settings', // Identifier for the type of the webview - title, // Webview panel title - vscode.ViewColumn.One, // Editor column to show the new webview panel in - {} // Webview options - ); - - kconfigViewPanel.webview.options = { - enableScripts: true - }; - kconfigViewPanel.webview.html = getWebviewContent(url); - kconfigViewPanel.onDidDispose(() => { - kconfigViewPanel = null; - }, null, context.subscriptions); - } -} - -function getWebviewContent(url: string) { - return ` - - - - - - - -
- -
- - - - `; -} diff --git a/src/webviews/setting.ts b/src/webviews/setting.ts index d26ae31..8415f11 100644 --- a/src/webviews/setting.ts +++ b/src/webviews/setting.ts @@ -1,217 +1,102 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import * as os from 'os'; -import { spawn } from 'child_process'; -import { getEnvROOT, getExtensionVersion, installEnv, readJsonObject, openFolder, writeJsonObject } from '../api'; -import { handleSDKMessage } from './settings_sdk'; -import { checkEnvStatus, handleEnvMessage } from './settings_env'; +import { getEnvROOT, readJsonObject, openFolder, writeJsonObject } from '../api'; +import { handleEnvMessage } from './settings_env'; import { postMessageExtensionData } from '../extension'; -let settingViewPanel: vscode.WebviewPanel | null = null; -const name = "setting"; -const title = "RT-Thread Setting"; +let settingViewPanel: vscode.WebviewPanel | undefined; -const cfgFn = path.join(os.homedir(), '.env/cfg.json'); -const sdkCfgFn = path.join(os.homedir(), '.env/tools/scripts/sdk_cfg.json'); - -let extensionInfo = { - version: "0.1.1", - env: { - path: "~/.env", - version: "0.0.1" - }, - SDKConfig : {}, - configInfo : [{name: "RT-Thread", path: "d:/workspace/rt-thread", description: "RT-Thread主干路径"}] -}; - -function readReadmeFile(fn: string): string { - if (fs.existsSync(fn)) { - let data = fs.readFileSync(fn, 'utf-8'); - return data; - } - - return ""; +function readConfig(): any[] { + const config = readJsonObject(path.join(getEnvROOT(), 'cfg.json')); + return Array.isArray(config) ? config : []; } -function readBoardInfoFile() { - let bi = readJsonObject(path.join('resources', 'bi.json')); - - return ; +function postSettingInfo(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): void { + panel.webview.postMessage({ + command: 'extensionInfo', + data: { + version: context.extension.packageJSON.version, + env: { path: getEnvROOT() }, + configInfo: readConfig(), + }, + }); + postMessageExtensionData(context, panel); } -// Env 相关函数已移动到 settings_env.ts - -export function openSettingWebview(context: vscode.ExtensionContext) { +export async function openSettingWebview(context: vscode.ExtensionContext): Promise { if (settingViewPanel) { settingViewPanel.reveal(vscode.ViewColumn.One); + return settingViewPanel; } - 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, - retainContextWhenHidden: true, // Keep the webview's context when it is hidden - }); - let localResourceRoots = vscode.Uri.joinPath(context.extensionUri, 'out').toString(); - // console.log(localResourceRoots); - - const iconPath = path.join(context.extensionPath, 'resources', 'images', 'rt-thread.png'); - panel.iconPath = vscode.Uri.file(iconPath); - - // handle close webview event - panel.onDidDispose(() => { - settingViewPanel = null; - }); - - // update extensionInfo - extensionInfo.version = getExtensionVersion(); - extensionInfo.env.path = path.join(os.homedir(), '.env'); - // if .env/tools/script/env.json exist, to read the version of env script - if (fs.existsSync(path.join(extensionInfo.env.path, 'tools', 'scripts', 'env.json'))) { - let envInfo = readJsonObject(path.join(extensionInfo.env.path, 'tools', 'scripts', 'env.json')); - extensionInfo.env.version = envInfo.version; - } - extensionInfo.SDKConfig = readJsonObject(sdkCfgFn); - - // read RT-Thread folder - let cfg:any [] = readJsonObject(cfgFn); - if (cfg.length > 0 && cfg[0].path) { - extensionInfo.configInfo[0].path = cfg[0].path; - } - else { - // 设置为空字符串而不是字符串'undefined' - extensionInfo.configInfo[0].path = ''; - } - - // 初始化时检查 Env 状态 - checkEnvStatus().then(envStatus => { - panel.webview.postMessage({ command: 'envStatus', status: envStatus }); - }); - // 初始化时发送SDK配置 - if (extensionInfo.SDKConfig) { - panel.webview.postMessage({ command: 'setSDKConfig', data: extensionInfo.SDKConfig }); + const rootDir = path.join(context.extensionPath, 'out'); + const panel = vscode.window.createWebviewPanel('webview', 'RT-Thread Setting', vscode.ViewColumn.One, { + enableScripts: true, + localResourceRoots: [vscode.Uri.file(rootDir)], + retainContextWhenHidden: true, + }); + settingViewPanel = panel; + panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources', 'images', 'RT-Thread.png'); + panel.onDidDispose(() => { + settingViewPanel = undefined; + }, undefined, context.subscriptions); + + panel.webview.onDidReceiveMessage(async message => { + if (handleEnvMessage(panel.webview, message)) { + return; } - - // 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()}"`; - }); - - // 在HTML设置完成后延迟发送初始数据 - setTimeout(() => { - // 发送SDK配置 - if (extensionInfo.SDKConfig) { - panel.webview.postMessage({ command: 'setSDKConfig', data: extensionInfo.SDKConfig }); - } - // 发送扩展信息 - panel.webview.postMessage({command: 'extensionInfo', data: extensionInfo}); - }, 100); - }); - panel.webview.onDidReceiveMessage(async (message) => { - // 先尝试使用SDK处理器处理消息 - if (handleSDKMessage(panel.webview, message)) { - return; - } - - // 尝试使用Env处理器处理消息 - if (handleEnvMessage(panel.webview, message)) { - return; - } - - let data : any = {}; - let defaultPath:any; - + try { switch (message.command) { case 'getExtensionInfo': - panel.webview.postMessage({command: 'extensionInfo', data: extensionInfo}); - return ; - case 'browseToolchainFolder': - defaultPath = message.args[0]; - let folder = await openFolder(defaultPath); + postSettingInfo(context, panel); + break; + case 'browseItemFolder': { + const defaultPath = message.args?.[0]; + const folder = await openFolder(typeof defaultPath === 'string' && defaultPath ? defaultPath : undefined); if (folder) { - panel.webview.postMessage({command: 'setToolchainFolder', data: folder}); + panel.webview.postMessage({ command: 'setItemFolder', data: folder }); } - return; - case 'browseItemFolder': - defaultPath = message.args[0]; - let itemFolder = await openFolder(defaultPath); - if (itemFolder) { - panel.webview.postMessage({command: 'setItemFolder', data: itemFolder}); - } - return; - - // configuration for toolchains - case 'getSDkConfig': - data = readJsonObject(sdkCfgFn); - if (data) { - panel.webview.postMessage({command: 'setSDKConfig', data: data}); - } - return ; - case 'setSDKConfig': - data = message.args[0]; - writeJsonObject(message.args[0], sdkCfgFn); - // 更新内存中的配置 - extensionInfo.SDKConfig = data; - - vscode.window.showInformationMessage('保存工具链配置成功'); - return ; - - // configuration for RT-Thread folder + break; + } case 'getConfig': - data = readJsonObject(cfgFn); - if (data) { - panel.webview.postMessage({command: 'setConfig', data: data}); + panel.webview.postMessage({ command: 'setConfig', data: readConfig() }); + break; + case 'setConfig': { + const config = message.args?.[0]?.[0]; + if (!config || config.name !== 'RT-Thread' || typeof config.path !== 'string') { + throw new Error('RT-Thread 根目录配置无效。'); } - return ; - case 'setConfig': - data = message.args[0]; - writeJsonObject(message.args[0], cfgFn); - - vscode.window.showInformationMessage('保存路径配置成功'); - // update configData - if (data && data.length > 0 && data[0].path) { - extensionInfo.configInfo[0].path = data[0].path; - } else { - extensionInfo.configInfo[0].path = ''; + if (!fs.existsSync(getEnvROOT())) { + throw new Error('请先安装 Env,再保存 RT-Thread 根目录配置。'); } - return ; - - case 'getBoardReadme': - data = message.args[0]; - let readme = readReadmeFile('d:/workspace/rt-thread/bsp/qemu-vexpress-a9/README.md'); - if (readme) { - panel.webview.postMessage({command: 'setBoardReadme', data: readme}); - } - - return ; - - case 'showMessage': - data = message.args[0]; - vscode.window.showInformationMessage(data); - return ; + const configs = readConfig().filter(item => item.name !== 'RT-Thread'); + configs.unshift({ name: config.name, path: config.path, description: config.description }); + writeJsonObject(configs, path.join(getEnvROOT(), 'cfg.json')); + panel.webview.postMessage({ command: 'setConfig', data: configs }); + vscode.window.showInformationMessage('保存路径配置成功'); + break; + } } - }, undefined, context.subscriptions); - panel.onDidChangeViewState((e) => { - if (e.webviewPanel.visible) { - panel.webview.postMessage({command: 'extensionInfo', data: extensionInfo}); + } catch (error) { + if (message.command === 'setConfig') { + panel.webview.postMessage({ command: 'configSaveFailed' }); } - }); - - settingViewPanel = panel; + vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)); + } + }, undefined, context.subscriptions); + + try { + const buffer = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(context.extensionUri, 'out', 'setting', 'index.html')); + if (settingViewPanel === panel) { + panel.webview.html = buffer.toString().replace(/"[\w\-\.\/]+?\.(?:css|js)"/ig, str => { + const resource = vscode.Uri.file(path.join(rootDir, str.slice(1, -1))); + return `"${panel.webview.asWebviewUri(resource).toString()}"`; + }); + } + } catch (error) { + panel.dispose(); + vscode.window.showErrorMessage(`无法打开 RT-Thread Setting:${error instanceof Error ? error.message : String(error)}`); } - - postMessageExtensionData(context, settingViewPanel); - return settingViewPanel; } diff --git a/src/webviews/settings_env.ts b/src/webviews/settings_env.ts index f258d07..87bac1d 100644 --- a/src/webviews/settings_env.ts +++ b/src/webviews/settings_env.ts @@ -232,14 +232,11 @@ export async function updateEnvFunction(webview: vscode.Webview) { const homeDir = os.homedir(); const scriptsDir = path.join(homeDir, '.env', 'tools', 'scripts'); const packagesDir = path.join(homeDir, '.env', 'packages', 'packages'); - const sdkDir = path.join(homeDir, '.env', 'packages', 'sdk'); try { webview.postMessage({ command: 'installProgress', type: 'info', message: '正在更新 ~/.env/tools/scripts ...' }); await executeCommand('git pull origin', webview, scriptsDir); webview.postMessage({ command: 'installProgress', type: 'info', message: '正在更新 ~/.env/packages/packages ...' }); await executeCommand('git pull origin', webview, packagesDir); - webview.postMessage({ command: 'installProgress', type: 'info', message: '正在更新 ~/.env/packages/sdk ...' }); - await executeCommand('git pull origin', webview, sdkDir); webview.postMessage({ command: 'installProgress', type: 'success', message: 'Env 更新完成!' }); } catch (error) { webview.postMessage({ command: 'installProgress', type: 'error', message: `Env 更新失败: ${error}` }); @@ -410,4 +407,4 @@ export function handleEnvMessage(webview: vscode.Webview, message: any): boolean return true; } return false; -} \ No newline at end of file +} diff --git a/src/webviews/settings_sdk.ts b/src/webviews/settings_sdk.ts deleted file mode 100644 index 5cc726e..0000000 --- a/src/webviews/settings_sdk.ts +++ /dev/null @@ -1,458 +0,0 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { spawn } from 'child_process'; - -// SDK版本信息接口 -interface SDKVersion { - version: string; - URL: string; -} - -// SDK包信息接口 -interface SDKPackage { - name: string; - description: string; - site: SDKVersion[]; -} - -// SDK配置信息接口 -interface SDKConfig { - name: string; - installed: boolean; - installedVersion?: string; - path?: string; -} - -// SDK列表项接口 -interface SDKListItem { - name: string; - description: string; - versions: string[]; - installedVersion?: string; - installed: boolean; - path?: string; -} - -// 获取SDK目录路径 -function getSDKDirectory(): string { - const envPath = path.join(os.homedir(), '.env', 'packages', 'sdk'); - const platform = os.platform(); - - if (platform === 'linux') { - return path.join(envPath, 'Linux'); - } else if (platform === 'win32') { - return path.join(envPath, 'Windows'); - } - - return envPath; -} - -// 获取配置文件路径 -function getConfigPath(): string { - return path.join(os.homedir(), '.env', 'tools', 'scripts', '.config'); -} - -// 读取SDK包信息 -async function readSDKPackage(sdkPath: string): Promise { - try { - const packageJsonPath = path.join(sdkPath, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - return null; - } - - const content = fs.readFileSync(packageJsonPath, 'utf8'); - const packageData = JSON.parse(content); - - return { - name: packageData.name || path.basename(sdkPath), - description: packageData.description || '', - site: packageData.site || [] - }; - } catch (error) { - console.error(`Failed to read package.json from ${sdkPath}:`, error); - return null; - } -} - -// 读取.config文件中的SDK配置 -function readSDKConfigs(): Map { - const configs = new Map(); - const configPath = getConfigPath(); - - if (!fs.existsSync(configPath)) { - return configs; - } - - try { - const content = fs.readFileSync(configPath, 'utf8'); - const lines = content.split('\n'); - - // 解析配置文件 - const configData: { [key: string]: string } = {}; - for (const line of lines) { - const match = line.match(/^(CONFIG_[A-Z0-9_]+)=(.*)$/); - if (match) { - const key = match[1]; - const value = match[2].replace(/^"(.*)"$/, '$1'); // 去除引号 - configData[key] = value; - } - } - - // 查找所有已安装的SDK - for (const key in configData) { - if (key.startsWith('CONFIG_PKG_USING_') && configData[key] === 'y') { - const upperName = key.substring('CONFIG_PKG_USING_'.length); - // 将下划线转换回连字符,并转为小写 - const sdkName = upperName.replace(/_/g, '-').toLowerCase(); - - const config: SDKConfig = { - name: sdkName, - installed: true - }; - - // 获取版本 - const versionKey = `CONFIG_PKG_${upperName}_VER`; - if (configData[versionKey]) { - config.installedVersion = configData[versionKey]; - } - - // 获取路径 - const pathKey = `CONFIG_PKG_${upperName}_PATH`; - if (configData[pathKey]) { - config.path = configData[pathKey]; - } - - configs.set(sdkName, config); - } - } - } catch (error) { - console.error('Failed to read .config file:', error); - } - - return configs; -} - -// 获取SDK列表 -export async function getSDKList(): Promise { - const sdkDir = getSDKDirectory(); - const sdkList: SDKListItem[] = []; - - if (!fs.existsSync(sdkDir)) { - console.warn(`SDK directory not found: ${sdkDir}`); - // 返回空列表 - return []; - } - - // 读取已安装的SDK配置 - const installedConfigs = readSDKConfigs(); - - // 扫描SDK目录 - const sdkDirs = fs.readdirSync(sdkDir).filter(name => { - const fullPath = path.join(sdkDir, name); - return fs.statSync(fullPath).isDirectory(); - }); - - for (const sdkName of sdkDirs) { - const sdkPath = path.join(sdkDir, sdkName); - const packageInfo = await readSDKPackage(sdkPath); - - if (packageInfo) { - const config = installedConfigs.get(sdkName.toLowerCase()); - const versions = packageInfo.site.map(s => s.version); - - sdkList.push({ - name: sdkName, - description: packageInfo.description, - versions: versions, - installed: config?.installed || false, - installedVersion: config?.installedVersion, - path: config?.path - }); - } - } - - return sdkList; -} - -// 更新.config文件 -export async function updateSDKConfig(configs: Array<{ name: string; version: string; install: boolean }>): Promise { - const configPath = getConfigPath(); - let content = ''; - - // 读取现有配置 - if (fs.existsSync(configPath)) { - content = fs.readFileSync(configPath, 'utf8'); - } else { - // 创建默认配置头 - content = 'CONFIG_TARGET_FILE=""\n'; - } - - // 更新配置 - for (const config of configs) { - const upperName = config.name.replace(/-/g, '_').toUpperCase(); - const usingKey = `CONFIG_PKG_USING_${upperName}`; - const versionKey = `CONFIG_PKG_${upperName}_VER`; - const pathKey = `CONFIG_PKG_${upperName}_PATH`; - - if (config.install) { - // 添加或更新配置 - content = updateConfigLine(content, usingKey, 'y'); - content = updateConfigLine(content, versionKey, `"${config.version}"`); - - // 设置默认路径(可以根据实际需求调整) - const defaultPath = path.join(os.homedir(), '.env', 'tools', config.name); - content = updateConfigLine(content, pathKey, `"${defaultPath}"`); - } else { - // 移除配置 - content = removeConfigLine(content, usingKey); - content = removeConfigLine(content, versionKey); - content = removeConfigLine(content, pathKey); - } - } - - // 确保目录存在 - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - // 写入配置文件 - fs.writeFileSync(configPath, content, 'utf8'); -} - -// 更新配置行 -function updateConfigLine(content: string, key: string, value: string): string { - const regex = new RegExp(`^${key}=.*$`, 'm'); - const newLine = `${key}=${value}`; - - if (regex.test(content)) { - // 更新现有行 - return content.replace(regex, newLine); - } else { - // 添加新行 - return content + '\n' + newLine; - } -} - -// 移除配置行 -function removeConfigLine(content: string, key: string): string { - const regex = new RegExp(`^${key}=.*$\\n?`, 'gm'); - return content.replace(regex, ''); -} - -// 保存完整的SDK配置到.config文件 -export async function saveSDKDotConfig(configs: Array<{ name: string; version: string; selected: boolean; path: string; versions?: string[] }>): Promise { - const configPath = getConfigPath(); - let content = 'CONFIG_TARGET_FILE=""\n'; - - // 生成所有SDK的配置 - for (const config of configs) { - const upperName = config.name.replace(/-/g, '_').toUpperCase(); - const usingKey = `CONFIG_PKG_USING_${upperName}`; - - if (config.selected && config.version) { - // SDK已选择 - content += `${usingKey}=y\n`; - - // 设置路径 - 使用相对路径格式 - const platform = os.platform() === 'win32' ? 'Windows' : 'Linux'; - const sdkPath = config.path || `sdk/${platform}/${config.name}`; - content += `CONFIG_PKG_${upperName}_PATH="${sdkPath}"\n`; - - // 添加版本选择标记 - if (config.versions && config.versions.length > 0) { - for (const ver of config.versions) { - // 将版本号转换为大写,移除点和横线 - const versionKey = `CONFIG_PKG_USING_${upperName}_${ver.replace(/[\.-]/g, '').toUpperCase()}`; - if (ver === config.version) { - content += `${versionKey}=y\n`; - } else { - content += `# ${versionKey} is not set\n`; - } - } - } - - // 设置版本 - content += `CONFIG_PKG_${upperName}_VER="${config.version}"\n`; - } else { - // SDK未选择 - content += `# ${usingKey} is not set\n`; - } - } - - // 确保目录存在 - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - // 写入配置文件 - fs.writeFileSync(configPath, content, 'utf8'); -} - -// 当前运行的env进程 -let currentEnvProcess: any = null; - -// 执行 env.py pkgs --update-force 命令 -export async function runEnvPkgsUpdate(webview: vscode.Webview): Promise { - const envScriptPath = path.join(os.homedir(), '.env', 'tools', 'scripts', 'env.py'); - - console.log('Running env.py from:', envScriptPath); - - // 检查脚本是否存在 - if (!fs.existsSync(envScriptPath)) { - const errorMsg = `Error: env.py script not found at ${envScriptPath}`; - console.error(errorMsg); - webview.postMessage({ - command: 'sdkUpdateLog', - log: errorMsg + '\n' - }); - webview.postMessage({ - command: 'sdkUpdateFinished', - success: false - }); - return; - } - - // 发送开始消息 - webview.postMessage({ - command: 'sdkUpdateStarted' - }); - - // 执行脚本 - const pythonCmd = os.platform() === 'win32' ? 'python' : 'python3'; - currentEnvProcess = spawn(pythonCmd, [envScriptPath, 'pkgs', '--update-force'], { - cwd: path.dirname(envScriptPath), - env: { ...process.env } - }); - - // 实时输出日志 - currentEnvProcess.stdout.on('data', (data: Buffer) => { - webview.postMessage({ - command: 'sdkUpdateLog', - log: data.toString() - }); - }); - - currentEnvProcess.stderr.on('data', (data: Buffer) => { - webview.postMessage({ - command: 'sdkUpdateLog', - log: data.toString() - }); - }); - - // 进程结束 - currentEnvProcess.on('close', (code: number | null) => { - currentEnvProcess = null; - webview.postMessage({ - command: 'sdkUpdateFinished', - success: code === 0 - }); - }); - - currentEnvProcess.on('error', (error: Error) => { - currentEnvProcess = null; - webview.postMessage({ - command: 'sdkUpdateLog', - log: `Error: ${error.message}` - }); - webview.postMessage({ - command: 'sdkUpdateFinished', - success: false - }); - }); -} - -// 取消SDK更新 -export function cancelSDKUpdate(): void { - if (currentEnvProcess) { - console.log('Cancelling SDK update process...'); - // 尝试正常终止进程 - currentEnvProcess.kill('SIGTERM'); - - // 如果在Windows上,使用taskkill - if (os.platform() === 'win32') { - spawn('taskkill', ['/F', '/T', '/PID', currentEnvProcess.pid.toString()]); - } - - // 5秒后强制终止 - setTimeout(() => { - if (currentEnvProcess) { - currentEnvProcess.kill('SIGKILL'); - currentEnvProcess = null; - } - }, 5000); - } -} - -// 处理SDK相关消息 -export function handleSDKMessage(webview: vscode.Webview, message: any): boolean { - switch (message.command) { - case 'getSDKList': - getSDKList().then(sdkList => { - webview.postMessage({ - command: 'setSDKList', - data: sdkList - }); - }).catch(error => { - console.error('Failed to get SDK list:', error); - webview.postMessage({ - command: 'sdkConfigError', - error: '获取SDK列表失败' - }); - }); - return true; - - case 'saveSDKDotConfig': - if (message.args && message.args[0] && Array.isArray(message.args[0])) { - saveSDKDotConfig(message.args[0]).then(() => { - // vscode.window.showInformationMessage('SDK配置已保存到 .config 文件'); - // 执行 env.py pkgs --update-force - runEnvPkgsUpdate(webview); - }).catch(error => { - console.error('Failed to save SDK config:', error); - vscode.window.showErrorMessage(`保存SDK配置失败: ${error.message}`); - webview.postMessage({ - command: 'sdkConfigError', - error: `保存SDK配置失败: ${error.message}` - }); - }); - } else { - console.error('Invalid data format for saveSDKDotConfig:', message); - vscode.window.showErrorMessage('SDK配置数据格式错误'); - webview.postMessage({ - command: 'sdkConfigError', - error: 'SDK配置数据格式错误' - }); - } - return true; - - case 'applySDKConfig': - if (message.data && Array.isArray(message.data)) { - updateSDKConfig(message.data).then(() => { - webview.postMessage({ - command: 'sdkConfigApplied' - }); - - // TODO: 触发实际的SDK安装/卸载操作 - // 这里可以调用env工具的相关命令 - - }).catch(error => { - console.error('Failed to update SDK config:', error); - webview.postMessage({ - command: 'sdkConfigError', - error: '更新SDK配置失败' - }); - }); - } - return true; - - case 'cancelSDKUpdate': - cancelSDKUpdate(); - return true; - } - - return false; -} \ No newline at end of file From 1469e935d8113ec53a362c7a6835ccdfca773ede Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 26 Oct 2026 11:46:06 +0800 Subject: [PATCH 2/3] Update extension packaging configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 递增扩展版本至 0.4.14,移除不再使用的 Vue Router 依赖并同步前端构建入口。 调整 VSIX 依赖白名单,确保内置 Markdown 渲染资源完整且保持精简。 --- .vscodeignore | 17 ++++++++++++----- package-lock.json | 29 ++++------------------------- package.json | 14 +++++++++----- src/vue/package.json | 1 + src/vue/vite.config.ts | 2 +- tsconfig.json | 2 +- 6 files changed, 28 insertions(+), 37 deletions(-) diff --git a/.vscodeignore b/.vscodeignore index f270aab..fc3f686 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,6 +1,9 @@ .vscode/** .vscode-test/** src/** +out/test/** +AGENTS.md +CLAUDE.md ref/** .gitignore .yarnrc @@ -20,13 +23,17 @@ vsc-extension-quickstart.md !node_modules/mdurl/** !node_modules/punycode.js/** !node_modules/uc.micro/** -!node_modules/highlight.js/** +# Include only the core and languages registered by the Markdown renderer. +!node_modules/highlight.js/package.json +!node_modules/highlight.js/LICENSE +!node_modules/highlight.js/lib/core.js +!node_modules/highlight.js/lib/languages/c.js +!node_modules/highlight.js/lib/languages/cpp.js +!node_modules/highlight.js/lib/languages/python.js +!node_modules/highlight.js/lib/languages/javascript.js +!node_modules/highlight.js/lib/languages/typescript.js # Exclude unnecessary files from included packages to reduce size -node_modules/highlight.js/**/*.md -node_modules/highlight.js/styles/** -node_modules/highlight.js/scss/** -node_modules/highlight.js/es/** node_modules/markdown-it*/**/*.md **/.git/** **/package-lock.json diff --git a/package-lock.json b/package-lock.json index 1c1b9a8..8a65b45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rt-thread-smart", - "version": "0.4.13", + "version": "0.4.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rt-thread-smart", - "version": "0.4.13", + "version": "0.4.14", "workspaces": [ "src/vue" ], @@ -21,8 +21,7 @@ "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", "markdown-it-task-lists": "^2.1.1", - "vue": "^3.3.8", - "vue-router": "^4.5.0" + "vue": "^3.3.8" }, "devDependencies": { "@types/jest": "^29.5.12", @@ -2921,12 +2920,6 @@ "he": "^1.2.0" } }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, "node_modules/@vue/language-core": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", @@ -9078,21 +9071,6 @@ } } }, - "node_modules/vue-router": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", - "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", @@ -9437,6 +9415,7 @@ "name": "smart-vue", "version": "0.0.1", "dependencies": { + "@element-plus/icons-vue": "^2.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0" } diff --git a/package.json b/package.json index 0e918fa..7f43963 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "displayName": "RT-Thread Smart Extension", "description": "Smart Helper Extension for RT-Thread Development", "icon": "resources/logo.png", - "version": "0.4.13", + "version": "0.4.14", "repository": { "url": "https://github.com/RT-Thread/vscode-rt-smart" }, @@ -20,7 +20,8 @@ ], "activationEvents": [ "workspaceContains:rtconfig.h", - "onView:rt-thread" + "onView:rt-thread", + "onUri" ], "main": "./out/extension.js", "contributes": { @@ -61,6 +62,10 @@ "command": "extension.showSetting", "title": "Show RT-Thread Setting" }, + { + "command": "extension.openWebUrl", + "title": "RT-Thread: Open Web URL in Editor" + }, { "command": "extension.showCreateProject", "title": "Create RT-Thread Project" @@ -241,7 +246,7 @@ ] }, "scripts": { - "package:vs": "vsce package --no-dependencies", + "package:vs": "vsce package", "vscode:prepublish": "npm run compile", "build:vue": "npm run build --workspace=smart-vue", "compile": "npm run build:vue && tsc -p ./", @@ -261,8 +266,7 @@ "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", "markdown-it-task-lists": "^2.1.1", - "vue": "^3.3.8", - "vue-router": "^4.5.0" + "vue": "^3.3.8" }, "extensionDependencies": [ "ms-python.python" diff --git a/src/vue/package.json b/src/vue/package.json index 59c0e1d..7c76a27 100644 --- a/src/vue/package.json +++ b/src/vue/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@element-plus/icons-vue": "^2.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0" } diff --git a/src/vue/vite.config.ts b/src/vue/vite.config.ts index b885abe..d141736 100644 --- a/src/vue/vite.config.ts +++ b/src/vue/vite.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ }), Components({ resolvers: [ElementPlusResolver()], + types: [], }), ], build: { @@ -20,7 +21,6 @@ export default defineConfig({ rollupOptions: { input: { about: resolve(__dirname, 'about/index.html'), - analyze: resolve(__dirname, 'analyze/index.html'), setting: resolve(__dirname, 'setting/index.html'), projects: resolve(__dirname, 'projects/index.html'), 'create-project': resolve(__dirname, 'create-project/index.html'), diff --git a/tsconfig.json b/tsconfig.json index 99bbe26..72c0287 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "rootDir": "src", "strict": true, }, - "exclude": ["node_modules", "out", "src/vue", "src/test", "ref", "third_party"] + "exclude": ["node_modules", "out", "src/vue", "ref", "third_party"] } From a9bd253ed6da3063c95f85b327f0466b6743f929 Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 26 Oct 2026 11:49:01 +0800 Subject: [PATCH 3/3] Document the slimmed extension workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 README 和变更日志,说明 Env 设置、移除的页面功能及版本 0.4.14。 补充编辑器内网页入口和 RT-Thread Setting 使用文档,删除过时的配置说明。 --- CHANGELOG.md | 6 + README.md | 10 ++ README_settings.md | 291 ---------------------------------------- docs/README.md | 4 + docs/browser/README.md | 46 +++++++ docs/settings/README.md | 28 ++++ 6 files changed, 94 insertions(+), 291 deletions(-) delete mode 100644 README_settings.md create mode 100644 docs/README.md create mode 100644 docs/browser/README.md create mode 100644 docs/settings/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c94f92..fe2497c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +**v0.4.14** +- 移除 Kconfig webview、SDK 管理器、工具链设置及 Symbolic Analysis。 +- 设置页聚焦 Env 管理,保留工程根目录配置和终端 menuconfig。 +- 新增编辑器内网页命令及命令行 URI 入口,兼容原有登录回调。 +- 修正运行时依赖打包,精简高亮库并排除测试文件。 + **v0.4.10** - 加入Project Files功能,可以显示RT-Thread目录以及Groups - 需要RT-Thread升级到最新版本,在生成vscode的配置时,也同时生成project.json文件,用于显示目录结构 diff --git a/README.md b/README.md index 07ee167..3fe8d05 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,22 @@ RT-Thread VSCode扩展是一款专为RT-Thread及RT-Thread Smart (版本>5.2.0) 开发的VSCode辅助工具。该插件遵循简化与灵活扩展的设计原则,旨在最小化对VSCode干预,同时允许用户利用RT-Thread的脚本功能来实现个性化定制。 +当前设置页面聚焦 Env 的安装、更新和状态,保留创建工程所需的 RT-Thread 根目录配置。SDK 管理器、工具链设置、Kconfig webview 和 Symbolic Analysis 已移除;终端 menuconfig 及已有 Kconfig 扩展集成保留。 + +网页可通过 `RT-Thread: Open Web URL in Editor` 命令或 `code --open-url` 在编辑器内打开。详细用法见 [项目文档](docs/README.md)。 + ### env脚本状态 虚拟Python环境安装状态: **${status}** ### 版本说明 +**v0.4.14** +- 移除 Kconfig webview、SDK 管理器、工具链设置和 Symbolic Analysis; +- RT-Thread Setting 聚焦 Env 安装、更新及状态,保留工程根目录配置; +- 新增编辑器内网页命令及 `code --open-url` 入口; +- 修正 VSIX 运行时依赖打包,并精简高亮库文件。 + **v0.4.13** - 添加aichat界面 diff --git a/README_settings.md b/README_settings.md deleted file mode 100644 index a776cf7..0000000 --- a/README_settings.md +++ /dev/null @@ -1,291 +0,0 @@ -# RT-Thread Smart Extension 配置文件说明 - -本文档详细说明了 RT-Thread Smart Extension 中使用的各种 JSON 配置文件的结构和字段含义。 - -## 目录 - -1. [工作区相关配置文件](#工作区相关配置文件) -2. [环境配置文件](#环境配置文件) -3. [资源配置文件](#资源配置文件) - ---- - -## 工作区相关配置文件 - -这些配置文件位于项目工作区的 `.vscode` 目录下,用于管理项目结构和工作区设置。 - -### 1. project.json - -**文件路径**: `{workspace}/.vscode/project.json` - -**用途**: 存储单个 RT-Thread 项目的结构信息,包括分组和文件组织。 - -**结构说明**: - -```json -{ - "RT-Thread": "项目根目录路径", - "Groups": [ - { - "name": "分组名称", - "files": [ - "文件路径1", - "文件路径2" - ] - } - ] -} -``` - -**字段详解**: - -- **RT-Thread** (string): RT-Thread 项目的根目录路径 - - 用于指定项目的主要源码目录 - - 可以是绝对路径或相对于工作区的相对路径 - -- **Groups** (array): 项目文件分组数组 - - **name** (string): 分组名称,在项目树中显示 - - **files** (array): 该分组包含的文件路径列表 - - 支持相对路径(相对于工作区根目录) - - 支持以 `..` 开头的相对路径 - - 文件路径将自动解析为绝对路径 - -**使用场景**: -- 由 scons 构建工具生成,用于在 VS Code 中显示项目文件结构 -- 支持文件分组管理,方便代码组织和浏览 -- 在项目树视图中按分组显示源文件 - -### 2. workspace.json - -**文件路径**: `{workspace}/.vscode/workspace.json` - -**用途**: 管理多个 BSP(Board Support Package)项目的工作区配置。 - -**结构说明**: - -```json -{ - "currentProject": "当前激活的项目路径", - "bsps": { - "folder": "BSP目录相对路径", - "stars": [ - "bsp项目路径1", - "bsp项目路径2" - ] - } -} -``` - -**字段详解**: - -- **currentProject** (string): 当前激活的 BSP 项目路径 - - 指定当前工作的 BSP 项目 - - 用于在多个 BSP 项目之间切换 - - 路径相对于 bsps.folder 指定的目录 - -- **bsps** (object): BSP 项目配置 - - **folder** (string): BSP 项目的根目录,相对于工作区根目录 - - **stars** (array): 收藏的/可用的 BSP 项目列表 - - 每个项目路径相对于 `bsps.folder` - - 支持嵌套目录结构(如 `stm32/stm32f407-rt-spark`) - -**使用场景**: -- 工作区模式下管理多个 BSP 项目 -- 在项目树中显示可用的 BSP 项目列表 -- 支持项目切换和状态保存 - -### 3. rtthread.json - -**文件路径**: `{workspace}/.vscode/rtthread.json` - -**用途**: 存储 RT-Thread BSP 项目的板级信息和配置。 - -**结构说明**: - -```json -{ - "board_info": { - "name": "开发板名称", - "description": "开发板描述", - "manufacturer": "制造商", - "board": "板型标识", - "path": "BSP路径" - } -} -``` - -**字段详解**: - -- **board_info** (object): 开发板信息 - - **name** (string): 开发板的显示名称 - - **description** (string): 开发板的详细描述 - - **manufacturer** (string): 开发板制造商 - - **board** (string): 开发板的标识符,通常与目录名对应 - - **path** (string): BSP 在 RT-Thread 源码中的相对路径 - -**使用场景**: -- 在创建项目界面显示开发板信息 -- 为特定 BSP 项目提供元数据 -- 与 `resources/bi.json` 中的板级信息对应 - ---- - -## 环境配置文件 - -这些配置文件位于用户主目录的 `.env` 文件夹下,用于管理 RT-Thread 开发环境。 - -### 1. cfg.json - -**文件路径**: `~/.env/cfg.json` - -**用途**: 存储 RT-Thread 主干路径配置。 - -**结构说明**: - -```json -[ - { - "name": "配置项名称", - "path": "RT-Thread主干路径", - "description": "配置项描述" - } -] -``` - -**字段详解**: - -- **name** (string): 配置项的名称,通常为 "RT-Thread" -- **path** (string): RT-Thread 主干代码的本地路径 - - 指向 RT-Thread 源码仓库的根目录 - - 用于访问 BSP 和系统源码 -- **description** (string): 配置项的描述信息 - -**使用场景**: -- 在设置界面配置 RT-Thread 源码路径 -- 创建项目时定位 BSP 目录 -- 提供系统源码访问路径 - -### 2. sdk_cfg.json - -**文件路径**: `~/.env/tools/scripts/sdk_cfg.json` - -**用途**: 存储工具链(SDK)配置信息。 - -**结构说明**: - -```json -[ - { - "name": "工具链名称", - "path": "工具链路径", - "description": "工具链描述" - } -] -``` - -**字段详解**: - -- **name** (string): 工具链的名称 - - 例如: "arm-none-eabi-gcc", "armcc", "iar" 等 -- **path** (string): 工具链的安装路径 - - 指向编译器的 bin 目录或根目录 - - 用于构建时定位编译工具 -- **description** (string): 工具链的描述信息 - - 例如: "ARM GNU GCC", "Keil MDK-ARM" 等 - -**使用场景**: -- 在设置界面管理多个工具链 -- 为不同项目选择合适的编译工具链 -- 自动配置构建环境 - -### 3. env.json - -**文件路径**: `~/.env/tools/scripts/env.json` - -**用途**: 存储环境脚本的版本信息。这个文件在env脚本git仓库中就默认携带。 - -**结构说明**: - -```json -{ - "version": "环境脚本版本号" -} -``` - -**字段详解**: - -- **version** (string): 环境脚本的版本号 - - 格式通常为语义化版本,如 "v2.0.1" - - 用于显示和检查环境脚本更新 - -**使用场景**: -- 在设置界面显示当前环境版本 -- 检查环境更新状态 -- 版本兼容性检查 - ---- - -## 资源配置文件 - -### 1. bi.json (Board Information) - -**文件路径**: `resources/bi.json` - -**用途**: 创建工程时支持的开发板信息数据库。 - -**结构说明**: - -```json -[ - { - "manufacturer": "制造商名称", - "boards": [ - { - "name": "开发板名称", - "description": "开发板描述", - "board": "板型标识", - "path": "BSP相对路径" - } - ] - } -] -``` - -**字段详解**: - -- **manufacturer** (string): 开发板制造商名称 - - 例如: "ST", "QEMU", "Raspberry Pi" 等 - - 用于在创建项目界面按制造商分类 - -- **boards** (array): 该制造商的开发板列表 - - **name** (string): 开发板的显示名称 - - **description** (string): 开发板的详细描述和特性 - - **board** (string): 开发板的唯一标识符 - - **path** (string): 在 RT-Thread 源码中的 BSP 相对路径 - -**使用场景**: -- 在创建项目向导中显示可用开发板 -- 按制造商分类显示开发板选项 -- 提供开发板详细信息和 BSP 路径映射 - ---- - -## 配置文件生命周期 - -1. **安装阶段**: 创建 `~/.env/` 目录和基础配置文件 -2. **配置阶段**: 用户通过设置界面配置 `cfg.json` 和 `sdk_cfg.json` -3. **项目创建**: 生成 `.vscode/rtthread.json` 和相关配置 -4. **构建阶段**: scons 生成 `.vscode/project.json` 或 `.vscode/workspace.json` -5. **运行阶段**: 扩展读取所有配置文件以提供功能 - -## 注意事项 - -1. **路径处理**: 所有相对路径都会被解析为绝对路径 -2. **文件权限**: 确保配置文件具有适当的读写权限 -3. **版本兼容**: 不同版本的扩展可能使用不同的配置格式 -4. **备份建议**: 重要配置文件建议定期备份 -5. **错误处理**: 配置文件损坏时,扩展会使用默认配置或显示错误提示 - ---- - -*此文档基于 RT-Thread Smart Extension v0.4.12 版本编写* diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c4a3539 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# RT-Thread Smart 扩展文档 + +- [RT-Thread Setting](settings/README.md):Env 安装、更新及 RT-Thread 根目录配置。 +- [编辑器内浏览网页](browser/README.md):命令面板、命令行 URI 和兼容性说明。 diff --git a/docs/browser/README.md b/docs/browser/README.md new file mode 100644 index 0000000..e7d037d --- /dev/null +++ b/docs/browser/README.md @@ -0,0 +1,46 @@ +# 在编辑器中打开网页 + +## 命令面板 + +执行 `RT-Thread: Open Web URL in Editor`,输入完整的 HTTP 或 HTTPS 地址。 + +扩展命令为 `extension.openWebUrl`,也可以由其他扩展调用并传入地址字符串: + +```ts +await vscode.commands.executeCommand('extension.openWebUrl', 'http://localhost:3000/'); +``` + +## code 命令行 + +安装并启用本扩展后,可以直接使用 VS Code 的 URI 打开入口,无需打开 RT-Thread 工程: + +```bash +code --open-url "vscode://rt-thread.rt-thread-smart/open-web?url=https%3A%2F%2Fwww.rt-thread.org" +``` + +仅支持一个 `url` 参数。其值必须是完整目标地址经过一次 URL 编码的结果;不要在扩展 URI 上附加其他参数。 + +含查询参数、片段或中文时,推荐程序生成 URI。Linux 示例: + +```bash +code --open-url "$(node -e 'console.log("vscode://rt-thread.rt-thread-smart/open-web?url=" + encodeURIComponent(process.argv[1]))' 'http://localhost:3000/docs?a=1&b=2#intro')" +``` + +Windows PowerShell 示例: + +```powershell +$url = 'http://localhost:3000/docs?a=1&b=2#intro' +$encodedUrl = [Uri]::EscapeDataString($url) +code --open-url "vscode://rt-thread.rt-thread-smart/open-web?url=$encodedUrl" +``` + +VS Code Insiders 应使用 `code-insiders` 命令和 `vscode-insiders://` 协议。 + +## 兼容性与限制 + +- 优先使用可用的集成浏览器命令 `workbench.action.browser.open`,旧版本使用 `simpleBrowser.show`。 +- Simple Browser 无法显示通过 `X-Frame-Options` 或 CSP 禁止嵌入的站点;这不是链接参数错误。 +- 内置浏览器不可用时明确报错,不会自动转到系统浏览器。 +- 只允许 HTTP/HTTPS,拒绝 `command:`、`file:`、`javascript:` 等协议及包含用户名或密码的地址。 +- `/auth-callback` 仍由原认证模块处理,网页入口不会覆盖登录回调。 +- VS Code 的 URI 打开确认提示由编辑器自身控制。 diff --git a/docs/settings/README.md b/docs/settings/README.md new file mode 100644 index 0000000..b1366ee --- /dev/null +++ b/docs/settings/README.md @@ -0,0 +1,28 @@ +# RT-Thread Setting + +## Env 环境 + +从侧边栏 `RT-Thread Setting` 或命令面板 `Show RT-Thread Setting` 打开。 + +- 未安装时显示安装按钮;安装路径为当前用户的 `~/.env`。 +- 已安装时显示版本、Git 修订、更新及删除按钮。 +- 安装和更新在页面中显示执行日志,长时间运行时提供强制关闭入口。 +- 删除需要确认,取消不会触发删除。 +- 更新只涉及 `~/.env/tools/scripts` 和 `~/.env/packages/packages`,不再更新独立 SDK 索引。 + +Windows、Linux 的安装继续使用现有 Env 安装脚本。安装前需准备系统已有的网络下载、Git 和脚本运行环境。删除操作不可恢复,应先备份用户自行保存到 `~/.env` 的内容。 + +## RT-Thread 根目录 + +Env 操作区域下方保留 RT-Thread 根目录配置,供 `Create Project` 使用。 + +- 配置保存于 `~/.env/cfg.json`。 +- 保存前需要先安装 Env,不会因保存配置而创建一个不完整的 `.env` 目录。 +- 配置仍采用数组结构;只更新名称为 `RT-Thread` 的条目,保留其他条目。 +- 工具链路径、SDK 安装及管理不再由本页面负责。 + +## 移除的功能 + +扩展不再提供 Kconfig webview、SDK 管理器、工具链设置或 Symbolic Analysis。终端 menuconfig、已有 Kconfig 扩展集成、构建、软件包命令和工程创建功能保留。 + +现有 `sdk_cfg.json`、SDK 安装目录和工具链文件不会被迁移或删除;扩展不再读取或写入 SDK 配置。