rl-rockcli 0.0.9 → 0.0.11
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/commands/attach/basic-repl.js +212 -0
- package/commands/attach/cleanup-history.js +189 -0
- package/commands/attach/cleanup-manager.js +163 -0
- package/commands/attach/copy-ui/copyRepl.js +195 -0
- package/commands/attach/copy-ui/index.js +7 -0
- package/commands/attach/copy-ui/render/outputBlock.js +25 -0
- package/commands/attach/copy-ui/viewport/viewport.js +23 -0
- package/commands/attach/copy-ui/viewport/wheel.js +14 -0
- package/commands/attach/history-manager.js +507 -0
- package/commands/attach/history-session.js +48 -0
- package/commands/attach/ink-repl/InkREPL.js +1507 -0
- package/commands/attach/ink-repl/builtinCommands.js +1253 -0
- package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
- package/commands/attach/ink-repl/components/Console.js +191 -0
- package/commands/attach/ink-repl/components/DetailView.js +148 -0
- package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
- package/commands/attach/ink-repl/components/InputArea.js +125 -0
- package/commands/attach/ink-repl/components/InputLine.js +18 -0
- package/commands/attach/ink-repl/components/OutputArea.js +22 -0
- package/commands/attach/ink-repl/components/OutputItem.js +96 -0
- package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
- package/commands/attach/ink-repl/components/Spinner.js +79 -0
- package/commands/attach/ink-repl/components/StatusBar.js +106 -0
- package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
- package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
- package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
- package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
- package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
- package/commands/attach/ink-repl/hooks/useResources.js +132 -0
- package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
- package/commands/attach/ink-repl/index.js +112 -0
- package/commands/attach/ink-repl/package.json +3 -0
- package/commands/attach/ink-repl/replState.js +947 -0
- package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
- package/commands/attach/ink-repl/shortcuts/index.js +332 -0
- package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
- package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
- package/commands/attach/ink-repl/themes/index.js +4 -0
- package/commands/attach/ink-repl/themes/themeManager.js +45 -0
- package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
- package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
- package/commands/attach/ink-repl/utils/clipboard.js +50 -0
- package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
- package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
- package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
- package/commands/attach/ink-repl/utils/formatTime.js +12 -0
- package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
- package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
- package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
- package/commands/attach/ink-repl/utils/paramHint.js +60 -0
- package/commands/attach/ink-repl/utils/parseError.js +174 -0
- package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
- package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
- package/commands/attach/ink-repl/utils/replSelection.js +205 -0
- package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
- package/commands/attach/ink-repl/utils/textWrap.js +117 -0
- package/commands/attach/ink-repl/utils/truncate.js +115 -0
- package/commands/attach/opentui-repl/App.tsx +891 -0
- package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
- package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
- package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
- package/commands/attach/opentui-repl/components/Console.tsx +73 -0
- package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
- package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
- package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
- package/commands/attach/opentui-repl/components/Header.tsx +24 -0
- package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
- package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
- package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
- package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
- package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
- package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
- package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
- package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
- package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
- package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
- package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
- package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
- package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
- package/commands/attach/opentui-repl/index.js +99 -0
- package/commands/attach/opentui-repl/keybindings.ts +39 -0
- package/commands/attach/opentui-repl/package.json +3 -0
- package/commands/attach/opentui-repl/render.tsx +72 -0
- package/commands/attach/opentui-repl/tsconfig.json +12 -0
- package/commands/attach/repl.js +791 -0
- package/commands/attach/sandbox-id-resolver.js +56 -0
- package/commands/attach/session-manager.js +307 -0
- package/commands/attach/ui-mode.js +146 -0
- package/commands/attach.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL State Management
|
|
3
|
+
* Pure functions for state transitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import i18n from '../../../utils/i18n.js';
|
|
7
|
+
import { processCarriageReturns } from './utils/truncate.js';
|
|
8
|
+
const { t } = i18n;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create initial REPL state
|
|
12
|
+
*/
|
|
13
|
+
export function createInitialState(overrides = {}) {
|
|
14
|
+
// Welcome message as initial output
|
|
15
|
+
const welcomeOutput = {
|
|
16
|
+
id: 'welcome',
|
|
17
|
+
command: null,
|
|
18
|
+
output: t('welcome.connected'),
|
|
19
|
+
exitCode: 0,
|
|
20
|
+
timestamp: new Date(),
|
|
21
|
+
truncated: false,
|
|
22
|
+
prompt: null,
|
|
23
|
+
isWelcome: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
viewMode: 'repl', // 'repl' | 'detail'
|
|
28
|
+
detailContent: null,
|
|
29
|
+
|
|
30
|
+
outputs: [welcomeOutput],
|
|
31
|
+
outputScrollOffset: 0, // DEPRECATED: Item-based scrolling
|
|
32
|
+
lineScrollOffset: 0, // Line-based scrolling: number of lines scrolled up from bottom
|
|
33
|
+
|
|
34
|
+
buffer: '',
|
|
35
|
+
cursorPosition: 0, // Cursor position in buffer (0 = start, buffer.length = end)
|
|
36
|
+
isExecuting: false,
|
|
37
|
+
executingCommand: '',
|
|
38
|
+
executionStartTime: null,
|
|
39
|
+
lastExecutionDuration: null, // in milliseconds
|
|
40
|
+
|
|
41
|
+
menuVisible: false,
|
|
42
|
+
menuItems: [],
|
|
43
|
+
selectedIndex: 0,
|
|
44
|
+
menuType: null, // 'slash' | 'path' | 'at' | null
|
|
45
|
+
atContext: null, // Context for @ completion (from parseAtContext)
|
|
46
|
+
|
|
47
|
+
historyIndex: -1,
|
|
48
|
+
commandHistory: [],
|
|
49
|
+
savedBuffer: '',
|
|
50
|
+
|
|
51
|
+
resources: null,
|
|
52
|
+
shellPrompt: '$ ',
|
|
53
|
+
|
|
54
|
+
// Console state
|
|
55
|
+
consoleVisible: false,
|
|
56
|
+
consoleLogs: [],
|
|
57
|
+
consoleScrollOffset: 0,
|
|
58
|
+
consoleSelectedIndex: -1, // -1 = no selection, 0+ = selected log index
|
|
59
|
+
|
|
60
|
+
// Exit confirmation state
|
|
61
|
+
exitPending: false,
|
|
62
|
+
|
|
63
|
+
// Mouse capture mode (SGR mouse reporting). When disabled in REPL view,
|
|
64
|
+
// terminals can use native text selection.
|
|
65
|
+
mouseCaptureEnabled: true,
|
|
66
|
+
|
|
67
|
+
// Paginated file viewing state (for large files)
|
|
68
|
+
paginatedFile: null, // { filePath, totalLines, loadedStartLine, loadedEndLine, isLoading }
|
|
69
|
+
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Determine whether InkREPL 应启用鼠标捕获模式
|
|
76
|
+
* 仅在非输入区域(detail view、console)可见时返回 true
|
|
77
|
+
* @param {Object} state
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function shouldEnableMouseMode(state) {
|
|
81
|
+
if (!state) return false;
|
|
82
|
+
// When disabled, allow terminal native selection everywhere.
|
|
83
|
+
if (state.mouseCaptureEnabled === false) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// REPL mode: disable mouse capture to allow terminal native text selection
|
|
87
|
+
// This enables mouse selection and copy without any additional key presses
|
|
88
|
+
if (state.viewMode === 'repl') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Only enable mouse capture in detail view and console
|
|
92
|
+
if (state.viewMode === 'detail' && state.detailContent) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (state.consoleVisible) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Toggle mouse capture mode for the REPL.
|
|
103
|
+
* DEPRECATED: Mouse mode is now always enabled.
|
|
104
|
+
* @param {Object} state
|
|
105
|
+
* @returns {Object}
|
|
106
|
+
*/
|
|
107
|
+
export function toggleMouseCapture(state) {
|
|
108
|
+
// Mouse mode is always enabled now, return state unchanged
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set exit pending state
|
|
114
|
+
*/
|
|
115
|
+
export function setExitPending(state, pending) {
|
|
116
|
+
return { ...state, exitPending: pending };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add command output to history
|
|
121
|
+
* @param {Object} state
|
|
122
|
+
* @param {string} command
|
|
123
|
+
* @param {string} output
|
|
124
|
+
* @param {number} exitCode
|
|
125
|
+
* @param {string} prompt - The prompt at the time of execution (optional, uses current if not provided)
|
|
126
|
+
* @param {string} metaInfo - Optional meta info (e.g., "[exit code: 1]")
|
|
127
|
+
* @param {string} tips - Optional tips for failed commands
|
|
128
|
+
*/
|
|
129
|
+
export function addOutput(state, command, output, exitCode = 0, prompt = null, metaInfo = null, tips = null) {
|
|
130
|
+
// Process carriage returns to simulate terminal behavior
|
|
131
|
+
// This fixes issues with progress bars (e.g., apt-get update) that use \r
|
|
132
|
+
const processedOutput = processCarriageReturns(output);
|
|
133
|
+
|
|
134
|
+
const newOutput = {
|
|
135
|
+
id: `output-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
136
|
+
command,
|
|
137
|
+
output: processedOutput,
|
|
138
|
+
exitCode,
|
|
139
|
+
timestamp: Date.now(), // Unix timestamp in milliseconds
|
|
140
|
+
truncated: false,
|
|
141
|
+
prompt: prompt || state.shellPrompt, // Save the prompt at execution time
|
|
142
|
+
metaInfo,
|
|
143
|
+
tips,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Always scroll to bottom when new output is added
|
|
147
|
+
// This ensures the new command output is always visible
|
|
148
|
+
const outputScrollOffset = 0;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
outputs: [...state.outputs, newOutput],
|
|
153
|
+
outputScrollOffset,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Scroll REPL outputs (history) by N items.
|
|
159
|
+
* Offset is measured from the bottom: 0 = show latest, higher = show older.
|
|
160
|
+
*
|
|
161
|
+
* @param {Object} state
|
|
162
|
+
* @param {number} delta Positive = scroll up (older), negative = scroll down (newer)
|
|
163
|
+
* @param {number} maxVisibleOutputs How many output items can be shown at once
|
|
164
|
+
* @returns {Object} New state
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* Scroll outputs (支持小数 delta 实现平滑滚动)
|
|
168
|
+
* @param {Object} state
|
|
169
|
+
* @param {number} delta - 滚动增量,可以是小数(如 0.2)实现渐进式滚动
|
|
170
|
+
* @param {number} maxVisibleOutputs
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* Scroll outputs by number of lines (new line-based scrolling)
|
|
174
|
+
* @param {Object} state
|
|
175
|
+
* @param {number} deltaLines - Positive = scroll up (show older content), Negative = scroll down
|
|
176
|
+
* @param {number} totalVisibleLines - Total lines that can be displayed
|
|
177
|
+
* @returns {Object}
|
|
178
|
+
*/
|
|
179
|
+
export function scrollOutputsByLines(state, deltaLines, totalVisibleLines) {
|
|
180
|
+
// Calculate total lines in all outputs
|
|
181
|
+
// This will be calculated by the rendering component and passed in
|
|
182
|
+
// For now, just accumulate the delta
|
|
183
|
+
const currentOffset = typeof state.lineScrollOffset === 'number' ? state.lineScrollOffset : 0;
|
|
184
|
+
const newOffset = Math.max(0, currentOffset + deltaLines);
|
|
185
|
+
|
|
186
|
+
return { ...state, lineScrollOffset: newOffset };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scroll outputs by item offset (legacy item-based scrolling)
|
|
191
|
+
* @deprecated Use scrollOutputsByLines instead
|
|
192
|
+
*/
|
|
193
|
+
export function scrollOutputs(state, delta, maxVisibleOutputs) {
|
|
194
|
+
const maxVisible = Math.max(1, Number(maxVisibleOutputs || 1));
|
|
195
|
+
const maxOffset = Math.max(0, (state.outputs?.length || 0) - maxVisible);
|
|
196
|
+
|
|
197
|
+
const current = typeof state.outputScrollOffset === 'number' ? state.outputScrollOffset : 0;
|
|
198
|
+
const next = Math.max(0, Math.min(maxOffset, current + delta));
|
|
199
|
+
|
|
200
|
+
if (next === current) return state;
|
|
201
|
+
return { ...state, outputScrollOffset: next };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Show menu with items
|
|
206
|
+
* @param {Object} state
|
|
207
|
+
* @param {Array} items
|
|
208
|
+
* @param {string} menuType - 'slash' | 'path' | 'at'
|
|
209
|
+
* @param {Object} atContext - Context for @ completion (optional)
|
|
210
|
+
*/
|
|
211
|
+
export function showMenu(state, items, menuType = 'slash', atContext = null) {
|
|
212
|
+
return {
|
|
213
|
+
...state,
|
|
214
|
+
menuVisible: true,
|
|
215
|
+
menuItems: items,
|
|
216
|
+
selectedIndex: 0,
|
|
217
|
+
menuType,
|
|
218
|
+
atContext: menuType === 'at' ? atContext : null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Hide menu and reset selection
|
|
224
|
+
*/
|
|
225
|
+
export function hideMenu(state) {
|
|
226
|
+
return {
|
|
227
|
+
...state,
|
|
228
|
+
menuVisible: false,
|
|
229
|
+
menuItems: [],
|
|
230
|
+
selectedIndex: 0,
|
|
231
|
+
menuType: null,
|
|
232
|
+
atContext: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Select next menu item (cycle)
|
|
238
|
+
*/
|
|
239
|
+
export function selectNext(state) {
|
|
240
|
+
if (state.menuItems.length === 0) return state;
|
|
241
|
+
return {
|
|
242
|
+
...state,
|
|
243
|
+
selectedIndex: (state.selectedIndex + 1) % state.menuItems.length,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Select previous menu item (cycle)
|
|
249
|
+
*/
|
|
250
|
+
export function selectPrev(state) {
|
|
251
|
+
if (state.menuItems.length === 0) return state;
|
|
252
|
+
return {
|
|
253
|
+
...state,
|
|
254
|
+
selectedIndex: state.selectedIndex <= 0
|
|
255
|
+
? state.menuItems.length - 1
|
|
256
|
+
: state.selectedIndex - 1,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Start command execution
|
|
262
|
+
*/
|
|
263
|
+
export function startExecuting(state, command) {
|
|
264
|
+
return {
|
|
265
|
+
...state,
|
|
266
|
+
isExecuting: true,
|
|
267
|
+
executingCommand: command,
|
|
268
|
+
executionStartTime: Date.now(),
|
|
269
|
+
buffer: '',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Finish command execution
|
|
275
|
+
*/
|
|
276
|
+
export function finishExecuting(state) {
|
|
277
|
+
const duration = state.executionStartTime
|
|
278
|
+
? Date.now() - state.executionStartTime
|
|
279
|
+
: null;
|
|
280
|
+
return {
|
|
281
|
+
...state,
|
|
282
|
+
isExecuting: false,
|
|
283
|
+
executingCommand: '',
|
|
284
|
+
executionStartTime: null,
|
|
285
|
+
lastExecutionDuration: duration,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Enter detail view mode
|
|
291
|
+
*/
|
|
292
|
+
export function enterDetailView(state, title, content) {
|
|
293
|
+
return {
|
|
294
|
+
...state,
|
|
295
|
+
viewMode: 'detail',
|
|
296
|
+
detailContent: { title, content, scrollOffset: 0, wrappedLines: null, wrapWidth: null, lineCount: null },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Exit detail view mode
|
|
302
|
+
*/
|
|
303
|
+
export function exitDetailView(state) {
|
|
304
|
+
return {
|
|
305
|
+
...state,
|
|
306
|
+
viewMode: 'repl',
|
|
307
|
+
detailContent: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Navigate history up (older commands)
|
|
313
|
+
*/
|
|
314
|
+
export function navigateHistoryUp(state) {
|
|
315
|
+
if (state.commandHistory.length === 0) return state;
|
|
316
|
+
|
|
317
|
+
if (state.historyIndex === -1) {
|
|
318
|
+
// First time navigating up, save current buffer
|
|
319
|
+
const newBuffer = state.commandHistory[state.commandHistory.length - 1];
|
|
320
|
+
return {
|
|
321
|
+
...state,
|
|
322
|
+
savedBuffer: state.buffer,
|
|
323
|
+
historyIndex: state.commandHistory.length - 1,
|
|
324
|
+
buffer: newBuffer,
|
|
325
|
+
cursorPosition: newBuffer.length, // Move cursor to end
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (state.historyIndex > 0) {
|
|
330
|
+
const newIndex = state.historyIndex - 1;
|
|
331
|
+
const newBuffer = state.commandHistory[newIndex];
|
|
332
|
+
return {
|
|
333
|
+
...state,
|
|
334
|
+
historyIndex: newIndex,
|
|
335
|
+
buffer: newBuffer,
|
|
336
|
+
cursorPosition: newBuffer.length, // Move cursor to end
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return state; // at the beginning
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Navigate history down (newer commands)
|
|
345
|
+
*/
|
|
346
|
+
export function navigateHistoryDown(state) {
|
|
347
|
+
if (state.historyIndex === -1) return state;
|
|
348
|
+
|
|
349
|
+
if (state.historyIndex < state.commandHistory.length - 1) {
|
|
350
|
+
const newIndex = state.historyIndex + 1;
|
|
351
|
+
const newBuffer = state.commandHistory[newIndex];
|
|
352
|
+
return {
|
|
353
|
+
...state,
|
|
354
|
+
historyIndex: newIndex,
|
|
355
|
+
buffer: newBuffer,
|
|
356
|
+
cursorPosition: newBuffer.length, // Move cursor to end
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Return to saved buffer
|
|
361
|
+
const restoredBuffer = state.savedBuffer;
|
|
362
|
+
return {
|
|
363
|
+
...state,
|
|
364
|
+
historyIndex: -1,
|
|
365
|
+
buffer: restoredBuffer,
|
|
366
|
+
cursorPosition: restoredBuffer.length, // Move cursor to end
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Update buffer
|
|
372
|
+
*/
|
|
373
|
+
export function setBuffer(state, buffer) {
|
|
374
|
+
return {
|
|
375
|
+
...state,
|
|
376
|
+
buffer,
|
|
377
|
+
cursorPosition: buffer.length, // Move cursor to end when setting buffer
|
|
378
|
+
historyIndex: -1, // Reset history navigation when typing
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Move cursor left
|
|
384
|
+
*/
|
|
385
|
+
export function moveCursorLeft(state) {
|
|
386
|
+
const newPos = Math.max(0, (state.cursorPosition || 0) - 1);
|
|
387
|
+
return { ...state, cursorPosition: newPos };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Move cursor right
|
|
392
|
+
*/
|
|
393
|
+
export function moveCursorRight(state) {
|
|
394
|
+
const maxPos = state.buffer.length;
|
|
395
|
+
const newPos = Math.min(maxPos, (state.cursorPosition || 0) + 1);
|
|
396
|
+
return { ...state, cursorPosition: newPos };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Move cursor to start of line (Ctrl+A)
|
|
401
|
+
*/
|
|
402
|
+
export function moveCursorToStart(state) {
|
|
403
|
+
return { ...state, cursorPosition: 0 };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Move cursor to end of line (Ctrl+E)
|
|
408
|
+
*/
|
|
409
|
+
export function moveCursorToEnd(state) {
|
|
410
|
+
return { ...state, cursorPosition: state.buffer.length };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Update buffer and slash menu state together
|
|
415
|
+
* This prevents flickering by updating both in a single render
|
|
416
|
+
*
|
|
417
|
+
* @param {Object} state
|
|
418
|
+
* @param {string} buffer - New buffer value
|
|
419
|
+
* @param {Function} getSlashItems - Function that returns menu items for slash commands
|
|
420
|
+
*/
|
|
421
|
+
export function setBufferWithSlashMenu(state, buffer, getSlashItems) {
|
|
422
|
+
const newState = {
|
|
423
|
+
...state,
|
|
424
|
+
buffer,
|
|
425
|
+
cursorPosition: buffer.length, // Move cursor to end when setting buffer
|
|
426
|
+
historyIndex: -1,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Only handle slash menu auto-show/hide (not path or @ completion)
|
|
430
|
+
if (state.isExecuting) {
|
|
431
|
+
return newState;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if buffer starts with /
|
|
435
|
+
if (buffer.startsWith('/')) {
|
|
436
|
+
const items = getSlashItems(buffer);
|
|
437
|
+
if (items.length > 0) {
|
|
438
|
+
// Show slash menu
|
|
439
|
+
return {
|
|
440
|
+
...newState,
|
|
441
|
+
menuVisible: true,
|
|
442
|
+
menuItems: items,
|
|
443
|
+
selectedIndex: state.menuType === 'slash' ? state.selectedIndex : 0,
|
|
444
|
+
menuType: 'slash',
|
|
445
|
+
};
|
|
446
|
+
} else if (state.menuType === 'slash') {
|
|
447
|
+
// Hide slash menu (no matches)
|
|
448
|
+
return {
|
|
449
|
+
...newState,
|
|
450
|
+
menuVisible: false,
|
|
451
|
+
menuItems: [],
|
|
452
|
+
selectedIndex: 0,
|
|
453
|
+
menuType: null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
} else if (state.menuType === 'slash') {
|
|
457
|
+
// Buffer no longer starts with /, hide slash menu
|
|
458
|
+
return {
|
|
459
|
+
...newState,
|
|
460
|
+
menuVisible: false,
|
|
461
|
+
menuItems: [],
|
|
462
|
+
selectedIndex: 0,
|
|
463
|
+
menuType: null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return newState;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Add command to history
|
|
472
|
+
*/
|
|
473
|
+
export function addToHistory(state, command) {
|
|
474
|
+
if (!command.trim()) return state;
|
|
475
|
+
|
|
476
|
+
// Avoid duplicates at end
|
|
477
|
+
const lastCommand = state.commandHistory[state.commandHistory.length - 1];
|
|
478
|
+
if (lastCommand === command) return state;
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
...state,
|
|
482
|
+
commandHistory: [...state.commandHistory, command],
|
|
483
|
+
historyIndex: -1,
|
|
484
|
+
savedBuffer: '',
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Update resources
|
|
490
|
+
*/
|
|
491
|
+
export function updateResources(state, resources) {
|
|
492
|
+
return {
|
|
493
|
+
...state,
|
|
494
|
+
resources,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Update shell prompt
|
|
500
|
+
*/
|
|
501
|
+
export function updatePrompt(state, prompt) {
|
|
502
|
+
return {
|
|
503
|
+
...state,
|
|
504
|
+
shellPrompt: prompt,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Scroll detail view
|
|
510
|
+
* @param {Object} state - Current state
|
|
511
|
+
* @param {number} delta - Scroll amount (positive = down, negative = up)
|
|
512
|
+
* @param {number} viewportHeight - Optional viewport height for max scroll calculation
|
|
513
|
+
*/
|
|
514
|
+
export function scrollDetailView(state, delta, viewportHeight = 0) {
|
|
515
|
+
if (!state.detailContent) return state;
|
|
516
|
+
|
|
517
|
+
const lines = Number.isFinite(state.detailContent.lineCount)
|
|
518
|
+
? state.detailContent.lineCount
|
|
519
|
+
: state.detailContent.content.split('\n').length;
|
|
520
|
+
|
|
521
|
+
// Calculate max scroll offset
|
|
522
|
+
// If viewport height is provided, limit so last page of content is visible
|
|
523
|
+
// Otherwise, allow scrolling to last line (legacy behavior)
|
|
524
|
+
let maxOffset;
|
|
525
|
+
if (viewportHeight > 0) {
|
|
526
|
+
// Content area height = viewportHeight - 2 (header)
|
|
527
|
+
const contentHeight = viewportHeight - 2;
|
|
528
|
+
maxOffset = Math.max(0, lines - contentHeight);
|
|
529
|
+
} else {
|
|
530
|
+
maxOffset = Math.max(0, lines - 1);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const newOffset = Math.max(0, Math.min(maxOffset, state.detailContent.scrollOffset + delta));
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
...state,
|
|
537
|
+
detailContent: {
|
|
538
|
+
...state.detailContent,
|
|
539
|
+
scrollOffset: newOffset,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Toggle console visibility
|
|
546
|
+
*/
|
|
547
|
+
export function toggleConsole(state) {
|
|
548
|
+
return {
|
|
549
|
+
...state,
|
|
550
|
+
consoleVisible: !state.consoleVisible,
|
|
551
|
+
consoleScrollOffset: 0, // Reset scroll when toggling
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Add log entry to console
|
|
557
|
+
* Auto-scrolls to bottom to show latest logs
|
|
558
|
+
*/
|
|
559
|
+
export function addConsoleLog(state, level, message, consoleHeight = 7) {
|
|
560
|
+
const newLog = {
|
|
561
|
+
id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
562
|
+
level, // 'info' | 'warn' | 'error' | 'debug'
|
|
563
|
+
message,
|
|
564
|
+
timestamp: new Date(),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const newLogs = [...state.consoleLogs, newLog];
|
|
568
|
+
// Auto-scroll to bottom: offset = total logs - visible lines (keep latest visible)
|
|
569
|
+
const newScrollOffset = Math.max(0, newLogs.length - consoleHeight);
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
...state,
|
|
573
|
+
consoleLogs: newLogs,
|
|
574
|
+
consoleScrollOffset: newScrollOffset,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Clear console logs
|
|
580
|
+
*/
|
|
581
|
+
export function clearConsoleLogs(state) {
|
|
582
|
+
return {
|
|
583
|
+
...state,
|
|
584
|
+
consoleLogs: [],
|
|
585
|
+
consoleScrollOffset: 0,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Scroll console
|
|
591
|
+
*/
|
|
592
|
+
export function scrollConsole(state, delta) {
|
|
593
|
+
const maxOffset = Math.max(0, state.consoleLogs.length - 6); // 6 visible lines
|
|
594
|
+
const newOffset = Math.max(0, Math.min(maxOffset, state.consoleScrollOffset + delta));
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
...state,
|
|
598
|
+
consoleScrollOffset: newOffset,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Select console log (up/down navigation)
|
|
604
|
+
*/
|
|
605
|
+
export function selectConsoleLog(state, delta) {
|
|
606
|
+
if (state.consoleLogs.length === 0) return state;
|
|
607
|
+
|
|
608
|
+
let newIndex = state.consoleSelectedIndex + delta;
|
|
609
|
+
|
|
610
|
+
// Wrap around or clamp
|
|
611
|
+
if (newIndex < 0) newIndex = 0;
|
|
612
|
+
if (newIndex >= state.consoleLogs.length) newIndex = state.consoleLogs.length - 1;
|
|
613
|
+
|
|
614
|
+
// Auto-scroll to keep selection visible
|
|
615
|
+
const visibleHeight = 7;
|
|
616
|
+
let newOffset = state.consoleScrollOffset;
|
|
617
|
+
|
|
618
|
+
if (newIndex < newOffset) {
|
|
619
|
+
newOffset = newIndex;
|
|
620
|
+
} else if (newIndex >= newOffset + visibleHeight) {
|
|
621
|
+
newOffset = newIndex - visibleHeight + 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
...state,
|
|
626
|
+
consoleSelectedIndex: newIndex,
|
|
627
|
+
consoleScrollOffset: newOffset,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get selected console log message
|
|
633
|
+
*/
|
|
634
|
+
export function getSelectedConsoleLog(state) {
|
|
635
|
+
if (state.consoleSelectedIndex < 0 || state.consoleSelectedIndex >= state.consoleLogs.length) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
return state.consoleLogs[state.consoleSelectedIndex];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Clear console selection
|
|
643
|
+
*/
|
|
644
|
+
export function clearConsoleSelection(state) {
|
|
645
|
+
return {
|
|
646
|
+
...state,
|
|
647
|
+
consoleSelectedIndex: -1,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ============================================
|
|
652
|
+
// Mouse Drag Selection State Management
|
|
653
|
+
// ============================================
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Selection state structure:
|
|
657
|
+
* {
|
|
658
|
+
* isActive: boolean, // Whether selection is in progress
|
|
659
|
+
* startX: number, // Start column (1-based)
|
|
660
|
+
* startY: number, // Start row (1-based)
|
|
661
|
+
* endX: number, // End column (1-based)
|
|
662
|
+
* endY: number, // End row (1-based)
|
|
663
|
+
* content: string, // Selected text content
|
|
664
|
+
* source: 'detail' | 'console' | null, // Source of selection
|
|
665
|
+
* }
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Start text selection
|
|
670
|
+
* @param {Object} state
|
|
671
|
+
* @param {number} x - Starting column (1-based)
|
|
672
|
+
* @param {number} y - Starting row (1-based)
|
|
673
|
+
* @param {string} source - Selection source ('detail' | 'console')
|
|
674
|
+
*/
|
|
675
|
+
export function startSelection(state, x, y, source = null) {
|
|
676
|
+
return {
|
|
677
|
+
...state,
|
|
678
|
+
selection: {
|
|
679
|
+
isActive: true,
|
|
680
|
+
startX: x,
|
|
681
|
+
startY: y,
|
|
682
|
+
endX: x,
|
|
683
|
+
endY: y,
|
|
684
|
+
content: '',
|
|
685
|
+
source,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Update selection end position during drag
|
|
692
|
+
* @param {Object} state
|
|
693
|
+
* @param {number} x - Current column (1-based)
|
|
694
|
+
* @param {number} y - Current row (1-based)
|
|
695
|
+
* @param {Array<string>} lines - Text lines to extract content from
|
|
696
|
+
*/
|
|
697
|
+
export function updateSelection(state, x, y, lines = []) {
|
|
698
|
+
if (!state.selection || !state.selection.isActive) return state;
|
|
699
|
+
|
|
700
|
+
const selection = {
|
|
701
|
+
...state.selection,
|
|
702
|
+
endX: x,
|
|
703
|
+
endY: y,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Calculate selected text content
|
|
707
|
+
selection.content = extractSelectedText(selection, lines);
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
...state,
|
|
711
|
+
selection,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* End selection (mouse up)
|
|
717
|
+
* @param {Object} state
|
|
718
|
+
* @param {number} x - End column (1-based)
|
|
719
|
+
* @param {number} y - End row (1-based)
|
|
720
|
+
* @param {Array<string>} lines - Text lines to extract content from
|
|
721
|
+
*/
|
|
722
|
+
export function endSelection(state, x, y, lines = []) {
|
|
723
|
+
if (!state.selection || !state.selection.isActive) return state;
|
|
724
|
+
|
|
725
|
+
const selection = {
|
|
726
|
+
...state.selection,
|
|
727
|
+
isActive: false,
|
|
728
|
+
endX: x,
|
|
729
|
+
endY: y,
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Calculate final selected text content
|
|
733
|
+
selection.content = extractSelectedText(selection, lines);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
...state,
|
|
737
|
+
selection,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Clear selection
|
|
743
|
+
* @param {Object} state
|
|
744
|
+
*/
|
|
745
|
+
export function clearSelection(state) {
|
|
746
|
+
return {
|
|
747
|
+
...state,
|
|
748
|
+
selection: null,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Get selected text content
|
|
754
|
+
* @param {Object} state
|
|
755
|
+
* @returns {string|null} Selected text or null if no selection
|
|
756
|
+
*/
|
|
757
|
+
export function getSelectedText(state) {
|
|
758
|
+
return state.selection?.content || null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Check if there is an active selection
|
|
763
|
+
* @param {Object} state
|
|
764
|
+
* @returns {boolean}
|
|
765
|
+
*/
|
|
766
|
+
export function hasSelection(state) {
|
|
767
|
+
return !!state.selection && !!state.selection.content;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Extract selected text from lines based on selection coordinates
|
|
772
|
+
* @param {Object} selection - Selection state
|
|
773
|
+
* @param {Array<string>} lines - Array of text lines
|
|
774
|
+
* @returns {string} Selected text
|
|
775
|
+
*/
|
|
776
|
+
function extractSelectedText(selection, lines) {
|
|
777
|
+
if (!lines || lines.length === 0) return '';
|
|
778
|
+
|
|
779
|
+
const { startX, startY, endX, endY } = normalizeSelection(selection);
|
|
780
|
+
|
|
781
|
+
// Convert 1-based coordinates to 0-based
|
|
782
|
+
const startRow = Math.max(0, startY - 1);
|
|
783
|
+
const endRow = Math.min(lines.length - 1, endY - 1);
|
|
784
|
+
const startCol = Math.max(0, startX - 1);
|
|
785
|
+
const endCol = Math.max(0, endX - 1);
|
|
786
|
+
|
|
787
|
+
if (startRow > endRow) return '';
|
|
788
|
+
if (startRow === endRow && startCol > endCol) return '';
|
|
789
|
+
|
|
790
|
+
const selectedLines = [];
|
|
791
|
+
|
|
792
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
793
|
+
const line = lines[row] || '';
|
|
794
|
+
|
|
795
|
+
if (row === startRow && row === endRow) {
|
|
796
|
+
// Single line selection
|
|
797
|
+
selectedLines.push(line.substring(startCol, endCol));
|
|
798
|
+
} else if (row === startRow) {
|
|
799
|
+
// First line of multi-line selection
|
|
800
|
+
selectedLines.push(line.substring(startCol));
|
|
801
|
+
} else if (row === endRow) {
|
|
802
|
+
// Last line of multi-line selection
|
|
803
|
+
selectedLines.push(line.substring(0, endCol));
|
|
804
|
+
} else {
|
|
805
|
+
// Middle lines - select entire line
|
|
806
|
+
selectedLines.push(line);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return selectedLines.join('\n');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Normalize selection coordinates (ensure start <= end)
|
|
815
|
+
* @param {Object} selection - Selection state
|
|
816
|
+
* @returns {Object} Normalized coordinates
|
|
817
|
+
*/
|
|
818
|
+
function normalizeSelection(selection) {
|
|
819
|
+
let { startX, startY, endX, endY } = selection;
|
|
820
|
+
|
|
821
|
+
// Swap if end is before start
|
|
822
|
+
if (startY > endY || (startY === endY && startX > endX)) {
|
|
823
|
+
[startX, endX] = [endX, startX];
|
|
824
|
+
[startY, endY] = [endY, startY];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return { startX, startY, endX, endY };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Check if a position is within the selection
|
|
832
|
+
* @param {Object} selection - Selection state
|
|
833
|
+
* @param {number} x - Column (1-based)
|
|
834
|
+
* @param {number} y - Row (1-based)
|
|
835
|
+
* @returns {boolean}
|
|
836
|
+
*/
|
|
837
|
+
export function isPositionSelected(selection, x, y) {
|
|
838
|
+
if (!selection) return false;
|
|
839
|
+
|
|
840
|
+
const { startX, startY, endX, endY } = normalizeSelection(selection);
|
|
841
|
+
|
|
842
|
+
if (y < startY || y > endY) return false;
|
|
843
|
+
if (y === startY && y === endY) {
|
|
844
|
+
return x >= startX && x < endX;
|
|
845
|
+
}
|
|
846
|
+
if (y === startY) return x >= startX;
|
|
847
|
+
if (y === endY) return x < endX;
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Check if a line has selection and return selection ranges
|
|
853
|
+
* @param {Object} selection - Selection state
|
|
854
|
+
* @param {number} lineIndex - 0-based line index
|
|
855
|
+
* @param {number} lineLength - Length of the line
|
|
856
|
+
* @returns {Array<{start: number, end: number}>} Selection ranges for this line
|
|
857
|
+
*/
|
|
858
|
+
export function getLineSelectionRanges(selection, lineIndex, lineLength) {
|
|
859
|
+
if (!selection) return [];
|
|
860
|
+
|
|
861
|
+
const lineY = lineIndex + 1; // Convert to 1-based
|
|
862
|
+
const { startX, startY, endX, endY } = normalizeSelection(selection);
|
|
863
|
+
|
|
864
|
+
// Line is outside selection
|
|
865
|
+
if (lineY < startY || lineY > endY) return [];
|
|
866
|
+
|
|
867
|
+
const ranges = [];
|
|
868
|
+
|
|
869
|
+
if (startY === endY) {
|
|
870
|
+
// Single line selection
|
|
871
|
+
if (lineY === startY) {
|
|
872
|
+
ranges.push({
|
|
873
|
+
start: Math.max(0, startX - 1),
|
|
874
|
+
end: Math.min(lineLength, endX - 1),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
} else if (lineY === startY) {
|
|
878
|
+
// First line of multi-line
|
|
879
|
+
ranges.push({
|
|
880
|
+
start: Math.max(0, startX - 1),
|
|
881
|
+
end: lineLength,
|
|
882
|
+
});
|
|
883
|
+
} else if (lineY === endY) {
|
|
884
|
+
// Last line of multi-line
|
|
885
|
+
ranges.push({
|
|
886
|
+
start: 0,
|
|
887
|
+
end: Math.min(lineLength, endX - 1),
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
// Middle line - entire line selected
|
|
891
|
+
ranges.push({
|
|
892
|
+
start: 0,
|
|
893
|
+
end: lineLength,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return ranges;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Set paginated file state (for large file viewing)
|
|
902
|
+
*/
|
|
903
|
+
export function setPaginatedFile(state, fileInfo) {
|
|
904
|
+
return {
|
|
905
|
+
...state,
|
|
906
|
+
paginatedFile: fileInfo,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Clear paginated file state
|
|
912
|
+
*/
|
|
913
|
+
export function clearPaginatedFile(state) {
|
|
914
|
+
return {
|
|
915
|
+
...state,
|
|
916
|
+
paginatedFile: null,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Update paginated file loading state
|
|
922
|
+
*/
|
|
923
|
+
export function setPaginatedFileLoading(state, isLoading) {
|
|
924
|
+
if (!state.paginatedFile) return state;
|
|
925
|
+
return {
|
|
926
|
+
...state,
|
|
927
|
+
paginatedFile: {
|
|
928
|
+
...state.paginatedFile,
|
|
929
|
+
isLoading,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Extend paginated file loaded range (prepend or append content)
|
|
936
|
+
*/
|
|
937
|
+
export function extendPaginatedFileRange(state, newStartLine, newEndLine) {
|
|
938
|
+
if (!state.paginatedFile) return state;
|
|
939
|
+
return {
|
|
940
|
+
...state,
|
|
941
|
+
paginatedFile: {
|
|
942
|
+
...state.paginatedFile,
|
|
943
|
+
loadedStartLine: Math.min(state.paginatedFile.loadedStartLine, newStartLine),
|
|
944
|
+
loadedEndLine: Math.max(state.paginatedFile.loadedEndLine, newEndLine),
|
|
945
|
+
},
|
|
946
|
+
};
|
|
947
|
+
}
|