@lapis-notes/history 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/CHANGELOG.md +6 -0
- package/LICENSE.md +8 -0
- package/README.md +20 -0
- package/dist/history-capture.d.ts +7 -0
- package/dist/history-capture.js +21 -0
- package/dist/history-compare-panel.css +126 -0
- package/dist/history-compare-panel.svelte +314 -0
- package/dist/history-compare-panel.svelte.d.ts +10 -0
- package/dist/history-compare-view.d.ts +24 -0
- package/dist/history-compare-view.js +97 -0
- package/dist/history-format.d.ts +3 -0
- package/dist/history-format.js +19 -0
- package/dist/history-panel.css +200 -0
- package/dist/history-panel.svelte +302 -0
- package/dist/history-panel.svelte.d.ts +8 -0
- package/dist/history-plugin.d.ts +88 -0
- package/dist/history-plugin.js +435 -0
- package/dist/history-settings-tab.d.ts +9 -0
- package/dist/history-settings-tab.js +72 -0
- package/dist/history-settings.d.ts +14 -0
- package/dist/history-settings.js +47 -0
- package/dist/history-tracking.d.ts +4 -0
- package/dist/history-tracking.js +49 -0
- package/dist/history-view-type.d.ts +2 -0
- package/dist/history-view-type.js +2 -0
- package/dist/history-view.d.ts +12 -0
- package/dist/history-view.js +36 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/public-components.d.ts +2 -0
- package/dist/public-components.js +2 -0
- package/dist/register-history-settings.d.ts +105 -0
- package/dist/register-history-settings.js +199 -0
- package/dist/styles.css +2 -0
- package/manifest.json +27 -0
- package/package.json +72 -0
- package/styles.css +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
.history-panel {
|
|
2
|
+
--history-marker-col: 1.25rem;
|
|
3
|
+
--history-row-gap: 0.375rem;
|
|
4
|
+
--history-row-pad-y: 0.35rem;
|
|
5
|
+
--history-row-pad-x: 0.45rem;
|
|
6
|
+
--history-timeline-pad-x: 0.75rem;
|
|
7
|
+
--history-timeline-pad-top: 0.5rem;
|
|
8
|
+
--history-timeline-pad-bottom: 0.75rem;
|
|
9
|
+
display: flex;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
min-width: 0;
|
|
14
|
+
min-height: 0;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
color: var(--ui-workspace-view-foreground, inherit);
|
|
18
|
+
background: var(--ui-workspace-view-background, transparent);
|
|
19
|
+
font: inherit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.history-panel__chrome {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
padding: 0.25rem;
|
|
28
|
+
border-bottom: 1px solid
|
|
29
|
+
var(--ui-workspace-border, color-mix(in srgb, currentColor 14%, transparent));
|
|
30
|
+
background: var(--ui-workspace-view-background, transparent);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.history-panel__toolbar {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
gap: 0.125rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.history-panel__toolbar
|
|
41
|
+
button[data-ui-component="button"].history-panel__toolbar-action {
|
|
42
|
+
color: inherit;
|
|
43
|
+
background: transparent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.history-panel__toolbar
|
|
47
|
+
button[data-ui-component="button"].history-panel__toolbar-action:hover,
|
|
48
|
+
.history-panel__toolbar
|
|
49
|
+
button[data-ui-component="button"].history-panel__toolbar-action:focus-visible {
|
|
50
|
+
color: var(
|
|
51
|
+
--ui-workspace-panel-action-hover-foreground,
|
|
52
|
+
var(--sidebar-accent-foreground, inherit)
|
|
53
|
+
);
|
|
54
|
+
background: var(
|
|
55
|
+
--ui-workspace-panel-action-hover-background,
|
|
56
|
+
var(--sidebar-accent, #e3e3e3)
|
|
57
|
+
);
|
|
58
|
+
outline: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.history-panel__toolbar
|
|
62
|
+
button[data-ui-component="button"].history-panel__toolbar-action[aria-pressed="true"],
|
|
63
|
+
.history-panel__toolbar
|
|
64
|
+
button[data-ui-component="button"].history-panel__toolbar-action[aria-pressed="true"]:hover {
|
|
65
|
+
color: var(--ui-workspace-accent, #7c3aed);
|
|
66
|
+
background: var(
|
|
67
|
+
--ui-workspace-selection,
|
|
68
|
+
color-mix(in srgb, var(--ui-workspace-accent, #7c3aed) 14%, transparent)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.history-panel__empty {
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0.75rem;
|
|
75
|
+
color: var(--ui-workspace-muted-foreground, var(--muted-foreground));
|
|
76
|
+
font-size: 0.8125rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.history-panel__scroll {
|
|
80
|
+
min-height: 0;
|
|
81
|
+
flex: 1 1 auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.history-panel__timeline {
|
|
85
|
+
position: relative;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
gap: var(--history-row-gap);
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: var(--history-timeline-pad-top) var(--history-timeline-pad-x)
|
|
91
|
+
var(--history-timeline-pad-bottom);
|
|
92
|
+
list-style: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.history-panel__timeline::before {
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: calc(
|
|
98
|
+
var(--history-timeline-pad-top) + var(--history-row-pad-y) +
|
|
99
|
+
var(--history-marker-col) / 2
|
|
100
|
+
);
|
|
101
|
+
bottom: calc(
|
|
102
|
+
var(--history-timeline-pad-bottom) + var(--history-row-pad-y) +
|
|
103
|
+
var(--history-marker-col) / 2
|
|
104
|
+
);
|
|
105
|
+
left: calc(
|
|
106
|
+
var(--history-timeline-pad-x) + var(--history-row-pad-x) +
|
|
107
|
+
var(--history-marker-col) / 2
|
|
108
|
+
);
|
|
109
|
+
width: 1px;
|
|
110
|
+
background: var(
|
|
111
|
+
--ui-workspace-border,
|
|
112
|
+
color-mix(in srgb, currentColor 14%, transparent)
|
|
113
|
+
);
|
|
114
|
+
transform: translateX(-50%);
|
|
115
|
+
content: "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.history-panel__row {
|
|
119
|
+
display: flex;
|
|
120
|
+
width: 100%;
|
|
121
|
+
min-width: 0;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 0.65rem;
|
|
124
|
+
padding: var(--history-row-pad-y) var(--history-row-pad-x);
|
|
125
|
+
border: 0;
|
|
126
|
+
border-radius: 0.4rem;
|
|
127
|
+
color: inherit;
|
|
128
|
+
background: transparent;
|
|
129
|
+
font: inherit;
|
|
130
|
+
text-align: start;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.history-panel__row:hover,
|
|
135
|
+
.history-panel__row:focus-visible {
|
|
136
|
+
background: var(
|
|
137
|
+
--ui-workspace-selection,
|
|
138
|
+
color-mix(in srgb, var(--ui-workspace-accent, #7c3aed) 14%, transparent)
|
|
139
|
+
);
|
|
140
|
+
outline: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.history-panel__row[aria-current="true"] {
|
|
144
|
+
background: var(
|
|
145
|
+
--ui-workspace-selection,
|
|
146
|
+
color-mix(in srgb, var(--ui-workspace-accent, #7c3aed) 18%, transparent)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.history-panel__row[data-compare-anchor="true"] {
|
|
151
|
+
box-shadow: inset 0 0 0 1px
|
|
152
|
+
color-mix(in srgb, var(--ui-workspace-accent, #7c3aed) 45%, transparent);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.history-panel__marker {
|
|
156
|
+
position: relative;
|
|
157
|
+
z-index: 1;
|
|
158
|
+
flex: 0 0 var(--history-marker-col);
|
|
159
|
+
width: var(--history-marker-col);
|
|
160
|
+
height: var(--history-marker-col);
|
|
161
|
+
color: var(--ui-workspace-muted-foreground, currentColor);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.history-panel__marker path {
|
|
165
|
+
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.history-panel__row[aria-current="true"] .history-panel__marker,
|
|
169
|
+
.history-panel__row[data-compare-anchor="true"] .history-panel__marker {
|
|
170
|
+
color: var(--ui-workspace-accent, #7c3aed);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.history-panel__row[aria-current="true"] .history-panel__marker circle {
|
|
174
|
+
fill: currentColor;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.history-panel__meta {
|
|
178
|
+
display: flex;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
flex: 1 1 auto;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
gap: 0.1rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.history-panel__event {
|
|
186
|
+
font-size: 0.8125rem;
|
|
187
|
+
font-weight: 500;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.history-panel__time {
|
|
191
|
+
color: var(--ui-workspace-muted-foreground, var(--muted-foreground));
|
|
192
|
+
font-size: 0.75rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.history-panel__pin {
|
|
196
|
+
flex: 0 0 auto;
|
|
197
|
+
width: 0.9rem;
|
|
198
|
+
height: 0.9rem;
|
|
199
|
+
color: var(--ui-workspace-accent, #7c3aed);
|
|
200
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "./history-panel.css";
|
|
3
|
+
import { Menu, type App } from "@lapis-notes/api";
|
|
4
|
+
import { Button } from "@lapismd/design-core/shadcn/button";
|
|
5
|
+
import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
|
|
6
|
+
import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
|
|
7
|
+
import ArchiveRestore from "@lucide/svelte/icons/archive-restore";
|
|
8
|
+
import GitCommitVertical from "@lucide/svelte/icons/git-commit-vertical";
|
|
9
|
+
import GitCompare from "@lucide/svelte/icons/git-compare";
|
|
10
|
+
import GitCompareArrows from "@lucide/svelte/icons/git-compare-arrows";
|
|
11
|
+
import { onMount, untrack } from "svelte";
|
|
12
|
+
import {
|
|
13
|
+
formatHistoryEvent,
|
|
14
|
+
formatHistoryTimestamp,
|
|
15
|
+
} from "./history-format";
|
|
16
|
+
import type {
|
|
17
|
+
HistoryCompareAnchor,
|
|
18
|
+
HistoryCompareViewState,
|
|
19
|
+
HistoryRevision,
|
|
20
|
+
HistoryViewModel,
|
|
21
|
+
} from "./history-plugin";
|
|
22
|
+
import { HistoryCompareViewType } from "./history-view-type";
|
|
23
|
+
|
|
24
|
+
type HistoryPanelPlugin = {
|
|
25
|
+
getHistoryViewModel(): Promise<HistoryViewModel>;
|
|
26
|
+
getCompareAnchor(): HistoryCompareAnchor | null;
|
|
27
|
+
toggleCompareAnchor(filePath: string, revisionId: string): void;
|
|
28
|
+
openHistoryCompareView(state: HistoryCompareViewState): Promise<void>;
|
|
29
|
+
restoreRevision(filePath: string, revision: HistoryRevision): Promise<void>;
|
|
30
|
+
onHistoryChanged(listener: () => void): () => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let { app }: { app: App } = $props();
|
|
34
|
+
|
|
35
|
+
const plugin = untrack(() => {
|
|
36
|
+
const registered = app.plugins.plugins.get("history") as
|
|
37
|
+
| HistoryPanelPlugin
|
|
38
|
+
| undefined;
|
|
39
|
+
if (!registered) throw new Error("History plugin is not registered");
|
|
40
|
+
return registered;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let model = $state<HistoryViewModel>({
|
|
44
|
+
filePath: null,
|
|
45
|
+
history: null,
|
|
46
|
+
currentContent: "",
|
|
47
|
+
fileExists: false,
|
|
48
|
+
});
|
|
49
|
+
let anchor = $state<HistoryCompareAnchor | null>(null);
|
|
50
|
+
let openRevisionId = $state<string | null>(null);
|
|
51
|
+
let openCompareMode = $state<HistoryCompareViewState["compareMode"] | null>(
|
|
52
|
+
null,
|
|
53
|
+
);
|
|
54
|
+
let selectedRevisionId = $state<string | null>(null);
|
|
55
|
+
|
|
56
|
+
const selectedRevision = $derived(
|
|
57
|
+
model.history?.revisions.find(
|
|
58
|
+
(revision) => revision.revisionId === selectedRevisionId,
|
|
59
|
+
) ?? null,
|
|
60
|
+
);
|
|
61
|
+
const previousRevision = $derived.by(() => {
|
|
62
|
+
if (!selectedRevision || !model.history) return undefined;
|
|
63
|
+
const index = model.history.revisions.findIndex(
|
|
64
|
+
(entry) => entry.revisionId === selectedRevision.revisionId,
|
|
65
|
+
);
|
|
66
|
+
return index >= 0 ? model.history.revisions[index + 1] : undefined;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
async function refresh(): Promise<void> {
|
|
70
|
+
model = await plugin.getHistoryViewModel();
|
|
71
|
+
anchor = plugin.getCompareAnchor();
|
|
72
|
+
const leaf = app.workspace.getLeavesOfType(HistoryCompareViewType)[0];
|
|
73
|
+
const state = leaf?.view.getState() ?? {};
|
|
74
|
+
openRevisionId =
|
|
75
|
+
typeof state["revisionId"] === "string" ? state["revisionId"] : null;
|
|
76
|
+
const mode = state["compareMode"];
|
|
77
|
+
openCompareMode =
|
|
78
|
+
mode === "current" || mode === "previous" || mode === "selected"
|
|
79
|
+
? mode
|
|
80
|
+
: null;
|
|
81
|
+
const revisionIds = new Set(
|
|
82
|
+
model.history?.revisions.map((revision) => revision.revisionId) ?? [],
|
|
83
|
+
);
|
|
84
|
+
if (openRevisionId && revisionIds.has(openRevisionId)) {
|
|
85
|
+
selectedRevisionId = openRevisionId;
|
|
86
|
+
} else if (!selectedRevisionId || !revisionIds.has(selectedRevisionId)) {
|
|
87
|
+
selectedRevisionId = model.history?.revisions[0]?.revisionId ?? null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function openCompare(
|
|
92
|
+
revision: HistoryRevision,
|
|
93
|
+
compareMode: "current" | "previous" | "selected",
|
|
94
|
+
otherRevisionId?: string,
|
|
95
|
+
): void {
|
|
96
|
+
if (!model.filePath) return;
|
|
97
|
+
selectedRevisionId = revision.revisionId;
|
|
98
|
+
void plugin.openHistoryCompareView({
|
|
99
|
+
filePath: model.filePath,
|
|
100
|
+
revisionId: revision.revisionId,
|
|
101
|
+
compareMode,
|
|
102
|
+
otherRevisionId,
|
|
103
|
+
sourceLeafId: app.workspace.activeLeaf?.id,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function showRevisionMenu(event: MouseEvent, revision: HistoryRevision): void {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
const previous = model.history?.revisions[
|
|
110
|
+
model.history.revisions.findIndex(
|
|
111
|
+
(entry) => entry.revisionId === revision.revisionId,
|
|
112
|
+
) + 1
|
|
113
|
+
];
|
|
114
|
+
const canCompareSelected = Boolean(
|
|
115
|
+
anchor &&
|
|
116
|
+
anchor.filePath === model.filePath &&
|
|
117
|
+
anchor.revisionId !== revision.revisionId,
|
|
118
|
+
);
|
|
119
|
+
new Menu()
|
|
120
|
+
.addItem((item) =>
|
|
121
|
+
item.setTitle("Compare with current").onClick(() => {
|
|
122
|
+
openCompare(revision, "current");
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
.addItem((item) =>
|
|
126
|
+
item
|
|
127
|
+
.setTitle("Compare with previous")
|
|
128
|
+
.setDisabled(!previous)
|
|
129
|
+
.onClick(() => {
|
|
130
|
+
if (previous) openCompare(revision, "previous");
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
.addSeparator()
|
|
134
|
+
.addItem((item) =>
|
|
135
|
+
item.setTitle("Select for compare").onClick(() => {
|
|
136
|
+
if (!model.filePath) return;
|
|
137
|
+
plugin.toggleCompareAnchor(model.filePath, revision.revisionId);
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
.addItem((item) =>
|
|
141
|
+
item
|
|
142
|
+
.setTitle("Compare with selected")
|
|
143
|
+
.setDisabled(!canCompareSelected)
|
|
144
|
+
.onClick(() => {
|
|
145
|
+
if (!anchor) return;
|
|
146
|
+
openCompare(revision, "selected", anchor.revisionId);
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
.addSeparator()
|
|
150
|
+
.addItem((item) =>
|
|
151
|
+
item.setTitle("Restore this revision").onClick(() => {
|
|
152
|
+
if (!model.filePath) return;
|
|
153
|
+
void plugin.restoreRevision(model.filePath, revision);
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
.showAtMouseEvent(event);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onMount(() => {
|
|
160
|
+
const unsubscribe = plugin.onHistoryChanged(() => void refresh());
|
|
161
|
+
const leafChange = app.workspace.on("active-leaf-change", () => {
|
|
162
|
+
void refresh();
|
|
163
|
+
});
|
|
164
|
+
const layoutChange = app.workspace.on("layout-change", () => {
|
|
165
|
+
void refresh();
|
|
166
|
+
});
|
|
167
|
+
void refresh();
|
|
168
|
+
return () => {
|
|
169
|
+
unsubscribe();
|
|
170
|
+
app.workspace.offref(leafChange);
|
|
171
|
+
app.workspace.offref(layoutChange);
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<div
|
|
177
|
+
class="history-panel"
|
|
178
|
+
data-ui-component="history-panel"
|
|
179
|
+
data-testid="history-panel"
|
|
180
|
+
>
|
|
181
|
+
<div class="history-panel__chrome" data-ui-part="chrome">
|
|
182
|
+
<Tooltip.Provider delayDuration={0}>
|
|
183
|
+
<div class="history-panel__toolbar" data-ui-part="toolbar">
|
|
184
|
+
<Tooltip.Root>
|
|
185
|
+
<Tooltip.Trigger>
|
|
186
|
+
{#snippet child({ props })}
|
|
187
|
+
<Button
|
|
188
|
+
{...props}
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="icon-sm"
|
|
191
|
+
class="history-panel__toolbar-action"
|
|
192
|
+
aria-label="Compare with current"
|
|
193
|
+
aria-pressed={openCompareMode === "current"}
|
|
194
|
+
disabled={!selectedRevision}
|
|
195
|
+
onclick={() => {
|
|
196
|
+
if (selectedRevision) openCompare(selectedRevision, "current");
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<GitCompare aria-hidden="true" />
|
|
200
|
+
</Button>
|
|
201
|
+
{/snippet}
|
|
202
|
+
</Tooltip.Trigger>
|
|
203
|
+
<Tooltip.Content side="bottom">Compare with current</Tooltip.Content>
|
|
204
|
+
</Tooltip.Root>
|
|
205
|
+
<Tooltip.Root>
|
|
206
|
+
<Tooltip.Trigger>
|
|
207
|
+
{#snippet child({ props })}
|
|
208
|
+
<Button
|
|
209
|
+
{...props}
|
|
210
|
+
variant="ghost"
|
|
211
|
+
size="icon-sm"
|
|
212
|
+
class="history-panel__toolbar-action"
|
|
213
|
+
aria-label="Compare with previous"
|
|
214
|
+
aria-pressed={openCompareMode === "previous"}
|
|
215
|
+
disabled={!previousRevision}
|
|
216
|
+
onclick={() => {
|
|
217
|
+
if (selectedRevision && previousRevision) {
|
|
218
|
+
openCompare(selectedRevision, "previous");
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<GitCompareArrows aria-hidden="true" />
|
|
223
|
+
</Button>
|
|
224
|
+
{/snippet}
|
|
225
|
+
</Tooltip.Trigger>
|
|
226
|
+
<Tooltip.Content side="bottom">Compare with previous</Tooltip.Content>
|
|
227
|
+
</Tooltip.Root>
|
|
228
|
+
<Tooltip.Root>
|
|
229
|
+
<Tooltip.Trigger>
|
|
230
|
+
{#snippet child({ props })}
|
|
231
|
+
<Button
|
|
232
|
+
{...props}
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="icon-sm"
|
|
235
|
+
class="history-panel__toolbar-action"
|
|
236
|
+
aria-label="Restore this revision"
|
|
237
|
+
disabled={!selectedRevision || !model.filePath}
|
|
238
|
+
onclick={() => {
|
|
239
|
+
if (!model.filePath || !selectedRevision) return;
|
|
240
|
+
void plugin.restoreRevision(model.filePath, selectedRevision);
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<ArchiveRestore aria-hidden="true" />
|
|
244
|
+
</Button>
|
|
245
|
+
{/snippet}
|
|
246
|
+
</Tooltip.Trigger>
|
|
247
|
+
<Tooltip.Content side="bottom">Restore this revision</Tooltip.Content>
|
|
248
|
+
</Tooltip.Root>
|
|
249
|
+
</div>
|
|
250
|
+
</Tooltip.Provider>
|
|
251
|
+
</div>
|
|
252
|
+
{#if !model.filePath}
|
|
253
|
+
<p class="history-panel__empty">Open a file to see its history.</p>
|
|
254
|
+
{:else if !model.history?.revisions.length}
|
|
255
|
+
<p class="history-panel__empty">No revisions stored for this file yet.</p>
|
|
256
|
+
{:else}
|
|
257
|
+
<ScrollArea class="history-panel__scroll">
|
|
258
|
+
<ol class="history-panel__timeline" aria-label="File history">
|
|
259
|
+
{#each model.history.revisions as revision (revision.revisionId)}
|
|
260
|
+
<li>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
class="history-panel__row"
|
|
264
|
+
data-revision-id={revision.revisionId}
|
|
265
|
+
data-compare-anchor={anchor?.revisionId === revision.revisionId
|
|
266
|
+
? "true"
|
|
267
|
+
: undefined}
|
|
268
|
+
aria-current={selectedRevisionId === revision.revisionId
|
|
269
|
+
? "true"
|
|
270
|
+
: undefined}
|
|
271
|
+
aria-label={`${formatHistoryEvent(revision.eventType)} ${formatHistoryTimestamp(revision.createdAt)}`}
|
|
272
|
+
onclick={() => openCompare(revision, "current")}
|
|
273
|
+
oncontextmenu={(event) => showRevisionMenu(event, revision)}
|
|
274
|
+
>
|
|
275
|
+
<GitCommitVertical
|
|
276
|
+
class="history-panel__marker"
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
/>
|
|
279
|
+
<span class="history-panel__meta">
|
|
280
|
+
<span class="history-panel__event"
|
|
281
|
+
>{formatHistoryEvent(revision.eventType)}</span
|
|
282
|
+
>
|
|
283
|
+
<time
|
|
284
|
+
class="history-panel__time"
|
|
285
|
+
datetime={new Date(revision.createdAt).toISOString()}
|
|
286
|
+
>{formatHistoryTimestamp(revision.createdAt)}</time
|
|
287
|
+
>
|
|
288
|
+
</span>
|
|
289
|
+
{#if anchor?.revisionId === revision.revisionId}
|
|
290
|
+
<GitCompare
|
|
291
|
+
class="history-panel__pin"
|
|
292
|
+
data-testid="history-compare-anchor"
|
|
293
|
+
aria-hidden="true"
|
|
294
|
+
/>
|
|
295
|
+
{/if}
|
|
296
|
+
</button>
|
|
297
|
+
</li>
|
|
298
|
+
{/each}
|
|
299
|
+
</ol>
|
|
300
|
+
</ScrollArea>
|
|
301
|
+
{/if}
|
|
302
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./history-panel.css";
|
|
2
|
+
import { type App } from "@lapis-notes/api";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
app: App;
|
|
5
|
+
};
|
|
6
|
+
declare const HistoryPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type HistoryPanel = ReturnType<typeof HistoryPanel>;
|
|
8
|
+
export default HistoryPanel;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Plugin, TFile, type App, type PluginManifest } from "@lapis-notes/api";
|
|
2
|
+
import { type HistoryPluginSettings, type HistoryPluginSettingsPatch } from "./history-settings";
|
|
3
|
+
export declare const HISTORY_PLUGIN_ID = "history";
|
|
4
|
+
export type HistoryCaptureEventType = "baseline" | "create" | "modify" | "rename" | "delete" | "restore";
|
|
5
|
+
export type HistoryCompareMode = "current" | "previous" | "selected";
|
|
6
|
+
export interface HistoryRevision {
|
|
7
|
+
revisionId: string;
|
|
8
|
+
path: string;
|
|
9
|
+
previousPath?: string;
|
|
10
|
+
eventType: HistoryCaptureEventType;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
sourceMtime?: number;
|
|
13
|
+
sourceSize?: number;
|
|
14
|
+
contentHash: string;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
export interface HistoryFileHistory {
|
|
18
|
+
file: {
|
|
19
|
+
id: string;
|
|
20
|
+
path: string;
|
|
21
|
+
deleted: boolean;
|
|
22
|
+
};
|
|
23
|
+
revisions: HistoryRevision[];
|
|
24
|
+
}
|
|
25
|
+
export interface HistoryViewModel {
|
|
26
|
+
filePath: string | null;
|
|
27
|
+
history: HistoryFileHistory | null;
|
|
28
|
+
currentContent: string;
|
|
29
|
+
fileExists: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface HistoryCompareViewState {
|
|
32
|
+
filePath: string;
|
|
33
|
+
revisionId: string;
|
|
34
|
+
compareMode: HistoryCompareMode;
|
|
35
|
+
otherRevisionId?: string;
|
|
36
|
+
sourceLeafId?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface HistoryComparisonModel {
|
|
39
|
+
filePath: string;
|
|
40
|
+
history: HistoryFileHistory;
|
|
41
|
+
currentContent: string;
|
|
42
|
+
fileExists: boolean;
|
|
43
|
+
compareMode: HistoryCompareMode;
|
|
44
|
+
selectedRevision: HistoryRevision;
|
|
45
|
+
previousRevision: HistoryRevision | null;
|
|
46
|
+
otherRevision: HistoryRevision | null;
|
|
47
|
+
compareBaseText: string;
|
|
48
|
+
compareNewText: string;
|
|
49
|
+
beforeLabel: string;
|
|
50
|
+
afterLabel: string;
|
|
51
|
+
}
|
|
52
|
+
export interface HistoryCompareAnchor {
|
|
53
|
+
filePath: string;
|
|
54
|
+
revisionId: string;
|
|
55
|
+
}
|
|
56
|
+
export declare class HistoryPlugin extends Plugin {
|
|
57
|
+
private settings;
|
|
58
|
+
private currentFilePath;
|
|
59
|
+
private compareAnchor;
|
|
60
|
+
private readonly pendingCaptures;
|
|
61
|
+
private readonly lastModifyStoredAt;
|
|
62
|
+
private readonly suppressedHashes;
|
|
63
|
+
private readonly listeners;
|
|
64
|
+
constructor(app: App, manifest?: PluginManifest);
|
|
65
|
+
onload(): Promise<void>;
|
|
66
|
+
getSettings(): HistoryPluginSettings;
|
|
67
|
+
updateSettings(patch: HistoryPluginSettingsPatch): Promise<void>;
|
|
68
|
+
onHistoryChanged(listener: () => void): () => void;
|
|
69
|
+
getCompareAnchor(): HistoryCompareAnchor | null;
|
|
70
|
+
setCompareAnchor(anchor: HistoryCompareAnchor | null): void;
|
|
71
|
+
toggleCompareAnchor(filePath: string, revisionId: string): void;
|
|
72
|
+
openHistoryView(filePath?: string): Promise<void>;
|
|
73
|
+
openHistoryCompareView(state: HistoryCompareViewState): Promise<void>;
|
|
74
|
+
closeHistoryCompareView(sourceLeafId?: string): void;
|
|
75
|
+
getHistoryViewModel(): Promise<HistoryViewModel>;
|
|
76
|
+
getHistoryComparisonModel(state: HistoryCompareViewState): Promise<HistoryComparisonModel>;
|
|
77
|
+
restoreRevision(filePath: string, revision: HistoryRevision): Promise<void>;
|
|
78
|
+
saveCurrentFileContent(filePath: string, content: string): Promise<void>;
|
|
79
|
+
getFocusedFile(): TFile | null;
|
|
80
|
+
private setCurrentFilePath;
|
|
81
|
+
private scheduleCapture;
|
|
82
|
+
private ensureBaselineForFile;
|
|
83
|
+
private captureSnapshot;
|
|
84
|
+
private captureRename;
|
|
85
|
+
private captureDelete;
|
|
86
|
+
private notify;
|
|
87
|
+
}
|
|
88
|
+
export default HistoryPlugin;
|