Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Display initial loading message in terminal #6957

Closed
wants to merge 4 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/task/src/browser/task-frontend-module.ts
Original file line number Diff line number Diff line change
@@ -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 { TaskTerminal } from './task-terminal';

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(TaskTerminal).toSelf().inSingletonScope();

bindProcessTaskModule(bind);
bindTaskPreferences(bind);
35 changes: 14 additions & 21 deletions packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
@@ -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 { TaskTerminal } from './task-terminal';

export interface QuickPickProblemMatcherItem {
problemMatchers: NamedProblemMatcher[] | undefined;
@@ -169,6 +170,10 @@ export class TaskService implements TaskConfigurationClient {

@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;

@inject(TaskTerminal)
protected readonly taskTerminal: TaskTerminal;

/**
* @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<TaskInfo | undefined> {
// Open an empty terminal, display a message informing the user that terminal is starting
await this.taskTerminal.openEmptyTerminal(taskLabel);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that code below can exit without reaching attach ever what should happen with the terminal then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elaihau can a task be executed without the terminal? background tasks?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vitaliy-guliy
Maybe it's not good place for creating a new terminal widget, for example here a task can be terminated or restarted, so how is a new terminal widget handled for this case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elaihau can a task be executed without the terminal? background tasks?

@akosyakov sorry for replying so late.
as far as I can tell, we create terminalProcess for all tasks.

@marechal-p please correct me if i am wrong


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) {
@@ -972,29 +981,13 @@ export class TaskService implements TaskConfigurationClient {
async attach(processId: number, taskId: number): Promise<void> {
// 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 = <TerminalWidget>await this.widgetManager.getOrCreateWidget(
TERMINAL_WIDGET_FACTORY_ID,
<TerminalWidgetFactoryOptions>{
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.taskTerminal.attach(processId, taskId, taskInfo, this.getTerminalWidgetId(processId));
}

private getTerminalWidgetId(terminalId: number): string {
99 changes: 99 additions & 0 deletions packages/task/src/browser/task-terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/********************************************************************************
* 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 TaskTerminal {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskTerminal will mean that it provides an access to a terminal for a task. This class seems to be responsible to manage many tasks terminals. TakTerminalManager or TaskTerminalService would be the better name. Also it will be aligned with other wrappers around WidgetManager, e.g. EditorManager, TerminalService and so on

@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use termialservice, it is responsible to create terminals and hides the logic of working with widhermanager


@inject(ApplicationShell)
protected readonly shell: ApplicationShell;

protected terminalWidgets: TerminalWidget[] = [];
Copy link
Member

@akosyakov akosyakov Jan 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a single place which keeps references to all widget. It’s WidgetManager, please use its methods to look up terminal widgets, don’t cache it. Caching is common cause of race conditions, memory leaks and so on


async openEmptyTerminal(taskLabel: string): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about returning a widget id here and passing it to attach later to avoid guessing?

const id = TERMINAL_WIDGET_FACTORY_ID + '-connecting';

const widget = <TerminalWidget>await this.widgetManager.getOrCreateWidget(
TERMINAL_WIDGET_FACTORY_ID,
<TerminalWidgetFactoryOptions>{
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);

this.terminalWidgets.push(widget);
}

private findWidget(taskInfo: TaskInfo | undefined): TerminalWidget | undefined {
if (taskInfo && taskInfo.config.label) {
for (let i = 0; i < this.terminalWidgets.length; i++) {
const w = this.terminalWidgets[i];
if (taskInfo.config.label === w.title.label) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think label usage to find the corresponding widget is not enough: user can have tasks with the same label, but different source or type, for example.

this.terminalWidgets.splice(i, 1);
return w;
}
}
}

return undefined;
}

async attach(processId: number, taskId: number, taskInfo: TaskInfo | undefined, terminalWidgetId: string): Promise<void> {
let widget: TerminalWidget | undefined = this.findWidget(taskInfo);
if (widget) {
widget.id = terminalWidgetId;
} else {
widget = <TerminalWidget>await this.widgetManager.getOrCreateWidget(
TERMINAL_WIDGET_FACTORY_ID,
<TerminalWidgetFactoryOptions>{
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);
}

}
5 changes: 5 additions & 0 deletions packages/terminal/src/browser/base/terminal-widget.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test reload page reload. What would you expect should happen? We try to preserve terminals. I assume terminal should appear immediately with loading message till connection to the task process is not recovered. We need ways to test it with the vanilla Theia.

}
25 changes: 25 additions & 0 deletions packages/terminal/src/browser/terminal-widget-impl.ts
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
protected waitForConnection: Deferred<MessageConnection> | undefined;
protected hoverMessage: HTMLDivElement;
protected lastTouchEnd: TouchEvent | undefined;
protected loadingMessage: HTMLDivElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you considering using some icon-loader for terminal tab till connection to the task process is not recovered instead of a loading message?
Like here, for example:

loader-icon


@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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use css, you can find css guidance in our coding guidelines

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;