本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

DiffView

DiffView 以程式碼審查工具的方式顯示兩段文字之間的差異:並排顯示時,舊文字在左、新文字在右,各行對齊;統一顯示時,刪除的行在上、新增的行在下。已更改的行內,更改了的字詞會被突顯;位於更改之間的未更改行會摺疊成可展開的分隔列;文字會以 Editor 的語法顏色突顯。它可顯示兩段文字(oldValue 和 newValue),或統一 diff 的各個檔案(patch:git diff 的、pull request 的),每個檔案各有標題。它專為審查更改而設,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 負責行為,並附帶以主題 token 建構的預設外觀:主題的 font-mono(13pt,列高 20pt)、以 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 時,長行會按該側的寬度換行,並排的一列高度與較高的一側相同,因此兩側保持對齊。

#兩段文字或修補檔

  • 兩段文字:oldValue 和 newValue。各行按原樣比較,因此新增或移除最後的換行符號即屬更改,與 git 的顯示一致。
  • 修補檔:patch,一個或多個檔案的 unified diff(git diff、git format-patch、diff -u;重新命名、模式、新增、刪除及二進位檔案,以及其前的提交訊息,均按 git 的寫法讀取)。檢視會顯示其 hunk 的行,行號與檔案中相同,每個 hunk 之上有以其標頭(所在函式)組成的分隔列,多個檔案時每個檔案各有自己的標題(修補檔的檔案)。未設定 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。應用程式的標題會呼叫 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 為 "split"(預設:並排,每條刪除行旁邊是與它最相似的新增行)或 "unified"(單欄,更改的刪除行在其新增行之上,並顯示兩個行號)。inline 決定已更改行內突顯的內容:

inline 突顯內容
"wordAlt"(預設) 已更改的字詞;相隔一個字元時會合併,使更改的短語視為一個
"word" 每個已更改的字詞
"char" 每個已更改的字元
"none" 無:僅顯示行的底色

字詞是由字母、數字和 _ 組成的連續字元;每個 CJK 字元、假名和諺文音節各自成為一個字詞,因此句子中更改了一個字元,只會標示該字元本身。幾乎整行都已更改的行只顯示其底色。

indicators 在行號旁標記已更改的行:"bars"(預設,行號欄邊緣的色條)、"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 - 1 與 i 之間為 i)、lines(所隱藏的行數)、first 和 last,修補檔還有 hunk 的 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。逐項保留或還原 AI 的編輯如下:

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 橫跨其行所在的一側繪製(統一檢視時為整個檢視),高度按其版面配置而定,並於其所屬行顯示時顯示。renderAddAnnotation 是應用程式用來新增評論的控制項,位於指標所在行及最後選取行的行號欄右側;當其行屬於所選行之一時,它會獲得所選的行,否則獲得該行。

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 classes,styles 則按部分接受內聯樣式;某個部分的 classes 排在其自身預設樣式之後,因此會優先生效,而其 styles 又會優先於其 classes。字型由 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 已收合行的分隔線,或 patch 的 hunk 標題 ⋯,以及位於 muted 上的數量
hunkActions 應用程式對某項變更所提供操作的方框 位於變更的右上角
lineSelected 覆蓋於所選行在其所屬一側的列上 主題的選取顏色(與 Editor 的選取範圍相同)
annotation 註解的方框,位於其所屬行之下 橫跨其所屬行所在的一側
addAnnotation 新增控制項的方框,寬度與行號欄相同,高度為一行 控制項位於其右側
fileHeader patch 中某個檔案的標題列 路徑和數量顯示於 secondary,下方有一條 border 線

#Props

Prop Rust 類型 預設值 說明
oldValue .old_value(..) string 舊文字。
newValue .new_value(..) string 新文字。
patch .patch(..) string 一個或多個檔案的 unified diff;在 oldValue 和 newValue 未設定時顯示。
language .language(..) string 純文字(patch:各檔案自身的語言) 語言的名稱、別名、副檔名或檔案名稱。
layout .layout(..) "split" | "unified" "split" 並排顯示或單欄顯示。
inline .inline(..) "wordAlt" | "word" | "char" | "none" "wordAlt" 在已更改的行內要突出顯示的內容。
indicators .indicators(..) "bars" | "classic" | "none" "bars" 已更改的行在行號旁如何標記。
contextLines .context_lines(..) number 3 每項變更周圍顯示的未更改行。
expandAll .expand_all(..) boolean false 顯示所有未更改的行。
lineNumbers .line_numbers(..) boolean true 行號。
background .background(..) boolean true 為已更改的行加上底色。
fontFamily .font_family(..) string 主題的 font-mono 文字的字型。
fontSize .font_size(..) number 13 字型大小,單位為 point。
lineHeight .line_height(..) number 20 每一列的高度,單位為 point。
renderSeparator .render_separator(..) (separator: DiffSeparator) => JSX.Element 繪製已收合行的分隔線。
renderHunkActions .render_hunk_actions(..) (hunk: DiffHunk) => JSX.Element 應用程式對每項變更所提供的操作。
softWrap .soft_wrap(..) boolean false 長行會按該側的寬度換行。
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 繪製 patch 中每個檔案的標題列,高度為一列。
classes .classes(..) DiffViewClasses {} 按部分設定的 Tailwind classes。
styles .styles(..) DiffViewStyles {} 按部分設定的內聯樣式,優先於 classes。
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 changes」這類文字行。
onFileToggle .on_file_toggle(|file: &DiffFile| ..) patch 中的某個檔案已被收合或展開(透過其標題列或 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:其他基礎元件。