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

Implement CustomExecution extension API #9189

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
60 changes: 52 additions & 8 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export interface CommandRegistryExt {
export interface TerminalServiceExt {
$terminalCreated(id: string, name: string): void;
$terminalNameChanged(id: string, name: string): void;
$terminalOpened(id: string, processId: number, cols: number, rows: number): void;
$terminalOpened(id: string, processId: number, terminalId: number, cols: number, rows: number): void;
$terminalClosed(id: string): void;
$terminalOnInput(id: string, data: string): void;
$terminalSizeChanged(id: string, cols: number, rows: number): void;
Expand Down Expand Up @@ -283,46 +283,89 @@ export interface TerminalServiceMain {

/**
* Send text to the terminal by id.
* @param id - terminal id.
* @param id - terminal widget id.
* @param text - text content.
* @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line.
*/
$sendText(id: string, text: string, addNewLine?: boolean): void;

/**
* Write data to the terminal by id.
* @param id - terminal id.
* @param id - terminal widget id.
* @param data - data.
*/
$write(id: string, data: string): void;

/**
* Resize the terminal by id.
* @param id - terminal id.
* @param id - terminal widget id.
* @param cols - columns.
* @param rows - rows.
*/
$resize(id: string, cols: number, rows: number): void;

/**
* Show terminal on the UI panel.
* @param id - terminal id.
* @param id - terminal widget id.
* @param preserveFocus - set terminal focus in case true value, and don't set focus otherwise.
*/
$show(id: string, preserveFocus?: boolean): void;

/**
* Hide UI panel where is located terminal widget.
* @param id - terminal id.
* @param id - terminal widget id.
*/
$hide(id: string): void;

/**
* Destroy terminal.
* @param id - terminal id.
* @param id - terminal widget id.
*/
$dispose(id: string): void;

/**
* Send text to the terminal by id.
* @param id - terminal id.
* @param text - text content.
* @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line.
*/
$sendTextByTerminalId(id: number, text: string, addNewLine?: boolean): void;

/**
* Write data to the terminal by id.
* @param id - terminal id.
* @param data - data.
*/
$writeByTerminalId(id: number, data: string): void;

/**
* Resize the terminal by id.
* @param id - terminal id.
* @param cols - columns.
* @param rows - rows.
*/
$resizeByTerminalId(id: number, cols: number, rows: number): void;

/**
* Show terminal on the UI panel.
* @param id - terminal id.
* @param preserveFocus - set terminal focus in case true value, and don't set focus otherwise.
*/
$showByTerminalId(id: number, preserveFocus?: boolean): void;

/**
* Hide UI panel where is located terminal widget.
* @param id - terminal id.
*/
$hideByTerminalId(id: number): void;

/**
* Destroy terminal.
* @param id - terminal id.
* @param waitOnExit - Whether to wait for a key press before closing the terminal.
*/
$disposeByTerminalId(id: number, waitOnExit?: boolean | string): void;

$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void;
}

Expand Down Expand Up @@ -1718,7 +1761,7 @@ export const MAIN_RPC_CONTEXT = {
export interface TasksExt {
$provideTasks(handle: number, token?: CancellationToken): Promise<TaskDto[] | undefined>;
$resolveTask(handle: number, task: TaskDto, token?: CancellationToken): Promise<TaskDto | undefined>;
$onDidStartTask(execution: TaskExecutionDto): void;
$onDidStartTask(execution: TaskExecutionDto, terminalId: number): void;
$onDidEndTask(id: number): void;
$onDidStartTaskProcess(processId: number | undefined, execution: TaskExecutionDto): void;
$onDidEndTaskProcess(exitCode: number | undefined, taskId: number): void;
Expand All @@ -1731,6 +1774,7 @@ export interface TasksMain {
$taskExecutions(): Promise<TaskExecutionDto[]>;
$unregister(handle: number): void;
$terminateTask(id: number): void;
$customExecutionComplete(id: number, exitCode: number | undefined): void;
}

export interface AuthenticationExt {
Expand Down
6 changes: 5 additions & 1 deletion packages/plugin-ext/src/main/browser/tasks-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class TasksMainImpl implements TasksMain, Disposable {
this.proxy.$onDidStartTask({
id: event.taskId,
task: this.fromTaskConfiguration(event.config)
});
}, event.terminalId!);
}));

this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => {
Expand Down Expand Up @@ -177,6 +177,10 @@ export class TasksMainImpl implements TasksMain, Disposable {
this.taskService.kill(id);
}

async $customExecutionComplete(id: number, exitCode: number | undefined): Promise<void> {
this.taskService.customExecutionComplete(id, exitCode);
}

protected createTaskProvider(handle: number): TaskProvider {
return {
provideTasks: () =>
Expand Down
56 changes: 55 additions & 1 deletion packages/plugin-ext/src/main/browser/terminal-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable
this.toDispose.push(Disposable.create(() => terminal.title.changed.disconnect(updateTitle)));

const updateProcessId = () => terminal.processId.then(
processId => this.extProxy.$terminalOpened(terminal.id, processId, terminal.dimensions.cols, terminal.dimensions.rows),
processId => this.extProxy.$terminalOpened(terminal.id, processId, terminal.terminalId, terminal.dimensions.cols, terminal.dimensions.rows),
() => {/* no-op */ }
);
updateProcessId();
Expand Down Expand Up @@ -174,4 +174,58 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable
terminal.dispose();
}
}

$sendTextByTerminalId(id: number, text: string, addNewLine?: boolean): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
text = text.replace(/\r?\n/g, '\r');
if (addNewLine && text.charAt(text.length - 1) !== '\r') {
text += '\r';
}
terminal.sendText(text);
}
}
$writeByTerminalId(id: number, data: string): void {
const terminal = this.terminals.getByTerminalId(id);
if (!terminal) {
return;
}
terminal.write(data);
}
$resizeByTerminalId(id: number, cols: number, rows: number): void {
const terminal = this.terminals.getByTerminalId(id);
if (!terminal) {
return;
}
terminal.resize(cols, rows);
}
$showByTerminalId(id: number, preserveFocus?: boolean): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
const options: WidgetOpenerOptions = {};
if (preserveFocus) {
options.mode = 'reveal';
}
this.terminals.open(terminal, options);
}
}
$hideByTerminalId(id: number): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal && terminal.isVisible) {
const area = this.shell.getAreaFor(terminal);
if (area) {
this.shell.collapsePanel(area);
}
}
}
$disposeByTerminalId(id: number, waitOnExit?: boolean | string): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
if (waitOnExit) {
terminal.waitOnExit(waitOnExit);
return;
}
terminal.dispose();
}
}
}
4 changes: 3 additions & 1 deletion packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
ShellQuoting,
ShellExecution,
ProcessExecution,
CustomExecution,
TaskScope,
TaskPanelKind,
TaskRevealKind,
Expand Down Expand Up @@ -194,7 +195,7 @@ export function createAPIFactory(
const outputChannelRegistryExt = rpc.set(MAIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_EXT, new OutputChannelRegistryExtImpl(rpc));
const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents, commandRegistry));
const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry));
const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc));
const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc, terminalExt));
const connectionExt = rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc));
const fileSystemExt = rpc.set(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT, new FileSystemExtImpl(rpc, languagesExt));
const extHostFileSystemEvent = rpc.set(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpc, editorsAndDocumentsExt));
Expand Down Expand Up @@ -910,6 +911,7 @@ export function createAPIFactory(
ShellQuoting,
ShellExecution,
ProcessExecution,
CustomExecution,
TaskScope,
TaskRevealKind,
TaskPanelKind,
Expand Down
86 changes: 82 additions & 4 deletions packages/plugin-ext/src/plugin/tasks/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ import {
} from '../../common/plugin-api-rpc';
import * as theia from '@theia/plugin';
import * as converter from '../type-converters';
import { Disposable } from '../types-impl';
import { CustomExecution, Disposable } from '../types-impl';
import { RPCProtocol, ConnectionClosedError } from '../../common/rpc-protocol';
import { TaskProviderAdapter } from './task-provider';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { TerminalServiceExtImpl } from '../terminal-ext';

export class TasksExtImpl implements TasksExt {
private proxy: TasksMain;

private callId = 0;
private adaptersMap = new Map<number, TaskProviderAdapter>();
private executions = new Map<number, theia.TaskExecution>();
protected providedCustomExecutions: Map<number, CustomExecution>;
protected notProvidedCustomExecutions: Set<number>;
protected activeCustomExecutions: Map<number, CustomExecution>;
protected lastStartedTask: number | undefined;

private readonly onDidExecuteTask: Emitter<theia.TaskStartEvent> = new Emitter<theia.TaskStartEvent>();
private readonly onDidTerminateTask: Emitter<theia.TaskEndEvent> = new Emitter<theia.TaskEndEvent>();
Expand All @@ -42,8 +47,11 @@ export class TasksExtImpl implements TasksExt {

private disposed = false;

constructor(rpc: RPCProtocol) {
constructor(rpc: RPCProtocol, readonly terminalExt: TerminalServiceExtImpl) {
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TASKS_MAIN);
this.providedCustomExecutions = new Map<number, CustomExecution>();
this.notProvidedCustomExecutions = new Set<number>();
this.activeCustomExecutions = new Map<number, CustomExecution>();
this.fetchTaskExecutions();
}

Expand All @@ -59,7 +67,28 @@ export class TasksExtImpl implements TasksExt {
return this.onDidExecuteTask.event;
}

$onDidStartTask(execution: TaskExecutionDto): void {
async $onDidStartTask(execution: TaskExecutionDto, terminalId: number): Promise<void> {
const customExecution: CustomExecution | undefined = this.providedCustomExecutions.get(execution.task.id);
if (customExecution) {
if (this.activeCustomExecutions.get(execution.id) !== undefined) {
throw new Error('We should not be trying to start the same custom task executions twice.');
}

// Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task.
this.activeCustomExecutions.set(execution.id, customExecution);
const taskDefinition = converter.toTask(execution.task).definition;
const pty = await customExecution.callback(taskDefinition);
this.terminalExt.attachPtyToTerminal(terminalId, pty);
if (pty.onDidClose) {
const disposable = pty.onDidClose((e: number | void = undefined) => {
disposable.dispose();
// eslint-disable-next-line no-void
this.proxy.$customExecutionComplete(execution.id, e === void 0 ? undefined : e);
});
}
}
this.lastStartedTask = execution.id;

this.onDidExecuteTask.fire({
execution: this.getTaskExecution(execution)
});
Expand All @@ -76,6 +105,7 @@ export class TasksExtImpl implements TasksExt {
}

this.executions.delete(id);
this.customExecutionComplete(id);

this.onDidTerminateTask.fire({
execution: taskExecution
Expand Down Expand Up @@ -125,6 +155,12 @@ export class TasksExtImpl implements TasksExt {
async executeTask(task: theia.Task): Promise<theia.TaskExecution> {
const taskDto = converter.fromTask(task);
if (taskDto) {
// If this task is a custom execution, then we need to save it away
// in the provided custom execution map that is cleaned up after the
// task is executed.
if (CustomExecution.is(task.execution!)) {
this.addCustomExecution(taskDto, false);
}
const executionDto = await this.proxy.$executeTask(taskDto);
if (executionDto) {
const taskExecution = this.getTaskExecution(executionDto);
Expand All @@ -138,7 +174,16 @@ export class TasksExtImpl implements TasksExt {
$provideTasks(handle: number, token: theia.CancellationToken): Promise<TaskDto[] | undefined> {
const adapter = this.adaptersMap.get(handle);
if (adapter) {
return adapter.provideTasks(token);
return adapter.provideTasks(token).then(tasks => {
if (tasks) {
for (const task of tasks) {
if (task.type === 'customExecution' || task.taskType === 'customExecution') {
this.addCustomExecution(task, true);
}
}
}
return tasks;
});
} else {
return Promise.reject(new Error('No adapter found to provide tasks'));
}
Expand Down Expand Up @@ -198,4 +243,37 @@ export class TasksExtImpl implements TasksExt {
this.executions.set(executionId, result);
return result;
}

private addCustomExecution(taskDto: TaskDto, isProvided: boolean): void {
const taskId = taskDto.id;
if (!isProvided && !this.providedCustomExecutions.has(taskId)) {
this.notProvidedCustomExecutions.add(taskId);
}
this.providedCustomExecutions.set(taskDto.id, new CustomExecution(taskDto.callback));
}

private customExecutionComplete(id: number): void {
const extensionCallback2: CustomExecution | undefined = this.activeCustomExecutions.get(id);
if (extensionCallback2) {
this.activeCustomExecutions.delete(id);
}

// Technically we don't really need to do this, however, if an extension
// is executing a task through "executeTask" over and over again
// with different properties in the task definition, then the map of executions
// could grow indefinitely, something we don't want.
if (this.notProvidedCustomExecutions.has(id) && (this.lastStartedTask !== id)) {
this.providedCustomExecutions.delete(id);
this.notProvidedCustomExecutions.delete(id);
}
const iterator = this.notProvidedCustomExecutions.values();
let iteratorResult = iterator.next();
while (!iteratorResult.done) {
if (!this.activeCustomExecutions.has(iteratorResult.value) && (this.lastStartedTask !== iteratorResult.value)) {
this.providedCustomExecutions.delete(iteratorResult.value);
this.notProvidedCustomExecutions.delete(iteratorResult.value);
}
iteratorResult = iterator.next();
}
}
}
Loading