@fresh-editor/fresh-editor 0.3.8 → 0.3.9

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.3.9
4
+
5
+ ### Features
6
+
7
+ #### Universal Search (multi-scope Live Grep)
8
+
9
+ Live Grep grows into a universal search overlay: search across multiple **scopes** — project files, open **Buffers**, and **Terminals** scrollback — with **Word** and **Regex** search modes (each keybindable). The overlay gets a clickable widget toolbar (focusable provider button, toggle controls), a full-width header band, and inline status shown in the overlay itself. Closed-terminal scrollback is retained so it stays searchable (#2099).
10
+
11
+ #### Orchestrator & git worktrees
12
+
13
+ * **Attach sessions to existing git worktrees** and discover them automatically (#2095).
14
+ * Multi-select **bulk actions**, worktree ordering, and a *Show all worktrees* filter toggle.
15
+ * Draggable scrollbars on the picker lists, with bulk-pane reorder.
16
+
17
+ #### Editor
18
+
19
+ * **Move to Next / Previous Paragraph** actions, including translations (#2084, contributed by @PavelLoparev; requested in #2083).
20
+
21
+ #### Language Support
22
+
23
+ * **C3** (`.c3`) syntax highlighting and LSP (#2101, requested by @data-man).
24
+
25
+ #### Plugins & API
26
+
27
+ * New `getWorkingDataDir()` (per-working-dir data root) and `getTerminalDir()` plugin APIs.
28
+ * Overlay toolbar widget APIs — clickable `Toggle`/`Button` controls, `toggleOverlayToolbarWidget`, and a `Row { wrap }` layout that reflows toolbars across lines.
29
+
30
+ ### Improvements
31
+
32
+ * The **dashboard no longer opens automatically** on startup by default. To re-enable it, open **Settings**, select **Plugin: dashboard**, and turn on **AutoOpen** ("Show the dashboard automatically when Fresh starts with no real files open."). The dashboard remains available any time via the **Show Dashboard** command.
33
+ * Homebrew install simplified now that fresh-editor is in homebrew-core — `brew install fresh-editor`, no tap step.
34
+
35
+ ### Bug Fixes
36
+
37
+ * Plugin `getCursorLine()` now returns the real cursor line instead of `0` (#2076, reported by @pmburov).
38
+ * **Reduced serial-console lag** by not repainting on non-visual plugin async events (#2100, reported by @jetpax).
39
+ * **Custom theme colours** no longer silently ignored for many UI fields (#2080, contributed by @flexiondotorg; reported in #2079).
40
+ * `PageDown`/`PageUp` no longer overshoot on a single soft-wrapped line (#2085).
41
+ * **Workspace restore** no longer mixes tabs from multiple projects in one window (#2056, reported by @mandolyte).
42
+ * **Live Grep**: *Resume* keeps the query and opens the selected result; overlay preview and input-box undo/redo corrected; Buffers scope finds unmodified open buffers; non-file-scope previews and read-only data-dir files fixed.
43
+ * `getKeybindingLabel` resolves correctly for bound plugin actions.
44
+
45
+ ### Internal
46
+
47
+ * Major window refactor: window-scoped save/restore (`Window::from_workspace`), `WindowId`-parameterized persistence, `working_dir` derived from the active window, and `open_file`/LSP/watch helpers moved onto `Window`.
48
+ * Orchestrator bring-up characterization tests and fixtures across persistence layouts.
49
+ * Serial-console lag benchmark/diagnostic scripts; theme-key resolver schema-drift guard.
50
+ * Internal docs excluded from the public docs build.
51
+
3
52
  ## 0.3.8
4
53
 
5
54
  ### Features / Improvements
package/README.md CHANGED
@@ -84,7 +84,6 @@ On macOS and some linux distros (Bazzite/Bluefin/Aurora):
84
84
  > **Note:** On macOS, see [macOS Terminal Tips](https://getfresh.dev/docs/configuration/keyboard#macos-terminal-tips) for recommended terminal configuration.
85
85
 
86
86
  ```bash
87
- brew tap sinelaw/fresh
88
87
  brew install fresh-editor
89
88
  ```
90
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@ const editor = getEditor();
16
16
  // it is never loaded, so no buffers are created, no timers run,
17
17
  // and no network fetches fire.
18
18
  //
19
- // A second flag `plugins.dashboard.auto-open` (default true) gates
19
+ // A second flag `plugins.dashboard.auto-open` (default false) gates
20
20
  // only the ambient open paths (startup + last-buffer-closed). When
21
21
  // false the plugin still loads and the "Show Dashboard" command is
22
22
  // still available — it just won't appear on its own.
@@ -1590,7 +1590,7 @@ registerHandler("dashboardShowOrFocus", dashboardShowOrFocus);
1590
1590
  // comes from the typed plugin-config field declared below. The field
1591
1591
  // shows up in the Settings UI under "Plugin Settings → dashboard".
1592
1592
  editor.defineConfigBoolean("autoOpen", {
1593
- default: true,
1593
+ default: false,
1594
1594
  description: "Show the dashboard automatically when Fresh starts with no real files open.",
1595
1595
  });
1596
1596
 
@@ -1599,7 +1599,7 @@ let autoOpenOverride: boolean | null = null;
1599
1599
  function autoOpenEnabled(): boolean {
1600
1600
  if (autoOpenOverride !== null) return autoOpenOverride;
1601
1601
  const cfg = (editor.getPluginConfig() ?? {}) as { autoOpen?: boolean };
1602
- return cfg.autoOpen !== false;
1602
+ return cfg.autoOpen === true;
1603
1603
  }
1604
1604
 
1605
1605
  function shouldShowDashboard(): boolean {
@@ -46,8 +46,9 @@ function getCurrentLocation(): {
46
46
 
47
47
  // Helper: Get actual line number using the API
48
48
  function getCurrentLineCol(): { line: number; column: number } {
49
- // Use the actual getCursorLine API for accurate line number
50
- const lineNumber = editor.getCursorLine();
49
+ // Read the primary cursor's line from the snapshot. `line` is null when the
50
+ // buffer has no line index yet (huge files); fall back to 0 there.
51
+ const lineNumber = editor.getPrimaryCursor()?.line ?? 0;
51
52
 
52
53
  // Get cursor position within the line by reading buffer content
53
54
  const bufferId = editor.getActiveBufferId();
@@ -447,6 +447,33 @@ export class Finder<T> {
447
447
  // Mode flags
448
448
  private isPromptMode = false;
449
449
  private isPanelMode = false;
450
+ /** True when the active prompt is a centred floating overlay. Search
451
+ * status then goes to the overlay's own footer (visible inside the frame)
452
+ * rather than the editor status bar (off at the bottom, easy to miss). */
453
+ private isOverlay = false;
454
+
455
+ /** Present a search-status message where the user is actually looking: on
456
+ * the overlay's input row (right-aligned by the match count) for a
457
+ * floating overlay, else the editor status bar. */
458
+ private setSearchStatus(message: string): void {
459
+ if (this.isOverlay) {
460
+ this.editor.setPromptStatus(message);
461
+ } else {
462
+ this.editor.setStatus(message);
463
+ }
464
+ }
465
+
466
+ /** Report a successful search with `count` matches. In overlay mode the
467
+ * "N / total" count on the input row already conveys this, so the status
468
+ * is cleared to avoid duplicating it; the status bar (non-overlay) still
469
+ * shows "Found N matches". */
470
+ private reportFound(count: number): void {
471
+ if (this.isOverlay) {
472
+ this.editor.setPromptStatus("");
473
+ } else {
474
+ this.editor.setStatus(`Found ${count} matches`);
475
+ }
476
+ }
450
477
 
451
478
  // Handler names (for cleanup)
452
479
  private handlerPrefix: string;
@@ -513,6 +540,7 @@ export class Finder<T> {
513
540
 
514
541
  // Start the prompt
515
542
  const overlay = options.floatingOverlay === true;
543
+ this.isOverlay = overlay;
516
544
  if (options.initialQuery) {
517
545
  this.editor.startPromptWithInitial(
518
546
  options.title,
@@ -525,7 +553,7 @@ export class Finder<T> {
525
553
  const result = this.editor.startPrompt(options.title, this.config.id, overlay);
526
554
  this.editor.debug(`[Finder] startPrompt returned: ${result}`);
527
555
  }
528
- this.editor.setStatus("Type to search...");
556
+ this.setSearchStatus("Type to search");
529
557
  }
530
558
 
531
559
  /**
@@ -545,6 +573,11 @@ export class Finder<T> {
545
573
  // unchanged query.
546
574
  this.promptState.lastQuery = "";
547
575
  if (query.length === 0) return;
576
+ // The backend (or scope set) changed, so the on-screen results are now
577
+ // stale. Clear them and show progress immediately rather than leaving the
578
+ // previous output up while the (possibly slow) new search runs.
579
+ this.updatePromptResults([]);
580
+ this.setSearchStatus("Searching…");
548
581
  await this.runSearch(query, this.currentSource);
549
582
  }
550
583
 
@@ -713,9 +746,9 @@ export class Finder<T> {
713
746
  this.updatePromptResults(filtered);
714
747
 
715
748
  if (filtered.length > 0) {
716
- this.editor.setStatus(`Found ${filtered.length} matches`);
749
+ this.reportFound(filtered.length);
717
750
  } else {
718
- this.editor.setStatus("No matches");
751
+ this.setSearchStatus("No matches");
719
752
  }
720
753
  } else {
721
754
  // Search mode: run external search
@@ -745,6 +778,7 @@ export class Finder<T> {
745
778
  }
746
779
  this.editor.setPromptSuggestions([]);
747
780
  this.promptState.results = [];
781
+ this.setSearchStatus("");
748
782
  return;
749
783
  }
750
784
 
@@ -768,6 +802,11 @@ export class Finder<T> {
768
802
  }
769
803
  this.promptState.lastQuery = query;
770
804
 
805
+ // A search is now actually starting (every query change that gets here —
806
+ // typing, deleting, provider/scope refresh). Show pending status so the
807
+ // user sees the re-scan in progress rather than a stale result count.
808
+ this.setSearchStatus("Searching…");
809
+
771
810
  try {
772
811
  const searchResult = source.search(query);
773
812
 
@@ -793,21 +832,21 @@ export class Finder<T> {
793
832
  this.updatePromptResults(parsed);
794
833
 
795
834
  if (parsed.length > 0) {
796
- this.editor.setStatus(`Found ${parsed.length} matches`);
835
+ this.reportFound(parsed.length);
797
836
  // Show preview of first result
798
837
  if (this.shouldShowPreview()) {
799
838
  await this.updatePreview(this.promptState.entries[0]);
800
839
  }
801
840
  } else {
802
- this.editor.setStatus("No matches");
841
+ this.setSearchStatus("No matches");
803
842
  }
804
843
  } else if (result.exit_code === 1) {
805
844
  // No matches
806
845
  this.updatePromptResults([]);
807
- this.editor.setStatus("No matches");
846
+ this.setSearchStatus("No matches");
808
847
  } else if (result.exit_code !== -1) {
809
848
  // Error (ignore -1 which means killed)
810
- this.editor.setStatus(`Search error: ${result.stderr}`);
849
+ this.setSearchStatus(`Search error: ${result.stderr}`);
811
850
  }
812
851
  } else {
813
852
  // Promise<T[]>
@@ -821,12 +860,12 @@ export class Finder<T> {
821
860
  this.updatePromptResults(results);
822
861
 
823
862
  if (results.length > 0) {
824
- this.editor.setStatus(`Found ${results.length} matches`);
863
+ this.reportFound(results.length);
825
864
  if (this.shouldShowPreview()) {
826
865
  await this.updatePreview(this.promptState.entries[0]);
827
866
  }
828
867
  } else {
829
- this.editor.setStatus("No matches");
868
+ this.setSearchStatus("No matches");
830
869
  }
831
870
  }
832
871
  } catch (e) {
@@ -864,7 +903,15 @@ export class Finder<T> {
864
903
  (entry, i) => ({
865
904
  text: entry.label,
866
905
  description: entry.description,
867
- value: `${i}`,
906
+ // The preview pane uses `value` as the authoritative
907
+ // `path:line:col` for the result. We must not rely on parsing the
908
+ // user-facing label, which may carry source badges (e.g. "[term]")
909
+ // that make the label unparseable as a path.
910
+ value: entry.location
911
+ ? `${entry.location.file}:${entry.location.line}:${
912
+ entry.location.column ?? 1
913
+ }`
914
+ : `${i}`,
868
915
  disabled: false,
869
916
  })
870
917
  );
@@ -735,6 +735,13 @@ type CursorInfo = {
735
735
  start: number;
736
736
  end: number;
737
737
  } | null;
738
+ /**
739
+ * 0-indexed line number of the cursor. `None` when the line index is
740
+ * unavailable — e.g. a huge file whose line scan hasn't completed, where
741
+ * the editor positions purely by byte offset. Plugins must treat `null`
742
+ * as "unknown", never as line 0.
743
+ */
744
+ line: number | null;
738
745
  };
739
746
  type OverlayOptions = {
740
747
  /**
@@ -1695,7 +1702,13 @@ interface EditorAPI {
1695
1702
  */
1696
1703
  listSplits(): SplitSnapshot[];
1697
1704
  /**
1698
- * Get the line number (0-indexed) of the primary cursor
1705
+ * Get the line number (0-indexed) of the primary cursor.
1706
+ *
1707
+ * @deprecated Use `getPrimaryCursor()?.line` instead. This accessor cannot
1708
+ * represent "line index unavailable" (huge files before their line scan) —
1709
+ * it returns `0` in that case, indistinguishable from a real first line.
1710
+ * `getPrimaryCursor().line` is `number | null` and also covers every cursor
1711
+ * via `getAllCursors()`.
1699
1712
  */
1700
1713
  getCursorLine(): number;
1701
1714
  /**
@@ -1864,38 +1877,17 @@ interface EditorAPI {
1864
1877
  */
1865
1878
  getAuthorityLabel(): string;
1866
1879
  /**
1867
- * Current Workspace Trust level for the active project:
1868
- * `"restricted"`, `"trusted"`, or `"blocked"` (empty `""` when trust
1869
- * state is unavailable, e.g. the default local authority).
1870
- *
1871
- * Trust is a per-project, user-granted decision. Plugins that run
1872
- * repo-controlled work (env activation, project tooling, repo-local
1873
- * binaries) MUST gate on this and treat anything other than
1874
- * `"trusted"` as "do not execute".
1875
- */
1876
- workspaceTrustLevel(): "restricted" | "trusted" | "blocked" | "";
1877
- /**
1878
- * Activate an environment by setting the live env recipe: an activation
1879
- * shell `snippet` (e.g. `eval "$(direnv export bash)"`,
1880
- * `source .venv/bin/activate`, or `""` for a pure login shell) run in
1881
- * `dir` (defaults to the workspace). It is re-evaluated on demand on the
1882
- * active backend and applied to every spawn — language servers,
1883
- * formatters, `spawnProcess` — so they see the project environment. No
1884
- * authority rebuild; the LSP is restarted to pick it up.
1885
- *
1886
- * Honored only when `workspaceTrustLevel() === "trusted"` (it runs
1887
- * repo-controlled code). Call `clearEnv()` to deactivate.
1888
- */
1889
- setEnv(snippet: string, dir?: string): void;
1890
- /**
1891
- * Deactivate the environment set by `setEnv` — spawns return to the
1892
- * inherited environment.
1880
+ * Current Workspace Trust level for the active project: `"restricted"`,
1881
+ * `"trusted"`, or `"blocked"` (empty when unavailable). Exposed to JS as
1882
+ * `editor.workspaceTrustLevel()`. Plugins that run repo-controlled work
1883
+ * should treat anything other than `"trusted"` as "do not execute".
1893
1884
  */
1894
- clearEnv(): void;
1885
+ workspaceTrustLevel(): string;
1895
1886
  /**
1896
- * Whether an environment is currently active (a recipe was set via
1897
- * `setEnv`). Survives the restart `setEnv` triggers, so a plugin can
1898
- * re-establish its file watch and reflect activation after reloading.
1887
+ * Whether an environment is currently active (set via `editor.setEnv`).
1888
+ * Exposed to JS as `editor.envActive()`. Lets the env-manager plugin
1889
+ * reflect activation and re-establish its file watch after the restart
1890
+ * that `setEnv` triggers.
1899
1891
  */
1900
1892
  envActive(): boolean;
1901
1893
  /**
@@ -2134,6 +2126,21 @@ interface EditorAPI {
2134
2126
  */
2135
2127
  getDataDir(): string;
2136
2128
  /**
2129
+ * Per-working-directory data root for plugin state that should be scoped
2130
+ * to the current project root / worktree rather than shared across all of
2131
+ * them (`<data_dir>/workdirs/<encoded-cwd>/`). Prefer this over
2132
+ * `getDataDir()` for per-project state; the directory is not created for
2133
+ * you. Note: terminal scrollback and orchestrator state use their own
2134
+ * dedicated layouts (see `getTerminalDir()`), not this root.
2135
+ */
2136
+ getWorkingDataDir(): string;
2137
+ /**
2138
+ * Directory holding terminal scrollback backing files for the current
2139
+ * working directory. Each project root / worktree has its own subdir, so
2140
+ * a search can stay scoped to the active project's terminals.
2141
+ */
2142
+ getTerminalDir(): string;
2143
+ /**
2137
2144
  * Get themes directory path
2138
2145
  */
2139
2146
  getThemesDir(): string;
@@ -2472,6 +2479,29 @@ interface EditorAPI {
2472
2479
  */
2473
2480
  setPromptFooter(footer: StyledText[]): boolean;
2474
2481
  /**
2482
+ * Set the floating-overlay prompt's header toolbar as a `WidgetSpec`
2483
+ * (real, clickable `Toggle`/`Button` widgets), rendered in place of the
2484
+ * styled-text title. Give each control a `key` equal to the action it
2485
+ * should fire on click (e.g. `"live_grep_toggle_files"`). Pass `null` to
2486
+ * clear it. No visible effect on non-overlay prompts.
2487
+ */
2488
+ setPromptToolbar(spec: WidgetSpec | null): boolean;
2489
+ /**
2490
+ * Set the floating-overlay prompt's input-row status text, shown
2491
+ * right-aligned just left of the `selected / total` count (e.g.
2492
+ * "Searching…", "No matches"). Empty string clears it. No effect on
2493
+ * non-overlay prompts.
2494
+ */
2495
+ setPromptStatus(status: string): boolean;
2496
+ /**
2497
+ * Toggle a floating-overlay toolbar control by its widget `key`. The host
2498
+ * owns the toggle's checked state, flips it in place, and emits a
2499
+ * `widget_event` (`event_type: "toggle"`, payload `{ checked }`). Use this
2500
+ * to route a plugin's own keyboard shortcut through the same host path as
2501
+ * a click or Space on the toggle, then react in your `widget_event` handler.
2502
+ */
2503
+ toggleOverlayToolbarWidget(key: string): boolean;
2504
+ /**
2475
2505
  * Override the currently-highlighted suggestion row in the
2476
2506
  * open prompt. The editor clamps `index` to the suggestion
2477
2507
  * list's bounds and the renderer scrolls it into view on
@@ -2860,6 +2890,16 @@ interface EditorAPI {
2860
2890
  */
2861
2891
  clearAuthority(): void;
2862
2892
  /**
2893
+ * Activate an environment: set the live env recipe (`snippet` run in
2894
+ * `dir`). Applied to every spawn, re-evaluated on demand — no restart.
2895
+ * Honored only when the workspace is Trusted.
2896
+ */
2897
+ setEnv(snippet: string, dir: string | null): void;
2898
+ /**
2899
+ * Deactivate the environment — spawns return to the inherited env.
2900
+ */
2901
+ clearEnv(): void;
2902
+ /**
2863
2903
  * Override the Remote Indicator's displayed state. Plugins call
2864
2904
  * this to surface lifecycle transitions that the authority layer
2865
2905
  * doesn't know about yet — "Connecting" while `devcontainer up`
@@ -60,6 +60,14 @@ export function row(...children: WidgetSpec[]): WidgetSpec {
60
60
  return { kind: "row", children };
61
61
  }
62
62
 
63
+ /** Horizontal layout that **wraps**: children that don't fit on one line
64
+ * reflow onto additional lines (growing the row's height) instead of being
65
+ * truncated. Children are never split, so wrap a logical group (e.g. a
66
+ * toggle + its accelerator) in a nested `row(...)` to keep it intact. */
67
+ export function wrappingRow(...children: WidgetSpec[]): WidgetSpec {
68
+ return { kind: "row", children, wrap: true };
69
+ }
70
+
63
71
  /** Vertical layout. Children stacked top-to-bottom. */
64
72
  export function col(...children: WidgetSpec[]): WidgetSpec {
65
73
  return { kind: "col", children };