@lapis-notes/graph 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/embed.d.ts +5 -0
- package/dist/embed.js +4 -0
- package/dist/graph-build-generation.d.ts +6 -0
- package/dist/graph-build-generation.js +13 -0
- package/dist/graph-controls-overlay.svelte +840 -0
- package/dist/graph-controls-overlay.svelte.d.ts +30 -0
- package/dist/graph-data-coordinator.d.ts +41 -0
- package/dist/graph-data-coordinator.js +265 -0
- package/dist/graph-data.d.ts +23 -0
- package/dist/graph-data.js +493 -0
- package/dist/graph-embed.svelte +92 -0
- package/dist/graph-embed.svelte.d.ts +12 -0
- package/dist/graph-file-preview.svelte +115 -0
- package/dist/graph-file-preview.svelte.d.ts +12 -0
- package/dist/graph-hover-intent.d.ts +22 -0
- package/dist/graph-hover-intent.js +85 -0
- package/dist/graph-load-alignment.d.ts +2 -0
- package/dist/graph-load-alignment.js +3 -0
- package/dist/graph-navigation.d.ts +7 -0
- package/dist/graph-navigation.js +78 -0
- package/dist/graph-node-activation.d.ts +4 -0
- package/dist/graph-node-activation.js +16 -0
- package/dist/graph-plugin.d.ts +38 -0
- package/dist/graph-plugin.js +177 -0
- package/dist/graph-problems.d.ts +15 -0
- package/dist/graph-problems.js +46 -0
- package/dist/graph-query-resolution.d.ts +9 -0
- package/dist/graph-query-resolution.js +44 -0
- package/dist/graph-renderer.d.ts +226 -0
- package/dist/graph-renderer.js +1350 -0
- package/dist/graph-settings.d.ts +20 -0
- package/dist/graph-settings.js +125 -0
- package/dist/graph-types.d.ts +74 -0
- package/dist/graph-types.js +1 -0
- package/dist/graph-view.d.ts +62 -0
- package/dist/graph-view.js +553 -0
- package/dist/graph-viewport-alignment.d.ts +6 -0
- package/dist/graph-viewport-alignment.js +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/mount-local-component.svelte.d.ts +7 -0
- package/dist/mount-local-component.svelte.js +12 -0
- package/dist/public-components.d.ts +2 -0
- package/dist/public-components.js +2 -0
- package/dist/styles.css +441 -0
- package/manifest.json +30 -0
- package/package.json +84 -0
- package/styles.css +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type App } from "@lapis-notes/api";
|
|
2
|
+
import type { GraphNodePreview } from "./graph-renderer";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
app: App;
|
|
5
|
+
preview: GraphNodePreview | null;
|
|
6
|
+
closeDelayMs: number;
|
|
7
|
+
onOpenFile: (preview: GraphNodePreview, event: MouseEvent) => void;
|
|
8
|
+
onDismiss: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const GraphFilePreview: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type GraphFilePreview = ReturnType<typeof GraphFilePreview>;
|
|
12
|
+
export default GraphFilePreview;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface GraphHoverDelaySettings {
|
|
2
|
+
activationDelayMs: number;
|
|
3
|
+
releaseDelayMs: number;
|
|
4
|
+
}
|
|
5
|
+
/** Separates immediate pointer hover from delayed graph emphasis. */
|
|
6
|
+
export declare class GraphHoverIntent {
|
|
7
|
+
private readonly getDelays;
|
|
8
|
+
private readonly onEmphasisChange;
|
|
9
|
+
private activationTimer;
|
|
10
|
+
private releaseTimer;
|
|
11
|
+
private pointerNodeId;
|
|
12
|
+
private emphasisNodeId;
|
|
13
|
+
constructor(getDelays: () => GraphHoverDelaySettings, onEmphasisChange: (nodeId: string | null) => void);
|
|
14
|
+
setPointerNode(nodeId: string | null): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
private activate;
|
|
18
|
+
private scheduleRelease;
|
|
19
|
+
private setEmphasisNode;
|
|
20
|
+
private clearActivationTimer;
|
|
21
|
+
private clearReleaseTimer;
|
|
22
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function normalizedDelay(value) {
|
|
2
|
+
return Math.max(0, Number.isFinite(value) ? value : 0);
|
|
3
|
+
}
|
|
4
|
+
/** Separates immediate pointer hover from delayed graph emphasis. */
|
|
5
|
+
export class GraphHoverIntent {
|
|
6
|
+
getDelays;
|
|
7
|
+
onEmphasisChange;
|
|
8
|
+
activationTimer = null;
|
|
9
|
+
releaseTimer = null;
|
|
10
|
+
pointerNodeId = null;
|
|
11
|
+
emphasisNodeId = null;
|
|
12
|
+
constructor(getDelays, onEmphasisChange) {
|
|
13
|
+
this.getDelays = getDelays;
|
|
14
|
+
this.onEmphasisChange = onEmphasisChange;
|
|
15
|
+
}
|
|
16
|
+
setPointerNode(nodeId) {
|
|
17
|
+
this.pointerNodeId = nodeId;
|
|
18
|
+
this.clearActivationTimer();
|
|
19
|
+
if (nodeId === this.emphasisNodeId) {
|
|
20
|
+
this.clearReleaseTimer();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (this.emphasisNodeId !== null) {
|
|
24
|
+
this.scheduleRelease();
|
|
25
|
+
}
|
|
26
|
+
if (nodeId !== null) {
|
|
27
|
+
const delay = normalizedDelay(this.getDelays().activationDelayMs);
|
|
28
|
+
if (delay === 0) {
|
|
29
|
+
this.activate(nodeId);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.activationTimer = setTimeout(() => {
|
|
33
|
+
this.activationTimer = null;
|
|
34
|
+
if (this.pointerNodeId === nodeId)
|
|
35
|
+
this.activate(nodeId);
|
|
36
|
+
}, delay);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.pointerNodeId = null;
|
|
42
|
+
this.clearActivationTimer();
|
|
43
|
+
this.clearReleaseTimer();
|
|
44
|
+
this.setEmphasisNode(null);
|
|
45
|
+
}
|
|
46
|
+
destroy() {
|
|
47
|
+
this.clear();
|
|
48
|
+
}
|
|
49
|
+
activate(nodeId) {
|
|
50
|
+
this.clearReleaseTimer();
|
|
51
|
+
this.setEmphasisNode(nodeId);
|
|
52
|
+
}
|
|
53
|
+
scheduleRelease() {
|
|
54
|
+
this.clearReleaseTimer();
|
|
55
|
+
const source = this.emphasisNodeId;
|
|
56
|
+
if (source === null)
|
|
57
|
+
return;
|
|
58
|
+
const delay = normalizedDelay(this.getDelays().releaseDelayMs);
|
|
59
|
+
if (delay === 0) {
|
|
60
|
+
this.setEmphasisNode(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.releaseTimer = setTimeout(() => {
|
|
64
|
+
this.releaseTimer = null;
|
|
65
|
+
if (this.emphasisNodeId === source)
|
|
66
|
+
this.setEmphasisNode(null);
|
|
67
|
+
}, delay);
|
|
68
|
+
}
|
|
69
|
+
setEmphasisNode(nodeId) {
|
|
70
|
+
if (nodeId === this.emphasisNodeId)
|
|
71
|
+
return;
|
|
72
|
+
this.emphasisNodeId = nodeId;
|
|
73
|
+
this.onEmphasisChange(nodeId);
|
|
74
|
+
}
|
|
75
|
+
clearActivationTimer() {
|
|
76
|
+
if (this.activationTimer !== null)
|
|
77
|
+
clearTimeout(this.activationTimer);
|
|
78
|
+
this.activationTimer = null;
|
|
79
|
+
}
|
|
80
|
+
clearReleaseTimer() {
|
|
81
|
+
if (this.releaseTimer !== null)
|
|
82
|
+
clearTimeout(this.releaseTimer);
|
|
83
|
+
this.releaseTimer = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const GRAPH_VIEW_TYPE = "graph";
|
|
2
|
+
const LOCAL_GRAPH_VIEW_TYPE = "graph-local";
|
|
3
|
+
const GRAPH_VIEW_TYPES = new Set([
|
|
4
|
+
GRAPH_VIEW_TYPE,
|
|
5
|
+
LOCAL_GRAPH_VIEW_TYPE,
|
|
6
|
+
"empty",
|
|
7
|
+
]);
|
|
8
|
+
function isRootLeaf(workspace, leaf) {
|
|
9
|
+
return Boolean(workspace.rootSplit.iterateAllLeaves((candidate) => (candidate === leaf ? leaf : undefined)));
|
|
10
|
+
}
|
|
11
|
+
function viewTypeForLeaf(leaf) {
|
|
12
|
+
const view = leaf.view;
|
|
13
|
+
if (view && typeof view.getViewType === "function") {
|
|
14
|
+
return view.getViewType();
|
|
15
|
+
}
|
|
16
|
+
return typeof leaf.state?.type === "string" ? leaf.state.type : "";
|
|
17
|
+
}
|
|
18
|
+
function isGraphLikeViewType(viewType) {
|
|
19
|
+
return GRAPH_VIEW_TYPES.has(viewType);
|
|
20
|
+
}
|
|
21
|
+
function isDocumentNavigationLeaf(leaf, graphLeaf) {
|
|
22
|
+
if (leaf === graphLeaf) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const viewType = viewTypeForLeaf(leaf);
|
|
26
|
+
if (isGraphLikeViewType(viewType)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const filePath = leaf.state?.state?.["file"];
|
|
30
|
+
if (typeof filePath === "string" && filePath.length > 0) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const view = leaf.view;
|
|
34
|
+
if (view && typeof view === "object" && "file" in view) {
|
|
35
|
+
const file = view.file;
|
|
36
|
+
return typeof file?.path === "string" && file.path.length > 0;
|
|
37
|
+
}
|
|
38
|
+
return viewType.length > 0 && !isGraphLikeViewType(viewType);
|
|
39
|
+
}
|
|
40
|
+
function findRootDocumentLeaf(workspace, graphLeaf) {
|
|
41
|
+
const selectedInTab = workspace.rootSplit.iterateAllTabs((tabs) => {
|
|
42
|
+
const leaf = tabs.selectedLeaf;
|
|
43
|
+
if (leaf && isDocumentNavigationLeaf(leaf, graphLeaf)) {
|
|
44
|
+
return leaf;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
});
|
|
48
|
+
if (selectedInTab) {
|
|
49
|
+
return selectedInTab;
|
|
50
|
+
}
|
|
51
|
+
let fallback = null;
|
|
52
|
+
workspace.rootSplit.iterateAllLeaves((leaf) => {
|
|
53
|
+
if (isDocumentNavigationLeaf(leaf, graphLeaf)) {
|
|
54
|
+
fallback ??= leaf;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
export function resolveGraphNavigationLeaf(options) {
|
|
60
|
+
const { workspace, graphLeaf, isLocal, openInNewPane } = options;
|
|
61
|
+
if (isLocal || !isRootLeaf(workspace, graphLeaf)) {
|
|
62
|
+
return workspace.getLeaf(openInNewPane ? true : undefined);
|
|
63
|
+
}
|
|
64
|
+
const activeRoot = workspace.activeRootLeaf;
|
|
65
|
+
if (activeRoot &&
|
|
66
|
+
activeRoot !== graphLeaf &&
|
|
67
|
+
isDocumentNavigationLeaf(activeRoot, graphLeaf)) {
|
|
68
|
+
return activeRoot;
|
|
69
|
+
}
|
|
70
|
+
const rootDocumentLeaf = findRootDocumentLeaf(workspace, graphLeaf);
|
|
71
|
+
if (rootDocumentLeaf) {
|
|
72
|
+
return rootDocumentLeaf;
|
|
73
|
+
}
|
|
74
|
+
if (openInNewPane) {
|
|
75
|
+
return workspace.getLeaf(true);
|
|
76
|
+
}
|
|
77
|
+
return graphLeaf;
|
|
78
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { App } from "@lapis-notes/api";
|
|
2
|
+
import type { GraphNode } from "./graph-types";
|
|
3
|
+
export declare function graphTagSearchQuery(node: GraphNode): string | null;
|
|
4
|
+
export declare function openGraphTagSearch(app: Pick<App, "commands">, node: GraphNode): Promise<boolean>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { normalizeGraphTag } from "./graph-data";
|
|
2
|
+
export function graphTagSearchQuery(node) {
|
|
3
|
+
if (node.type !== "tag")
|
|
4
|
+
return null;
|
|
5
|
+
const tag = normalizeGraphTag(node.label || node.path || "");
|
|
6
|
+
return tag ? `tag:${tag}` : null;
|
|
7
|
+
}
|
|
8
|
+
export async function openGraphTagSearch(app, node) {
|
|
9
|
+
const query = graphTagSearchQuery(node);
|
|
10
|
+
if (query == null)
|
|
11
|
+
return false;
|
|
12
|
+
await app.commands
|
|
13
|
+
.executeCommand("search:open-search", query)
|
|
14
|
+
.catch(() => undefined);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Plugin, type App, type PluginManifest } from "@lapis-notes/api";
|
|
2
|
+
import { type GraphCoordinatorState } from "./graph-data-coordinator";
|
|
3
|
+
import type { GraphSettings } from "./graph-types";
|
|
4
|
+
import type { GraphData } from "./graph-types";
|
|
5
|
+
import { type GraphQueryMatches } from "./graph-query-resolution";
|
|
6
|
+
import { type GraphProblemScope } from "./graph-problems";
|
|
7
|
+
interface GraphFocusableView {
|
|
8
|
+
focusActiveFile(): void;
|
|
9
|
+
applyGraphSettings(settings: GraphSettings): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class GraphPlugin extends Plugin {
|
|
12
|
+
private readonly views;
|
|
13
|
+
private readonly graphCoordinator;
|
|
14
|
+
private settings;
|
|
15
|
+
private settingsSaveTimer;
|
|
16
|
+
private pendingSettingsSave;
|
|
17
|
+
private graphProblems;
|
|
18
|
+
private readonly pathQueryCache;
|
|
19
|
+
constructor(app: App, manifest?: PluginManifest);
|
|
20
|
+
onload(): Promise<void>;
|
|
21
|
+
onunload(): Promise<void>;
|
|
22
|
+
getSettings(): GraphSettings;
|
|
23
|
+
updateSettings(nextSettings: GraphSettings): Promise<void>;
|
|
24
|
+
subscribeToGlobalGraph(listener: (state: GraphCoordinatorState) => void): () => void;
|
|
25
|
+
refreshGlobalGraph(force?: boolean): Promise<void>;
|
|
26
|
+
reportGraphBuildFailure(scope: GraphProblemScope, path: string | null, error: unknown): void;
|
|
27
|
+
clearGraphBuildFailure(scope: GraphProblemScope): void;
|
|
28
|
+
resolveGraphSettings(graph: GraphData, settings: GraphSettings): Promise<{
|
|
29
|
+
graph: GraphData;
|
|
30
|
+
matches: GraphQueryMatches;
|
|
31
|
+
}>;
|
|
32
|
+
registerGraphView(view: GraphFocusableView): () => void;
|
|
33
|
+
private initializeSettings;
|
|
34
|
+
private scheduleSettingsSave;
|
|
35
|
+
private matchSearchPaths;
|
|
36
|
+
private openGraphView;
|
|
37
|
+
}
|
|
38
|
+
export default GraphPlugin;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Plugin, } from "@lapis-notes/api";
|
|
2
|
+
import { DEFAULT_GRAPH_SETTINGS, loadPersistedGraphSettings, mergeGraphSettings, serializeGraphSettings, } from "./graph-settings";
|
|
3
|
+
import { GraphDataCoordinator, } from "./graph-data-coordinator";
|
|
4
|
+
import { filterGraphBySettings } from "./graph-data";
|
|
5
|
+
import { resolveGraphQueryMatches, } from "./graph-query-resolution";
|
|
6
|
+
import { GraphView, GraphViewType, LocalGraphView, LocalGraphViewType, } from "./graph-view";
|
|
7
|
+
import { createGraphProblemReporter, } from "./graph-problems";
|
|
8
|
+
import manifestSpec from "../../manifest.json";
|
|
9
|
+
export class GraphPlugin extends Plugin {
|
|
10
|
+
views = new Set();
|
|
11
|
+
graphCoordinator;
|
|
12
|
+
settings = mergeGraphSettings(DEFAULT_GRAPH_SETTINGS);
|
|
13
|
+
settingsSaveTimer = null;
|
|
14
|
+
pendingSettingsSave = null;
|
|
15
|
+
graphProblems = null;
|
|
16
|
+
pathQueryCache = new Map();
|
|
17
|
+
constructor(app, manifest = manifestSpec) {
|
|
18
|
+
super(app, manifest);
|
|
19
|
+
this.graphCoordinator = new GraphDataCoordinator(app);
|
|
20
|
+
}
|
|
21
|
+
async onload() {
|
|
22
|
+
await this.initializeSettings();
|
|
23
|
+
this.graphProblems = createGraphProblemReporter(this);
|
|
24
|
+
this.register(this.graphCoordinator.subscribe((state) => {
|
|
25
|
+
if (state.status === "error") {
|
|
26
|
+
this.reportGraphBuildFailure("global", null, state.error);
|
|
27
|
+
}
|
|
28
|
+
else if (state.status === "ready") {
|
|
29
|
+
this.clearGraphBuildFailure("global");
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
this.registerView(GraphViewType, (leaf) => new GraphView(leaf, this), {
|
|
33
|
+
kind: "command",
|
|
34
|
+
command: {
|
|
35
|
+
id: "open-graph-view",
|
|
36
|
+
name: "Open graph view",
|
|
37
|
+
callback: () => void this.openGraphView(false),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
this.registerSidebarView(LocalGraphViewType, (leaf) => new LocalGraphView(leaf, this), { side: "right", title: "Local graph", icon: "git-branch-plus" }, {
|
|
41
|
+
kind: "command",
|
|
42
|
+
command: {
|
|
43
|
+
id: "open-local-graph",
|
|
44
|
+
name: "Open local graph",
|
|
45
|
+
callback: () => void this.openGraphView(true),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
this.addRibbonIcon("waypoints", "Open graph view", () => {
|
|
49
|
+
void this.openGraphView(false);
|
|
50
|
+
});
|
|
51
|
+
this.addCommand({
|
|
52
|
+
id: "focus-active-file-in-graph",
|
|
53
|
+
name: "Focus active file in graph",
|
|
54
|
+
callback: () => {
|
|
55
|
+
this.views.forEach((view) => view.focusActiveFile());
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
this.registerEvent(this.app.metadataCache.on("index-changed", (change) => {
|
|
59
|
+
if (change.reset ||
|
|
60
|
+
change.domains.includes("metadata") ||
|
|
61
|
+
change.domains.includes("search")) {
|
|
62
|
+
this.pathQueryCache.clear();
|
|
63
|
+
}
|
|
64
|
+
if (change.reset || change.domains.includes("metadata")) {
|
|
65
|
+
void this.graphCoordinator.requestRefresh("metadata-change", true);
|
|
66
|
+
}
|
|
67
|
+
else if (change.domains.includes("search")) {
|
|
68
|
+
const snapshot = this.getSettings();
|
|
69
|
+
this.views.forEach((view) => view.applyGraphSettings(snapshot));
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
void this.graphCoordinator.start();
|
|
73
|
+
}
|
|
74
|
+
async onunload() {
|
|
75
|
+
this.graphCoordinator.dispose();
|
|
76
|
+
this.graphProblems = null;
|
|
77
|
+
if (this.settingsSaveTimer)
|
|
78
|
+
clearTimeout(this.settingsSaveTimer);
|
|
79
|
+
this.settingsSaveTimer = null;
|
|
80
|
+
const pending = this.pendingSettingsSave;
|
|
81
|
+
this.pendingSettingsSave = null;
|
|
82
|
+
if (pending)
|
|
83
|
+
await this.saveData(pending);
|
|
84
|
+
}
|
|
85
|
+
getSettings() {
|
|
86
|
+
return mergeGraphSettings(this.settings);
|
|
87
|
+
}
|
|
88
|
+
async updateSettings(nextSettings) {
|
|
89
|
+
this.settings = mergeGraphSettings(nextSettings);
|
|
90
|
+
const snapshot = this.getSettings();
|
|
91
|
+
this.views.forEach((view) => {
|
|
92
|
+
view.applyGraphSettings(snapshot);
|
|
93
|
+
});
|
|
94
|
+
this.scheduleSettingsSave(snapshot);
|
|
95
|
+
}
|
|
96
|
+
subscribeToGlobalGraph(listener) {
|
|
97
|
+
return this.graphCoordinator.subscribe(listener);
|
|
98
|
+
}
|
|
99
|
+
refreshGlobalGraph(force = false) {
|
|
100
|
+
return this.graphCoordinator.requestRefresh("view-refresh", force);
|
|
101
|
+
}
|
|
102
|
+
reportGraphBuildFailure(scope, path, error) {
|
|
103
|
+
this.graphProblems?.report(scope, path, error);
|
|
104
|
+
}
|
|
105
|
+
clearGraphBuildFailure(scope) {
|
|
106
|
+
this.graphProblems?.clear(scope);
|
|
107
|
+
}
|
|
108
|
+
async resolveGraphSettings(graph, settings) {
|
|
109
|
+
const matches = await resolveGraphQueryMatches(settings, (query) => this.matchSearchPaths(query));
|
|
110
|
+
return {
|
|
111
|
+
graph: filterGraphBySettings(graph, settings, matches),
|
|
112
|
+
matches,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
registerGraphView(view) {
|
|
116
|
+
this.views.add(view);
|
|
117
|
+
return () => {
|
|
118
|
+
this.views.delete(view);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async initializeSettings() {
|
|
122
|
+
const storedData = await this.loadData();
|
|
123
|
+
const loaded = loadPersistedGraphSettings(storedData);
|
|
124
|
+
this.settings = loaded.settings;
|
|
125
|
+
if (loaded.migrated) {
|
|
126
|
+
await this.saveData(serializeGraphSettings(this.settings));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
scheduleSettingsSave(settings) {
|
|
130
|
+
this.pendingSettingsSave = serializeGraphSettings(settings);
|
|
131
|
+
if (this.settingsSaveTimer)
|
|
132
|
+
clearTimeout(this.settingsSaveTimer);
|
|
133
|
+
this.settingsSaveTimer = setTimeout(() => {
|
|
134
|
+
this.settingsSaveTimer = null;
|
|
135
|
+
const pending = this.pendingSettingsSave;
|
|
136
|
+
this.pendingSettingsSave = null;
|
|
137
|
+
if (pending)
|
|
138
|
+
void this.saveData(pending);
|
|
139
|
+
}, 180);
|
|
140
|
+
}
|
|
141
|
+
matchSearchPaths(query) {
|
|
142
|
+
let cached = this.pathQueryCache.get(query);
|
|
143
|
+
if (!cached) {
|
|
144
|
+
cached = this.app.appDatabase
|
|
145
|
+
.searchDocumentPaths(query, { mode: "lexical", limit: 50_000 })
|
|
146
|
+
.then((paths) => new Set(paths));
|
|
147
|
+
this.pathQueryCache.set(query, cached);
|
|
148
|
+
void cached.catch(() => this.pathQueryCache.delete(query));
|
|
149
|
+
}
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
async openGraphView(local) {
|
|
153
|
+
const viewType = local ? LocalGraphViewType : GraphViewType;
|
|
154
|
+
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
|
155
|
+
if (leaves.length) {
|
|
156
|
+
const leaf = leaves[0];
|
|
157
|
+
this.app.workspace.activateLeaf(leaf, {
|
|
158
|
+
focusRootHost: false,
|
|
159
|
+
source: "api",
|
|
160
|
+
operation: local ? "open-local-graph" : "open-graph-view",
|
|
161
|
+
});
|
|
162
|
+
await this.app.workspace.revealLeaf(leaf);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const leaf = local
|
|
166
|
+
? this.app.workspace.ensureSideLeaf(viewType, "right")
|
|
167
|
+
: this.app.workspace.getLeaf(true);
|
|
168
|
+
await leaf.setViewState({ type: viewType }, { history: true });
|
|
169
|
+
this.app.workspace.activateLeaf(leaf, {
|
|
170
|
+
focusRootHost: false,
|
|
171
|
+
source: "api",
|
|
172
|
+
operation: local ? "open-local-graph" : "open-graph-view",
|
|
173
|
+
});
|
|
174
|
+
await this.app.workspace.revealLeaf(leaf);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export default GraphPlugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DiagnosticCollection, type DiagnosticCollectionOptions } from "@lapis-notes/api";
|
|
2
|
+
export type GraphProblemScope = "global" | "local";
|
|
3
|
+
interface GraphProblemOwner {
|
|
4
|
+
createDiagnosticCollection(id: string, options?: DiagnosticCollectionOptions): DiagnosticCollection;
|
|
5
|
+
}
|
|
6
|
+
export declare class GraphProblemReporter {
|
|
7
|
+
private readonly collection;
|
|
8
|
+
private readonly problems;
|
|
9
|
+
constructor(collection: DiagnosticCollection);
|
|
10
|
+
report(scope: GraphProblemScope, path: string | null, error: unknown): void;
|
|
11
|
+
clear(scope: GraphProblemScope): void;
|
|
12
|
+
private publish;
|
|
13
|
+
}
|
|
14
|
+
export declare function createGraphProblemReporter(owner: GraphProblemOwner): GraphProblemReporter;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { diagnosticResourceForPath, } from "@lapis-notes/api";
|
|
2
|
+
function failureMessage(scope, error) {
|
|
3
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4
|
+
return `${scope === "local" ? "Local Graph" : "Graph"} failed: ${detail}`;
|
|
5
|
+
}
|
|
6
|
+
export class GraphProblemReporter {
|
|
7
|
+
collection;
|
|
8
|
+
problems = new Map();
|
|
9
|
+
constructor(collection) {
|
|
10
|
+
this.collection = collection;
|
|
11
|
+
}
|
|
12
|
+
report(scope, path, error) {
|
|
13
|
+
this.problems.set(scope, {
|
|
14
|
+
resource: scope === "local" && path ? diagnosticResourceForPath(path) : null,
|
|
15
|
+
diagnostic: {
|
|
16
|
+
message: failureMessage(scope, error),
|
|
17
|
+
severity: "error",
|
|
18
|
+
source: "Graph",
|
|
19
|
+
code: `lapis-graph/${scope}-build`,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
this.publish();
|
|
23
|
+
}
|
|
24
|
+
clear(scope) {
|
|
25
|
+
if (!this.problems.delete(scope))
|
|
26
|
+
return;
|
|
27
|
+
this.publish();
|
|
28
|
+
}
|
|
29
|
+
publish() {
|
|
30
|
+
this.collection.clear();
|
|
31
|
+
const grouped = new Map();
|
|
32
|
+
for (const problem of this.problems.values()) {
|
|
33
|
+
const key = problem.resource?.uri ?? "workspace";
|
|
34
|
+
const current = grouped.get(key) ?? {
|
|
35
|
+
resource: problem.resource,
|
|
36
|
+
diagnostics: [],
|
|
37
|
+
};
|
|
38
|
+
current.diagnostics.push(problem.diagnostic);
|
|
39
|
+
grouped.set(key, current);
|
|
40
|
+
}
|
|
41
|
+
this.collection.set([...grouped.values()].map(({ resource, diagnostics }) => [resource, diagnostics]));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function createGraphProblemReporter(owner) {
|
|
45
|
+
return new GraphProblemReporter(owner.createDiagnosticCollection("build", { label: "Graph" }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GraphSettings } from "./graph-types";
|
|
2
|
+
export interface GraphQueryMatches {
|
|
3
|
+
filterPaths?: ReadonlySet<string>;
|
|
4
|
+
groupPaths: ReadonlyMap<string, ReadonlySet<string>>;
|
|
5
|
+
filterDiagnostic: string | null;
|
|
6
|
+
groupDiagnostics: Readonly<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
export type GraphPathQuery = (query: string) => Promise<ReadonlySet<string>>;
|
|
9
|
+
export declare function resolveGraphQueryMatches(settings: GraphSettings, matchPaths: GraphPathQuery): Promise<GraphQueryMatches>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseSearchQueryAst } from "@lapis-notes/api";
|
|
2
|
+
function queryDiagnostic(query) {
|
|
3
|
+
return parseSearchQueryAst(query).diagnostics[0]?.message ?? null;
|
|
4
|
+
}
|
|
5
|
+
export async function resolveGraphQueryMatches(settings, matchPaths) {
|
|
6
|
+
const groupPaths = new Map();
|
|
7
|
+
const groupDiagnostics = {};
|
|
8
|
+
const filterQuery = settings.filters.searchQuery.trim();
|
|
9
|
+
let filterPaths;
|
|
10
|
+
let filterDiagnostic = null;
|
|
11
|
+
if (filterQuery) {
|
|
12
|
+
filterDiagnostic = queryDiagnostic(filterQuery);
|
|
13
|
+
if (!filterDiagnostic) {
|
|
14
|
+
try {
|
|
15
|
+
filterPaths = await matchPaths(filterQuery);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
filterDiagnostic = "Unable to evaluate this filter";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(settings.groups.map(async (group) => {
|
|
23
|
+
const query = group.query.trim();
|
|
24
|
+
if (!query)
|
|
25
|
+
return;
|
|
26
|
+
const diagnostic = queryDiagnostic(query);
|
|
27
|
+
if (diagnostic) {
|
|
28
|
+
groupDiagnostics[group.id] = diagnostic;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
groupPaths.set(group.id, await matchPaths(query));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
groupDiagnostics[group.id] = "Unable to evaluate this Group";
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
return {
|
|
39
|
+
filterPaths,
|
|
40
|
+
groupPaths,
|
|
41
|
+
filterDiagnostic,
|
|
42
|
+
groupDiagnostics,
|
|
43
|
+
};
|
|
44
|
+
}
|