diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f39e6b..559ae17a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,9 +167,9 @@ jobs: strategy: matrix: node-version: [18] # just one as integration tests are about testing in browser - runs-on: [ubuntu] # macos is flaky + runs-on: [ubuntu-22.04] # macos is flaky browser: [chromium, firefox, webkit] - runs-on: ${{ matrix.runs-on }}-latest + runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }}.x diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 6c270d12..3eb13761 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -377,8 +377,11 @@ export class WebglRenderer extends Disposable implements IRenderer { let line: IBufferLine; let joinedRanges: [number, number][]; let isJoined: boolean; + let skipJoinedCheckUntilX: number = 0; + let isValidJoinRange: boolean = true; let lastCharX: number; let range: [number, number]; + let isCursorRow: boolean; let chars: string; let code: number; let width: number; @@ -405,6 +408,8 @@ export class WebglRenderer extends Disposable implements IRenderer { row = y + terminal.buffer.ydisp; line = terminal.buffer.lines.get(row)!; this._model.lineLengths[y] = 0; + isCursorRow = cursorY === row; + skipJoinedCheckUntilX = 0; joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (x = 0; x < terminal.cols; x++) { lastBg = this._cellColorResolver.result.bg; @@ -416,25 +421,43 @@ export class WebglRenderer extends Disposable implements IRenderer { // If true, indicates that the current character(s) to draw were joined. isJoined = false; + + // Indicates whether this cell is part of a joined range that should be ignored as it cannot + // be rendered entirely, like the selection state differs across the range. + isValidJoinRange = (x >= skipJoinedCheckUntilX); + lastCharX = x; // Process any joined character ranges as needed. Because of how the // ranges are produced, we know that they are valid for the characters // and attributes of our input. - if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { - isJoined = true; + if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) { range = joinedRanges.shift()!; - // We already know the exact start and end column of the joined range, - // so we get the string and width representing it directly. - cell = new JoinedCellData( - cell, - line!.translateToString(true, range[0], range[1]), - range[1] - range[0] - ); + // If the ligature's selection state is not consistent, don't join it. This helps the + // selection render correctly regardless whether they should be joined. + const firstSelectionState = this._model.selection.isCellSelected(this._terminal, range[0], row); + for (i = range[0] + 1; i < range[1]; i++) { + isValidJoinRange &&= (firstSelectionState === this._model.selection.isCellSelected(this._terminal, i, row)); + } + // Similarly, if the cursor is in the ligature, don't join it. + isValidJoinRange &&= !isCursorRow || cursorX < range[0] || cursorX >= range[1]; + if (!isValidJoinRange) { + skipJoinedCheckUntilX = range[1]; + } else { + isJoined = true; - // Skip over the cells occupied by this range in the loop - lastCharX = range[1] - 1; + // We already know the exact start and end column of the joined range, + // so we get the string and width representing it directly. + cell = new JoinedCellData( + cell, + line!.translateToString(true, range[0], range[1]), + range[1] - range[0] + ); + + // Skip over the cells occupied by this range in the loop + lastCharX = range[1] - 1; + } } chars = cell.getChars(); diff --git a/demo/client.ts b/demo/client.ts index f7503210..02cef73b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -240,6 +240,7 @@ if (document.location.pathname === '/test') { document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); + document.getElementById('ligatures-test').addEventListener('click', ligaturesTest); document.getElementById('weblinks-test').addEventListener('click', testWeblinks); document.getElementById('bce').addEventListener('click', coloredErase); addVtButtons(); @@ -1307,6 +1308,20 @@ function addVtButtons(): void { document.querySelector('#vt-container').appendChild(vtFragment); } +function ligaturesTest(): void { + term.write([ + '', + '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-', + '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', + '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', + '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', + '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', + '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|