kajji 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/bin/kajji.js +2 -0
- package/package.json +56 -0
- package/src/App.tsx +229 -0
- package/src/commander/bookmarks.ts +129 -0
- package/src/commander/diff.ts +186 -0
- package/src/commander/executor.ts +285 -0
- package/src/commander/files.ts +87 -0
- package/src/commander/log.ts +99 -0
- package/src/commander/operations.ts +313 -0
- package/src/commander/types.ts +21 -0
- package/src/components/AnsiText.tsx +77 -0
- package/src/components/BorderBox.tsx +124 -0
- package/src/components/FileTreeList.tsx +105 -0
- package/src/components/Layout.tsx +48 -0
- package/src/components/Panel.tsx +143 -0
- package/src/components/RevisionPicker.tsx +165 -0
- package/src/components/StatusBar.tsx +158 -0
- package/src/components/modals/BookmarkNameModal.tsx +170 -0
- package/src/components/modals/DescribeModal.tsx +124 -0
- package/src/components/modals/HelpModal.tsx +372 -0
- package/src/components/modals/RevisionPickerModal.tsx +70 -0
- package/src/components/modals/UndoModal.tsx +75 -0
- package/src/components/panels/BookmarksPanel.tsx +768 -0
- package/src/components/panels/CommandLogPanel.tsx +40 -0
- package/src/components/panels/LogPanel.tsx +774 -0
- package/src/components/panels/MainArea.tsx +354 -0
- package/src/context/command.tsx +106 -0
- package/src/context/commandlog.tsx +45 -0
- package/src/context/dialog.tsx +217 -0
- package/src/context/focus.tsx +63 -0
- package/src/context/helper.tsx +24 -0
- package/src/context/keybind.tsx +51 -0
- package/src/context/loading.tsx +68 -0
- package/src/context/sync.tsx +868 -0
- package/src/context/theme.tsx +90 -0
- package/src/context/types.ts +51 -0
- package/src/index.tsx +15 -0
- package/src/keybind/index.ts +2 -0
- package/src/keybind/parser.ts +88 -0
- package/src/keybind/types.ts +83 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/presets/lazygit.ts +45 -0
- package/src/theme/presets/opencode.ts +45 -0
- package/src/theme/types.ts +47 -0
- package/src/utils/double-click.ts +59 -0
- package/src/utils/file-tree.ts +154 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import {
|
|
3
|
+
For,
|
|
4
|
+
Show,
|
|
5
|
+
createEffect,
|
|
6
|
+
createMemo,
|
|
7
|
+
createSignal,
|
|
8
|
+
onMount,
|
|
9
|
+
} from "solid-js"
|
|
10
|
+
import { jjBookmarkCreate } from "../../commander/bookmarks"
|
|
11
|
+
import {
|
|
12
|
+
type OpLogEntry,
|
|
13
|
+
type OperationResult,
|
|
14
|
+
fetchOpLog,
|
|
15
|
+
isImmutableError,
|
|
16
|
+
jjAbandon,
|
|
17
|
+
jjDescribe,
|
|
18
|
+
jjEdit,
|
|
19
|
+
jjNew,
|
|
20
|
+
jjOpRestore,
|
|
21
|
+
jjRedo,
|
|
22
|
+
jjRestore,
|
|
23
|
+
jjShowDescription,
|
|
24
|
+
jjSquash,
|
|
25
|
+
jjUndo,
|
|
26
|
+
parseOpLog,
|
|
27
|
+
} from "../../commander/operations"
|
|
28
|
+
import { useCommand } from "../../context/command"
|
|
29
|
+
import { useCommandLog } from "../../context/commandlog"
|
|
30
|
+
import { useDialog } from "../../context/dialog"
|
|
31
|
+
import { useFocus } from "../../context/focus"
|
|
32
|
+
import { useLoading } from "../../context/loading"
|
|
33
|
+
import { useSync } from "../../context/sync"
|
|
34
|
+
import { useTheme } from "../../context/theme"
|
|
35
|
+
import type { Context } from "../../context/types"
|
|
36
|
+
import { createDoubleClickDetector } from "../../utils/double-click"
|
|
37
|
+
import { AnsiText } from "../AnsiText"
|
|
38
|
+
import { FileTreeList } from "../FileTreeList"
|
|
39
|
+
import { Panel } from "../Panel"
|
|
40
|
+
import { BookmarkNameModal } from "../modals/BookmarkNameModal"
|
|
41
|
+
import { DescribeModal } from "../modals/DescribeModal"
|
|
42
|
+
import { UndoModal } from "../modals/UndoModal"
|
|
43
|
+
|
|
44
|
+
type LogTab = "revisions" | "oplog"
|
|
45
|
+
|
|
46
|
+
const LOG_TABS: Array<{ id: LogTab; label: string; context: Context }> = [
|
|
47
|
+
{ id: "revisions", label: "Revisions", context: "log.revisions" },
|
|
48
|
+
{ id: "oplog", label: "Oplog", context: "log.oplog" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export function LogPanel() {
|
|
52
|
+
const {
|
|
53
|
+
commits,
|
|
54
|
+
selectedIndex,
|
|
55
|
+
setSelectedIndex,
|
|
56
|
+
selectedCommit,
|
|
57
|
+
loading,
|
|
58
|
+
error,
|
|
59
|
+
selectNext,
|
|
60
|
+
selectPrev,
|
|
61
|
+
enterFilesView,
|
|
62
|
+
exitFilesView,
|
|
63
|
+
viewMode,
|
|
64
|
+
refresh,
|
|
65
|
+
flatFiles,
|
|
66
|
+
selectedFileIndex,
|
|
67
|
+
setSelectedFileIndex,
|
|
68
|
+
filesLoading,
|
|
69
|
+
filesError,
|
|
70
|
+
collapsedPaths,
|
|
71
|
+
toggleFolder,
|
|
72
|
+
selectNextFile,
|
|
73
|
+
selectPrevFile,
|
|
74
|
+
} = useSync()
|
|
75
|
+
const focus = useFocus()
|
|
76
|
+
const command = useCommand()
|
|
77
|
+
const commandLog = useCommandLog()
|
|
78
|
+
const dialog = useDialog()
|
|
79
|
+
const globalLoading = useLoading()
|
|
80
|
+
const { colors } = useTheme()
|
|
81
|
+
|
|
82
|
+
const [activeTab, setActiveTab] = createSignal<LogTab>("revisions")
|
|
83
|
+
const [opLogEntries, setOpLogEntries] = createSignal<OpLogEntry[]>([])
|
|
84
|
+
const [opLogLoading, setOpLogLoading] = createSignal(false)
|
|
85
|
+
const [opLogSelectedIndex, setOpLogSelectedIndex] = createSignal(0)
|
|
86
|
+
const [opLogLimit, setOpLogLimit] = createSignal(50)
|
|
87
|
+
const [opLogHasMore, setOpLogHasMore] = createSignal(true)
|
|
88
|
+
|
|
89
|
+
const isFocused = () => focus.isPanel("log")
|
|
90
|
+
const isFilesView = () => viewMode() === "files"
|
|
91
|
+
|
|
92
|
+
const tabs = () => (isFilesView() ? undefined : LOG_TABS)
|
|
93
|
+
|
|
94
|
+
const title = () => (isFilesView() ? "Files" : undefined)
|
|
95
|
+
|
|
96
|
+
const loadOpLog = async (limit?: number) => {
|
|
97
|
+
const effectiveLimit = limit ?? opLogLimit()
|
|
98
|
+
const isInitialLoad = opLogEntries().length === 0
|
|
99
|
+
if (isInitialLoad) setOpLogLoading(true)
|
|
100
|
+
try {
|
|
101
|
+
await globalLoading.run("Loading...", async () => {
|
|
102
|
+
const lines = await fetchOpLog(effectiveLimit)
|
|
103
|
+
const entries = parseOpLog(lines)
|
|
104
|
+
setOpLogEntries(entries)
|
|
105
|
+
setOpLogHasMore(entries.length >= effectiveLimit)
|
|
106
|
+
})
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error("Failed to load op log:", e)
|
|
109
|
+
} finally {
|
|
110
|
+
if (isInitialLoad) setOpLogLoading(false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const loadMoreOpLog = async () => {
|
|
115
|
+
if (!opLogHasMore() || opLogLoading()) return
|
|
116
|
+
const newLimit = opLogLimit() + 50
|
|
117
|
+
setOpLogLimit(newLimit)
|
|
118
|
+
await loadOpLog(newLimit)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onMount(() => {
|
|
122
|
+
loadOpLog()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const switchTab = (tabId: string) => {
|
|
126
|
+
const tab = LOG_TABS.find((t) => t.id === tabId)
|
|
127
|
+
if (!tab) return
|
|
128
|
+
setActiveTab(tab.id)
|
|
129
|
+
focus.setActiveContext(tab.context)
|
|
130
|
+
if (tab.id === "oplog") {
|
|
131
|
+
loadOpLog()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const runOperation = async (
|
|
136
|
+
text: string,
|
|
137
|
+
op: () => Promise<OperationResult>,
|
|
138
|
+
) => {
|
|
139
|
+
const result = await globalLoading.run(text, op)
|
|
140
|
+
commandLog.addEntry(result)
|
|
141
|
+
if (result.success) {
|
|
142
|
+
refresh()
|
|
143
|
+
loadOpLog()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let scrollRef: ScrollBoxRenderable | undefined
|
|
148
|
+
const [scrollTop, setScrollTop] = createSignal(0)
|
|
149
|
+
|
|
150
|
+
let opLogScrollRef: ScrollBoxRenderable | undefined
|
|
151
|
+
const [opLogScrollTop, setOpLogScrollTop] = createSignal(0)
|
|
152
|
+
|
|
153
|
+
createEffect(() => {
|
|
154
|
+
const index = selectedIndex()
|
|
155
|
+
const commitList = commits()
|
|
156
|
+
if (!scrollRef || commitList.length === 0) return
|
|
157
|
+
|
|
158
|
+
let lineOffset = 0
|
|
159
|
+
const clampedIndex = Math.min(index, commitList.length)
|
|
160
|
+
for (const commit of commitList.slice(0, clampedIndex)) {
|
|
161
|
+
lineOffset += commit.lines.length
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const margin = 2
|
|
165
|
+
const refAny = scrollRef as unknown as Record<string, unknown>
|
|
166
|
+
const viewportHeight =
|
|
167
|
+
(typeof refAny.height === "number" ? refAny.height : null) ??
|
|
168
|
+
(typeof refAny.rows === "number" ? refAny.rows : null) ??
|
|
169
|
+
10
|
|
170
|
+
const currentScrollTop = scrollTop()
|
|
171
|
+
|
|
172
|
+
const visibleStart = currentScrollTop
|
|
173
|
+
const visibleEnd = currentScrollTop + viewportHeight - 1
|
|
174
|
+
const safeStart = visibleStart + margin
|
|
175
|
+
const safeEnd = visibleEnd - margin
|
|
176
|
+
|
|
177
|
+
let newScrollTop = currentScrollTop
|
|
178
|
+
if (lineOffset < safeStart) {
|
|
179
|
+
newScrollTop = Math.max(0, lineOffset - margin)
|
|
180
|
+
} else if (lineOffset > safeEnd) {
|
|
181
|
+
newScrollTop = Math.max(0, lineOffset - viewportHeight + margin + 1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (newScrollTop !== currentScrollTop) {
|
|
185
|
+
scrollRef.scrollTo(newScrollTop)
|
|
186
|
+
setScrollTop(newScrollTop)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
createEffect(() => {
|
|
191
|
+
const index = opLogSelectedIndex()
|
|
192
|
+
const entries = opLogEntries()
|
|
193
|
+
if (!opLogScrollRef || entries.length === 0) return
|
|
194
|
+
|
|
195
|
+
let lineOffset = 0
|
|
196
|
+
const clampedIndex = Math.min(index, entries.length)
|
|
197
|
+
for (const entry of entries.slice(0, clampedIndex)) {
|
|
198
|
+
lineOffset += entry.lines.length
|
|
199
|
+
}
|
|
200
|
+
const selectedHeight = entries[clampedIndex]?.lines.length ?? 1
|
|
201
|
+
const lineEnd = lineOffset + selectedHeight - 1
|
|
202
|
+
|
|
203
|
+
const margin = 2
|
|
204
|
+
const refAny = opLogScrollRef as unknown as Record<string, unknown>
|
|
205
|
+
const viewportHeight =
|
|
206
|
+
(typeof refAny.height === "number" ? refAny.height : null) ??
|
|
207
|
+
(typeof refAny.rows === "number" ? refAny.rows : null) ??
|
|
208
|
+
10
|
|
209
|
+
const currentScrollTop = opLogScrollTop()
|
|
210
|
+
|
|
211
|
+
const visibleStart = currentScrollTop
|
|
212
|
+
const visibleEnd = currentScrollTop + viewportHeight - 1
|
|
213
|
+
const safeStart = visibleStart + margin
|
|
214
|
+
const safeEnd = visibleEnd - margin
|
|
215
|
+
|
|
216
|
+
let newScrollTop = currentScrollTop
|
|
217
|
+
if (lineOffset < safeStart) {
|
|
218
|
+
newScrollTop = Math.max(0, lineOffset - margin)
|
|
219
|
+
} else if (lineEnd > safeEnd) {
|
|
220
|
+
newScrollTop = Math.max(0, lineEnd - viewportHeight + margin + 1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (newScrollTop !== currentScrollTop) {
|
|
224
|
+
opLogScrollRef.scrollTo(newScrollTop)
|
|
225
|
+
setOpLogScrollTop(newScrollTop)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
let filesScrollRef: ScrollBoxRenderable | undefined
|
|
230
|
+
const [filesScrollTop, setFilesScrollTop] = createSignal(0)
|
|
231
|
+
|
|
232
|
+
createEffect(() => {
|
|
233
|
+
const index = selectedFileIndex()
|
|
234
|
+
if (!filesScrollRef || flatFiles().length === 0) return
|
|
235
|
+
|
|
236
|
+
const margin = 2
|
|
237
|
+
const refAny = filesScrollRef as unknown as Record<string, unknown>
|
|
238
|
+
const viewportHeight =
|
|
239
|
+
(typeof refAny.height === "number" ? refAny.height : null) ??
|
|
240
|
+
(typeof refAny.rows === "number" ? refAny.rows : null) ??
|
|
241
|
+
10
|
|
242
|
+
const currentScrollTop = filesScrollTop()
|
|
243
|
+
|
|
244
|
+
const visibleStart = currentScrollTop
|
|
245
|
+
const visibleEnd = currentScrollTop + viewportHeight - 1
|
|
246
|
+
const safeStart = visibleStart + margin
|
|
247
|
+
const safeEnd = visibleEnd - margin
|
|
248
|
+
|
|
249
|
+
let newScrollTop = currentScrollTop
|
|
250
|
+
if (index < safeStart) {
|
|
251
|
+
newScrollTop = Math.max(0, index - margin)
|
|
252
|
+
} else if (index > safeEnd) {
|
|
253
|
+
newScrollTop = Math.max(0, index - viewportHeight + margin + 1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (newScrollTop !== currentScrollTop) {
|
|
257
|
+
filesScrollRef.scrollTo(newScrollTop)
|
|
258
|
+
setFilesScrollTop(newScrollTop)
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const handleFileEnter = () => {
|
|
263
|
+
const file = flatFiles()[selectedFileIndex()]
|
|
264
|
+
if (file?.node.isDirectory) {
|
|
265
|
+
toggleFolder(file.node.path)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const selectPrevOpLog = () => {
|
|
270
|
+
setOpLogSelectedIndex((i) => Math.max(0, i - 1))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const selectNextOpLog = () => {
|
|
274
|
+
const entries = opLogEntries()
|
|
275
|
+
const currentIndex = opLogSelectedIndex()
|
|
276
|
+
const newIndex = Math.min(entries.length - 1, currentIndex + 1)
|
|
277
|
+
setOpLogSelectedIndex(newIndex)
|
|
278
|
+
|
|
279
|
+
if (entries.length - newIndex <= 5 && opLogHasMore()) {
|
|
280
|
+
loadMoreOpLog()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const selectedOperation = () => opLogEntries()[opLogSelectedIndex()]
|
|
285
|
+
|
|
286
|
+
const openUndoModal = (type: "undo" | "redo") => {
|
|
287
|
+
dialog.open(
|
|
288
|
+
() => (
|
|
289
|
+
<UndoModal
|
|
290
|
+
type={type}
|
|
291
|
+
onConfirm={async () => {
|
|
292
|
+
dialog.close()
|
|
293
|
+
const op = type === "undo" ? jjUndo : jjRedo
|
|
294
|
+
await runOperation(
|
|
295
|
+
type === "undo" ? "Undoing..." : "Redoing...",
|
|
296
|
+
op,
|
|
297
|
+
)
|
|
298
|
+
}}
|
|
299
|
+
onCancel={() => dialog.close()}
|
|
300
|
+
/>
|
|
301
|
+
),
|
|
302
|
+
{
|
|
303
|
+
id: `${type}-modal`,
|
|
304
|
+
hints: [
|
|
305
|
+
{ key: "y", label: "confirm" },
|
|
306
|
+
{ key: "n", label: "cancel" },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
command.register(() => [
|
|
313
|
+
{
|
|
314
|
+
id: "log.oplog.prev",
|
|
315
|
+
title: "up",
|
|
316
|
+
keybind: "nav_up",
|
|
317
|
+
context: "log.oplog",
|
|
318
|
+
type: "navigation",
|
|
319
|
+
panel: "log",
|
|
320
|
+
visibility: "help-only",
|
|
321
|
+
onSelect: selectPrevOpLog,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "log.oplog.next",
|
|
325
|
+
title: "down",
|
|
326
|
+
keybind: "nav_down",
|
|
327
|
+
context: "log.oplog",
|
|
328
|
+
type: "navigation",
|
|
329
|
+
panel: "log",
|
|
330
|
+
visibility: "help-only",
|
|
331
|
+
onSelect: selectNextOpLog,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: "log.revisions.next",
|
|
335
|
+
title: "down",
|
|
336
|
+
keybind: "nav_down",
|
|
337
|
+
context: "log.revisions",
|
|
338
|
+
type: "navigation",
|
|
339
|
+
panel: "log",
|
|
340
|
+
visibility: "help-only",
|
|
341
|
+
onSelect: selectNext,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "log.revisions.prev",
|
|
345
|
+
title: "up",
|
|
346
|
+
keybind: "nav_up",
|
|
347
|
+
context: "log.revisions",
|
|
348
|
+
type: "navigation",
|
|
349
|
+
panel: "log",
|
|
350
|
+
visibility: "help-only",
|
|
351
|
+
onSelect: selectPrev,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "log.revisions.view_files",
|
|
355
|
+
title: "view files",
|
|
356
|
+
keybind: "enter",
|
|
357
|
+
context: "log.revisions",
|
|
358
|
+
type: "view",
|
|
359
|
+
panel: "log",
|
|
360
|
+
visibility: "help-only",
|
|
361
|
+
onSelect: () => enterFilesView(),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "log.revisions.new",
|
|
365
|
+
title: "new",
|
|
366
|
+
keybind: "jj_new",
|
|
367
|
+
context: "log.revisions",
|
|
368
|
+
type: "action",
|
|
369
|
+
panel: "log",
|
|
370
|
+
onSelect: () => {
|
|
371
|
+
const commit = selectedCommit()
|
|
372
|
+
if (commit) runOperation("Creating...", () => jjNew(commit.changeId))
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "log.revisions.edit",
|
|
377
|
+
title: "edit",
|
|
378
|
+
keybind: "jj_edit",
|
|
379
|
+
context: "log.revisions",
|
|
380
|
+
type: "action",
|
|
381
|
+
panel: "log",
|
|
382
|
+
onSelect: () => {
|
|
383
|
+
const commit = selectedCommit()
|
|
384
|
+
if (commit) runOperation("Editing...", () => jjEdit(commit.changeId))
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: "log.revisions.squash",
|
|
389
|
+
title: "squash",
|
|
390
|
+
keybind: "jj_squash",
|
|
391
|
+
context: "log.revisions",
|
|
392
|
+
type: "action",
|
|
393
|
+
panel: "log",
|
|
394
|
+
onSelect: async () => {
|
|
395
|
+
const commit = selectedCommit()
|
|
396
|
+
if (!commit) return
|
|
397
|
+
const result = await jjSquash(commit.changeId)
|
|
398
|
+
if (isImmutableError(result)) {
|
|
399
|
+
const confirmed = await dialog.confirm({
|
|
400
|
+
message: "Parent is immutable. Squash anyway?",
|
|
401
|
+
})
|
|
402
|
+
if (confirmed) {
|
|
403
|
+
await runOperation("Squashing...", () =>
|
|
404
|
+
jjSquash(commit.changeId, { ignoreImmutable: true }),
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
commandLog.addEntry(result)
|
|
409
|
+
if (result.success) {
|
|
410
|
+
refresh()
|
|
411
|
+
loadOpLog()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "log.revisions.describe",
|
|
418
|
+
title: "describe",
|
|
419
|
+
keybind: "jj_describe",
|
|
420
|
+
context: "log.revisions",
|
|
421
|
+
type: "action",
|
|
422
|
+
panel: "log",
|
|
423
|
+
onSelect: async () => {
|
|
424
|
+
const commit = selectedCommit()
|
|
425
|
+
if (!commit) return
|
|
426
|
+
|
|
427
|
+
let ignoreImmutable = false
|
|
428
|
+
if (commit.immutable) {
|
|
429
|
+
const confirmed = await dialog.confirm({
|
|
430
|
+
message: "Commit is immutable. Describe anyway?",
|
|
431
|
+
})
|
|
432
|
+
if (!confirmed) return
|
|
433
|
+
ignoreImmutable = true
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const desc = await jjShowDescription(commit.changeId)
|
|
437
|
+
dialog.open(
|
|
438
|
+
() => (
|
|
439
|
+
<DescribeModal
|
|
440
|
+
initialSubject={desc.subject}
|
|
441
|
+
initialBody={desc.body}
|
|
442
|
+
onSave={(subject, body) => {
|
|
443
|
+
const message = body ? `${subject}\n\n${body}` : subject
|
|
444
|
+
runOperation("Describing...", () =>
|
|
445
|
+
jjDescribe(commit.changeId, message, { ignoreImmutable }),
|
|
446
|
+
)
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
),
|
|
450
|
+
{
|
|
451
|
+
id: "describe",
|
|
452
|
+
hints: [
|
|
453
|
+
{ key: "tab", label: "switch field" },
|
|
454
|
+
{ key: "enter", label: "save" },
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: "log.revisions.abandon",
|
|
462
|
+
title: "abandon",
|
|
463
|
+
keybind: "jj_abandon",
|
|
464
|
+
context: "log.revisions",
|
|
465
|
+
type: "action",
|
|
466
|
+
panel: "log",
|
|
467
|
+
onSelect: async () => {
|
|
468
|
+
const commit = selectedCommit()
|
|
469
|
+
if (!commit) return
|
|
470
|
+
const confirmed = await dialog.confirm({
|
|
471
|
+
message: `Abandon change ${commit.changeId.slice(0, 8)}?`,
|
|
472
|
+
})
|
|
473
|
+
if (confirmed) {
|
|
474
|
+
await runOperation("Abandoning...", () => jjAbandon(commit.changeId))
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: "log.revisions.bookmark",
|
|
480
|
+
title: "create bookmark",
|
|
481
|
+
keybind: "bookmark_set",
|
|
482
|
+
context: "log.revisions",
|
|
483
|
+
type: "action",
|
|
484
|
+
panel: "log",
|
|
485
|
+
onSelect: () => {
|
|
486
|
+
const commit = selectedCommit()
|
|
487
|
+
if (!commit) return
|
|
488
|
+
dialog.open(
|
|
489
|
+
() => (
|
|
490
|
+
<BookmarkNameModal
|
|
491
|
+
title="Create Bookmark"
|
|
492
|
+
commits={commits()}
|
|
493
|
+
defaultRevision={commit.changeId}
|
|
494
|
+
onSave={(name, revision) => {
|
|
495
|
+
runOperation("Creating bookmark...", () =>
|
|
496
|
+
jjBookmarkCreate(name, { revision }),
|
|
497
|
+
)
|
|
498
|
+
}}
|
|
499
|
+
/>
|
|
500
|
+
),
|
|
501
|
+
{
|
|
502
|
+
id: "bookmark-create",
|
|
503
|
+
hints: [
|
|
504
|
+
{ key: "tab", label: "switch field" },
|
|
505
|
+
{ key: "enter", label: "save" },
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "log.revisions.undo",
|
|
513
|
+
title: "undo",
|
|
514
|
+
keybind: "jj_undo",
|
|
515
|
+
context: "log.revisions",
|
|
516
|
+
type: "action",
|
|
517
|
+
panel: "log",
|
|
518
|
+
onSelect: () => openUndoModal("undo"),
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
id: "log.revisions.redo",
|
|
522
|
+
title: "redo",
|
|
523
|
+
keybind: "jj_redo",
|
|
524
|
+
context: "log.revisions",
|
|
525
|
+
type: "action",
|
|
526
|
+
panel: "log",
|
|
527
|
+
onSelect: () => openUndoModal("redo"),
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: "log.oplog.restore",
|
|
531
|
+
title: "restore",
|
|
532
|
+
keybind: "jj_restore",
|
|
533
|
+
context: "log.oplog",
|
|
534
|
+
type: "action",
|
|
535
|
+
panel: "log",
|
|
536
|
+
onSelect: () => {
|
|
537
|
+
const op = selectedOperation()
|
|
538
|
+
if (!op) return
|
|
539
|
+
dialog.open(
|
|
540
|
+
() => (
|
|
541
|
+
<UndoModal
|
|
542
|
+
type="restore"
|
|
543
|
+
operationLines={op.lines}
|
|
544
|
+
onConfirm={async () => {
|
|
545
|
+
dialog.close()
|
|
546
|
+
await runOperation("Restoring...", () =>
|
|
547
|
+
jjOpRestore(op.operationId),
|
|
548
|
+
)
|
|
549
|
+
}}
|
|
550
|
+
onCancel={() => dialog.close()}
|
|
551
|
+
/>
|
|
552
|
+
),
|
|
553
|
+
{
|
|
554
|
+
id: "restore-modal",
|
|
555
|
+
hints: [
|
|
556
|
+
{ key: "y", label: "confirm" },
|
|
557
|
+
{ key: "n", label: "cancel" },
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
)
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "log.files.next",
|
|
565
|
+
title: "down",
|
|
566
|
+
keybind: "nav_down",
|
|
567
|
+
context: "log.files",
|
|
568
|
+
type: "navigation",
|
|
569
|
+
panel: "log",
|
|
570
|
+
visibility: "help-only",
|
|
571
|
+
onSelect: selectNextFile,
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: "log.files.prev",
|
|
575
|
+
title: "up",
|
|
576
|
+
keybind: "nav_up",
|
|
577
|
+
context: "log.files",
|
|
578
|
+
type: "navigation",
|
|
579
|
+
panel: "log",
|
|
580
|
+
visibility: "help-only",
|
|
581
|
+
onSelect: selectPrevFile,
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
id: "log.files.toggle",
|
|
585
|
+
title: "toggle folder",
|
|
586
|
+
keybind: "enter",
|
|
587
|
+
context: "log.files",
|
|
588
|
+
type: "action",
|
|
589
|
+
panel: "log",
|
|
590
|
+
visibility: "help-only",
|
|
591
|
+
onSelect: handleFileEnter,
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "log.files.back",
|
|
595
|
+
title: "back",
|
|
596
|
+
keybind: "escape",
|
|
597
|
+
context: "log.files",
|
|
598
|
+
type: "view",
|
|
599
|
+
panel: "log",
|
|
600
|
+
visibility: "help-only",
|
|
601
|
+
onSelect: exitFilesView,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: "log.files.restore",
|
|
605
|
+
title: "restore",
|
|
606
|
+
keybind: "jj_restore",
|
|
607
|
+
context: "log.files",
|
|
608
|
+
type: "action",
|
|
609
|
+
panel: "log",
|
|
610
|
+
onSelect: async () => {
|
|
611
|
+
const file = flatFiles()[selectedFileIndex()]
|
|
612
|
+
if (!file) return
|
|
613
|
+
const node = file.node
|
|
614
|
+
const pathType = node.isDirectory ? "folder" : "file"
|
|
615
|
+
const confirmed = await dialog.confirm({
|
|
616
|
+
message: `Restore ${pathType} "${node.path}"? This will discard changes.`,
|
|
617
|
+
})
|
|
618
|
+
if (confirmed) {
|
|
619
|
+
await runOperation("Restoring...", () => jjRestore([node.path]))
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
])
|
|
624
|
+
|
|
625
|
+
createEffect(() => {
|
|
626
|
+
if (isFocused() && !isFilesView()) {
|
|
627
|
+
const tab = LOG_TABS.find((t) => t.id === activeTab())
|
|
628
|
+
if (tab) focus.setActiveContext(tab.context)
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const renderLogContent = () => (
|
|
633
|
+
<>
|
|
634
|
+
<Show when={loading() && commits().length === 0}>
|
|
635
|
+
<text>Loading...</text>
|
|
636
|
+
</Show>
|
|
637
|
+
<Show when={error() && commits().length === 0}>
|
|
638
|
+
<text>Error: {error()}</text>
|
|
639
|
+
</Show>
|
|
640
|
+
<Show when={commits().length > 0}>
|
|
641
|
+
<scrollbox
|
|
642
|
+
ref={scrollRef}
|
|
643
|
+
flexGrow={1}
|
|
644
|
+
scrollbarOptions={{ visible: false }}
|
|
645
|
+
>
|
|
646
|
+
<For each={commits()}>
|
|
647
|
+
{(commit, index) => {
|
|
648
|
+
const isSelected = () => index() === selectedIndex()
|
|
649
|
+
const handleClick = createDoubleClickDetector(() => {
|
|
650
|
+
setSelectedIndex(index())
|
|
651
|
+
enterFilesView()
|
|
652
|
+
})
|
|
653
|
+
const handleMouseDown = () => {
|
|
654
|
+
setSelectedIndex(index())
|
|
655
|
+
handleClick()
|
|
656
|
+
}
|
|
657
|
+
return (
|
|
658
|
+
<box onMouseDown={handleMouseDown}>
|
|
659
|
+
<For each={commit.lines}>
|
|
660
|
+
{(line) => (
|
|
661
|
+
<box
|
|
662
|
+
backgroundColor={
|
|
663
|
+
isSelected()
|
|
664
|
+
? colors().selectionBackground
|
|
665
|
+
: undefined
|
|
666
|
+
}
|
|
667
|
+
overflow="hidden"
|
|
668
|
+
>
|
|
669
|
+
<AnsiText
|
|
670
|
+
content={line}
|
|
671
|
+
bold={commit.isWorkingCopy}
|
|
672
|
+
wrapMode="none"
|
|
673
|
+
/>
|
|
674
|
+
</box>
|
|
675
|
+
)}
|
|
676
|
+
</For>
|
|
677
|
+
</box>
|
|
678
|
+
)
|
|
679
|
+
}}
|
|
680
|
+
</For>
|
|
681
|
+
</scrollbox>
|
|
682
|
+
</Show>
|
|
683
|
+
</>
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
const renderOpLogContent = () => (
|
|
687
|
+
<>
|
|
688
|
+
<Show when={opLogLoading() && opLogEntries().length === 0}>
|
|
689
|
+
<text>Loading...</text>
|
|
690
|
+
</Show>
|
|
691
|
+
<Show when={opLogEntries().length > 0}>
|
|
692
|
+
<scrollbox
|
|
693
|
+
ref={opLogScrollRef}
|
|
694
|
+
flexGrow={1}
|
|
695
|
+
scrollbarOptions={{ visible: false }}
|
|
696
|
+
>
|
|
697
|
+
<For each={opLogEntries()}>
|
|
698
|
+
{(entry, index) => {
|
|
699
|
+
const isSelected = () => index() === opLogSelectedIndex()
|
|
700
|
+
return (
|
|
701
|
+
<For each={entry.lines}>
|
|
702
|
+
{(line) => (
|
|
703
|
+
<box
|
|
704
|
+
backgroundColor={
|
|
705
|
+
isSelected() ? colors().selectionBackground : undefined
|
|
706
|
+
}
|
|
707
|
+
overflow="hidden"
|
|
708
|
+
>
|
|
709
|
+
<AnsiText content={line} wrapMode="none" />
|
|
710
|
+
</box>
|
|
711
|
+
)}
|
|
712
|
+
</For>
|
|
713
|
+
)
|
|
714
|
+
}}
|
|
715
|
+
</For>
|
|
716
|
+
</scrollbox>
|
|
717
|
+
</Show>
|
|
718
|
+
</>
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
const renderFilesContent = () => {
|
|
722
|
+
const commit = selectedCommit()
|
|
723
|
+
return (
|
|
724
|
+
<>
|
|
725
|
+
<Show when={filesLoading()}>
|
|
726
|
+
<text fg={colors().textMuted}>Loading files...</text>
|
|
727
|
+
</Show>
|
|
728
|
+
<Show when={filesError()}>
|
|
729
|
+
<text fg={colors().error}>Error: {filesError()}</text>
|
|
730
|
+
</Show>
|
|
731
|
+
<Show when={!filesLoading() && !filesError()}>
|
|
732
|
+
<scrollbox
|
|
733
|
+
ref={filesScrollRef}
|
|
734
|
+
flexGrow={1}
|
|
735
|
+
scrollbarOptions={{ visible: false }}
|
|
736
|
+
>
|
|
737
|
+
<FileTreeList
|
|
738
|
+
files={flatFiles}
|
|
739
|
+
selectedIndex={selectedFileIndex}
|
|
740
|
+
setSelectedIndex={setSelectedFileIndex}
|
|
741
|
+
collapsedPaths={collapsedPaths}
|
|
742
|
+
toggleFolder={toggleFolder}
|
|
743
|
+
/>
|
|
744
|
+
</scrollbox>
|
|
745
|
+
</Show>
|
|
746
|
+
</>
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const filesTitle = () => {
|
|
751
|
+
const commit = selectedCommit()
|
|
752
|
+
return commit ? `Files (${commit.changeId.slice(0, 8)})` : "Files"
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return (
|
|
756
|
+
<Panel
|
|
757
|
+
title={isFilesView() ? filesTitle() : title()}
|
|
758
|
+
tabs={tabs()}
|
|
759
|
+
activeTab={activeTab()}
|
|
760
|
+
onTabChange={switchTab}
|
|
761
|
+
panelId="log"
|
|
762
|
+
hotkey="1"
|
|
763
|
+
focused={isFocused()}
|
|
764
|
+
>
|
|
765
|
+
<Show when={isFilesView()}>{renderFilesContent()}</Show>
|
|
766
|
+
<Show when={!isFilesView() && activeTab() === "revisions"}>
|
|
767
|
+
{renderLogContent()}
|
|
768
|
+
</Show>
|
|
769
|
+
<Show when={!isFilesView() && activeTab() === "oplog"}>
|
|
770
|
+
{renderOpLogContent()}
|
|
771
|
+
</Show>
|
|
772
|
+
</Panel>
|
|
773
|
+
)
|
|
774
|
+
}
|