Skip to content

Commit

Permalink
fix: auto-close debug terminal after finished or closed (#1652)
Browse files Browse the repository at this point in the history
* style: improve teminal tabs style

* style: improve badge style

* fix: auto-close debug terminal after finished or closed
  • Loading branch information
erha19 authored Sep 13, 2022
1 parent 0a64da0 commit 093215e
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 19 deletions.
10 changes: 5 additions & 5 deletions packages/debug/src/browser/debug-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
BreakpointsChangeEvent,
IDebugBreakpoint,
IMemoryRegion,
prepareCommand,
} from '../common';
import { DebugConfiguration } from '../common';

Expand Down Expand Up @@ -315,17 +316,16 @@ export class DebugSession implements IDebugSession {
protected async runInTerminal({
arguments: { title, cwd, args, env },
}: DebugProtocol.RunInTerminalRequest): Promise<DebugProtocol.RunInTerminalResponse['body']> {
return this.doRunInTerminal({ name: title, cwd, env }, args.join(' '));
return this.doRunInTerminal({ name: title, cwd, env, args });
}

protected async doRunInTerminal(
options: TerminalOptions,
command?: string,
): Promise<DebugProtocol.RunInTerminalResponse['body']> {
protected async doRunInTerminal(options: TerminalOptions): Promise<DebugProtocol.RunInTerminalResponse['body']> {
const activeTerminal = this.terminalService.terminals.find(
(terminal) => terminal.name === options.name && terminal.isActive,
);
let processId: number | undefined;
const shellPath = await this.terminalService.getDefaultShellPath();
const command = prepareCommand(shellPath, options.args, false, options.cwd?.toString(), options.env);
// 当存在同名终端并且处于激活状态时,复用该终端
if (activeTerminal) {
if (command) {
Expand Down
1 change: 1 addition & 0 deletions packages/debug/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './debug-schema-updater';
export * from './inline-values';
export * from './types';
export * from './constants';
export * from './terminal';
163 changes: 163 additions & 0 deletions packages/debug/src/common/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* eslint-disable guard-for-in */
import { CharCode, isWindows } from '@opensumi/ide-core-common';

const enum ShellType {
cmd,
powershell,
bash,
}

export function isWindowsDriveLetter(char0: number): boolean {
return (char0 >= CharCode.A && char0 <= CharCode.Z) || (char0 >= CharCode.a && char0 <= CharCode.z);
}

export function hasDriveLetter(path: string, isWindowsOS: boolean = isWindows): boolean {
if (isWindowsOS) {
return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
}

return false;
}

export function getDriveLetter(path: string): string | undefined {
return hasDriveLetter(path) ? path[0] : undefined;
}

export function prepareCommand(
shell: string,
args: string[],
argsCanBeInterpretedByShell: boolean,
cwd?: string,
env?: { [key: string]: string | null },
): string {
shell = shell.trim().toLowerCase();

// try to determine the shell type
let shellType;
if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0) {
shellType = ShellType.powershell;
} else if (shell.indexOf('cmd.exe') >= 0) {
shellType = ShellType.cmd;
} else if (shell.indexOf('bash') >= 0) {
shellType = ShellType.bash;
} else if (isWindows) {
shellType = ShellType.cmd; // pick a good default for Windows
} else {
shellType = ShellType.bash; // pick a good default for anything else
}

let quote: (s: string) => string;
// begin command with a space to avoid polluting shell history
let command = ' ';

switch (shellType) {
case ShellType.powershell:
quote = (s: string) => {
s = s.replace(/'/g, "''");
if (s.length > 0 && s.charAt(s.length - 1) === '\\') {
return `'${s}\\'`;
}
return `'${s}'`;
};

if (cwd) {
const driveLetter = getDriveLetter(cwd);
if (driveLetter) {
command += `${driveLetter}:; `;
}
command += `cd ${quote(cwd)}; `;
}
if (env) {
for (const key in env) {
const value = env[key];
if (value === null) {
command += `Remove-Item env:${key}; `;
} else {
command += `\${env:${key}}='${value}'; `;
}
}
}
if (args.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const arg = args.shift()!;
const cmd = argsCanBeInterpretedByShell ? arg : quote(arg);
command += cmd[0] === "'" ? `& ${cmd} ` : `${cmd} `;
for (const a of args) {
command += a === '<' || a === '>' || argsCanBeInterpretedByShell ? a : quote(a);
command += ' ';
}
}
break;

case ShellType.cmd:
quote = (s: string) => {
// Note: Wrapping in cmd /C "..." complicates the escaping.
// cmd /C "node -e "console.log(process.argv)" """A^>0"""" # prints "A>0"
// cmd /C "node -e "console.log(process.argv)" "foo^> bar"" # prints foo> bar
// Outside of the cmd /C, it could be a simple quoting, but here, the ^ is needed too
s = s.replace(/"/g, '""');
s = s.replace(/([><!^&|])/g, '^$1');
return ' "'.split('').some((char) => s.includes(char)) || s.length === 0 ? `"${s}"` : s;
};

if (cwd) {
const driveLetter = getDriveLetter(cwd);
if (driveLetter) {
command += `${driveLetter}: && `;
}
command += `cd ${quote(cwd)} && `;
}
if (env) {
command += 'cmd /C "';
for (const key in env) {
let value = env[key];
if (value === null) {
command += `set "${key}=" && `;
} else {
value = value.replace(/[&^|<>]/g, (s) => `^${s}`);
command += `set "${key}=${value}" && `;
}
}
}
for (const a of args) {
command += a === '<' || a === '>' || argsCanBeInterpretedByShell ? a : quote(a);
command += ' ';
}
if (env) {
command += '"';
}
break;

case ShellType.bash: {
quote = (s: string) => {
s = s.replace(/(["'\\$!><#()[\]*&^| ;{}`])/g, '\\$1');
return s.length === 0 ? '""' : s;
};

const hardQuote = (s: string) => (/[^\w@%/+=,.:^-]/.test(s) ? `'${s.replace(/'/g, "'\\''")}'` : s);

if (cwd) {
command += `cd ${quote(cwd)} ; `;
}
if (env) {
command += '/usr/bin/env';
for (const key in env) {
const value = env[key];
if (value === null) {
command += ` -u ${hardQuote(key)}`;
} else {
command += ` ${hardQuote(`${key}=${value}`)}`;
}
}
command += ' ';
}
for (const a of args) {
command += a === '<' || a === '>' || argsCanBeInterpretedByShell ? a : quote(a);
command += ' ';
}
break;
}
}

return command;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injector } from '@opensumi/di';
import { IWebSocket } from '@opensumi/ide-connection';
import { LabelService } from '@opensumi/ide-core-browser/lib/services';
import { IDebugSessionManager } from '@opensumi/ide-debug';
import { DebugSessionOptions } from '@opensumi/ide-debug';
import { localize } from '@opensumi/ide-core-common';
import { IDebugSessionManager, DebugSessionOptions } from '@opensumi/ide-debug';
import {
DebugSession,
DebugSessionConnection,
Expand All @@ -19,6 +19,8 @@ import { IMessageService } from '@opensumi/ide-overlay';
import { ITerminalApiService, TerminalOptions } from '@opensumi/ide-terminal-next';
import { DebugProtocol } from '@opensumi/vscode-debugprotocol';

import { ThemeIcon } from '../../../../common/vscode/ext-types';

export class ExtensionDebugSession extends DebugSession {
constructor(
readonly id: string,
Expand Down Expand Up @@ -51,10 +53,19 @@ export class ExtensionDebugSession extends DebugSession {

protected async doRunInTerminal(
terminalOptions: TerminalOptions,
command?: string,
): Promise<DebugProtocol.RunInTerminalResponse['body']> {
const terminalWidgetOptions = Object.assign({}, terminalOptions, this.terminalOptionsExt);
return super.doRunInTerminal(terminalWidgetOptions, command);
if (!terminalOptions.name) {
terminalOptions.name = localize('debug.terminal.title', 'Debug Process');
}
if (!terminalOptions.iconPath) {
terminalOptions.iconPath = new ThemeIcon('debug');
}
const terminalWidgetOptions = {
...terminalOptions,
...this.terminalOptionsExt,
};

return super.doRunInTerminal(terminalWidgetOptions);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/common/en-US.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,7 @@ export const localizationBundle = {
'connection.stop.rtt': 'Stop Connection RTT',

'debug.terminal.label': 'Javascript Debug Terminal',
'debug.terminal.title': 'Debug Process',

'output.channel.clear': 'Clear Output Panel',

Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/common/zh-CN.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,7 @@ export const localizationBundle = {
'connection.stop.rtt': '关闭通信延迟检查',

'debug.terminal.label': '创建 Javascript Debug Terminal',
'debug.terminal.title': '调试进程',
'workbench.action.tasks.runTask': '运行任务',
'workbench.action.tasks.reRunTask': '执行上次运行的任务',
'workbench.action.tasks.restartTask': '重新开始运行中的任务',
Expand Down
2 changes: 1 addition & 1 deletion packages/main-layout/src/browser/tabbar/styles.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
user-select: none;
align-items: center;
border-bottom: 1px solid var(--sideBar-border);
box-sizing: border-box;
box-sizing: content-box;
h1 {
margin: 0;
font-size: 12px;
Expand Down
9 changes: 9 additions & 0 deletions packages/terminal-next/src/browser/terminal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ITerminalNetwork,
ITerminalExitEvent,
ITerminalTitleChangeEvent,
ITerminalProfileInternalService,
} from '../common';

@Injectable()
Expand All @@ -40,6 +41,9 @@ export class TerminalApiService implements ITerminalApiService {
@Autowired(ITerminalNetwork)
protected readonly network: ITerminalNetwork;

@Autowired(ITerminalProfileInternalService)
protected readonly terminalProfileInternalService: ITerminalProfileInternalService;

constructor() {
this.controller.onDidOpenTerminal((info) => {
this._onDidOpenTerminal.fire(info);
Expand Down Expand Up @@ -115,6 +119,11 @@ export class TerminalApiService implements ITerminalApiService {
return client.pid;
}

async getDefaultShellPath() {
const defaultProfile = await this.terminalProfileInternalService.resolveDefaultProfile();
return defaultProfile?.path || '/bin/bash';
}

sendText(sessionId: string, text: string, addNewLine = true) {
this.service.sendText(sessionId, `${text}${addNewLine ? '\r' : ''}`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/terminal-next/src/common/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ITerminalApiService {
createTerminal(options: TerminalOptions, id?: string): Promise<ITerminalExternalClient>;
sendText(id: string, text: string, addNewLine?: boolean): void;
getProcessId(sessionId: string): Promise<number | undefined>;
getDefaultShellPath(): Promise<string>;

onDidOpenTerminal: Event<ITerminalInfo>;
onDidCloseTerminal: Event<ITerminalExitEvent>;
Expand Down
13 changes: 7 additions & 6 deletions packages/terminal-next/src/common/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ export interface TerminalOptions {
*/
iconPath?: Uri | { light: Uri; dark: Uri } | vscode.ThemeIcon;

/**
* The CLI arguments to use with executable, a string[] is in argv format and will be escaped,
* a string is in "CommandLine" pre-escaped format and will be used as is. The string option is
* only supported on Windows and will throw an exception if used on macOS or Linux.
*/
args?: any;

/**
* The icon {@link ThemeColor} for the terminal.
* The `terminal.ansi*` theme keys are
Expand All @@ -265,12 +272,6 @@ export interface TerminalOptions {
*/
closeWhenExited?: boolean;

/**
* @deprecated Use `ICreateClientWithWidgetOptions.args` instead. Will removed in 2.17.0
* 自定义的参数,由上层集成方自行控制
*/
args?: any;

/**
* @deprecated Use `ICreateClientWithWidgetOptions.beforeCreate` instead. Will removed in 2.17.0
* 自定义的参数,由上层集成方自行控制
Expand Down
5 changes: 3 additions & 2 deletions packages/theme/src/common/color-tokens/custom/badge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { localize } from '@opensumi/ide-core-common';

import { registerColor } from '../../utils';
import { badgeBackground } from '../badge';
import { contrastBorder } from '../base';

export const ActivityBarBadgeBorder = registerColor(
Expand All @@ -19,8 +20,8 @@ export const ActivityBarBadgeBorder = registerColor(
export const BadgeBorder = registerColor(
'kt.badge.border',
{
dark: null,
light: null,
dark: badgeBackground,
light: badgeBackground,
hc: contrastBorder,
},
localize('badgeBorder', 'Badge border color. Badges are small information labels, e.g. for search results count.'),
Expand Down

0 comments on commit 093215e

Please sign in to comment.