Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

DiffView

DiffView mostra le modifiche tra due testi come negli strumenti di code review: affiancati (side by side), con il testo vecchio a sinistra e quello nuovo a destra, con le righe allineate, oppure unificati, con le righe eliminate sopra quelle aggiunte. All’interno di una riga modificata le parole cambiate sono evidenziate, le righe invariate tra una modifica e l’altra si comprimono in separatori espandibili e il testo è evidenziato con i colori di sintassi del Editor. Mostra due testi (oldValue e newValue) oppure i file di un unified diff (patch: quello di git diff, di una pull request), ciascuno sotto la propria intestazione, ed è pensato per rivedere modifiche, comprese quelle proposte da un’AI: le azioni dell’app su ogni modifica la mantengono o la annullano. Vengono create solo le righe visibili, quindi un file lungo scorre come uno breve.

el-diff-view.tsx
el-diff-view.tsxLive
import { mount, signal, Text, View } from "nsh";
import { DiffView, type DiffViewRef } from "nsh/base";
import { button, buttonText, desktop, Log, Panel, Segmented, size, Split, ui, wide } from "./palette";

const OLD = `fn greet(name: &str) -> String {
    format!("Hello, {}", name)
}

fn main() {
    let names = vec!["Ada", "Grace"];
    for name in names {
        println!("{}", greet(name));
    }
}
`;

const NEW = `fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

fn main() {
    let names = ["Ada", "Grace", "Linus"];
    for name in names {
        println!("{}", greet(name));
    }
}
`;

// The texts are the app's: a change reverted or kept is reported
// (`onChange`) and set back as `oldValue` and `newValue`.
function App() {
  const layout = signal<"split" | "unified">(desktop ? "split" : "unified");
  const inline = signal<"wordAlt" | "char" | "none">("wordAlt");
  const oldValue = signal(OLD);
  const newValue = signal(NEW);
  const stats = signal("");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  let diff: DiffViewRef | undefined;
  const count = async () => {
    const s = await diff?.stats();
    stats.set(s ? `+${s.added} −${s.deleted} · ${s.hunks} changes` : "");
  };

  const action = (label: string, onPress: () => void, primary?: boolean) => (
    <View onPress={onPress} role="button" accessibilityLabel={label} {...button(primary ? "accent" : "secondary", { size: "sm" })}>
      <Text style={buttonText(primary ? "accent" : "secondary")}>{label}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>diff view</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click Revert or Keep on a change, or a line number" : "Tap Revert or Keep on a change, or a line number"}
      </Text>

      <Split
        grow
        width={680}
        inspector={
          <>
            <Segmented label="layout" options={["split", "unified"] as const} value={layout()} onChange={(v) => layout.set(v)} />
            <Segmented label="inline" options={["wordAlt", "char", "none"] as const} value={inline()} onChange={(v) => inline.set(v)} />
            <Panel title="stats">
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>{stats()}</Text>
              <View style={{ flexDirection: "row", gap: 8, marginTop: 6 }}>
                <View
                  onPress={() => {
                    oldValue.set(OLD);
                    newValue.set(NEW);
                    setTimeout(count, 0);
                  }}
                  role="button"
                  {...button("secondary", { size: "sm" })}
                >
                  <Text style={buttonText()}>Reset</Text>
                </View>
              </View>
            </Panel>
            <Log title="events" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 360 : 320, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <DiffView
            ref={(r) => {
              diff = r;
              count();
            }}
            oldValue={oldValue()}
            newValue={newValue()}
            language="rust"
            layout={layout()}
            inline={inline()}
            contextLines={1}
            renderHunkActions={(hunk) => (
              <View style={{ flexDirection: "row", gap: 4 }}>
                {action("Revert", () => diff?.revertHunk(hunk.index))}
                {action("Keep", () => diff?.acceptHunk(hunk.index), true)}
              </View>
            )}
            onChange={({ change }) => {
              oldValue.set(change.oldValue);
              newValue.set(change.newValue);
              log(`onChange: ${change.newValue.split("\n").length - 1} lines`);
              count();
            }}
            onLineNumberPress={({ line }) => log(`onLineNumberPress: ${line.side} ${line.line + 1}`)}
            style={{ flex: 1 }}
          />
        </View>
      </Split>
    </View>
  );
}

mount(App);

DiffView gestisce il comportamento e ha un aspetto proprio, costruito con i token del tema: il font-mono del tema a 13pt in righe da 20pt, i numeri di riga in muted-foreground, le righe aggiunte con tinta verde e quelle eliminate con tinta rossa, le parole modificate con una tinta più intensa, una barra al bordo della gutter di ogni riga modificata e i separatori su muted. Ogni parte accetta gli stili dell’app al posto di questi valori predefiniti (classes e styles). Rust ne crea uno con nsh_base::diff_view(), TSX con <DiffView> da nsh/base.

use nsh_base::diff_view;
use nsh_core::prelude::*;

const OLD: &str = "fn total(prices: &[u32]) -> u32 {\n    prices.iter().sum()\n}\n";
const NEW: &str = "fn total(prices: &[u64]) -> u64 {\n    prices.iter().copied().sum()\n}\n";

fn review() -> impl IntoElement {
    diff_view().old_value(OLD).new_value(NEW).language("rust").flex_1()
}

La vista riempie il riquadro che l’app le assegna (flex-1, un’altezza) e scorre al suo interno: in verticale attraverso le righe e in orizzontale fino alla riga più lunga, con i due lati di una vista affiancata che scorrono insieme. Con softWrap le righe lunghe vanno a capo alla larghezza del lato, e una riga affiancata è alta quanto il suo lato più alto, così i lati restano allineati.

#Due testi o una patch

  • Due testi: oldValue e newValue. Le righe sono confrontate così come sono, quindi un’interruzione di riga finale aggiunta o rimossa è una modifica, come mostra git.
  • Una patch: patch, un unified diff di uno o più file (git diff, git format-patch, diff -u; rinominazioni, modalità, file nuovi, eliminati e binari, e un messaggio di commit che li precede, letti come li scrive git). La vista mostra le righe dei suoi hunk, numerate come nel file, ciascun hunk sotto un separatore con la propria intestazione (la funzione in cui si trova), e ciascun file, se sono più di uno, sotto un’intestazione propria (File di una patch). Il percorso di ogni file indica il suo linguaggio quando language non è impostato.
const PATCH = `diff --git a/src/total.rs b/src/total.rs
--- a/src/total.rs
+++ b/src/total.rs
@@ -10,3 +10,3 @@ impl Cart {
-    fn total(&self) -> u32 {
+    fn total(&self) -> u64 {
         self.prices.iter().sum()
     }
`;

<DiffView patch={PATCH} layout="unified" class="flex-1" />

language indica il linguaggio come fa il Editor: un nome, un alias, un’estensione o un nome di file ("rust", "tsx", "main.go", "Dockerfile").

#File di una patch

Una patch di più file mostra ogni file sotto una riga di intestazione: per impostazione predefinita il suo percorso (old → new se rinominato), il numero di righe aggiunte ed eliminate e ▾, e una pressione su di essa comprime il file alla sua sola intestazione (▸) oppure lo espande. renderFileHeader disegna le intestazioni a modo dell’app (e ne mostra una anche sopra una patch di un solo file), ricevendo il file (DiffFile): index, path, oldPath (rinominato o copiato da), status ("modified", "added", "deleted", "renamed", "copied"), binary (la patch non mostra righe di questo file), added, deleted, hunks, firstHunk e collapsed. L’intestazione dell’app chiama il toggleFile(path) del ref; collapseFile e expandFile impostano lo stato, e ogni cambiamento viene notificato con onFileToggle({ file }). Un file compresso resta compresso finché un nuovo patch lo contiene ancora.

Le modifiche si contano tra i file, in ordine: le modifiche di un file sono firstHunk a firstHunk + hunks - 1, che goToHunk, renderHunkActions e hunks() utilizzano (il file di un DiffHunk è l’indice del suo file e le sue righe si contano in quel file), e stats() somma quelle di ogni file. goToHunk espande prima un file compresso.

let diff!: DiffViewRef;
const files = signal<DiffFile[]>([]);

<View class="flex-1 flex-row">
  <View class="w-48">
    <Each of={files()}>
      {(file) => (
        <Button onPress={() => diff.scrollToFile(file().path)} class="px-2 py-1">
          <Text class="text-sm">{file().path} +{file().added} −{file().deleted}</Text>
        </Button>
      )}
    </Each>
  </View>
  <DiffView
    ref={(r) => {
      diff = r;
      r.files().then((list) => files.set(list));
    }}
    patch={PULL_REQUEST}
    renderFileHeader={(file) => (
      <Button onPress={() => diff.toggleFile(file.path)} class="flex-1 flex-row items-center gap-2 bg-muted px-2">
        <Text>{file.collapsed ? "▸" : "▾"}</Text>
        <Text class="flex-1 text-sm font-medium">{file.oldPath ? `${file.oldPath} → ${file.path}` : file.path}</Text>
        <Text class="text-xs text-muted-foreground">{file.binary ? "Binary" : file.status}</Text>
      </Button>
    )}
    class="flex-1"
  />
</View>

#Layout e modifiche inline

layout è "split" (il valore predefinito: affiancato, con ogni riga eliminata accanto alla riga aggiunta più simile) oppure "unified" (una colonna, con le righe eliminate di una modifica sopra le sue righe aggiunte, con entrambi i numeri di riga). inline stabilisce che cosa viene evidenziato all’interno di una riga modificata:

inlineCosa viene evidenziato
"wordAlt" (predefinito)Le parole cambiate, unite quando distano un solo carattere, così una frase modificata si legge come una sola
"word"Ogni parola cambiata
"char"Ogni carattere cambiato
"none"Niente: solo le tinte delle righe

Le parole sono sequenze di lettere, cifre e _; ogni carattere CJK, kana e sillaba hangul è una parola a sé, quindi un carattere modificato in una frase segna solo se stesso. Una riga modificata quasi per intero mostra solo la propria tinta.

indicators segna le righe modificate accanto ai loro numeri: "bars" (il valore predefinito, una barra al bordo della gutter), "classic" (+ e − prima del testo) oppure "none". background={false} lascia le righe modificate senza tinta, e lineNumbers={false} nasconde i numeri.

<DiffView oldValue={before()} newValue={after()} language="md" layout="unified" inline="word" indicators="classic" class="flex-1" />

#Righe compresse

Attorno a ogni modifica sono visibili contextLines righe invariate (3 per impostazione predefinita); le righe invariate tra una modifica e l’altra, e quelle prima della prima e dopo l’ultima, si comprimono in un separatore che indica quante righe nasconde. Premendolo, le mostra tutte. expandAll mostra ogni riga; expand, expandAll() e collapseAll() del ref le rivelano e le comprimono dall’app.

renderSeparator disegna i separatori a modo dell’app. Riceve il separatore (DiffSeparator): gap, l’indice della sequenza compressa (0 prima della prima modifica, i tra le modifiche i - 1 e i), lines, quante righe nasconde, first e last, e per una patch l’header dell’hunk. expand(gap, direction, lines) mostra "all" le sue righe (il valore predefinito), oppure lines (predefinito 20) "up", appena sopra la modifica che segue il separatore, oppure "down", appena sotto la modifica che lo precede. Il primo separatore si espande verso l’alto e l’ultimo verso il basso.

let diff!: DiffViewRef;

<DiffView
  ref={diff}
  oldValue={before()}
  newValue={after()}
  language="ts"
  renderSeparator={(sep) => (
    <View class="flex-1 flex-row items-center gap-1 bg-muted px-2">
      <If when={!sep.first}>
        <Button onPress={() => diff.expand(sep.gap, "down")} class="px-1"><Text>↓</Text></Button>
      </If>
      <If when={!sep.last}>
        <Button onPress={() => diff.expand(sep.gap, "up")} class="px-1"><Text>↑</Text></Button>
      </If>
      <Button onPress={() => diff.expand(sep.gap)} class="flex-1">
        <Text class="text-xs text-muted-foreground">{sep.header ?? `${sep.lines} unchanged lines`}</Text>
      </Button>
    </View>
  )}
  class="flex-1"
/>

#Mantenere e annullare le modifiche

renderHunkActions mette le azioni dell’app in alto a destra di ogni modifica visibile, ricevendo la modifica (DiffHunk: index, oldStart, oldLines, newStart, newLines, righe a partire da 0, e file, l’indice del suo file in una patch). Chiamano il ref: revertHunk(index) rimette le vecchie righe della modifica nel nuovo testo, acceptHunk(index) rende le nuove righe della modifica quelle del vecchio testo. La vista mostra subito il risultato e notifica entrambi i testi con onChange({ change }) ({ oldValue, newValue }), che l’app imposta come prop. Mantenere o annullare la modifica di un’AI una alla volta si fa così:

import { signal, Text, View } from "nsh";
import { Button, DiffView, type DiffViewRef } from "nsh/base";

function Proposal(props: { file: string; current: string; proposed: string; onApply: (text: string) => void }) {
  let diff!: DiffViewRef;
  const current = signal(props.current);
  const proposed = signal(props.proposed);
  return (
    <View class="flex-1">
      <DiffView
        ref={diff}
        oldValue={current()}
        newValue={proposed()}
        language={props.file}
        onChange={({ change: { oldValue, newValue } }) => {
          current.set(oldValue);
          proposed.set(newValue);
          props.onApply(oldValue);
        }}
        renderHunkActions={(hunk) => (
          <View class="flex-row gap-1">
            <Button onPress={() => diff.revertHunk(hunk.index)} class="rounded bg-background px-2 py-0.5 border border-border">
              <Text class="text-xs">Revert</Text>
            </Button>
            <Button onPress={() => diff.acceptHunk(hunk.index)} class="rounded bg-primary px-2 py-0.5">
              <Text class="text-xs text-primary-foreground">Keep</Text>
            </Button>
          </View>
        )}
        class="flex-1"
      />
    </View>
  );
}

Qui il vecchio testo è il file così com’è: mantenere una modifica la applica al file (onApply), annullarla la toglie dalla proposta. Una vista che mostra un patch mostra estratti dei file, e le sue modifiche restano come sono.

#Metodi

ref={diff} fornisce i metodi della vista (DiffViewRef). I metodi che restituiscono un valore (hunks(), stats()) in JS ne restituiscono una Promise, quindi usa await su di essi: const changes = await diff.hunks(). Per i conteggi mostrati a schermo, onStatsChange li notifica man mano che cambiano. Un metodo a cui si passa una modifica, un separatore o un file che il diff non ha non cambia nulla e registra un avviso con il nome del metodo e il valore.

MetodoDescrizione
hunks()Promise<DiffHunk[]>: le modifiche, in ordine.
stats()Promise<DiffStats>: { added, deleted, hunks }, le righe aggiunte ed eliminate e il numero di modifiche.
goToHunk(index)Porta in vista la modifica index, con la sua prima riga in alto.
revertHunk(index)Rimette le vecchie righe della modifica index nel nuovo testo e notifica i testi (onChange).
acceptHunk(index)Rende le nuove righe della modifica index quelle del vecchio testo e notifica i testi (onChange).
expand(gap, direction?, lines?)Mostra le righe che il separatore gap nasconde: "all" (predefinito), oppure lines (predefinito 20) "up" o "down".
expandAll()Mostra ogni riga invariata.
collapseAll()Comprime di nuovo le righe invariate.
nextHunk()Promise<number | null>: porta in alto la modifica successiva e ne restituisce l’indice (dopo l’ultima, la prima).
previousHunk()Promise<number | null>: porta in alto la modifica precedente e ne restituisce l’indice (prima della prima, l’ultima).
currentHunk()Promise<number | null>: l’ultima modifica raggiunta finché la vista resta lì, altrimenti quella in cima alla vista o la prima visibile.
selectedLines()Promise<DiffLineRange | null>: le righe selezionate sui numeri di riga.
setSelectedLines(range)Seleziona righe di un lato (null: nessuna), senza onLineSelectionChange.
selectedText()Promise<string>: il testo delle righe selezionate.
files()Promise<DiffFile[]>: i file della patch, in ordine, con i loro conteggi (nessuno per due testi).
scrollToFile(path)Porta in alto l’intestazione del file in path.
collapseFile(path)Mostra solo l’intestazione del file in path.
expandFile(path)Mostra di nuovo le righe del file in path.
toggleFile(path)Comprime il file in path, oppure lo espande se è compresso.
hunkBounds()Promise<DiffHunkBounds[]>: dove si trova ogni modifica tra le righe, in ordine (Un righello di panoramica).
scrollOffset()Promise<Point>: di quanto sono scorse le righe (x è 0).
setScrollOffset(offset)Fa scorrere subito le righe a offset.y, fin dove arrivano, come un cursore che segue il puntatore che lo trascina.

nextHunk() e previousHunk() proseguono dalla modifica a cui sono andati finché la vista resta lì, altrimenti dalla cima della vista, così una modifica vicina alla fine che non può scorrere fino in alto conta comunque. Pulsanti per la modifica successiva e precedente, con i conteggi da onStatsChange:

let diff!: DiffViewRef;
const stats = signal<DiffStats>({ added: 0, deleted: 0, hunks: 0 });

<View class="flex-row items-center gap-2 p-2">
  <Text class="text-sm text-green-600">+{stats().added}</Text>
  <Text class="text-sm text-red-600">−{stats().deleted}</Text>
  <Text class="text-sm text-muted-foreground">{stats().hunks} changes</Text>
  <Button onPress={() => diff.previousHunk()}><Text>Previous</Text></Button>
  <Button onPress={() => diff.nextHunk()}><Text>Next</Text></Button>
</View>
<DiffView ref={diff} oldValue={before()} newValue={after()} onStatsChange={(e) => stats.set(e.stats)} class="flex-1" />

#Un righello di panoramica

onScroll({ offset, contentSize, size }) notifica dove si trovano le righe, con le chiavi di quelle di un Scroll: offset.y, di quanto sono scorse (le righe scorrono in verticale, larghe quanto la vista, e i lati scorrono in orizzontale per conto proprio: offset.x è 0), contentSize, la dimensione di tutte le righe insieme, e size, quella del viewport. Scatta una volta che la vista è stata impaginata e di nuovo ogni volta che uno di questi valori cambia: quando le righe scorrono, quando la vista viene ridimensionata e quando le righe cambiano altezza (altri testi, layout, softWrap, un file o un separatore espanso o compresso, un’annotazione impaginata). hunkBounds() indica dove si trova ogni modifica tra le righe (DiffHunkBounds: { index, bounds }, bounds dalla cima della sua prima riga al fondo dell’ultima, con le annotazioni sotto, per tutta la larghezza della vista), cioè le modifiche che si vedono: quelle di un file compresso non hanno righe. onHunkBoundsChange({ hunkBounds }) fornisce lo stesso elenco una volta che la vista è stata impaginata e di nuovo ogni volta che una modifica si sposta o cambia dimensione, anche quando le righe mantengono la loro altezza (una riga modificata che ne diventa un’altra): un righello disegna i propri segni a partire da esso, anziché leggerli su onScroll. scrollOffset() legge di quanto sono scorse le righe, e setScrollOffset({ x, y }) fa scorrere subito le righe a y, così un cursore trascinato segue il puntatore nello stesso frame.

Insieme formano un righello di panoramica accanto alla vista, come negli editor di codice: un segno per ogni modifica, un cursore sulle righe visibili e una pressione o un trascinamento che scorre fin lì. Qui il righello è alto quanto la vista:

use nsh_base::{DiffHunkBounds, DiffView, DiffViewApi, DiffViewEvent, diff_view, theme};
use nsh_core::prelude::*;

fn review(old: SharedString, new: SharedString) -> impl IntoElement {
    let diff = component_ref::<DiffView>();
    let ruler = element_ref();
    let ruler_top = stored(px(0.0));
    // How far the rows are scrolled, their height and the viewport's.
    let rows = signal((px(0.0), px(0.0), px(0.0)));
    let marks = signal(Vec::<DiffHunkBounds>::new());
    // Points of the rows per point of the ruler.
    let scale = move || {
        let (_, height, view) = rows.get();
        height.max(px(1.0)) / view.max(px(1.0))
    };
    // The thumb's middle at the pointer.
    let follow = move |page_y: Points| {
        let view = rows.get_untracked().2;
        let y = (page_y - ruler_top.get()) * scale() - view / 2.0;
        if let Some(api) = diff.api::<DiffViewApi>() {
            api.set_scroll_offset(point(px(0.0), y));
        }
    };
    view()
        .flex_1()
        .flex_row()
        .child(
            diff_view()
                .old_value(old)
                .new_value(new)
                .on_scroll(move |e: &DiffViewEvent| {
                    if let DiffViewEvent::Scroll(offset, content, size) = e {
                        rows.set((offset.y, content.height, size.height));
                    }
                })
                .on_hunk_bounds_change(move |list: &Vec<DiffHunkBounds>| marks.set(list.clone()))
                .component_ref(&diff)
                .flex_1(),
        )
        .child(
            view()
                .element_ref(&ruler)
                .w(px(12.0))
                .bg(move || theme().muted)
                .claim_touches(TouchClaim::on_start())
                .on_responder_grant(move |e: &ResponderEvent| {
                    let y = e.page.y;
                    spawn(async move {
                        ruler_top.replace(ruler.measure().await.map_or(px(0.0), |m| m.page.y));
                        follow(y);
                    });
                })
                .on_responder_move(move |e: &ResponderEvent| follow(e.page.y))
                .child(each(
                    marks,
                    |mark: &DiffHunkBounds| mark.index,
                    move |mark, _| {
                        view()
                            .absolute()
                            .left(px(0.0))
                            .right(px(0.0))
                            .bg(rgb(0xf59e0b))
                            .top(move || mark.get().bounds.origin.y / scale())
                            .h(move || (mark.get().bounds.size.height / scale()).max(px(2.0)))
                    },
                ))
                .child(
                    view()
                        .absolute()
                        .left(px(0.0))
                        .right(px(0.0))
                        .bg(move || theme().foreground.opacity(0.2))
                        .top(move || rows.get().0 / scale())
                        .h(move || rows.get().2 / scale()),
                ),
        )
}

Il index di un segno è quello della modifica in hunks(), i cui oldLines e newLines distinguono una modifica aggiunta (senza vecchie righe) da una eliminata (senza nuove righe), per colorare il segno.

#Commenti di revisione

I numeri di riga selezionano righe di un lato: una pressione ne seleziona una, una pressione con Shift estende la selezione fino a essa, e trascinare il mouse sui numeri seleziona quelli su cui passa. onLineSelectionChange notifica le righe come range (DiffLineRange: { side, start, end }, righe a partire da 0, null per nessuna), e la pressione su un numero notifica anche onLineNumberPress({ line }) ({ side, line }). Il ref legge la selezione (selectedLines(), selectedText()) e la imposta.

annotations mette il contenuto dell’app sotto le righe: ogni { id, side, line } è disegnata da renderAnnotation su tutta la larghezza del lato della sua riga (l’intera vista se unificata), alta quanto risulta dall’impaginazione, e resta visibile finché lo è la sua riga. renderAddAnnotation è il controllo dell’app per aggiungerne una, a destra della gutter della riga su cui si trova il puntatore e dell’ultima riga selezionata; riceve le righe selezionate quando la sua riga è una di esse, altrimenti quella riga.

type Comment = { id: string; side: "old" | "new"; line: number; text: string };
const comments = signal<Comment[]>([]);
const drafting = signal<DiffLineRange | null>(null);

<DiffView
  oldValue={before()}
  newValue={after()}
  annotations={comments().map(({ id, side, line }) => ({ id, side, line }))}
  renderAnnotation={(a) => (
    <View class="m-2 rounded-md border border-border bg-card p-2">
      <Text class="text-sm">{comments().find((c) => c.id === a.id)?.text}</Text>
    </View>
  )}
  renderAddAnnotation={(range) => (
    <Button onPress={() => drafting.set(range)} class="size-4 items-center justify-center rounded bg-primary">
      <Text class="text-xs text-primary-foreground">+</Text>
    </Button>
  )}
  class="flex-1"
/>

#Stile delle parti

class e style stilizzano il box della vista. classes accetta classi Tailwind per parte e styles stili inline per parte; le classi di una parte vengono dopo i suoi stili predefiniti, quindi prevalgono, e i suoi styles prevalgono sulle sue classi. Il font si imposta con fontFamily, fontSize e lineHeight.

<DiffView
  oldValue={before()}
  newValue={after()}
  language="py"
  fontSize={12}
  lineHeight={18}
  class="flex-1 rounded-lg border border-border"
  classes={{
    gutter: "bg-muted",
    lineAdded: "bg-blue-500/10",
    wordAdded: "bg-blue-500/30",
    indicatorAdded: "bg-blue-500",
    separator: "bg-transparent",
  }}
/>
ParteChe cos’èPredefinito
sideOgni lato (split) o la colonna unica (unified)Una linea border tra i lati
gutterI numeri di riga e gli indicatori di un lato
lineNumberOgni numero di rigaAllineato a destra, muted-foreground
lineNumberAddedI numeri delle linee aggiunte, dopo lineNumber
lineNumberDeletedI numeri delle linee eliminate, dopo lineNumber
contentIl testo di un lato, accanto al suo gutter8pt prima e dopo il testo
lineOgni riga di un lato
lineAddedLe righe delle linee aggiunte, dopo lineUna tinta verde
lineDeletedLe righe delle linee eliminate, dopo lineUna tinta rossa
fillerLa metà vuota di una riga split accanto a una linea che l’altro lato non haUn riempimento muted tenue
wordAddedLe parole modificate all’interno di una linea aggiuntaUn verde più intenso
wordDeletedLe parole modificate all’interno di una linea eliminataUn rosso più intenso
indicatorAddedLa barra di una linea aggiunta ("bars") o + ("classic")Verde, larga 3pt
indicatorDeletedLa barra di una linea eliminata o −Rossa, larga 3pt
separatorUn separatore di linee compresse, o l’intestazione di un hunk di una patch⋯ e il conteggio su muted
hunkActionsIl box delle azioni dell’app su una modificaIn alto a destra della modifica
lineSelectedSopra le righe selezionate del loro latoIl colore di selezione del tema (come le selezioni di Editor)
annotationIl box di un’annotazione, sotto la sua lineaPer tutta la larghezza del lato della sua linea
addAnnotationIl box del controllo di aggiunta, largo quanto il gutter e alto una lineaIl controllo alla sua destra
fileHeaderL’intestazione di un file di una patchIl percorso e i conteggi su secondary, una linea border sotto

#Props

PropRustTipoPredefinitoDescrizione
oldValue.old_value(..)stringIl testo vecchio.
newValue.new_value(..)stringIl testo nuovo.
patch.patch(..)stringUn unified diff di uno o più file, mostrato quando oldValue e newValue non sono impostati.
language.language(..)stringtesto semplice (una patch: quello di ogni file)Il nome, l’alias, l’estensione o il nome file di un linguaggio.
layout.layout(..)"split" | "unified""split"Affiancato o a colonna unica.
inline.inline(..)"wordAlt" | "word" | "char" | "none""wordAlt"Che cosa viene evidenziato all’interno delle linee modificate.
indicators.indicators(..)"bars" | "classic" | "none""bars"Come le linee modificate vengono contrassegnate accanto ai loro numeri.
contextLines.context_lines(..)number3Linee non modificate mostrate attorno a ogni modifica.
expandAll.expand_all(..)booleanfalseTutte le linee non modificate vengono mostrate.
lineNumbers.line_numbers(..)booleantrueI numeri di riga.
background.background(..)booleantrueLe linee modificate hanno una tinta.
fontFamily.font_family(..)stringfont-mono del temaIl font del testo.
fontSize.font_size(..)number13La dimensione del font, in punti.
lineHeight.line_height(..)number20L’altezza di una riga, in punti.
renderSeparator.render_separator(..)(separator: DiffSeparator) => JSX.ElementDisegna i separatori delle linee compresse.
renderHunkActions.render_hunk_actions(..)(hunk: DiffHunk) => JSX.ElementLe azioni dell’app su ogni modifica.
softWrap.soft_wrap(..)booleanfalseLe linee lunghe vanno a capo alla larghezza del lato.
annotations.annotations(..)DiffAnnotation[][]Il contenuto dell’app sotto le linee: { id, side, line }.
renderAnnotation.render_annotation(..)(annotation: DiffAnnotation) => JSX.ElementDisegna un’annotazione sotto la sua linea.
renderAddAnnotation.render_add_annotation(..)(range: DiffLineRange) => JSX.ElementIl controllo dell’app per aggiungere un’annotazione, accanto alla linea sotto il puntatore e all’ultima selezionata.
renderFileHeader.render_file_header(..)(file: DiffFile) => JSX.ElementDisegna l’intestazione di ogni file di una patch, alta una riga.
classes.classes(..)DiffViewClasses{}Classi Tailwind per parte.
styles.styles(..)DiffViewStyles{}Stili inline per parte, sopra le classi.
class, stylemetodi di stilestring, StyleProp<ViewStyle>Il box della vista.

#Eventi

EventoRustDescrizione
onChange.on_change(|change: &DiffChange| ..)Una modifica è stata mantenuta o annullata: { change }, i testi attuali ({ oldValue, newValue }).
onLineNumberPress.on_line_number_press(|line: &DiffLine| ..)Un numero di riga è stato premuto: { line }, un { side, line }.
onLineSelectionChange.on_line_selection_change(|range: &Option<DiffLineRange>| ..)Le linee selezionate sono cambiate: { range }, un { side, start, end } o null.
onStatsChange.on_stats_change(|stats: &DiffStats| ..)I conteggi sono cambiati: { stats }, un { added, deleted, hunks }. Viene emesso una volta quando la vista viene creata e di nuovo ogni volta che i testi vengono confrontati con conteggi diversi, per una riga “+8 −8 · 5 changes”.
onFileToggle.on_file_toggle(|file: &DiffFile| ..)Un file della patch è stato compresso o espanso (la sua intestazione, o il ref): { file }, il DiffFile attuale.
onScroll.on_scroll(|e: &DiffViewEvent| ..)Le righe sono scorse, oppure le righe o il viewport hanno cambiato dimensione: { offset, contentSize, size }, come quello di un Scroll (DiffViewEvent::Scroll(offset, content_size, size) in Rust). Viene emesso una volta che la vista è stata impaginata e di nuovo quando uno di essi cambia.
onHunkBoundsChange.on_hunk_bounds_change(|bounds: &Vec<DiffHunkBounds>| ..)La posizione delle modifiche nelle righe è cambiata: { hunkBounds }, l’elenco restituito da hunkBounds(). Viene emesso una volta che la vista è stata impaginata e di nuovo quando una modifica si sposta o cambia dimensione, che l’altezza delle righe sia rimasta invariata o meno, per i segnalini di un overview ruler.

#Correlati

  • Editor: l’editor di codice di cui DiffView usa i colori della sintassi e i linguaggi.
  • TextView: documenti in Markdown e HTML, per il testo attorno a una review.
  • nsh/base: gli altri componenti di base.