Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/PLUGINS_LIB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Plugins Lib Notes

Status and next steps for reusing and extending `plugins/lib`.

## Existing Helpers (usable now)
- `PanelManager`: open/close a virtual-buffer panel in a split, remember the source split/buffer, update content.
- `NavigationController<T>`: manage a selected index with wrap, status updates, and selection-change callback.
- `VirtualBufferFactory`: thin helpers for creating virtual buffers (current split, existing split, or new split) with sensible defaults.

## Completed Refactors

### Plugins now using PanelManager + NavigationController:
- `find_references.ts` - uses `PanelManager` for panel lifecycle and `NavigationController` for reference selection
- `search_replace.ts` - uses `PanelManager` for panel lifecycle and `NavigationController` for result management

### Moved to tests/plugins (sample/example code):
- `diagnostics_panel.ts` - refactored to use lib, moved since it uses dummy sample data (not real LSP diagnostics)

### Not refactored (different architecture):
- `git_log.ts` - uses a multi-view stacking pattern (log → commit detail → file view) where buffers are swapped within the same split. This doesn't fit `PanelManager`'s single-panel model. Could benefit from `VirtualBufferFactory` for buffer creation, but the complexity is in view state management and highlighting.

## Remaining Low-Impact Refactors
- Prompt-driven pickers (`git_grep.ts`, `git_find_file.ts`) could share a tiny prompt helper in `plugins/lib/` instead of wiring three prompt event handlers and result caches in each plugin. (Helper not written yet; see below.)

## Missing Primitives (would simplify mature plugins)
- Git:
- `editor.gitDiff({ path, against? }): Array<{ type: "added" | "modified" | "deleted"; startLine: number; lineCount: number }>` or a higher-level `editor.setLineIndicatorsFromGitDiff(bufferId, opts)` to replace `git_gutter.ts`'s diff parsing.
- `editor.gitBlame({ path, commit? }): BlameBlock[]` (hash/author/summary/line ranges) to replace porcelain parsing and block grouping in `git_blame.ts`.
- `editor.gitFiles(): string[]` and `editor.gitGrep(query, opts): GitGrepResult[]` to drop custom spawn/parse in git find/grep/search-replace.
- Prompt convenience:
- `PromptController` helper (could live in `plugins/lib`) that owns `startPrompt`, `prompt_changed/confirmed/cancelled` wiring, and suggestion cache. This would collapse the repeated glue in git grep/find-file into a few lines.
- Line/byte ergonomics:
- Line-based virtual line helper (`addVirtualLineAtLine` or `byteOffsetAtLine`) so blame headers don't need custom byte-offset tables.
- Possibly a `setOverlaysForLines` helper to batch per-line overlays/indicators.

## Next Actions
1) ~~Refactor the list-based plugins to use `PanelManager` + `NavigationController`~~ ✓ Done
2) Add a `PromptController` to `plugins/lib` and adopt it in `git_grep.ts` / `git_find_file.ts`.
3) Design editor-level git helpers (diff/blame/files/grep) and line-position helpers; once added, simplify `git_gutter.ts` and `git_blame.ts` around them.
1,202 changes: 678 additions & 524 deletions docs/visual-regression/screenshots/Comprehensive_UI_B_01_state_b.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 42 additions & 67 deletions plugins/find_references.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference path="../types/fresh.d.ts" />

import { PanelManager, NavigationController } from "./lib/index.ts";

/**
* Find References Plugin (TypeScript)
*
Expand All @@ -8,18 +10,15 @@
* Uses cursor movement for navigation (Up/Down/j/k work naturally).
*/

// Panel state
let panelOpen = false;
let referencesBufferId: number | null = null;
let sourceSplitId: number | null = null;
let referencesSplitId: number | null = null; // Track the split we created
let currentReferences: ReferenceItem[] = [];
let currentSymbol: string = "";
let lineCache: Map<string, string[]> = new Map(); // Cache file contents

// Maximum number of results to display
const MAX_RESULTS = 100;

// Line cache for file contents
let lineCache: Map<string, string[]> = new Map();

// Current symbol being searched
let currentSymbol: string = "";

// Reference item structure
interface ReferenceItem {
file: string;
Expand All @@ -28,6 +27,13 @@ interface ReferenceItem {
lineText?: string; // Cached line text
}

// Panel and navigation state
const panel = new PanelManager("*References*", "references-list");
const nav = new NavigationController<ReferenceItem>({
itemLabel: "Reference",
wrap: false,
});

// Define the references mode with minimal keybindings
// Navigation uses normal cursor movement (arrows, j/k work naturally)
editor.defineMode(
Expand Down Expand Up @@ -75,25 +81,26 @@ function formatReference(item: ReferenceItem): string {
// Build entries for the virtual buffer
function buildPanelEntries(): TextPropertyEntry[] {
const entries: TextPropertyEntry[] = [];
const references = nav.getItems();

// Header with symbol name
const totalCount = currentReferences.length;
const totalCount = references.length;
const limitNote = totalCount >= MAX_RESULTS ? ` (limited to ${MAX_RESULTS})` : "";
const symbolDisplay = currentSymbol ? `'${currentSymbol}'` : "symbol";
entries.push({
text: `═══ References to ${symbolDisplay} (${totalCount}${limitNote}) ═══\n`,
properties: { type: "header" },
});

if (currentReferences.length === 0) {
if (references.length === 0) {
entries.push({
text: " No references found\n",
properties: { type: "empty" },
});
} else {
// Add each reference
for (let i = 0; i < currentReferences.length; i++) {
const ref = currentReferences[i];
for (let i = 0; i < references.length; i++) {
const ref = references[i];
entries.push({
text: formatReference(ref),
properties: {
Expand Down Expand Up @@ -164,54 +171,34 @@ async function loadLineTexts(references: ReferenceItem[]): Promise<void> {

// Show references panel
async function showReferencesPanel(symbol: string, references: ReferenceItem[]): Promise<void> {
// Only save the source split ID if panel is not already open
// (avoid overwriting it with the references split ID on subsequent calls)
if (!panelOpen) {
sourceSplitId = editor.getActiveSplitId();
}

// Limit results
const limitedRefs = references.slice(0, MAX_RESULTS);

// Set references and symbol
// Set symbol and references in navigation controller
currentSymbol = symbol;
currentReferences = limitedRefs;
nav.setItems(limitedRefs);

// Load line texts for preview
await loadLineTexts(currentReferences);

// Build panel entries
const entries = buildPanelEntries();
await loadLineTexts(limitedRefs);

// Create or update virtual buffer in horizontal split
// The panel_id mechanism will reuse the existing buffer/split if it exists
// Open or update panel using PanelManager
try {
referencesBufferId = await editor.createVirtualBufferInSplit({
name: "*References*",
mode: "references-list",
read_only: true,
entries: entries,
ratio: 0.7, // Original pane takes 70%, references takes 30%
panel_id: "references-panel",
show_line_numbers: false,
show_cursors: true, // Enable cursor for navigation
await panel.open({
entries: buildPanelEntries(),
ratio: 0.3,
});

panelOpen = true;
// Track the references split (it becomes active after creation)
referencesSplitId = editor.getActiveSplitId();

const limitMsg = references.length > MAX_RESULTS
? ` (showing first ${MAX_RESULTS})`
: "";
editor.setStatus(
`Found ${references.length} reference(s)${limitMsg} - ↑/↓ navigate, RET jump, q close`
);
editor.debug(`References panel opened with buffer ID ${referencesBufferId}, split ID ${referencesSplitId}`);
editor.debug(`References panel opened with buffer ID ${panel.bufferId}, split ID ${panel.splitId}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
editor.setStatus("Failed to open references panel");
editor.debug(`ERROR: createVirtualBufferInSplit failed: ${errorMessage}`);
editor.debug(`ERROR: panel.open failed: ${errorMessage}`);
}
}

Expand Down Expand Up @@ -242,7 +229,7 @@ globalThis.on_references_cursor_moved = function (data: {
new_position: number;
}): void {
// Only handle cursor movement in our references buffer
if (referencesBufferId === null || data.buffer_id !== referencesBufferId) {
if (panel.bufferId === null || data.buffer_id !== panel.bufferId) {
return;
}

Expand All @@ -253,8 +240,8 @@ globalThis.on_references_cursor_moved = function (data: {
// Line 0 is header, lines 1 to N are references
const refIndex = cursorLine - 1;

if (refIndex >= 0 && refIndex < currentReferences.length) {
editor.setStatus(`Reference ${refIndex + 1}/${currentReferences.length}`);
if (refIndex >= 0 && refIndex < nav.count) {
nav.selectedIndex = refIndex;
}
};

Expand All @@ -263,59 +250,47 @@ editor.on("cursor_moved", "on_references_cursor_moved");

// Hide references panel
globalThis.hide_references_panel = function (): void {
if (!panelOpen) {
if (!panel.isOpen) {
return;
}

if (referencesBufferId !== null) {
editor.closeBuffer(referencesBufferId);
}

// Close the split we created (if it exists and is different from source)
if (referencesSplitId !== null && referencesSplitId !== sourceSplitId) {
editor.closeSplit(referencesSplitId);
}

panelOpen = false;
referencesBufferId = null;
sourceSplitId = null;
referencesSplitId = null;
currentReferences = [];
panel.close();
nav.reset();
currentSymbol = "";
lineCache.clear();
editor.setStatus("References panel closed");
};

// Navigation: go to selected reference (based on cursor position)
globalThis.references_goto = function (): void {
if (currentReferences.length === 0) {
if (nav.isEmpty) {
editor.setStatus("No references to jump to");
return;
}

if (sourceSplitId === null) {
if (panel.sourceSplitId === null) {
editor.setStatus("Source split not available");
return;
}

if (referencesBufferId === null) {
if (panel.bufferId === null) {
return;
}

// Get text properties at cursor position
const props = editor.getTextPropertiesAtCursor(referencesBufferId);
editor.debug(`references_goto: props.length=${props.length}, referencesBufferId=${referencesBufferId}, sourceSplitId=${sourceSplitId}`);
const props = editor.getTextPropertiesAtCursor(panel.bufferId);
editor.debug(`references_goto: props.length=${props.length}, bufferId=${panel.bufferId}, sourceSplitId=${panel.sourceSplitId}`);

if (props.length > 0) {
editor.debug(`references_goto: props[0]=${JSON.stringify(props[0])}`);
const location = props[0].location as
| { file: string; line: number; column: number }
| undefined;
if (location) {
editor.debug(`references_goto: opening ${location.file}:${location.line}:${location.column} in split ${sourceSplitId}`);
editor.debug(`references_goto: opening ${location.file}:${location.line}:${location.column} in split ${panel.sourceSplitId}`);
// Open file in the source split, not the references split
editor.openFileInSplit(
sourceSplitId,
panel.sourceSplitId,
location.file,
location.line,
location.column || 0
Expand Down
Loading
Loading