SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

Diff

A change to read and review.

0 / 2 files viewed
Modifiedsrc/lexer/identifier.ts9 additions, 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
import type { Token } from "./tokens.js";
import { isIdentifierPart, isIdentifierStart } from "./chars.js";
import { isIdentifierPart, isIdentifierStart } from "./unicode.js";
/** Reads one identifier starting at `offset`, or nothing when none starts there. */
export function readIdentifier(source: string, offset: number): Token | undefined {
if (!isIdentifierStart(source.charCodeAt(offset))) return undefined;
let first = source.codePointAt(offset);
if (first === undefined || !isIdentifierStart(first)) return undefined;
let end = offset + 1;
while (end < source.length && isIdentifierPart(source.charCodeAt(end))) end++;
let end = offset + (first > 0xffff ? 2 : 1);
  1. AS

    anacommentedyesterday

    The same > 0xffff ? 2 : 1 check shows up again on line 14. Worth a codePointLength() helper?

  2. SX

    sergiocommented2 hours ago

    Agreed, the string lexer needs it too. I'll add it to unicode.ts.

while (end < source.length) {
let code = source.codePointAt(end) ?? 0;
if (!isIdentifierPart(code)) break;
end += code > 0xffff ? 2 : 1;
}
return {
kind: "identifier",
Addedsrc/lexer/unicode.ts17 additions, 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
/**
* Identifier classes from Unicode's ID_Start and ID_Continue, which ECMAScript names follow,
* so `ñandú` and `𝑥` read as identifiers.
*/
const ID_START = /\p{ID_Start}/u;
const ID_CONTINUE = /\p{ID_Continue}/u;
/** Whether `code` may begin an identifier: a letter, `$` or `_`. */
export function isIdentifierStart(code: number): boolean {
return code === 0x24 || code === 0x5f || ID_START.test(String.fromCodePoint(code));
}
/** Whether `code` may continue one: anything that starts one, a digit, a mark, ZWNJ or ZWJ. */
export function isIdentifierPart(code: number): boolean {
if (code === 0x200c || code === 0x200d) return true;
return isIdentifierStart(code) || ID_CONTINUE.test(String.fromCodePoint(code));
}
View code
tsx
import { diffLines } from "@sdxc/diff";

let { hunks } = diffLines(before, after);

<Diff labels={{ copied: "Copied" }}>
	<Diff.Toolbar
		view="unified"
		unifiedHref="/acme/delta/pull/412/files?diff=unified"
		splitHref="/acme/delta/pull/412/files?diff=split"
		viewed={0}
		total={2}
		labels={toolbarLabels}
	/>
	<Diff.File
		path="src/lexer/identifier.ts"
		status="modified"
		additions={9}
		deletions={4}
		hunks={hunks}
		language="ts"
		commentHref={(path, side, line) => `/acme/delta/pull/412/files?comment=${path}:${side}${line}`}
		threads={{ R10: <ReviewThread comments={comments} replyAction={replyAction} resolveAction={resolveAction} labels={threadLabels} /> }}
		menu={<FileMenu />}
		viewedControl={<OptimisticToggle action="/acme/delta/pull/412/files/viewed" name="viewed" pressed={false} … />}
		labels={fileLabels}
	/>
	<Diff.File path="src/lexer/unicode.ts" status="added" additions={17} deletions={0} hunks={added} language="ts" labels={fileLabels} />
</Diff>

Demo apps

Whole pages built only from @sdxc/ui, each for an invented product with its own palette and type. They open in a new tab.

Every demo, landings and docs sites alike, is listed in the demo gallery.

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

Terminal
npm add @sdxc/ui remix
typescript
import "@sdxc/ui/theme.css";

Usage

typescript
import { Diff } from "@sdxc/ui/app";

Without script each + links to commentHref; mapping @sdxc/ui/app/diff runs one DiffInteractions for every file, turning +, drags and copy into DIFF_COMMENT_EVENT. Set --diff-font-size, --diff-line-height and the --diff-add-*/--diff-del-* tints.

Composition

The parts the component publishes, each a static property of the host.

  • Diff
  • Diff.ToolbarRenders the bar above the files: the unified/split switch as two GET links carried in the address, the app's own controls, then "n / N files viewed" with a bar that fills as the review burns down.
  • Diff.FileRenders one file's card: a <details> whose summary carries the chevron, status, path, counts and five-block bar, with copy, ⋯ and "Viewed" beside it, then the lines or the note standing in for them.
  • Diff.HunksRenders hunks as one table without a card, for Diff.File and for a thread's excerpt or suggestion.

Examples

Split, old and new side by side

Modifiedsrc/lexer/identifier.ts9 additions, 4 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -1,13 +1,18 @@
import type { Token } from "./tokens.js";import type { Token } from "./tokens.js";
import { isIdentifierPart, isIdentifierStart } from "./chars.js";import { isIdentifierPart, isIdentifierStart } from "./unicode.js";
/** Reads one identifier starting at `offset`, or nothing when none starts there. *//** Reads one identifier starting at `offset`, or nothing when none starts there. */
export function readIdentifier(source: string, offset: number): Token | undefined {export function readIdentifier(source: string, offset: number): Token | undefined {
if (!isIdentifierStart(source.charCodeAt(offset))) return undefined; let first = source.codePointAt(offset);
if (first === undefined || !isIdentifierStart(first)) return undefined;
let end = offset + 1; let end = offset + (first > 0xffff ? 2 : 1);
while (end < source.length && isIdentifierPart(source.charCodeAt(end))) end++; while (end < source.length) {
let code = source.codePointAt(end) ?? 0;
if (!isIdentifierPart(code)) break;
end += code > 0xffff ? 2 : 1;
}
return { return {
kind: "identifier", kind: "identifier",
View code
tsx
<Diff labels={{ copied: "Copied" }}>
	<Diff.Toolbar
		view="split"
		unifiedHref="/acme/delta/pull/412/files?diff=unified"
		splitHref="/acme/delta/pull/412/files?diff=split"
		labels={toolbarLabels}
	/>
	<Diff.File path="src/lexer/identifier.ts" status="modified" additions={9} deletions={4} hunks={hunks} language="ts" view="split" labels={fileLabels} />
</Diff>

Binary and too large to show

Modifieddocs/assets/identifiers.png0 additions, 0 deletions

Binary file not shown.

Modifiedpackage-lock.json1834 additions, 1210 deletions

Large diff not shown.

View code
tsx
<Diff labels={{ copied: "Copied" }}>
	<Diff.File path="docs/assets/identifiers.png" status="modified" additions={0} deletions={0} binary labels={fileLabels} />
	<Diff.File path="package-lock.json" status="modified" additions={1834} deletions={1210} tooLarge labels={fileLabels} />
</Diff>

A viewed file, collapsed

1 / 2 files viewed
Modifiedsrc/lexer/identifier.ts9 additions, 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
import type { Token } from "./tokens.js";
import { isIdentifierPart, isIdentifierStart } from "./chars.js";
import { isIdentifierPart, isIdentifierStart } from "./unicode.js";
/** Reads one identifier starting at `offset`, or nothing when none starts there. */
export function readIdentifier(source: string, offset: number): Token | undefined {
if (!isIdentifierStart(source.charCodeAt(offset))) return undefined;
let first = source.codePointAt(offset);
if (first === undefined || !isIdentifierStart(first)) return undefined;
let end = offset + 1;
while (end < source.length && isIdentifierPart(source.charCodeAt(end))) end++;
let end = offset + (first > 0xffff ? 2 : 1);
while (end < source.length) {
let code = source.codePointAt(end) ?? 0;
if (!isIdentifierPart(code)) break;
end += code > 0xffff ? 2 : 1;
}
return {
kind: "identifier",
Modifiedsrc/lexer/index.ts1 additions, 1 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
export { readIdentifier } from "./identifier.js";
export { readNumber } from "./number.js";
export { readString } from "./string.js";
export { isIdentifierPart, isIdentifierStart } from "./chars.js";
export { isIdentifierPart, isIdentifierStart } from "./unicode.js";
View code
tsx
<Diff labels={{ copied: "Copied" }}>
	<Diff.Toolbar view="unified" unifiedHref={unifiedHref} splitHref={splitHref} viewed={1} total={2} labels={toolbarLabels} />
	<Diff.File
		path="src/lexer/identifier.ts"
		status="modified"
		additions={9}
		deletions={4}
		hunks={hunks}
		viewed
		viewedControl={<OptimisticToggle action="/acme/delta/pull/412/files/viewed" name="viewed" pressed … />}
		labels={fileLabels}
	/>
	<Diff.File path="src/lexer/index.ts" status="modified" additions={1} deletions={1} hunks={indexHunks} language="ts" labels={fileLabels} />
</Diff>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeRequiredDescription
labelsLabelsYesThe strings the page's island announces, so it reads in any language.
childrenRemixNodeNoA Diff.Toolbar and the page's Diff.File cards.

Also accepts everything in TagProps<"div">.

Diff.Toolbar

PropTypeRequiredDescription
view"unified" | "split"YesThe view the page renders, which the switch marks current.
unifiedHrefstringYesThis page's address with ?diff=unified.
splitHrefstringYesThis page's address with ?diff=split.
targetstringNoThe frame the view links swap, such as "main".
viewednumberNoFiles marked viewed; with total, draws "n / N files viewed" and its bar.
totalnumberNoThe files on the page; with viewed, draws the progress.
labelsToolbarLabelsYesThe toolbar's strings.
childrenRemixNodeNoControls between the switch and the progress: "Jump to", a file filter.

Also accepts everything in TagProps<"div">.

Diff.File

PropTypeRequiredDescription
oldPathstringNoThe path on the old side, drawn as old → new when it differs from path.
statusFileStatusYesDrawn as a toned letter (A, M, D, R, C) and read out through labels.statuses.
additionsnumberYesLines added, counted in the header and its five-block bar.
deletionsnumberYesLines deleted, counted in the header and its five-block bar.
hunksreadonly Hunk[]NoThe file's hunks; omitted for a binary file, a dropped patch or a lazy one.
binarybooleanNoGit declared the file binary.
tooLargebooleanNoThe patch was dropped for size, so the file draws "Large diff not shown".
collapsedbooleanNoRenders closed, as a reader's collapse would.
viewedbooleanNoThe reader marked the file viewed, which renders it closed.
viewedControlRemixNodeNoThe "Viewed" checkbox, as a form that posts the mark (an OptimisticToggle).
menuRemixNodeNoA ⋯ menu trigger at the header's end.
loadRemixNodeNoWhat a large file shows in place of its lines, such as a lazy frame's "Load diff"; given, a file past largeThreshold renders collapsed and draws no row.
largeThresholdnumberNoChanged lines past which the file counts as large. @default DEFAULT_LARGE_THRESHOLD
labelsFileLabelsYesThe card's strings, so it reads in any language.

Also accepts everything in Omit<TagProps<"section">, "children">.

Also accepts everything in Omit<HunksProps, keyof TagProps<"div"> | "hunks" | "labels" | "fileId" | "anchored">.

Diff.Hunks

PropTypeRequiredDescription
pathstringYesThe file's path, which names the table and keys the comment links.
hunksreadonly Hunk[]YesThe parsed hunks to draw, with their word-level ranges.
languagestringNoLanguage the code highlights as (ts, css); omitted, lines draw plain.
view"unified" | "split"NoOne column, or old and new side by side above 48rem. @default "unified"
fileIdstringNoPrefix of every line anchor, <fileId>R12. @default "diff-" plus a hash of the path
threadsReadonly<Record<string, RemixNode>>NoReview threads drawn as a full-width row after their line, keyed R12 (new side) or L12 (old side).
lineHref(side: DiffSide, line: number) => stringNoThe address a line's number links to. @default #<fileId><side><line>
commentHref(path: string, side: DiffSide, line: number) => stringNoWhere a line's + goes without script; set, every line draws a +.
expandHref(hunk: Hunk, index: number) => stringNoWhere a hunk's expand link goes, which the app can answer as a frame.
chunkSizenumberNoLines per chunk. @default DEFAULT_CHUNK_SIZE
anchoredbooleanNoGives each number an id and a link. Turn it off for a copy of lines shown beside the file itself (a thread's excerpt or suggestion), so anchors stay unique. @default true
labelsHunksLabelsYesThe table's strings.

Also accepts everything in TagProps<"div">.

Types

NameValuesWhat it decides
View"unified" | "split"How hunks draw: one column of lines, or the old and new sides beside each other.
@sdxc/ui

Can’t find it? Contact us