1import { createSignal, For, Match, Show, Switch } from "solid-js";
2import type { DiffTextFragment } from "../../../util/types";
3import type { TreeNode } from "./data";
4
5export function RenderTree(props: { tree: TreeNode; skip?: boolean }) {
6 if (props.skip)
7 return (
8 <For each={props.tree.children}>
9 {(node) => <RenderTree tree={node} />}
10 </For>
11 );
12 const [displayChildren, setDisplayChildren] = createSignal(true);
13 return (
14 <Switch>
15 <Match when={props.tree.type === "file"}>
16 <a
17 class="flex min-w-fit cursor-default select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
18 href={`#file-${encodeURI(props.tree.fullPath)}`}
19 onClick={() => {
20 const hash = `#file-${encodeURI(props.tree.fullPath)}`;
21 if (window.location.hash === hash) {
22 document
23 .getElementById(`file-${props.tree.fullPath}`)
24 ?.scrollIntoView({ behavior: "instant", block: "start" });
25 }
26 }}
27 >
28 <div class="iconify gravity-ui--file" />
29 <span class="select-text">{props.tree.name}</span>
30 </a>
31 </Match>
32 <Match when={props.tree.type === "directory"}>
33 <button
34 type="button"
35 class="flex min-w-fit select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
36 onClick={() => setDisplayChildren(!displayChildren())}
37 >
38 <div class="iconify gravity-ui--folder-fill" />
39 <span class="select-text">{props.tree.name}</span>
40 </button>
41 <div
42 class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`}
43 >
44 <For each={props.tree.children}>
45 {(node) => <RenderTree tree={node} />}
46 </For>
47 </div>
48 </Match>
49 </Switch>
50 );
51}
52
53function Line(props: {
54 file: string;
55 index: number;
56 line: { Op: number; Line: string };
57 lineNumber: number;
58 lineNumberOld: string;
59 lineNumberNew: string;
60 filler: string;
61}) {
62 const id = `line-${encodeURI(props.file)}-${props.index}-${props.lineNumber.toString()}`;
63 return (
64 <div
65 class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500"
66 id={id}
67 >
68 <div class="sticky left-0 select-none border-gray-200 border-r bg-white *:flex dark:border-gray-700 dark:bg-gray-800">
69 <Show
70 when={props.lineNumberOld}
71 fallback={
72 <span class="float-right w-1/2 justify-end pr-1 pl-1.5">
73 {props.filler}
74 </span>
75 }
76 >
77 <a
78 href={`#${id}`}
79 class="float-right w-1/2 justify-end pr-1 pl-1.5 hover:text-gray-700 hover:dark:text-gray-200"
80 >
81 {props.lineNumberOld}
82 </a>
83 </Show>
84 <Show
85 when={props.lineNumberNew}
86 fallback={
87 <span class="float-right w-1/2 justify-end pr-1.5 pl-1">
88 {props.filler}
89 </span>
90 }
91 >
92 <a
93 href={`#${id}`}
94 class="float-right w-1/2 justify-end pr-1.5 pl-1 hover:text-gray-700 hover:dark:text-gray-200"
95 >
96 {props.lineNumberNew}
97 </a>
98 </Show>
99 </div>
100 <Switch>
101 <Match when={props.line.Op === 0}>
102 <div class="w-full text-gray-500 dark:text-gray-500">
103 <div class="select-none">{" "}</div>
104 {props.line.Line}
105 </div>
106 </Match>
107 <Match when={props.line.Op === 2}>
108 <div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">
109 <div class="select-none">{" + "}</div>
110 {props.line.Line}
111 </div>
112 </Match>
113 <Match when={props.line.Op === 1}>
114 <div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400">
115 <div class="select-none">{" - "}</div>
116 {props.line.Line}
117 </div>
118 </Match>
119 </Switch>
120 </div>
121 );
122}
123
124function Fragment(props: {
125 file: string;
126 data: DiffTextFragment;
127 index: number;
128 numberSize: number;
129}) {
130 let lineNumber = props.data.NewPosition;
131 let iOld = props.data.OldPosition;
132 let iNew = props.data.NewPosition;
133
134 return (
135 <>
136 <Show when={props.index !== 0}>
137 <div class="h-5 w-full select-none bg-gray-100 text-center font-mono text-gray-700 dark:bg-gray-700 dark:text-gray-300">
138 ···
139 </div>
140 </Show>
141 <div class="w-full whitespace-pre font-mono">
142 <For each={props.data.Lines}>
143 {(line) => {
144 const lineNumberOld =
145 line.Op === 2
146 ? ""
147 : (iOld++).toString().padStart(props.numberSize, " ");
148 const lineNumberNew =
149 line.Op === 1
150 ? ""
151 : (iNew++).toString().padStart(props.numberSize, " ");
152 const filler = " ".repeat(props.numberSize);
153 return (
154 <Line
155 file={props.file}
156 index={props.index}
157 line={line}
158 lineNumber={lineNumber++}
159 lineNumberNew={lineNumberNew}
160 lineNumberOld={lineNumberOld}
161 filler={filler}
162 />
163 );
164 }}
165 </For>
166 </div>
167 </>
168 );
169}
170
171export function DiffView(props: {
172 diff: {
173 oldName: string;
174 newName: string;
175 textFragments: DiffTextFragment[];
176 }[];
177}) {
178 return (
179 <For each={props.diff}>
180 {(diff) => {
181 const [show, setShow] = createSignal(true);
182
183 const [addedLines, removedLines] = diff.textFragments
184 ? diff.textFragments.reduce(
185 (acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted],
186 [0, 0],
187 )
188 : [0, 0];
189
190 const header = (
191 <button
192 type="button"
193 class={`sticky top-0 z-10 flex cursor-default select-none flex-row items-center gap-2 bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 hover:dark:bg-gray-700 ${show() ? "rounded-t border-gray-200 border-b dark:border-gray-700" : "rounded"}`}
194 onClick={() => setShow(!show())}
195 >
196 <div
197 class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`}
198 />
199 <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
200 <Show when={addedLines > 0}>
201 <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div>
202 </Show>
203 <Show when={removedLines > 0}>
204 <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div>
205 </Show>
206 </div>
207 <Show when={diff.oldName !== "" && diff.newName !== diff.oldName}>
208 <div class="select-text">{diff.oldName}</div>
209 <div class="iconify gravity-ui--arrow-right" />
210 </Show>
211 <Show
212 when={diff.newName !== ""}
213 fallback={<div class="iconify gravity-ui--trash-bin" />}
214 >
215 <div class="select-text">{diff.newName}</div>
216 </Show>
217 </button>
218 );
219
220 if (!diff.textFragments)
221 return (
222 <div
223 id={`file-${encodeURI(diff.newName)}`}
224 class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
225 >
226 {header}
227 <div
228 class={`flex select-text justify-center rounded-b bg-white py-2 text-gray-500 dark:bg-gray-800 dark:text-gray-300 ${show() ? "" : "hidden"}`}
229 >
230 This is a binary file and will not be displayed.
231 </div>
232 </div>
233 );
234
235 const lastFrag = diff.textFragments[diff.textFragments.length - 1];
236 const numberSize = Math.max(
237 2,
238 (
239 Math.max(lastFrag.NewPosition, lastFrag.OldPosition) +
240 lastFrag.Lines.length
241 ).toString().length,
242 );
243
244 return (
245 <div
246 id={`file-${diff.newName}`}
247 class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
248 >
249 {header}
250 <div
251 class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`}
252 >
253 <div class="min-w-max">
254 <For each={diff.textFragments}>
255 {(frag, i) => (
256 <Fragment
257 file={diff.newName}
258 data={frag}
259 index={i()}
260 numberSize={numberSize}
261 />
262 )}
263 </For>
264 </div>
265 </div>
266 </div>
267 );
268 }}
269 </For>
270 );
271}
272
273export function Sidebar(props: {
274 insertions: number;
275 deletions: number;
276 sidebar: TreeNode;
277}) {
278 return (
279 <div class="flex overflow-y-auto p-1 max-md:pb-0 md:sticky md:top-0 md:max-h-screen md:w-50 md:pr-0">
280 <div class="flex min-h-max w-full grow cursor-default flex-col rounded border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800">
281 <div class="flex flex-row items-center justify-between p-1">
282 <div class="font-bold">CHANGED FILES</div>
283 <div class="flex h-6 select-text flex-row items-center overflow-clip rounded font-mono text-xs *:h-full *:content-center *:px-1">
284 <Show when={props.insertions > 0}>
285 <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${props.insertions}`}</div>
286 </Show>
287 <Show when={props.deletions > 0}>
288 <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${props.deletions}`}</div>
289 </Show>
290 </div>
291 </div>
292 <div class="flex max-w-full flex-col overflow-x-auto text-nowrap">
293 <RenderTree tree={props.sidebar} skip={true} />
294 </div>
295 </div>
296 </div>
297 );
298}