このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

DiffView

DiffView は、コードレビューツールのように2つのテキストの差分を表示します。表示形式は、古いテキストを左、新しいテキストを右に行を揃えて並べる左右分割(side by side)か、削除行を追加行の上に表示する unified のどちらかです。変更された行では変更された単語がハイライトされ、変更と変更の間の未変更行は展開できるセパレーターに折りたたまれ、テキストは Editor のシンタックスカラーでハイライトされます。2つのテキスト(oldValue と newValue)、または unified diff のファイル群(patch: git diff のもの、プルリクエストのもの)を、それぞれのヘッダーの下に表示します。変更のレビュー、特に AI が提案した編集のレビューを目的としており、各変更に対するアプリのアクションで、その変更を残すか元に戻すかを選べます。表示範囲内の行だけが生成されるため、長いファイルも短いファイルと同じようにスクロールできます。

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 を 20pt の行に 13pt で表示し、行番号は muted-foreground、追加行は緑、削除行は赤で色付けし、変更された単語はより濃い色で示し、変更された各行のガターの端にバーを表示し、セパレーターは muted に表示します。各パーツは、これらのデフォルトの上にアプリのスタイルを適用します(classes と styles)。Rust では nsh_base::diff_view() で、TSX では nsh/base の <DiffView> で作成します。

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、高さ)を埋め、その内部でスクロールします。縦方向には行をスクロールし、横方向には最も長い行までスクロールします。左右分割ビューでは両側が一緒にスクロールします。softWrap を指定すると、長い行は折り返されずに各側の幅で折り返され、左右分割の行は高い方の側の高さになるため、両側の位置は揃ったままです。

#2つのテキストまたはパッチ

  • 2つのテキスト: oldValue と newValue。行はそのまま比較されるため、末尾の改行の追加や削除も、git と同じく変更として扱われます。
  • パッチ: patch。1つまたは複数のファイルの unified diff です(git diff、git format-patch、diff -u。リネーム、モード、新規ファイル、削除されたファイル、バイナリファイル、先頭のコミットメッセージは、git が出力する形式で読み取られます)。ビューにはハンクの行がファイル内の行番号付きで表示され、各ハンクはそのヘッダー(含まれる関数)のセパレーターの下に置かれ、複数ファイルの場合は各ファイルが専用のヘッダーの下に置かれます(Files of a patch)。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")。

#Files of a patch

複数ファイルのパッチでは、各ファイルがヘッダー行の下に表示されます。デフォルトでは、パス(リネームされた場合は old → new)、追加行数と削除行数、▾ が表示され、押すとそのファイルはヘッダーだけに折りたたまれ(▸)、もう一度押すと展開されます。renderFileHeader はヘッダーをアプリ独自の方法で描画します(1ファイルのパッチでも上に1つ表示されます)。ファイルの情報(DiffFile)として、index、path、oldPath(リネーム元またはコピー元)、status("modified"、"added"、"deleted"、"renamed"、"copied")、binary(パッチにそのファイルの行が含まれない場合)、added、deleted、hunks、firstHunk、collapsed が渡されます。アプリのヘッダーは ref の toggleFile(path) を呼び出します。状態は 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 and inline changes

layout は "split"(デフォルト。左右分割で、各削除行は最も似ている追加行の隣に表示されます)または "unified"(1列。変更の削除行が追加行の上に並び、両方の行番号が表示されます)です。inline は、変更された行の中で何をハイライトするかを決めます。

inlineハイライトされるもの
"wordAlt"(デフォルト)変更された単語。1文字だけ離れている場合は結合されるため、変更されたフレーズが1つにまとまって見えます
"word"変更された各単語
"char"変更された各文字
"none"なし。行の色付けのみ

単語とは、文字、数字、_ の連続です。CJK の各文字、かな、ハングルの音節はそれぞれ1つの単語として扱われるため、文中の1文字が変更されても、その文字だけがマークされます。ほぼ全体が変更された行では、行の色付けのみが表示されます。

indicators は、変更された行を行番号の横にマークします。"bars"(デフォルト。ガターの端のバー)、"classic"(テキストの前に + と −)、または "none" を指定できます。background={false} を指定すると変更された行は色付けされず、lineNumbers={false} を指定すると行番号が非表示になります。

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

#Collapsed lines

各変更の前後には contextLines 行の未変更行が表示されます(デフォルトは3)。変更と変更の間、および最初の変更の前と最後の変更の後の未変更行は、隠れている行数を示すセパレーターに折りたたまれます。セパレーターを押すと、すべての行が表示されます。expandAll はすべての行を表示します。ref の expand、expandAll()、collapseAll() を使うと、アプリから行の展開と折りたたみができます。

renderSeparator はセパレーターをアプリ独自の方法で描画します。セパレーター(DiffSeparator)として、gap、折りたたまれた範囲のインデックス(最初の変更の前は 0、変更 i - 1 と i の間は 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"
/>

#Keeping and reverting changes

renderHunkActions は、表示範囲内の各変更の右上にアプリのアクションを配置し、変更の情報(DiffHunk: index、oldStart、oldLines、newStart、newLines(0 から始まる行)、パッチの場合はそのファイルのインデックスである file)を渡します。アクションは ref を呼び出します。revertHunk(index) は変更の古い行を新しいテキストに戻し、acceptHunk(index) は変更の新しい行を古いテキストにします。ビューは結果をすぐに表示し、両方のテキストを onChange({ change })({ oldValue, newValue })で通知します。アプリはこれを props として設定します。AI の編集を1つずつ残すか元に戻すかは、次のように行います。

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 を表示しているビューはファイルの抜粋を表示するため、その変更はそのまま残ります。

#Methods

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[]>: パッチのファイルを順番に、それぞれの件数とともに返します(2つのテキストの場合はなし)。
scrollToFile(path)path のファイルのヘッダーを先頭にスクロールします。
collapseFile(path)path のファイルのヘッダーだけを表示します。
expandFile(path)path のファイルの行を再び表示します。
toggleFile(path)path のファイルを折りたたみ、折りたたまれている場合は展開します。
hunkBounds()Promise<DiffHunkBounds[]>: 各変更の行内での位置を順番に返します(An overview ruler)。
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" />

#An overview ruler

onScroll({ offset, contentSize, size }) は、Scroll のキーと同じキーで、行の位置を通知します。offset.y は行がどこまでスクロールされているかです(行は縦にスクロールし、幅はビューと同じで、左右の側は独立して横にスクロールします。offset.x は 0 です)。contentSize はすべての行を合わせたサイズ、size はビューポートのサイズです。このコールバックは、ビューのレイアウトが完了すると一度呼ばれ、その後はこれらのいずれかが変わるたびに呼ばれます。行のスクロール時、ビューのリサイズ時、行の高さが変わったとき(別のテキスト、layout、softWrap、ファイルやセパレーターの展開や折りたたみ、アノテーションのレイアウト)です。hunkBounds() は、各変更が行のどこにあるかを返します(DiffHunkBounds: { index, bounds }、bounds は最初の行の上端から最後の行の下端まで、その下のアノテーションを含み、ビューの全幅にわたります)。対象は表示されている変更で、折りたたまれたファイルの変更には行がありません。onHunkBoundsChange({ hunkBounds }) は、ビューのレイアウト完了後に同じリストを一度通知し、その後は変更が移動したりサイズが変わったりするたびに(行の高さが変わらない場合、たとえば変更された1行が別の行に置き換わった場合も)通知します。ルーラーは、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 により、追加された変更(古い行なし)と削除された変更(新しい行なし)を区別でき、マークの色分けに使えます。

#Review comments

行番号は片側の行を選択します。押すと1行が選択され、Shift を押しながら押すとその行まで選択が拡張され、行番号の上をマウスでドラッグすると通過した行が選択されます。onLineSelectionChange は選択された行を range として通知し(DiffLineRange: { side, start, end }、行は 0 から、選択なしは null)、行番号を押すと onLineNumberPress({ line }) も通知されます({ side, line })。ref は選択を読み取り(selectedLines()、selectedText())、設定できます。

annotations は、アプリのコンテンツを行の下に配置します。各 { id, side, line } は renderAnnotation によってその行の側全体(unified の場合はビュー全体)にわたって描画され、レイアウトされた高さになり、その行が表示されている間は表示されます。renderAddAnnotation は、コメントを追加するためのアプリのコントロールで、ポインターが乗っている行と、最後に選択された行のガターの右側に表示されます。その行が選択された行の1つである場合は選択された行が、そうでなければその行が渡されます。

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反対側にない行の隣にある、分割行の空の半分muted のかすかな塗り
wordAdded追加された行の中で変更された単語より濃い緑
wordDeleted削除された行の中で変更された単語より濃い赤
indicatorAdded追加された行のバー("bars")または +("classic")緑、幅 3pt
indicatorDeleted削除された行のバーまたは −赤、幅 3pt
separator折りたたまれた行のセパレーター、またはパッチのハンクヘッダー⋯ と、muted 上のカウント
hunkActions変更に対するアプリのアクションのボックス変更の右上
lineSelected選択された行の、そのサイドの行の上テーマの選択色(Editor の選択と同じ)
annotationアノテーションのボックス。その行の下その行のサイド全体
addAnnotation追加コントロールのボックス。ガターの幅で、1 行分の高さ右側のコントロール
fileHeaderパッチ内のファイルのヘッダーsecondary 上のパスとカウント、その下に border の線

#Props

PropRust型デフォルト説明
oldValue.old_value(..)string古いテキスト。
newValue.new_value(..)string新しいテキスト。
patch.patch(..)string1 つまたは複数ファイルの unified diff。oldValue と newValue が設定されていない場合に表示されます。
language.language(..)stringプレーンテキスト(パッチの場合: 各ファイルのもの)言語の名前、エイリアス、拡張子、またはファイル名。
layout.layout(..)"split" | "unified""split"左右に並べて表示するか、1 カラムで表示するか。
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(..)stringテーマの font-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パッチ内の各ファイルのヘッダーを、1 行の高さで描画します。
classes.classes(..)DiffViewClasses{}パーツごとの Tailwind クラス。
styles.styles(..)DiffViewStyles{}クラスより優先される、パーツごとのインラインスタイル。
class、styleスタイルメソッドstring、StyleProp<ViewStyle>ビューのボックス。

#Events

EventRust説明
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 }。ビューの作成時に一度発火し、その後テキストが diff されて別のカウントになるたびに再度発火します。“+8 −8 · 5 changes” のような行の表示に使えます。
onFileToggle.on_file_toggle(|file: &DiffFile| ..)パッチのファイルが折りたたまれたか展開されました(そのヘッダー、または ref 経由): { file }、現在の DiffFile。
onScroll.on_scroll(|e: &DiffViewEvent| ..)行がスクロールされたか、行またはビューポートのサイズが変更されました: { offset, contentSize, size }、Scroll のもの(Rust では DiffViewEvent::Scroll(offset, content_size, size))。ビューがレイアウトされたときに一度発火し、これらのいずれかが変わるたびに再度発火します。
onHunkBoundsChange.on_hunk_bounds_change(|bounds: &Vec<DiffHunkBounds>| ..)行の中での変更の位置が変わりました: { hunkBounds }、hunkBounds() が返すリスト。ビューがレイアウトされたときに一度発火し、変更が移動したりサイズが変わったりするたびに(行の高さが保たれているかどうかにかかわらず)再度発火します。オーバービュールーラーのマークに使えます。

#関連項目

  • Editor: DiffView が使用するシンタックスカラーと言語を提供するコードエディタ。
  • TextView: Markdown や HTML のドキュメント。レビューの周囲に置くテキスト向け。
  • nsh/base: その他の基本コンポーネント。