diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index 71e002c103aa0..31c46f18e9eb8 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -39,6 +39,7 @@ import './tasks-monaco-contribution'; import { TaskNameResolver } from './task-name-resolver'; import { TaskSourceResolver } from './task-source-resolver'; import { TaskTemplateSelector } from './task-templates'; +import { TaskTerminalManager } from './task-terminal-manager'; export default new ContainerModule(bind => { bind(TaskFrontendContribution).toSelf().inSingletonScope(); @@ -77,6 +78,7 @@ export default new ContainerModule(bind => { bind(TaskNameResolver).toSelf().inSingletonScope(); bind(TaskSourceResolver).toSelf().inSingletonScope(); bind(TaskTemplateSelector).toSelf().inSingletonScope(); + bind(TaskTerminalManager).toSelf().inSingletonScope(); bindProcessTaskModule(bind); bindTaskPreferences(bind); diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 8b81387706505..dc825012adbb1 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -60,6 +60,7 @@ import { TaskSchemaUpdater } from './task-schema-updater'; import { TaskConfigurationManager } from './task-configuration-manager'; import { PROBLEMS_WIDGET_ID, ProblemWidget } from '@theia/markers/lib/browser/problem/problem-widget'; import { TaskNode } from './task-node'; +import { TaskTerminalManager } from './task-terminal-manager'; export interface QuickPickProblemMatcherItem { problemMatchers: NamedProblemMatcher[] | undefined; @@ -169,6 +170,10 @@ export class TaskService implements TaskConfigurationClient { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + + @inject(TaskTerminalManager) + protected readonly taskTerminalManager: TaskTerminalManager; + /** * @deprecated To be removed in 0.5.0 */ @@ -454,6 +459,9 @@ export class TaskService implements TaskConfigurationClient { * It looks for configured and detected tasks. */ async run(source: string, taskLabel: string, scope?: string): Promise { + // Open an empty terminal, display a message informing the user that terminal is starting + const widget = await this.taskTerminalManager.openEmptyTerminal(taskLabel); + let task = await this.getProvidedTask(source, taskLabel, scope); if (!task) { // if a detected task cannot be found, search from tasks.json task = this.taskConfigurations.getTask(source, taskLabel); @@ -462,6 +470,7 @@ export class TaskService implements TaskConfigurationClient { return; } } + const customizationObject = await this.getTaskCustomization(task); if (!customizationObject.problemMatcher) { @@ -502,7 +511,7 @@ export class TaskService implements TaskConfigurationClient { return undefined; } return this.runTasksGraph(task, tasks, { - customization: { ...customizationObject, ...{ problemMatcher: resolvedMatchers } } + customization: { ...customizationObject, ...{ problemMatcher: resolvedMatchers }, widget} }).catch(error => { console.log(error.message); return undefined; @@ -513,7 +522,8 @@ export class TaskService implements TaskConfigurationClient { * A recursive function that runs a task and all its sub tasks that it depends on. * A task can be executed only when all of its dependencies have been executed, or when it doesn’t have any dependencies at all. */ - async runTasksGraph(task: TaskConfiguration, tasks: TaskConfiguration[], option?: RunTaskOption): Promise { + async runTasksGraph(task: TaskConfiguration, tasks: TaskConfiguration[], + option?: RunTaskOption, widget?: TerminalWidget): Promise { if (task && task.dependsOn) { // In case it is an array of task dependencies if (Array.isArray(task.dependsOn) && task.dependsOn.length > 0) { @@ -528,7 +538,7 @@ export class TaskService implements TaskConfigurationClient { // In case the 'dependsOrder' is 'sequence' if (task.dependsOrder && task.dependsOrder === DependsOrder.Sequence) { await this.runTasksGraph(dependentTask, tasks, { - customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers } } + customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers }, widget } }); } } @@ -536,7 +546,7 @@ export class TaskService implements TaskConfigurationClient { if (((!task.dependsOrder) || (task.dependsOrder && task.dependsOrder === DependsOrder.Parallel))) { const promises = dependentTasks.map(item => this.runTasksGraph(item.task, tasks, { - customization: { ...item.taskCustomization, ...{ problemMatcher: item.resolvedMatchers } } + customization: { ...item.taskCustomization, ...{ problemMatcher: item.resolvedMatchers }, widget } }) ); await Promise.all(promises); @@ -548,12 +558,12 @@ export class TaskService implements TaskConfigurationClient { const taskCustomization = await this.getTaskCustomization(dependentTask); const resolvedMatchers = await this.resolveProblemMatchers(dependentTask, taskCustomization); await this.runTasksGraph(dependentTask, tasks, { - customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers } } + customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers }, widget } }); } } - const taskInfo = await this.runTask(task, option); + const taskInfo = await this.runTask(task, option, widget); if (taskInfo) { const getExitCodePromise: Promise = this.getExitCode(taskInfo.taskId).then(result => ({ taskEndedType: TaskEndedTypes.TaskExited, value: result })); const isBackgroundTaskEndedPromise: Promise = this.isBackgroundTaskEnded(taskInfo.taskId).then(result => @@ -678,7 +688,7 @@ export class TaskService implements TaskConfigurationClient { } } - async runTask(task: TaskConfiguration, option?: RunTaskOption): Promise { + async runTask(task: TaskConfiguration, option?: RunTaskOption, widget?: TerminalWidget): Promise { const runningTasksInfo: TaskInfo[] = await this.getRunningTasks(); // check if the task is active @@ -706,7 +716,7 @@ export class TaskService implements TaskConfigurationClient { return this.restartTask(matchedRunningTaskInfo, option); } } else { // run task as the task is not active - return this.doRunTask(task, option); + return this.doRunTask(task, option, widget); } } @@ -728,7 +738,7 @@ export class TaskService implements TaskConfigurationClient { return this.doRunTask(activeTaskInfo.config, option); } - protected async doRunTask(task: TaskConfiguration, option?: RunTaskOption): Promise { + protected async doRunTask(task: TaskConfiguration, option?: RunTaskOption, widget?: TerminalWidget): Promise { if (option && option.customization) { const taskDefinition = this.taskDefinitionRegistry.getDefinition(task); if (taskDefinition) { // use the customization object to override the task config @@ -745,7 +755,7 @@ export class TaskService implements TaskConfigurationClient { if (resolvedTask) { // remove problem markers from the same source before running the task await this.removeProblemMarks(option); - return this.runResolvedTask(resolvedTask, option); + return this.runResolvedTask(resolvedTask, option, widget); } } @@ -893,7 +903,8 @@ export class TaskService implements TaskConfigurationClient { * @param resolvedTask the resolved task * @param option options to run the resolved task */ - private async runResolvedTask(resolvedTask: TaskConfiguration, option?: RunTaskOption): Promise { + private async runResolvedTask(resolvedTask: TaskConfiguration, option?: RunTaskOption, + widget?: TerminalWidget): Promise { const source = resolvedTask._source; const taskLabel = resolvedTask.label; try { @@ -908,8 +919,9 @@ export class TaskService implements TaskConfigurationClient { * Reason: Maybe a new task type wants to also be displayed in a terminal. */ if (typeof taskInfo.terminalId === 'number') { - this.attach(taskInfo.terminalId, taskInfo.taskId); + this.attach(taskInfo.terminalId, taskInfo.taskId, widget); } + return taskInfo; } catch (error) { const errorStr = `Error launching task '${taskLabel}': ${error.message}`; @@ -969,32 +981,16 @@ export class TaskService implements TaskConfigurationClient { terminal.sendText(selectedText); } - async attach(processId: number, taskId: number): Promise { + async attach(processId: number, taskId: number, widget?: TerminalWidget): Promise { // Get the list of all available running tasks. const runningTasks: TaskInfo[] = await this.getRunningTasks(); + // Get the corresponding task information based on task id if available. const taskInfo: TaskInfo | undefined = runningTasks.find((t: TaskInfo) => t.taskId === taskId); - // Create terminal widget to display an execution output of a task that was launched as a command inside a shell. - const widget = await this.widgetManager.getOrCreateWidget( - TERMINAL_WIDGET_FACTORY_ID, - { - created: new Date().toString(), - id: this.getTerminalWidgetId(processId), - title: taskInfo - ? `Task: ${taskInfo.config.label}` - : `Task: #${taskId}`, - destroyTermOnClose: true - } - ); - this.shell.addWidget(widget, { area: 'bottom' }); - if (taskInfo && taskInfo.config.presentation && taskInfo.config.presentation.reveal === RevealKind.Always) { - if (taskInfo.config.presentation.focus) { // assign focus to the terminal if presentation.focus is true - this.shell.activateWidget(widget.id); - } else { // show the terminal but not assign focus - this.shell.revealWidget(widget.id); - } - } - widget.start(processId); + + // Attach terminal to the running task + // to display an execution output of a task that was launched as a command inside a shell. + this.taskTerminalManager.attach(processId, taskId, taskInfo, this.getTerminalWidgetId(processId), widget); } private getTerminalWidgetId(terminalId: number): string { diff --git a/packages/task/src/browser/task-terminal-manager.ts b/packages/task/src/browser/task-terminal-manager.ts new file mode 100644 index 0000000000000..5baea3817a2a7 --- /dev/null +++ b/packages/task/src/browser/task-terminal-manager.ts @@ -0,0 +1,80 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { TerminalWidgetFactoryOptions, TERMINAL_WIDGET_FACTORY_ID } from '@theia/terminal/lib/browser/terminal-widget-impl'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { ApplicationShell, WidgetManager } from '@theia/core/lib/browser'; +import { TaskInfo, RevealKind } from '../common'; + +@injectable() +export class TaskTerminalManager { + + @inject(WidgetManager) + protected readonly widgetManager: WidgetManager; + + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + + async openEmptyTerminal(taskLabel: string): Promise { + const id = TERMINAL_WIDGET_FACTORY_ID + '-connecting'; + + const widget = await this.widgetManager.getOrCreateWidget( + TERMINAL_WIDGET_FACTORY_ID, + { + created: new Date().toString(), + id: id, + title: taskLabel, + destroyTermOnClose: true, + loadingMessage: `Task '${taskLabel}' - Connecting...` + } + ); + + this.shell.addWidget(widget, { area: 'bottom' }); + this.shell.revealWidget(widget.id); + return widget; + } + + async attach(processId: number, taskId: number, taskInfo: TaskInfo | undefined, + terminalWidgetId: string, widget?: TerminalWidget): Promise { + if (!widget) { + widget = await this.widgetManager.getOrCreateWidget( + TERMINAL_WIDGET_FACTORY_ID, + { + created: new Date().toString(), + id: terminalWidgetId, + title: taskInfo + ? `Task: ${taskInfo.config.label}` + : `Task: #${taskId}`, + destroyTermOnClose: true + } + ); + + this.shell.addWidget(widget, { area: 'bottom' }); + } + + if (taskInfo && taskInfo.config.presentation && taskInfo.config.presentation.reveal === RevealKind.Always) { + if (taskInfo.config.presentation.focus) { // assign focus to the terminal if presentation.focus is true + this.shell.activateWidget(widget.id); + } else { // show the terminal but not assign focus + this.shell.revealWidget(widget.id); + } + } + + widget.start(processId); + } + +} diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index 8f04abf5aa0b0..77f2aed6b8e3f 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -106,4 +106,9 @@ export interface TerminalWidgetOptions { * Terminal attributes. Can be useful to apply some implementation specific information. */ readonly attributes?: { [key: string]: string | null }; + + /** + * Initial message to be displayed while terminal is not started. + */ + readonly loadingMessage?: string; } diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 82c82102c8477..08da339838b09 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -54,6 +54,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected waitForConnection: Deferred | undefined; protected hoverMessage: HTMLDivElement; protected lastTouchEnd: TouchEvent | undefined; + protected loadingMessage: HTMLDivElement; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(WebSocketConnectionProvider) protected readonly webSocketConnectionProvider: WebSocketConnectionProvider; @@ -193,6 +194,24 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget for (const contribution of this.terminalContributionProvider.getContributions()) { contribution.onCreate(this); } + + if (this.options.loadingMessage) { + this.loadingMessage = document.createElement('div'); + this.loadingMessage.style.zIndex = '20'; + this.loadingMessage.style.display = 'block'; + this.loadingMessage.style.position = 'absolute'; + this.loadingMessage.style.left = '0px'; + this.loadingMessage.style.right = '0px'; + this.loadingMessage.style.top = '30%'; + this.loadingMessage.style.textAlign = 'center'; + this.loadingMessage.style.color = 'var(--theia-editorWidget-foreground)'; + + const text = document.createElement('pre'); + text.textContent = this.options.loadingMessage; + this.loadingMessage.appendChild(text); + + this.node.appendChild(this.loadingMessage); + } } /** @@ -280,6 +299,12 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.terminalId = typeof id !== 'number' ? await this.createTerminal() : await this.attachTerminal(id); this.resizeTerminalProcess(); this.connectTerminalProcess(); + + // Hide loading message after starting the terminal. + if (this.loadingMessage) { + this.loadingMessage.style.display = 'none'; + } + if (IBaseTerminalServer.validateId(this.terminalId)) { this.onDidOpenEmitter.fire(undefined); return this.terminalId;