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

hooks for custom control sequences (updated) #1853

Merged
merged 24 commits into from
Jan 1, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4d660de
hooks for custom control sequences
PerBothner Dec 5, 2018
b689745
Cleanups required by tslink.
PerBothner Dec 5, 2018
0311563
Optimize parsing of OSC_STRING to minimize string concatenation.
PerBothner Dec 9, 2018
30a667c
Revert "Optimize parsing of OSC_STRING to minimize string concatenati…
PerBothner Dec 9, 2018
8ad2d1b
Merge remote-tracking branch 'upstream/master'
PerBothner Dec 12, 2018
ffb2708
Revert "Cleanups required by tslink."
PerBothner Dec 12, 2018
53fd04a
Revert "hooks for custom control sequences"
PerBothner Dec 12, 2018
8ceea11
hooks for custom control sequences
PerBothner Dec 12, 2018
6351a5b
Merge branch 'master' into master
Tyriar Dec 13, 2018
5af4626
Change addCsiHandler/addOscHandler to not use Object.assign.
PerBothner Dec 14, 2018
8a5a032
New method _linkHandler used by both addCsiHandler and addOscHandler.
PerBothner Dec 15, 2018
6b65ebd
Various typing and API fixes, doc comments, typing test etc.
PerBothner Dec 15, 2018
29cc0bf
Merge remote-tracking branch 'upstream/master' into control-seq-handler
PerBothner Dec 21, 2018
48ff841
Be more paranoid about cleaning up escape sequence handlers.
PerBothner Dec 23, 2018
a80baa7
Merge branch 'master' into control-seq-handler
Tyriar Dec 24, 2018
d01efdd
Use array instead of linkedlist, add typings
Tyriar Dec 26, 2018
38796a0
Add tests, fix NPE
Tyriar Dec 26, 2018
c045c80
Add tests for dispose
Tyriar Dec 26, 2018
adbb929
Wrap .d.ts comments to 80 chars
Tyriar Dec 26, 2018
51e1f49
Make dispose more resilient
Tyriar Dec 27, 2018
bbfe149
Merge pull request #1 from Tyriar/hooks_changes
PerBothner Dec 29, 2018
48c1d36
Merge branch 'master' into control-seq-handler
Tyriar Jan 1, 2019
8fbeadd
Add missing deleteCount argument to Array.splice calls.
PerBothner Jan 1, 2019
f534758
Merge branch 'master' into control-seq-handler
jerch Jan 1, 2019
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
8 changes: 7 additions & 1 deletion fixtures/typings-test/typings-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/// <reference path="../../typings/xterm.d.ts" />

import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';

namespace constructor {
{
Expand Down Expand Up @@ -119,6 +119,12 @@ namespace methods_core {
const t: Terminal = new Terminal();
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
const d1: IDisposable = t.addCsiHandler("x",
(params: number[], collect: string): boolean => params[0]===1);
d1.dispose();
const d2: IDisposable = t.addOscHandler(199,
(data: string): boolean => true);
d2.dispose();
}
namespace options {
{
Expand Down
50 changes: 49 additions & 1 deletion src/EscapeSequenceParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
*/

import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';

interface IHandlerLink extends IDisposable {
nextHandler: IHandlerLink | null;
}

/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
* @param low The low number.
Expand Down Expand Up @@ -41,7 +46,7 @@ export class TransitionTable {
* @param action parser action to be done
* @param next next parser state
*/
add(code: number, state: number, action: number | null, next: number | null): void {
add(code: number, state: number, action: number | null, next: number | null): void {
this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next);
}

Expand Down Expand Up @@ -303,6 +308,38 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlerFb = callback;
}

private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable {
const newHead: any = newCallback;
newHead.nextHandler = handlers[index] as IHandlerLink;
newHead.dispose = function (): void {
Copy link
Member

@jerch jerch Dec 23, 2018

Choose a reason for hiding this comment

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

nextHandler gets not freed anywhere possibly holding that handler forever (if the disposed handler gets not removed on caller side). Same with the handlers object. Imho nulling both afterwards and conditionally exiting at the beginning when they not exist anymore (already disposed) solves this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"nextHandler gets not freed anywhere possibly holding that handler forever (if the disposed handler gets not removed on caller side)."

Right - but wouldn't that be a leak on the caller side? You really shouldn't keep a reference tofoo after calling foo.dispose(). We can null out nextHandler as a precaution, but it would only serve to paper over a leak in the caller, methinks:

        if (cur === newHead) {
          if (previous) { previous.nextHandler = cur.nextHandler; }
          else { handlers[index] = cur.nextHandler; }
+         cur.nextHandler = null;
          break;
        }

"Same with the handlers object."

I don't think that is an issue. The handlers object is this._csiHandlers or this._oscHandlers, which are both allocated in the constructor.

Copy link
Member

@jerch jerch Dec 23, 2018

Choose a reason for hiding this comment

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

Yepp thats basically a leak on caller side, but in JS ppl tend not to think about lifetime of an object at all. And we cannot control caller side here as its exposed to the public. Therefore I think we should take care over what we can control as a precaution. Both refs are likely to contain handlers of InputHandler which itself contains a Terminal ref. Terminal is kinda the root of our "disposable object tree", thus forgetting to remove a disposed handler ref might prevent the whole object tree from GCing, which is far worse than just a leftover handler ref.

Now talking about dispose and GC - I think it is also flawed regarding the invocation on a higher tree object that takes care of the subtree objects (the default dispose descent). I think EscapeSequenceParser.dispose also would have to invoke dispose on the added handlers (with nulling in place), otherwise InputHandler will be kept bound in the added handlers. (InputHandler.dispose itself nulls the terminal object, so at least the majority of objects will be released.)

Copy link
Member

@jerch jerch Dec 23, 2018

Choose a reason for hiding this comment

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

Additional note on that: From API perspective its a surprising side effect, that an added handler pulls in refs to the terminal object (through handlers and nextHandler). Its not obvious for ppl, they only see the interface that acts on parser states and input. Atm dispose covers the "remove" thing, but not the ref releasing.

Btw the arrow function as added handler binds the parser object as this, just to be able to call the fallback. Hmm. Thats not the case for handlers via set.... Imho this needs a slightly different approach.

let previous = null;
let cur = handlers[index] as IHandlerLink;
for (; cur && cur.nextHandler;
previous = cur, cur = cur.nextHandler) {
if (cur === newHead) {
if (previous) { previous.nextHandler = cur.nextHandler; }
else { handlers[index] = cur.nextHandler; }
break;
}
}
};
handlers[index] = newHead;
return newHead;
}

addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
const index = flag.charCodeAt(0);
const newHead =
(params: number[], collect: string): void => {
if (! callback(params, collect)) {
const next = (newHead as unknown as IHandlerLink).nextHandler;
if (next) { (next as any)(params, collect); }
else { this._csiHandlerFb(collect, params, index); }
}
};
return this._linkHandler(this._csiHandlers, index, newHead);
}

setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = callback;
}
Expand All @@ -323,6 +360,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlerFb = callback;
}

addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
const newHead =
(data: string): void => {
if (! callback(data)) {
const next = (newHead as unknown as IHandlerLink).nextHandler;
if (next) { (next as any)(data); }
else { this._oscHandlerFb(ident, data); }
}
};
return this._linkHandler(this._oscHandlers, ident, newHead);
}
setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = callback;
}
Expand Down
8 changes: 8 additions & 0 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
jerch marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down Expand Up @@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.updateRange(buffer.y);
}

addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._parser.addCsiHandler(flag, callback);
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._parser.addOscHandler(ident, callback);
}

/**
* BEL
* Bell (Ctrl-G).
Expand Down
9 changes: 9 additions & 0 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._customKeyEventHandler = customKeyEventHandler;
}

/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(flag, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._inputHandler.addOscHandler(ident, callback);
}

/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
Expand Down
2 changes: 2 additions & 0 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable {
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;

setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
Expand Down
6 changes: 6 additions & 0 deletions src/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export class Terminal implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
Expand Down
6 changes: 6 additions & 0 deletions src/ui/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal {
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
}
Expand Down
25 changes: 25 additions & 0 deletions typings/xterm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,31 @@ declare module 'xterm' {
*/
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;

/**
* (EXPERIMENTAL) Adds a handler for CSI escape sequences.
* @param flag The flag should be one-character string, which specifies
* the final character (e.g "m" for SGR) of the CSI sequence.
* @param callback The function to handle the escape sequence.
* The callback is called with the numerical params,
* as well as the special characters (e.g. "$" for DECSCPP).
* Return true if the sequence was handled; false if we should
* try a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;

/**
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.
* @param ident The number (first parameter) of the sequence.
* @param callback The function to handle the escape sequence.
* The callback is called with OSC data string.
* Return true if the sequence was handled; false if we should
* try a previous handler (set by addOscHandler or setOscHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to
* be matched and handled.
Expand Down