blob: 2acedc85b76dc0141a391a3c25eac4d20256cda8 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {LitElement, html, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
import {isNewDiff, diffClasses} from '../gr-diff/gr-diff-utils';
const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
const TAB = '\t';
/**
* Renders one line of code on one side of the diff. It takes care of:
* - Tabs, see `tabSize` property.
* - Line Breaks, see `lineLimit` property.
* - Surrogate Character Pairs.
*
* Note that other modifications to the code in a gr-diff is done via diff
* layers, which manipulate the DOM directly. So `gr-diff-text` is thrown
* away and re-rendered every time something changes by its parent
* `gr-diff-row`. So don't bother to optimize this component for re-rendering
* performance. And be aware that building longer lived local state is not
* useful here.
*/
export class GrDiffText extends LitElement {
/**
* The browser API for handling selection does not (yet) work for selection
* across multiple shadow DOM elements. So we are rendering gr-diff components
* into the light DOM instead of the shadow DOM by overriding this method,
* which was the recommended workaround by the lit team.
* See also https://github.com/WICG/webcomponents/issues/79.
*/
override createRenderRoot() {
return this;
}
@property({type: String})
text = '';
@property({type: Boolean})
isResponsive = false;
@property({type: Number})
tabSize = 2;
@property({type: Number})
lineLimit = 80;
/** Temporary state while rendering. */
private textOffset = 0;
/** Temporary state while rendering. */
private columnPos = 0;
/** Temporary state while rendering. */
private pieces: (string | TemplateResult)[] = [];
/** Split up the string into tabs, surrogate pairs and regular segments. */
override render() {
this.textOffset = 0;
this.columnPos = 0;
this.pieces = [];
const splitByTab = this.text.split('\t');
for (let i = 0; i < splitByTab.length; i++) {
const splitBySurrogate = splitByTab[i].split(SURROGATE_PAIR);
for (let j = 0; j < splitBySurrogate.length; j++) {
this.renderSegment(splitBySurrogate[j]);
if (j < splitBySurrogate.length - 1) {
this.renderSurrogatePair();
}
}
if (i < splitByTab.length - 1) {
this.renderTab();
}
}
if (this.textOffset !== this.text.length) throw new Error('unfinished');
return this.pieces;
}
/** Render regular characters, but insert line breaks appropriately. */
private renderSegment(segment: string) {
let segmentOffset = 0;
while (segmentOffset < segment.length) {
const newOffset = Math.min(
segment.length,
segmentOffset + this.lineLimit - this.columnPos
);
this.renderString(segment.substring(segmentOffset, newOffset));
segmentOffset = newOffset;
if (segmentOffset < segment.length && this.columnPos === this.lineLimit) {
this.renderLineBreak();
}
}
}
/** Render regular characters. */
private renderString(s: string) {
if (s.length === 0) return;
this.pieces.push(s);
this.textOffset += s.length;
this.columnPos += s.length;
if (this.columnPos > this.lineLimit) throw new Error('over line limit');
}
/** Render a tab character. */
private renderTab() {
let tabSize = this.tabSize - (this.columnPos % this.tabSize);
if (this.columnPos + tabSize > this.lineLimit) {
this.renderLineBreak();
tabSize = this.tabSize;
}
const piece = html`<span
class=${diffClasses('tab')}
style=${styleMap({'tab-size': `${tabSize}`})}
>${TAB}</span
>`;
this.pieces.push(piece);
this.textOffset += 1;
this.columnPos += tabSize;
}
/** Render a surrogate pair: string length is 2, but is just 1 char. */
private renderSurrogatePair() {
if (this.columnPos === this.lineLimit) {
this.renderLineBreak();
}
this.pieces.push(this.text.substring(this.textOffset, this.textOffset + 2));
this.textOffset += 2;
this.columnPos += 1;
}
/** Render a line break, don't advance text offset, reset col position. */
private renderLineBreak() {
if (this.isResponsive) {
this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`);
} else {
this.pieces.push(html`<span class=${diffClasses('br')}></span>`);
}
// this.textOffset += 0;
this.columnPos = 0;
}
}
// TODO(newdiff-cleanup): Remove once newdiff migration is completed.
if (isNewDiff()) {
customElements.define('gr-diff-text', GrDiffText);
}
declare global {
interface HTMLElementTagNameMap {
// TODO(newdiff-cleanup): Replace once newdiff migration is completed.
'gr-diff-text': LitElement;
}
}