@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.
@@ -8,6 +8,7 @@ import {
8
8
  fetchCommitShow,
9
9
  fetchGitLog,
10
10
  } from "./lib/git_history.ts";
11
+ import { button, flexSpacer, key, list, row, WidgetPanel } from "./lib/index.ts";
11
12
 
12
13
  const editor = getEditor();
13
14
 
@@ -37,8 +38,15 @@ interface GitLogState {
37
38
  logBufferId: number | null;
38
39
  detailBufferId: number | null;
39
40
  toolbarBufferId: number | null;
40
- /** Click-regions for the toolbar's buttons, populated by `renderToolbar`. */
41
- toolbarButtons: ToolbarButton[];
41
+ /** Widget panel rendering the toolbar (Row of Buttons). Created in
42
+ * `show_git_log` once the buffer group exists; cleaned up in
43
+ * `git_log_close`. */
44
+ toolbarPanel: WidgetPanel | null;
45
+ /** Widget panel rendering the log (List of commit rows). Owns
46
+ * `selected_index` + `scroll_offset` as instance state — the
47
+ * plugin's `state.selectedIndex` mirrors what the host reports
48
+ * via `widget_event "select"`. */
49
+ logPanel: WidgetPanel | null;
42
50
  commits: GitCommit[];
43
51
  selectedIndex: number;
44
52
  /** Cached `git show` output for the currently-displayed detail commit. */
@@ -49,20 +57,13 @@ interface GitLogState {
49
57
  */
50
58
  pendingDetailId: number;
51
59
  /**
52
- * Debounce token for `cursor_moved`. Rapid cursor motion (PageDown, held
53
- * j/k) would otherwise trigger a full log re-render + `git show` per
54
- * intermediate row; we bump this id on every event and only do the work
55
- * after a short delay if no newer event has arrived.
60
+ * Debounce token for List `select` events. Rapid selection moves
61
+ * (PageDown, held j/k) would otherwise trigger a full `git show`
62
+ * spawn per intermediate row; we bump this id on every event
63
+ * and only do the work after a short delay if no newer event
64
+ * has arrived.
56
65
  */
57
- pendingCursorMoveId: number;
58
- /**
59
- * Byte offset at the start of each row in the rendered log panel, plus
60
- * the total buffer length at the end. Populated by `renderLog` so the
61
- * cursor_moved handler can map byte positions to commit indices without
62
- * relying on `getCursorLine` (which is not implemented for virtual
63
- * buffers).
64
- */
65
- logRowByteOffsets: number[];
66
+ pendingSelectId: number;
66
67
  }
67
68
 
68
69
  const state: GitLogState = {
@@ -71,54 +72,21 @@ const state: GitLogState = {
71
72
  logBufferId: null,
72
73
  detailBufferId: null,
73
74
  toolbarBufferId: null,
74
- toolbarButtons: [],
75
+ toolbarPanel: null,
76
+ logPanel: null,
75
77
  commits: [],
76
78
  selectedIndex: 0,
77
79
  detailCache: null,
78
80
  pendingDetailId: 0,
79
- pendingCursorMoveId: 0,
80
- logRowByteOffsets: [],
81
+ pendingSelectId: 0,
81
82
  };
82
83
 
83
84
  /**
84
- * Delay before reacting to `cursor_moved`. Long enough to collapse a burst
85
- * of events from held j/k or PageDown into a single render, short enough
86
- * that the detail panel still feels live.
85
+ * Delay before spawning `git show` after a List `select` event. Long
86
+ * enough to collapse a burst (held j/k or PageDown) into one fetch,
87
+ * short enough that the detail panel still feels live.
87
88
  */
88
- const CURSOR_DEBOUNCE_MS = 60;
89
-
90
- // UTF-8 byte length — the overlay API expects byte offsets; JS strings are
91
- // UTF-16. Matches the helper used by `lib/git_history.ts`.
92
- function utf8Len(s: string): number {
93
- let b = 0;
94
- for (let i = 0; i < s.length; i++) {
95
- const c = s.charCodeAt(i);
96
- if (c <= 0x7f) b += 1;
97
- else if (c <= 0x7ff) b += 2;
98
- else if (c >= 0xd800 && c <= 0xdfff) {
99
- b += 4;
100
- i++;
101
- } else b += 3;
102
- }
103
- return b;
104
- }
105
-
106
- /**
107
- * Binary search `logRowByteOffsets` for the 0-indexed row whose byte
108
- * offset is the largest one ≤ `bytePos`. Returns 0 on an empty table.
109
- */
110
- function rowFromByte(bytePos: number): number {
111
- const offs = state.logRowByteOffsets;
112
- if (offs.length === 0) return 0;
113
- let lo = 0;
114
- let hi = offs.length - 1;
115
- while (lo < hi) {
116
- const mid = (lo + hi + 1) >> 1;
117
- if (offs[mid] <= bytePos) lo = mid;
118
- else hi = mid - 1;
119
- }
120
- return lo;
121
- }
89
+ const SELECT_DEBOUNCE_MS = 60;
122
90
 
123
91
  // =============================================================================
124
92
  // Modes
@@ -129,15 +97,20 @@ function rowFromByte(bytePos: number): number {
129
97
  // the log, and opens the file at the cursor when pressed in the detail).
130
98
  // =============================================================================
131
99
 
132
- // j/k as vi-style aliases for Up/Down, plus the plugin-specific action
133
- // keys. Everything else (arrows, Page{Up,Down}, Home/End, Shift+motion for
134
- // selection, Ctrl+C copy, …) is inherited from the Normal keymap because
135
- // the mode is registered with `inheritNormalBindings: true`.
100
+ // j/k/Up/Down/PageUp/PageDown route to the log List widget so the host
101
+ // owns selection + scroll + auto-scroll. The List's `select` event then
102
+ // fires back into the plugin's `widget_event` handler for detail-pane
103
+ // refresh. Other plugin actions (q/r/y/Tab/Return) stay as direct
104
+ // bindings — they don't depend on which row is highlighted.
136
105
  editor.defineMode(
137
106
  "git-log",
138
107
  [
139
- ["k", "move_up"],
140
- ["j", "move_down"],
108
+ ["k", "git_log_select_up"],
109
+ ["j", "git_log_select_down"],
110
+ ["Up", "git_log_select_up"],
111
+ ["Down", "git_log_select_down"],
112
+ ["PageUp", "git_log_select_page_up"],
113
+ ["PageDown", "git_log_select_page_down"],
141
114
  ["Return", "git_log_enter"],
142
115
  ["Tab", "git_log_tab"],
143
116
  ["q", "git_log_q"],
@@ -149,6 +122,52 @@ editor.defineMode(
149
122
  true, // inherit Normal-context bindings for unbound keys
150
123
  );
151
124
 
125
+ function git_log_select_up(): void {
126
+ if (isLogPanelActive()) {
127
+ state.logPanel?.command(key("Up"));
128
+ } else {
129
+ editor.executeAction("move_up");
130
+ }
131
+ }
132
+ function git_log_select_down(): void {
133
+ if (isLogPanelActive()) {
134
+ state.logPanel?.command(key("Down"));
135
+ } else {
136
+ editor.executeAction("move_down");
137
+ }
138
+ }
139
+ function git_log_select_page_up(): void {
140
+ if (isLogPanelActive()) {
141
+ state.logPanel?.command(key("PageUp"));
142
+ } else {
143
+ editor.executeAction("page_up");
144
+ }
145
+ }
146
+ function git_log_select_page_down(): void {
147
+ if (isLogPanelActive()) {
148
+ state.logPanel?.command(key("PageDown"));
149
+ } else {
150
+ editor.executeAction("page_down");
151
+ }
152
+ }
153
+
154
+ /** True iff the log panel is the focused buffer in the group. The
155
+ * group's bindings (j/k/Up/Down/PageUp/PageDown) apply to all panels
156
+ * uniformly; we only want navigation to drive the List widget when
157
+ * the user is *on* the log panel. From the detail panel, the same
158
+ * keys must move the buffer cursor (so users can scroll the diff
159
+ * before pressing Enter on a diff line to open the file view). */
160
+ function isLogPanelActive(): boolean {
161
+ return (
162
+ state.logBufferId !== null &&
163
+ editor.getActiveBufferId() === state.logBufferId
164
+ );
165
+ }
166
+ registerHandler("git_log_select_up", git_log_select_up);
167
+ registerHandler("git_log_select_down", git_log_select_down);
168
+ registerHandler("git_log_select_page_up", git_log_select_page_up);
169
+ registerHandler("git_log_select_page_down", git_log_select_page_down);
170
+
152
171
  // =============================================================================
153
172
  // Panel layout
154
173
  // =============================================================================
@@ -177,116 +196,87 @@ const GROUP_LAYOUT = JSON.stringify({
177
196
  // =============================================================================
178
197
  // Toolbar
179
198
  // =============================================================================
199
+ //
200
+ // The toolbar is a one-row panel mounted above the log/detail split. It's
201
+ // rendered through the widget runtime — a `Row` of `Button` widgets — so
202
+ // the host owns hit-testing, focus styling, and keystroke dispatch, and the
203
+ // plugin only handles the resulting `widget_event` actions.
204
+ //
205
+ // Each button's `key` is a stable identifier (`toolbar.tab`, `toolbar.q`,
206
+ // etc.) that `widget_event` carries back so the plugin can look up the
207
+ // right handler without per-row column arithmetic. The previous custom
208
+ // hit-region tracking (`state.toolbarButtons`, `on_git_log_toolbar_click`)
209
+ // is gone.
180
210
 
181
- interface ToolbarHint {
211
+ interface ToolbarItem {
182
212
  key: string;
183
213
  label: string;
184
- /** Click action `null` for hints that are keyboard-only (j/k, PgUp). */
185
- onClick: (() => void | Promise<void>) | null;
214
+ onClick: () => void | Promise<void>;
186
215
  }
187
216
 
188
- interface ToolbarButton {
189
- row: number;
190
- startCol: number;
191
- endCol: number;
192
- onClick: (() => void | Promise<void>) | null;
193
- }
217
+ const TOOLBAR_KEY_PREFIX = "toolbar.";
194
218
 
195
- function toolbarHints(): ToolbarHint[] {
219
+ function toolbarItems(): ToolbarItem[] {
196
220
  return [
197
- { key: "Tab", label: "switch pane", onClick: git_log_tab },
198
- { key: "RET", label: "open file", onClick: git_log_enter },
199
- { key: "y", label: "copy hash", onClick: git_log_copy_hash },
200
- { key: "r", label: "refresh", onClick: git_log_refresh },
201
- { key: "q", label: "quit", onClick: git_log_q },
221
+ { key: "tab", label: "Tab switch pane", onClick: git_log_tab },
222
+ { key: "ret", label: "RET open file", onClick: git_log_enter },
223
+ { key: "y", label: "y copy hash", onClick: git_log_copy_hash },
224
+ { key: "r", label: "r refresh", onClick: git_log_refresh },
225
+ { key: "q", label: "q quit", onClick: git_log_q },
202
226
  ];
203
227
  }
204
228
 
205
- /**
206
- * Build the single-row toolbar. Each hint renders as a discrete button with
207
- * its own background so it reads as clickable; the column range of each
208
- * button is captured in `state.toolbarButtons` so `on_git_log_toolbar_click`
209
- * can map a mouse click back to the right handler.
210
- */
211
- function buildToolbarEntries(width: number): TextPropertyEntry[] {
212
- const W = Math.max(20, width);
213
- const buttons: ToolbarButton[] = [];
214
- let text = "";
215
- const overlays: InlineOverlay[] = [];
216
-
217
- for (const hint of toolbarHints()) {
218
- const body = ` [${hint.key}] ${hint.label} `;
219
- const bodyLen = body.length;
220
- const gap = text.length > 0 ? 1 : 0;
221
- if (text.length + gap + bodyLen > W) break;
222
-
223
- if (gap) text += " ";
224
-
225
- const startCol = text.length;
226
- const startByte = utf8Len(text);
227
- text += body;
228
- const endByte = utf8Len(text);
229
- const endCol = text.length;
230
-
231
- overlays.push({
232
- start: startByte,
233
- end: endByte,
234
- style: { bg: "ui.status_bar_bg" },
235
- });
236
- const keyDisplay = `[${hint.key}]`;
237
- const keyStartByte = startByte + utf8Len(" ");
238
- const keyEndByte = keyStartByte + utf8Len(keyDisplay);
239
- overlays.push({
240
- start: keyStartByte,
241
- end: keyEndByte,
242
- style: { fg: "editor.fg", bold: true },
243
- });
244
- overlays.push({
245
- start: keyEndByte,
246
- end: endByte,
247
- style: { fg: "editor.line_number_fg" },
248
- });
249
-
250
- buttons.push({ row: 0, startCol, endCol, onClick: hint.onClick });
251
- }
252
-
253
- state.toolbarButtons = buttons;
254
-
255
- return [
256
- {
257
- text: text + "\n",
258
- properties: { type: "git-log-toolbar" },
259
- style: { bg: "editor.bg", extendToLineEnd: true },
260
- inlineOverlays: overlays,
261
- },
262
- ];
229
+ function toolbarSpec(): WidgetSpec {
230
+ const items = toolbarItems();
231
+ // `flexSpacer` at the end pushes the buttons to the left and lets the
232
+ // toolbar background extend across the row.
233
+ return row(
234
+ ...items.map((item) =>
235
+ button(item.label, { key: TOOLBAR_KEY_PREFIX + item.key }),
236
+ ),
237
+ flexSpacer(),
238
+ );
263
239
  }
264
240
 
265
241
  function renderToolbar(): void {
266
- if (state.groupId === null) return;
267
- const vp = editor.getViewport();
268
- const width = vp ? vp.width : 80;
269
- editor.setPanelContent(state.groupId, "toolbar", buildToolbarEntries(width));
242
+ if (state.toolbarPanel === null) return;
243
+ state.toolbarPanel.set(toolbarSpec());
270
244
  }
271
245
 
272
- function on_git_log_toolbar_click(data: {
273
- buffer_id: number | null;
274
- buffer_row: number | null;
275
- buffer_col: number | null;
276
- }): void {
277
- if (!state.isOpen) return;
278
- if (data.buffer_id === null || data.buffer_id !== state.toolbarBufferId) return;
279
- if (data.buffer_row === null || data.buffer_col === null) return;
280
- const row = data.buffer_row;
281
- const col = data.buffer_col;
282
- const hit = state.toolbarButtons.find(
283
- (b) => b.row === row && col >= b.startCol && col < b.endCol
284
- );
285
- if (hit && hit.onClick) {
286
- void hit.onClick();
246
+ editor.on("widget_event", (data) => {
247
+ // Toolbar (Row of Buttons) — `activate` from keypress or click on a
248
+ // button.
249
+ if (
250
+ state.toolbarPanel !== null &&
251
+ data.panel_id === state.toolbarPanel.id()
252
+ ) {
253
+ if (data.event_type !== "activate") return;
254
+ const items = toolbarItems();
255
+ for (const item of items) {
256
+ if (data.widget_key === TOOLBAR_KEY_PREFIX + item.key) {
257
+ void item.onClick();
258
+ return;
259
+ }
260
+ }
261
+ return;
287
262
  }
288
- }
289
- registerHandler("on_git_log_toolbar_click", on_git_log_toolbar_click);
263
+ // Log pane (List of commit rows) — `select` fires on j/k/Up/Down/
264
+ // PageUp/PageDown navigation and on row clicks; `activate` fires on
265
+ // Enter or double-click.
266
+ if (state.logPanel !== null && data.panel_id === state.logPanel.id()) {
267
+ if (data.event_type === "select") {
268
+ const idx =
269
+ typeof data.payload?.index === "number" ? data.payload.index : -1;
270
+ if (idx >= 0) void on_log_select(idx);
271
+ return;
272
+ }
273
+ if (data.event_type === "activate") {
274
+ void git_log_enter();
275
+ return;
276
+ }
277
+ return;
278
+ }
279
+ });
290
280
 
291
281
  function on_git_log_resize(_data: { width: number; height: number }): void {
292
282
  if (!state.isOpen) return;
@@ -303,27 +293,29 @@ function detailFooter(hash: string): string {
303
293
  }
304
294
 
305
295
  function renderLog(): void {
306
- if (state.groupId === null) return;
307
- // No header row and no footer: the sticky toolbar above the group
308
- // carries the shortcut hints, and the commit count goes to the status
309
- // line when the group opens.
310
- const entries = buildCommitLogEntries(state.commits, {
311
- selectedIndex: state.selectedIndex,
296
+ if (state.logPanel === null) return;
297
+ // List takes the per-row entries directly. selectedIndex: -1 on the
298
+ // entry builder suppresses the plugin's selection styling the host
299
+ // renders the focused-row highlight from the List widget's instance
300
+ // state instead.
301
+ const items = buildCommitLogEntries(state.commits, {
302
+ selectedIndex: -1,
312
303
  header: null,
313
304
  });
314
- // Rebuild the byte-offset table used by cursor_moved to map positions
315
- // to commit indices. `offsets[i]` is the byte offset of commit i; the
316
- // final entry is the total buffer length, so row lookups clamp
317
- // correctly on the last row.
318
- const offsets: number[] = [];
319
- let running = 0;
320
- for (const e of entries) {
321
- offsets.push(running);
322
- running += utf8Len(e.text);
323
- }
324
- offsets.push(running);
325
- state.logRowByteOffsets = offsets;
326
- editor.setPanelContent(state.groupId, "log", entries);
305
+ const itemKeys = state.commits.map((c) => c.hash);
306
+ state.logPanel.set(
307
+ list({
308
+ items,
309
+ itemKeys,
310
+ selectedIndex: state.selectedIndex,
311
+ // Visible-rows only matters for virtualization; setting it to
312
+ // commits.length renders all rows and lets the buffer's natural
313
+ // scroll handle viewport. Revisit if commit lists grow into the
314
+ // tens of thousands.
315
+ visibleRows: Math.max(1, state.commits.length),
316
+ key: "git-log-list",
317
+ }),
318
+ );
327
319
  }
328
320
 
329
321
  function renderDetailPlaceholder(message: string): void {
@@ -415,14 +407,6 @@ function selectedCommit(): GitCommit | null {
415
407
  return state.commits[i] ?? null;
416
408
  }
417
409
 
418
- function indexFromCursorByte(bytePos: number): number {
419
- // No header row — row 0 is commit 0.
420
- const idx = rowFromByte(bytePos);
421
- if (idx < 0) return 0;
422
- if (idx >= state.commits.length) return state.commits.length - 1;
423
- return idx;
424
- }
425
-
426
410
  // =============================================================================
427
411
  // Commands
428
412
  // =============================================================================
@@ -456,16 +440,20 @@ async function show_git_log(): Promise<void> {
456
440
  state.logBufferId = (group.panels["log"] as number | undefined) ?? null;
457
441
  state.detailBufferId = (group.panels["detail"] as number | undefined) ?? null;
458
442
  state.toolbarBufferId = (group.panels["toolbar"] as number | undefined) ?? null;
443
+ if (state.toolbarBufferId !== null) {
444
+ state.toolbarPanel = new WidgetPanel(state.toolbarBufferId);
445
+ }
446
+ if (state.logBufferId !== null) {
447
+ state.logPanel = new WidgetPanel(state.logBufferId);
448
+ }
459
449
  state.selectedIndex = 0;
460
450
  state.detailCache = null;
461
451
  state.isOpen = true;
462
452
 
463
- // The log panel owns a native cursor so j/k/Up/Down navigate commits,
464
- // and the detail panel also gets a cursor so diff lines can be clicked
465
- // / traversed before pressing Enter to open a file.
466
- if (state.logBufferId !== null) {
467
- editor.setBufferShowCursors(state.logBufferId, true);
468
- }
453
+ // The detail panel still owns a native cursor so diff lines can be
454
+ // clicked / traversed before pressing Enter to open a file. The log
455
+ // panel's selection is owned by the List widget — no buffer cursor
456
+ // needed (the focused-row highlight indicates position).
469
457
  if (state.detailBufferId !== null) {
470
458
  editor.setBufferShowCursors(state.detailBufferId, true);
471
459
  // Wrap long lines in the detail panel — git diffs often exceed the
@@ -478,18 +466,14 @@ async function show_git_log(): Promise<void> {
478
466
 
479
467
  renderToolbar();
480
468
  renderLog();
481
- // Position the cursor on the first commit (row 0 now that the header
482
- // row is gone).
483
- if (state.logBufferId !== null && state.commits.length > 0) {
484
- editor.setBufferCursor(state.logBufferId, 0);
485
- }
469
+ // List widget's instance state is the source of truth for selection;
470
+ // no buffer-cursor positioning needed (the renderer auto-scrolls so
471
+ // the selected row stays visible).
486
472
  await refreshDetail();
487
473
 
488
474
  if (state.groupId !== null) {
489
475
  editor.focusBufferGroupPanel(state.groupId, "log");
490
476
  }
491
- editor.on("cursor_moved", on_git_log_cursor_moved);
492
- editor.on("mouse_click", on_git_log_toolbar_click);
493
477
  editor.on("resize", on_git_log_resize);
494
478
  editor.on("buffer_closed", on_git_log_buffer_closed);
495
479
 
@@ -504,16 +488,19 @@ registerHandler("show_git_log", show_git_log);
504
488
  * close button, which triggers `buffer_closed`). */
505
489
  function git_log_cleanup(): void {
506
490
  if (!state.isOpen) return;
507
- editor.off("cursor_moved", on_git_log_cursor_moved);
508
- editor.off("mouse_click", on_git_log_toolbar_click);
509
491
  editor.off("resize", on_git_log_resize);
510
492
  editor.off("buffer_closed", on_git_log_buffer_closed);
493
+ // The buffer-group's `close` will tear down the panel buffers too,
494
+ // which implicitly drops the widget panels rendering into them. We
495
+ // still null out the handles so any stray `renderToolbar()` /
496
+ // `renderLog()` call post-cleanup is a no-op.
497
+ state.toolbarPanel = null;
498
+ state.logPanel = null;
511
499
  state.isOpen = false;
512
500
  state.groupId = null;
513
501
  state.logBufferId = null;
514
502
  state.detailBufferId = null;
515
503
  state.toolbarBufferId = null;
516
- state.toolbarButtons = [];
517
504
  state.commits = [];
518
505
  state.selectedIndex = 0;
519
506
  state.detailCache = null;
@@ -712,32 +699,18 @@ function git_log_file_view_close(): void {
712
699
  registerHandler("git_log_file_view_close", git_log_file_view_close);
713
700
 
714
701
  // =============================================================================
715
- // Cursor tracking — live-update the detail panel as the user scrolls through
716
- // the commit list.
702
+ // Selection tracking — live-update the detail panel as the user
703
+ // navigates the List. Driven by `widget_event "select"` from the host.
717
704
  // =============================================================================
718
705
 
719
- async function on_git_log_cursor_moved(data: {
720
- buffer_id: number;
721
- cursor_id: number;
722
- old_position: number;
723
- new_position: number;
724
- }): Promise<void> {
706
+ async function on_log_select(idx: number): Promise<void> {
725
707
  if (!state.isOpen) return;
726
- // Only react to movement inside the log panel.
727
- if (data.buffer_id !== state.logBufferId) return;
728
-
729
- // Map the cursor's byte offset to a commit index via the row-offset
730
- // table built in `renderLog`. This avoids relying on `getCursorLine`
731
- // which is not implemented for virtual buffers.
732
- const idx = indexFromCursorByte(data.new_position);
733
708
  if (idx === state.selectedIndex) return;
734
709
  state.selectedIndex = idx;
735
710
 
736
- // Immediate feedback: update the log panel's selection highlight and
737
- // either show the cached detail or a "loading" placeholder. Only the
738
- // actual `git show` spawn is debounced below, so a burst of j/k events
739
- // still feels responsive even though we collapse the fetches into one.
740
- renderLog();
711
+ // Immediate feedback: cached detail or "loading" placeholder.
712
+ // The host already re-rendered the List with the new selection
713
+ // highlight, so we only need to update the right-hand pane.
741
714
  const pending = refreshDetailImmediate();
742
715
 
743
716
  const commit = state.commits[state.selectedIndex];
@@ -746,22 +719,21 @@ async function on_git_log_cursor_moved(data: {
746
719
  editor.t("status.commit_position", {
747
720
  current: String(state.selectedIndex + 1),
748
721
  total: String(state.commits.length),
749
- })
722
+ }),
750
723
  );
751
724
  }
752
725
 
753
726
  if (!pending) return;
754
727
 
755
728
  // Debounce: bump the token, wait a beat, bail if a newer event has
756
- // arrived. `git show` is expensive; a burst of cursor events (held
729
+ // arrived. `git show` is expensive; a burst of select events (held
757
730
  // j/k, PageDown) must collapse to one spawn.
758
- const myId = ++state.pendingCursorMoveId;
759
- await editor.delay(CURSOR_DEBOUNCE_MS);
760
- if (myId !== state.pendingCursorMoveId) return;
731
+ const myId = ++state.pendingSelectId;
732
+ await editor.delay(SELECT_DEBOUNCE_MS);
733
+ if (myId !== state.pendingSelectId) return;
761
734
  if (!state.isOpen) return;
762
735
  await fetchAndRenderDetail(pending);
763
736
  }
764
- registerHandler("on_git_log_cursor_moved", on_git_log_cursor_moved);
765
737
 
766
738
  // =============================================================================
767
739
  // Command registration
@@ -0,0 +1,58 @@
1
+ {
2
+ "en": {
3
+ "cmd.goto_line_with_selection": "Go to Line with Selection",
4
+ "cmd.goto_line_with_selection_desc": "Select from current position to target line"
5
+ },
6
+ "cs": {
7
+ "cmd.goto_line_with_selection": "Jít na řádek s výběrem",
8
+ "cmd.goto_line_with_selection_desc": "Vybrat od aktuální pozice po cílový řádek"
9
+ },
10
+ "de": {
11
+ "cmd.goto_line_with_selection": "Zur Zeile mit Auswahl",
12
+ "cmd.goto_line_with_selection_desc": "Von der aktuellen Position zur Zielzeile auswählen"
13
+ },
14
+ "es": {
15
+ "cmd.goto_line_with_selection": "Ir a línea con selección",
16
+ "cmd.goto_line_with_selection_desc": "Seleccionar desde la posición actual hasta la línea destino"
17
+ },
18
+ "fr": {
19
+ "cmd.goto_line_with_selection": "Aller à la ligne avec sélection",
20
+ "cmd.goto_line_with_selection_desc": "Sélectionner de la position actuelle jusqu'à la ligne cible"
21
+ },
22
+ "it": {
23
+ "cmd.goto_line_with_selection": "Vai alla riga con selezione",
24
+ "cmd.goto_line_with_selection_desc": "Seleziona dalla posizione attuale alla riga di destinazione"
25
+ },
26
+ "ja": {
27
+ "cmd.goto_line_with_selection": "選択付きで移動先へ",
28
+ "cmd.goto_line_with_selection_desc": "現在位置から移動先行まで選択"
29
+ },
30
+ "ko": {
31
+ "cmd.goto_line_with_selection": "선택 포함 줄로 이동",
32
+ "cmd.goto_line_with_selection_desc": "현재 위치에서 대상 줄까지 선택"
33
+ },
34
+ "pt-BR": {
35
+ "cmd.goto_line_with_selection": "Ir para linha com seleção",
36
+ "cmd.goto_line_with_selection_desc": "Selecionar da posição atual até a linha de destino"
37
+ },
38
+ "ru": {
39
+ "cmd.goto_line_with_selection": "Перейти к строке с выделением",
40
+ "cmd.goto_line_with_selection_desc": "Выделить от текущей позиции до целевой строки"
41
+ },
42
+ "th": {
43
+ "cmd.goto_line_with_selection": "ไปยังบรรทัดพร้อมการเลือก",
44
+ "cmd.goto_line_with_selection_desc": "เลือกจากตำแหน่งปัจจุบันไปยังบรรทัดเป้าหมาย"
45
+ },
46
+ "uk": {
47
+ "cmd.goto_line_with_selection": "Перейти до рядка з виділенням",
48
+ "cmd.goto_line_with_selection_desc": "Виділити від поточної позиції до цільового рядка"
49
+ },
50
+ "vi": {
51
+ "cmd.goto_line_with_selection": "Đến dòng với lựa chọn",
52
+ "cmd.goto_line_with_selection_desc": "Chọn từ vị trí hiện tại đến dòng đích"
53
+ },
54
+ "zh-CN": {
55
+ "cmd.goto_line_with_selection": "转到行并选中",
56
+ "cmd.goto_line_with_selection_desc": "从当前位置选中到目标行"
57
+ }
58
+ }
@@ -0,0 +1,17 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ async function goto_line_with_selection_handler(): Promise<void> {
5
+ editor.executeActions([
6
+ { action: "set_mark", count: 1 },
7
+ { action: "goto_line", count: 1 },
8
+ ]);
9
+ }
10
+
11
+ registerHandler("goto_line_with_selection", goto_line_with_selection_handler);
12
+
13
+ editor.registerCommand(
14
+ "%cmd.goto_line_with_selection",
15
+ "%cmd.goto_line_with_selection_desc",
16
+ "goto_line_with_selection",
17
+ );