@fresh-editor/fresh-editor 0.3.5 → 0.3.7

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 (39) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +9 -2
  3. package/package.json +1 -1
  4. package/plugins/audit_mode.i18n.json +84 -0
  5. package/plugins/audit_mode.ts +139 -3
  6. package/plugins/config-schema.json +33 -3
  7. package/plugins/dashboard.ts +34 -111
  8. package/plugins/flash.ts +22 -4
  9. package/plugins/git_blame.ts +10 -6
  10. package/plugins/git_log.ts +705 -323
  11. package/plugins/git_statusbar.i18n.json +72 -0
  12. package/plugins/git_statusbar.ts +133 -0
  13. package/plugins/goto_with_selection.i18n.json +58 -0
  14. package/plugins/goto_with_selection.ts +17 -0
  15. package/plugins/lib/fresh.d.ts +911 -15
  16. package/plugins/lib/index.ts +34 -0
  17. package/plugins/lib/widgets.ts +903 -0
  18. package/plugins/live_diff.ts +442 -32
  19. package/plugins/merge_conflict.ts +89 -64
  20. package/plugins/orchestrator.ts +3425 -0
  21. package/plugins/pkg.ts +235 -54
  22. package/plugins/rust-lsp.ts +58 -40
  23. package/plugins/schemas/theme.schema.json +18 -0
  24. package/plugins/search_replace.i18n.json +140 -28
  25. package/plugins/search_replace.ts +1335 -515
  26. package/plugins/tab_actions.i18n.json +212 -0
  27. package/plugins/tab_actions.ts +76 -0
  28. package/plugins/theme_editor.i18n.json +112 -0
  29. package/plugins/theme_editor.ts +30 -5
  30. package/plugins/tsconfig.json +3 -0
  31. package/plugins/vi_mode.ts +49 -17
  32. package/themes/dark.json +1 -0
  33. package/themes/dracula.json +1 -0
  34. package/themes/high-contrast.json +1 -0
  35. package/themes/light.json +1 -0
  36. package/themes/nord.json +1 -0
  37. package/themes/nostalgia.json +1 -0
  38. package/themes/solarized-dark.json +1 -0
  39. package/themes/terminal.json +4 -0
@@ -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;
@@ -1655,18 +1586,20 @@ async function dashboardShowOrFocus() {
1655
1586
  registerHandler("dashboardShowOrFocus", dashboardShowOrFocus);
1656
1587
 
1657
1588
  // Auto-open resolution: the session override (set via the exported
1658
- // plugin API from init.ts) wins over the user config. We read from
1659
- // getUserConfig (raw file) rather than getConfig because unknown
1660
- // fields are dropped when the Config struct reserializes. Default
1661
- // is true.
1589
+ // plugin API from init.ts) wins over the user-configured value, which
1590
+ // comes from the typed plugin-config field declared below. The field
1591
+ // shows up in the Settings UI under "Plugin Settings → dashboard".
1592
+ editor.defineConfigBoolean("autoOpen", {
1593
+ default: true,
1594
+ description: "Show the dashboard automatically when Fresh starts with no real files open.",
1595
+ });
1596
+
1662
1597
  let autoOpenOverride: boolean | null = null;
1663
1598
 
1664
1599
  function autoOpenEnabled(): boolean {
1665
1600
  if (autoOpenOverride !== null) return autoOpenOverride;
1666
- const cfg = editor.getUserConfig() as Record<string, unknown> | null;
1667
- const plugins = cfg?.plugins as Record<string, unknown> | undefined;
1668
- const dashboard = plugins?.dashboard as Record<string, unknown> | undefined;
1669
- return dashboard?.["auto-open"] !== false;
1601
+ const cfg = (editor.getPluginConfig() ?? {}) as { autoOpen?: boolean };
1602
+ return cfg.autoOpen !== false;
1670
1603
  }
1671
1604
 
1672
1605
  function shouldShowDashboard(): boolean {
@@ -1694,10 +1627,6 @@ registerHandler(
1694
1627
  // If the dashboard itself was closed, clear our handle so we'll
1695
1628
  // re-open on the next "last tab closed" event.
1696
1629
  if (dashboardBufferId !== null && e.buffer_id === dashboardBufferId) {
1697
- if (activeAnimationId !== null) {
1698
- editor.cancelAnimation(activeAnimationId);
1699
- activeAnimationId = null;
1700
- }
1701
1630
  dashboardBufferId = null;
1702
1631
  return;
1703
1632
  }
@@ -1717,10 +1646,6 @@ registerHandler(
1717
1646
  "dashboardOnAfterFileOpen",
1718
1647
  (_e: { buffer_id: number; path: string }) => {
1719
1648
  if (dashboardBufferId === null) return;
1720
- if (activeAnimationId !== null) {
1721
- editor.cancelAnimation(activeAnimationId);
1722
- activeAnimationId = null;
1723
- }
1724
1649
  editor.closeBuffer(dashboardBufferId);
1725
1650
  dashboardBufferId = null;
1726
1651
  },
@@ -1878,12 +1803,14 @@ editor.exportPluginApi("dashboard", {
1878
1803
  // `plugins.dashboard.enabled` is true in the resolved config — so the
1879
1804
  // standard settings UI is the single enable/disable surface.
1880
1805
  //
1881
- // If the plugin loads mid-session (user toggles it on in Settings),
1882
- // the `ready` hook has already fired, so we also run an immediate
1883
- // check. At startup the `listBuffers().length > 0` guard keeps us
1884
- // dormant until the workspace has actually restored: plugins load
1885
- // before restore, and opening a buffer here would race with the
1886
- // restore and leave a stray Dashboard tab even when real files exist.
1806
+ // Auto-open is driven exclusively by the `ready` hook (and the
1807
+ // `buffer_closed` handler for the last-tab-closed case). We
1808
+ // deliberately do NOT auto-open at module load: dashboard.ts loads
1809
+ // during the startup plugin batch, *before* the user's init.ts has
1810
+ // been evaluated, so an immediate auto-open would race
1811
+ // `setAutoOpen(false)` and dismiss the user's preference. Users who
1812
+ // hot-load the plugin mid-session (toggle on in Settings) get the
1813
+ // dashboard via the "Show Dashboard" command in the palette.
1887
1814
  editor.on("ready", "dashboardOnReady");
1888
1815
  editor.on("buffer_closed", "dashboardOnBufferClosed");
1889
1816
  editor.on("viewport_changed", "dashboardOnViewportChanged");
@@ -1897,7 +1824,3 @@ editor.registerCommand(
1897
1824
  "Open the dashboard, or bring it to the front if it's already open",
1898
1825
  "dashboardShowOrFocus",
1899
1826
  );
1900
-
1901
- if (editor.listBuffers().length > 0 && shouldShowDashboard()) {
1902
- openDashboard();
1903
- }
package/plugins/flash.ts CHANGED
@@ -33,7 +33,24 @@ const VTEXT_PREFIX = "flash-";
33
33
  // the closest jump targets get the most comfortable keys. All
34
34
  // lowercase: case-sensitive matching keeps the label letter from also
35
35
  // being a valid pattern continuation, which matters for the skip rule.
36
- const LABEL_POOL = "asdfghjklqwertyuiopzxcvbnm";
36
+ editor.defineConfigString("labelPool", {
37
+ default: "asdfghjklqwertyuiopzxcvbnm",
38
+ description: "Characters used as jump labels, in comfort order. Labels are assigned to matches by distance from the cursor, so leftmost characters here land on the nearest matches.",
39
+ });
40
+ editor.defineConfigBoolean("skipRule", {
41
+ default: true,
42
+ description: "Skip a label character if it is also the next character after a match — prevents ambiguity between extending the search pattern and jumping.",
43
+ });
44
+
45
+ function flashSettings(): { labelPool: string; skipRule: boolean } {
46
+ const cfg = (editor.getPluginConfig() ?? {}) as { labelPool?: string; skipRule?: boolean };
47
+ return {
48
+ labelPool: cfg.labelPool && cfg.labelPool.length > 0
49
+ ? cfg.labelPool
50
+ : "asdfghjklqwertyuiopzxcvbnm",
51
+ skipRule: cfg.skipRule ?? true,
52
+ };
53
+ }
37
54
 
38
55
  interface Match {
39
56
  /** Byte offset where the match starts in its buffer. */
@@ -334,9 +351,10 @@ function assignLabels(
334
351
  prevLabelByKey: Map<string, string>,
335
352
  ): Match[] {
336
353
  if (matches.length === 0) return matches;
337
- const skip = buildSkipSet(matches, views, emptyPattern);
354
+ const { labelPool, skipRule } = flashSettings();
355
+ const skip = skipRule ? buildSkipSet(matches, views, emptyPattern) : new Set<string>();
338
356
  const remaining = new Set<string>();
339
- for (const c of LABEL_POOL) if (!skip.has(c)) remaining.add(c);
357
+ for (const c of labelPool) if (!skip.has(c)) remaining.add(c);
340
358
 
341
359
  const sorted = sortMatches(matches, startSplitId, startCursor);
342
360
 
@@ -353,7 +371,7 @@ function assignLabels(
353
371
  // matches in distance order. Iterate the pool in its native
354
372
  // (comfort-ranked) order so home-row letters go to nearest matches.
355
373
  const orderedRemaining: string[] = [];
356
- for (const c of LABEL_POOL) if (remaining.has(c)) orderedRemaining.push(c);
374
+ for (const c of labelPool) if (remaining.has(c)) orderedRemaining.push(c);
357
375
  let next = 0;
358
376
  for (const m of sorted) {
359
377
  if (m.label) continue;
@@ -87,10 +87,13 @@ const blameState: BlameState = {
87
87
  // Color Definitions for Header Styling
88
88
  // =============================================================================
89
89
 
90
- const colors = {
91
- headerFg: [0, 0, 0] as [number, number, number], // Black text
92
- headerBg: [200, 200, 200] as [number, number, number], // Light gray background
93
- };
90
+ // Blame headers are rendered via `addVirtualLine`, which accepts theme
91
+ // keys directly so we don't expose colors as plugin settings. Themes
92
+ // drive the look; if a theme lacks specific blame keys, these fall
93
+ // through to the editor's status-bar palette which is what every theme
94
+ // defines.
95
+ const HEADER_FG_KEY = "ui.status_bar_fg";
96
+ const HEADER_BG_KEY = "ui.status_bar_bg";
94
97
 
95
98
  // =============================================================================
96
99
  // Mode Definition
@@ -373,7 +376,8 @@ function addBlameHeaders(): void {
373
376
  // Clear existing headers first
374
377
  editor.clearVirtualTextNamespace(blameState.bufferId, BLAME_NAMESPACE);
375
378
 
376
- // Add a virtual line above each block
379
+ // Add a virtual line above each block. Pass theme keys so the headers
380
+ // restyle automatically when the user switches themes.
377
381
  for (const block of blameState.blocks) {
378
382
  const headerText = formatBlockHeader(block);
379
383
 
@@ -381,7 +385,7 @@ function addBlameHeaders(): void {
381
385
  blameState.bufferId,
382
386
  block.startByte, // anchor position
383
387
  headerText, // text content
384
- { fg: colors.headerFg, bg: colors.headerBg }, // colors (RGB tuples; passing theme key strings would also work)
388
+ { fg: HEADER_FG_KEY, bg: HEADER_BG_KEY },
385
389
  true, // above (LineAbove)
386
390
  BLAME_NAMESPACE, // namespace for bulk removal
387
391
  0 // priority