@lapis-notes/workspace 0.2.1 → 0.3.1
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 +123 -12
- package/README.md +6 -2
- package/dist/WorkspaceShell.svelte +19 -2
- package/dist/WorkspaceShell.svelte.d.ts +3 -1
- package/dist/desktop/DesktopRecoveryBanner.css +46 -0
- package/dist/desktop/DesktopRecoveryBanner.svelte +70 -0
- package/dist/desktop/DesktopRecoveryBanner.svelte.d.ts +15 -0
- package/dist/desktop/DesktopVaultLauncher.svelte +278 -0
- package/dist/desktop/DesktopVaultLauncher.svelte.d.ts +17 -0
- package/dist/desktop/DesktopVaultLauncherDialogs.svelte +136 -0
- package/dist/desktop/DesktopVaultLauncherDialogs.svelte.d.ts +9 -0
- package/dist/desktop/DesktopWorkspaceStartup.css +8 -0
- package/dist/desktop/DesktopWorkspaceStartup.svelte +34 -0
- package/dist/desktop/DesktopWorkspaceStartup.svelte.d.ts +9 -0
- package/dist/desktop/desktop-vault-launcher-controls.css +142 -0
- package/dist/desktop/desktop-vault-launcher.css +181 -0
- package/dist/desktop/desktop-vault-launcher.d.ts +30 -0
- package/dist/desktop/desktop-vault-launcher.js +26 -0
- package/dist/desktop/desktop-vault-launcher.svelte.d.ts +36 -0
- package/dist/desktop/desktop-vault-launcher.svelte.js +140 -0
- package/dist/desktop/lapis-logo.svg +471 -0
- package/dist/file-conflicts/ConflictReview.svelte +153 -0
- package/dist/file-conflicts/ConflictReview.svelte.d.ts +8 -0
- package/dist/file-conflicts/ConflictReviewControls.svelte +66 -0
- package/dist/file-conflicts/ConflictReviewControls.svelte.d.ts +12 -0
- package/dist/file-conflicts/conflict-review-state.svelte.d.ts +22 -0
- package/dist/file-conflicts/conflict-review-state.svelte.js +59 -0
- package/dist/file-conflicts/conflict-review-view.d.ts +19 -0
- package/dist/file-conflicts/conflict-review-view.js +50 -0
- package/dist/file-conflicts/register-conflict-reviews.d.ts +7 -0
- package/dist/file-conflicts/register-conflict-reviews.js +132 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/plugin-management/CorePluginsSettingsPage.svelte +47 -16
- package/dist/plugin-management/PluginBrowser.svelte +16 -1
- package/dist/plugin-management/PluginRegistryToolbar.svelte.d.ts +1 -1
- package/dist/plugin-management/plugin-registry-controller.svelte.js +5 -1
- package/dist/plugin-management/register-plugin-management-settings.js +7 -6
- package/package.json +5 -4
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MergeEditor } from "@lapismd/design-core/diff";
|
|
3
|
+
import { Button } from "@lapismd/design-core/shadcn/button";
|
|
4
|
+
import FileCheckIcon from "@lucide/svelte/icons/file-check";
|
|
5
|
+
import Undo2Icon from "@lucide/svelte/icons/undo-2";
|
|
6
|
+
import ConflictReviewControls from "./ConflictReviewControls.svelte";
|
|
7
|
+
import RefreshCwIcon from "@lucide/svelte/icons/refresh-cw";
|
|
8
|
+
import type {
|
|
9
|
+
ConflictReviewState,
|
|
10
|
+
ConflictSelection,
|
|
11
|
+
} from "./conflict-review-state.svelte";
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
model,
|
|
15
|
+
selection,
|
|
16
|
+
}: { model: ConflictReviewState; selection: ConflictSelection } = $props();
|
|
17
|
+
const conflict = $derived(
|
|
18
|
+
model.entries.find((entry) => entry.path === selection.path),
|
|
19
|
+
);
|
|
20
|
+
let options = $state({
|
|
21
|
+
wrap: false,
|
|
22
|
+
ignoreWhitespace: false,
|
|
23
|
+
ignoreCase: false,
|
|
24
|
+
syncHorizontalScroll: true,
|
|
25
|
+
});
|
|
26
|
+
const disabled = $derived(model.disabled() || model.busy !== null);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if conflict}
|
|
30
|
+
<section
|
|
31
|
+
class="conflict-review"
|
|
32
|
+
aria-label={`File conflict: ${conflict.path}`}
|
|
33
|
+
aria-busy={model.busy === conflict.path}
|
|
34
|
+
>
|
|
35
|
+
<div class="comparison">
|
|
36
|
+
{#key conflict}
|
|
37
|
+
{@const seed = selection.seed(conflict)}
|
|
38
|
+
<MergeEditor
|
|
39
|
+
path={conflict.path}
|
|
40
|
+
left={conflict.local}
|
|
41
|
+
base={seed}
|
|
42
|
+
right={conflict.external ?? ""}
|
|
43
|
+
leftLabel="Your version"
|
|
44
|
+
baseLabel="Merged version"
|
|
45
|
+
rightLabel="Disk version"
|
|
46
|
+
readOnly={disabled || conflict.external === undefined}
|
|
47
|
+
editable
|
|
48
|
+
editableSides={["base"]}
|
|
49
|
+
wrap={options.wrap}
|
|
50
|
+
ignoreWhitespace={options.ignoreWhitespace}
|
|
51
|
+
ignoreCase={options.ignoreCase}
|
|
52
|
+
syncHorizontalScroll={options.syncHorizontalScroll}
|
|
53
|
+
onResolvedChange={({ content }) => {
|
|
54
|
+
selection.result = content;
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{#snippet headerCenter()}
|
|
58
|
+
<ConflictReviewControls bind:options {disabled} />
|
|
59
|
+
{/snippet}
|
|
60
|
+
</MergeEditor>
|
|
61
|
+
{/key}
|
|
62
|
+
</div>
|
|
63
|
+
<footer>
|
|
64
|
+
<div class="explanation">
|
|
65
|
+
{#if model.error}<p role="alert">{model.error}</p>{/if}
|
|
66
|
+
{#if conflict.external === undefined}
|
|
67
|
+
<p role="status">
|
|
68
|
+
The disk version could not be read: {conflict.readError}
|
|
69
|
+
</p>
|
|
70
|
+
{:else if conflict.external === null}
|
|
71
|
+
<p>The file was deleted from disk.</p>
|
|
72
|
+
{:else}
|
|
73
|
+
<p>
|
|
74
|
+
Edit the centre or accept changes from either side, then save the
|
|
75
|
+
merged version.
|
|
76
|
+
</p>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
<div class="actions">
|
|
80
|
+
<Button
|
|
81
|
+
variant="ghost"
|
|
82
|
+
disabled={disabled || conflict.external === undefined}
|
|
83
|
+
onclick={() => model.recover(conflict.path, "use-external")}
|
|
84
|
+
>
|
|
85
|
+
<Undo2Icon aria-hidden="true" />Use disk version
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
variant="outline"
|
|
89
|
+
disabled={disabled ||
|
|
90
|
+
conflict.external === undefined ||
|
|
91
|
+
selection.result === undefined}
|
|
92
|
+
onclick={() =>
|
|
93
|
+
model.recover(conflict.path, undefined, selection.result)}
|
|
94
|
+
>
|
|
95
|
+
<FileCheckIcon aria-hidden="true" />Save merged version
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost"
|
|
99
|
+
{disabled}
|
|
100
|
+
onclick={() => model.recover(conflict.path)}
|
|
101
|
+
>
|
|
102
|
+
<RefreshCwIcon aria-hidden="true" />Check disk again
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
</footer>
|
|
106
|
+
</section>
|
|
107
|
+
{/if}
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.conflict-review {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
height: 100%;
|
|
114
|
+
min-height: 0;
|
|
115
|
+
color: var(--ui-workspace-view-foreground, var(--foreground));
|
|
116
|
+
background: var(--ui-workspace-view-background, var(--background));
|
|
117
|
+
}
|
|
118
|
+
.comparison {
|
|
119
|
+
--ui-diff-frame-border: 0;
|
|
120
|
+
--ui-diff-frame-radius: 0;
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-height: 0;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
footer {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
gap: 0.75rem;
|
|
130
|
+
padding: 0.75rem 1rem;
|
|
131
|
+
border-top: 1px solid var(--ui-workspace-border);
|
|
132
|
+
background: var(--ui-workspace-background, var(--background));
|
|
133
|
+
}
|
|
134
|
+
.explanation {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-width: 12rem;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
}
|
|
139
|
+
p {
|
|
140
|
+
margin: 0;
|
|
141
|
+
}
|
|
142
|
+
.actions {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
gap: 0.5rem;
|
|
146
|
+
}
|
|
147
|
+
[role="alert"] {
|
|
148
|
+
color: var(--destructive);
|
|
149
|
+
}
|
|
150
|
+
:global(.dark) [role="alert"] {
|
|
151
|
+
color: color-mix(in srgb, var(--destructive) 60%, var(--foreground));
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConflictReviewState, ConflictSelection } from "./conflict-review-state.svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
model: ConflictReviewState;
|
|
4
|
+
selection: ConflictSelection;
|
|
5
|
+
};
|
|
6
|
+
declare const ConflictReview: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type ConflictReview = ReturnType<typeof ConflictReview>;
|
|
8
|
+
export default ConflictReview;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Toggle } from "@lapismd/design-core/shadcn/toggle";
|
|
3
|
+
import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
|
|
4
|
+
import WrapText from "@lucide/svelte/icons/wrap-text";
|
|
5
|
+
import Space from "@lucide/svelte/icons/space";
|
|
6
|
+
import CaseSensitive from "@lucide/svelte/icons/case-sensitive";
|
|
7
|
+
import ArrowRightLeft from "@lucide/svelte/icons/arrow-right-left";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
options = $bindable(),
|
|
11
|
+
disabled = false,
|
|
12
|
+
}: {
|
|
13
|
+
options: {
|
|
14
|
+
wrap: boolean;
|
|
15
|
+
ignoreWhitespace: boolean;
|
|
16
|
+
ignoreCase: boolean;
|
|
17
|
+
syncHorizontalScroll: boolean;
|
|
18
|
+
};
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
} = $props();
|
|
21
|
+
const actions = [
|
|
22
|
+
{ key: "wrap", label: "Wrap text", icon: WrapText },
|
|
23
|
+
{ key: "ignoreWhitespace", label: "Ignore whitespace", icon: Space },
|
|
24
|
+
{ key: "ignoreCase", label: "Ignore case", icon: CaseSensitive },
|
|
25
|
+
{
|
|
26
|
+
key: "syncHorizontalScroll",
|
|
27
|
+
label: "Synchronize horizontal scrolling",
|
|
28
|
+
icon: ArrowRightLeft,
|
|
29
|
+
},
|
|
30
|
+
] as const;
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="merge-controls" role="group" aria-label="Merge editor settings">
|
|
34
|
+
<Tooltip.Provider>
|
|
35
|
+
{#each actions as action (action.key)}
|
|
36
|
+
<Tooltip.Root>
|
|
37
|
+
<Tooltip.Trigger>
|
|
38
|
+
{#snippet child({ props })}
|
|
39
|
+
<Toggle
|
|
40
|
+
{...props}
|
|
41
|
+
size="sm"
|
|
42
|
+
{disabled}
|
|
43
|
+
aria-label={action.label}
|
|
44
|
+
pressed={options[action.key]}
|
|
45
|
+
onPressedChange={(pressed) =>
|
|
46
|
+
(options = { ...options, [action.key]: pressed })}
|
|
47
|
+
>
|
|
48
|
+
<action.icon aria-hidden="true" />
|
|
49
|
+
</Toggle>
|
|
50
|
+
{/snippet}
|
|
51
|
+
</Tooltip.Trigger>
|
|
52
|
+
<Tooltip.Content side="bottom">{action.label}</Tooltip.Content>
|
|
53
|
+
</Tooltip.Root>
|
|
54
|
+
{/each}
|
|
55
|
+
</Tooltip.Provider>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.merge-controls {
|
|
60
|
+
--spacing: 0.1875rem;
|
|
61
|
+
--muted: color-mix(in srgb, var(--primary) 22%, var(--background));
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.125rem;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
options: {
|
|
3
|
+
wrap: boolean;
|
|
4
|
+
ignoreWhitespace: boolean;
|
|
5
|
+
ignoreCase: boolean;
|
|
6
|
+
syncHorizontalScroll: boolean;
|
|
7
|
+
};
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const ConflictReviewControls: import("svelte").Component<$$ComponentProps, {}, "options">;
|
|
11
|
+
type ConflictReviewControls = ReturnType<typeof ConflictReviewControls>;
|
|
12
|
+
export default ConflictReviewControls;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
2
|
+
import type { FileTextConflict, FileTextConflictRegistry, FileTextResolution } from "@lapis-notes/api";
|
|
3
|
+
export declare class ConflictSelection {
|
|
4
|
+
#private;
|
|
5
|
+
private readonly results;
|
|
6
|
+
path: string;
|
|
7
|
+
constructor(results: SvelteMap<string, string>);
|
|
8
|
+
get result(): string | undefined;
|
|
9
|
+
set result(value: string | undefined);
|
|
10
|
+
seed(conflict: FileTextConflict): string;
|
|
11
|
+
}
|
|
12
|
+
/** Presentation state only; every recovery still uses the writer-owned decision. */
|
|
13
|
+
export declare class ConflictReviewState {
|
|
14
|
+
readonly registry: FileTextConflictRegistry;
|
|
15
|
+
readonly disabled: () => boolean;
|
|
16
|
+
readonly results: SvelteMap<string, string>;
|
|
17
|
+
entries: readonly FileTextConflict[];
|
|
18
|
+
busy: string | null;
|
|
19
|
+
error: string;
|
|
20
|
+
constructor(registry: FileTextConflictRegistry, disabled: () => boolean);
|
|
21
|
+
recover(path: string, choice?: FileTextResolution, merged?: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
2
|
+
export class ConflictSelection {
|
|
3
|
+
results;
|
|
4
|
+
path = $state("");
|
|
5
|
+
constructor(results) {
|
|
6
|
+
this.results = results;
|
|
7
|
+
}
|
|
8
|
+
get result() {
|
|
9
|
+
return this.results.get(this.path);
|
|
10
|
+
}
|
|
11
|
+
set result(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
this.results.delete(this.path);
|
|
14
|
+
else
|
|
15
|
+
this.results.set(this.path, value);
|
|
16
|
+
}
|
|
17
|
+
#seeds = new WeakMap();
|
|
18
|
+
seed(conflict) {
|
|
19
|
+
if (!this.#seeds.has(conflict))
|
|
20
|
+
this.#seeds.set(conflict, this.result ?? conflict.baseline ?? "");
|
|
21
|
+
return this.#seeds.get(conflict);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Presentation state only; every recovery still uses the writer-owned decision. */
|
|
25
|
+
export class ConflictReviewState {
|
|
26
|
+
registry;
|
|
27
|
+
disabled;
|
|
28
|
+
results = new SvelteMap();
|
|
29
|
+
entries = $state.raw([]);
|
|
30
|
+
busy = $state(null);
|
|
31
|
+
error = $state("");
|
|
32
|
+
constructor(registry, disabled) {
|
|
33
|
+
this.registry = registry;
|
|
34
|
+
this.disabled = disabled;
|
|
35
|
+
}
|
|
36
|
+
async recover(path, choice, merged) {
|
|
37
|
+
if (this.disabled() || this.busy !== null)
|
|
38
|
+
return;
|
|
39
|
+
const conflict = this.entries.find((entry) => entry.path === path);
|
|
40
|
+
if (!conflict)
|
|
41
|
+
return;
|
|
42
|
+
this.busy = path;
|
|
43
|
+
this.error = "";
|
|
44
|
+
try {
|
|
45
|
+
if (merged !== undefined)
|
|
46
|
+
await conflict.resolveMerged(merged);
|
|
47
|
+
else if (choice === undefined)
|
|
48
|
+
await conflict.refresh();
|
|
49
|
+
else
|
|
50
|
+
await conflict.resolve(choice);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
this.busy = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { View, type WorkspaceLeaf } from "@lapis-notes/api";
|
|
2
|
+
import { ConflictSelection, type ConflictReviewState } from "./conflict-review-state.svelte";
|
|
3
|
+
export declare const CONFLICT_REVIEW_VIEW = "desktop:file-conflict";
|
|
4
|
+
export declare class ConflictReviewView extends View {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly model;
|
|
7
|
+
readonly selection: ConflictSelection;
|
|
8
|
+
constructor(leaf: WorkspaceLeaf, model: ConflictReviewState);
|
|
9
|
+
getViewType(): string;
|
|
10
|
+
getDisplayText(): string;
|
|
11
|
+
getIcon(): string;
|
|
12
|
+
getClosePersistenceScope(): "layout";
|
|
13
|
+
getState(): Record<string, unknown>;
|
|
14
|
+
setState(state: Record<string, unknown>): Promise<void>;
|
|
15
|
+
protected onOpen(): Promise<void>;
|
|
16
|
+
protected onClose(): Promise<void>;
|
|
17
|
+
onload(): void;
|
|
18
|
+
onunload(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { View } from "@lapis-notes/api";
|
|
2
|
+
import { mount, unmount } from "svelte";
|
|
3
|
+
import ConflictReview from "./ConflictReview.svelte";
|
|
4
|
+
import { ConflictSelection, } from "./conflict-review-state.svelte";
|
|
5
|
+
export const CONFLICT_REVIEW_VIEW = "desktop:file-conflict";
|
|
6
|
+
export class ConflictReviewView extends View {
|
|
7
|
+
model;
|
|
8
|
+
selection;
|
|
9
|
+
#component;
|
|
10
|
+
constructor(leaf, model) {
|
|
11
|
+
super(leaf);
|
|
12
|
+
this.model = model;
|
|
13
|
+
this.selection = new ConflictSelection(model.results);
|
|
14
|
+
}
|
|
15
|
+
getViewType() {
|
|
16
|
+
return CONFLICT_REVIEW_VIEW;
|
|
17
|
+
}
|
|
18
|
+
getDisplayText() {
|
|
19
|
+
return this.selection.path
|
|
20
|
+
? `Resolve ${this.selection.path}`
|
|
21
|
+
: "File conflict";
|
|
22
|
+
}
|
|
23
|
+
getIcon() {
|
|
24
|
+
return "git-compare-arrows";
|
|
25
|
+
}
|
|
26
|
+
getClosePersistenceScope() {
|
|
27
|
+
return "layout";
|
|
28
|
+
}
|
|
29
|
+
getState() {
|
|
30
|
+
return { path: this.selection.path };
|
|
31
|
+
}
|
|
32
|
+
async setState(state) {
|
|
33
|
+
const path = typeof state.path === "string" ? state.path : "";
|
|
34
|
+
this.selection.path = path;
|
|
35
|
+
await super.setState(state);
|
|
36
|
+
}
|
|
37
|
+
async onOpen() { }
|
|
38
|
+
async onClose() { }
|
|
39
|
+
onload() {
|
|
40
|
+
this.#component = mount(ConflictReview, {
|
|
41
|
+
target: this.containerEl,
|
|
42
|
+
props: { model: this.model, selection: this.selection },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
onunload() {
|
|
46
|
+
if (this.#component)
|
|
47
|
+
void unmount(this.#component);
|
|
48
|
+
this.#component = undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { App } from "@lapis-notes/api";
|
|
2
|
+
export interface FileConflictReviewOptions {
|
|
3
|
+
/** Host-owned leave/startup inhibition, read on every action and render. */
|
|
4
|
+
disabled?: () => boolean;
|
|
5
|
+
}
|
|
6
|
+
/** One presentation owner per App, shared by early host and mounted shell leases. */
|
|
7
|
+
export declare function bindFileConflictReviews(app: App, options?: FileConflictReviewOptions): () => void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SvelteSet } from "svelte/reactivity";
|
|
2
|
+
import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
|
|
3
|
+
import { CONFLICT_REVIEW_VIEW, ConflictReviewView, } from "./conflict-review-view";
|
|
4
|
+
import { ConflictReviewState } from "./conflict-review-state.svelte";
|
|
5
|
+
const prefix = "lapis-file-conflict:";
|
|
6
|
+
const bindings = new WeakMap();
|
|
7
|
+
/** One presentation owner per App, shared by early host and mounted shell leases. */
|
|
8
|
+
export function bindFileConflictReviews(app, options = {}) {
|
|
9
|
+
let binding = bindings.get(app);
|
|
10
|
+
if (!binding) {
|
|
11
|
+
const disabled = new SvelteSet();
|
|
12
|
+
binding = {
|
|
13
|
+
references: 0,
|
|
14
|
+
disabled,
|
|
15
|
+
dispose: registerConflictReviews(app, () => [...disabled].some((read) => read())),
|
|
16
|
+
};
|
|
17
|
+
bindings.set(app, binding);
|
|
18
|
+
}
|
|
19
|
+
const owned = binding;
|
|
20
|
+
const disabled = () => options.disabled?.() ?? false;
|
|
21
|
+
owned.disabled.add(disabled);
|
|
22
|
+
owned.references++;
|
|
23
|
+
let held = true;
|
|
24
|
+
return () => {
|
|
25
|
+
if (!held)
|
|
26
|
+
return;
|
|
27
|
+
held = false;
|
|
28
|
+
owned.disabled.delete(disabled);
|
|
29
|
+
if (--owned.references === 0) {
|
|
30
|
+
owned.dispose();
|
|
31
|
+
bindings.delete(app);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function registerConflictReviews(app, disabled) {
|
|
36
|
+
const registry = app.fileEditingSessions.textConflicts;
|
|
37
|
+
const model = new ConflictReviewState(registry, disabled);
|
|
38
|
+
const controller = getWorkspaceHostBinding(app.workspace).controller;
|
|
39
|
+
const collection = app.workspace.diagnostics.createCollection("desktop:file-conflicts", { label: "File conflicts" });
|
|
40
|
+
app.workspace.registerView(CONFLICT_REVIEW_VIEW, (leaf) => new ConflictReviewView(leaf, model));
|
|
41
|
+
const stopNavigation = app.workspace.registerDiagnosticOpener(async (location) => {
|
|
42
|
+
if (!location.resource.uri.startsWith(prefix))
|
|
43
|
+
return false;
|
|
44
|
+
if (!active)
|
|
45
|
+
return true;
|
|
46
|
+
const path = decodeURIComponent(location.resource.uri.slice(prefix.length));
|
|
47
|
+
if (!model.entries.some((entry) => entry.path === path))
|
|
48
|
+
return true;
|
|
49
|
+
let leaf = app.workspace
|
|
50
|
+
.getLeavesOfType(CONFLICT_REVIEW_VIEW)
|
|
51
|
+
.find((leaf) => leaf.view.getState().path === path);
|
|
52
|
+
if (!leaf) {
|
|
53
|
+
leaf = app.workspace.getLeaf("tab");
|
|
54
|
+
await leaf.setViewState({
|
|
55
|
+
type: CONFLICT_REVIEW_VIEW,
|
|
56
|
+
state: { path },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!active)
|
|
60
|
+
return true;
|
|
61
|
+
app.workspace.setActiveLeaf(leaf);
|
|
62
|
+
if (!active)
|
|
63
|
+
return true;
|
|
64
|
+
await app.workspace.revealLeaf(leaf);
|
|
65
|
+
if (!active)
|
|
66
|
+
return true;
|
|
67
|
+
app.workspace.requestSaveLayout({
|
|
68
|
+
source: "api",
|
|
69
|
+
operation: "open-file-conflict",
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
let active = true;
|
|
74
|
+
let paths = new Set();
|
|
75
|
+
const stopRegistry = registry.subscribe((entries) => {
|
|
76
|
+
model.entries = entries;
|
|
77
|
+
if (!entries.length)
|
|
78
|
+
model.error = "";
|
|
79
|
+
const current = new Set(entries.map((entry) => entry.path));
|
|
80
|
+
for (const path of paths)
|
|
81
|
+
if (!current.has(path)) {
|
|
82
|
+
collection.delete({ uri: prefix + encodeURIComponent(path) });
|
|
83
|
+
model.results.delete(path);
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
collection.set({
|
|
87
|
+
uri: prefix + encodeURIComponent(entry.path),
|
|
88
|
+
label: entry.path,
|
|
89
|
+
icon: "file-warning",
|
|
90
|
+
}, [
|
|
91
|
+
{
|
|
92
|
+
message: entry.external === undefined
|
|
93
|
+
? "The disk version could not be read. Review the retained draft."
|
|
94
|
+
: entry.external === null
|
|
95
|
+
? "The file was deleted on disk. Review the retained draft."
|
|
96
|
+
: "File changed on disk. Review your draft before saving.",
|
|
97
|
+
severity: "error",
|
|
98
|
+
source: "File conflicts",
|
|
99
|
+
code: "write-conflict",
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
const added = [...current].some((path) => !paths.has(path));
|
|
104
|
+
paths = current;
|
|
105
|
+
for (const leaf of app.workspace.getLeavesOfType(CONFLICT_REVIEW_VIEW)) {
|
|
106
|
+
if (!current.has(String(leaf.view.getState().path)))
|
|
107
|
+
controller.workspace.closeLeaf(leaf.id);
|
|
108
|
+
}
|
|
109
|
+
if (added)
|
|
110
|
+
void controller
|
|
111
|
+
.start()
|
|
112
|
+
.then(async () => {
|
|
113
|
+
if (active)
|
|
114
|
+
await controller.commands.execute("app-shell:show-problems");
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
if (active)
|
|
118
|
+
app.notifications.notify({
|
|
119
|
+
severity: "error",
|
|
120
|
+
source: "files",
|
|
121
|
+
message: String(error),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
return () => {
|
|
126
|
+
active = false;
|
|
127
|
+
stopRegistry();
|
|
128
|
+
stopNavigation();
|
|
129
|
+
collection.dispose();
|
|
130
|
+
app.workspace.unregisterView(CONFLICT_REVIEW_VIEW);
|
|
131
|
+
};
|
|
132
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,9 @@ export { default as CommunityPluginsSettingsPage } from "./plugin-management/Com
|
|
|
7
7
|
export { PluginsView } from "./plugin-management/plugins-view.svelte";
|
|
8
8
|
export { default as PluginBrowser } from "./plugin-management/PluginBrowser.svelte";
|
|
9
9
|
export { openPluginBrowser } from "./plugin-management/plugin-browser-navigation";
|
|
10
|
+
export { default as DesktopVaultLauncher } from "./desktop/DesktopVaultLauncher.svelte";
|
|
11
|
+
export { default as DesktopWorkspaceStartup } from "./desktop/DesktopWorkspaceStartup.svelte";
|
|
12
|
+
export type { DesktopVaultLauncherHost, DesktopLauncherAppearance, LauncherStatus, } from "./desktop/desktop-vault-launcher";
|
|
13
|
+
export { default as DesktopRecoveryBanner } from "./desktop/DesktopRecoveryBanner.svelte";
|
|
14
|
+
export { bindFileConflictReviews } from "./file-conflicts/register-conflict-reviews";
|
|
15
|
+
export type { FileConflictReviewOptions } from "./file-conflicts/register-conflict-reviews";
|
package/dist/index.js
CHANGED
|
@@ -6,3 +6,7 @@ export { default as CommunityPluginsSettingsPage } from "./plugin-management/Com
|
|
|
6
6
|
export { PluginsView } from "./plugin-management/plugins-view.svelte";
|
|
7
7
|
export { default as PluginBrowser } from "./plugin-management/PluginBrowser.svelte";
|
|
8
8
|
export { openPluginBrowser } from "./plugin-management/plugin-browser-navigation";
|
|
9
|
+
export { default as DesktopVaultLauncher } from "./desktop/DesktopVaultLauncher.svelte";
|
|
10
|
+
export { default as DesktopWorkspaceStartup } from "./desktop/DesktopWorkspaceStartup.svelte";
|
|
11
|
+
export { default as DesktopRecoveryBanner } from "./desktop/DesktopRecoveryBanner.svelte";
|
|
12
|
+
export { bindFileConflictReviews } from "./file-conflicts/register-conflict-reviews";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
|
|
3
3
|
import { onMount, tick, untrack } from "svelte";
|
|
4
|
+
import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
|
|
4
5
|
import { getPluginManagementContext } from "./plugin-management-context.svelte";
|
|
5
6
|
import "./plugin-management.css";
|
|
6
7
|
import PluginSettingsRow from "./PluginSettingsRow.svelte";
|
|
@@ -13,15 +14,34 @@
|
|
|
13
14
|
let { controller }: WorkspaceSettingsPageProps = $props();
|
|
14
15
|
const context = untrack(() => getPluginManagementContext(controller));
|
|
15
16
|
const app = context.app;
|
|
17
|
+
const managed = untrack(
|
|
18
|
+
() => getWorkspaceHostBinding(app.workspace).controller.managedPlugins,
|
|
19
|
+
);
|
|
16
20
|
let revision = $state(0);
|
|
17
21
|
let expandedPluginIds = $state(new Set<string>());
|
|
18
22
|
let entries = $derived.by(() => {
|
|
19
23
|
revision;
|
|
20
|
-
const
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const staticEntries = app.plugins.corePluginEntries;
|
|
25
|
+
return managed.states.map((entry) => {
|
|
26
|
+
const plugin =
|
|
27
|
+
entry.sourceId === "lapis"
|
|
28
|
+
? staticEntries.find((plugin) => plugin.manifest.id === entry.id)
|
|
29
|
+
: undefined;
|
|
30
|
+
return {
|
|
31
|
+
...entry,
|
|
32
|
+
manifest: {
|
|
33
|
+
id: entry.id,
|
|
34
|
+
name: entry.name,
|
|
35
|
+
description: entry.description,
|
|
36
|
+
version: plugin?.manifest.version,
|
|
37
|
+
},
|
|
38
|
+
errorMessage:
|
|
39
|
+
entry.error instanceof Error
|
|
40
|
+
? entry.error.message
|
|
41
|
+
: entry.error == null
|
|
42
|
+
? null
|
|
43
|
+
: String(entry.error),
|
|
44
|
+
};
|
|
25
45
|
});
|
|
26
46
|
});
|
|
27
47
|
|
|
@@ -34,8 +54,13 @@
|
|
|
34
54
|
) {
|
|
35
55
|
return;
|
|
36
56
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
57
|
+
const target = context.reveal.entryId.slice("core:".length);
|
|
58
|
+
const entry = entries.find(
|
|
59
|
+
(entry) => entry.key === target || entry.id === target,
|
|
60
|
+
);
|
|
61
|
+
if (!entry) return;
|
|
62
|
+
const pluginId = entry.id;
|
|
63
|
+
expandedPluginIds = new Set([...expandedPluginIds, entry.key]);
|
|
39
64
|
void tick().then(() => {
|
|
40
65
|
document
|
|
41
66
|
.querySelector(`[data-settings-plugin-id="${CSS.escape(pluginId)}"]`)
|
|
@@ -56,7 +81,6 @@
|
|
|
56
81
|
app.plugins.offref(error);
|
|
57
82
|
};
|
|
58
83
|
});
|
|
59
|
-
|
|
60
84
|
</script>
|
|
61
85
|
|
|
62
86
|
<section
|
|
@@ -77,7 +101,7 @@
|
|
|
77
101
|
No core plugins are registered.
|
|
78
102
|
</p>
|
|
79
103
|
{/if}
|
|
80
|
-
{#each entries as entry (entry.
|
|
104
|
+
{#each entries as entry (entry.key)}
|
|
81
105
|
<PluginSettingsRow
|
|
82
106
|
pluginId={entry.manifest.id}
|
|
83
107
|
name={entry.manifest.name}
|
|
@@ -92,18 +116,25 @@
|
|
|
92
116
|
error={entry.errorMessage ?? undefined}
|
|
93
117
|
enabled={entry.enabled}
|
|
94
118
|
required={entry.required}
|
|
95
|
-
expanded={expandedPluginIds.has(entry.
|
|
119
|
+
expanded={expandedPluginIds.has(entry.key)}
|
|
96
120
|
hasOptions={Boolean(
|
|
97
121
|
pluginSettingsSectionId(controller, entry.manifest.id),
|
|
98
122
|
)}
|
|
99
|
-
diagnostics={
|
|
123
|
+
diagnostics={entry.sourceId === "lapis"
|
|
124
|
+
? pluginDiagnosticRows(app, entry.manifest.id)
|
|
125
|
+
: [
|
|
126
|
+
{ label: "State", value: entry.status },
|
|
127
|
+
{ label: "Source", value: entry.sourceId },
|
|
128
|
+
]}
|
|
100
129
|
onOptions={() => openPluginSettings(controller, entry.manifest.id)}
|
|
101
|
-
onRestart={
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
onRestart={entry.sourceId === "lapis"
|
|
131
|
+
? async () => {
|
|
132
|
+
await app.plugins.restartPlugin(entry.manifest.id);
|
|
133
|
+
}
|
|
134
|
+
: undefined}
|
|
104
135
|
onToggle={async (enabled) => {
|
|
105
|
-
if (enabled) await
|
|
106
|
-
else await
|
|
136
|
+
if (enabled) await managed.enable(entry.key);
|
|
137
|
+
else await managed.disable(entry.key);
|
|
107
138
|
}}
|
|
108
139
|
/>
|
|
109
140
|
{/each}
|