Skip to content

Commit 00fc449

Browse files
authored
add rerun task option for task terminals (#239886)
1 parent 6449f47 commit 00fc449

17 files changed

+117
-13
lines changed

Diff for: src/vs/base/common/codiconsLibrary.ts

+1
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export const codiconsLibrary = {
318318
repoPush: register('repo-push', 0xeb41),
319319
report: register('report', 0xeb42),
320320
requestChanges: register('request-changes', 0xeb43),
321+
rerunTask: register('rerun-task', 0xead2),
321322
rocket: register('rocket', 0xeb44),
322323
rootFolderOpened: register('root-folder-opened', 0xeb45),
323324
rootFolder: register('root-folder', 0xeb46),

Diff for: src/vs/platform/terminal/common/terminal.ts

+14
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export interface IPtyHostAttachTarget {
193193
type?: TerminalType;
194194
hasChildProcesses: boolean;
195195
shellIntegrationNonce: string;
196+
tabActions?: ITerminalTabAction[];
196197
}
197198

198199
export interface IReconnectionProperties {
@@ -558,6 +559,7 @@ export interface IShellLaunchConfig {
558559
hideFromUser?: boolean;
559560
isFeatureTerminal?: boolean;
560561
shellIntegrationNonce: string;
562+
tabActions?: ITerminalTabAction[];
561563
};
562564

563565
/**
@@ -635,6 +637,17 @@ export interface IShellLaunchConfig {
635637
* Create a terminal without shell integration even when it's enabled
636638
*/
637639
ignoreShellIntegration?: boolean;
640+
641+
/**
642+
* Actions to include inline on hover of the terminal tab. E.g. the "Rerun task" action
643+
*/
644+
tabActions?: ITerminalTabAction[];
645+
}
646+
647+
export interface ITerminalTabAction {
648+
id: string;
649+
label: string;
650+
icon?: ThemeIcon;
638651
}
639652

640653
export type WaitOnExitValue = boolean | string | ((exitCode: number) => string);
@@ -669,6 +682,7 @@ export interface IShellLaunchConfigDto {
669682
reconnectionProperties?: IReconnectionProperties;
670683
type?: 'Task' | 'Local';
671684
isFeatureTerminal?: boolean;
685+
tabActions?: ITerminalTabAction[];
672686
}
673687

674688
/**

Diff for: src/vs/platform/terminal/common/terminalProcess.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { UriComponents } from '../../../base/common/uri.js';
77
import { ISerializableEnvironmentVariableCollection, ISerializableEnvironmentVariableCollections } from './environmentVariable.js';
8-
import { IFixedTerminalDimensions, IRawTerminalTabLayoutInfo, IReconnectionProperties, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TerminalType, TitleEventSource, WaitOnExitValue } from './terminal.js';
8+
import { IFixedTerminalDimensions, IRawTerminalTabLayoutInfo, IReconnectionProperties, ITerminalEnvironment, ITerminalTabAction, ITerminalTabLayoutInfoById, TerminalIcon, TerminalType, TitleEventSource, WaitOnExitValue } from './terminal.js';
99

1010
export interface ISingleTerminalConfiguration<T> {
1111
userValue: T | undefined;
@@ -58,6 +58,7 @@ export interface IProcessDetails {
5858
type?: TerminalType;
5959
hasChildProcesses: boolean;
6060
shellIntegrationNonce: string;
61+
tabActions?: ITerminalTabAction[];
6162
}
6263

6364
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;

Diff for: src/vs/platform/terminal/node/ptyService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,8 @@ export class PtyService extends Disposable implements IPtyService {
603603
isFeatureTerminal: persistentProcess.shellLaunchConfig.isFeatureTerminal,
604604
type: persistentProcess.shellLaunchConfig.type,
605605
hasChildProcesses: persistentProcess.hasChildProcesses,
606-
shellIntegrationNonce: persistentProcess.processLaunchOptions.options.shellIntegration.nonce
606+
shellIntegrationNonce: persistentProcess.processLaunchOptions.options.shellIntegration.nonce,
607+
tabActions: persistentProcess.shellLaunchConfig.tabActions
607608
};
608609
performance.mark(`code/didBuildProcessDetails/${id}`);
609610
return result;

Diff for: src/vs/server/node/remoteTerminalChannel.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
200200
useShellEnvironment: args.shellLaunchConfig.useShellEnvironment,
201201
reconnectionProperties: args.shellLaunchConfig.reconnectionProperties,
202202
type: args.shellLaunchConfig.type,
203-
isFeatureTerminal: args.shellLaunchConfig.isFeatureTerminal
203+
isFeatureTerminal: args.shellLaunchConfig.isFeatureTerminal,
204+
tabActions: args.shellLaunchConfig.tabActions
204205
};
205206

206207

Diff for: src/vs/workbench/api/browser/mainThreadTerminalService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
381381
args: terminalInstance.shellLaunchConfig.args,
382382
cwd: terminalInstance.shellLaunchConfig.cwd,
383383
env: terminalInstance.shellLaunchConfig.env,
384-
hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser
384+
hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser,
385+
tabActions: terminalInstance.shellLaunchConfig.tabActions
385386
};
386387
this._proxy.$acceptTerminalOpened(terminalInstance.instanceId, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto);
387388
}

Diff for: src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
20072007
this._contextService, this._environmentService,
20082008
AbstractTaskService.OutputChannelId, this._fileService, this._terminalProfileResolverService,
20092009
this._pathService, this._viewDescriptorService, this._logService, this._notificationService,
2010-
this._instantiationService,
2010+
this._contextKeyService, this._instantiationService,
20112011
(workspaceFolder: IWorkspaceFolder | undefined) => {
20122012
if (workspaceFolder) {
20132013
return this._getTaskSystemInfo(workspaceFolder.uri.scheme);
@@ -2948,6 +2948,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
29482948
});
29492949
}
29502950

2951+
2952+
rerun(terminalInstanceId: number): void {
2953+
const task = this._taskSystem?.getTaskForTerminal(terminalInstanceId);
2954+
if (task) {
2955+
this._restart(task);
2956+
} else {
2957+
this._reRunTaskCommand();
2958+
}
2959+
}
2960+
29512961
private _reRunTaskCommand(): void {
29522962

29532963
ProblemMatcherRegistry.onReady().then(() => {

Diff for: src/vs/workbench/contrib/tasks/browser/task.contribution.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as nls from '../../../../nls.js';
88
import { Disposable } from '../../../../base/common/lifecycle.js';
99
import { Registry } from '../../../../platform/registry/common/platform.js';
1010
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
11-
import { MenuRegistry, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
11+
import { MenuRegistry, MenuId, registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
1212

1313
import { ProblemMatcherRegistry } from '../common/problemMatcher.js';
1414
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
@@ -20,7 +20,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus
2020

2121
import { IOutputChannelRegistry, Extensions as OutputExt } from '../../../services/output/common/output.js';
2222

23-
import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE } from '../common/tasks.js';
23+
import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE } from '../common/tasks.js';
2424
import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js';
2525

2626
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
@@ -40,6 +40,12 @@ import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry.js';
4040
import { TerminalMenuBarGroup } from '../../terminal/browser/terminalMenus.js';
4141
import { isString } from '../../../../base/common/types.js';
4242
import { promiseWithResolvers } from '../../../../base/common/async.js';
43+
import { Codicon } from '../../../../base/common/codicons.js';
44+
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
45+
46+
import { TerminalContextKeys } from '../../terminal/common/terminalContextKey.js';
47+
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
48+
import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js';
4349

4450
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
4551
workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually);
@@ -559,3 +565,33 @@ configurationRegistry.registerConfiguration({
559565
},
560566
}
561567
});
568+
569+
export const rerunTaskIcon = registerIcon('rerun-task', Codicon.rerunTask, nls.localize('rerunTaskIcon', 'View icon of the rerun task.'));
570+
export const RerunForActiveTerminalCommandId = 'workbench.action.tasks.rerunForActiveTerminal';
571+
registerAction2(class extends Action2 {
572+
constructor() {
573+
super({
574+
id: RerunForActiveTerminalCommandId,
575+
icon: rerunTaskIcon,
576+
title: nls.localize2('workbench.action.tasks.rerunForActiveTerminal', 'Rerun Task'),
577+
precondition: TASK_TERMINAL_ACTIVE,
578+
menu: [{ id: MenuId.TerminalInstanceContext, when: TASK_TERMINAL_ACTIVE }],
579+
keybinding: {
580+
when: TerminalContextKeys.focus,
581+
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyR,
582+
mac: {
583+
primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyR
584+
},
585+
weight: KeybindingWeight.WorkbenchContrib
586+
}
587+
});
588+
}
589+
async run(accessor: ServicesAccessor, args: any): Promise<void> {
590+
const terminalService = accessor.get(ITerminalService);
591+
const taskSystem = accessor.get(ITaskService);
592+
const instance = args as ITerminalInstance ?? terminalService.activeInstance;
593+
if (instance) {
594+
await taskSystem.rerun(instance.instanceId);
595+
}
596+
}
597+
});

Diff for: src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { TaskTerminalStatus } from './taskTerminalStatus.js';
4040
import { ProblemCollectorEventKind, ProblemHandlingStrategy, StartStopProblemCollector, WatchingProblemCollector } from '../common/problemCollectors.js';
4141
import { GroupKind } from '../common/taskConfiguration.js';
4242
import { IResolveSet, IResolvedVariables, ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskSystemInfoResolver, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers } from '../common/taskSystem.js';
43-
import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IConfigurationProperties, IExtensionTaskSource, IPresentationOptions, IShellConfiguration, IShellQuotingOptions, ITaskEvent, InMemoryTask, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, Task, TaskEvent, TaskEventKind, TaskScope, TaskSourceKind } from '../common/tasks.js';
43+
import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IConfigurationProperties, IExtensionTaskSource, IPresentationOptions, IShellConfiguration, IShellQuotingOptions, ITaskEvent, InMemoryTask, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, TASK_TERMINAL_ACTIVE, Task, TaskEvent, TaskEventKind, TaskScope, TaskSourceKind } from '../common/tasks.js';
4444
import { ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js';
4545
import { VSCodeOscProperty, VSCodeOscPt, VSCodeSequence } from '../../terminal/browser/terminalEscapeSequences.js';
4646
import { TerminalProcessExtHostProxy } from '../../terminal/browser/terminalProcessExtHostProxy.js';
@@ -50,6 +50,8 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm
5050
import { IOutputService } from '../../../services/output/common/output.js';
5151
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
5252
import { IPathService } from '../../../services/path/common/pathService.js';
53+
import { RerunForActiveTerminalCommandId, rerunTaskIcon } from './task.contribution.js';
54+
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
5355

5456
interface ITerminalData {
5557
terminal: ITerminalInstance;
@@ -197,6 +199,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
197199
private _hasReconnected: boolean = false;
198200
private readonly _onDidStateChange: Emitter<ITaskEvent>;
199201
private _reconnectedTerminals: ITerminalInstance[] | undefined;
202+
private _terminalTabActions = [{ id: RerunForActiveTerminalCommandId, label: nls.localize('rerunTask', 'Rerun Task'), icon: rerunTaskIcon }];
203+
private _taskTerminalActive: IContextKey<boolean>;
200204

201205
taskShellIntegrationStartSequence(cwd: string | URI | undefined): string {
202206
return (
@@ -231,6 +235,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
231235
private _viewDescriptorService: IViewDescriptorService,
232236
private _logService: ILogService,
233237
private _notificationService: INotificationService,
238+
contextKeyService: IContextKeyService,
234239
instantiationService: IInstantiationService,
235240
taskSystemInfoResolver: ITaskSystemInfoResolver,
236241
) {
@@ -244,6 +249,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
244249
this._onDidStateChange = new Emitter();
245250
this._taskSystemInfoResolver = taskSystemInfoResolver;
246251
this._register(this._terminalStatusManager = instantiationService.createInstance(TaskTerminalStatus));
252+
this._taskTerminalActive = TASK_TERMINAL_ACTIVE.bindTo(contextKeyService);
253+
this._register(this._terminalService.onDidChangeActiveInstance((e) => this._taskTerminalActive.set(e?.shellLaunchConfig.type === 'Task')));
247254
}
248255

249256
public get onDidStateChange(): Event<ITaskEvent> {
@@ -1261,6 +1268,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
12611268
}
12621269
shellLaunchConfig.isFeatureTerminal = true;
12631270
shellLaunchConfig.useShellEnvironment = true;
1271+
shellLaunchConfig.tabActions = this._terminalTabActions;
12641272
return shellLaunchConfig;
12651273
}
12661274

@@ -1461,6 +1469,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
14611469
const terminalData = { terminal: terminal, lastTask: taskKey, group };
14621470
terminal.onDisposed(() => this._deleteTaskAndTerminal(terminal, terminalData));
14631471
this._terminals[terminalKey] = terminalData;
1472+
terminal.shellLaunchConfig.tabActions = this._terminalTabActions;
14641473
return [terminal, undefined];
14651474
}
14661475

@@ -1823,6 +1832,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
18231832
return 'other';
18241833
}
18251834

1835+
public getTaskForTerminal(instanceId: number): Task | undefined {
1836+
for (const key in this._activeTasks) {
1837+
const activeTask = this._activeTasks[key];
1838+
if (activeTask.terminal?.instanceId === instanceId) {
1839+
return activeTask.task;
1840+
}
1841+
}
1842+
return undefined;
1843+
}
1844+
18261845
private _appendOutput(output: string): void {
18271846
const outputChannel = this._outputService.getChannel(this._outputChannelId);
18281847
outputChannel?.append(output);

Diff for: src/vs/workbench/contrib/tasks/common/taskService.ts

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface ITaskService {
7777
getBusyTasks(): Promise<Task[]>;
7878
terminate(task: Task): Promise<ITaskTerminateResponse>;
7979
tasks(filter?: ITaskFilter): Promise<Task[]>;
80+
rerun(terminalInstanceId: number): void;
8081
/**
8182
* Gets tasks currently known to the task system. Unlike {@link tasks},
8283
* this does not activate extensions or prompt for workspace trust.

Diff for: src/vs/workbench/contrib/tasks/common/taskSystem.ts

+1
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,5 @@ export interface ITaskSystem {
116116
revealTask(task: Task): boolean;
117117
customExecutionComplete(task: Task, result: number): Promise<void>;
118118
isTaskVisible(task: Task): boolean;
119+
getTaskForTerminal(instanceId: number): Task | undefined;
119120
}

Diff for: src/vs/workbench/contrib/tasks/common/tasks.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { TerminalExitReason } from '../../../../platform/terminal/common/termina
2323
export const USER_TASKS_GROUP_KEY = 'settings';
2424

2525
export const TASK_RUNNING_STATE = new RawContextKey<boolean>('taskRunning', false, nls.localize('tasks.taskRunningContext', "Whether a task is currently running."));
26+
/** Whether the active terminal is a task terminal. */
27+
export const TASK_TERMINAL_ACTIVE = new RawContextKey<boolean>('taskTerminalActive', false, nls.localize('taskTerminalActive', "Whether the active terminal is a task terminal."));
2628
export const TASKS_CATEGORY = nls.localize2('tasksCategory', "Tasks");
2729

2830
export enum ShellQuoting {

Diff for: src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack
188188
useShellEnvironment: shellLaunchConfig.useShellEnvironment,
189189
reconnectionProperties: shellLaunchConfig.reconnectionProperties,
190190
type: shellLaunchConfig.type,
191-
isFeatureTerminal: shellLaunchConfig.isFeatureTerminal
191+
isFeatureTerminal: shellLaunchConfig.isFeatureTerminal,
192+
tabActions: shellLaunchConfig.tabActions,
192193
};
193194
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot();
194195

Diff for: src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

+4
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
419419
this._shellLaunchConfig.type = this._shellLaunchConfig.attachPersistentProcess.type;
420420
}
421421

422+
if (this._shellLaunchConfig.attachPersistentProcess?.tabActions) {
423+
this._shellLaunchConfig.tabActions = this._shellLaunchConfig.attachPersistentProcess.tabActions;
424+
}
425+
422426
if (this.shellLaunchConfig.cwd) {
423427
const cwdUri = typeof this._shellLaunchConfig.cwd === 'string' ? URI.from({
424428
scheme: Schemas.file,

Diff for: src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { getColorForSeverity } from './terminalStatusList.js';
5252
import { TerminalContextActionRunner } from './terminalContextMenu.js';
5353
import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js';
5454
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
55+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
5556

5657
const $ = DOM.$;
5758

@@ -257,7 +258,8 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer<ITerminal
257258
@IKeybindingService private readonly _keybindingService: IKeybindingService,
258259
@IListService private readonly _listService: IListService,
259260
@IThemeService private readonly _themeService: IThemeService,
260-
@IContextViewService private readonly _contextViewService: IContextViewService
261+
@IContextViewService private readonly _contextViewService: IContextViewService,
262+
@ICommandService private readonly _commandService: ICommandService,
261263
) {
262264
super();
263265
}
@@ -505,6 +507,13 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer<ITerminal
505507
this._runForSelectionOrInstance(instance, e => this._terminalService.safeDisposeTerminal(e));
506508
}))
507509
];
510+
if (instance.shellLaunchConfig.tabActions) {
511+
for (const action of instance.shellLaunchConfig.tabActions) {
512+
actions.push(this._register(new Action(action.id, action.label, action.icon ? ThemeIcon.asClassName(action.icon) : undefined, true, async () => {
513+
this._runForSelectionOrInstance(instance, e => this._commandService.executeCommand(action.id, instance));
514+
})));
515+
}
516+
}
508517
// TODO: Cache these in a way that will use the correct instance
509518
template.actionBar.clear();
510519
for (const action of actions) {

Diff for: src/vs/workbench/contrib/terminal/browser/terminalView.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,9 @@ class TerminalThemeIconStyle extends Themable {
640640
}
641641
const color = colorTheme.getColor(instance.color);
642642
if (color) {
643-
// exclude status icons (file-icon) and inline action icons (trashcan and horizontalSplit)
643+
// exclude status icons (file-icon) and inline action icons (trashcan, horizontalSplit, rerunTask)
644644
css += (
645-
`.monaco-workbench .${colorClass} .codicon:first-child:not(.codicon-split-horizontal):not(.codicon-trashcan):not(.file-icon)` +
645+
`.monaco-workbench .${colorClass} .codicon:first-child:not(.codicon-split-horizontal):not(.codicon-trashcan):not(.file-icon):not(.codicon-rerun-task)` +
646646
`{ color: ${color} !important; }`
647647
);
648648
}

0 commit comments

Comments
 (0)