Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

DiffView

DiffView показывает изменения между двумя текстами так, как это делают инструменты код-ревью: рядом (split), со старым текстом слева и новым справа, строки выровнены, или единым списком (unified), где удалённые строки идут над добавленными. Внутри изменённой строки подсвечиваются изменившиеся слова, неизменённые строки между изменениями сворачиваются в разделители, которые можно развернуть, а текст подсвечивается цветами синтаксиса Editor. Компонент показывает два текста (oldValue и newValue) или файлы unified diff (patch: вывод git diff, pull request), каждый под своим заголовком. Он предназначен для просмотра изменений, в том числе правок, предложенных ИИ: действия приложения над каждым изменением сохраняют его или откатывают. Создаются только строки в области видимости, поэтому длинный файл прокручивается так же легко, как короткий.

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 отвечает за поведение и имеет собственный внешний вид, построенный на токенах темы: font-mono темы размером 13pt в строках высотой 20pt, номера строк в muted-foreground, добавленные строки подкрашены зелёным, удалённые красным, изменившиеся слова более насыщенным оттенком, у края gutter каждой изменённой строки проходит полоса, а разделители лежат на muted. Каждая часть принимает стили приложения поверх этих значений по умолчанию (classes и styles). В Rust его создаёт nsh_base::diff_view(), в TSX — <DiffView> из 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()
}

Представление заполняет блок, который выделяет ему приложение (flex-1, высота), и прокручивается внутри: по вертикали по строкам, а по горизонтали до самой длинной строки, причём обе стороны split-представления прокручиваются вместе. При softWrap длинные строки вместо этого переносятся по ширине стороны, а строка split-представления имеет высоту более высокой из своих сторон, поэтому стороны остаются выровненными.

#Два текста или патч

  • Два текста: oldValue и newValue. Строки сравниваются как есть, поэтому добавленный или удалённый перевод строки в конце является изменением, как его показывает git.
  • Патч: patch, unified diff одного или нескольких файлов (git diff, git format-patch, diff -u; переименования, режимы, новые, удалённые и бинарные файлы, а также сообщение коммита перед ним читаются так, как их пишет git). Представление показывает строки ханков с нумерацией как в файле, каждый ханк под разделителем с его заголовком (функция, в которой он находится), а каждый файл из нескольких под собственным заголовком (Файлы патча). Путь каждого файла определяет его язык, если language не задан.
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 задаёт язык так же, как Editor: именем, псевдонимом, расширением или именем файла ("rust", "tsx", "main.go", "Dockerfile").

#Файлы патча

Патч из нескольких файлов показывает каждый файл под строкой заголовка: по умолчанию это его путь (old → new при переименовании), числа добавленных и удалённых строк и ▾. Нажатие на заголовок сворачивает файл до заголовка (▸) или разворачивает его. renderFileHeader рисует заголовки так, как нужно приложению (и показывает заголовок также над патчем из одного файла); ему передаётся файл (DiffFile): index, path, oldPath (имя, из которого файл переименован или скопирован), status ("modified", "added", "deleted", "renamed", "copied"), binary (патч не показывает его строк), added, deleted, hunks, firstHunk и collapsed. Заголовок приложения вызывает toggleFile(path) ref; collapseFile и expandFile задают состояние, а каждое изменение сообщается через onFileToggle({ file }). Свёрнутый файл остаётся свёрнутым, пока он есть в новом patch.

Изменения нумеруются по всем файлам подряд: изменения файла — это с firstHunk по firstHunk + hunks - 1, и именно их используют goToHunk, renderHunkActions и hunks() (у DiffHunk поле file — индекс его файла, а его строки считаются внутри этого файла), а stats() суммирует изменения всех файлов. goToHunk сначала разворачивает свёрнутый файл.

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 принимает значение "split" (по умолчанию: рядом, каждая удалённая строка напротив наиболее похожей на неё добавленной) или "unified" (одна колонка, удалённые строки изменения над добавленными, с обоими номерами строк). inline определяет, что подсвечивается внутри изменённой строки:

inlineЧто подсвечивается
"wordAlt" (по умолчанию)Изменившиеся слова; если их разделяет один символ, они объединяются, так что изменённая фраза воспринимается как одно целое
"word"Каждое изменившееся слово
"char"Каждый изменившийся символ
"none"Ничего: только оттенки строк

Слова — это последовательности букв, цифр и _; каждый CJK-символ, каждая кана и каждый слог хангыля являются отдельным словом, поэтому изменённый символ в предложении выделяет только себя. Строка, изменившаяся почти целиком, показывается только с оттенком.

indicators отмечает изменённые строки рядом с их номерами: "bars" (по умолчанию, полоса у края gutter), "classic" (+ и − перед текстом) или "none". background={false} оставляет изменённые строки без оттенка, а lineNumbers={false} скрывает номера.

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

#Свёрнутые строки

Вокруг каждого изменения показываются contextLines неизменённых строк (по умолчанию 3); неизменённые строки между изменениями, а также перед первым и после последнего сворачиваются в разделитель, который сообщает, сколько строк он скрывает. Нажатие на него показывает все эти строки. expandAll показывает все строки; методы ref expand, expandAll() и collapseAll() раскрывают и сворачивают их из приложения.

renderSeparator рисует разделители так, как нужно приложению. Он получает разделитель (DiffSeparator): gap — индекс свёрнутого участка (0 перед первым изменением, i между изменениями i - 1 и i), lines — сколько строк он скрывает, first и last, а для патча ещё и header ханка. expand(gap, direction, lines) показывает "all" его строки (по умолчанию), или lines (по умолчанию 20) "up" — прямо над изменением под разделителем, или "down" — прямо под изменением над ним. Первый разделитель разворачивается вверх, последний вниз.

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"
/>

#Сохранение и откат изменений

renderHunkActions помещает действия приложения в правый верхний угол каждого видимого изменения; ему передаётся изменение (DiffHunk: index, oldStart, oldLines, newStart, newLines, строки нумеруются с 0, и file — индекс его файла в патче). Они вызывают ref: revertHunk(index) возвращает старые строки изменения в новый текст, acceptHunk(index) делает его новые строки частью старого текста. Представление сразу показывает результат и сообщает оба текста через onChange({ change }) ({ oldValue, newValue }), которые приложение задаёт как props. Сохранение или откат правки ИИ по одному изменению выглядит так:

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>
  );
}

Здесь старый текст — это файл как он есть: сохранение изменения применяет его к файлу (onApply), откат убирает его из предложения. Представление, показывающее patch, показывает фрагменты файлов, и его изменения остаются как есть.

#Методы

ref={diff} даёт доступ к методам представления (DiffViewRef). Методы, возвращающие значение (hunks(), stats()), в JS возвращают Promise с ним, поэтому применяйте к ним await: const changes = await diff.hunks(). Для чисел, отображаемых на экране, onStatsChange сообщает их по мере изменения. Метод, которому передано изменение, разделитель или файл, которых нет в diff, ничего не меняет и записывает предупреждение с именем метода и значением.

МетодОписание
hunks()Promise<DiffHunk[]>: изменения по порядку.
stats()Promise<DiffStats>: { added, deleted, hunks }, число добавленных и удалённых строк и число изменений.
goToHunk(index)Прокручивает изменение index в область видимости, его первая строка оказывается вверху.
revertHunk(index)Возвращает старые строки изменения index в новый текст и сообщает тексты (onChange).
acceptHunk(index)Делает новые строки изменения index частью старого текста и сообщает тексты (onChange).
expand(gap, direction?, lines?)Показывает строки, скрытые разделителем gap: "all" (по умолчанию) или lines (по умолчанию 20) "up" или "down".
expandAll()Показывает все неизменённые строки.
collapseAll()Снова сворачивает неизменённые строки.
nextHunk()Promise<number | null>: прокручивает следующее изменение к верху и возвращает его индекс (после последнего — первое).
previousHunk()Promise<number | null>: прокручивает предыдущее изменение к верху и возвращает его индекс (перед первым — последнее).
currentHunk()Promise<number | null>: изменение, к которому перешли последним, пока представление остаётся там, иначе изменение вверху представления или первое из видимых.
selectedLines()Promise<DiffLineRange | null>: строки, выделенные по номерам строк.
setSelectedLines(range)Выделяет строки одной стороны (null: никакие), без onLineSelectionChange.
selectedText()Promise<string>: текст выделенных строк.
files()Promise<DiffFile[]>: файлы патча по порядку с их счётчиками (для двух текстов — пусто).
scrollToFile(path)Прокручивает заголовок файла path к верху.
collapseFile(path)Показывает только заголовок файла path.
expandFile(path)Снова показывает строки файла path.
toggleFile(path)Сворачивает файл path или разворачивает, если он свёрнут.
hunkBounds()Promise<DiffHunkBounds[]>: где находится каждое изменение среди строк, по порядку (Линейка обзора).
scrollOffset()Promise<Point>: насколько прокручены строки (x равно 0).
setScrollOffset(offset)Сразу прокручивает строки до offset.y, насколько они позволяют, как бегунок следует за указателем, который его перетаскивает.

nextHunk() и previousHunk() продолжают с изменения, к которому перешли, пока представление остаётся там, а иначе с верха представления, поэтому изменение ближе к концу, которое нельзя прокрутить к верху, всё равно учитывается. Кнопки следующего и предыдущего изменения со счётчиками из 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" />

#Линейка обзора

onScroll({ offset, contentSize, size }) сообщает, где находятся строки, с ключами как у Scroll: offset.y — насколько они прокручены (строки прокручиваются по вертикали и имеют ширину представления, а стороны прокручиваются по горизонтали сами по себе: offset.x равно 0), contentSize — суммарный размер всех строк и size — размер области просмотра. Событие срабатывает, когда представление скомпоновано, и затем при каждом изменении любого из них: при прокрутке строк, при изменении размера представления и при изменении высоты строк (другие тексты, layout, softWrap, развёрнутый или свёрнутый файл или разделитель, скомпонованная аннотация). hunkBounds() возвращает, где находится каждое изменение среди строк (DiffHunkBounds: { index, bounds }, bounds от верха его первой строки до низа последней, с аннотациями под ними, по всей ширине представления), для изменений, которые видны: у изменений свёрнутого файла строк нет. onHunkBoundsChange({ hunkBounds }) выдаёт тот же список, когда представление скомпоновано, и затем каждый раз, когда изменение сдвигается или меняет размер, даже если высота строк остаётся прежней (одна изменённая строка превращается в другую): линейка рисует свои метки по нему, а не считывает их при onScroll. scrollOffset() считывает, насколько прокручены строки, а setScrollOffset({ x, y }) сразу прокручивает строки до y, так что перетаскиваемый бегунок следует за указателем в том же кадре.

Вместе они образуют линейку обзора рядом с представлением, как в редакторах кода: метка для каждого изменения, бегунок над видимыми строками, а нажатие или перетаскивание прокручивает к нужному месту. Здесь линейка той же высоты, что и представление:

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()),
                ),
        )
}

Значение index метки совпадает со значением изменения в hunks(), где oldLines и newLines отличают добавленное изменение (нет старых строк) от удалённого (нет новых строк), чтобы раскрасить метку.

#Комментарии при ревью

Номера строк выделяют строки одной стороны: нажатие выделяет одну строку, нажатие с Shift расширяет выделение до неё, а перетаскивание мыши по номерам выделяет те, над которыми она проходит. onLineSelectionChange сообщает строки как range (DiffLineRange: { side, start, end }, строки нумеруются с 0, null — если ничего не выбрано), а нажатие на номер также сообщает onLineNumberPress({ line }) ({ side, line }). ref считывает выделение (selectedLines(), selectedText()) и задаёт его.

annotations помещает содержимое приложения под строками: каждый { id, side, line } рисуется с помощью renderAnnotation на всю сторону своей строки (на всё представление в unified), той высоты, какую получит при компоновке, и виден, пока видна его строка. renderAddAnnotation — элемент управления приложения для добавления такого комментария, справа от gutter строки, над которой находится указатель, и последней выделенной строки; он получает выделенные строки, если его строка — одна из них, иначе эту строку.

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"
/>

#Стилизация частей

class и style задают стиль контейнера представления. classes принимает классы Tailwind по частям, а styles — инлайн-стили по частям; классы части применяются после её собственных стилей по умолчанию, поэтому имеют над ними приоритет, а её styles имеют приоритет над её классами. Шрифт задаётся через fontFamily, fontSize и 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",
  }}
/>
ЧастьЧто этоПо умолчанию
sideКаждая сторона (split) или единственная колонка (unified)Линия border между сторонами
gutterНомера строк и индикаторы стороны
lineNumberКаждый номер строкиПо правому краю, muted-foreground
lineNumberAddedНомера добавленных строк, после lineNumber
lineNumberDeletedНомера удалённых строк, после lineNumber
contentТекст стороны рядом с полем номеров8pt до и после текста
lineКаждый ряд стороны
lineAddedРяды добавленных строк, после lineЗелёный оттенок
lineDeletedРяды удалённых строк, после lineКрасный оттенок
fillerПустая половина split-ряда рядом со строкой, которой нет на другой сторонеЕдва заметная заливка muted
wordAddedСлова, изменённые внутри добавленной строкиБолее насыщенный зелёный
wordDeletedСлова, изменённые внутри удалённой строкиБолее насыщенный красный
indicatorAddedПолоса добавленной строки ("bars") или + ("classic")Зелёная, шириной 3pt
indicatorDeletedПолоса удалённой строки или −Красная, шириной 3pt
separatorРазделитель свёрнутых строк или заголовок hunk в патче⋯ и количество на muted
hunkActionsКонтейнер действий приложения над изменениемВ правом верхнем углу изменения
lineSelectedПоверх рядов выделенных строк своей стороныЦвет выделения темы (как у выделений Editor)
annotationКонтейнер аннотации под её строкойНа всю ширину стороны её строки
addAnnotationКонтейнер элемента добавления: шириной с поле номеров и высотой в строкуЭлемент справа от него
fileHeaderЗаголовок файла патчаПуть и счётчики на secondary, линия border снизу

#Свойства

СвойствоRustТипПо умолчаниюОписание
oldValue.old_value(..)stringСтарый текст.
newValue.new_value(..)stringНовый текст.
patch.patch(..)stringUnified diff одного или нескольких файлов; показывается, когда oldValue и newValue не заданы.
language.language(..)stringпростой текст (для патча — язык каждого файла)Название языка, его псевдоним, расширение или имя файла.
layout.layout(..)"split" | "unified""split"Две стороны рядом или одна колонка.
inline.inline(..)"wordAlt" | "word" | "char" | "none""wordAlt"Что подсвечивается внутри изменённых строк.
indicators.indicators(..)"bars" | "classic" | "none""bars"Как изменённые строки отмечаются рядом с их номерами.
contextLines.context_lines(..)number3Неизменённые строки, показываемые вокруг каждого изменения.
expandAll.expand_all(..)booleanfalseПоказываются все неизменённые строки.
lineNumbers.line_numbers(..)booleantrueНомера строк.
background.background(..)booleantrueИзменённые строки подкрашиваются.
fontFamily.font_family(..)stringfont-mono темыШрифт текста.
fontSize.font_size(..)number13Размер шрифта в пунктах.
lineHeight.line_height(..)number20Высота ряда в пунктах.
renderSeparator.render_separator(..)(separator: DiffSeparator) => JSX.ElementОтрисовывает разделители свёрнутых строк.
renderHunkActions.render_hunk_actions(..)(hunk: DiffHunk) => JSX.ElementДействия приложения для каждого изменения.
softWrap.soft_wrap(..)booleanfalseДлинные строки переносятся по ширине стороны.
annotations.annotations(..)DiffAnnotation[][]Содержимое приложения под строками: { id, side, line }.
renderAnnotation.render_annotation(..)(annotation: DiffAnnotation) => JSX.ElementОтрисовывает аннотацию под её строкой.
renderAddAnnotation.render_add_annotation(..)(range: DiffLineRange) => JSX.ElementЭлемент приложения для добавления аннотации рядом со строкой под курсором и последней выделенной строкой.
renderFileHeader.render_file_header(..)(file: DiffFile) => JSX.ElementОтрисовывает заголовок каждого файла патча высотой в один ряд.
classes.classes(..)DiffViewClasses{}Классы Tailwind по частям.
styles.styles(..)DiffViewStyles{}Инлайн-стили по частям, поверх классов.
class, styleметоды стилейstring, StyleProp<ViewStyle>Контейнер представления.

#События

СобытиеRustОписание
onChange.on_change(|change: &DiffChange| ..)Изменение сохранено или отменено: { change }, текущие тексты ({ oldValue, newValue }).
onLineNumberPress.on_line_number_press(|line: &DiffLine| ..)Нажат номер строки: { line }, значение { side, line }.
onLineSelectionChange.on_line_selection_change(|range: &Option<DiffLineRange>| ..)Выделенные строки изменились: { range }, значение { side, start, end } или null.
onStatsChange.on_stats_change(|stats: &DiffStats| ..)Счётчики изменились: { stats }, значение { added, deleted, hunks }. Срабатывает один раз при создании представления и снова каждый раз, когда сравнение текстов даёт другие счётчики, — например, для строки «+8 −8 · 5 изменений».
onFileToggle.on_file_toggle(|file: &DiffFile| ..)Файл патча свёрнут или развёрнут (через его заголовок или ref): { file }, текущее значение DiffFile.
onScroll.on_scroll(|e: &DiffViewEvent| ..)Ряды прокручены, либо изменился размер рядов или области просмотра: { offset, contentSize, size }, как у Scroll (DiffViewEvent::Scroll(offset, content_size, size) в Rust). Срабатывает после компоновки представления и снова при изменении любого из них.
onHunkBoundsChange.on_hunk_bounds_change(|bounds: &Vec<DiffHunkBounds>| ..)Изменилось положение изменений среди рядов: { hunkBounds }, список, который возвращает hunkBounds(). Срабатывает после компоновки представления и снова, когда изменение сдвигается или меняет размер, независимо от того, сохранилась ли высота рядов, — например, для меток обзорной линейки.

#См. также

  • Editor: редактор кода, чьи цвета синтаксиса и языки использует DiffView.
  • TextView: документы Markdown и HTML, для текста вокруг ревью.
  • nsh/base: остальные базовые компоненты.