@tmustier/pi-files-widget 0.1.14 → 0.1.16
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 +11 -0
- package/browser.ts +27 -13
- package/constants.ts +0 -1
- package/input-utils.ts +86 -2
- package/package.json +3 -3
- package/viewer.ts +100 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.16] - 2026-04-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Let `/readfiles` browser search accept `j` and `k` as search text instead of hijacking them for navigation.
|
|
9
|
+
- Fix viewer scrolling so the last lines of a file remain reachable.
|
|
10
|
+
- Restore `G` / `Shift+G` navigation to jump to the bottom of the viewer.
|
|
11
|
+
- Refresh an open viewer when the file changes on disk while `/readfiles` is open.
|
|
12
|
+
- Accept pasted, multi-character, and chunked bracketed-paste input in browser search and the inline comment prompt.
|
|
13
|
+
- Keep viewer search results in sync after live refreshes.
|
|
14
|
+
- Pause live refresh while a line selection or inline comment is active so comments stay anchored to what the user selected.
|
|
15
|
+
|
|
5
16
|
## [0.1.14] - 2026-02-03
|
|
6
17
|
|
|
7
18
|
### Added
|
package/browser.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { buildFileTreeFromPaths, flattenTree, getIgnoredNames, sortChildren, upd
|
|
|
22
22
|
import type { DiffStats, FileNode, FlatNode } from "./types";
|
|
23
23
|
import { isIgnoredStatus, isUntrackedStatus } from "./utils";
|
|
24
24
|
import { createViewer, type CommentPayload, type ViewerAction } from "./viewer";
|
|
25
|
-
import {
|
|
25
|
+
import { createTextInputBuffer } from "./input-utils";
|
|
26
26
|
|
|
27
27
|
export interface BrowserController {
|
|
28
28
|
render(width: number): string[];
|
|
@@ -219,6 +219,7 @@ export function createFileBrowser(
|
|
|
219
219
|
const gitBranch = repo ? getGitBranch(cwd) : "";
|
|
220
220
|
|
|
221
221
|
const viewer = createViewer(cwd, theme, requestComment);
|
|
222
|
+
const textInput = createTextInputBuffer();
|
|
222
223
|
|
|
223
224
|
const root = repo
|
|
224
225
|
? buildFileTreeFromPaths(cwd, getGitFileList(cwd), gitStatus, diffStats, ignored, agentModifiedFiles)
|
|
@@ -794,8 +795,10 @@ export function createFileBrowser(
|
|
|
794
795
|
|
|
795
796
|
function handleBrowserInput(data: string): void {
|
|
796
797
|
const displayList = getDisplayList();
|
|
798
|
+
const maxIndex = Math.max(0, displayList.length - 1);
|
|
797
799
|
|
|
798
800
|
if (matchesKey(data, "q") && !browser.searchMode) {
|
|
801
|
+
textInput.reset();
|
|
799
802
|
stopBackgroundTasks();
|
|
800
803
|
onClose();
|
|
801
804
|
return;
|
|
@@ -804,7 +807,9 @@ export function createFileBrowser(
|
|
|
804
807
|
if (browser.searchMode) {
|
|
805
808
|
browser.searchMode = false;
|
|
806
809
|
browser.searchQuery = "";
|
|
810
|
+
textInput.reset();
|
|
807
811
|
} else {
|
|
812
|
+
textInput.reset();
|
|
808
813
|
stopBackgroundTasks();
|
|
809
814
|
onClose();
|
|
810
815
|
}
|
|
@@ -813,29 +818,38 @@ export function createFileBrowser(
|
|
|
813
818
|
if (matchesKey(data, "/") && !browser.searchMode) {
|
|
814
819
|
browser.searchMode = true;
|
|
815
820
|
browser.searchQuery = "";
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
|
|
819
|
-
browser.selectedIndex = Math.min(displayList.length - 1, browser.selectedIndex + 1);
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
|
|
823
|
-
browser.selectedIndex = Math.max(0, browser.selectedIndex - 1);
|
|
821
|
+
textInput.reset();
|
|
824
822
|
return;
|
|
825
823
|
}
|
|
826
824
|
if (browser.searchMode) {
|
|
827
825
|
if (matchesKey(data, Key.enter)) {
|
|
828
826
|
browser.searchMode = false;
|
|
829
827
|
browser.selectedIndex = 0;
|
|
828
|
+
textInput.reset();
|
|
830
829
|
} else if (matchesKey(data, Key.backspace)) {
|
|
831
830
|
browser.searchQuery = browser.searchQuery.slice(0, -1);
|
|
832
831
|
browser.selectedIndex = 0;
|
|
833
|
-
} else if (
|
|
834
|
-
browser.
|
|
835
|
-
|
|
832
|
+
} else if (matchesKey(data, Key.down)) {
|
|
833
|
+
browser.selectedIndex = Math.min(maxIndex, browser.selectedIndex + 1);
|
|
834
|
+
} else if (matchesKey(data, Key.up)) {
|
|
835
|
+
browser.selectedIndex = Math.max(0, browser.selectedIndex - 1);
|
|
836
|
+
} else {
|
|
837
|
+
const text = textInput.push(data);
|
|
838
|
+
if (text) {
|
|
839
|
+
browser.searchQuery += text;
|
|
840
|
+
browser.selectedIndex = 0;
|
|
841
|
+
}
|
|
836
842
|
}
|
|
837
843
|
return;
|
|
838
844
|
}
|
|
845
|
+
if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
|
|
846
|
+
browser.selectedIndex = Math.min(maxIndex, browser.selectedIndex + 1);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
|
|
850
|
+
browser.selectedIndex = Math.max(0, browser.selectedIndex - 1);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
839
853
|
if (matchesKey(data, Key.enter)) {
|
|
840
854
|
const item = displayList[browser.selectedIndex];
|
|
841
855
|
if (item) {
|
|
@@ -864,7 +878,7 @@ export function createFileBrowser(
|
|
|
864
878
|
return;
|
|
865
879
|
}
|
|
866
880
|
if (matchesKey(data, Key.pageDown)) {
|
|
867
|
-
browser.selectedIndex = Math.min(
|
|
881
|
+
browser.selectedIndex = Math.min(maxIndex, browser.selectedIndex + browser.browserHeight);
|
|
868
882
|
return;
|
|
869
883
|
}
|
|
870
884
|
if (matchesKey(data, Key.pageUp)) {
|
package/constants.ts
CHANGED
package/input-utils.ts
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { decodeKittyPrintable } from "@mariozechner/pi-tui";
|
|
2
|
+
|
|
3
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
4
|
+
const BRACKETED_PASTE_START = "\u001b[200~";
|
|
5
|
+
const BRACKETED_PASTE_END = "\u001b[201~";
|
|
6
|
+
|
|
7
|
+
function sanitizeTextInput(data: string): string {
|
|
8
|
+
const normalized = decodeKittyPrintable(data) ?? data;
|
|
9
|
+
if (!normalized || normalized.includes("\u001b")) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return normalized
|
|
14
|
+
.replace(/\r\n?/g, "\n")
|
|
15
|
+
.replace(/\n/g, " ")
|
|
16
|
+
.replace(/\t/g, " ")
|
|
17
|
+
.replace(CONTROL_CHARS, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getPendingStartSuffix(data: string): string {
|
|
21
|
+
const maxLength = BRACKETED_PASTE_START.length - 1;
|
|
22
|
+
for (let length = Math.min(data.length, maxLength); length > 0; length--) {
|
|
23
|
+
const suffix = data.slice(-length);
|
|
24
|
+
if (BRACKETED_PASTE_START.startsWith(suffix)) {
|
|
25
|
+
return suffix;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TextInputBuffer {
|
|
32
|
+
push(data: string): string;
|
|
33
|
+
reset(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createTextInputBuffer(): TextInputBuffer {
|
|
37
|
+
let isInPaste = false;
|
|
38
|
+
let pasteBuffer = "";
|
|
39
|
+
let pendingStart = "";
|
|
40
|
+
|
|
41
|
+
const push = (data: string): string => {
|
|
42
|
+
if (!data) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const combined = pendingStart + data;
|
|
47
|
+
pendingStart = "";
|
|
48
|
+
|
|
49
|
+
if (!isInPaste) {
|
|
50
|
+
const startIndex = combined.indexOf(BRACKETED_PASTE_START);
|
|
51
|
+
if (startIndex === -1) {
|
|
52
|
+
const pendingSuffix = getPendingStartSuffix(combined);
|
|
53
|
+
const completeText = pendingSuffix ? combined.slice(0, combined.length - pendingSuffix.length) : combined;
|
|
54
|
+
pendingStart = pendingSuffix;
|
|
55
|
+
return sanitizeTextInput(completeText);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const beforePaste = combined.slice(0, startIndex);
|
|
59
|
+
const afterStart = combined.slice(startIndex + BRACKETED_PASTE_START.length);
|
|
60
|
+
isInPaste = true;
|
|
61
|
+
pasteBuffer = "";
|
|
62
|
+
return sanitizeTextInput(beforePaste) + push(afterStart);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pasteBuffer += combined;
|
|
66
|
+
const endIndex = pasteBuffer.indexOf(BRACKETED_PASTE_END);
|
|
67
|
+
if (endIndex === -1) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pastedText = pasteBuffer.slice(0, endIndex);
|
|
72
|
+
const remaining = pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
|
|
73
|
+
isInPaste = false;
|
|
74
|
+
pasteBuffer = "";
|
|
75
|
+
|
|
76
|
+
return sanitizeTextInput(pastedText) + push(remaining);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
push,
|
|
81
|
+
reset(): void {
|
|
82
|
+
isInPaste = false;
|
|
83
|
+
pasteBuffer = "";
|
|
84
|
+
pendingStart = "";
|
|
85
|
+
},
|
|
86
|
+
};
|
|
3
87
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-files-widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "In-terminal file browser and viewer for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Mustier",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
16
|
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/files-widget",
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@mariozechner/pi-coding-agent": "
|
|
19
|
-
"@mariozechner/pi-tui": "
|
|
18
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
19
|
+
"@mariozechner/pi-tui": "*"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"postinstall": "node -e \"console.log('pi-files-widget: required deps: bat, delta, glow. Install with: brew install bat git-delta glow')\""
|
package/viewer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Key, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { relative } from "node:path";
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -8,12 +8,11 @@ import {
|
|
|
8
8
|
MAX_VIEWER_HEIGHT,
|
|
9
9
|
MIN_PANEL_HEIGHT,
|
|
10
10
|
SEARCH_SCROLL_OFFSET,
|
|
11
|
-
VIEWER_SCROLL_MARGIN,
|
|
12
11
|
} from "./constants";
|
|
13
12
|
import { loadFileContent } from "./file-viewer";
|
|
14
13
|
import type { FileNode } from "./types";
|
|
15
14
|
import { isUntrackedStatus } from "./utils";
|
|
16
|
-
import {
|
|
15
|
+
import { createTextInputBuffer } from "./input-utils";
|
|
17
16
|
|
|
18
17
|
export interface CommentPayload {
|
|
19
18
|
relPath: string;
|
|
@@ -43,6 +42,7 @@ interface ViewerState {
|
|
|
43
42
|
searchMatches: number[];
|
|
44
43
|
searchIndex: number;
|
|
45
44
|
lastRenderWidth: number;
|
|
45
|
+
lastLoadedMtimeMs: number | null;
|
|
46
46
|
height: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -61,6 +61,8 @@ export function createViewer(
|
|
|
61
61
|
theme: Theme,
|
|
62
62
|
requestComment: (payload: CommentPayload, comment: string) => void
|
|
63
63
|
): ViewerController {
|
|
64
|
+
const textInput = createTextInputBuffer();
|
|
65
|
+
|
|
64
66
|
const state: ViewerState = {
|
|
65
67
|
file: null,
|
|
66
68
|
content: [],
|
|
@@ -75,6 +77,7 @@ export function createViewer(
|
|
|
75
77
|
searchMatches: [],
|
|
76
78
|
searchIndex: 0,
|
|
77
79
|
lastRenderWidth: 0,
|
|
80
|
+
lastLoadedMtimeMs: null,
|
|
78
81
|
height: DEFAULT_VIEWER_HEIGHT,
|
|
79
82
|
};
|
|
80
83
|
|
|
@@ -94,6 +97,10 @@ export function createViewer(
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
function setMode(mode: ViewerMode): void {
|
|
100
|
+
if (mode !== state.mode) {
|
|
101
|
+
textInput.reset();
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
state.mode = mode;
|
|
98
105
|
if (mode !== "search") resetSearch();
|
|
99
106
|
if (mode !== "comment") resetComment();
|
|
@@ -102,16 +109,59 @@ export function createViewer(
|
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
function getMaxScroll(): number {
|
|
113
|
+
return Math.max(0, state.content.length - state.height);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function refreshRawContent(): void {
|
|
117
|
+
if (!state.file) return;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const fileStat = statSync(state.file.path);
|
|
121
|
+
state.rawContent = readFileSync(state.file.path, "utf-8");
|
|
122
|
+
state.file.lineCount = state.rawContent.split("\n").length;
|
|
123
|
+
state.lastLoadedMtimeMs = fileStat.mtimeMs;
|
|
124
|
+
} catch {
|
|
125
|
+
state.rawContent = "";
|
|
126
|
+
state.file.lineCount = undefined;
|
|
127
|
+
state.lastLoadedMtimeMs = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasFileChangedOnDisk(): boolean {
|
|
132
|
+
if (!state.file) return false;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
return state.lastLoadedMtimeMs === null || statSync(state.file.path).mtimeMs !== state.lastLoadedMtimeMs;
|
|
136
|
+
} catch {
|
|
137
|
+
return state.lastLoadedMtimeMs !== null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function clampScroll(): void {
|
|
142
|
+
state.scroll = Math.min(getMaxScroll(), Math.max(0, state.scroll));
|
|
143
|
+
}
|
|
144
|
+
|
|
105
145
|
function reloadContent(width: number): void {
|
|
106
146
|
if (!state.file) return;
|
|
147
|
+
refreshRawContent();
|
|
107
148
|
const hasChanges = !!state.file.gitStatus;
|
|
108
149
|
state.content = loadFileContent(state.file.path, cwd, state.diffMode, hasChanges, width);
|
|
109
150
|
state.lastRenderWidth = width;
|
|
151
|
+
clampScroll();
|
|
152
|
+
if (state.searchQuery) {
|
|
153
|
+
updateSearchMatches({ preserveActiveMatch: true });
|
|
154
|
+
}
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
function updateSearchMatches(): void {
|
|
157
|
+
function updateSearchMatches(options: { preserveActiveMatch?: boolean } = {}): void {
|
|
158
|
+
const activeMatch = options.preserveActiveMatch ? state.searchMatches[state.searchIndex] : undefined;
|
|
159
|
+
|
|
113
160
|
state.searchMatches = [];
|
|
114
|
-
if (!state.searchQuery)
|
|
161
|
+
if (!state.searchQuery) {
|
|
162
|
+
state.searchIndex = 0;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
115
165
|
|
|
116
166
|
const q = state.searchQuery.toLowerCase();
|
|
117
167
|
const rawLines = state.rawContent.split("\n");
|
|
@@ -120,11 +170,29 @@ export function createViewer(
|
|
|
120
170
|
state.searchMatches.push(i);
|
|
121
171
|
}
|
|
122
172
|
}
|
|
123
|
-
state.searchIndex = 0;
|
|
124
173
|
|
|
125
|
-
if (state.searchMatches.length
|
|
126
|
-
state.
|
|
174
|
+
if (state.searchMatches.length === 0) {
|
|
175
|
+
state.searchIndex = 0;
|
|
176
|
+
return;
|
|
127
177
|
}
|
|
178
|
+
|
|
179
|
+
if (activeMatch === undefined) {
|
|
180
|
+
state.searchIndex = 0;
|
|
181
|
+
} else {
|
|
182
|
+
let nearestIndex = 0;
|
|
183
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
184
|
+
for (let i = 0; i < state.searchMatches.length; i++) {
|
|
185
|
+
const distance = Math.abs(state.searchMatches[i] - activeMatch);
|
|
186
|
+
if (distance < nearestDistance) {
|
|
187
|
+
nearestDistance = distance;
|
|
188
|
+
nearestIndex = i;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
state.searchIndex = nearestIndex;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
state.scroll = Math.max(0, state.searchMatches[state.searchIndex] - SEARCH_SCROLL_OFFSET);
|
|
195
|
+
clampScroll();
|
|
128
196
|
}
|
|
129
197
|
|
|
130
198
|
function jumpToNextMatch(direction: 1 | -1): void {
|
|
@@ -133,6 +201,7 @@ export function createViewer(
|
|
|
133
201
|
if (state.searchIndex < 0) state.searchIndex = state.searchMatches.length - 1;
|
|
134
202
|
if (state.searchIndex >= state.searchMatches.length) state.searchIndex = 0;
|
|
135
203
|
state.scroll = Math.max(0, state.searchMatches[state.searchIndex] - SEARCH_SCROLL_OFFSET);
|
|
204
|
+
clampScroll();
|
|
136
205
|
}
|
|
137
206
|
|
|
138
207
|
function buildCommentPayload(): CommentPayload | null {
|
|
@@ -242,14 +311,8 @@ export function createViewer(
|
|
|
242
311
|
setMode("normal");
|
|
243
312
|
state.content = [];
|
|
244
313
|
state.lastRenderWidth = 0;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
state.rawContent = readFileSync(file.path, "utf-8");
|
|
248
|
-
file.lineCount = state.rawContent.split("\n").length;
|
|
249
|
-
} catch {
|
|
250
|
-
state.rawContent = "";
|
|
251
|
-
file.lineCount = undefined;
|
|
252
|
-
}
|
|
314
|
+
state.lastLoadedMtimeMs = null;
|
|
315
|
+
refreshRawContent();
|
|
253
316
|
},
|
|
254
317
|
|
|
255
318
|
updateFileRef(file: FileNode | null): void {
|
|
@@ -259,13 +322,16 @@ export function createViewer(
|
|
|
259
322
|
close(): void {
|
|
260
323
|
state.file = null;
|
|
261
324
|
state.content = [];
|
|
325
|
+
state.rawContent = "";
|
|
326
|
+
state.lastLoadedMtimeMs = null;
|
|
262
327
|
setMode("normal");
|
|
263
328
|
},
|
|
264
329
|
|
|
265
330
|
render(width: number): string[] {
|
|
266
331
|
if (!state.file) return [];
|
|
267
332
|
|
|
268
|
-
|
|
333
|
+
const shouldAutoRefresh = state.mode !== "select" && state.mode !== "comment";
|
|
334
|
+
if (state.lastRenderWidth !== width || state.content.length === 0 || (shouldAutoRefresh && hasFileChangedOnDisk())) {
|
|
269
335
|
reloadContent(width);
|
|
270
336
|
}
|
|
271
337
|
|
|
@@ -308,8 +374,11 @@ export function createViewer(
|
|
|
308
374
|
setMode("normal");
|
|
309
375
|
} else if (matchesKey(data, Key.backspace)) {
|
|
310
376
|
state.commentText = state.commentText.slice(0, -1);
|
|
311
|
-
} else
|
|
312
|
-
|
|
377
|
+
} else {
|
|
378
|
+
const text = textInput.push(data);
|
|
379
|
+
if (text) {
|
|
380
|
+
state.commentText += text;
|
|
381
|
+
}
|
|
313
382
|
}
|
|
314
383
|
return { type: "none" };
|
|
315
384
|
}
|
|
@@ -322,9 +391,12 @@ export function createViewer(
|
|
|
322
391
|
} else if (matchesKey(data, Key.backspace)) {
|
|
323
392
|
state.searchQuery = state.searchQuery.slice(0, -1);
|
|
324
393
|
updateSearchMatches();
|
|
325
|
-
} else
|
|
326
|
-
|
|
327
|
-
|
|
394
|
+
} else {
|
|
395
|
+
const text = textInput.push(data);
|
|
396
|
+
if (text) {
|
|
397
|
+
state.searchQuery += text;
|
|
398
|
+
updateSearchMatches();
|
|
399
|
+
}
|
|
328
400
|
}
|
|
329
401
|
return { type: "none" };
|
|
330
402
|
}
|
|
@@ -358,7 +430,7 @@ export function createViewer(
|
|
|
358
430
|
if (state.mode === "select") {
|
|
359
431
|
state.selectEnd = Math.min(state.content.length - 1, state.selectEnd + 1);
|
|
360
432
|
} else {
|
|
361
|
-
state.scroll = Math.min(
|
|
433
|
+
state.scroll = Math.min(getMaxScroll(), state.scroll + 1);
|
|
362
434
|
}
|
|
363
435
|
return { type: "none" };
|
|
364
436
|
}
|
|
@@ -371,7 +443,7 @@ export function createViewer(
|
|
|
371
443
|
return { type: "none" };
|
|
372
444
|
}
|
|
373
445
|
if (matchesKey(data, Key.pageDown)) {
|
|
374
|
-
state.scroll = Math.min(
|
|
446
|
+
state.scroll = Math.min(getMaxScroll(), state.scroll + state.height);
|
|
375
447
|
return { type: "none" };
|
|
376
448
|
}
|
|
377
449
|
if (matchesKey(data, Key.pageUp)) {
|
|
@@ -382,16 +454,18 @@ export function createViewer(
|
|
|
382
454
|
state.scroll = 0;
|
|
383
455
|
return { type: "none" };
|
|
384
456
|
}
|
|
385
|
-
if (matchesKey(data, "
|
|
386
|
-
state.scroll =
|
|
457
|
+
if (matchesKey(data, "shift+g")) {
|
|
458
|
+
state.scroll = getMaxScroll();
|
|
387
459
|
return { type: "none" };
|
|
388
460
|
}
|
|
389
461
|
if (matchesKey(data, "+") || matchesKey(data, "=")) {
|
|
390
462
|
state.height = Math.min(MAX_VIEWER_HEIGHT, state.height + 5);
|
|
463
|
+
clampScroll();
|
|
391
464
|
return { type: "none" };
|
|
392
465
|
}
|
|
393
466
|
if (matchesKey(data, "-") || matchesKey(data, "_")) {
|
|
394
467
|
state.height = Math.max(MIN_PANEL_HEIGHT, state.height - 5);
|
|
468
|
+
clampScroll();
|
|
395
469
|
return { type: "none" };
|
|
396
470
|
}
|
|
397
471
|
if (matchesKey(data, "d") && state.mode !== "select" && state.file.gitStatus && !isUntrackedStatus(state.file.gitStatus)) {
|