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

Add terminal observer API #13402

Merged
merged 1 commit into from
Mar 21, 2024
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
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 {
jonah-iden marked this conversation as resolved.
Show resolved Hide resolved
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
Loading