logancyang_obsidian-copilot/src/editor/quickAskExtension.ts
Emt-lin 76520d0259
Add Editor "Quick Ask" Floating Panel + Persistent Selection Highlights (#2139)
* feat: implement floating quick command with AI-powered inline editing

Add a new floating quick command feature that allows users to interact
with AI directly from the editor. Key features include:

- Quick Ask panel with draggable modal interface
- Multiple interaction modes: ask, edit, and edit-direct
- Selection highlight and replace guard for safe text operations
- Streaming chat session support for real-time AI responses
- Context menu integration for quick access
- Resizable and draggable UI components

# Conflicts:
#	src/commands/CustomCommandChatModal.tsx
#	src/styles/tailwind.css

* feat: add persistent selection highlight for Chat panel

* refactor: extract persistent highlight factory and fix state consistency

* fix: improve popup positioning with flip logic and line-start trap fix

- Use selection.head instead of selection.to for anchor point
- Fix line-start trap: only apply when head is selection end
- Add flip logic: show above when not enough space below
- Add horizontal visibility check
- Single-line selection: center popup horizontally
- Multi-line selection: follow head position
- Cursor mode: align near cursor instead of centering
2026-02-03 17:37:33 -08:00

101 lines
3.4 KiB
TypeScript

/**
* CodeMirror 6 Extension for Quick Ask feature.
* Provides StateEffect and ViewPlugin for managing the Quick Ask overlay.
*
* NOTE: SelectionHighlight is managed by quickAskController.ts, not here.
* This avoids "Calls to EditorView.update are not allowed while an update is in progress" errors
* that occur when dispatching from within ViewPlugin.update().
*/
import { StateEffect } from "@codemirror/state";
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { QuickAskOverlay } from "@/components/quick-ask/QuickAskOverlay";
import type { QuickAskWidgetPayload } from "@/components/quick-ask/types";
/**
* StateEffect for showing/hiding the Quick Ask widget.
* Pass null to close, or a payload to open.
*/
export const quickAskWidgetEffect = StateEffect.define<QuickAskWidgetPayload | null>();
/**
* ViewPlugin that manages the QuickAskOverlay lifecycle.
* Responds to StateEffects and updates position on document changes.
*/
export const quickAskOverlayPlugin = ViewPlugin.fromClass(
class {
private overlay: QuickAskOverlay | null = null;
private pos: number | null = null;
private fallbackPos: number | null = null;
constructor(private readonly view: EditorView) {}
update(update: ViewUpdate) {
// Handle StateEffects
for (const tr of update.transactions) {
for (const effect of tr.effects) {
if (!effect.is(quickAskWidgetEffect)) continue;
const payload = effect.value;
if (!payload) {
// Close the overlay (highlight is handled by controller)
this.overlay?.destroy();
this.overlay = null;
this.pos = null;
this.fallbackPos = null;
continue;
}
// Close existing overlay before opening new one
this.overlay?.destroy();
this.pos = payload.pos;
this.fallbackPos = typeof payload.fallbackPos === "number" ? payload.fallbackPos : null;
// NOTE: SelectionHighlight is now managed by quickAskController.ts
// to avoid dispatch-during-update errors
this.overlay = new QuickAskOverlay(payload.options);
this.overlay.mount(payload.pos, this.fallbackPos);
}
}
// Update position and selection range on document changes
if (this.overlay && update.docChanged) {
// Update ReplaceGuard's range mapping (single source of truth for Replace)
const guard = this.overlay.getReplaceGuard();
if (guard?.onDocChanged) {
guard.onDocChanged(update.changes);
}
// Update anchor position for panel positioning
if (this.pos !== null) {
this.pos = update.changes.mapPos(this.pos);
}
if (this.fallbackPos !== null) {
this.fallbackPos = update.changes.mapPos(this.fallbackPos);
}
if (this.pos !== null) {
this.overlay.updatePosition(this.pos, this.fallbackPos);
}
// Trigger panel re-render to update Replace button disabled state
this.overlay.schedulePanelRerender();
}
}
destroy() {
// NOTE: SelectionHighlight cleanup is handled by quickAskController.ts
this.overlay?.destroy();
this.overlay = null;
this.pos = null;
this.fallbackPos = null;
}
}
);
/**
* Creates the Quick Ask CM6 extension array.
*/
export function createQuickAskExtension() {
return [quickAskOverlayPlugin];
}