Files
opencode-obsidian/src/context/ContextManager.ts

210 lines
5.7 KiB
TypeScript

import { App, EventRef, MarkdownView, WorkspaceLeaf } from "obsidian";
import { OpenCodeSettings, OPENCODE_VIEW_TYPE } from "../types";
import { OpenCodeClient } from "../client/OpenCodeClient";
import { WorkspaceContext } from "./WorkspaceContext";
import { OpenCodeView } from "../ui/OpenCodeView";
import { ServerState } from "../server/types";
type ContextManagerDeps = {
app: App;
settings: OpenCodeSettings;
client: OpenCodeClient;
getServerState: () => ServerState;
getCachedIframeUrl: () => string | null;
setCachedIframeUrl: (url: string | null) => void;
registerEvent: (ref: EventRef) => void;
};
export class ContextManager {
private app: App;
private settings: OpenCodeSettings;
private client: OpenCodeClient;
private workspaceContext: WorkspaceContext;
private getServerState: () => ServerState;
private getCachedIframeUrl: () => string | null;
private setCachedIframeUrl: (url: string | null) => void;
private registerEvent: (ref: EventRef) => void;
private contextEventRefs: EventRef[] = [];
private contextRefreshTimer: number | null = null;
constructor(deps: ContextManagerDeps) {
this.app = deps.app;
this.settings = deps.settings;
this.client = deps.client;
this.workspaceContext = new WorkspaceContext(this.app);
this.getServerState = deps.getServerState;
this.getCachedIframeUrl = deps.getCachedIframeUrl;
this.setCachedIframeUrl = deps.setCachedIframeUrl;
this.registerEvent = deps.registerEvent;
}
updateSettings(settings: OpenCodeSettings): void {
this.settings = settings;
this.updateListeners();
}
private updateListeners(): void {
if (!this.settings.injectWorkspaceContext) {
this.clearListeners();
return;
}
if (this.contextEventRefs.length > 0) {
return;
}
const activeLeafRef = this.app.workspace.on("active-leaf-change", (leaf) => {
if (leaf?.view instanceof MarkdownView) {
this.workspaceContext.trackViewSelection(leaf.view);
}
this.scheduleRefresh(0);
});
const fileOpenRef = this.app.workspace.on("file-open", () => {
this.scheduleRefresh();
});
const fileCloseRef = (this.app.workspace as any).on("file-close", () => {
this.scheduleRefresh();
});
const layoutChangeRef = this.app.workspace.on("layout-change", () => {
this.scheduleRefresh();
});
const editorChangeRef = this.app.workspace.on(
"editor-change",
(_editor, view) => {
if (view instanceof MarkdownView) {
this.workspaceContext.trackViewSelection(view);
}
this.scheduleRefresh(500);
}
);
const selectionChangeRef = (this.app.workspace as any).on(
"editor-selection-change",
(_editor: unknown, view: unknown) => {
if (view instanceof MarkdownView) {
this.workspaceContext.trackViewSelection(view);
}
this.scheduleRefresh(200);
}
);
this.contextEventRefs = [
activeLeafRef,
fileOpenRef,
fileCloseRef,
layoutChangeRef,
editorChangeRef,
selectionChangeRef,
];
this.contextEventRefs.forEach((ref) => this.registerEvent(ref));
}
private clearListeners(): void {
for (const ref of this.contextEventRefs) {
this.app.workspace.offref(ref);
}
this.contextEventRefs = [];
if (this.contextRefreshTimer !== null) {
window.clearTimeout(this.contextRefreshTimer);
this.contextRefreshTimer = null;
}
}
private scheduleRefresh(delayMs: number = 300): void {
const leaf = this.getLeafForRefresh();
if (!leaf) {
return;
}
if (this.contextRefreshTimer !== null) {
window.clearTimeout(this.contextRefreshTimer);
}
this.contextRefreshTimer = window.setTimeout(() => {
this.contextRefreshTimer = null;
void this.refreshContext(leaf);
}, delayMs);
}
private getLeafForRefresh(): WorkspaceLeaf | null {
const activeLeaf = this.app.workspace.activeLeaf;
if (activeLeaf?.view.getViewType() === OPENCODE_VIEW_TYPE) {
return activeLeaf;
}
return this.getVisibleSidebarLeaf();
}
private getVisibleSidebarLeaf(): WorkspaceLeaf | null {
const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE);
if (leaves.length === 0) {
return null;
}
const rightSplit = this.app.workspace.rightSplit;
if (!rightSplit || rightSplit.collapsed) {
return null;
}
const leaf = leaves[0];
return leaf.getRoot() === rightSplit ? leaf : null;
}
async handleServerRunning(): Promise<void> {
const activeLeaf = this.app.workspace.activeLeaf;
if (activeLeaf?.view.getViewType() === OPENCODE_VIEW_TYPE) {
await this.refreshContext(activeLeaf);
}
}
async refreshContextForView(view: OpenCodeView): Promise<void> {
if (!this.settings.injectWorkspaceContext) {
return;
}
const leaf = this.getLeafForRefresh();
if (!leaf) {
return;
}
await this.refreshContext(leaf);
}
private async refreshContext(leaf: WorkspaceLeaf): Promise<void> {
if (!this.settings.injectWorkspaceContext) {
return;
}
if (this.getServerState() !== "running") {
return;
}
const view = leaf.view instanceof OpenCodeView ? leaf.view : null;
const iframeUrl = this.getCachedIframeUrl() ?? view?.getIframeUrl();
if (!iframeUrl) {
return;
}
const sessionId = this.client.resolveSessionId(iframeUrl);
if (!sessionId) {
return;
}
this.setCachedIframeUrl(iframeUrl);
const { contextText } = this.workspaceContext.gatherContext(
this.settings.maxNotesInContext,
this.settings.maxSelectionLength
);
await this.client.updateContext({
sessionId,
contextText,
});
}
destroy(): void {
this.clearListeners();
}
}