@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 +72 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/plugins/config-schema.json +7 -1
- package/plugins/dashboard.ts +16 -93
- package/plugins/git_grep.ts +3 -1
- package/plugins/git_log.ts +196 -224
- package/plugins/goto_with_selection.i18n.json +58 -0
- package/plugins/goto_with_selection.ts +17 -0
- package/plugins/lib/finder.ts +27 -6
- package/plugins/lib/fresh.d.ts +620 -14
- package/plugins/lib/index.ts +34 -0
- package/plugins/lib/widgets.ts +796 -0
- package/plugins/live_diff.ts +324 -29
- package/plugins/live_grep.ts +114 -48
- package/plugins/orchestrator.ts +1685 -0
- package/plugins/pkg.ts +234 -53
- package/plugins/rust-lsp.ts +58 -40
- package/plugins/schemas/theme.schema.json +4 -0
- package/plugins/search_replace.ts +780 -517
- package/plugins/theme_editor.i18n.json +84 -0
- package/plugins/theme_editor.ts +30 -5
- package/plugins/tsconfig.json +2 -0
- package/plugins/vi_mode.ts +38 -17
- package/themes/terminal.json +3 -0
package/plugins/git_log.ts
CHANGED
|
@@ -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
|
-
/**
|
|
41
|
-
|
|
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 `
|
|
53
|
-
* j/k) would otherwise trigger a full
|
|
54
|
-
* intermediate row; we bump this id on every event
|
|
55
|
-
* after a short delay if no newer event
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
toolbarPanel: null,
|
|
76
|
+
logPanel: null,
|
|
75
77
|
commits: [],
|
|
76
78
|
selectedIndex: 0,
|
|
77
79
|
detailCache: null,
|
|
78
80
|
pendingDetailId: 0,
|
|
79
|
-
|
|
80
|
-
logRowByteOffsets: [],
|
|
81
|
+
pendingSelectId: 0,
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* Delay before
|
|
85
|
-
*
|
|
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
|
|
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
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
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", "
|
|
140
|
-
["j", "
|
|
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
|
|
211
|
+
interface ToolbarItem {
|
|
182
212
|
key: string;
|
|
183
213
|
label: string;
|
|
184
|
-
|
|
185
|
-
onClick: (() => void | Promise<void>) | null;
|
|
214
|
+
onClick: () => void | Promise<void>;
|
|
186
215
|
}
|
|
187
216
|
|
|
188
|
-
|
|
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
|
|
219
|
+
function toolbarItems(): ToolbarItem[] {
|
|
196
220
|
return [
|
|
197
|
-
{ key: "
|
|
198
|
-
{ key: "
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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.
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
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
|
-
//
|
|
482
|
-
//
|
|
483
|
-
|
|
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
|
-
//
|
|
716
|
-
// the
|
|
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
|
|
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:
|
|
737
|
-
//
|
|
738
|
-
//
|
|
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
|
|
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.
|
|
759
|
-
await editor.delay(
|
|
760
|
-
if (myId !== state.
|
|
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
|
+
);
|