From d4c49f9686a6398ed3a4fa5b46989575e58376e5 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Sun, 31 May 2026 15:43:37 +0800 Subject: [PATCH 1/8] Update types --- packages/diffs/src/types.ts | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/diffs/src/types.ts b/packages/diffs/src/types.ts index c72327250..3199a4980 100644 --- a/packages/diffs/src/types.ts +++ b/packages/diffs/src/types.ts @@ -888,19 +888,7 @@ export interface StickySpecs { height: number; } -export interface DiffsEditor { - syncWithRender( - highlighter: DiffsHighlighter, - fileContainer: HTMLElement, - fileContents: FileContents, - lineAnnotations: LineAnnotation[] | undefined, - renderRange: RenderRange | undefined, - editMode?: 'simple' | 'advanced' - ): void; - cleanUp(): void; -} - -export interface DiffsEditorOptions extends BaseCodeOptions { +export interface DiffsComponentOptions extends BaseCodeOptions { enableGutterUtility?: boolean; enableLineSelection?: boolean; expandUnchanged?: boolean; @@ -909,8 +897,8 @@ export interface DiffsEditorOptions extends BaseCodeOptions { export interface DiffsBaseComponent { readonly top?: number; - readonly options: DiffsEditorOptions; - setOptions: (options: Partial) => void; + readonly options: DiffsComponentOptions; + setOptions: (options: Partial) => void; setSelectedLines: (range: { start: number; end: number } | null) => void; rerender(): void; cleanUp(): void; @@ -931,10 +919,16 @@ export interface DiffsEditableComponent< ) => void; } -export interface DiffsTextDocument { - lineCount: number; - getLineText: (lineNumber: number) => string; - getText: () => string; +export interface DiffsEditor { + syncWithRender( + highlighter: DiffsHighlighter, + fileContainer: HTMLElement, + fileContents: FileContents, + lineAnnotations: LineAnnotation[] | undefined, + renderRange: RenderRange | undefined, + editMode?: 'simple' | 'advanced' + ): void; + cleanUp(): void; } export interface DiffsEditorSelection { @@ -948,3 +942,9 @@ export interface DiffsEditorSelection { }; direction: 'none' | 'backward' | 'forward'; } + +export interface DiffsTextDocument { + readonly lineCount: number; + getLineText: (lineNumber: number) => string; + getText: () => string; +} From e805f027551524e8e936104b28ffaf3b567594b3 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Sun, 31 May 2026 16:00:49 +0800 Subject: [PATCH 2/8] Fix selection range rendering --- packages/diffs/src/editor/editor.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index 589b7ea30..b914af625 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -1633,12 +1633,14 @@ export class Editor implements DiffsEditor { left: number, applyEolSpacing = true ) { - const spacing = - !applyEolSpacing || - selection.end.line === ln || - (startChar === endChar && ln !== selection.start.line) - ? 0 - : this.#metrics.ch; + let spacing = 0; + if (applyEolSpacing && ln < selection.end.line && startChar !== endChar) { + spacing = this.#metrics.ch; + } + if (width === 0 && spacing === 0) { + return; + } + const css = `width:${width + spacing}px;transform:translateY(${this.#getLineY(ln) + wrapLine * this.#metrics.lineHeight}px) translateX(${left}px);`; const cacheKey = 'selection-range-' + css; const selectionEls = this.#selectionElements; From 6bd8a4e2829452ed72dee63d32de2e5d1cb1676e Mon Sep 17 00:00:00 2001 From: Je Xia Date: Sun, 31 May 2026 16:54:46 +0800 Subject: [PATCH 3/8] Fix selection range rendering --- packages/diffs/src/editor/editor.ts | 105 ++++++++++------------------ 1 file changed, 35 insertions(+), 70 deletions(-) diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index b914af625..becd57ccb 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -1478,10 +1478,9 @@ export class Editor implements DiffsEditor { const endChar = ln === end.line ? end.character : lineText.length; if (this.#wrap) { - const paddingInline = this.#metrics.ch; // 1ch, align to diff css: padding-inline: 1ch const contentWidth = this.#getContentWidth(); const textWidth = - 2 * paddingInline + this.#metrics.measureTextWidth(lineText); + 2 * this.#metrics.ch + this.#metrics.measureTextWidth(lineText); if (textWidth > contentWidth) { this.#renderWrappedSelection( renderCtx, @@ -1489,8 +1488,7 @@ export class Editor implements DiffsEditor { ln, lineText, startChar, - endChar, - paddingInline + endChar ); continue; } @@ -1498,24 +1496,18 @@ export class Editor implements DiffsEditor { let left = 0; let width = 0; - if (startChar === endChar && startChar === 0) { + if (startChar === 0) { left = this.#getGutterWidth() + this.#metrics.ch; // gutter width + inline padding (1ch) - width = ln === end.line ? 0 : this.#metrics.ch; } else { left = this.#getCharX(ln, startChar)[0]; + } + if (startChar === endChar) { + width = ln === end.line ? 0 : this.#metrics.ch; + } else { width = endChar === startChar ? 0 : this.#getCharX(ln, endChar)[0] - left; } - this.#renderSelectionRange( - renderCtx, - selection, - ln, - 0, - startChar, - endChar, - width, - left - ); + this.#renderSelectionRange(renderCtx, ln, 0, width, left); } } @@ -1534,17 +1526,15 @@ export class Editor implements DiffsEditor { line: number, lineText: string, startChar: number, - endChar: number, - paddingInline: number + endChar: number ) { const wrapOffsets = this.#wrapLineText(line); const segmentCount = wrapOffsets.length - 1; - const lastSegmentIndex = segmentCount - 1; - const offsetLeft = this.#getGutterWidth() + paddingInline; + const offsetLeft = this.#getGutterWidth() + this.#metrics.ch; - for (let w = 0; w < segmentCount; w++) { - const segmentStart = wrapOffsets[w]; - const segmentEnd = wrapOffsets[w + 1]; + for (let wrapLine = 0; wrapLine < segmentCount; wrapLine++) { + const segmentStart = wrapOffsets[wrapLine]; + const segmentEnd = wrapOffsets[wrapLine + 1]; const wrapStartChar = Math.max(startChar, segmentStart); const wrapEndChar = Math.min(endChar, segmentEnd); @@ -1553,26 +1543,10 @@ export class Editor implements DiffsEditor { continue; } - // Zero-width slices on segment boundaries can appear on two consecutive - // segments (end of one, start of the next). Only render at the natural - // anchor positions: the very beginning of the first visual line, or the - // very end of the last visual line. - if (wrapStartChar === wrapEndChar) { - const isAtLineStart = wrapStartChar === 0 && w === 0; - const isAtLineEnd = - wrapEndChar === lineText.length && w === lastSegmentIndex; - if (!isAtLineStart && !isAtLineEnd) { - continue; - } - } - let segmentLeft: number; let segmentWidth: number; - if (wrapStartChar === 0 && wrapEndChar === 0) { - // Empty range pinned to line start (e.g. multi-line selection ending - // with end.character === 0). Mirrors the non-wrap path. + if (wrapStartChar === 0) { segmentLeft = offsetLeft; - segmentWidth = line === selection.end.line ? 0 : paddingInline; } else { const prefixInSegment = lineText.slice(segmentStart, wrapStartChar); const prefixAsciiColumns = getExpandedAsciiTextColumns( @@ -1584,32 +1558,27 @@ export class Editor implements DiffsEditor { (prefixAsciiColumns !== -1 ? prefixAsciiColumns * this.#metrics.ch : this.#metrics.measureTextWidth(prefixInSegment)); - - if (wrapStartChar === wrapEndChar) { - segmentWidth = 0; - } else { - const selectionInSegment = lineText.slice(wrapStartChar, wrapEndChar); - const selectionAsciiWidth = getExpandedAsciiTextColumns( - selectionInSegment, - this.#metrics.tabSize - ); - segmentWidth = - selectionAsciiWidth !== -1 - ? selectionAsciiWidth * this.#metrics.ch - : this.#metrics.measureTextWidth(selectionInSegment); - } + } + if (wrapStartChar === wrapEndChar) { + segmentWidth = this.#metrics.ch; + } else { + const selectionInSegment = lineText.slice(wrapStartChar, wrapEndChar); + const selectionAsciiWidth = getExpandedAsciiTextColumns( + selectionInSegment, + this.#metrics.tabSize + ); + segmentWidth = + selectionAsciiWidth !== -1 + ? selectionAsciiWidth * this.#metrics.ch + : this.#metrics.measureTextWidth(selectionInSegment); } this.#renderSelectionRange( renderCtx, - selection, line, - w, - wrapStartChar, - wrapEndChar, + wrapLine, segmentWidth, - segmentLeft, - w === lastSegmentIndex + segmentLeft ); } } @@ -1624,24 +1593,17 @@ export class Editor implements DiffsEditor { fragment: DocumentFragment; elements: Map; }, - selection: EditorSelection, ln: number, wrapLine: number, - startChar: number, - endChar: number, width: number, left: number, - applyEolSpacing = true + _rounded = true ) { - let spacing = 0; - if (applyEolSpacing && ln < selection.end.line && startChar !== endChar) { - spacing = this.#metrics.ch; - } - if (width === 0 && spacing === 0) { + if (width === 0) { return; } - const css = `width:${width + spacing}px;transform:translateY(${this.#getLineY(ln) + wrapLine * this.#metrics.lineHeight}px) translateX(${left}px);`; + const css = `width:${width}px;transform:translateY(${this.#getLineY(ln) + wrapLine * this.#metrics.lineHeight}px) translateX(${left}px);`; const cacheKey = 'selection-range-' + css; const selectionEls = this.#selectionElements; @@ -1664,6 +1626,9 @@ export class Editor implements DiffsEditor { ); } + // todo: when the `_rounded` parameter is true, + // based on the collapsed state, add the rounded-top-left/right or rounded-bottom-left/right dataset to the element + renderCtx.elements.set(cacheKey, rangeEl); } From a63cd64166d808e338babbb13748b459ba0590bc Mon Sep 17 00:00:00 2001 From: Je Xia Date: Sun, 31 May 2026 22:52:24 +0800 Subject: [PATCH 4/8] Add `roundedSelection` option --- packages/diffs/src/editor/css.ts | 17 +++ packages/diffs/src/editor/editor.ts | 189 +++++++++++++++++++++++----- 2 files changed, 172 insertions(+), 34 deletions(-) diff --git a/packages/diffs/src/editor/css.ts b/packages/diffs/src/editor/css.ts index e6d9b4bba..33b811b88 100644 --- a/packages/diffs/src/editor/css.ts +++ b/packages/diffs/src/editor/css.ts @@ -76,6 +76,23 @@ export const editorCSS: string = /* CSS */ ` z-index: -10; background-color: var(--diffs-editor-selection-bg); } + [data-selection-corner] { + width: 100%; + height: 100%; + background-color: var(--diffs-bg); + } + [data-rtl] { + border-top-left-radius: 3px; + } + [data-rtr] { + border-top-right-radius: 3px; + } + [data-rbl] { + border-bottom-left-radius: 3px; + } + [data-rbr] { + border-bottom-right-radius: 3px; + } [data-editor-overlay] { display: contents; } diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index becd57ccb..e69551853 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -76,6 +76,7 @@ function clampDomOffset(node: Node, offset: number): number { } export interface EditorOptions { + roundedSelection?: boolean; enabledQuickEdit?: boolean; renderQuickEdit?: (context: QuickEditContext) => HTMLElement; onChange?: ( @@ -648,12 +649,12 @@ export class Editor implements DiffsEditor { ) { return; } - const lineIndex = Number(lineNumber) - 1; + const line = Number(lineNumber) - 1; const selection: EditorSelection = { - start: { line: lineIndex, character: 0 }, + start: { line, character: 0 }, end: { - line: lineIndex, - character: textDocument.getLineText(lineIndex).length, + line, + character: textDocument.getLineText(line).length, }, direction: DirectionForward, }; @@ -1468,14 +1469,15 @@ export class Editor implements DiffsEditor { } const { start, end } = selection; - for (let ln = start.line; ln <= end.line; ln++) { - if (!this.#isLineVisible(ln)) { + for (let line = start.line; line <= end.line; line++) { + if (!this.#isLineVisible(line)) { continue; } - const lineText = this.#textDocument.getLineText(ln); - const startChar = ln === start.line ? start.character : 0; - const endChar = ln === end.line ? end.character : lineText.length; + const isLastLine = line === end.line; + const lineText = this.#textDocument.getLineText(line); + const startChar = line === start.line ? start.character : 0; + const endChar = isLastLine ? end.character : lineText.length; if (this.#wrap) { const contentWidth = this.#getContentWidth(); @@ -1484,11 +1486,11 @@ export class Editor implements DiffsEditor { if (textWidth > contentWidth) { this.#renderWrappedSelection( renderCtx, - selection, - ln, + line, lineText, startChar, - endChar + endChar, + isLastLine ); continue; } @@ -1499,15 +1501,17 @@ export class Editor implements DiffsEditor { if (startChar === 0) { left = this.#getGutterWidth() + this.#metrics.ch; // gutter width + inline padding (1ch) } else { - left = this.#getCharX(ln, startChar)[0]; + left = this.#getCharX(line, startChar)[0]; } if (startChar === endChar) { - width = ln === end.line ? 0 : this.#metrics.ch; + width = isLastLine ? 0 : this.#metrics.ch; } else { width = - endChar === startChar ? 0 : this.#getCharX(ln, endChar)[0] - left; + this.#getCharX(line, endChar)[0] - + left + + (isLastLine ? 0 : this.#metrics.ch); } - this.#renderSelectionRange(renderCtx, ln, 0, width, left); + this.#renderSelectionLine(renderCtx, line, 0, left, width); } } @@ -1522,11 +1526,11 @@ export class Editor implements DiffsEditor { fragment: DocumentFragment; elements: Map; }, - selection: EditorSelection, line: number, lineText: string, startChar: number, - endChar: number + endChar: number, + isLastLine: boolean ) { const wrapOffsets = this.#wrapLineText(line); const segmentCount = wrapOffsets.length - 1; @@ -1571,14 +1575,17 @@ export class Editor implements DiffsEditor { selectionAsciiWidth !== -1 ? selectionAsciiWidth * this.#metrics.ch : this.#metrics.measureTextWidth(selectionInSegment); + if (!isLastLine) { + segmentWidth += this.#metrics.ch; + } } - this.#renderSelectionRange( + this.#renderSelectionLine( renderCtx, line, wrapLine, - segmentWidth, - segmentLeft + segmentLeft, + segmentWidth ); } } @@ -1588,30 +1595,142 @@ export class Editor implements DiffsEditor { // appended at the end. For wrapped logical lines this must be false on every // visual segment except the last one, since an intra-line wrap is not a real // newline and shouldn't visually extend past the wrapped content. - #renderSelectionRange( + #renderSelectionLine( renderCtx: { fragment: DocumentFragment; elements: Map; + previousSelectionRange?: { + element: HTMLElement; + line: number; + wrapLine: number; + left: number; + width: number; + }; }, - ln: number, + line: number, wrapLine: number, - width: number, left: number, - _rounded = true + width: number ) { if (width === 0) { return; } - const css = `width:${width}px;transform:translateY(${this.#getLineY(ln) + wrapLine * this.#metrics.lineHeight}px) translateX(${left}px);`; - const cacheKey = 'selection-range-' + css; + const { ch, lineHeight } = this.#metrics; + const y = this.#getLineY(line) + wrapLine * lineHeight; + const css = `width:${width}px;transform:translateX(${left}px) translateY(${y}px);`; + const cacheKey = `selection-line-${left}-${y}-${width}`; const selectionEls = this.#selectionElements; - if (renderCtx.elements.has(cacheKey)) { + const rounded = this.#options.roundedSelection ?? true; + const addRoundedCorner = ( + line: number, + left: number, + radius: 'rtl' | 'rbl' | 'rbr' + ) => { + const top = this.#getLineY(line) + wrapLine * lineHeight; + const css = `width:${ch}px;transform:translateX(${left}px) translateY(${top}px);`; + const dataset = { + selectionCorner: '', + [radius]: '', + }; + const cacheKeyPrefix = `selection-line-${left}-${top}-1ch`; + let cacheKey = cacheKeyPrefix + '-' + radius; + if (radius === 'rbl') { + const prevCornerKey = cacheKeyPrefix + '-rtl'; + const prevCorner = renderCtx.elements.get(prevCornerKey); + if (prevCorner !== undefined) { + prevCorner.remove(); + renderCtx.elements.delete(prevCornerKey); + cacheKey += '-rtl'; + dataset.rtl = ''; + } + } + let cornerEl = renderCtx.elements.get(cacheKey); + if (cornerEl !== undefined) { + return; + } + if (selectionEls?.has(cacheKey) === true) { + cornerEl = selectionEls.get(cacheKey)!; + selectionEls.delete(cacheKey); + } else { + cornerEl = h( + 'div', + { + dataset: 'selectionRange', + style: { cssText: css }, + children: [ + h('div', { + dataset: dataset, + }), + ], + }, + renderCtx.fragment + ); + } + renderCtx.elements.set(cacheKey, cornerEl); + }; + const addRadiusStyle = (element: HTMLElement) => { + const end = left + width; + const dataset = element.dataset; + const previousSelectionRange = renderCtx.previousSelectionRange; + if ( + previousSelectionRange === undefined || + previousSelectionRange.line !== line || + previousSelectionRange.wrapLine !== wrapLine + ) { + renderCtx.previousSelectionRange = { + element, + line, + wrapLine, + left, + width, + }; + } + if ( + previousSelectionRange === undefined || + end <= previousSelectionRange.left + ) { + ['rtl', 'rtr', 'rbl', 'rbr'].forEach((key) => { + dataset[key] = ''; + }); + } else { + const prevLine = previousSelectionRange.line; + const prevLeft = previousSelectionRange.left; + const prevDataset = previousSelectionRange.element.dataset; + const prevEnd = prevLeft + previousSelectionRange.width; + if (prevLeft > left) { + addRoundedCorner(prevLine, prevLeft - ch, 'rbr'); + } + delete prevDataset.rbl; + delete dataset.rtl; + delete dataset.rtr; + if (end >= prevEnd) { + delete prevDataset.rbr; + } + if (end > prevEnd) { + addRoundedCorner(prevLine, prevEnd, 'rbl'); + dataset.rtr = ''; + } + if (end < prevEnd) { + addRoundedCorner(line, end, 'rtl'); + } + if (left < prevLeft) { + dataset.rtl = ''; + } + dataset.rbl = ''; + dataset.rbr = ''; + } + }; + + let rangeEl = renderCtx.elements.get(cacheKey); + if (rangeEl !== undefined) { + if (rounded) { + addRadiusStyle(rangeEl); + } return; } - let rangeEl: HTMLElement | undefined; if (selectionEls?.has(cacheKey) === true) { rangeEl = selectionEls.get(cacheKey)!; selectionEls.delete(cacheKey); @@ -1626,9 +1745,9 @@ export class Editor implements DiffsEditor { ); } - // todo: when the `_rounded` parameter is true, - // based on the collapsed state, add the rounded-top-left/right or rounded-bottom-left/right dataset to the element - + if (rounded) { + addRadiusStyle(rangeEl); + } renderCtx.elements.set(cacheKey, rangeEl); } @@ -1645,16 +1764,18 @@ export class Editor implements DiffsEditor { return; } const [left, wrapLine] = this.#getCharX(line, character); - const cacheKey = 'caret-' + line + '(' + wrapLine + ')-' + character; + const cacheKey = 'caret-' + line + '/' + wrapLine + ':' + character; if (renderCtx.elements.has(cacheKey)) { return; } + const x = left - 1; + const y = this.#getLineY(line) + wrapLine * this.#metrics.lineHeight; const caretEl = h( 'div', { dataset: 'caret', style: { - transform: `translateY(${this.#getLineY(line) + wrapLine * this.#metrics.lineHeight}px) translateX(${left - 1}px)`, + transform: `translateX(${x}px) translateY(${y}px)`, }, }, renderCtx.fragment From 292fc8c32ba305b45f178f0aef6182881d082844 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Mon, 1 Jun 2026 09:01:55 +0800 Subject: [PATCH 5/8] Add jsDocs --- packages/diffs/src/editor/editor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index e69551853..5889097c3 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -76,9 +76,13 @@ function clampDomOffset(node: Node, offset: number): number { } export interface EditorOptions { + /** Render rounded corners for selection ranges, default is true. */ roundedSelection?: boolean; + /** Show the clickable quick edit icon, default is disabled. */ enabledQuickEdit?: boolean; + /** Render the quick edit widget element. */ renderQuickEdit?: (context: QuickEditContext) => HTMLElement; + /** Callback when the editor document changes. */ onChange?: ( file: FileContents, lineAnnotations?: DiffLineAnnotation[] From e523b6590a50da1aa301f1b429543f40b7ec32a0 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Mon, 1 Jun 2026 14:52:44 +0800 Subject: [PATCH 6/8] Use 'editorCursor.foreground' color --- packages/diffs/src/editor/css.ts | 13 ++++++++----- packages/diffs/src/editor/tokenzier.ts | 2 ++ packages/diffs/src/style.css | 8 -------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/diffs/src/editor/css.ts b/packages/diffs/src/editor/css.ts index 33b811b88..fabd07938 100644 --- a/packages/diffs/src/editor/css.ts +++ b/packages/diffs/src/editor/css.ts @@ -1,8 +1,6 @@ -const DEBUG_SELECTION = false; - export const editorCSS: string = /* CSS */ ` ::selection { - background-color: ${DEBUG_SELECTION ? 'rgba(255, 0, 0, 0.1)' : 'transparent'}; + background-color: transparent; } @keyframes blinking { 0% { opacity: 1; } @@ -22,7 +20,7 @@ export const editorCSS: string = /* CSS */ ` } @media (min-width: 480px) { [data-content] { - caret-color: ${DEBUG_SELECTION ? 'red' : 'transparent'}; + caret-color: transparent; } [data-quick-edit] { caret-color: currentColor; @@ -66,7 +64,12 @@ export const editorCSS: string = /* CSS */ ` [data-caret] { width: 2px; height: 1lh; - background-color: ${DEBUG_SELECTION ? 'transparent' : 'var(--diffs-bg-caret)'}; + background-color: var(--diffs-bg-caret-override, var(--diffs-editor-cursor-fg, + light-dark( + color-mix(in lab, var(--diffs-fg) 50%, var(--diffs-bg)), + color-mix(in lab, var(--diffs-fg) 75%, var(--diffs-bg)) + )) + ); animation: blinking 1.2s infinite; animation-delay: 0.8s; visibility: hidden; diff --git a/packages/diffs/src/editor/tokenzier.ts b/packages/diffs/src/editor/tokenzier.ts index 9e2d5f69b..57875c591 100644 --- a/packages/diffs/src/editor/tokenzier.ts +++ b/packages/diffs/src/editor/tokenzier.ts @@ -92,12 +92,14 @@ export class EditorTokenizer { const lineHighlightBackground = colors['editor.lineHighlightBackground']; const gutterForeground = colors['editorLineNumber.foreground']; const gutterActiveForeground = colors['editorLineNumber.activeForeground']; + const cursorForeground = colors['editorCursor.foreground']; this.#setStyle(`:host { --diffs-editor-selection-bg: ${selectionBackground ?? 'var(--diffs-line-bg)'}; --diffs-editor-line-highlight-bg: ${lineHighlightBackground ?? 'var(--diffs-line-bg)'}; --diffs-editor-line-number-fg: ${gutterForeground ?? 'var(--diffs-fg-number)'}; --diffs-editor-line-number-active-bg: ${lineHighlightBackground ?? 'var(--diffs-line-bg, var(--diffs-bg))'}; --diffs-editor-line-number-active-fg: ${gutterActiveForeground ?? 'var(--diffs-selection-number-fg)'}; + ${cursorForeground !== undefined ? '--diffs-editor-cursor-fg: ' + cursorForeground : ''}; }`); }; diff --git a/packages/diffs/src/style.css b/packages/diffs/src/style.css index 44cccdf0b..981c90a3a 100644 --- a/packages/diffs/src/style.css +++ b/packages/diffs/src/style.css @@ -132,14 +132,6 @@ var(--diffs-fg-number) ); - --diffs-bg-caret: var( - --diffs-bg-caret-override, - light-dark( - color-mix(in lab, var(--diffs-fg) 50%, var(--diffs-bg)), - color-mix(in lab, var(--diffs-fg) 75%, var(--diffs-bg)) - ) - ); - --diffs-deletion-base: var( --diffs-deletion-color-override, light-dark( From ff893c667979aa016679997503a40f26495df4bd Mon Sep 17 00:00:00 2001 From: Je Xia Date: Tue, 2 Jun 2026 00:36:13 +0800 Subject: [PATCH 7/8] Fix --- packages/diffs/src/editor/css.ts | 2 +- packages/diffs/src/editor/editor.ts | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/diffs/src/editor/css.ts b/packages/diffs/src/editor/css.ts index fabd07938..f12cf2ef0 100644 --- a/packages/diffs/src/editor/css.ts +++ b/packages/diffs/src/editor/css.ts @@ -84,7 +84,7 @@ export const editorCSS: string = /* CSS */ ` height: 100%; background-color: var(--diffs-bg); } - [data-rtl] { + [data-rtl] { border-top-left-radius: 3px; } [data-rtr] { diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index 5889097c3..fa3c0da4b 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -1568,7 +1568,7 @@ export class Editor implements DiffsEditor { : this.#metrics.measureTextWidth(prefixInSegment)); } if (wrapStartChar === wrapEndChar) { - segmentWidth = this.#metrics.ch; + segmentWidth = wrapLine === segmentCount - 1 ? 0 : this.#metrics.ch; } else { const selectionInSegment = lineText.slice(wrapStartChar, wrapEndChar); const selectionAsciiWidth = getExpandedAsciiTextColumns( @@ -1579,7 +1579,7 @@ export class Editor implements DiffsEditor { selectionAsciiWidth !== -1 ? selectionAsciiWidth * this.#metrics.ch : this.#metrics.measureTextWidth(selectionInSegment); - if (!isLastLine) { + if (!isLastLine && wrapLine === segmentCount - 1) { segmentWidth += this.#metrics.ch; } } @@ -1594,11 +1594,6 @@ export class Editor implements DiffsEditor { } } - // Render one selection range div for a single visual line. `applyEolSpacing` - // controls whether the trailing one-character "line continuation" marker is - // appended at the end. For wrapped logical lines this must be false on every - // visual segment except the last one, since an intra-line wrap is not a real - // newline and shouldn't visually extend past the wrapped content. #renderSelectionLine( renderCtx: { fragment: DocumentFragment; @@ -1629,6 +1624,7 @@ export class Editor implements DiffsEditor { const rounded = this.#options.roundedSelection ?? true; const addRoundedCorner = ( line: number, + wrapLine: number, left: number, radius: 'rtl' | 'rbl' | 'rbr' ) => { @@ -1700,11 +1696,12 @@ export class Editor implements DiffsEditor { }); } else { const prevLine = previousSelectionRange.line; + const prevWrapLine = previousSelectionRange.wrapLine; const prevLeft = previousSelectionRange.left; const prevDataset = previousSelectionRange.element.dataset; const prevEnd = prevLeft + previousSelectionRange.width; if (prevLeft > left) { - addRoundedCorner(prevLine, prevLeft - ch, 'rbr'); + addRoundedCorner(prevLine, prevWrapLine, prevLeft - ch, 'rbr'); } delete prevDataset.rbl; delete dataset.rtl; @@ -1713,11 +1710,11 @@ export class Editor implements DiffsEditor { delete prevDataset.rbr; } if (end > prevEnd) { - addRoundedCorner(prevLine, prevEnd, 'rbl'); + addRoundedCorner(prevLine, prevWrapLine, prevEnd, 'rbl'); dataset.rtr = ''; } if (end < prevEnd) { - addRoundedCorner(line, end, 'rtl'); + addRoundedCorner(line, wrapLine, end, 'rtl'); } if (left < prevLeft) { dataset.rtl = ''; From 7f639cbceb150133cd6bd79f668d956b34f66c77 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Tue, 2 Jun 2026 10:27:23 +0800 Subject: [PATCH 8/8] Use parseInt --- packages/diffs/src/editor/editor.ts | 120 ++++++++++++++--------- packages/diffs/src/editor/selection.ts | 13 ++- packages/diffs/src/editor/textMeasure.ts | 6 +- 3 files changed, 90 insertions(+), 49 deletions(-) diff --git a/packages/diffs/src/editor/editor.ts b/packages/diffs/src/editor/editor.ts index fa3c0da4b..4ed93814b 100644 --- a/packages/diffs/src/editor/editor.ts +++ b/packages/diffs/src/editor/editor.ts @@ -347,9 +347,12 @@ export class Editor implements DiffsEditor { const line = el.dataset.line; const lineType = el.dataset.lineType; if (line !== undefined) { - const lineIndex = Number(line) - 1; - startingLine ??= lineIndex; - endLine = lineIndex; + const lineNumber = parseInt(line, 10); + if (!Number.isNaN(lineNumber)) { + const lineIndex = lineNumber - 1; + startingLine ??= lineIndex; + endLine = lineIndex; + } } if (lineType === undefined || !isLineEditable(lineType)) { el.contentEditable = 'false'; @@ -373,9 +376,11 @@ export class Editor implements DiffsEditor { // inject editor css to the file container if (this.#componentContainer !== fileContainer) { this.#componentContainer = fileContainer; - this.#codePaddingTop = Number( - getComputedStyle(codeElement).paddingTop.slice(0, -2) + const codePaddingTop = parseInt( + getComputedStyle(codeElement).paddingTop.slice(0, -2), + 10 ); + this.#codePaddingTop = Number.isNaN(codePaddingTop) ? 0 : codePaddingTop; if (this.#globalStyleElement !== undefined) { fileContainer.appendChild(this.#globalStyleElement); } @@ -644,16 +649,20 @@ export class Editor implements DiffsEditor { if (target === undefined || textDocument === undefined) { return; } - const lineNumber = target.dataset.columnNumber; + const columnNumber = target.dataset.columnNumber; const lineType = target.dataset.lineType; if ( - lineNumber === undefined || + columnNumber === undefined || lineType === undefined || !isLineEditable(lineType) ) { return; } - const line = Number(lineNumber) - 1; + const lineNumber = parseInt(columnNumber, 10); + if (Number.isNaN(lineNumber)) { + return; + } + const line = lineNumber - 1; const selection: EditorSelection = { start: { line, character: 0 }, end: { @@ -682,17 +691,22 @@ export class Editor implements DiffsEditor { if (target === undefined) { return; } - const lineNumber = + + const line = target.dataset.columnNumber ?? target.dataset.line; const lineType = target.dataset.lineType; if ( this.#isGutterMouseDown && this.#textDocument !== undefined && - lineNumber !== undefined && + line !== undefined && lineType !== undefined && isLineEditable(lineType) ) { - const lineIndex = Number(lineNumber) - 1; + const lineNumber = parseInt(line, 10); + if (Number.isNaN(lineNumber)) { + return; + } + const lineIndex = lineNumber - 1; let selection: EditorSelection = { start: { line: lineIndex, character: 0 }, end: { @@ -1123,15 +1137,18 @@ export class Editor implements DiffsEditor { const el = child as HTMLElement; const line = el.dataset.line; if (line !== undefined) { - const lineIndex = Number(el.dataset.line) - 1; - const tokens = dirtyLines.get(lineIndex); - if (tokens !== undefined) { - el.replaceChildren( - ...renderLineTokens(tokens, tokenizer.themeType) - ); - dirtyLineIndexes.delete(lineIndex); - if (dirtyLineIndexes.size === 0) { - break; + const lineNumber = parseInt(line, 10); + if (!Number.isNaN(lineNumber)) { + const lineIndex = lineNumber - 1; + const tokens = dirtyLines.get(lineIndex); + if (tokens !== undefined) { + el.replaceChildren( + ...renderLineTokens(tokens, tokenizer.themeType) + ); + dirtyLineIndexes.delete(lineIndex); + if (dirtyLineIndexes.size === 0) { + break; + } } } } @@ -1144,16 +1161,19 @@ export class Editor implements DiffsEditor { i++ ) { const child = children[i] as HTMLElement | undefined; - if (child?.dataset.line !== undefined) { - const lineIndex = Number(child.dataset.line) - 1; - if (dirtyLines.has(lineIndex)) { - const tokens = dirtyLines.get(lineIndex)!; - child.replaceChildren( - ...renderLineTokens(tokens, tokenizer.themeType) - ); - dirtyLineIndexes.delete(lineIndex); - if (dirtyLineIndexes.size === 0) { - break; + if (child !== undefined && child.dataset.line !== undefined) { + const lineNumber = parseInt(child.dataset.line, 10); + if (!Number.isNaN(lineNumber)) { + const lineIndex = lineNumber - 1; + if (dirtyLines.has(lineIndex)) { + const tokens = dirtyLines.get(lineIndex)!; + child.replaceChildren( + ...renderLineTokens(tokens, tokenizer.themeType) + ); + dirtyLineIndexes.delete(lineIndex); + if (dirtyLineIndexes.size === 0) { + break; + } } } } @@ -1208,18 +1228,25 @@ export class Editor implements DiffsEditor { const children = parent.children; for (let i = children.length - 1; i >= 0; i--) { const child = children[i] as HTMLElement; - const { lineIndex, lineAnnotation } = child.dataset; - if (lineIndex !== undefined || lineAnnotation !== undefined) { - const lineIndexNum = Number( - lineAnnotation !== undefined - ? lineAnnotation.split(',')[1] - : lineIndex - ); - if (lineIndexNum < change.lineCount) { - break; - } - child.remove(); + const { line, columnNumber, lineAnnotation } = child.dataset; + if ( + line === undefined && + columnNumber === undefined && + lineAnnotation === undefined + ) { + continue; + } + const lineIndex = + lineAnnotation !== undefined + ? parseInt(lineAnnotation.split(',')[1], 10) + : parseInt(line ?? columnNumber!, 10) - 1; + if (Number.isNaN(lineIndex)) { + continue; } + if (lineIndex < change.lineCount) { + break; + } + child.remove(); } } } @@ -2234,7 +2261,7 @@ export class Editor implements DiffsEditor { lineNumber !== undefined && lineType !== undefined && isLineEditable(lineType) && - Number(lineNumber) === line + 1 + parseInt(lineNumber, 10) === line + 1 ) { return child; } @@ -2274,7 +2301,10 @@ export class Editor implements DiffsEditor { diffsColumnNumberWidth.length > 2 && diffsColumnNumberWidth.endsWith('px') ) { - this.#gutterWidthCache = Number(diffsColumnNumberWidth.slice(0, -2)); + this.#gutterWidthCache = parseInt( + diffsColumnNumberWidth.slice(0, -2), + 10 + ); } else { this.#gutterWidthCache = gutterElement.offsetWidth; } @@ -2298,7 +2328,9 @@ export class Editor implements DiffsEditor { diffsColumnContentWidth.length > 2 && diffsColumnContentWidth.endsWith('px') ) { - this.#contentWidthCache = Number(diffsColumnContentWidth.slice(0, -2)); + this.#contentWidthCache = parseFloat( + diffsColumnContentWidth.slice(0, -2) + ); } else { this.#contentWidthCache = this.#contentElement.offsetWidth; } diff --git a/packages/diffs/src/editor/selection.ts b/packages/diffs/src/editor/selection.ts index 31c4e9248..1db533fc7 100644 --- a/packages/diffs/src/editor/selection.ts +++ b/packages/diffs/src/editor/selection.ts @@ -1460,14 +1460,23 @@ function getLineChildEnd( function getLineIndex(el: HTMLElement): number | undefined { const { line } = el.dataset; if (line !== undefined) { - return parseInt(line) - 1; + const lineNumber = parseInt(line, 10); + if (!Number.isNaN(lineNumber)) { + return lineNumber - 1; + } } return undefined; } function getCharacterIndex(el: HTMLElement): number | undefined { const { char } = el.dataset; - return char !== undefined ? parseInt(char) : undefined; + if (char !== undefined) { + const charIndex = parseInt(char, 10); + if (!Number.isNaN(charIndex)) { + return charIndex; + } + } + return undefined; } function getTextOffset( diff --git a/packages/diffs/src/editor/textMeasure.ts b/packages/diffs/src/editor/textMeasure.ts index 24c270017..3839a08be 100644 --- a/packages/diffs/src/editor/textMeasure.ts +++ b/packages/diffs/src/editor/textMeasure.ts @@ -33,10 +33,10 @@ export class Metrics { const { fontSize, fontFamily, tabSize, lineHeight } = getComputedStyle(root); if (lineHeight.endsWith('px')) { - this.lineHeight = Number(lineHeight.slice(0, -2)); + this.lineHeight = parseFloat(lineHeight.slice(0, -2)); } else if (fontSize.endsWith('px')) { this.lineHeight = round( - Number(fontSize.slice(0, -2)) * Number(lineHeight) + parseFloat(fontSize.slice(0, -2)) * parseFloat(lineHeight) ); } const font = fontSize + ' ' + fontFamily; @@ -45,7 +45,7 @@ export class Metrics { this.#canvasCtx.font = font; this.ch = this.canvasMeasureTextWidth('0'); } - this.tabSize = Number(tabSize); + this.tabSize = parseInt(tabSize, 10); } /** measure the width of the text */