mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-23 01:22:12 +03:00
use default differ component to render commit diff
This commit is contained in:
parent
c6ec0d9b77
commit
7efdf0a5e7
@ -344,7 +344,7 @@ impl App {
|
||||
pub fn git_wd_diff(
|
||||
&self,
|
||||
project_id: &str,
|
||||
max_lines: usize,
|
||||
context_lines: usize,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
let project = self
|
||||
.projects_storage
|
||||
@ -353,7 +353,7 @@ impl App {
|
||||
.ok_or_else(|| anyhow::anyhow!("project wd not found"))?;
|
||||
let project_repository = project_repository::Repository::open(&project)
|
||||
.context("failed to open project repository")?;
|
||||
project_repository.git_wd_diff(max_lines)
|
||||
project_repository.git_wd_diff(context_lines)
|
||||
}
|
||||
|
||||
pub fn git_match_paths(&self, project_id: &str, pattern: &str) -> Result<Vec<String>> {
|
||||
|
@ -157,7 +157,7 @@ impl<'repository> Repository<'repository> {
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
pub fn git_wd_diff(&self, max_lines: usize) -> Result<HashMap<String, String>> {
|
||||
pub fn git_wd_diff(&self, context_lines: usize) -> Result<HashMap<String, String>> {
|
||||
let head = self.git_repository.head()?;
|
||||
let tree = head.peel_to_tree()?;
|
||||
|
||||
@ -166,6 +166,11 @@ impl<'repository> Repository<'repository> {
|
||||
opts.recurse_untracked_dirs(true)
|
||||
.include_untracked(true)
|
||||
.show_untracked_content(true)
|
||||
.context_lines(if context_lines == 0 {
|
||||
3
|
||||
} else {
|
||||
context_lines.try_into()?
|
||||
})
|
||||
.include_ignored(true);
|
||||
|
||||
let diff = self
|
||||
@ -192,14 +197,12 @@ impl<'repository> Repository<'repository> {
|
||||
current_line_count = 0;
|
||||
last_path = new_path.clone();
|
||||
}
|
||||
if current_line_count <= max_lines {
|
||||
match line.origin() {
|
||||
'+' | '-' | ' ' => results.push_str(&format!("{}", line.origin())),
|
||||
_ => {}
|
||||
}
|
||||
results.push_str(&format!("{}", std::str::from_utf8(line.content()).unwrap()));
|
||||
current_line_count += 1;
|
||||
match line.origin() {
|
||||
'+' | '-' | ' ' => results.push_str(&format!("{}", line.origin())),
|
||||
_ => {}
|
||||
}
|
||||
results.push_str(&format!("{}", std::str::from_utf8(line.content()).unwrap()));
|
||||
current_line_count += 1;
|
||||
true
|
||||
})?;
|
||||
result.insert(last_path.clone(), results.clone());
|
||||
|
@ -334,10 +334,11 @@ async fn git_status(
|
||||
async fn git_wd_diff(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
context_lines: usize,
|
||||
) -> Result<HashMap<String, String>, Error> {
|
||||
let app = handle.state::<app::App>();
|
||||
let diff = app
|
||||
.git_wd_diff(project_id, 100)
|
||||
.git_wd_diff(project_id, context_lines)
|
||||
.with_context(|| format!("failed to get git wd diff for project {}", project_id))?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ import { invoke } from '$lib/ipc';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sessions, git } from '$lib/api';
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
const list = (params: { projectId: string; contextLines?: number }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', {
|
||||
projectId: params.projectId,
|
||||
contextLines: params.contextLines ?? 10000
|
||||
});
|
||||
|
||||
export const Diffs = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { CharacterIdMap } from './characterIdMap';
|
||||
import { diff_match_patch } from 'diff-match-patch';
|
||||
|
||||
export const charDiff = (
|
||||
text1: string,
|
||||
text2: string,
|
||||
cleanup?: boolean
|
||||
): { 0: number; 1: string }[] => {
|
||||
const differ = new diff_match_patch();
|
||||
const diff = differ.diff_main(text1, text2);
|
||||
if (cleanup) {
|
||||
differ.diff_cleanupSemantic(diff);
|
||||
}
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const lineDiff = (lines1: string[], lines2: string[]): DiffArray => {
|
||||
const idMap = new CharacterIdMap<string>();
|
||||
const text1 = lines1.map((line) => idMap.toChar(line)).join('');
|
||||
const text2 = lines2.map((line) => idMap.toChar(line)).join('');
|
||||
|
||||
const diff = charDiff(text1, text2);
|
||||
const lineDiff = [];
|
||||
for (let i = 0; i < diff.length; i++) {
|
||||
const lines = [];
|
||||
for (let j = 0; j < diff[i][1].length; j++) {
|
||||
lines.push(idMap.fromChar(diff[i][1][j]) || '');
|
||||
}
|
||||
|
||||
lineDiff.push({ 0: diff[i][0], 1: lines });
|
||||
}
|
||||
return lineDiff;
|
||||
};
|
||||
|
||||
export enum Operation {
|
||||
Equal = 0,
|
||||
Insert = 1,
|
||||
Delete = -1,
|
||||
Edit = 2
|
||||
}
|
||||
|
||||
export type DiffArray = { 0: Operation; 1: string[] }[];
|
@ -1,3 +0,0 @@
|
||||
import { default as CodeViewer } from './CodeViewer.svelte';
|
||||
|
||||
export default CodeViewer;
|
35
src/lib/components/DeltasViewer.svelte
Normal file
35
src/lib/components/DeltasViewer.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { type Delta, Operation } from '$lib/api';
|
||||
import { Differ } from '$lib/components';
|
||||
import { lineDiff } from './Differ/diff';
|
||||
|
||||
export let doc: string;
|
||||
export let deltas: Delta[];
|
||||
export let filepath: string;
|
||||
export let highlight: string[] = [];
|
||||
export let paddingLines = 10000;
|
||||
|
||||
const applyDeltas = (text: string, deltas: Delta[]) => {
|
||||
const operations = deltas.flatMap((delta) => delta.operations);
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (Operation.isInsert(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.insert[0]) +
|
||||
operation.insert[1] +
|
||||
text.slice(operation.insert[0]);
|
||||
} else if (Operation.isDelete(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.delete[0]) +
|
||||
text.slice(operation.delete[0] + operation.delete[1]);
|
||||
}
|
||||
});
|
||||
return text;
|
||||
};
|
||||
|
||||
$: left = deltas.length > 0 ? applyDeltas(doc, deltas.slice(0, deltas.length - 1)) : doc;
|
||||
$: right = deltas.length > 0 ? applyDeltas(left, deltas.slice(deltas.length - 1)) : left;
|
||||
$: diff = lineDiff(left.split('\n'), right.split('\n'));
|
||||
</script>
|
||||
|
||||
<Differ {diff} {filepath} {highlight} {paddingLines} />
|
@ -1,56 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { create } from '$lib/components/CodeViewer/CodeHighlighter';
|
||||
import { parseDiff } from './Differ/diff';
|
||||
import { Differ } from '$lib/components';
|
||||
|
||||
export let diff: string;
|
||||
export let path: string;
|
||||
export let paddingLines = 3;
|
||||
|
||||
const sanitize = (text: string) => {
|
||||
var element = document.createElement('div');
|
||||
element.innerText = text;
|
||||
return element.innerHTML;
|
||||
};
|
||||
|
||||
let currentDiff = '';
|
||||
let middleDiff = '';
|
||||
let currentOffset = 0;
|
||||
|
||||
let htmlTagRegex = /(<([^>]+)>)/gi;
|
||||
|
||||
$: if (diff) {
|
||||
middleDiff = '';
|
||||
currentDiff = '';
|
||||
currentOffset = 0;
|
||||
let lineClass = '';
|
||||
|
||||
let doc = create(diff, path);
|
||||
doc.highlightRange(0, diff.length, (text, style) => {
|
||||
middleDiff += style ? `<span class=${style}>${sanitize(text)}</span>` : sanitize(text);
|
||||
});
|
||||
|
||||
let diffLines = middleDiff.split('<br>');
|
||||
diffLines.forEach((line, index) => {
|
||||
lineClass = 'lineContext ';
|
||||
let firstChar = line.replace(htmlTagRegex, '').slice(0, 1);
|
||||
if (index < 4) {
|
||||
lineClass = 'lineDiff bg-zinc-800 text-zinc-500';
|
||||
} else if (line.slice(0, 2) == '@@') {
|
||||
lineClass =
|
||||
'lineSplit bg-zinc-900 bg-opacity-60 pt-1 pb-1 border-t border-b border-zinc-700 mt-1 mb-1';
|
||||
} else if (firstChar == '+') {
|
||||
if (!line.includes('+++')) {
|
||||
lineClass = 'lineSplit bg-green-500 bg-opacity-20';
|
||||
}
|
||||
} else if (firstChar == '-') {
|
||||
if (!line.includes('---')) {
|
||||
lineClass = 'lineSplit bg-red-600 bg-opacity-20';
|
||||
}
|
||||
}
|
||||
currentDiff += `<div class="${lineClass}">`;
|
||||
currentDiff += line;
|
||||
currentDiff += '</div>';
|
||||
currentOffset += line.length;
|
||||
});
|
||||
}
|
||||
$: parsedDiff = parseDiff(diff);
|
||||
</script>
|
||||
|
||||
<pre class="h-full w-full">{@html currentDiff}</pre>
|
||||
<Differ diff={parsedDiff} filepath={path} {paddingLines} />
|
||||
|
@ -1,35 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type Delta, Operation } from '$lib/api';
|
||||
import { lineDiff } from './diff';
|
||||
import { create } from './CodeHighlighter';
|
||||
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
|
||||
|
||||
import './diff.css';
|
||||
import './colors/gruvbox.css';
|
||||
import type { DiffArray } from './diff';
|
||||
|
||||
export let doc: string;
|
||||
export let deltas: Delta[];
|
||||
export let filepath: string;
|
||||
export let highlight: string[] = [];
|
||||
export let paddingLines = 10000;
|
||||
|
||||
const applyDeltas = (text: string, deltas: Delta[]) => {
|
||||
const operations = deltas.flatMap((delta) => delta.operations);
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (Operation.isInsert(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.insert[0]) +
|
||||
operation.insert[1] +
|
||||
text.slice(operation.insert[0]);
|
||||
} else if (Operation.isDelete(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.delete[0]) +
|
||||
text.slice(operation.delete[0] + operation.delete[1]);
|
||||
}
|
||||
});
|
||||
return text;
|
||||
};
|
||||
export let diff: DiffArray;
|
||||
|
||||
const sanitize = (text: string) => {
|
||||
var element = document.createElement('div');
|
||||
@ -37,10 +17,6 @@
|
||||
return element.innerHTML;
|
||||
};
|
||||
|
||||
$: left = deltas.length > 0 ? applyDeltas(doc, deltas.slice(0, deltas.length - 1)) : doc;
|
||||
$: right = deltas.length > 0 ? applyDeltas(left, deltas.slice(deltas.length - 1)) : left;
|
||||
$: diff = lineDiff(left.split('\n'), right.split('\n'));
|
||||
|
||||
$: diffRows = buildDiffRows(diff, { paddingLines });
|
||||
$: originalHighlighter = create(diffRows.originalLines.join('\n'), filepath);
|
||||
$: originalMap = documentMap(diffRows.originalLines);
|
||||
@ -179,7 +155,7 @@
|
||||
changedLines[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
};
|
||||
$: deltas.length && scrollToChangedLine();
|
||||
$: diff && scrollToChangedLine();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full select-text whitespace-pre font-mono">
|
103
src/lib/components/Differ/diff.ts
Normal file
103
src/lib/components/Differ/diff.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { CharacterIdMap } from './characterIdMap';
|
||||
import { diff_match_patch } from 'diff-match-patch';
|
||||
|
||||
export const charDiff = (
|
||||
text1: string,
|
||||
text2: string,
|
||||
cleanup?: boolean
|
||||
): { 0: number; 1: string }[] => {
|
||||
const differ = new diff_match_patch();
|
||||
const diff = differ.diff_main(text1, text2);
|
||||
if (cleanup) {
|
||||
differ.diff_cleanupSemantic(diff);
|
||||
}
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const lineDiff = (lines1: string[], lines2: string[]): DiffArray => {
|
||||
const idMap = new CharacterIdMap<string>();
|
||||
const text1 = lines1.map((line) => idMap.toChar(line)).join('');
|
||||
const text2 = lines2.map((line) => idMap.toChar(line)).join('');
|
||||
|
||||
const diff = charDiff(text1, text2);
|
||||
const lineDiff = [];
|
||||
for (let i = 0; i < diff.length; i++) {
|
||||
const lines = [];
|
||||
for (let j = 0; j < diff[i][1].length; j++) {
|
||||
lines.push(idMap.fromChar(diff[i][1][j]) || '');
|
||||
}
|
||||
|
||||
lineDiff.push({ 0: diff[i][0], 1: lines });
|
||||
}
|
||||
return lineDiff;
|
||||
};
|
||||
|
||||
export const parseDiff = (rawDiff: string): DiffArray => {
|
||||
const lines = rawDiff.split('\n');
|
||||
|
||||
// skip header lines
|
||||
while (isHeaderLine(lines[0])) lines.shift();
|
||||
|
||||
const diff: DiffArray = [];
|
||||
for (const line of lines) {
|
||||
const gutter = line.substring(0, 1);
|
||||
const operation = gutterToOperation(gutter);
|
||||
const content = line.substring(1);
|
||||
|
||||
if (diff.length === 0) {
|
||||
diff.push([operation, [content]]);
|
||||
} else {
|
||||
const last = diff[diff.length - 1];
|
||||
if (last[0] === operation) {
|
||||
last[1].push(content);
|
||||
} else {
|
||||
diff.push([operation, [content]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
|
||||
function gutterToOperation(gutter: string): Operation {
|
||||
switch (gutter) {
|
||||
case '+':
|
||||
return Operation.Insert;
|
||||
case '-':
|
||||
return Operation.Delete;
|
||||
default:
|
||||
return Operation.Equal;
|
||||
}
|
||||
}
|
||||
|
||||
function isHeaderLine(line: string): boolean {
|
||||
const headers = [
|
||||
'---',
|
||||
'+++',
|
||||
'@@',
|
||||
'index',
|
||||
'diff',
|
||||
'rename',
|
||||
'similarity',
|
||||
'new',
|
||||
'deleted',
|
||||
'old',
|
||||
'copy',
|
||||
'dissimilarity'
|
||||
];
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (line.startsWith(headers[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export enum Operation {
|
||||
Equal = 0,
|
||||
Insert = 1,
|
||||
Delete = -1,
|
||||
Edit = 2
|
||||
}
|
||||
|
||||
export type DiffArray = { 0: Operation; 1: string[] }[];
|
2
src/lib/components/Differ/index.ts
Normal file
2
src/lib/components/Differ/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { default as Differ } from './Differ.svelte';
|
||||
export default Differ;
|
@ -20,14 +20,14 @@ export const enum RowType {
|
||||
Spacer = 'spacer'
|
||||
}
|
||||
|
||||
export function buildDiffRows(
|
||||
export const buildDiffRows = (
|
||||
diff: DiffArray,
|
||||
opts = { paddingLines: 10000 }
|
||||
): {
|
||||
originalLines: readonly string[];
|
||||
currentLines: readonly string[];
|
||||
rows: readonly Row[];
|
||||
} {
|
||||
} => {
|
||||
const { paddingLines } = opts;
|
||||
|
||||
let currentLineNumber = 0;
|
||||
@ -153,13 +153,13 @@ export function buildDiffRows(
|
||||
size
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function documentMap(lines: readonly string[]): Map<number, number> {
|
||||
export const documentMap = (lines: readonly string[]): Map<number, number> => {
|
||||
const map = new Map<number, number>();
|
||||
for (let pos = 0, lineNo = 0; lineNo < lines.length; lineNo++) {
|
||||
map.set(lineNo + 1, pos);
|
||||
pos += lines[lineNo].length + 1;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
export { default as BackForwardButtons } from './BackForwardButtons.svelte';
|
||||
export { default as Login } from './Login.svelte';
|
||||
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
||||
export { default as CodeViewer } from './CodeViewer';
|
||||
export { default as CommandPalette } from './CommandPalette/CommandPalette.svelte';
|
||||
export { default as Modal } from './Modal.svelte';
|
||||
export { default as ButtonGroup } from './ButtonGroup';
|
||||
@ -10,3 +9,5 @@ export { default as Tooltip } from './Tooltip';
|
||||
export { default as Button } from './Button';
|
||||
export { default as Link } from './Link';
|
||||
export { default as Statuses } from './Statuses.svelte';
|
||||
export { default as Differ } from './Differ';
|
||||
export { default as DeltasViewer } from './DeltasViewer.svelte';
|
||||
|
@ -24,9 +24,16 @@
|
||||
let summary = '';
|
||||
let description = '';
|
||||
|
||||
const selectedDiffPath = writable<string | undefined>(Object.keys($statuses).at(0));
|
||||
const selectedDiffPath = writable<string | undefined>(
|
||||
Object.keys($statuses)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.at(0)
|
||||
);
|
||||
statuses.subscribe((statuses) => {
|
||||
$selectedDiffPath = Object.keys(statuses).at(0);
|
||||
if ($selectedDiffPath && Object.keys(statuses).includes($selectedDiffPath)) return;
|
||||
$selectedDiffPath = Object.keys(statuses)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.at(0);
|
||||
});
|
||||
const selectedDiff = derived([diffs, selectedDiffPath], ([diffs, selectedDiffPath]) =>
|
||||
selectedDiffPath ? diffs[selectedDiffPath] : undefined
|
||||
@ -332,7 +339,7 @@
|
||||
>
|
||||
{#if $selectedDiffPath !== undefined}
|
||||
{#if $selectedDiff !== undefined}
|
||||
<DiffViewer diff={$selectedDiff} path={$selectedDiffPath} />
|
||||
<DiffViewer diff={$selectedDiff ?? ''} path={$selectedDiffPath} />
|
||||
{:else}
|
||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
<p class="text-lg">Unable to load diff</p>
|
||||
|
@ -32,7 +32,7 @@
|
||||
} from '$lib/components/icons';
|
||||
import { collapse } from '$lib/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { CodeViewer } from '$lib/components';
|
||||
import { DeltasViewer } from '$lib/components';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { format } from 'date-fns';
|
||||
import { onMount } from 'svelte';
|
||||
@ -375,7 +375,7 @@
|
||||
</header>
|
||||
<div id="code" class="flex-auto overflow-auto px-2">
|
||||
<div class="pb-[200px]">
|
||||
<CodeViewer
|
||||
<DeltasViewer
|
||||
doc={$frame.doc}
|
||||
deltas={$frame.deltas}
|
||||
filepath={$frame.filepath}
|
||||
|
@ -5,7 +5,7 @@
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { IconLoading } from '$lib/components/icons';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { CodeViewer } from '$lib/components';
|
||||
import { DeltasViewer } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -101,7 +101,7 @@
|
||||
<div
|
||||
class="flex-auto overflow-auto rounded-lg border border-zinc-700 bg-[#2F2F33] p-2 text-[#EBDBB2] shadow-lg"
|
||||
>
|
||||
<CodeViewer {doc} {deltas} {filepath} paddingLines={2} {highlight} />
|
||||
<DeltasViewer {doc} {deltas} {filepath} paddingLines={2} {highlight} />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
Loading…
Reference in New Issue
Block a user