已合并
[feature][ide-plugins] #80 新增 clean 子命令 #78
Qunka创建于 11 天前
[feature][ide-plugins] #80 新增 clean 子命令 #78
已合并
共 7 个文件变更+510-9
| @@ -46,6 +46,11 @@ | |||
| 46 | "light": "images/hammer_light.svg" | 46 | "light": "images/hammer_light.svg" |
| 47 | } | 47 | } |
| 48 | }, | 48 | }, |
| 49 | + { | ||
| 50 | + "command": "cjmp.clean", | ||
| 51 | + "title": "CJMP Clean", | ||
| 52 | + "category": "CJMP" | ||
| 53 | + }, | ||
| 49 | { | 54 | { |
| 50 | "command": "cjmp.selectDevice", | 55 | "command": "cjmp.selectDevice", |
| 51 | "category": "CJMP", | 56 | "category": "CJMP", |
| @@ -67,6 +72,10 @@ | |||
| 67 | "command": "cjmp.build", | 72 | "command": "cjmp.build", |
| 68 | "when": "cjmp.isCjmpProject" | 73 | "when": "cjmp.isCjmpProject" |
| 69 | }, | 74 | }, |
| 75 | + { | ||
| 76 | + "command": "cjmp.clean", | ||
| 77 | + "when": "cjmp.isCjmpProject" | ||
| 78 | + }, | ||
| 70 | { | 79 | { |
| 71 | "command": "cjmp.selectDevice", | 80 | "command": "cjmp.selectDevice", |
| 72 | "when": "cjmp.isCjmpProject" | 81 | "when": "cjmp.isCjmpProject" |
| @@ -0,0 +1,30 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import * as vscode from 'vscode'; | ||
| 17 | + | ||
| 18 | +let cjmpOutputChannel: vscode.OutputChannel | undefined; | ||
| 19 | + | ||
| 20 | +export function getCjmpOutputChannel(): vscode.OutputChannel { | ||
| 21 | + if (!cjmpOutputChannel) { | ||
| 22 | + cjmpOutputChannel = vscode.window.createOutputChannel('CJMP'); | ||
| 23 | + } | ||
| 24 | + return cjmpOutputChannel; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +export function disposeCjmpOutputChannel(): void { | ||
| 28 | + cjmpOutputChannel?.dispose(); | ||
| 29 | + cjmpOutputChannel = undefined; | ||
| 30 | +} | ||
| @@ -15,6 +15,8 @@ | |||
| 15 | 15 | ||
| 16 | import * as vscode from 'vscode'; | 16 | import * as vscode from 'vscode'; |
| 17 | import { buildCjmpProject } from './keelsBuild'; | 17 | import { buildCjmpProject } from './keelsBuild'; |
| 18 | +import { cleanCjmpProject } from './keelsClean'; | ||
| 19 | +import { disposeCjmpOutputChannel } from './cjmpOutputChannel'; | ||
| 18 | import { createCjmpProject, executeSDK } from './keelsCreate'; | 20 | import { createCjmpProject, executeSDK } from './keelsCreate'; |
| 19 | import { LaunchEmulator } from './keelsEmulators'; | 21 | import { LaunchEmulator } from './keelsEmulators'; |
| 20 | import { KeelsDebugAdapterDescriptorFactory, KeelsConfigurationProvider } from './keelsRun'; | 22 | import { KeelsDebugAdapterDescriptorFactory, KeelsConfigurationProvider } from './keelsRun'; |
| @@ -65,6 +67,14 @@ export async function activate(context: vscode.ExtensionContext) { | |||
| 65 | await buildCjmpProject(); | 67 | await buildCjmpProject(); |
| 66 | }); | 68 | }); |
| 67 | 69 | ||
| 70 | + const cleanCommand = vscode.commands.registerCommand('cjmp.clean', async () => { | ||
| 71 | + const isSdkReady = await executeSDK(context); | ||
G | |||
| 72 | + if (!isSdkReady) { | ||
| 73 | + return; | ||
| 74 | + } | ||
| 75 | + await cleanCjmpProject(); | ||
| 76 | + }); | ||
| 77 | + | ||
| 68 | context.subscriptions.push( | 78 | context.subscriptions.push( |
| 69 | vscode.commands.registerCommand( | 79 | vscode.commands.registerCommand( |
| 70 | 'cjmp.selectDevice', | 80 | 'cjmp.selectDevice', |
| @@ -81,8 +91,10 @@ export async function activate(context: vscode.ExtensionContext) { | |||
| 81 | 91 | ||
| 82 | // Dispose commands with the extension. | 92 | // Dispose commands with the extension. |
| 83 | context.subscriptions.push(buildCommand); | 93 | context.subscriptions.push(buildCommand); |
| 94 | + context.subscriptions.push(cleanCommand); | ||
| 84 | context.subscriptions.push(createCommand); | 95 | context.subscriptions.push(createCommand); |
| 85 | context.subscriptions.push(launchCommand); | 96 | context.subscriptions.push(launchCommand); |
| 97 | + context.subscriptions.push({ dispose: disposeCjmpOutputChannel }); | ||
| 86 | context.subscriptions.push(deviceManager); | 98 | context.subscriptions.push(deviceManager); |
| 87 | context.subscriptions.push(vscode.debug.onDidTerminateDebugSession((session) => sessionTerminated(session))); | 99 | context.subscriptions.push(vscode.debug.onDidTerminateDebugSession((session) => sessionTerminated(session))); |
| 88 | context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { | 100 | context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { |
| @@ -0,0 +1,326 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import * as childProcess from 'child_process'; | ||
| 17 | +import * as vscode from 'vscode'; | ||
| 18 | +import { getCjmpProjectWorkspaceFolder } from './cjmpProjectContext'; | ||
| 19 | +import { resolveKeelsCommand } from './cjmpSdk'; | ||
| 20 | +import { getCjmpOutputChannel } from './cjmpOutputChannel'; | ||
| 21 | +import { getProjectType } from './utils'; | ||
| 22 | + | ||
| 23 | +export type CleanPlatform = 'all' | 'android' | 'hos' | 'ios'; | ||
| 24 | +type SpecificCleanPlatform = Exclude<CleanPlatform, 'all'>; | ||
| 25 | + | ||
| 26 | +function isSpecificCleanPlatform(value: CleanPlatform): value is SpecificCleanPlatform { | ||
| 27 | + return value !== 'all'; | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +type PlatformSelection = | ||
| 31 | + | { kind: 'all' } | ||
| 32 | + | { kind: 'specific'; values: readonly SpecificCleanPlatform[] }; | ||
| 33 | +export type CleanMode = 'normal' | 'deep' | 'test-results' | 'deep-test-results'; | ||
| 34 | + | ||
| 35 | +interface CleanPlatformOption extends vscode.QuickPickItem { | ||
| 36 | + value: CleanPlatform; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +interface SpecificCleanPlatformOption extends vscode.QuickPickItem { | ||
| 40 | + value: SpecificCleanPlatform; | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +interface CleanModeOption extends vscode.QuickPickItem { | ||
| 44 | + value: CleanMode; | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +interface CleanCommandContext { | ||
| 48 | + executablePath: string; | ||
| 49 | + environment: NodeJS.ProcessEnv; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +export interface CleanPrompt { | ||
| 53 | + pickPlatforms( | ||
| 54 | + items: readonly CleanPlatformOption[], | ||
| 55 | + options: vscode.QuickPickOptions & { canPickMany: true }, | ||
| 56 | + ): Promise<readonly CleanPlatformOption[] | undefined>; | ||
| 57 | + pickMode( | ||
| 58 | + items: readonly CleanModeOption[], | ||
| 59 | + options: vscode.QuickPickOptions, | ||
| 60 | + ): Promise<CleanModeOption | undefined>; | ||
| 61 | + confirm(message: string): Promise<boolean>; | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +export interface CleanProcessRunner { | ||
| 65 | + run( | ||
| 66 | + executablePath: string, | ||
| 67 | + args: readonly string[], | ||
| 68 | + options: childProcess.SpawnOptionsWithoutStdio, | ||
| 69 | + onOutput: (output: string) => void, | ||
| 70 | + ): Promise<number | null>; | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +export interface CleanDependencies { | ||
| 74 | + prompt: CleanPrompt; | ||
| 75 | + processRunner: CleanProcessRunner; | ||
| 76 | + resolveCommand: () => CleanCommandContext | undefined; | ||
| 77 | + createOutputChannel: () => vscode.OutputChannel; | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +const SUPPORTED_CLEAN_PLATFORMS: readonly SpecificCleanPlatform[] = ['android', 'hos', 'ios']; | ||
| 81 | + | ||
| 82 | +const PLATFORM_OPTIONS: readonly CleanPlatformOption[] = [ | ||
| 83 | + { label: 'All Platforms', value: 'all' }, | ||
| 84 | + { label: 'Android', value: 'android' }, | ||
| 85 | + { label: 'HarmonyOS', value: 'hos' }, | ||
| 86 | + { label: 'iOS', value: 'ios' }, | ||
| 87 | +]; | ||
| 88 | + | ||
| 89 | +const MODE_OPTIONS: readonly CleanModeOption[] = [ | ||
| 90 | + { label: 'Normal Clean', value: 'normal' }, | ||
| 91 | + { label: 'Deep Clean', value: 'deep' }, | ||
| 92 | + { label: 'Clean Including Test Results', value: 'test-results' }, | ||
| 93 | + { label: 'Deep Clean Including Test Results', value: 'deep-test-results' }, | ||
| 94 | +]; | ||
| 95 | + | ||
| 96 | +function normalizePlatformSelection( | ||
| 97 | + selection: readonly CleanPlatformOption[], | ||
| 98 | +): PlatformSelection { | ||
| 99 | + const specificPlatforms = [...new Set( | ||
| 100 | + selection | ||
| 101 | + .map((item) => item.value) | ||
| 102 | + .filter(isSpecificCleanPlatform), | ||
| 103 | + )]; | ||
| 104 | + | ||
| 105 | + return specificPlatforms.length > 0 | ||
| 106 | + ? { kind: 'specific', values: specificPlatforms } | ||
| 107 | + : { kind: 'all' }; | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +function getSelectedPlatformOptions( | ||
| 111 | + selection: PlatformSelection, | ||
| 112 | + allOption: CleanPlatformOption, | ||
| 113 | + specificOptions: readonly SpecificCleanPlatformOption[], | ||
| 114 | +): readonly CleanPlatformOption[] { | ||
| 115 | + if (selection.kind === 'all') { | ||
| 116 | + return [allOption]; | ||
| 117 | + } | ||
| 118 | + return specificOptions.filter((item) => selection.values.includes(item.value)); | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +function hasSamePlatformOptions( | ||
| 122 | + current: readonly CleanPlatformOption[], | ||
| 123 | + expected: readonly CleanPlatformOption[], | ||
| 124 | +): boolean { | ||
| 125 | + return current.length === expected.length | ||
| 126 | + && current.every((item) => expected.some((expectedItem) => expectedItem.value === item.value)); | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +function showPlatformQuickPick( | ||
| 130 | + items: readonly CleanPlatformOption[], | ||
| 131 | + options: vscode.QuickPickOptions & { canPickMany: true }, | ||
| 132 | +): Promise<readonly CleanPlatformOption[] | undefined> { | ||
| 133 | + return new Promise((resolve) => { | ||
| 134 | + const quickPick = vscode.window.createQuickPick<CleanPlatformOption>(); | ||
| 135 | + const allOption = items.find((item) => item.value === 'all'); | ||
| 136 | + const specificOptions: readonly SpecificCleanPlatformOption[] = items.filter( | ||
| 137 | + (item): item is SpecificCleanPlatformOption => item.value !== 'all', | ||
| 138 | + ); | ||
| 139 | + if (!allOption) { | ||
| 140 | + quickPick.dispose(); | ||
| 141 | + resolve(undefined); | ||
| 142 | + return; | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + let platformSelection: PlatformSelection = { kind: 'all' }; | ||
| 146 | + let accepted = false; | ||
| 147 | + let hidden = false; | ||
| 148 | + | ||
| 149 | + const getExpectedSelection = () => getSelectedPlatformOptions( | ||
| 150 | + platformSelection, | ||
| 151 | + allOption, | ||
| 152 | + specificOptions, | ||
| 153 | + ); | ||
| 154 | + | ||
| 155 | + quickPick.placeholder = options.placeHolder; | ||
| 156 | + quickPick.canSelectMany = true; | ||
| 157 | + quickPick.items = [allOption, ...specificOptions]; | ||
| 158 | + quickPick.selectedItems = getExpectedSelection(); | ||
| 159 | + | ||
| 160 | + quickPick.onDidChangeSelection((selection) => { | ||
| 161 | + if (hidden) { | ||
| 162 | + return; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + platformSelection = normalizePlatformSelection(selection); | ||
| 166 | + const expectedSelection = getExpectedSelection(); | ||
| 167 | + if (!hasSamePlatformOptions(selection, expectedSelection)) { | ||
| 168 | + // Keep the item list stable. Updating selectedItems alone avoids the | ||
| 169 | + // Windows QuickPick flicker caused by replacing items in this event. | ||
| 170 | + quickPick.selectedItems = expectedSelection; | ||
| 171 | + } | ||
| 172 | + }); | ||
| 173 | + | ||
| 174 | + quickPick.onDidAccept(() => { | ||
| 175 | + const result = getExpectedSelection(); | ||
| 176 | + accepted = true; | ||
| 177 | + resolve(result); | ||
| 178 | + quickPick.hide(); | ||
| 179 | + }); | ||
| 180 | + quickPick.onDidHide(() => { | ||
| 181 | + hidden = true; | ||
| 182 | + if (!accepted) { | ||
| 183 | + resolve(undefined); | ||
| 184 | + } | ||
| 185 | + quickPick.dispose(); | ||
| 186 | + }); | ||
| 187 | + quickPick.show(); | ||
| 188 | + }); | ||
| 189 | +} | ||
| 190 | + | ||
| 191 | +const defaultCleanDependencies: CleanDependencies = { | ||
| 192 | + prompt: { | ||
| 193 | + pickPlatforms: showPlatformQuickPick, | ||
| 194 | + pickMode: async (items, options) => vscode.window.showQuickPick(items, options), | ||
| 195 | + confirm: async (message) => { | ||
| 196 | + const selection = await vscode.window.showWarningMessage( | ||
| 197 | + message, | ||
| 198 | + { modal: true }, | ||
| 199 | + 'Clean', | ||
| 200 | + ); | ||
| 201 | + return selection === 'Clean'; | ||
| 202 | + }, | ||
| 203 | + }, | ||
| 204 | + processRunner: { | ||
| 205 | + run: (executablePath, args, options, onOutput) => new Promise((resolve, reject) => { | ||
| 206 | + const spawnExecutable = process.platform === 'win32' | ||
| 207 | + ? `"${executablePath}"` | ||
| 208 | + : executablePath; | ||
| 209 | + const cleanProcess = childProcess.spawn(spawnExecutable, [...args], { | ||
| 210 | + ...options, | ||
| 211 | + shell: process.platform === 'win32', | ||
| 212 | + }); | ||
| 213 | + | ||
| 214 | + cleanProcess.stdout?.on('data', (data) => onOutput(data.toString())); | ||
| 215 | + cleanProcess.stderr?.on('data', (data) => onOutput(data.toString())); | ||
| 216 | + cleanProcess.on('error', reject); | ||
| 217 | + cleanProcess.on('close', resolve); | ||
| 218 | + }), | ||
| 219 | + }, | ||
| 220 | + resolveCommand: () => resolveKeelsCommand(), | ||
| 221 | + createOutputChannel: () => getCjmpOutputChannel(), | ||
| 222 | +}; | ||
| 223 | + | ||
| 224 | +export function getCleanCommandArgs( | ||
| 225 | + platforms: readonly CleanPlatform[], | ||
| 226 | + mode: CleanMode, | ||
| 227 | +): string[] { | ||
| 228 | + const args = ['clean']; | ||
| 229 | + const specificPlatforms = [...new Set( | ||
| 230 | + platforms | ||
| 231 | + .filter(isSpecificCleanPlatform) | ||
| 232 | + .filter((platform) => SUPPORTED_CLEAN_PLATFORMS.includes(platform)), | ||
| 233 | + )]; | ||
| 234 | + const cleansAllPlatforms = platforms.includes('all') | ||
| 235 | + || SUPPORTED_CLEAN_PLATFORMS.every((platform) => specificPlatforms.includes(platform)); | ||
| 236 | + | ||
| 237 | + if (!cleansAllPlatforms) { | ||
| 238 | + args.push('--platform', specificPlatforms.join(',')); | ||
| 239 | + } | ||
| 240 | + if (mode === 'deep' || mode === 'deep-test-results') { | ||
| 241 | + args.push('--deep'); | ||
| 242 | + } | ||
| 243 | + if (mode === 'test-results' || mode === 'deep-test-results') { | ||
| 244 | + args.push('--test-results'); | ||
| 245 | + } | ||
| 246 | + | ||
| 247 | + args.push('-v'); | ||
| 248 | + return args; | ||
| 249 | +} | ||
| 250 | + | ||
| 251 | +export async function cleanCjmpProject( | ||
| 252 | + dependencies: CleanDependencies = defaultCleanDependencies, | ||
| 253 | +): Promise<void> { | ||
| 254 | + const workspaceFolder = getCjmpProjectWorkspaceFolder(vscode.workspace.workspaceFolders); | ||
| 255 | + if (!workspaceFolder) { | ||
| 256 | + vscode.window.showErrorMessage('The opened folder is not a valid CJMP project.'); | ||
| 257 | + return; | ||
| 258 | + } | ||
| 259 | + if (getProjectType() === 'logic-module') { | ||
| 260 | + vscode.window.showErrorMessage('CJMP clean does not support logic-module projects.'); | ||
| 261 | + return; | ||
| 262 | + } | ||
| 263 | + const workspaceRoot = workspaceFolder.uri.fsPath; | ||
| 264 | + | ||
| 265 | + const platforms = await dependencies.prompt.pickPlatforms(PLATFORM_OPTIONS, { | ||
| 266 | + placeHolder: 'Select platforms to clean', | ||
| 267 | + canPickMany: true, | ||
| 268 | + }); | ||
| 269 | + if (!platforms || platforms.length === 0) { | ||
| 270 | + return; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + const mode = await dependencies.prompt.pickMode(MODE_OPTIONS, { | ||
| 274 | + placeHolder: 'Select the clean mode', | ||
| 275 | + }); | ||
| 276 | + if (!mode) { | ||
| 277 | + return; | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + const platformLabels = platforms.map((platform) => platform.label).join(', '); | ||
| 281 | + const confirmed = await dependencies.prompt.confirm( | ||
| 282 | + `Clean generated files for ${platformLabels} using ${mode.label}?`, | ||
| 283 | + ); | ||
| 284 | + if (!confirmed) { | ||
| 285 | + return; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + const keelsCommand = dependencies.resolveCommand(); | ||
| 289 | + if (!keelsCommand) { | ||
| 290 | + return; | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + const args = getCleanCommandArgs( | ||
| 294 | + platforms.map((platform) => platform.value), | ||
| 295 | + mode.value, | ||
| 296 | + ); | ||
| 297 | + const outputChannel = dependencies.createOutputChannel(); | ||
G 每次执行 Clean 都会创建一个新的 ![]() ![]() | |||
| 298 | + outputChannel.show(true); | ||
| 299 | + outputChannel.appendLine(''); | ||
| 300 | + outputChannel.appendLine('=== CJMP Clean ==='); | ||
| 301 | + outputChannel.appendLine(`> ${keelsCommand.executablePath} ${args.join(' ')}`); | ||
| 302 | + | ||
| 303 | + try { | ||
| 304 | + const exitCode = await dependencies.processRunner.run( | ||
| 305 | + keelsCommand.executablePath, | ||
| 306 | + args, | ||
| 307 | + { | ||
| 308 | + cwd: workspaceRoot, | ||
| 309 | + env: keelsCommand.environment, | ||
| 310 | + }, | ||
| 311 | + (output) => outputChannel.append(output), | ||
| 312 | + ); | ||
| 313 | + | ||
| 314 | + if (exitCode === 0) { | ||
| 315 | + vscode.window.showInformationMessage('CJMP clean completed successfully.'); | ||
| 316 | + } else { | ||
| 317 | + vscode.window.showErrorMessage( | ||
| 318 | + `CJMP clean failed. Exit code: ${exitCode ?? 'unknown'}. See the CJMP Clean output for details.`, | ||
| 319 | + ); | ||
| 320 | + } | ||
| 321 | + } catch (error) { | ||
| 322 | + const message = error instanceof Error ? error.message : String(error); | ||
| 323 | + outputChannel.appendLine(`Error: ${message}`); | ||
| 324 | + vscode.window.showErrorMessage(`Failed to execute CJMP clean: ${message}`); | ||
| 325 | + } | ||
| 326 | +} | ||
| @@ -25,6 +25,7 @@ import { KeelsDebugAdapterConfigProvider } from './keelsDebugConfigProvider'; | |||
| 25 | import { RemotePlatform } from './types'; | 25 | import { RemotePlatform } from './types'; |
| 26 | import { PythonEnvUtils } from './apple/pythonEnvUtils'; | 26 | import { PythonEnvUtils } from './apple/pythonEnvUtils'; |
| 27 | import { resolveKeelsCommand } from './cjmpSdk'; | 27 | import { resolveKeelsCommand } from './cjmpSdk'; |
| 28 | +import { getCjmpOutputChannel } from './cjmpOutputChannel'; | ||
| 28 | 29 | ||
| 29 | export class KeelsDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { | 30 | export class KeelsDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { |
| 30 | constructor(private deviceManager: KeelsDeviceManager, private context: vscode.ExtensionContext) { } | 31 | constructor(private deviceManager: KeelsDeviceManager, private context: vscode.ExtensionContext) { } |
| @@ -69,15 +70,6 @@ interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { | |||
| 69 | remotePlatform?: string; | 70 | remotePlatform?: string; |
| 70 | } | 71 | } |
| 71 | 72 | ||
| 72 | -let cjmpOutputChannel: vscode.OutputChannel | undefined; | ||
| 73 | - | ||
| 74 | -function getCjmpOutputChannel(): vscode.OutputChannel { | ||
| 75 | - if (!cjmpOutputChannel) { | ||
| 76 | - cjmpOutputChannel = vscode.window.createOutputChannel('CJMP'); | ||
| 77 | - } | ||
| 78 | - return cjmpOutputChannel; | ||
| 79 | -} | ||
| 80 | - | ||
| 81 | export class KeelsDebugAdapter implements vscode.DebugAdapter { | 73 | export class KeelsDebugAdapter implements vscode.DebugAdapter { |
| 82 | private _onDidSendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>(); | 74 | private _onDidSendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>(); |
| 83 | 75 | ||
| @@ -39,6 +39,7 @@ interface ExtensionManifest { | |||
| 39 | contributes: { | 39 | contributes: { |
| 40 | commands: CommandContribution[]; | 40 | commands: CommandContribution[]; |
| 41 | menus: { | 41 | menus: { |
| 42 | + commandPalette: MenuContribution[]; | ||
| 42 | 'editor/title': MenuContribution[]; | 43 | 'editor/title': MenuContribution[]; |
| 43 | }; | 44 | }; |
| 44 | }; | 45 | }; |
| @@ -70,6 +71,22 @@ suite('Extension manifest', () => { | |||
| 70 | assert.strictEqual(buildMenu?.when, cjmpProjectContextKey); | 71 | assert.strictEqual(buildMenu?.when, cjmpProjectContextKey); |
| 71 | }); | 72 | }); |
| 72 | 73 | ||
| 74 | + test('shows CJMP Clean only in the command palette for CJMP projects', () => { | ||
| 75 | + const cleanCommand = manifest.contributes.commands.find( | ||
| 76 | + ({ command }) => command === 'cjmp.clean', | ||
| 77 | + ); | ||
| 78 | + const cleanPaletteMenu = manifest.contributes.menus.commandPalette.find( | ||
| 79 | + ({ command }) => command === 'cjmp.clean', | ||
| 80 | + ); | ||
| 81 | + const cleanEditorMenu = manifest.contributes.menus['editor/title'].find( | ||
| 82 | + ({ command }) => command === 'cjmp.clean', | ||
| 83 | + ); | ||
| 84 | + | ||
| 85 | + assert.strictEqual(cleanCommand?.command, 'cjmp.clean'); | ||
| 86 | + assert.strictEqual(cleanPaletteMenu?.when, cjmpProjectContextKey); | ||
| 87 | + assert.strictEqual(cleanEditorMenu, undefined); | ||
| 88 | + }); | ||
| 89 | + | ||
| 73 | test('activates early enough to publish the mutual-exclusion context', () => { | 90 | test('activates early enough to publish the mutual-exclusion context', () => { |
| 74 | assert.ok(manifest.activationEvents.includes('workspaceContains:project.conf')); | 91 | assert.ok(manifest.activationEvents.includes('workspaceContains:project.conf')); |
| 75 | }); | 92 | }); |
| @@ -0,0 +1,115 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Device Co., Ltd. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import * as assert from 'assert'; | ||
| 17 | +import * as vscode from 'vscode'; | ||
| 18 | + | ||
| 19 | +import { | ||
| 20 | + cleanCjmpProject, | ||
| 21 | + CleanMode, | ||
| 22 | + CleanPlatform, | ||
| 23 | + CleanPrompt, | ||
| 24 | + getCleanCommandArgs, | ||
| 25 | +} from '../../src/keelsClean'; | ||
| 26 | + | ||
| 27 | +function createPrompt( | ||
| 28 | + platforms: readonly CleanPlatform[], | ||
| 29 | + mode: CleanMode, | ||
| 30 | + confirmed: boolean = true, | ||
| 31 | +): CleanPrompt { | ||
| 32 | + return { | ||
| 33 | + pickPlatforms: async (items) => items.filter((item) => platforms.includes(item.value)), | ||
| 34 | + pickMode: async (items) => items.find((item) => item.value === mode), | ||
| 35 | + confirm: async () => confirmed, | ||
| 36 | + }; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +suite('CJMP clean command flows', () => { | ||
| 40 | + test('maps any selected platform combination to one CLI platform argument', () => { | ||
| 41 | + assert.deepStrictEqual( | ||
| 42 | + getCleanCommandArgs(['android', 'ios'], 'deep-test-results'), | ||
| 43 | + ['clean', '--platform', 'android,ios', '--deep', '--test-results', '-v'], | ||
| 44 | + ); | ||
| 45 | + assert.deepStrictEqual( | ||
| 46 | + getCleanCommandArgs(['all'], 'normal'), | ||
| 47 | + ['clean', '-v'], | ||
| 48 | + ); | ||
| 49 | + }); | ||
| 50 | + | ||
| 51 | + test('executes clean from the workspace root with the resolved SDK environment', async () => { | ||
| 52 | + const runs: Array<{ | ||
| 53 | + executablePath: string; | ||
| 54 | + args: readonly string[]; | ||
| 55 | + options: import('child_process').SpawnOptionsWithoutStdio; | ||
| 56 | + }> = []; | ||
| 57 | + let output = ''; | ||
| 58 | + | ||
| 59 | + await cleanCjmpProject({ | ||
| 60 | + prompt: createPrompt(['android', 'ios'], 'deep'), | ||
| 61 | + resolveCommand: () => ({ | ||
| 62 | + executablePath: 'C:\\CJMP SDK\\cjmp-tools\\bin\\keels.bat', | ||
| 63 | + environment: { CJMP_SDK_HOME: 'C:\\CJMP SDK' }, | ||
| 64 | + }), | ||
| 65 | + processRunner: { | ||
| 66 | + run: async (executablePath, args, options, onOutput) => { | ||
| 67 | + runs.push({ executablePath, args, options }); | ||
| 68 | + onOutput('clean output'); | ||
| 69 | + return 0; | ||
| 70 | + }, | ||
| 71 | + }, | ||
| 72 | + createOutputChannel: () => ({ | ||
| 73 | + name: 'CJMP Clean', | ||
| 74 | + append: (value: string) => { output += value; }, | ||
| 75 | + appendLine: (value: string) => { output += `${value}\n`; }, | ||
| 76 | + clear: () => undefined, | ||
| 77 | + replace: () => undefined, | ||
| 78 | + show: () => undefined, | ||
| 79 | + hide: () => undefined, | ||
| 80 | + dispose: () => undefined, | ||
| 81 | + } as vscode.OutputChannel), | ||
| 82 | + }); | ||
| 83 | + | ||
| 84 | + assert.strictEqual(runs.length, 1); | ||
| 85 | + assert.strictEqual(runs[0].executablePath, 'C:\\CJMP SDK\\cjmp-tools\\bin\\keels.bat'); | ||
| 86 | + assert.deepStrictEqual( | ||
| 87 | + runs[0].args, | ||
| 88 | + ['clean', '--platform', 'android,ios', '--deep', '-v'], | ||
| 89 | + ); | ||
| 90 | + assert.strictEqual( | ||
| 91 | + runs[0].options.cwd, | ||
| 92 | + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath, | ||
| 93 | + ); | ||
| 94 | + assert.strictEqual(runs[0].options.env?.CJMP_SDK_HOME, 'C:\\CJMP SDK'); | ||
| 95 | + assert.ok(output.includes('clean output')); | ||
| 96 | + }); | ||
| 97 | + | ||
| 98 | + test('does not start clean when confirmation is cancelled', async () => { | ||
| 99 | + let runCount = 0; | ||
| 100 | + | ||
| 101 | + await cleanCjmpProject({ | ||
| 102 | + prompt: createPrompt(['all'], 'normal', false), | ||
| 103 | + resolveCommand: () => ({ executablePath: 'keels', environment: {} }), | ||
| 104 | + processRunner: { | ||
| 105 | + run: async () => { | ||
| 106 | + runCount += 1; | ||
| 107 | + return 0; | ||
| 108 | + }, | ||
| 109 | + }, | ||
| 110 | + createOutputChannel: () => vscode.window.createOutputChannel('Unused'), | ||
| 111 | + }); | ||
| 112 | + | ||
| 113 | + assert.strictEqual(runCount, 0); | ||
| 114 | + }); | ||
| 115 | +}); | ||


clean不支持logic-module,所以插件里也要加一下限制。可以在这个之前判断一下,如果是logic-module给出不支持提示,并退出。