@fresh-editor/fresh-editor 0.1.4

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.
Files changed (38) hide show
  1. package/.gitignore +2 -0
  2. package/LICENSE +117 -0
  3. package/README.md +54 -0
  4. package/binary-install.js +212 -0
  5. package/binary.js +126 -0
  6. package/install.js +4 -0
  7. package/npm-shrinkwrap.json +900 -0
  8. package/package.json +100 -0
  9. package/plugins/README.md +121 -0
  10. package/plugins/clangd_support.md +20 -0
  11. package/plugins/clangd_support.ts +323 -0
  12. package/plugins/color_highlighter.ts +302 -0
  13. package/plugins/diagnostics_panel.ts +308 -0
  14. package/plugins/examples/README.md +245 -0
  15. package/plugins/examples/async_demo.ts +165 -0
  16. package/plugins/examples/bookmarks.ts +329 -0
  17. package/plugins/examples/buffer_query_demo.ts +110 -0
  18. package/plugins/examples/git_grep.ts +262 -0
  19. package/plugins/examples/hello_world.ts +93 -0
  20. package/plugins/examples/virtual_buffer_demo.ts +116 -0
  21. package/plugins/find_references.ts +357 -0
  22. package/plugins/git_find_file.ts +298 -0
  23. package/plugins/git_grep.ts +188 -0
  24. package/plugins/git_log.ts +1283 -0
  25. package/plugins/lib/fresh.d.ts +849 -0
  26. package/plugins/lib/index.ts +24 -0
  27. package/plugins/lib/navigation-controller.ts +214 -0
  28. package/plugins/lib/panel-manager.ts +218 -0
  29. package/plugins/lib/types.ts +72 -0
  30. package/plugins/lib/virtual-buffer-factory.ts +158 -0
  31. package/plugins/manual_help.ts +243 -0
  32. package/plugins/markdown_compose.ts +1207 -0
  33. package/plugins/merge_conflict.ts +1811 -0
  34. package/plugins/path_complete.ts +163 -0
  35. package/plugins/search_replace.ts +481 -0
  36. package/plugins/todo_highlighter.ts +204 -0
  37. package/plugins/welcome.ts +74 -0
  38. package/run-fresh.js +4 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Fresh Editor Plugin Library
3
+ *
4
+ * Shared utilities for building LSP-related plugins with common patterns.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { PanelManager, NavigationController, VirtualBufferFactory } from "./lib/index.ts";
9
+ * import type { Location, RGB, PanelOptions } from "./lib/index.ts";
10
+ * ```
11
+ */
12
+
13
+ // Types
14
+ export type { RGB, Location, PanelOptions, PanelState, NavigationOptions, HighlightPattern } from "./types.ts";
15
+
16
+ // Panel Management
17
+ export { PanelManager } from "./panel-manager.ts";
18
+
19
+ // Navigation
20
+ export { NavigationController } from "./navigation-controller.ts";
21
+
22
+ // Buffer Creation
23
+ export { VirtualBufferFactory } from "./virtual-buffer-factory.ts";
24
+ export type { VirtualBufferOptions, SplitBufferOptions } from "./virtual-buffer-factory.ts";
@@ -0,0 +1,214 @@
1
+ /// <reference path="../../types/fresh.d.ts" />
2
+
3
+ import type { NavigationOptions } from "./types.ts";
4
+
5
+ /**
6
+ * NavigationController - Generic list navigation for panel plugins
7
+ *
8
+ * Handles the common pattern of:
9
+ * - Maintaining selected index
10
+ * - Boundary checking
11
+ * - Status message updates
12
+ * - Callback on selection change
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const nav = new NavigationController<DiagnosticItem>({
17
+ * itemLabel: "Diagnostic",
18
+ * onSelectionChange: (item, index) => {
19
+ * updateHighlight(index);
20
+ * }
21
+ * });
22
+ *
23
+ * // Set items when panel opens
24
+ * nav.setItems(diagnostics);
25
+ *
26
+ * // Navigation commands
27
+ * function next() { nav.next(); }
28
+ * function prev() { nav.prev(); }
29
+ * ```
30
+ */
31
+ export class NavigationController<T> {
32
+ private items: T[] = [];
33
+ private currentIndex: number = 0;
34
+ private options: NavigationOptions<T>;
35
+
36
+ constructor(options: NavigationOptions<T> = {}) {
37
+ this.options = {
38
+ itemLabel: "Item",
39
+ wrap: false,
40
+ ...options,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Set the items to navigate through
46
+ *
47
+ * @param items - Array of items
48
+ * @param resetIndex - Whether to reset index to 0 (default true)
49
+ */
50
+ setItems(items: T[], resetIndex: boolean = true): void {
51
+ this.items = items;
52
+ if (resetIndex) {
53
+ this.currentIndex = 0;
54
+ } else {
55
+ // Clamp to valid range
56
+ this.currentIndex = Math.min(this.currentIndex, Math.max(0, items.length - 1));
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get all items
62
+ */
63
+ getItems(): T[] {
64
+ return this.items;
65
+ }
66
+
67
+ /**
68
+ * Get the current selected index
69
+ */
70
+ get selectedIndex(): number {
71
+ return this.currentIndex;
72
+ }
73
+
74
+ /**
75
+ * Set the selected index directly
76
+ */
77
+ set selectedIndex(index: number) {
78
+ if (this.items.length === 0) return;
79
+ this.currentIndex = Math.max(0, Math.min(index, this.items.length - 1));
80
+ this.notifyChange();
81
+ }
82
+
83
+ /**
84
+ * Get the currently selected item
85
+ */
86
+ get selected(): T | null {
87
+ if (this.items.length === 0 || this.currentIndex >= this.items.length) {
88
+ return null;
89
+ }
90
+ return this.items[this.currentIndex];
91
+ }
92
+
93
+ /**
94
+ * Get the total number of items
95
+ */
96
+ get count(): number {
97
+ return this.items.length;
98
+ }
99
+
100
+ /**
101
+ * Check if there are any items
102
+ */
103
+ get isEmpty(): boolean {
104
+ return this.items.length === 0;
105
+ }
106
+
107
+ /**
108
+ * Move to the next item
109
+ */
110
+ next(): void {
111
+ if (this.items.length === 0) return;
112
+
113
+ if (this.options.wrap) {
114
+ this.currentIndex = (this.currentIndex + 1) % this.items.length;
115
+ } else {
116
+ this.currentIndex = Math.min(this.currentIndex + 1, this.items.length - 1);
117
+ }
118
+ this.notifyChange();
119
+ }
120
+
121
+ /**
122
+ * Move to the previous item
123
+ */
124
+ prev(): void {
125
+ if (this.items.length === 0) return;
126
+
127
+ if (this.options.wrap) {
128
+ this.currentIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
129
+ } else {
130
+ this.currentIndex = Math.max(this.currentIndex - 1, 0);
131
+ }
132
+ this.notifyChange();
133
+ }
134
+
135
+ /**
136
+ * Move to the first item
137
+ */
138
+ first(): void {
139
+ if (this.items.length === 0) return;
140
+ this.currentIndex = 0;
141
+ this.notifyChange();
142
+ }
143
+
144
+ /**
145
+ * Move to the last item
146
+ */
147
+ last(): void {
148
+ if (this.items.length === 0) return;
149
+ this.currentIndex = this.items.length - 1;
150
+ this.notifyChange();
151
+ }
152
+
153
+ /**
154
+ * Jump to a specific index
155
+ *
156
+ * @param index - Target index
157
+ */
158
+ jumpTo(index: number): void {
159
+ if (this.items.length === 0) return;
160
+ this.currentIndex = Math.max(0, Math.min(index, this.items.length - 1));
161
+ this.notifyChange();
162
+ }
163
+
164
+ /**
165
+ * Update the status message with current position
166
+ *
167
+ * @param customMessage - Optional custom message (overrides default)
168
+ */
169
+ showStatus(customMessage?: string): void {
170
+ if (this.items.length === 0) {
171
+ editor.setStatus(`No ${this.options.itemLabel}s`);
172
+ return;
173
+ }
174
+
175
+ const message = customMessage ||
176
+ `${this.options.itemLabel} ${this.currentIndex + 1}/${this.items.length}`;
177
+ editor.setStatus(message);
178
+ }
179
+
180
+ /**
181
+ * Reset the controller state
182
+ */
183
+ reset(): void {
184
+ this.items = [];
185
+ this.currentIndex = 0;
186
+ }
187
+
188
+ /**
189
+ * Find and select an item matching a predicate
190
+ *
191
+ * @param predicate - Function to test items
192
+ * @returns true if found and selected, false otherwise
193
+ */
194
+ findAndSelect(predicate: (item: T) => boolean): boolean {
195
+ const index = this.items.findIndex(predicate);
196
+ if (index !== -1) {
197
+ this.currentIndex = index;
198
+ this.notifyChange();
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ /**
205
+ * Internal: Notify about selection change
206
+ */
207
+ private notifyChange(): void {
208
+ this.showStatus();
209
+
210
+ if (this.options.onSelectionChange && this.selected !== null) {
211
+ this.options.onSelectionChange(this.selected, this.currentIndex);
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,218 @@
1
+ /// <reference path="../../types/fresh.d.ts" />
2
+
3
+ import type { PanelOptions, PanelState } from "./types.ts";
4
+
5
+ /**
6
+ * PanelManager - Manages panel lifecycle for split-view plugins
7
+ *
8
+ * Handles the common pattern of:
9
+ * - Opening a virtual buffer in a split
10
+ * - Tracking source split for navigation
11
+ * - Closing and restoring previous state
12
+ * - Updating panel content
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const panel = new PanelManager("diagnostics", "diagnostics-list");
17
+ *
18
+ * async function open() {
19
+ * await panel.open({ entries: buildEntries(), ratio: 0.3 });
20
+ * }
21
+ *
22
+ * function close() {
23
+ * panel.close();
24
+ * }
25
+ *
26
+ * function update() {
27
+ * panel.updateContent(buildEntries());
28
+ * }
29
+ * ```
30
+ */
31
+ export class PanelManager {
32
+ private state: PanelState = {
33
+ isOpen: false,
34
+ bufferId: null,
35
+ splitId: null,
36
+ sourceSplitId: null,
37
+ sourceBufferId: null,
38
+ };
39
+
40
+ /**
41
+ * Create a new PanelManager
42
+ *
43
+ * @param panelName - Display name for the panel (e.g., "*Diagnostics*")
44
+ * @param modeName - Mode name for keybindings (e.g., "diagnostics-list")
45
+ */
46
+ constructor(
47
+ private readonly panelName: string,
48
+ private readonly modeName: string
49
+ ) {}
50
+
51
+ /**
52
+ * Check if the panel is currently open
53
+ */
54
+ get isOpen(): boolean {
55
+ return this.state.isOpen;
56
+ }
57
+
58
+ /**
59
+ * Get the panel's buffer ID (null if not open)
60
+ */
61
+ get bufferId(): number | null {
62
+ return this.state.bufferId;
63
+ }
64
+
65
+ /**
66
+ * Get the panel's split ID (null if not open)
67
+ */
68
+ get splitId(): number | null {
69
+ return this.state.splitId;
70
+ }
71
+
72
+ /**
73
+ * Get the source split ID (where user was before opening panel)
74
+ */
75
+ get sourceSplitId(): number | null {
76
+ return this.state.sourceSplitId;
77
+ }
78
+
79
+ /**
80
+ * Get the source buffer ID (to restore when closing)
81
+ */
82
+ get sourceBufferId(): number | null {
83
+ return this.state.sourceBufferId;
84
+ }
85
+
86
+ /**
87
+ * Open the panel in a new split
88
+ *
89
+ * If already open, updates the content instead.
90
+ *
91
+ * @param options - Panel configuration
92
+ * @returns The buffer ID of the panel
93
+ */
94
+ async open(options: PanelOptions): Promise<number> {
95
+ const { entries, ratio = 0.3, showLineNumbers = false, editingDisabled = true } = options;
96
+
97
+ if (this.state.isOpen && this.state.bufferId !== null) {
98
+ // Panel already open - just update content
99
+ this.updateContent(entries);
100
+ return this.state.bufferId;
101
+ }
102
+
103
+ // Save current context
104
+ this.state.sourceSplitId = editor.getActiveSplitId();
105
+ this.state.sourceBufferId = editor.getActiveBufferId();
106
+
107
+ // Create virtual buffer in split
108
+ const bufferId = await editor.createVirtualBufferInSplit({
109
+ name: this.panelName,
110
+ mode: this.modeName,
111
+ read_only: true,
112
+ entries,
113
+ ratio,
114
+ panel_id: this.panelName,
115
+ show_line_numbers: showLineNumbers,
116
+ editing_disabled: editingDisabled,
117
+ });
118
+
119
+ // Track state
120
+ this.state.bufferId = bufferId;
121
+ this.state.splitId = editor.getActiveSplitId();
122
+ this.state.isOpen = true;
123
+
124
+ return bufferId;
125
+ }
126
+
127
+ /**
128
+ * Close the panel and restore previous state
129
+ */
130
+ close(): void {
131
+ if (!this.state.isOpen) {
132
+ return;
133
+ }
134
+
135
+ // Close the split containing the panel
136
+ if (this.state.splitId !== null) {
137
+ editor.closeSplit(this.state.splitId);
138
+ }
139
+
140
+ // Focus back on source split
141
+ if (this.state.sourceSplitId !== null) {
142
+ editor.focusSplit(this.state.sourceSplitId);
143
+ }
144
+
145
+ // Reset state
146
+ this.reset();
147
+ }
148
+
149
+ /**
150
+ * Update the panel content without closing/reopening
151
+ *
152
+ * @param entries - New entries to display
153
+ */
154
+ updateContent(entries: TextPropertyEntry[]): void {
155
+ if (!this.state.isOpen || this.state.bufferId === null) {
156
+ return;
157
+ }
158
+
159
+ editor.setVirtualBufferContent(this.state.bufferId, entries);
160
+ }
161
+
162
+ /**
163
+ * Reset panel state (called internally on close)
164
+ */
165
+ reset(): void {
166
+ this.state = {
167
+ isOpen: false,
168
+ bufferId: null,
169
+ splitId: null,
170
+ sourceSplitId: null,
171
+ sourceBufferId: null,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Focus the source split (useful for "goto" operations)
177
+ */
178
+ focusSource(): void {
179
+ if (this.state.sourceSplitId !== null) {
180
+ editor.focusSplit(this.state.sourceSplitId);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Focus the panel split
186
+ */
187
+ focusPanel(): void {
188
+ if (this.state.splitId !== null) {
189
+ editor.focusSplit(this.state.splitId);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Open a file in the source split (for navigation operations)
195
+ *
196
+ * @param filePath - Path to the file to open
197
+ * @param line - Line number to jump to (1-indexed)
198
+ * @param column - Column number to jump to (1-indexed)
199
+ */
200
+ async openInSource(filePath: string, line: number, column: number): Promise<void> {
201
+ if (this.state.sourceSplitId === null) {
202
+ return;
203
+ }
204
+
205
+ // Focus source split and open file
206
+ editor.focusSplit(this.state.sourceSplitId);
207
+ await editor.openFile(filePath);
208
+
209
+ // Jump to location
210
+ editor.gotoLine(line);
211
+ if (column > 1) {
212
+ editor.gotoColumn(column);
213
+ }
214
+
215
+ // Focus back on panel
216
+ this.focusPanel();
217
+ }
218
+ }
@@ -0,0 +1,72 @@
1
+ /// <reference path="./fresh.d.ts" />
2
+
3
+ /**
4
+ * Shared Types for Fresh Editor Plugin Library
5
+ *
6
+ * Common interfaces and types used across LSP-related plugins.
7
+ */
8
+
9
+ /**
10
+ * RGB color tuple for overlays and highlighting
11
+ */
12
+ export type RGB = [number, number, number];
13
+
14
+ /**
15
+ * File location with line and column
16
+ */
17
+ export interface Location {
18
+ file: string;
19
+ line: number;
20
+ column: number;
21
+ }
22
+
23
+ /**
24
+ * Options for opening a panel
25
+ */
26
+ export interface PanelOptions {
27
+ /** Text property entries to display */
28
+ entries: TextPropertyEntry[];
29
+ /** Split ratio (0.0 to 1.0), default 0.3 */
30
+ ratio?: number;
31
+ /** Whether to show line numbers, default false for panels */
32
+ showLineNumbers?: boolean;
33
+ /** Whether editing is disabled, default true for panels */
34
+ editingDisabled?: boolean;
35
+ }
36
+
37
+ /**
38
+ * State of a managed panel
39
+ */
40
+ export interface PanelState {
41
+ isOpen: boolean;
42
+ bufferId: number | null;
43
+ splitId: number | null;
44
+ sourceSplitId: number | null;
45
+ sourceBufferId: number | null;
46
+ }
47
+
48
+ /**
49
+ * Options for NavigationController
50
+ */
51
+ export interface NavigationOptions<T> {
52
+ /** Function to call when selection changes */
53
+ onSelectionChange?: (item: T, index: number) => void;
54
+ /** Label for status messages (e.g., "Diagnostic", "Reference") */
55
+ itemLabel?: string;
56
+ /** Whether to wrap around at boundaries */
57
+ wrap?: boolean;
58
+ }
59
+
60
+ /**
61
+ * Highlight pattern for syntax highlighting
62
+ */
63
+ export interface HighlightPattern {
64
+ /** Function to test if line matches */
65
+ match: (line: string) => boolean;
66
+ /** Color to apply */
67
+ rgb: RGB;
68
+ /** Whether to underline */
69
+ underline?: boolean;
70
+ /** Prefix for overlay IDs */
71
+ overlayIdPrefix?: string;
72
+ }
@@ -0,0 +1,158 @@
1
+ /// <reference path="../../types/fresh.d.ts" />
2
+
3
+ /**
4
+ * Options for creating a virtual buffer
5
+ */
6
+ export interface VirtualBufferOptions {
7
+ /** Display name (e.g., "*Commit Details*") */
8
+ name: string;
9
+ /** Mode name for keybindings */
10
+ mode: string;
11
+ /** Text property entries */
12
+ entries: TextPropertyEntry[];
13
+ /** Whether to show line numbers (default false) */
14
+ showLineNumbers?: boolean;
15
+ /** Whether editing is disabled (default true) */
16
+ editingDisabled?: boolean;
17
+ /** Whether buffer is read-only (default true) */
18
+ readOnly?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Options for creating a virtual buffer in a new split
23
+ */
24
+ export interface SplitBufferOptions extends VirtualBufferOptions {
25
+ /** Split ratio (default 0.3) */
26
+ ratio?: number;
27
+ /** Panel ID for idempotent operations */
28
+ panelId?: string;
29
+ }
30
+
31
+ /**
32
+ * VirtualBufferFactory - Simplified virtual buffer creation
33
+ *
34
+ * Provides convenience methods for creating virtual buffers with
35
+ * sensible defaults for read-only panel views.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Create buffer as a tab in current split (e.g., help, manual)
40
+ * const bufferId = await VirtualBufferFactory.create({
41
+ * name: "*Help*",
42
+ * mode: "help-manual",
43
+ * entries: helpEntries,
44
+ * });
45
+ *
46
+ * // Create buffer in existing split (e.g., commit detail view)
47
+ * const bufferId = await VirtualBufferFactory.createInSplit(splitId, {
48
+ * name: "*Commit Details*",
49
+ * mode: "git-commit-detail",
50
+ * entries: detailEntries,
51
+ * });
52
+ *
53
+ * // Create buffer in new split
54
+ * const bufferId = await VirtualBufferFactory.createWithSplit({
55
+ * name: "*References*",
56
+ * mode: "references-list",
57
+ * entries: refEntries,
58
+ * ratio: 0.4,
59
+ * });
60
+ * ```
61
+ */
62
+ export const VirtualBufferFactory = {
63
+ /**
64
+ * Create a virtual buffer as a new tab in the current split
65
+ * This is ideal for documentation, help panels, and content that should
66
+ * appear alongside other buffers rather than in a separate split.
67
+ *
68
+ * @param options - Buffer configuration
69
+ * @returns Buffer ID
70
+ */
71
+ async create(options: VirtualBufferOptions): Promise<number> {
72
+ const {
73
+ name,
74
+ mode,
75
+ entries,
76
+ showLineNumbers = false,
77
+ editingDisabled = true,
78
+ readOnly = true,
79
+ } = options;
80
+
81
+ return await editor.createVirtualBuffer({
82
+ name,
83
+ mode,
84
+ read_only: readOnly,
85
+ entries,
86
+ show_line_numbers: showLineNumbers,
87
+ editing_disabled: editingDisabled,
88
+ });
89
+ },
90
+
91
+ /**
92
+ * Create a virtual buffer in an existing split
93
+ *
94
+ * @param splitId - Target split ID
95
+ * @param options - Buffer configuration
96
+ * @returns Buffer ID
97
+ */
98
+ async createInSplit(splitId: number, options: VirtualBufferOptions): Promise<number> {
99
+ const {
100
+ name,
101
+ mode,
102
+ entries,
103
+ showLineNumbers = false,
104
+ editingDisabled = true,
105
+ readOnly = true,
106
+ } = options;
107
+
108
+ return await editor.createVirtualBufferInExistingSplit({
109
+ name,
110
+ mode,
111
+ read_only: readOnly,
112
+ entries,
113
+ split_id: splitId,
114
+ show_line_numbers: showLineNumbers,
115
+ editing_disabled: editingDisabled,
116
+ });
117
+ },
118
+
119
+ /**
120
+ * Create a virtual buffer in a new split
121
+ *
122
+ * @param options - Buffer and split configuration
123
+ * @returns Buffer ID
124
+ */
125
+ async createWithSplit(options: SplitBufferOptions): Promise<number> {
126
+ const {
127
+ name,
128
+ mode,
129
+ entries,
130
+ ratio = 0.3,
131
+ panelId,
132
+ showLineNumbers = false,
133
+ editingDisabled = true,
134
+ readOnly = true,
135
+ } = options;
136
+
137
+ return await editor.createVirtualBufferInSplit({
138
+ name,
139
+ mode,
140
+ read_only: readOnly,
141
+ entries,
142
+ ratio,
143
+ panel_id: panelId,
144
+ show_line_numbers: showLineNumbers,
145
+ editing_disabled: editingDisabled,
146
+ });
147
+ },
148
+
149
+ /**
150
+ * Update content of an existing virtual buffer
151
+ *
152
+ * @param bufferId - Buffer to update
153
+ * @param entries - New entries
154
+ */
155
+ updateContent(bufferId: number, entries: TextPropertyEntry[]): void {
156
+ editor.setVirtualBufferContent(bufferId, entries);
157
+ },
158
+ };