diff --git a/.vscode/launch.json b/.vscode/launch.json index fac9d03e39..7b0d41b0c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -111,7 +111,7 @@ "name": "Playwright Current File", "type": "node", "request": "launch", - "program": "${workspaceFolder}/tools/playwright/node_modules/.bin/playwright", + "program": "${workspaceFolder}/node_modules/.bin/playwright", "args": ["test", "--debug", "--config=./configs/playwright.debug.config.ts", "${fileBasenameNoExtension}"], "cwd": "${workspaceFolder}/tools/playwright", "console": "integratedTerminal", diff --git a/packages/core-browser/src/common/container-id.ts b/packages/core-browser/src/common/container-id.ts index 599adfd51e..b5b11509dd 100644 --- a/packages/core-browser/src/common/container-id.ts +++ b/packages/core-browser/src/common/container-id.ts @@ -2,7 +2,7 @@ export const MARKER_CONTAINER_ID = 'markers'; export const OUTPUT_CONTAINER_ID = 'output'; export const TERMINAL_CONTAINER_ID = 'terminal'; -export const DEBUG_CONSOLE_CONTAINER_ID = 'debug-console-container'; +export const DEBUG_CONSOLE_CONTAINER_ID = 'debug-console'; // Left Panel 的 Container export const DEBUG_CONTAINER_ID = 'debug'; diff --git a/packages/debug/src/browser/view/console/debug-console-session.ts b/packages/debug/src/browser/view/console/debug-console-session.ts index b5c0f4fdc5..06482caf46 100644 --- a/packages/debug/src/browser/view/console/debug-console-session.ts +++ b/packages/debug/src/browser/view/console/debug-console-session.ts @@ -1,7 +1,7 @@ import throttle from 'lodash/throttle'; import { Autowired, Injectable, Optional } from '@opensumi/di'; -import { DisposableCollection, Emitter, Event, MessageType, ILogger } from '@opensumi/ide-core-common'; +import { DisposableCollection, Emitter, Event, MessageType, ILogger, localize } from '@opensumi/ide-core-common'; import { IThemeService } from '@opensumi/ide-theme'; import { DebugProtocol } from '@opensumi/vscode-debugprotocol/lib/debugProtocol'; @@ -68,6 +68,8 @@ export class DebugConsoleSession implements IDebugConsoleSession { } protected async logOutput(session: DebugSession, event: DebugProtocol.OutputEvent): Promise { + // [2J is the ansi escape sequence for clearing the display http://ascii-table.com/ansi-escape-sequences.php + const clearAnsiSequence = '\u001b[2J'; const body = event.body; const { category, variablesReference, source, line } = body; if (!this.treeModel) { @@ -76,7 +78,9 @@ export class DebugConsoleSession implements IDebugConsoleSession { const severity = category === 'stderr' ? MessageType.Error - : event.body.category === 'console' + : category === 'stdout' + ? MessageType.Info + : category === 'console' ? MessageType.Warning : MessageType.Info; if (category === 'telemetry') { @@ -95,11 +99,23 @@ export class DebugConsoleSession implements IDebugConsoleSession { } } } else if (typeof body.output === 'string') { + let output = body.output; + if (output.indexOf(clearAnsiSequence) >= 0) { + this.clearConsole(); + await this.insertItemWithAnsi(localize('debug.console.consoleCleare'), MessageType.Info); + output = output.substring(output.lastIndexOf(clearAnsiSequence) + clearAnsiSequence.length); + } const previousItem = this.getLastItem(); /** - * 如果上一条 output 结尾没有换行符则应该与下一条 output 拼接在一起 + * 如果上一次输出结尾没有换行符并且输出类型(MessageType)一致 + * 则将接下来的输出拼接至上一次输出后 */ - if (previousItem && !previousItem.description.endsWith('\n') && !previousItem.description.endsWith('\r\n')) { + if ( + previousItem && + !previousItem.description.endsWith('\n') && + !previousItem.description.endsWith('\r\n') && + (previousItem as AnsiConsoleNode).severity === severity + ) { this.treeModel.root.unlinkItem(previousItem); await this.insertItemWithAnsi(previousItem.description + body.output, severity, source, line); } else { @@ -110,6 +126,16 @@ export class DebugConsoleSession implements IDebugConsoleSession { this.fireDidChange(); } + private async clearConsole() { + const items = this.treeModel.root.flattenedBranch?.map((id) => this.treeModel.root.getTreeNodeById(id)); + if (!items) { + return; + } + for (const item of items) { + this.treeModel.root.unlinkItem(item as AnsiConsoleNode); + } + } + private async insertItemWithAnsi( output: string, severity?: MessageType, diff --git a/packages/debug/src/browser/view/console/debug-console-tree.model.service.ts b/packages/debug/src/browser/view/console/debug-console-tree.model.service.ts index 0b920c0d57..81afefb54c 100644 --- a/packages/debug/src/browser/view/console/debug-console-tree.model.service.ts +++ b/packages/debug/src/browser/view/console/debug-console-tree.model.service.ts @@ -21,7 +21,7 @@ import { } from '@opensumi/ide-core-browser'; import { AbstractContextMenuService, MenuId, ICtxMenuRenderer } from '@opensumi/ide-core-browser/lib/menu/next'; -import { IDebugSessionManager } from '../../../common'; +import { IDebugConsoleModelService, IDebugSessionManager } from '../../../common'; import { LinkDetector } from '../../debug-link-detector'; import { DebugSession } from '../../debug-session'; import { DidChangeActiveDebugSession } from '../../debug-session-manager'; @@ -50,7 +50,7 @@ export interface IDebugConsoleModel { } @Injectable() -export class DebugConsoleModelService { +export class DebugConsoleModelService implements IDebugConsoleModelService { private static DEFAULT_REFRESH_DELAY = 200; @Autowired(INJECTOR_TOKEN) @@ -164,16 +164,16 @@ export class DebugConsoleModelService { return this.onDidRefreshedEmitter.event; } - clear = () => { + clear() { // 重新初始化Console中渲染的TreeModel this.initTreeModel(this.manager.currentSession, true); - }; + } - collapseAll = () => { + collapseAll() { this.treeModel?.root.collapsedAll(); - }; + } - copyAll = () => { + copyAll() { let text = ''; if (!this.treeModel?.root || !this.treeModel.root.children) { return; @@ -182,15 +182,18 @@ export class DebugConsoleModelService { text += this.getValidText(child as DebugConsoleNode) + '\n'; } this.clipboardService.writeText(text.slice(0, -'\n'.length)); - }; + } - copy = (node: DebugConsoleNode) => { + copy(node: DebugConsoleNode) { if (node) { this.clipboardService.writeText(this.getValidText(node)); } - }; + } private getValidText(node: DebugConsoleNode) { + if (node.description.endsWith('\n')) { + return node.description.slice(0, -'\n'.length); + } return node.description; } diff --git a/packages/debug/src/browser/view/console/debug-console.service.ts b/packages/debug/src/browser/view/console/debug-console.service.ts index ca3bd4464f..7cee0cedd6 100644 --- a/packages/debug/src/browser/view/console/debug-console.service.ts +++ b/packages/debug/src/browser/view/console/debug-console.service.ts @@ -22,6 +22,7 @@ import { IDebugSessionManager, CONTEXT_IN_DEBUG_MODE_KEY, DebugState, + IDebugConsoleModelService, } from '../../../common'; import { DebugSessionManager } from '../../debug-session-manager'; @@ -47,7 +48,7 @@ const consoleInputMonacoOptions: monaco.editor.IEditorOptions = { @Injectable() export class DebugConsoleService implements IHistoryNavigationWidget { - @Autowired(DebugConsoleModelService) + @Autowired(IDebugConsoleModelService) protected readonly debugConsoleModelService: DebugConsoleModelService; @Autowired(IMainLayoutService) @@ -115,7 +116,7 @@ export class DebugConsoleService implements IHistoryNavigationWidget { return bottomPanelHandler && bottomPanelHandler.isVisible; } - public get consoleModel(): DebugConsoleModelService { + public get consoleModel() { return this.debugConsoleModelService; } diff --git a/packages/debug/src/common/debug-console.ts b/packages/debug/src/common/debug-console.ts index 9070b346e5..6152317e19 100644 --- a/packages/debug/src/common/debug-console.ts +++ b/packages/debug/src/common/debug-console.ts @@ -12,9 +12,10 @@ export interface IDebugConsoleSession extends ITree { } export interface IDebugConsoleModelService { - debugConsoleSession: IDebugConsoleSession; + debugConsoleSession?: IDebugConsoleSession; clear(): void; copyAll(): void; collapseAll(): void; copy(node: ITreeNode): void; + execute(value: string): Promise; } diff --git a/packages/extension/src/browser/vscode/api/main.thread.debug.ts b/packages/extension/src/browser/vscode/api/main.thread.debug.ts index b1075e5ad5..8b365b79a4 100644 --- a/packages/extension/src/browser/vscode/api/main.thread.debug.ts +++ b/packages/extension/src/browser/vscode/api/main.thread.debug.ts @@ -23,6 +23,7 @@ import { DebugConfigurationManager } from '@opensumi/ide-debug/lib/browser/debug import { DebugPreferences } from '@opensumi/ide-debug/lib/browser/debug-preferences'; import { DebugSessionContributionRegistry } from '@opensumi/ide-debug/lib/browser/debug-session-contribution'; import { DebugSessionManager } from '@opensumi/ide-debug/lib/browser/debug-session-manager'; +import { DebugConsoleModelService } from '@opensumi/ide-debug/lib/browser/view/console/debug-console-tree.model.service'; import { IDebugService, IDebugServer } from '@opensumi/ide-debug/lib/common/debug-service'; import { IDebugSessionManager, IDebugSessionOptions } from '@opensumi/ide-debug/lib/common/debug-session'; import { WorkbenchEditorService } from '@opensumi/ide-editor'; @@ -98,7 +99,7 @@ export class MainThreadDebug implements IMainThreadDebug { protected readonly terminalService: ITerminalApiService; @Autowired(IDebugConsoleModelService) - protected readonly debugConsoleModelService: IDebugConsoleModelService; + protected readonly debugConsoleModelService: DebugConsoleModelService; @Autowired(OutputService) protected readonly outputService: OutputService; diff --git a/packages/i18n/src/common/en-US.lang.ts b/packages/i18n/src/common/en-US.lang.ts index ce1b512133..65a953b917 100644 --- a/packages/i18n/src/common/en-US.lang.ts +++ b/packages/i18n/src/common/en-US.lang.ts @@ -309,6 +309,7 @@ export const localizationBundle = { 'debug.console.followLink': '{0} + click to follow link', 'debug.console.input.placeholder': 'Please start a debug session to evaluate expressions', 'debug.console.errorMessage': 'Debug session initialization failed. See console for details.', + 'debug.console.consoleCleared': 'Console was cleared', 'debug.notSupported.type': 'Debug type "{0}" is not supported, please check your launch config.', 'debug.notSupported.any': 'Debug is not supported, please check your launch config.', diff --git a/packages/i18n/src/common/zh-CN.lang.ts b/packages/i18n/src/common/zh-CN.lang.ts index ed81b26fd9..159809475c 100644 --- a/packages/i18n/src/common/zh-CN.lang.ts +++ b/packages/i18n/src/common/zh-CN.lang.ts @@ -269,6 +269,7 @@ export const localizationBundle = { 'debug.console.followLink': '按住 {0} 并单击可访问链接', 'debug.console.errorMessage': '调试进程初始化异常,请打开控制面板查看错误日志', 'debug.console.input.placeholder': '请发起调试会话来对表达式求值', + 'debug.console.consoleCleared': '输出已清理', 'debug.notSupported.type': '调试类型 "{0}" 不是支持的调试类型,请检查配置或安装对应调试插件', 'debug.notSupported.any': '当前调试配置不支持,请检查配置或安装对应调试插件', diff --git a/tools/playwright/src/debug-console-view.ts b/tools/playwright/src/debug-console-view.ts new file mode 100644 index 0000000000..84fb73757d --- /dev/null +++ b/tools/playwright/src/debug-console-view.ts @@ -0,0 +1,21 @@ +import { OpenSumiApp } from './app'; +import { OpenSumiContextMenu } from './context-menu'; +import { OpenSumiPanel } from './panel'; + +export class OpenSumiDebugConsoleView extends OpenSumiPanel { + constructor(app: OpenSumiApp) { + super(app, 'DEBUG-CONSOLE'); + } + + async getOutputContainer() { + return this.view?.$('[class*="debug_console_output__"]'); + } + + async openConsoleContextMenu() { + const view = await this.getOutputContainer(); + if (!view) { + return; + } + return OpenSumiContextMenu.open(this.app, async () => view); + } +} diff --git a/tools/playwright/src/tests/debug.test.ts b/tools/playwright/src/tests/debug.test.ts index 3985d3955f..42a1dc41ce 100644 --- a/tools/playwright/src/tests/debug.test.ts +++ b/tools/playwright/src/tests/debug.test.ts @@ -3,6 +3,7 @@ import path from 'path'; import { expect } from '@playwright/test'; import { OpenSumiApp } from '../app'; +import { OpenSumiDebugConsoleView } from '../debug-console-view'; import { OpenSumiDebugView } from '../debug-view'; import { OpenSumiExplorerView } from '../explorer-view'; import { OpenSumiTerminalView } from '../terminal-view'; @@ -80,6 +81,41 @@ test.describe('OpenSumi Debug', () => { await page.waitForTimeout(1000); }); + test('ContextMenu on DebugConsole should be work', async () => { + editor = await app.openEditor(OpenSumiTextEditor, explorer, 'index.js', false); + await app.page.waitForTimeout(1000); + + debugView = await app.open(OpenSumiDebugView); + const glyphMarginModel = await editor.getGlyphMarginModel(); + const glyphOverlay = await glyphMarginModel.getOverlay(6); + expect(glyphOverlay).toBeDefined(); + if (!glyphOverlay) { + return; + } + const isClicked = await glyphMarginModel.hasBreakpoint(glyphOverlay); + if (!isClicked) { + await glyphOverlay?.click({ position: { x: 9, y: 9 }, force: true }); + await app.page.waitForTimeout(1000); + } + + await debugView.start(); + await app.page.waitForTimeout(2000); + + const debugConsole = await app.open(OpenSumiDebugConsoleView); + const contextMenu = await debugConsole.openConsoleContextMenu(); + await app.page.waitForTimeout(200); + expect(await contextMenu?.isOpen()).toBeTruthy(); + const copyAll = await contextMenu?.menuItemByName('Copy All'); + await copyAll?.click(); + await app.page.waitForTimeout(1000); + const text = (await page.evaluate('navigator.clipboard.readText()')) as string; + expect(text.includes('Debugger attached.')).toBeTruthy(); + + await editor.close(); + await debugView.stop(); + await page.waitForTimeout(1000); + }); + test('Run Debug by Javascript Debug Terminal', async () => { await explorer.open(); editor = await app.openEditor(OpenSumiTextEditor, explorer, 'index.js', false); diff --git a/tools/playwright/src/tests/workspaces/debug/.sumi/launch.json b/tools/playwright/src/tests/workspaces/debug/.sumi/launch.json index 030f9de90e..ff0a2afe28 100644 --- a/tools/playwright/src/tests/workspaces/debug/.sumi/launch.json +++ b/tools/playwright/src/tests/workspaces/debug/.sumi/launch.json @@ -9,6 +9,7 @@ "request": "launch", "name": "Launch Program", "program": "${workspaceFolder}/index.js", + "outputCapture": "std", "skipFiles": ["/**"] } ]