@fresh-editor/fresh-editor 0.3.4 → 0.3.6

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,77 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.3.6
4
+
5
+ This version includes a major internal refactoring to support multiple windows in a single Fresh process. The work will be used to add a multi-window orchestrator in a future version.
6
+
7
+ ### Features
8
+
9
+ * **Go to line with selection** (#1764, thanks @PavelLoparev!): New action extends the selection from cursor to target line. Supports absolute and relative (`+10`) jumps.
10
+
11
+ * **File explorer "follow active buffer"** (#1569, #1803, thanks @ko3n1g!): New `file_explorer.follow_active_buffer` setting (default off). When on, switching tabs expands and highlights the corresponding file without stealing focus.
12
+
13
+ * **Live Diff**: Near-identical lines now render as modified lines, with new words decorated as underline (instead of two separate add/remove rows). Deletion virtual lines get gutter markers. Fixed live diff colors in `terminal` theme.
14
+
15
+ * **Auto-start `vi_mode`** (#1086, reported by @jmcblane): Vi mode plugin now exposes `toggle / enable / disable` so `init.ts` can flip vi mode at startup. Add this to your `init.ts` to enable: `editor.getPluginApi("vi-mode")?.enable();`.
16
+
17
+ In the future I plan to add a way for plugins to register custom config sections in the Settings UI, and then enabling vi-mode auto-start will be available through the UI.
18
+
19
+ ### Improvements
20
+
21
+ * **openSUSE install instructions** (#1897, thanks @ilmanzo!) added to the README.
22
+
23
+ * **Quick Open ranking**: Matches at the start of a path segment now beat arbitrary substring hits — `ts` surfaces `tsconfig.json` and `tsc/...` before random `.ts` files.
24
+
25
+ * **Search & Replace no longer freezes** on result sets in the thousands.
26
+
27
+ * **Rust code actions** (#1915): `rust-analyzer` now returns `WorkspaceEdit`-based assists like "Fill struct fields" instead of "No code actions available" — Fresh wasn't advertising the capability.
28
+
29
+ * **Status-bar indicator menus**: Clicking another indicator's icon dismisses the currently-open menu instead of stacking a second one on top.
30
+
31
+ * **LSP-Servers popup is extensible**: Plugins (starting with the bundled `rust-lsp`) can contribute their own rows.
32
+
33
+ * **`terminal` built-in theme** (#1914): Diff backgrounds no longer collide with syntax foregrounds, so keywords on deletion lines and strings on addition lines stay readable on terminals (e.g. xfce-terminal) where each ANSI palette index renders identically for fg and bg.
34
+
35
+ * **Dashboard** matches the editor background and no longer slides in.
36
+
37
+ * **AppImage install script** has error handling and a `/tmp` fallback so it no longer silently fails when the target directory isn't writable.
38
+
39
+ ### Bug Fixes
40
+
41
+ * **Crash on startup from a corrupt workspace** (#1939, reported by @zdooder): Restoring a session whose persisted split pointed at a buffer no longer in the buffer map crashed in `render.rs`. The active-buffer pointer is now validated and falls back to a live buffer if it's stale.
42
+
43
+ * **Completion popup + multicursor** (#1901, reported by @dtwilliamson): Typing a word character or `Backspace` while the popup was open only edited the primary cursor — secondary cursors silently went out of sync after the first keystroke. Accepting a completion now also applies at every cursor.
44
+
45
+ * **LSP indicator click** (#1941): Bugs fixed: plugin popup stacked under the built-in one; plugin popup ignored the active theme's `popup_bg`; state stayed stale after the LSP process was killed externally; "Disable LSP" didn't actually stop the running server; repeated clicks stacked further popups.
46
+
47
+ * **Scrollbar theme overrides** (#1554, reported by @klonuo): User themes setting `ui.scrollbar_track_fg`, `ui.scrollbar_track_hover_fg`, or `ui.scrollbar_thumb_hover_fg` had their overrides silently dropped.
48
+
49
+ * **Utility dock routing** (#1932): `Ctrl+P` → filename now opens the file in the editor area instead of inside the bottom dock when the dock has focus. The split tab-bar `□` button now maximizes the split you clicked, not the active one.
50
+
51
+ * **Tab drag**: Dropping a tab onto a fresh split (e.g. a Search & Replace result row) no longer panics on the next keystroke.
52
+
53
+ * **File explorer**: A directory whose `.gitignore` filters out all its contents (e.g. `build/` with `*` inside) stays visible when expanded.
54
+
55
+ * **Live Diff visual-line motion**: `Down`/`Up` no longer freezes when traversing a deletion block that starts with a blank line.
56
+
57
+ * **`S-Tab` plugin bindings** now register correctly (terminals deliver Shift+Tab as `BackTab`).
58
+
59
+ ## 0.3.5
60
+
61
+ ### Improvements
62
+
63
+ * **Live Grep overlay polish**: Surrounding editor is now visibly dimmed; shortcut hints and the active provider moved into a toolbar row with plainer labels; match cap raised from 100 to 1000 (with a `1000+ matches` indicator); provider errors render as a disabled result entry instead of a silent "0 matches"; `ripgrep` provider renamed to `rg`.
64
+
65
+ ### Bug Fixes
66
+
67
+ * **Live Grep on Windows returned no results**: `git grep` outputs `\r\n`; splitting on `\n` left a trailing `\r` that broke the result regex. Split on `\r?\n` instead.
68
+
69
+ * **Plain `grep` provider was broken**: `--column` is a ripgrep-only flag and was being passed unconditionally. Removed.
70
+
71
+ ### Under the Hood
72
+
73
+ * **Plugin API: `setPromptTitle` takes styled segments** (`{ text, style? }[]`) instead of a single string, so plugins control hint colouring directly instead of the renderer guessing structure from punctuation.
74
+
3
75
  ## 0.3.4
4
76
 
5
77
  ### Features
package/README.md CHANGED
@@ -65,7 +65,8 @@ Or, pick your preferred method:
65
65
  | Windows | [winget](#windows-winget) |
66
66
  | Arch Linux | [AUR](#arch-linux-aur) |
67
67
  | Debian/Ubuntu | [.deb](#debianubuntu-deb) |
68
- | Fedora/RHEL | [.rpm](#fedorarhelopensuse-rpm), [Terra](https://terra.fyralabs.com/) |
68
+ | Fedora/RHEL | [.rpm](#fedorarhel-rpm), [Terra](https://terra.fyralabs.com/) |
69
+ | OpenSUSE | [zypper](#opensuse-zypper), [.rpm](#fedorarhel-rpm) |
69
70
  | FreeBSD | [ports / pkg](https://www.freshports.org/editors/fresh) |
70
71
  | Gentoo | [GURU](#gentoo-guru) |
71
72
  | Linux (any distro) | [AppImage](#appimage), [Flatpak](#flatpak) |
@@ -133,7 +134,7 @@ curl -sL $(curl -s https://api.github.com/repos/sinelaw/fresh/releases/latest |
133
134
 
134
135
  Or download the `.deb` file manually from the [releases page](https://github.com/sinelaw/fresh/releases).
135
136
 
136
- ### Fedora/RHEL/openSUSE (.rpm)
137
+ ### Fedora/RHEL (.rpm)
137
138
 
138
139
  Download and install the latest release:
139
140
 
@@ -143,6 +144,12 @@ curl -sL $(curl -s https://api.github.com/repos/sinelaw/fresh/releases/latest |
143
144
 
144
145
  Or download the `.rpm` file manually from the [releases page](https://github.com/sinelaw/fresh/releases).
145
146
 
147
+ ### OpenSUSE (zypper)
148
+
149
+ ```bash
150
+ zypper install fresh-editor
151
+ ```
152
+
146
153
  ### Gentoo ([GURU](https://wiki.gentoo.org/wiki/Project:GURU))
147
154
 
148
155
  Enable the repository as read in [Project:GURU/Information for End Users](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users) then emerge the package:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -133,7 +133,8 @@
133
133
  "width": "30%",
134
134
  "preview_tabs": true,
135
135
  "side": "left",
136
- "auto_open_on_last_buffer_close": true
136
+ "auto_open_on_last_buffer_close": true,
137
+ "follow_active_buffer": false
137
138
  }
138
139
  },
139
140
  "file_browser": {
@@ -952,6 +953,11 @@
952
953
  "description": "Automatically focus the file explorer when the last buffer is\nclosed. Set to `false` for a \"blank workspace\" workflow where\nnothing opens automatically and the user explicitly invokes the\nfile explorer (e.g. via keybinding or command palette).\nDefault: true",
953
954
  "type": "boolean",
954
955
  "default": true
956
+ },
957
+ "follow_active_buffer": {
958
+ "description": "When the file explorer sidebar is open, automatically expand the\ntree and highlight the file that corresponds to the active buffer\nwhenever you switch tabs. Set to `true` to keep the explorer\nselection in sync with the active tab.\nDefault: false",
959
+ "type": "boolean",
960
+ "default": false
955
961
  }
956
962
  }
957
963
  },
@@ -201,11 +201,6 @@ type RegisteredSection = {
201
201
  let dashboardBufferId: number | null = null;
202
202
  let fetchToken = 0; // bumped each open; late fetches from a prior open no-op.
203
203
 
204
- // Id of the in-flight slide-in, so we can cancel it when starting a
205
- // new one (on content change) or when the dashboard is closed
206
- // mid-slide. Null once the animation settles or is cleared.
207
- let activeAnimationId: number | null = null;
208
-
209
204
  // Hash of all entries at the last paint (post-focus-highlight too —
210
205
  // it's what ultimately lands in the virtual buffer). Used to decide
211
206
  // whether setVirtualBufferContent needs to run at all: identical
@@ -213,43 +208,11 @@ let activeAnimationId: number | null = null;
213
208
  // round-trip entirely.
214
209
  let lastPaintedFullKey: string | null = null;
215
210
 
216
- // Hash of the entries with the clock stamp stripped. Animations only
217
- // fire when THIS hash changes, so the 1 Hz clock tick on the top
218
- // frame updates in place without re-sliding the whole dashboard.
219
- // Keyboard focus changes don't move this hash either (the hash is
220
- // taken before the focus overlay is laid on top), so Tab/Shift-Tab
221
- // pan the highlight without re-animating.
222
- let lastPaintedStructuralKey: string | null = null;
223
-
224
211
  // focusedIndex the last successful setVirtualBufferContent ran with.
225
- // Paired with the keys above so we can tell "focus moved but section
226
- // data is the same" (update VB for the highlight, no animation).
212
+ // Paired with the key above so we can tell "focus moved but content
213
+ // is the same" and still update VB for the highlight.
227
214
  let lastPaintedFocusedIndex = -1;
228
215
 
229
- // Matches an HH:MM:SS clock stamp. Anything shaped like that is
230
- // stripped from the structural hash so clock ticks don't animate.
231
- // The frame renderer is the only dashboard author that emits such a
232
- // string; if a third-party section happens to show a value in the
233
- // same shape, the worst case is "we don't re-animate when that
234
- // value changes" — acceptable noise floor.
235
- const CLOCK_RE = /\d\d:\d\d:\d\d/g;
236
-
237
- // Edge the slide-in enters from. Maps 1:1 to the plugin API's `from`
238
- // field and is resolved from config (plugins.dashboard.slide_from) on
239
- // each paint() so hot-reload of the setting Just Works. Defaults to
240
- // "right" (new content pushes in from the right, old exits left).
241
- type SlideFrom = "top" | "bottom" | "left" | "right";
242
- function resolveSlideFrom(): SlideFrom {
243
- const config = editor.getConfig() as Record<string, unknown> | null;
244
- const plugins = config?.plugins as Record<string, unknown> | undefined;
245
- const dashCfg = plugins?.dashboard as Record<string, unknown> | undefined;
246
- const raw = dashCfg?.slide_from;
247
- if (raw === "top" || raw === "bottom" || raw === "left" || raw === "right") {
248
- return raw;
249
- }
250
- return "right";
251
- }
252
-
253
216
  // Registered sections, in render order. Built-ins are registered at
254
217
  // plugin load (see the bottom of this file); third-party plugins
255
218
  // append via the exported `registerSection` API.
@@ -735,6 +698,13 @@ function paint(dims?: { width: number; height: number }) {
735
698
  const entries: TextPropertyEntry[] = [];
736
699
  for (let i = 0; i < topPad; i++) entries.push({ text: "\n" });
737
700
  for (const e of drawToEntries(drawn)) entries.push(e);
701
+ // Pad below the frame so the buffer covers the full viewport height.
702
+ // Without this, rows past the last frame line render with
703
+ // `editor.after_eof_bg` (a deliberate shade off from `editor.bg` to mark
704
+ // end-of-file in code buffers) and show up as a different-colored strip
705
+ // at the bottom of the dashboard.
706
+ const bottomPad = Math.max(0, height - topPad - frameHeight);
707
+ for (let i = 0; i < bottomPad; i++) entries.push({ text: "\n" });
738
708
 
739
709
  // Translate frame-relative row actions to absolute buffer rows by
740
710
  // shifting by the vertical padding we just prepended. Columns are
@@ -775,32 +745,15 @@ function paint(dims?: { width: number; height: number }) {
775
745
  ((focusedIndex % targets.length) + targets.length) % targets.length;
776
746
  }
777
747
 
778
- // Two hashes, taken BEFORE the focus highlight goes on top:
779
- // fullKey — everything including the clock. Drives the
780
- // setVirtualBufferContent skip check, so the
781
- // clock still redraws in place every second.
782
- // structuralKey — clock stamps stripped. Drives the animation.
783
- // A clock tick alone does not flip this, so it
784
- // updates silently; a real section data change
785
- // does, and the slide fires.
748
+ // Taken BEFORE the focus highlight goes on top — fullKey captures
749
+ // everything (including the clock) that ultimately lands in the
750
+ // virtual buffer. Drives the setVirtualBufferContent skip check,
751
+ // so the clock still redraws in place every second.
786
752
  const fullKey = JSON.stringify(entries);
787
- const structuralKey = fullKey.replace(CLOCK_RE, "##:##:##");
788
753
  const fullChanged = fullKey !== lastPaintedFullKey;
789
- const structuralChanged = structuralKey !== lastPaintedStructuralKey;
790
754
  const focusChanged = focusedIndex !== lastPaintedFocusedIndex;
791
- // A resize / viewport-shape change reshapes frame padding, dash
792
- // runs, and centering, which flips the structural hash even when
793
- // section data is unchanged. We repaint so the new layout takes
794
- // effect but skip the slide — nothing NEW showed up, the user is
795
- // just resizing a window. openDashboard clears lastPaintedW/H to
796
- // -1 so the first paint after open doesn't trip this guard.
797
- const dimsChanged =
798
- lastPaintedW !== -1 &&
799
- lastPaintedH !== -1 &&
800
- (width !== lastPaintedW || height !== lastPaintedH);
801
-
802
- // Identical render → short-circuit. Nothing to push to the
803
- // buffer, nothing to animate.
755
+
756
+ // Identical render short-circuit. Nothing to push to the buffer.
804
757
  if (!fullChanged && !focusChanged) {
805
758
  return;
806
759
  }
@@ -838,25 +791,7 @@ function paint(dims?: { width: number; height: number }) {
838
791
  lastPaintedW = width;
839
792
  lastPaintedH = height;
840
793
  lastPaintedFullKey = fullKey;
841
- lastPaintedStructuralKey = structuralKey;
842
794
  lastPaintedFocusedIndex = focusedIndex;
843
-
844
- // Structural-change-driven re-animation: fire only when the
845
- // section payload actually differs AND the dashboard isn't just
846
- // reshaping in place (clock tick, focus move, and resize all
847
- // land here without animating). Cancel any in-flight slide
848
- // first so the new one snapshots the fresh content.
849
- if (structuralChanged && !dimsChanged) {
850
- if (activeAnimationId !== null) {
851
- editor.cancelAnimation(activeAnimationId);
852
- }
853
- activeAnimationId = editor.animateVirtualBuffer(bufferId, {
854
- kind: "slideIn",
855
- from: resolveSlideFrom(),
856
- durationMs: 520,
857
- delayMs: 0,
858
- });
859
- }
860
795
  }
861
796
 
862
797
  // Open a URL in the user's browser via the platform's "open" helper.
@@ -1613,12 +1548,8 @@ async function openDashboard() {
1613
1548
  }
1614
1549
 
1615
1550
  // Clear the content/focus keys and dims so the first paint after
1616
- // open is treated as a content change and the slide-in fires.
1617
- // Dim reset is needed because open is the one case where we DO
1618
- // want the animation despite "dims changed" (there was no prior
1619
- // dimension, so the change is really "buffer just appeared").
1551
+ // open is treated as a content change.
1620
1552
  lastPaintedFullKey = null;
1621
- lastPaintedStructuralKey = null;
1622
1553
  lastPaintedFocusedIndex = -1;
1623
1554
  lastPaintedW = -1;
1624
1555
  lastPaintedH = -1;
@@ -1694,10 +1625,6 @@ registerHandler(
1694
1625
  // If the dashboard itself was closed, clear our handle so we'll
1695
1626
  // re-open on the next "last tab closed" event.
1696
1627
  if (dashboardBufferId !== null && e.buffer_id === dashboardBufferId) {
1697
- if (activeAnimationId !== null) {
1698
- editor.cancelAnimation(activeAnimationId);
1699
- activeAnimationId = null;
1700
- }
1701
1628
  dashboardBufferId = null;
1702
1629
  return;
1703
1630
  }
@@ -1717,10 +1644,6 @@ registerHandler(
1717
1644
  "dashboardOnAfterFileOpen",
1718
1645
  (_e: { buffer_id: number; path: string }) => {
1719
1646
  if (dashboardBufferId === null) return;
1720
- if (activeAnimationId !== null) {
1721
- editor.cancelAnimation(activeAnimationId);
1722
- activeAnimationId = null;
1723
- }
1724
1647
  editor.closeBuffer(dashboardBufferId);
1725
1648
  dashboardBufferId = null;
1726
1649
  },
@@ -48,8 +48,10 @@ async function searchWithGitGrep(query: string): Promise<GrepMatch[]> {
48
48
  );
49
49
 
50
50
  if (result.exit_code === 0) {
51
- return parseGrepOutput(result.stdout, 100) as GrepMatch[];
51
+ return parseGrepOutput(result.stdout, 100, (msg) => editor.debug(msg)) as GrepMatch[];
52
52
  }
53
+ editor.error(`[git_grep] process exited with code ${result.exit_code}: ${result.stderr}`);
54
+ editor.setStatus(`git grep failed (exit ${result.exit_code})`);
53
55
  return [];
54
56
  }
55
57