-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.ts
More file actions
599 lines (553 loc) · 23.6 KB
/
Copy pathmain.ts
File metadata and controls
599 lines (553 loc) · 23.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
import {
MarkdownView,
Notice,
Plugin,
TFile,
WorkspaceLeaf
} from "obsidian";
import {
appendReply,
validateReplyText,
type SourceEdit,
} from "./src/operations";
import { criticDecorationsExtension } from "./src/editor/decorations";
import { FinalizeModal } from "./src/finalize";
import { makeReadingPostProcessor } from "./src/reading";
import { REVIEW_VIEW_TYPE, ReviewPanelView, type PanelHost } from "./src/panel/view";
import { parse } from "./src/parser";
import {
appendAgentReplyEdit,
buildReviewItems,
createAnchoredCommentEdit,
findThreadReviewItem,
resolveLinkedSuggestionEdits,
threadReviewItemSignature,
} from "./src/review-model";
import { clamp, getVaultBasePath, truncateForNotice } from "./src/util";
import {
AGENT_PRESETS,
DEFAULT_SETTINGS,
getActiveAgent,
mergeSettings,
type Agent,
type AgentCommentsSettings,
type AgentPresetKey,
} from "./src/settings/types";
import { AgentCommentsSettingTab } from "./src/settings/tab";
import { SourceSync } from "./src/source-sync";
import {
buildAllReviewItemsPrompt,
buildCurrentNotePrompt,
buildReplyAgentPrompt,
buildSelectedReviewItemsPrompt,
buildSelectionPrompt,
} from "./src/agent/prompts";
import { OverlayController, type OverlayHost, type SelectionContext } from "./src/ui/overlays";
// `agent/process`, `notify`, and `terminal` statically import Node builtins
// (child_process/fs/path) and a PTY bridge. Importing them eagerly would make
// the whole plugin fail to load on mobile, where those APIs do not exist. They
// back desktop-only features (background replies, terminal, shell notifications),
// so they are loaded lazily via `await import(...)` at their call sites; the
// review/comment UI itself has no Node dependency and runs on mobile.
const RIBBON_ICON = "messages-square";
export default class AgentCommentsPlugin extends Plugin {
settings: AgentCommentsSettings = DEFAULT_SETTINGS;
private sourceSync!: SourceSync;
private overlays!: OverlayController;
async onload(): Promise<void> {
await this.loadSettings();
this.sourceSync = new SourceSync({
app: this.app,
getReviewView: () => this.getReviewView(),
isRevealMarkupEnabled: () => this.settings.revealMarkupOnCommentJump,
});
this.overlays = new OverlayController(this.createOverlayHost());
this.registerView(REVIEW_VIEW_TYPE, (leaf) => this.makeReviewView(leaf));
this.registerEditorExtension(
criticDecorationsExtension({
onOpenPanel: (offset) => this.handleInlineClick(offset),
onSuggestionClick: (offset, event) => this.overlays.openSuggestionPopover(offset, event),
shouldOpenPanel: (event) => this.settings.clickMarksToOpenPanel || event.metaKey || event.ctrlKey
})
);
this.registerMarkdownPostProcessor(
makeReadingPostProcessor(() => ({
showComments: this.settings.readingShowComments
}))
);
const ribbonIcon = this.addRibbonIcon(RIBBON_ICON, "Open Agent Comments", () => void this.openReviewPanel());
ribbonIcon.addClass("agent-comments-ribbon-action");
this.registerDomEvent(document, "pointerdown", (event) => this.overlays.handlePointerDown(event));
this.registerDomEvent(document, "pointerup", () => this.overlays.handlePointerUp());
this.registerDomEvent(document, "keyup", () => this.overlays.handleKeyup());
this.registerDomEvent(document, "selectionchange", () => this.overlays.handleSelectionChange());
this.registerDomEvent(window, "scroll", () => this.overlays.handleScroll(), true);
this.addCommand({
id: "open-agent-comments-review",
name: "Open Agent Comments review",
callback: () => void this.openReviewPanel()
});
this.addCommand({
id: "open-agent-comments-terminal",
name: "Open Agent Comments terminal",
callback: () => void this.openTerminal()
});
this.addCommand({
id: "add-agent-comments-comment-to-selection",
name: "Add Agent Comments comment to selection",
editorCallback: () => {
if (!this.overlays.openCommentComposerForActiveSelection()) {
new Notice("Select text first.");
}
}
});
this.addCommand({
id: "send-selection-to-agent-comments-terminal",
name: "Send selection to Agent Comments terminal",
checkCallback: (checking) => {
const context = this.overlays.captureSelectionContext();
if (!context) return false;
if (!checking) void this.sendSelectionToTerminal(context);
return true;
}
});
this.addCommand({
id: "send-current-note-to-terminal",
name: "Send current note to Agent Comments terminal",
checkCallback: (checking) => {
const file = this.getActiveMarkdownFile();
if (!file) return false;
if (!checking) void this.sendCurrentNoteToTerminal(file);
return true;
}
});
this.addCommand({
id: "send-filepath-to-terminal",
name: "Send filepath to Agent Comments terminal",
checkCallback: (checking) => {
const file = this.getActiveMarkdownFile();
if (!file) return false;
if (!checking) void this.sendFilepathToTerminal(file);
return true;
}
});
this.addCommand({
id: "send-all-review-items-to-terminal",
name: "Send all review items to Agent Comments terminal",
checkCallback: (checking) => {
const file = this.getActiveMarkdownFile();
if (!file) return false;
if (!checking) void this.sendAllReviewItemsToTerminal(file);
return true;
}
});
this.addCommand({
id: "finalize-for-publish",
name: "Finalize CriticMarkup for publish",
checkCallback: (checking) => {
const file = this.getActiveMarkdownFile();
if (!file) return false;
if (!checking) void this.runFinalize(file);
return true;
}
});
this.addSettingTab(new AgentCommentsSettingTab(this.app, this));
}
onunload(): void {
this.overlays?.dispose();
}
async loadSettings(): Promise<void> {
const stored = ((await this.loadData()) ?? {}) as Partial<AgentCommentsSettings>;
this.settings = mergeSettings(stored);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
private uniqueAgentId(base: string): string {
const cleaned = base.trim() || "agent";
if (!this.settings.agents[cleaned]) return cleaned;
let n = 2;
while (this.settings.agents[`${cleaned}-${n}`]) n++;
return `${cleaned}-${n}`;
}
async addAgentFromPreset(key: string): Promise<void> {
const preset = AGENT_PRESETS[key as AgentPresetKey];
if (!preset) return;
const id = this.uniqueAgentId(preset.id);
this.settings.agents = { ...this.settings.agents, [id]: { ...preset, id } };
this.settings.activeAgentId = id;
await this.saveSettings();
}
async addBlankAgent(): Promise<string> {
const id = this.uniqueAgentId("agent");
const agent: Agent = {
id,
displayName: "New agent",
reply: { command: "", extraArgs: "", envVars: "", timeoutSeconds: 120 },
terminal: { launchCommand: "", startupDelayMs: 1000 }
};
this.settings.agents = { ...this.settings.agents, [id]: agent };
this.settings.activeAgentId = id;
await this.saveSettings();
return id;
}
async deleteAgent(id: string): Promise<boolean> {
if (Object.keys(this.settings.agents).length <= 1) {
new Notice("Keep at least one agent.");
return false;
}
const next = { ...this.settings.agents };
delete next[id];
this.settings.agents = next;
if (this.settings.activeAgentId === id) this.settings.activeAgentId = Object.keys(next)[0] ?? null;
await this.saveSettings();
return true;
}
async setActiveAgent(id: string): Promise<void> {
if (!this.settings.agents[id]) return;
this.settings.activeAgentId = id;
await this.saveSettings();
}
availablePresets(): { key: string; label: string }[] {
const existing = new Set(Object.values(this.settings.agents).map((agent) => agent.displayName.toLowerCase()));
return (Object.keys(AGENT_PRESETS) as AgentPresetKey[])
.filter((key) => !existing.has(AGENT_PRESETS[key].displayName.toLowerCase()))
.map((key) => ({ key, label: AGENT_PRESETS[key].displayName }));
}
openPluginSettings(): void {
const setting = (this.app as unknown as { setting?: { open?: () => void; openTabById?: (id: string) => void } }).setting;
setting?.open?.();
setting?.openTabById?.(this.manifest.id);
}
async testReplyAgent(target?: Agent): Promise<string> {
const agent = target ?? getActiveAgent(this.settings);
if (!agent?.reply.command.trim()) return "Background reply agent command is empty.";
const cwd = getVaultBasePath(this.app);
if (!cwd) throw new Error("Could not resolve vault folder for agent cwd.");
const { runReplyAgent } = await import("./src/agent/process");
const stdout = await runReplyAgent(
cwd,
"Return exactly AGENT_COMMENTS_HEALTH_OK and do not inspect files.",
agent.reply
);
return stdout.includes("AGENT_COMMENTS_HEALTH_OK")
? "Background reply agent responded."
: `Background reply agent ran, but returned unexpected output: ${truncateForNotice(stdout)}`;
}
async testTerminalAgent(target?: Agent): Promise<string> {
const agent = target ?? getActiveAgent(this.settings);
const launchCommand = agent?.terminal.launchCommand ?? "";
if (!launchCommand.trim()) return "Terminal agent launch command is empty.";
const { splitShellArgs, buildProcessEnv, resolveExecutable } = await import("./src/agent/process");
const command = splitShellArgs(launchCommand)[0] ?? launchCommand.trim().split(/\s+/, 1)[0];
if (!command) return "Terminal agent launch command is empty.";
const env = buildProcessEnv("");
const cwd = getVaultBasePath(this.app) ?? undefined;
const resolved = resolveExecutable(command, env, cwd);
if (!resolved) throw new Error(`Command not found or not executable: ${command}`);
return `Terminal agent command found: ${command}`;
}
async testNotification(): Promise<void> {
const { notifyAgentReply } = await import("./src/notify");
notifyAgentReply("Agent Comments", "Test notification.", this.settings.notifications);
}
rerenderReadingViews(): void {
this.app.workspace.getLeavesOfType("markdown").forEach((leaf) => {
const view = leaf.view;
if (view instanceof MarkdownView) view.previewMode?.rerender(true);
});
}
getActiveMarkdownFile(): TFile | null {
const file = this.app.workspace.getActiveFile();
return file instanceof TFile && file.extension === "md" ? file : null;
}
async openReviewPanel(): Promise<ReviewPanelView | null> {
const existing = this.app.workspace.getLeavesOfType(REVIEW_VIEW_TYPE);
if (existing.length > 0) {
await this.app.workspace.revealLeaf(existing[0]);
return existing[0].view instanceof ReviewPanelView ? existing[0].view : null;
}
const leaf = this.app.workspace.getRightLeaf(false);
if (!leaf) {
new Notice("Could not open Agent Comments review.");
return null;
}
await leaf.setViewState({ type: REVIEW_VIEW_TYPE, active: true });
await this.app.workspace.revealLeaf(leaf);
return leaf.view instanceof ReviewPanelView ? leaf.view : null;
}
async openTerminal(): Promise<ReviewPanelView | null> {
const view = await this.openReviewPanel();
if (!view) return null;
await view.showTerminal();
return view;
}
async sendCurrentNoteToTerminal(file: TFile): Promise<void> {
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
await this.sendTerminalPrompt(buildCurrentNotePrompt(file.path, source));
}
async sendFilepathToTerminal(file: TFile): Promise<void> {
await this.sendTerminalPrompt(`File: ${file.path}`);
}
async sendAllReviewItemsToTerminal(file: TFile): Promise<void> {
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
await this.sendTerminalPrompt(buildAllReviewItemsPrompt(file.path, source));
}
async sendReviewItemsToTerminal(file: TFile, itemIds: string[]): Promise<void> {
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
const prompt = buildSelectedReviewItemsPrompt(file.path, source, itemIds);
if (prompt === null) {
new Notice("No selected review items to send.");
return;
}
await this.sendTerminalPrompt(prompt);
}
async sendSelectionToTerminal(context: SelectionContext): Promise<void> {
const file = this.resolveMarkdownFile(context.filePath);
if (!file) {
new Notice("Could not resolve selected Markdown file.");
return;
}
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
await this.sendTerminalPrompt(buildSelectionPrompt(file.path, source, context.startOffset, context.endOffset, context.selectedText));
}
private async sendTerminalPrompt(prompt: string): Promise<void> {
const terminal = await this.openTerminal();
if (!terminal) return;
await terminal.sendContextToTerminal(prompt);
}
private async insertCommentAtSelection(context: SelectionContext, comment: string): Promise<{ ok: boolean; file?: TFile; itemId?: string }> {
const text = comment.trim();
if (!text) {
new Notice("Write a comment first.");
return { ok: false };
}
const validationError = validateReplyText(text);
if (validationError) {
new Notice(validationError);
return { ok: false };
}
const file = this.resolveMarkdownFile(context.filePath);
if (!file) {
new Notice("Could not resolve selected Markdown file.");
return { ok: false };
}
let edit: SourceEdit;
try {
edit = createAnchoredCommentEdit(context.markdown, context.startOffset, context.endOffset, text);
} catch (error) {
new Notice(error instanceof Error ? error.message : String(error));
return { ok: false };
}
const applied = await this.sourceSync.applyEditsToFile(file, [edit]);
if (!applied) return { ok: false };
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
const item = buildReviewItems(source, parse(source)).find((candidate) => candidate.from === context.startOffset && candidate.kind === "thread");
await this.openReviewPanel();
this.getReviewView()?.focusOffset(file, context.startOffset);
this.overlays.hideSelectionPopover();
return { ok: true, file, itemId: item?.id };
}
async sendThreadToBackgroundAgent(file: TFile, itemId: string, replyText: string): Promise<boolean> {
const agent = getActiveAgent(this.settings);
if (!agent?.reply.command.trim()) {
new Notice("Configure a background reply agent command first.");
return false;
}
const { runReplyAgent } = await import("./src/agent/process");
const { notifyAgentReply } = await import("./src/notify");
const view = this.getReviewView();
let userReplyAccepted = false;
try {
let source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
let parsed = parse(source);
let items = buildReviewItems(source, parsed);
let target = findThreadReviewItem(items, itemId);
if (!target) {
new Notice("Select a comment thread first.");
view?.clearItemRunState(itemId);
return false;
}
const targetSignature = threadReviewItemSignature(target);
const userReply = replyText.trim();
if (userReply) {
const validationError = validateReplyText(userReply);
if (validationError) {
new Notice(validationError);
view?.clearItemRunState(itemId);
return false;
}
const edit = appendReply(source, target.thread, parsed, userReply);
const applied = await this.sourceSync.applyEditsToFile(file, [edit]);
if (!applied) {
view?.clearItemRunState(itemId);
return false;
}
userReplyAccepted = true;
source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
parsed = parse(source);
items = buildReviewItems(source, parsed);
target = findThreadReviewItem(items, itemId, targetSignature);
if (!target) {
new Notice("Thread moved after appending reply.");
view?.clearItemRunState(itemId);
return userReplyAccepted;
}
}
const beforeRun = source;
await this.app.vault.modify(file, beforeRun);
const cwd = getVaultBasePath(this.app);
if (!cwd) throw new Error("Could not resolve vault folder for agent cwd.");
view?.setItemRunState(itemId, { label: "Starting agent..." });
const stdout = await runReplyAgent(
cwd,
buildReplyAgentPrompt(file.path, beforeRun, items, target),
agent.reply,
(elapsedSeconds) => {
if (view?.hasItemRunState(itemId)) {
view.setItemRunState(itemId, { label: `Agent running... ${elapsedSeconds}s` });
}
},
);
const afterRun = await this.app.vault.read(file);
if (afterRun === beforeRun && stdout.trim()) {
if (view?.hasItemRunState(itemId)) view.setItemRunState(itemId, { label: "Adding agent reply..." });
const fallbackParsed = parse(beforeRun);
const fallbackTarget = findThreadReviewItem(buildReviewItems(beforeRun, fallbackParsed), itemId, targetSignature);
if (fallbackTarget) {
const edit = appendAgentReplyEdit(
beforeRun,
fallbackParsed,
fallbackTarget,
agent.displayName || "Agent",
stdout,
);
await this.sourceSync.applyEditsToFile(file, [edit]);
}
} else {
if (view?.hasItemRunState(itemId)) view.setItemRunState(itemId, { label: "Applying agent changes..." });
this.sourceSync.syncExternalSourceToEditor(file, beforeRun, afterRun);
}
view?.clearItemRunState(itemId);
new Notice("Agent reply finished.");
notifyAgentReply("Agent Comments", "Agent reply finished.", this.settings.notifications);
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
view?.setItemRunState(itemId, { label: `Agent failed: ${message}`, error: true });
new Notice(`Agent reply failed: ${message}`, 8000);
notifyAgentReply("Agent Comments", `Agent reply failed: ${message}`, this.settings.notifications);
return userReplyAccepted;
}
}
private createOverlayHost(): OverlayHost {
return {
app: this.app,
getActiveMarkdownFile: () => this.getActiveMarkdownFile(),
getCurrentSource: (file) => this.sourceSync.getCurrentSource(file),
applyEdits: (file, edits) => this.sourceSync.applyEditsToFile(file, edits),
applySuggestionEdit: (file, suggestionOffset, edit, resolveLinkedThread) =>
this.applySuggestionEdit(file, suggestionOffset, edit, resolveLinkedThread),
openReviewPanel: () => this.openReviewPanel(),
getReviewView: () => this.getReviewView(),
sendSelectionToTerminal: (context) => this.sendSelectionToTerminal(context),
insertCommentAtSelection: (context, comment) => this.insertCommentAtSelection(context, comment),
sendThreadToBackgroundAgent: (file, itemId, replyText) => this.sendThreadToBackgroundAgent(file, itemId, replyText),
};
}
private async applySuggestionEdit(
file: TFile,
suggestionOffset: number,
edit: SourceEdit,
resolveLinkedThread: boolean,
): Promise<boolean> {
const source = this.sourceSync.getCurrentSource(file) ?? await this.app.vault.cachedRead(file);
const items = buildReviewItems(source, parse(source));
return this.sourceSync.applyEditsToFile(file, resolveLinkedSuggestionEdits(source, items, suggestionOffset, edit, resolveLinkedThread));
}
private makeReviewView(leaf: WorkspaceLeaf): ReviewPanelView {
const host: PanelHost = {
getActiveFile: () => this.getActiveMarkdownFile(),
getCurrentSource: (file) => this.sourceSync.getCurrentSource(file),
applyEdits: (file, edits) => this.sourceSync.applyEditsToFile(file, edits),
revealOffset: (file, offset, length, flashChip) => {
this.sourceSync.revealOffsetInEditor(file, offset, length, flashChip ?? false);
},
isFileOpen: (file) => this.sourceSync.findEditorForFile(file) !== null,
sendFilepathToTerminal: async (file) => {
await this.sendFilepathToTerminal(file);
},
sendAllReviewItemsToTerminal: async (file) => {
await this.sendAllReviewItemsToTerminal(file);
},
sendReviewItemsToTerminal: async (file, itemIds) => {
await this.sendReviewItemsToTerminal(file, itemIds);
},
sendReviewItemToTerminal: async (file, itemId) => {
await this.sendReviewItemsToTerminal(file, [itemId]);
},
sendThreadToAgent: async (file, itemId, replyText) => {
return this.sendThreadToBackgroundAgent(file, itemId, replyText);
},
getUserDisplayName: () => this.settings.userDisplayName.trim() || "user",
getCommentTextZoom: () => clamp(this.settings.commentTextZoom || 1, 0.75, 1.8),
setCommentTextZoomLocal: (value) => {
this.settings.commentTextZoom = clamp(value, 0.75, 1.8);
},
saveCommentTextZoom: async () => {
await this.saveSettings();
},
getVaultBasePath: () => getVaultBasePath(this.app),
getDefaultTerminalAgent: () => {
const agent = getActiveAgent(this.settings);
if (!agent) return null;
return {
displayName: agent.displayName,
launchCommand: agent.terminal.launchCommand,
startupDelayMs: agent.terminal.startupDelayMs,
};
},
listAgents: () => Object.values(this.settings.agents).map((agent) => ({ id: agent.id, displayName: agent.displayName })),
getActiveAgentId: () => this.settings.activeAgentId,
getActiveAgentLabel: () => {
const agent = getActiveAgent(this.settings);
if (!agent) return "Choose agent…";
const configured = agent.reply.command.trim() || agent.terminal.launchCommand.trim();
return configured ? agent.displayName : "Choose agent…";
},
isActiveAgentConfigured: () => {
const agent = getActiveAgent(this.settings);
return !!agent && (!!agent.reply.command.trim() || !!agent.terminal.launchCommand.trim());
},
setActiveAgent: (id) => this.setActiveAgent(id),
availablePresets: () => this.availablePresets(),
addAgentFromPreset: (key) => this.addAgentFromPreset(key),
openPluginSettings: () => this.openPluginSettings(),
};
return new ReviewPanelView(leaf, host);
}
private handleInlineClick(offset: number): void {
void (async () => {
await this.openReviewPanel();
const file = this.getActiveMarkdownFile();
const view = this.getReviewView();
if (file && view) view.focusOffset(file, offset);
})();
}
private getReviewView(): ReviewPanelView | null {
for (const leaf of this.app.workspace.getLeavesOfType(REVIEW_VIEW_TYPE)) {
if (leaf.view instanceof ReviewPanelView) return leaf.view;
}
return null;
}
private async runFinalize(file: TFile): Promise<void> {
const source = await this.app.vault.cachedRead(file);
new FinalizeModal(this.app, file, source, this.settings.finalize, async (edits) => {
await this.sourceSync.applyEditsToFile(file, edits, { expectedSource: source, requireAll: true });
}).open();
}
private resolveMarkdownFile(path: string): TFile | null {
const file = this.app.vault.getAbstractFileByPath(path);
return file instanceof TFile && file.extension === "md" ? file : null;
}
}