diff --git a/demo/client.ts b/demo/client.ts index 8f858b39..96f6b335 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -833,12 +833,19 @@ function addAnsiHyperlink() { term.writeln(`Regular link with no id:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); - term.writeln(`\nShared ID links:`); - term.writeln('╔════╗ ╔════╗'); - term.writeln('║\x1b]8;;https://github.com\x07GitH\x1b]8;;\x07║ ║ ║'); - term.writeln('║\x1b]8;;https://github.com\x07ub\x1b]8;;\x07 ║ ║ ║'); - term.writeln('╚════╝ ╚════╝'); - term.write('\x1b[3A\x1b[8C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[12D'); + term.writeln(`\nAdjacent links:`); + term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07xterm.js\x1b]8;;\x07'); + term.writeln(`\nShared ID link:`); + term.writeln('╔════╗'); + term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); + term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); + term.writeln('╚════╝'); + term.writeln(`\nWrapped link with no ID (not meant to share underline):`); + term.writeln('╔════╗'); + term.writeln('║ ║'); + term.writeln('║ ║'); + term.writeln('╚════╝'); + term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } function addDecoration() { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 76d35fd4..37a0a127 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -228,7 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _workCell: CellData = new CellData(); private _windowTitle = ''; private _iconName = ''; - private _currentHyperlink?: IOscLinkData; + private _currentLinkId?: number; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; @@ -639,6 +639,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (screenReaderMode) { this._onA11yChar.fire(stringFromCodePoint(code)); } + if (this._currentLinkId !== undefined) { + this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y); + } // insert combining char at last cursor position // this._activeBuffer.x should never be 0 for a combining char @@ -2924,7 +2927,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _createHyperlink(params: string, uri: string): boolean { // It's legal to open a new hyperlink without explicitly finishing the previous one - if (this._currentHyperlink) { + if (this._currentLinkId !== undefined) { this._finishHyperlink(); } const parsedParams = params.split(':'); @@ -2933,10 +2936,10 @@ export class InputHandler extends Disposable implements IInputHandler { if (idParamIndex !== -1) { id = parsedParams[idParamIndex].slice(3) || undefined; } - this._currentHyperlink = { id, uri }; - this._oscLinkService.registerLink(this._currentHyperlink); this._curAttrData.extended = this._curAttrData.extended.clone(); - this._curAttrData.extended.urlId = 1; + this._currentLinkId = this._oscLinkService.registerLink({ id, uri }); + this._curAttrData.extended.urlId = this._currentLinkId; + console.log('register', uri, `id=${this._curAttrData.extended.urlId}`); this._curAttrData.updateExtended(); return true; } @@ -2946,7 +2949,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._curAttrData.extended = this._curAttrData.extended.clone(); this._curAttrData.extended.urlId = 0; this._curAttrData.updateExtended(); - this._currentHyperlink = undefined; + this._currentLinkId = undefined; return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 1d353b91..215481af 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -146,6 +146,8 @@ export class MockOscLinkService implements IOscLinkService { public getLinkData(linkId: number): IOscLinkData | undefined { return undefined; } + public addLineToLink(linkId: number, y: number): void { + } } // defaults to V6 always to keep tests passing diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index b67b9846..a3f8af1d 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -1,23 +1,92 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ import { IBufferService, IOscLinkService } from 'common/services/Services'; -import { IOscLinkData } from 'common/Types'; +import { IMarker, IOscLinkData } from 'common/Types'; export class OscLinkService implements IOscLinkService { public serviceBrand: any; + private _nextId = 1; + + // TODO: Evict on marker dispose + private _entriesNoId: IOscLinkEntryNoId[] = []; + private _entriesWithId: Map = new Map(); + + // The "link id" (number) which is the numberic representation of a unique link should not be + // confused with "id" (string) which comes in with "id=" in the OSC link's properties + private _dataByLinkId: Map = new Map(); + constructor( @IBufferService private readonly _bufferService: IBufferService ) { } - public registerLink(linkData: IOscLinkData): number { - console.log('register link'); - // TODO: Add and return properly - return 1; + public registerLink(data: IOscLinkData): number { + // TODO: Extend range where appropriate + const buffer = this._bufferService.buffer; + + // Links with no id will only ever be registered a single time + if (data.id === undefined) { + const entry: IOscLinkEntryNoId = { + data, + id: this._nextId++, + lines: [buffer.addMarker(buffer.ybase + buffer.y)] + }; + this._entriesNoId.push(entry); + this._dataByLinkId.set(entry.id, entry); + return entry.id; + } + + const castData = data as Required; + const key = this._getEntryIdKey(castData); + const match = this._entriesWithId.get(key); + if (match) { + this.addLineToLink(match.id, buffer.ybase + buffer.y); + return match.id; + } + + const entry: IOscLinkEntryWithId = { + id: this._nextId++, + key: this._getEntryIdKey(castData), + data: castData, + lines: [buffer.addMarker(buffer.ybase + buffer.y)] + }; + this._entriesWithId.set(entry.key, entry); + this._dataByLinkId.set(entry.id, entry); + return entry.id; + } + + public addLineToLink(linkId: number, y: number): void { + const link = this._dataByLinkId.get(linkId); + if (!link) { + return; + } + if (link.lines.every(e => e.line !== y)) { + console.log(' add new line', y); + link.lines.push(this._bufferService.buffer.addMarker(y)); + } } public getLinkData(linkId: number): IOscLinkData | undefined { - return { - uri: 'https://github.com' - }; + return this._dataByLinkId.get(linkId)?.data; + } + + private _getEntryIdKey(linkData: Required): string { + return `${linkData.id};;${linkData.uri}`; } } + +interface IOscLinkEntry { + data: T; + id: number; + lines: IMarker[]; +} + +interface IOscLinkEntryNoId extends IOscLinkEntry { +} + +interface IOscLinkEntryWithId extends IOscLinkEntry> { + key: string; +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 5f97a487..88a21252 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -280,6 +280,10 @@ export interface IOscLinkService { * service and will be freed when this current cursor position is trimmed off the buffer. */ registerLink(linkData: IOscLinkData): number; + /** + * Adds a line to a link if needed. + */ + addLineToLink(linkId: number, y: number): void; /** Get the link data associated with a link ID. */ getLinkData(linkId: number): IOscLinkData | undefined; }