Skip to content

Commit

Permalink
Add terminal observer API (#13402)
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
  • Loading branch information
tsmaeder authored Mar 21, 2024
1 parent cd0200f commit cffeeac
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 2 deletions.
15 changes: 15 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export interface TerminalServiceExt {
$handleTerminalLink(link: ProvidedTerminalLink): Promise<void>;
getEnvironmentVariableCollection(extensionIdentifier: string): theia.GlobalEnvironmentVariableCollection;
$setShell(shell: string): void;
$reportOutputMatch(observerId: string, groups: string[]): void;
}
export interface OutputChannelRegistryExt {
createOutputChannel(name: string, pluginInfo: PluginInfo): theia.OutputChannel,
Expand Down Expand Up @@ -438,6 +439,20 @@ export interface TerminalServiceMain {
* @param providerId id of the terminal link provider to be unregistered.
*/
$unregisterTerminalLinkProvider(providerId: string): Promise<void>;

/**
* Register a new terminal observer.
* @param providerId id of the terminal link provider to be registered.
* @param nrOfLinesToMatch the number of lines to match the outputMatcherRegex against
* @param outputMatcherRegex the regex to match the output to
*/
$registerTerminalObserver(id: string, nrOfLinesToMatch: number, outputMatcherRegex: string): unknown;

/**
* Unregister the terminal observer with the specified id.
* @param providerId id of the terminal observer to be unregistered.
*/
$unregisterTerminalObserver(id: string): unknown;
}

export interface AutoFocus {
Expand Down
46 changes: 46 additions & 0 deletions packages/plugin-ext/src/main/browser/terminal-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import { getIconClass } from '../../plugin/terminal-ext';
import { PluginTerminalRegistry } from './plugin-terminal-registry';
import { CancellationToken } from '@theia/core';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
import debounce = require('@theia/core/shared/lodash.debounce');

interface TerminalObserverData {
nrOfLinesToMatch: number;
outputMatcherRegex: RegExp
disposables: DisposableCollection;
}

/**
* Plugin api service allows working with terminal emulator.
Expand All @@ -46,6 +53,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
private readonly terminalLinkProviders: string[] = [];

private readonly toDispose = new DisposableCollection();
private readonly observers = new Map<string, TerminalObserverData>();

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.terminals = container.get(TerminalService);
Expand Down Expand Up @@ -121,6 +129,8 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
this.extProxy.$terminalOnInput(terminal.id, data);
this.extProxy.$terminalStateChanged(terminal.id);
}));

this.observers.forEach((observer, id) => this.observeTerminal(id, terminal, observer));
}

$write(id: string, data: string): void {
Expand Down Expand Up @@ -293,6 +303,42 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
}
}

$registerTerminalObserver(id: string, nrOfLinesToMatch: number, outputMatcherRegex: string): void {
const observerData = {
nrOfLinesToMatch: nrOfLinesToMatch,
outputMatcherRegex: new RegExp(outputMatcherRegex, 'm'),
disposables: new DisposableCollection()
};
this.observers.set(id, observerData);
this.terminals.all.forEach(terminal => {
this.observeTerminal(id, terminal, observerData);
});
}

protected observeTerminal(observerId: string, terminal: TerminalWidget, observerData: TerminalObserverData): void {
const doMatch = debounce(() => {
const lineCount = Math.min(observerData.nrOfLinesToMatch, terminal.buffer.length);
const lines = terminal.buffer.getLines(terminal.buffer.length - lineCount, lineCount);
const result = lines.join('\n').match(observerData.outputMatcherRegex);
if (result) {
this.extProxy.$reportOutputMatch(observerId, result.map(value => value));
}
});
observerData.disposables.push(terminal.onOutput(output => {
doMatch();
}));
}

$unregisterTerminalObserver(id: string): void {
const observer = this.observers.get(id);
if (observer) {
observer.disposables.dispose();
this.observers.delete(id);
} else {
throw new Error(`Unregistering unknown terminal observer: ${id}`);
}
}

async provideLinks(line: string, terminal: TerminalWidget, cancellationToken?: CancellationToken | undefined): Promise<TerminalLink[]> {
if (this.terminalLinkProviders.length < 1) {
return [];
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,12 @@ export function createAPIFactory(
registerTerminalQuickFixProvider(id: string, provider: theia.TerminalQuickFixProvider): theia.Disposable {
return terminalExt.registerTerminalQuickFixProvider(id, provider);
},

/** Theia-specific TerminalObserver */
registerTerminalObserver(observer: theia.TerminalObserver): theia.Disposable {
return terminalExt.registerTerminalObserver(observer);
},

/** @stubbed ShareProvider */
registerShareProvider: () => Disposable.NULL,
};
Expand Down
21 changes: 20 additions & 1 deletion packages/plugin-ext/src/plugin/terminal-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export function getIconClass(options: theia.TerminalOptions | theia.ExtensionTer
*/
@injectable()
export class TerminalServiceExtImpl implements TerminalServiceExt {

private readonly proxy: TerminalServiceMain;

private readonly _terminals = new Map<string, TerminalExtImpl>();
Expand All @@ -58,6 +57,7 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {

private static nextProviderId = 0;
private readonly terminalLinkProviders = new Map<string, theia.TerminalLinkProvider>();
private readonly terminalObservers = new Map<string, theia.TerminalObserver>();
private readonly terminalProfileProviders = new Map<string, theia.TerminalProfileProvider>();
private readonly onDidCloseTerminalEmitter = new Emitter<Terminal>();
readonly onDidCloseTerminal: theia.Event<Terminal> = this.onDidCloseTerminalEmitter.event;
Expand Down Expand Up @@ -270,6 +270,25 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {
return Disposable.NULL;
}

registerTerminalObserver(observer: theia.TerminalObserver): theia.Disposable {
const id = (TerminalServiceExtImpl.nextProviderId++).toString();
this.terminalObservers.set(id, observer);
this.proxy.$registerTerminalObserver(id, observer.nrOfLinesToMatch, observer.outputMatcherRegex);
return Disposable.create(() => {
this.proxy.$unregisterTerminalObserver(id);
this.terminalObservers.delete(id);
});
}

$reportOutputMatch(observerId: string, groups: string[]): void {
const observer = this.terminalObservers.get(observerId);
if (observer) {
observer.matchOccurred(groups);
} else {
throw new Error(`reporting matches for unregistered observer: ${observerId} `);
}
}

protected isExtensionTerminalOptions(options: theia.TerminalOptions | theia.ExtensionTerminalOptions): options is theia.ExtensionTerminalOptions {
return 'pty' in options;
}
Expand Down
20 changes: 20 additions & 0 deletions packages/plugin/src/theia-extra.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,26 @@ export module '@theia/plugin' {
color?: ThemeColor;
}

export interface TerminalObserver {

/**
* A regex to match against the latest terminal output.
*/
readonly outputMatcherRegex: string;
/**
* The maximum number of lines to match the regex against. Maximum is 40 lines.
*/
readonly nrOfLinesToMatch: number;
/**
* Invoked when the regex matched against the terminal contents.
* @param groups The matched groups
*/
matchOccurred(groups: string[]): void;
}

export namespace window {
export function registerTerminalObserver(observer: TerminalObserver): Disposable;
}
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/terminal/src/browser/base/terminal-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ export interface TerminalSplitLocation {
readonly parentTerminal: string;
}

export interface TerminalBuffer {
readonly length: number;
/**
* @param start zero based index of the first line to return
* @param length the max number or lines to return
*/
getLines(start: number, length: number): string[];
}

/**
* Terminal UI widget.
*/
Expand Down Expand Up @@ -118,6 +127,10 @@ export abstract class TerminalWidget extends BaseWidget {
/** Event that fires when the terminal input data */
abstract onData: Event<string>;

abstract onOutput: Event<string>;

abstract buffer: TerminalBuffer;

abstract scrollLineUp(): void;

abstract scrollLineDown(): void;
Expand Down
31 changes: 30 additions & 1 deletion packages/terminal/src/browser/terminal-widget-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import { IBaseTerminalServer, TerminalProcessInfo, TerminalExitReason } from '..
import { TerminalWatcher } from '../common/terminal-watcher';
import {
TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions,
TerminalLocation
TerminalLocation,
TerminalBuffer
} from './base/terminal-widget';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { TerminalPreferences } from './terminal-preferences';
Expand Down Expand Up @@ -60,6 +61,23 @@ export interface TerminalContribution {
onCreate(term: TerminalWidgetImpl): void;
}

class TerminalBufferImpl implements TerminalBuffer {
constructor(private readonly term: Terminal) {
}

get length(): number {
return this.term.buffer.active.length;
};
getLines(start: number, length: number): string[] {
const result: string[] = [];
for (let i = 0; i < length && this.length - 1 - i >= 0; i++) {
result.push(this.term.buffer.active.getLine(this.length - 1 - i)!.translateToString());
}
return result;
}

}

@injectable()
export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget, EnhancedPreviewWidget {
readonly isExtractable: boolean = true;
Expand Down Expand Up @@ -123,6 +141,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
protected readonly onDataEmitter = new Emitter<string>();
readonly onData: Event<string> = this.onDataEmitter.event;

protected readonly onOutputEmitter = new Emitter<string>();
readonly onOutput: Event<string> = this.onOutputEmitter.event;

protected readonly onKeyEmitter = new Emitter<{ key: string, domEvent: KeyboardEvent }>();
readonly onKey: Event<{ key: string, domEvent: KeyboardEvent }> = this.onKeyEmitter.event;

Expand All @@ -134,6 +155,11 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget

protected readonly toDisposeOnConnect = new DisposableCollection();

private _buffer: TerminalBuffer;
override get buffer(): TerminalBuffer {
return this._buffer;
}

@postConstruct()
protected init(): void {
this.setTitle(this.options.title || TerminalWidgetImpl.LABEL);
Expand Down Expand Up @@ -174,6 +200,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
fastScrollSensitivity: this.preferences['terminal.integrated.fastScrollSensitivity'],
theme: this.themeService.theme
});
this._buffer = new TerminalBufferImpl(this.term);

this.fitAddon = new FitAddon();
this.term.loadAddon(this.fitAddon);
Expand Down Expand Up @@ -711,6 +738,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
write(data: string): void {
if (this.termOpened) {
this.term.write(data);
this.onOutputEmitter.fire(data);
} else {
this.initialData += data;
}
Expand Down Expand Up @@ -762,6 +790,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget

writeLine(text: string): void {
this.term.writeln(text);
this.onOutputEmitter.fire(text + '\n');
}

get onTerminalDidClose(): Event<TerminalWidget> {
Expand Down

0 comments on commit cffeeac

Please sign in to comment.