@churivibhav/reqex 0.1.2 → 0.1.3

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/dist/cli.js CHANGED
@@ -1,14 +1,93 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path4 from "path";
5
- import process2 from "process";
4
+ import path7 from "path";
5
+ import process3 from "process";
6
+ import { ZR_MOD_CTRL, ZR_MOD_SHIFT } from "@rezi-ui/core/keybindings";
6
7
  import { createNodeApp } from "@rezi-ui/node";
7
8
 
8
- // src/config/keybindings.ts
9
+ // src/config/config.ts
9
10
  import fs from "fs";
11
+ import path2 from "path";
12
+
13
+ // src/config/paths.ts
10
14
  import os from "os";
11
15
  import path from "path";
16
+ function getConfigDir() {
17
+ if (process.env.REQEX_CONFIG_DIR) {
18
+ return process.env.REQEX_CONFIG_DIR;
19
+ }
20
+ if (process.platform === "win32") {
21
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
22
+ return path.join(appData, "reqex");
23
+ }
24
+ if (process.platform === "darwin") {
25
+ return path.join(os.homedir(), "Library", "Application Support", "reqex");
26
+ }
27
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
28
+ return path.join(xdg, "reqex");
29
+ }
30
+ function getProjectConfigDir(workspaceRoot) {
31
+ return path.join(workspaceRoot, ".reqex");
32
+ }
33
+
34
+ // src/config/config.ts
35
+ var DEFAULT_THEME = "auto";
36
+ function readJsonIfExists(filePath) {
37
+ try {
38
+ if (!fs.existsSync(filePath)) {
39
+ return null;
40
+ }
41
+ const raw = fs.readFileSync(filePath, "utf8");
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function parseThemePreference(value) {
48
+ if (value === "auto" || value === "light" || value === "dark") {
49
+ return value;
50
+ }
51
+ return null;
52
+ }
53
+ function loadConfig(workspaceRoot) {
54
+ const userConfig = readJsonIfExists(path2.join(getConfigDir(), "config.json"));
55
+ const projectConfig = readJsonIfExists(
56
+ path2.join(getProjectConfigDir(workspaceRoot), "config.json")
57
+ );
58
+ const envTheme = parseThemePreference(process.env.REQEX_THEME);
59
+ const fileTheme = projectConfig?.theme ?? userConfig?.theme ?? DEFAULT_THEME;
60
+ return {
61
+ theme: envTheme ?? fileTheme
62
+ };
63
+ }
64
+ function watchConfig(workspaceRoot, onChange) {
65
+ const files = [
66
+ path2.join(getConfigDir(), "config.json"),
67
+ path2.join(getProjectConfigDir(workspaceRoot), "config.json")
68
+ ];
69
+ const watchers = files.map((filePath) => {
70
+ const dir = path2.dirname(filePath);
71
+ try {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ } catch {
74
+ }
75
+ return fs.watch(dir, { persistent: false }, (_event, filename) => {
76
+ if (filename === "config.json") {
77
+ onChange();
78
+ }
79
+ });
80
+ });
81
+ return () => {
82
+ for (const watcher of watchers) {
83
+ watcher.close();
84
+ }
85
+ };
86
+ }
87
+
88
+ // src/config/keybindings.ts
89
+ import fs2 from "fs";
90
+ import path3 from "path";
12
91
  var VSCODE_DEFAULTS = {
13
92
  F5: "request.send",
14
93
  "ctrl+enter": "request.send",
@@ -30,13 +109,14 @@ var VSCODE_DEFAULTS = {
30
109
  F11: "pane.zoom",
31
110
  z: "pane.zoom",
32
111
  F1: "help.show",
33
- "?": "help.show",
34
112
  "ctrl+/": "keybindings.show",
35
113
  escape: "overlay.close",
36
114
  "ctrl+q": "app.quit",
37
115
  "ctrl+x": "request.cancel",
38
116
  "ctrl+shift+c": "response.copy",
39
117
  "ctrl+f": "response.search",
118
+ "ctrl+[": "response.jsonFoldToggle",
119
+ "ctrl+]": "response.jsonUnfoldAll",
40
120
  "ctrl+tab": "response.tab.next",
41
121
  "ctrl+shift+tab": "response.tab.prev"
42
122
  };
@@ -71,6 +151,8 @@ var COMMAND_LABELS = {
71
151
  "response.tab.prev": "Previous response tab",
72
152
  "response.copy": "Copy response",
73
153
  "response.search": "Search response",
154
+ "response.jsonFoldToggle": "Fold/unfold JSON node",
155
+ "response.jsonUnfoldAll": "Unfold all JSON",
74
156
  "editor.searchNext": "Find next in editor"
75
157
  };
76
158
  var CHORD_PART_LABELS = {
@@ -116,38 +198,21 @@ function buildKeybindingsViewLines(bindings, maxLines) {
116
198
  }
117
199
  return rows;
118
200
  }
119
- function getConfigDir() {
120
- if (process.env.REQEX_CONFIG_DIR) {
121
- return process.env.REQEX_CONFIG_DIR;
122
- }
123
- if (process.platform === "win32") {
124
- const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
125
- return path.join(appData, "reqex");
126
- }
127
- if (process.platform === "darwin") {
128
- return path.join(os.homedir(), "Library", "Application Support", "reqex");
129
- }
130
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
131
- return path.join(xdg, "reqex");
132
- }
133
- function getProjectConfigDir(workspaceRoot) {
134
- return path.join(workspaceRoot, ".reqex");
135
- }
136
- function readJsonIfExists(filePath) {
201
+ function readJsonIfExists2(filePath) {
137
202
  try {
138
- if (!fs.existsSync(filePath)) {
203
+ if (!fs2.existsSync(filePath)) {
139
204
  return null;
140
205
  }
141
- const raw = fs.readFileSync(filePath, "utf8");
206
+ const raw = fs2.readFileSync(filePath, "utf8");
142
207
  return JSON.parse(raw);
143
208
  } catch {
144
209
  return null;
145
210
  }
146
211
  }
147
212
  function loadKeybindings(workspaceRoot) {
148
- const userConfig = readJsonIfExists(path.join(getConfigDir(), "keybindings.json"));
149
- const projectConfig = readJsonIfExists(
150
- path.join(getProjectConfigDir(workspaceRoot), "keybindings.json")
213
+ const userConfig = readJsonIfExists2(path3.join(getConfigDir(), "keybindings.json"));
214
+ const projectConfig = readJsonIfExists2(
215
+ path3.join(getProjectConfigDir(workspaceRoot), "keybindings.json")
151
216
  );
152
217
  const preset = projectConfig?.preset ?? userConfig?.preset ?? "vscode";
153
218
  const defaults = preset === "vim" ? VIM_DEFAULTS : VSCODE_DEFAULTS;
@@ -164,10 +229,10 @@ function watchKeybindings(workspaceRoot, onChange) {
164
229
  const dirs = [getConfigDir(), getProjectConfigDir(workspaceRoot)];
165
230
  const watchers = dirs.map((dir) => {
166
231
  try {
167
- fs.mkdirSync(dir, { recursive: true });
232
+ fs2.mkdirSync(dir, { recursive: true });
168
233
  } catch {
169
234
  }
170
- return fs.watch(dir, { persistent: false }, () => onChange());
235
+ return fs2.watch(dir, { persistent: false }, () => onChange());
171
236
  });
172
237
  return () => {
173
238
  for (const watcher of watchers) {
@@ -175,34 +240,166 @@ function watchKeybindings(workspaceRoot, onChange) {
175
240
  }
176
241
  };
177
242
  }
178
- var FOOTER_HINTS = {
179
- editor: ["F5 Send", "Ctrl+S Save", "Tab Panes", "F2 Palette"],
180
- response: ["Tab Panes", "Ctrl+Shift+C Copy", "F5 Send", "F2 Palette"],
181
- files: ["Enter Open", "Tab Panes", "Ctrl+P Files", "F2 Palette"],
182
- overlay: ["\u2191\u2193 Navigate", "Enter Select", "Esc Close"],
183
- sending: ["Ctrl+X Cancel", "Tab Panes", "Ctrl+Q Quit"]
243
+ var FOOTER_COMMANDS = {
244
+ editor: ["file.save", "palette.commands"],
245
+ response: ["response.copy", "response.jsonFoldToggle", "response.jsonUnfoldAll"],
246
+ files: ["palette.files", "pane.focusNext", "palette.commands"],
247
+ overlay: ["overlay.close"],
248
+ sending: ["pane.focusNext", "app.quit"]
249
+ };
250
+ var FOOTER_LABELS = {
251
+ "request.send": "Send",
252
+ "request.cancel": "Cancel",
253
+ "pane.focusNext": "Panes",
254
+ "pane.focusPrev": "Prev pane",
255
+ "pane.focusFiles": "Files pane",
256
+ "pane.focusEditor": "Editor pane",
257
+ "pane.focusResponse": "Response pane",
258
+ "sidebar.toggle": "Sidebar",
259
+ "file.save": "Save",
260
+ "env.switcher": "Env",
261
+ "env.selectNext": "Next env",
262
+ "env.selectPrev": "Prev env",
263
+ "env.apply": "Select",
264
+ "overlay.close": "Close",
265
+ "app.quit": "Quit",
266
+ "palette.commands": "Palette",
267
+ "palette.files": "Files",
268
+ "help.show": "Help",
269
+ "keybindings.show": "Keys",
270
+ "pane.zoom": "Zoom",
271
+ "response.tab.next": "Next tab",
272
+ "response.tab.prev": "Prev tab",
273
+ "response.copy": "Copy",
274
+ "response.search": "Search",
275
+ "response.jsonFoldToggle": "Fold",
276
+ "response.jsonUnfoldAll": "Unfold",
277
+ "editor.searchNext": "Find next"
184
278
  };
185
- function footerHints(context) {
186
- let hints;
279
+ var PREFERRED_FOOTER_KEYS = {
280
+ "request.send": ["F5", "ctrl+enter", "alt+enter"],
281
+ "request.cancel": ["ctrl+x"],
282
+ "pane.focusNext": ["tab"],
283
+ "pane.focusPrev": ["shift+tab"],
284
+ "pane.focusFiles": ["ctrl+1", "alt+1"],
285
+ "pane.focusEditor": ["ctrl+2", "alt+2"],
286
+ "pane.focusResponse": ["ctrl+3", "alt+3"],
287
+ "sidebar.toggle": ["ctrl+b"],
288
+ "file.save": ["ctrl+s"],
289
+ "env.switcher": ["ctrl+e"],
290
+ "env.selectNext": ["down"],
291
+ "env.selectPrev": ["up"],
292
+ "env.apply": ["enter"],
293
+ "overlay.close": ["escape"],
294
+ "app.quit": ["ctrl+q"],
295
+ "palette.commands": ["F2", "ctrl+shift+p"],
296
+ "palette.files": ["ctrl+p"],
297
+ "help.show": ["F1"],
298
+ "keybindings.show": ["ctrl+/"],
299
+ "pane.zoom": ["F11", "z"],
300
+ "response.tab.next": ["ctrl+tab"],
301
+ "response.tab.prev": ["ctrl+shift+tab"],
302
+ "response.copy": ["ctrl+shift+c"],
303
+ "response.search": ["ctrl+f"],
304
+ "response.jsonFoldToggle": ["ctrl+["],
305
+ "response.jsonUnfoldAll": ["ctrl+]"],
306
+ "editor.searchNext": []
307
+ };
308
+ function commandKey(bindings, command) {
309
+ const keys = Object.entries(bindings).filter(([, boundCommand]) => boundCommand === command).map(([key]) => key);
310
+ if (command === "help.show" && keys.length === 0) {
311
+ return "F1";
312
+ }
313
+ if (keys.length === 0) {
314
+ return null;
315
+ }
316
+ const preferred = PREFERRED_FOOTER_KEYS[command] ?? [];
317
+ keys.sort((a, b) => {
318
+ const aPreferred = preferred.indexOf(a);
319
+ const bPreferred = preferred.indexOf(b);
320
+ if (aPreferred !== -1 || bPreferred !== -1) {
321
+ return (aPreferred === -1 ? Number.MAX_SAFE_INTEGER : aPreferred) - (bPreferred === -1 ? Number.MAX_SAFE_INTEGER : bPreferred);
322
+ }
323
+ return formatKeyChord(a).length - formatKeyChord(b).length || a.localeCompare(b);
324
+ });
325
+ return keys[0] ?? null;
326
+ }
327
+ function footerCommandList(context) {
328
+ const action = context.sending ? "request.cancel" : "request.send";
329
+ let contextual;
187
330
  if (context.overlay !== "none") {
188
- hints = FOOTER_HINTS.overlay;
331
+ contextual = FOOTER_COMMANDS.overlay;
189
332
  } else if (context.sending) {
190
- hints = FOOTER_HINTS.sending;
333
+ contextual = FOOTER_COMMANDS.sending;
191
334
  } else {
192
- hints = FOOTER_HINTS[context.focusPane] ?? FOOTER_HINTS.editor;
335
+ contextual = FOOTER_COMMANDS[context.focusPane] ?? FOOTER_COMMANDS.editor;
336
+ }
337
+ const commands = [action, ...contextual];
338
+ if (!context.hasResponse) {
339
+ return commands.filter(
340
+ (command) => command !== "response.copy" && command !== "response.jsonFoldToggle" && command !== "response.jsonUnfoldAll"
341
+ );
342
+ }
343
+ if (context.responseTab !== "pretty") {
344
+ return commands.filter(
345
+ (command) => command !== "response.jsonFoldToggle" && command !== "response.jsonUnfoldAll"
346
+ );
347
+ }
348
+ if (!context.hasFoldedJson) {
349
+ return commands.filter((command) => command !== "response.jsonUnfoldAll");
193
350
  }
351
+ return commands;
352
+ }
353
+ function footerHintItems(context) {
354
+ const bindings = context.bindings ?? {};
355
+ const seen = /* @__PURE__ */ new Set();
356
+ const commands = [...footerCommandList(context), "help.show"].filter((command) => {
357
+ if (command === "help.show") {
358
+ seen.delete(command);
359
+ }
360
+ if (seen.has(command)) {
361
+ return false;
362
+ }
363
+ seen.add(command);
364
+ return true;
365
+ });
366
+ const items = commands.flatMap((command) => {
367
+ const key = commandKey(bindings, command);
368
+ if (!key) {
369
+ return [];
370
+ }
371
+ const formattedKey = command === "help.show" ? "F1" : formatKeyChord(key);
372
+ return [{ command, key: formattedKey, label: `${formattedKey} ${FOOTER_LABELS[command]}` }];
373
+ });
194
374
  const leftBudget = 48;
195
375
  const maxWidth = Math.max(20, context.viewportWidth - leftBudget);
196
- let text = hints.join(" \xB7 ");
197
- if (text.length > maxWidth) {
198
- text = `${text.slice(0, maxWidth - 1)}\u2026`;
376
+ const pinned = items.filter(
377
+ (item) => item.command === "request.send" || item.command === "request.cancel" || item.command === "help.show"
378
+ );
379
+ const middle = items.filter((item) => !pinned.includes(item));
380
+ const selected = [];
381
+ const append = (item) => {
382
+ selected.push(item);
383
+ };
384
+ append(pinned[0] ?? items[0]);
385
+ for (const item of middle) {
386
+ const candidate = [...selected, item, pinned[pinned.length - 1]].filter(Boolean);
387
+ if (candidate.map((entry) => entry.label).join(" \xB7 ").length <= maxWidth) {
388
+ append(item);
389
+ }
199
390
  }
200
- return text;
391
+ const help = pinned.find((item) => item.command === "help.show");
392
+ if (help && selected[selected.length - 1]?.command !== "help.show") {
393
+ append(help);
394
+ }
395
+ return selected.filter(Boolean);
201
396
  }
202
397
  var HELP_HINT_LINES = [
203
398
  "F5 Send request under cursor",
204
399
  "Tab / Shift+Tab Cycle panes",
205
400
  "Ctrl+S Save file",
401
+ "Mouse click Place editor cursor \xB7 Shift+click Select",
402
+ "Ctrl+A/C/X/V Select all, copy, cut, paste in editor",
206
403
  "Ctrl+E Environment switcher",
207
404
  "F2 / Ctrl+Shift+P Command palette",
208
405
  "Ctrl+Shift+C Copy response tab",
@@ -270,33 +467,91 @@ function resolveRegionAtLine(regions, line) {
270
467
  return best;
271
468
  });
272
469
  }
470
+ function resolveActiveRegion(parsedFile, cursorLine) {
471
+ if (!parsedFile) {
472
+ return null;
473
+ }
474
+ return resolveRegionAtLine(parsedFile.regions, cursorLine);
475
+ }
273
476
  var HTTP_METHOD_PREFIX = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|GRAPHQL)\b/u;
274
- function markerColumnAfterMethod(line) {
275
- const match = HTTP_METHOD_PREFIX.exec(line);
276
- return match ? match[0].length : 0;
277
- }
278
- function buildRegionDiagnostics(regions, activeRegionId, fileLines) {
279
- const markers = [];
280
- for (const region of regions) {
281
- if (!region.hasRequest || region.isGlobal) {
282
- continue;
477
+ function firstRequestLine(regions, fileLines) {
478
+ const firstRegion = regions.find((region) => region.hasRequest && !region.isGlobal);
479
+ if (!firstRegion) {
480
+ return null;
481
+ }
482
+ for (let line = firstRegion.startLine; line <= firstRegion.endLine; line++) {
483
+ if (HTTP_METHOD_PREFIX.test(fileLines[line] ?? "")) {
484
+ return line;
283
485
  }
284
- const isActive = region.id === activeRegionId;
285
- const line = fileLines[region.startLine] ?? "";
286
- const markerCol = markerColumnAfterMethod(line);
287
- markers.push({
288
- line: region.startLine,
289
- startColumn: markerCol,
290
- endColumn: markerCol + 1,
291
- severity: isActive ? "hint" : "info",
292
- message: `${region.method ?? "REQ"} ${region.name}`
293
- });
294
486
  }
295
- return markers;
487
+ return firstRegion.startLine;
296
488
  }
297
489
 
298
490
  // src/engine/store.ts
299
491
  import { store as httpyacStoreModule } from "httpyac";
492
+
493
+ // src/engine/env-config.ts
494
+ import fs3 from "fs/promises";
495
+ import path4 from "path";
496
+ var ENV_FILE_NAMES = [
497
+ ".env.json",
498
+ "http-client.env.json",
499
+ "http-client.private.env.json"
500
+ ];
501
+ function isRecord(value) {
502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
503
+ }
504
+ function mergeVariables(target, source) {
505
+ for (const [envName, variables] of Object.entries(source)) {
506
+ if (!isRecord(variables)) {
507
+ continue;
508
+ }
509
+ target[envName] = {
510
+ ...target[envName] ?? {},
511
+ ...variables
512
+ };
513
+ }
514
+ }
515
+ function directoriesToSearch(filePath, workingDir) {
516
+ const root = path4.resolve(workingDir);
517
+ const dirs = [];
518
+ let current = path4.dirname(path4.resolve(filePath));
519
+ while (current.startsWith(root)) {
520
+ dirs.unshift(current);
521
+ const parent = path4.dirname(current);
522
+ if (parent === current) {
523
+ break;
524
+ }
525
+ current = parent;
526
+ }
527
+ return dirs.length > 0 ? dirs : [path4.dirname(path4.resolve(filePath))];
528
+ }
529
+ async function readEnvFile(filePath) {
530
+ try {
531
+ const raw = await fs3.readFile(filePath, "utf8");
532
+ const parsed = JSON.parse(raw);
533
+ return isRecord(parsed) ? parsed : null;
534
+ } catch (error) {
535
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
536
+ return null;
537
+ }
538
+ return null;
539
+ }
540
+ }
541
+ async function loadEnvironmentConfig(filePath, workingDir) {
542
+ const environments = {};
543
+ for (const dir of directoriesToSearch(filePath, workingDir)) {
544
+ for (const fileName of ENV_FILE_NAMES) {
545
+ const parsed = await readEnvFile(path4.join(dir, fileName));
546
+ if (parsed) {
547
+ mergeVariables(environments, parsed);
548
+ }
549
+ }
550
+ }
551
+ return Object.keys(environments).length > 0 ? { environments } : {};
552
+ }
553
+
554
+ // src/engine/store.ts
300
555
  var store = new httpyacStoreModule.HttpFileStore();
301
556
  var versions = /* @__PURE__ */ new Map();
302
557
  function toRegion(region) {
@@ -321,7 +576,9 @@ function getParseVersion(filePath) {
321
576
  }
322
577
  async function parseFile(filePath, getText, workingDir, version) {
323
578
  const parseVersion = version ?? getParseVersion(filePath);
579
+ const config = await loadEnvironmentConfig(filePath, workingDir);
324
580
  const httpFile = await store.getOrCreate(filePath, getText, parseVersion, {
581
+ config,
325
582
  workingDir
326
583
  });
327
584
  const regions = httpFile.httpRegions.map(toRegion);
@@ -403,11 +660,13 @@ async function sendRegion(options) {
403
660
  const logResponse = async (response) => {
404
661
  capturedResponse = response;
405
662
  };
663
+ const config = await loadEnvironmentConfig(options.filePath, options.workingDir);
406
664
  try {
407
665
  await send({
408
666
  httpFile,
409
667
  httpRegion,
410
668
  activeEnvironment: options.activeEnvironment,
669
+ config,
411
670
  variables: options.variables,
412
671
  logResponse
413
672
  });
@@ -432,26 +691,28 @@ async function sendRegion(options) {
432
691
  };
433
692
  }
434
693
  }
435
- async function listEnvironments(filePath) {
694
+ async function listEnvironments(filePath, workingDir) {
436
695
  const httpFile = getHttpFile(filePath);
437
696
  if (!httpFile) {
438
697
  return [];
439
698
  }
440
- return getEnvironments({ httpFile });
699
+ const config = await loadEnvironmentConfig(filePath, workingDir);
700
+ return getEnvironments({ httpFile, config });
441
701
  }
442
- async function listVariables(filePath, activeEnvironment) {
702
+ async function listVariables(filePath, workingDir, activeEnvironment) {
443
703
  const httpFile = getHttpFile(filePath);
444
704
  if (!httpFile) {
445
705
  return {};
446
706
  }
447
- return getVariables({ httpFile, activeEnvironment });
707
+ const config = await loadEnvironmentConfig(filePath, workingDir);
708
+ return getVariables({ httpFile, activeEnvironment, config });
448
709
  }
449
710
 
450
711
  // src/keymap/dispatcher.ts
451
712
  function buildBindingMap(bindings, execute) {
452
713
  const map = {};
453
714
  for (const [key, command] of Object.entries(bindings)) {
454
- map[key] = () => execute(command);
715
+ map[key] = (ctx) => execute(command, ctx);
455
716
  }
456
717
  return map;
457
718
  }
@@ -464,6 +725,8 @@ function commandFromPaletteId(id) {
464
725
  "palette.commands": "palette.commands",
465
726
  "help.show": "help.show",
466
727
  "keybindings.show": "keybindings.show",
728
+ "response.jsonFoldToggle": "response.jsonFoldToggle",
729
+ "response.jsonUnfoldAll": "response.jsonUnfoldAll",
467
730
  "pane.zoom": "pane.zoom",
468
731
  "sidebar.toggle": "sidebar.toggle"
469
732
  };
@@ -475,6 +738,8 @@ var COMMAND_ITEMS = [
475
738
  { id: "file.save", label: "Save File", description: "Write editor to disk", shortcut: "Ctrl+S" },
476
739
  { id: "env.switcher", label: "Switch Environment", description: "Choose active environment", shortcut: "Ctrl+E" },
477
740
  { id: "sidebar.toggle", label: "Toggle Sidebar", description: "Show/hide file tree", shortcut: "Ctrl+B" },
741
+ { id: "response.jsonFoldToggle", label: "Fold/Unfold JSON", description: "Toggle JSON node in pretty response", shortcut: "Ctrl+[" },
742
+ { id: "response.jsonUnfoldAll", label: "Unfold All JSON", description: "Expand folded pretty response JSON", shortcut: "Ctrl+]" },
478
743
  { id: "pane.zoom", label: "Zoom Pane", description: "Zoom focused pane", shortcut: "F11" },
479
744
  { id: "help.show", label: "Help", description: "Show quick help", shortcut: "F1" },
480
745
  { id: "keybindings.show", label: "Keybindings", description: "Show all keybindings", shortcut: "Ctrl+/" }
@@ -487,7 +752,7 @@ import { EventEmitter } from "events";
487
752
 
488
753
  // src/workspace/discovery.ts
489
754
  import { readdir, stat } from "fs/promises";
490
- import path2 from "path";
755
+ import path5 from "path";
491
756
  var SKIP_DIRS = /* @__PURE__ */ new Set([
492
757
  ".git",
493
758
  "node_modules",
@@ -502,7 +767,7 @@ var FILE_KINDS = [
502
767
  { ext: ".env.json", kind: "env-json" }
503
768
  ];
504
769
  function classifyFile(filePath) {
505
- const base = path2.basename(filePath);
770
+ const base = path5.basename(filePath);
506
771
  for (const { ext, kind } of FILE_KINDS) {
507
772
  if (base === ext || base.endsWith(ext)) {
508
773
  return kind;
@@ -531,7 +796,7 @@ async function discoverDirectory(rootDir, currentDir) {
531
796
  continue;
532
797
  }
533
798
  }
534
- const fullPath = path2.join(currentDir, entry);
799
+ const fullPath = path5.join(currentDir, entry);
535
800
  let entryStat;
536
801
  try {
537
802
  entryStat = await stat(fullPath);
@@ -622,17 +887,17 @@ var Workspace = class extends EventEmitter {
622
887
  ],
623
888
  awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
624
889
  });
625
- const handle = async (type, path5) => {
890
+ const handle = async (type, path8) => {
626
891
  try {
627
892
  this.tree = await discoverFileTree(this.rootDir);
628
- this.emitChange({ type, path: path5 });
893
+ this.emitChange({ type, path: path8 });
629
894
  } catch (error) {
630
895
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
631
896
  }
632
897
  };
633
- this.watcher.on("add", (path5) => void handle("add", path5));
634
- this.watcher.on("change", (path5) => void handle("change", path5));
635
- this.watcher.on("unlink", (path5) => void handle("unlink", path5));
898
+ this.watcher.on("add", (path8) => void handle("add", path8));
899
+ this.watcher.on("change", (path8) => void handle("change", path8));
900
+ this.watcher.on("unlink", (path8) => void handle("unlink", path8));
636
901
  this.watcher.on("error", (error) => {
637
902
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
638
903
  });
@@ -662,12 +927,12 @@ function createInitialState(workspaceRoot) {
662
927
  dirty: false,
663
928
  parseVersion: 0,
664
929
  parsedFile: null,
665
- activeRegion: null,
666
930
  responseEditor: {
667
931
  scrollTop: 0,
668
932
  scrollLeft: 0,
669
933
  cursor: { line: 0, column: 0 },
670
- selection: null
934
+ selection: null,
935
+ foldedJsonPaths: []
671
936
  },
672
937
  resultGeneration: 0,
673
938
  editor: {
@@ -703,7 +968,9 @@ function createInitialState(workspaceRoot) {
703
968
  },
704
969
  settings: {
705
970
  keymapPreset: "vscode",
706
- keybindings: {}
971
+ keybindings: {},
972
+ theme: "auto",
973
+ themeMode: "dark"
707
974
  }
708
975
  };
709
976
  }
@@ -760,7 +1027,196 @@ function createSendController() {
760
1027
  };
761
1028
  }
762
1029
 
1030
+ // src/utils/http-syntax.ts
1031
+ var KEYWORDS = /* @__PURE__ */ new Set([
1032
+ "GET",
1033
+ "POST",
1034
+ "PUT",
1035
+ "PATCH",
1036
+ "DELETE",
1037
+ "HEAD",
1038
+ "OPTIONS",
1039
+ "CONNECT",
1040
+ "TRACE",
1041
+ "GRAPHQL"
1042
+ ]);
1043
+ function tokenizeHttpLine(line, _context) {
1044
+ const tokens = [];
1045
+ const methodMatch = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|GRAPHQL)\b/u.exec(
1046
+ line
1047
+ );
1048
+ if (methodMatch) {
1049
+ const method = methodMatch[1] ?? "";
1050
+ tokens.push({ text: method, kind: "keyword" });
1051
+ const rest = line.slice(methodMatch[0].length);
1052
+ if (rest.length > 0) {
1053
+ tokens.push({ text: rest, kind: "string" });
1054
+ }
1055
+ return tokens;
1056
+ }
1057
+ if (/^\s*#/u.test(line)) {
1058
+ return [{ text: line, kind: "comment" }];
1059
+ }
1060
+ if (/^\s*\/\//u.test(line)) {
1061
+ return [{ text: line, kind: "comment" }];
1062
+ }
1063
+ if (/^\s*@/u.test(line)) {
1064
+ return [{ text: line, kind: "type" }];
1065
+ }
1066
+ const headerMatch = /^\s*([!#$%&'*+\-.^_`|~0-9A-Za-z]+)(\s*:\s*)(.*)$/u.exec(line);
1067
+ if (headerMatch) {
1068
+ tokens.push({ text: headerMatch[1] ?? "", kind: "function" });
1069
+ tokens.push({ text: headerMatch[2] ?? "", kind: "operator" });
1070
+ tokens.push({ text: headerMatch[3] ?? "", kind: "string" });
1071
+ return tokens;
1072
+ }
1073
+ const words = line.split(/(\s+)/u);
1074
+ for (const word of words) {
1075
+ if (KEYWORDS.has(word)) {
1076
+ tokens.push({ text: word, kind: "keyword" });
1077
+ } else if (word.length > 0) {
1078
+ tokens.push({ text: word, kind: "plain" });
1079
+ }
1080
+ }
1081
+ return tokens.length > 0 ? tokens : [{ text: line, kind: "plain" }];
1082
+ }
1083
+ function prettyJsonIfPossible(text) {
1084
+ const trimmed = text.trim();
1085
+ if (!trimmed) {
1086
+ return text;
1087
+ }
1088
+ try {
1089
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
1090
+ } catch {
1091
+ return text;
1092
+ }
1093
+ }
1094
+ function statusTone(statusCode) {
1095
+ if (!statusCode) {
1096
+ return "cyan";
1097
+ }
1098
+ if (statusCode >= 200 && statusCode < 300) {
1099
+ return "green";
1100
+ }
1101
+ if (statusCode >= 400 && statusCode < 500) {
1102
+ return "yellow";
1103
+ }
1104
+ if (statusCode >= 500) {
1105
+ return "red";
1106
+ }
1107
+ return "cyan";
1108
+ }
1109
+
1110
+ // src/utils/json-folding.ts
1111
+ var INDENT = 2;
1112
+ function escapeJsonPointerSegment(segment) {
1113
+ return segment.replace(/~/gu, "~0").replace(/\//gu, "~1");
1114
+ }
1115
+ function isRecord2(value) {
1116
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1117
+ }
1118
+ function primitiveToJson(value) {
1119
+ return JSON.stringify(value);
1120
+ }
1121
+ function summarizeFolded(value) {
1122
+ if (Array.isArray(value)) {
1123
+ return `${value.length} ${value.length === 1 ? "item" : "items"}`;
1124
+ }
1125
+ if (isRecord2(value)) {
1126
+ const count = Object.keys(value).length;
1127
+ return `${count} ${count === 1 ? "property" : "properties"}`;
1128
+ }
1129
+ return "";
1130
+ }
1131
+ function renderValue(args) {
1132
+ const { value, path: path8, parentPath, key, indent, isLast, foldedPaths, lines, lineToFoldPath } = args;
1133
+ const leading = " ".repeat(indent);
1134
+ const keyPrefix = key === void 0 ? "" : `${JSON.stringify(key)}: `;
1135
+ const comma = isLast ? "" : ",";
1136
+ if (Array.isArray(value) || isRecord2(value)) {
1137
+ const isArray = Array.isArray(value);
1138
+ const open = isArray ? "[" : "{";
1139
+ const close = isArray ? "]" : "}";
1140
+ const entries = isArray ? value.map((item, index) => [String(index), item]) : Object.entries(value);
1141
+ if (entries.length === 0) {
1142
+ lines.push(`${leading}${keyPrefix}${open}${close}${comma}`);
1143
+ lineToFoldPath.push(parentPath);
1144
+ return;
1145
+ }
1146
+ if (foldedPaths.has(path8)) {
1147
+ lines.push(`${leading}${keyPrefix}${open} ... ${summarizeFolded(value)} ${close}${comma}`);
1148
+ lineToFoldPath.push(path8);
1149
+ return;
1150
+ }
1151
+ lines.push(`${leading}${keyPrefix}${open}`);
1152
+ lineToFoldPath.push(path8);
1153
+ entries.forEach(([entryKey, entryValue], index) => {
1154
+ const childPath = `${path8}/${escapeJsonPointerSegment(entryKey)}`;
1155
+ renderValue({
1156
+ value: entryValue,
1157
+ path: childPath,
1158
+ parentPath: path8,
1159
+ key: isArray ? void 0 : entryKey,
1160
+ indent: indent + INDENT,
1161
+ isLast: index === entries.length - 1,
1162
+ foldedPaths,
1163
+ lines,
1164
+ lineToFoldPath
1165
+ });
1166
+ });
1167
+ lines.push(`${leading}${close}${comma}`);
1168
+ lineToFoldPath.push(path8);
1169
+ return;
1170
+ }
1171
+ lines.push(`${leading}${keyPrefix}${primitiveToJson(value)}${comma}`);
1172
+ lineToFoldPath.push(parentPath);
1173
+ }
1174
+ function buildFoldableJsonView(text, foldedPaths) {
1175
+ try {
1176
+ const value = JSON.parse(text.trim());
1177
+ const lines = [];
1178
+ const lineToFoldPath = [];
1179
+ renderValue({
1180
+ value,
1181
+ path: "",
1182
+ parentPath: null,
1183
+ indent: 0,
1184
+ isLast: true,
1185
+ foldedPaths: new Set(foldedPaths),
1186
+ lines,
1187
+ lineToFoldPath
1188
+ });
1189
+ return { lines, lineToFoldPath };
1190
+ } catch {
1191
+ return { lines: text.split("\n"), lineToFoldPath: text.split("\n").map(() => null) };
1192
+ }
1193
+ }
1194
+ function toggleJsonFoldAtLine(text, foldedPaths, line) {
1195
+ const view = buildFoldableJsonView(text, foldedPaths);
1196
+ const path8 = view.lineToFoldPath[line] ?? null;
1197
+ if (path8 === null) {
1198
+ return foldedPaths;
1199
+ }
1200
+ const next = new Set(foldedPaths);
1201
+ if (next.has(path8)) {
1202
+ next.delete(path8);
1203
+ } else {
1204
+ next.add(path8);
1205
+ }
1206
+ return [...next].sort();
1207
+ }
1208
+
763
1209
  // src/state/commands.ts
1210
+ function envNameFromSelectedIndex(environments, selectedIndex) {
1211
+ return selectedIndex === 0 ? void 0 : environments[selectedIndex - 1];
1212
+ }
1213
+ function envSelectedIndexFromActive(environments, activeEnvironment) {
1214
+ if (activeEnvironment.length === 0) {
1215
+ return 0;
1216
+ }
1217
+ const idx = environments.indexOf(activeEnvironment[0]);
1218
+ return idx === -1 ? 0 : idx + 1;
1219
+ }
764
1220
  function createCommandContext(deps) {
765
1221
  setPromptHandler(async (request) => {
766
1222
  return new Promise((resolve) => {
@@ -791,27 +1247,28 @@ function createCommandContext(deps) {
791
1247
  ui: { ...state.ui, pendingPrompt: null }
792
1248
  }));
793
1249
  };
794
- const openFile = async (path5) => {
795
- const content = await deps.workspace.readFile(path5);
1250
+ const openFile = async (path8) => {
1251
+ const content = await deps.workspace.readFile(path8);
796
1252
  const lines = linesFromContent(content);
797
- const parsed = await parseFile(path5, async () => content, deps.workspace.rootDir);
798
- const environments = await listEnvironments(path5);
799
- const variables = await listVariables(path5, [...deps.getState().request.activeEnvironment]);
800
- const activeRegion = resolveRegionAtLine(parsed.regions, 0);
1253
+ const parsed = await parseFile(path8, async () => content, deps.workspace.rootDir);
1254
+ const environments = await listEnvironments(path8, deps.workspace.rootDir);
1255
+ const variables = await listVariables(path8, deps.workspace.rootDir, [
1256
+ ...deps.getState().request.activeEnvironment
1257
+ ]);
1258
+ const initialLine = firstRequestLine(parsed.regions, lines) ?? 0;
801
1259
  deps.update((state) => ({
802
1260
  ...state,
803
- selectedFilePath: path5,
1261
+ selectedFilePath: path8,
804
1262
  fileContent: content,
805
1263
  fileLines: lines,
806
1264
  dirty: false,
807
1265
  parseVersion: parsed.version,
808
1266
  parsedFile: parsed,
809
- activeRegion,
810
1267
  editor: {
811
1268
  ...state.editor,
812
- cursor: { line: activeRegion?.startLine ?? 0, column: 0 },
1269
+ cursor: { line: initialLine, column: 0 },
813
1270
  selection: null,
814
- scrollTop: activeRegion?.startLine ?? 0
1271
+ scrollTop: initialLine
815
1272
  },
816
1273
  request: {
817
1274
  ...state.request,
@@ -845,22 +1302,37 @@ function createCommandContext(deps) {
845
1302
  deps.workspace.rootDir,
846
1303
  version
847
1304
  );
848
- const activeRegion = state.activeRegion ? parsed.regions.find((region) => region.id === state.activeRegion?.id) ?? resolveRegionAtLine(parsed.regions, state.editor.cursor.line) : resolveRegionAtLine(parsed.regions, state.editor.cursor.line);
849
1305
  deps.update((s) => ({
850
1306
  ...s,
851
1307
  fileContent: content,
852
1308
  dirty: false,
853
1309
  parsedFile: parsed,
854
- activeRegion: activeRegion ?? null,
855
1310
  ui: { ...s.ui, statusMessage: "Saved" }
856
1311
  }));
857
1312
  };
858
1313
  const runSend = async () => {
859
1314
  const state = deps.getState();
860
- if (!state.selectedFilePath || !state.parsedFile) {
1315
+ if (!state.selectedFilePath) {
861
1316
  return;
862
1317
  }
863
- const region = resolveRegionAtLine(state.parsedFile.regions, state.editor.cursor.line);
1318
+ const cursorLine = state.editor.cursor.line;
1319
+ const content = contentFromLines(state.fileLines);
1320
+ let parsedFile = state.parsedFile;
1321
+ if (!parsedFile || state.dirty) {
1322
+ const version = bumpParseVersion(state.selectedFilePath);
1323
+ parsedFile = await parseFile(
1324
+ state.selectedFilePath,
1325
+ async () => content,
1326
+ deps.workspace.rootDir,
1327
+ version
1328
+ );
1329
+ deps.update((s) => ({
1330
+ ...s,
1331
+ parsedFile,
1332
+ parseVersion: parsedFile.version
1333
+ }));
1334
+ }
1335
+ const region = resolveActiveRegion(parsedFile, cursorLine);
864
1336
  if (!region) {
865
1337
  deps.update((s) => ({
866
1338
  ...s,
@@ -872,13 +1344,13 @@ function createCommandContext(deps) {
872
1344
  const gen = sendController.beginSend();
873
1345
  deps.update((s) => ({
874
1346
  ...s,
875
- activeRegion: region,
876
1347
  request: { ...s.request, sending: true, error: null, result: null },
877
1348
  responseEditor: {
878
1349
  scrollTop: 0,
879
1350
  scrollLeft: 0,
880
1351
  cursor: { line: 0, column: 0 },
881
- selection: null
1352
+ selection: null,
1353
+ foldedJsonPaths: []
882
1354
  },
883
1355
  resultGeneration: s.resultGeneration + 1,
884
1356
  ui: { ...s.ui, focusPane: "response", responseTab: "pretty" }
@@ -894,10 +1366,12 @@ function createCommandContext(deps) {
894
1366
  if (!sendController.isCurrent(gen)) {
895
1367
  return;
896
1368
  }
897
- const variables = await listVariables(state.selectedFilePath, [
898
- ...deps.getState().request.activeEnvironment
899
- ]);
900
- if (!sendController.isCurrent(gen)) {
1369
+ const variables = await listVariables(
1370
+ state.selectedFilePath,
1371
+ deps.workspace.rootDir,
1372
+ [...deps.getState().request.activeEnvironment]
1373
+ );
1374
+ if (!sendController.isCurrent(gen)) {
901
1375
  return;
902
1376
  }
903
1377
  deps.update((s) => ({
@@ -969,9 +1443,9 @@ function createCommandContext(deps) {
969
1443
  ui: {
970
1444
  ...s.ui,
971
1445
  overlay: s.ui.overlay === "env" ? "none" : "env",
972
- envSelectedIndex: Math.max(
973
- 0,
974
- s.request.environments.indexOf(s.request.activeEnvironment.join(",")) || 0
1446
+ envSelectedIndex: envSelectedIndexFromActive(
1447
+ s.request.environments,
1448
+ s.request.activeEnvironment
975
1449
  )
976
1450
  }
977
1451
  }));
@@ -1002,10 +1476,13 @@ function createCommandContext(deps) {
1002
1476
  }
1003
1477
  break;
1004
1478
  case "env.apply": {
1005
- const envName = state.request.environments[state.ui.envSelectedIndex];
1479
+ const envName = envNameFromSelectedIndex(
1480
+ state.request.environments,
1481
+ state.ui.envSelectedIndex
1482
+ );
1006
1483
  void (async () => {
1007
1484
  const activeEnvironment = envName ? [envName] : [];
1008
- const variables = state.selectedFilePath ? await listVariables(state.selectedFilePath, activeEnvironment) : {};
1485
+ const variables = state.selectedFilePath ? await listVariables(state.selectedFilePath, deps.workspace.rootDir, activeEnvironment) : {};
1009
1486
  deps.update((s) => ({
1010
1487
  ...s,
1011
1488
  request: { ...s.request, activeEnvironment, variables },
@@ -1095,6 +1572,46 @@ function createCommandContext(deps) {
1095
1572
  ui: { ...s.ui, focusPane: "response", responseTab: "pretty" }
1096
1573
  }));
1097
1574
  break;
1575
+ case "response.jsonFoldToggle": {
1576
+ const result = state.request.result;
1577
+ if (!result || state.ui.responseTab !== "pretty") {
1578
+ break;
1579
+ }
1580
+ const text = prettyJsonIfPossible(result.prettyBody || result.body);
1581
+ const foldedJsonPaths = toggleJsonFoldAtLine(
1582
+ text,
1583
+ state.responseEditor.foldedJsonPaths,
1584
+ state.responseEditor.cursor.line
1585
+ );
1586
+ if (foldedJsonPaths === state.responseEditor.foldedJsonPaths) {
1587
+ deps.update((s) => ({
1588
+ ...s,
1589
+ ui: { ...s.ui, statusMessage: "No foldable JSON node" }
1590
+ }));
1591
+ break;
1592
+ }
1593
+ const visibleLineCount = buildFoldableJsonView(text, foldedJsonPaths).lines.length;
1594
+ deps.update((s) => ({
1595
+ ...s,
1596
+ responseEditor: {
1597
+ ...s.responseEditor,
1598
+ foldedJsonPaths,
1599
+ cursor: {
1600
+ ...s.responseEditor.cursor,
1601
+ line: Math.min(s.responseEditor.cursor.line, Math.max(0, visibleLineCount - 1))
1602
+ }
1603
+ },
1604
+ ui: { ...s.ui, focusPane: "response", statusMessage: "JSON fold toggled" }
1605
+ }));
1606
+ break;
1607
+ }
1608
+ case "response.jsonUnfoldAll":
1609
+ deps.update((s) => ({
1610
+ ...s,
1611
+ responseEditor: { ...s.responseEditor, foldedJsonPaths: [] },
1612
+ ui: { ...s.ui, focusPane: "response", statusMessage: "JSON unfolded" }
1613
+ }));
1614
+ break;
1098
1615
  case "editor.searchNext":
1099
1616
  break;
1100
1617
  default:
@@ -1117,103 +1634,240 @@ function createCommandContext(deps) {
1117
1634
  return context;
1118
1635
  }
1119
1636
 
1637
+ // src/state/editor-edit.ts
1638
+ function clamp(value, min, max) {
1639
+ return Math.max(min, Math.min(max, value));
1640
+ }
1641
+ function clampCursor(lines, cursor) {
1642
+ const lineCount = Math.max(1, lines.length);
1643
+ const line = clamp(cursor.line, 0, lineCount - 1);
1644
+ const text = lines[line] ?? "";
1645
+ return { line, column: clamp(cursor.column, 0, text.length) };
1646
+ }
1647
+ function normalizeSelection(selection) {
1648
+ const { anchor, active } = selection;
1649
+ if (anchor.line < active.line || anchor.line === active.line && anchor.column <= active.column) {
1650
+ return [anchor, active];
1651
+ }
1652
+ return [active, anchor];
1653
+ }
1654
+ function deleteSelection(lines, selection) {
1655
+ const nextLines = [...lines];
1656
+ if (nextLines.length === 0) {
1657
+ nextLines.push("");
1658
+ }
1659
+ const [rawStart, rawEnd] = normalizeSelection(selection);
1660
+ const start = clampCursor(nextLines, rawStart);
1661
+ const end = clampCursor(nextLines, rawEnd);
1662
+ const startLine = nextLines[start.line] ?? "";
1663
+ const endLine = nextLines[end.line] ?? "";
1664
+ if (start.line === end.line) {
1665
+ nextLines[start.line] = startLine.slice(0, start.column) + startLine.slice(end.column);
1666
+ return { lines: nextLines, cursor: start };
1667
+ }
1668
+ nextLines.splice(
1669
+ start.line,
1670
+ end.line - start.line + 1,
1671
+ startLine.slice(0, start.column) + endLine.slice(end.column)
1672
+ );
1673
+ return { lines: nextLines, cursor: start };
1674
+ }
1675
+ function insertText(lines, cursor, text) {
1676
+ const nextLines = [...lines];
1677
+ if (nextLines.length === 0) {
1678
+ nextLines.push("");
1679
+ }
1680
+ const safeCursor = clampCursor(nextLines, cursor);
1681
+ const currentLine = nextLines[safeCursor.line] ?? "";
1682
+ const before = currentLine.slice(0, safeCursor.column);
1683
+ const after = currentLine.slice(safeCursor.column);
1684
+ const insertLines = text.replace(/\r\n?/gu, "\n").split("\n");
1685
+ if (insertLines.length === 1) {
1686
+ const inserted = insertLines[0] ?? "";
1687
+ nextLines[safeCursor.line] = before + inserted + after;
1688
+ return {
1689
+ lines: nextLines,
1690
+ cursor: { line: safeCursor.line, column: safeCursor.column + inserted.length }
1691
+ };
1692
+ }
1693
+ const first = before + (insertLines[0] ?? "");
1694
+ const lastInsert = insertLines[insertLines.length - 1] ?? "";
1695
+ const last = lastInsert + after;
1696
+ nextLines.splice(safeCursor.line, 1, first, ...insertLines.slice(1, -1), last);
1697
+ return {
1698
+ lines: nextLines,
1699
+ cursor: {
1700
+ line: safeCursor.line + insertLines.length - 1,
1701
+ column: lastInsert.length
1702
+ }
1703
+ };
1704
+ }
1705
+ function pasteIntoEditor(args) {
1706
+ const base = args.selection ? deleteSelection(args.lines, args.selection) : { lines: args.lines, cursor: args.cursor };
1707
+ const next = insertText(base.lines, base.cursor, args.text);
1708
+ return { ...next, selection: null };
1709
+ }
1710
+
1711
+ // src/ui/theme-colors.ts
1712
+ import {
1713
+ darkTheme,
1714
+ lightTheme,
1715
+ resolveColorToken
1716
+ } from "@rezi-ui/core";
1717
+ function token(theme, path8) {
1718
+ return resolveColorToken(theme, path8) ?? 0;
1719
+ }
1720
+ function themeForMode(mode) {
1721
+ return mode === "light" ? lightTheme : darkTheme;
1722
+ }
1723
+ function colorsFor(theme) {
1724
+ return {
1725
+ bgBase: token(theme, "bg.base"),
1726
+ bgElevated: token(theme, "bg.elevated"),
1727
+ bgSubtle: token(theme, "bg.subtle"),
1728
+ fgPrimary: token(theme, "fg.primary"),
1729
+ paneFocused: token(theme, "accent.secondary"),
1730
+ paneMuted: token(theme, "fg.muted"),
1731
+ selected: token(theme, "accent.primary"),
1732
+ dirty: token(theme, "warning"),
1733
+ success: token(theme, "success"),
1734
+ warning: token(theme, "warning"),
1735
+ error: token(theme, "error"),
1736
+ info: token(theme, "info")
1737
+ };
1738
+ }
1739
+ function colorsForMode(mode) {
1740
+ return colorsFor(themeForMode(mode));
1741
+ }
1742
+ function statusColorForTone(colors, tone) {
1743
+ switch (tone) {
1744
+ case "green":
1745
+ return colors.success;
1746
+ case "yellow":
1747
+ return colors.warning;
1748
+ case "red":
1749
+ return colors.error;
1750
+ default:
1751
+ return colors.info;
1752
+ }
1753
+ }
1754
+
1120
1755
  // src/ui/app-view.ts
1121
- import path3 from "path";
1756
+ import path6 from "path";
1122
1757
  import {
1123
- rgb,
1124
1758
  ui
1125
1759
  } from "@rezi-ui/core";
1126
1760
 
1127
- // src/utils/http-syntax.ts
1128
- var KEYWORDS = /* @__PURE__ */ new Set([
1129
- "GET",
1130
- "POST",
1131
- "PUT",
1132
- "PATCH",
1133
- "DELETE",
1134
- "HEAD",
1135
- "OPTIONS",
1136
- "CONNECT",
1137
- "TRACE",
1138
- "GRAPHQL"
1139
- ]);
1140
- function tokenizeHttpLine(line, _context) {
1141
- const tokens = [];
1142
- const methodMatch = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|GRAPHQL)\b/u.exec(
1143
- line
1144
- );
1145
- if (methodMatch) {
1146
- const method = methodMatch[1] ?? "";
1147
- tokens.push({ text: method, kind: "keyword" });
1148
- const rest = line.slice(methodMatch[0].length);
1149
- if (rest.length > 0) {
1150
- tokens.push({ text: rest, kind: "string" });
1151
- }
1152
- return tokens;
1153
- }
1154
- if (/^\s*#/u.test(line)) {
1155
- return [{ text: line, kind: "comment" }];
1156
- }
1157
- if (/^\s*\/\//u.test(line)) {
1158
- return [{ text: line, kind: "comment" }];
1159
- }
1160
- if (/^\s*@/u.test(line)) {
1161
- return [{ text: line, kind: "type" }];
1162
- }
1163
- const headerMatch = /^\s*([!#$%&'*+\-.^_`|~0-9A-Za-z]+)(\s*:\s*)(.*)$/u.exec(line);
1164
- if (headerMatch) {
1165
- tokens.push({ text: headerMatch[1] ?? "", kind: "function" });
1166
- tokens.push({ text: headerMatch[2] ?? "", kind: "operator" });
1167
- tokens.push({ text: headerMatch[3] ?? "", kind: "string" });
1168
- return tokens;
1169
- }
1170
- const words = line.split(/(\s+)/u);
1171
- for (const word of words) {
1172
- if (KEYWORDS.has(word)) {
1173
- tokens.push({ text: word, kind: "keyword" });
1174
- } else if (word.length > 0) {
1175
- tokens.push({ text: word, kind: "plain" });
1176
- }
1761
+ // src/ui/editor-gutter.ts
1762
+ var EDITOR_GUTTER_WIDTH = 1;
1763
+ var ACTIVE_GUTTER = "\u258E";
1764
+ var INACTIVE_GUTTER = " ";
1765
+ function clamp2(value, min, max) {
1766
+ return Math.max(min, Math.min(max, value));
1767
+ }
1768
+ function gutterTokenKind(method, highlighted) {
1769
+ if (!highlighted) {
1770
+ return "plain";
1771
+ }
1772
+ switch ((method ?? "GET").toUpperCase()) {
1773
+ case "GET":
1774
+ case "HEAD":
1775
+ case "OPTIONS":
1776
+ return "string";
1777
+ case "POST":
1778
+ case "PUT":
1779
+ case "PATCH":
1780
+ return "type";
1781
+ case "DELETE":
1782
+ return "operator";
1783
+ default:
1784
+ return "keyword";
1177
1785
  }
1178
- return tokens.length > 0 ? tokens : [{ text: line, kind: "plain" }];
1179
1786
  }
1180
- function prettyJsonIfPossible(text) {
1181
- const trimmed = text.trim();
1182
- if (!trimmed) {
1183
- return text;
1184
- }
1185
- try {
1186
- return JSON.stringify(JSON.parse(trimmed), null, 2);
1187
- } catch {
1188
- return text;
1189
- }
1787
+ function prefixEditorLines(lines, activeRegion) {
1788
+ return lines.map((line, index) => {
1789
+ const highlighted = activeRegion !== null && index >= activeRegion.startLine && index <= activeRegion.endLine;
1790
+ return `${highlighted ? ACTIVE_GUTTER : INACTIVE_GUTTER}${line}`;
1791
+ });
1190
1792
  }
1191
- function statusTone(statusCode) {
1192
- if (!statusCode) {
1193
- return "cyan";
1194
- }
1195
- if (statusCode >= 200 && statusCode < 300) {
1196
- return "green";
1793
+ function stripEditorLines(lines) {
1794
+ return lines.map((line) => line.slice(EDITOR_GUTTER_WIDTH));
1795
+ }
1796
+ function editorCursorFromSource(cursor) {
1797
+ return {
1798
+ line: cursor.line,
1799
+ column: cursor.column + EDITOR_GUTTER_WIDTH
1800
+ };
1801
+ }
1802
+ function sourceCursorFromEditor(cursor) {
1803
+ return {
1804
+ line: cursor.line,
1805
+ column: Math.max(0, cursor.column - EDITOR_GUTTER_WIDTH)
1806
+ };
1807
+ }
1808
+ function sourceCursorFromEditorPoint(args) {
1809
+ const { x, y, rect, lines, scrollTop, scrollLeft } = args;
1810
+ if (rect.w <= 0 || rect.h <= 0 || x < rect.x || x >= rect.x + rect.w || y < rect.y || y >= rect.y + rect.h) {
1811
+ return null;
1197
1812
  }
1198
- if (statusCode >= 400 && statusCode < 500) {
1199
- return "yellow";
1813
+ const lineCount = Math.max(1, lines.length);
1814
+ const lineNumberWidth2 = args.lineNumbers === false ? 0 : String(lineCount).length + 1;
1815
+ const line = clamp2(scrollTop + (y - rect.y), 0, lineCount - 1);
1816
+ const lineText = lines[line] ?? "";
1817
+ const localTextColumn = x - rect.x - lineNumberWidth2;
1818
+ const editorColumn = Math.max(0, scrollLeft + localTextColumn);
1819
+ const sourceColumn = clamp2(editorColumn - EDITOR_GUTTER_WIDTH, 0, lineText.length);
1820
+ return { line, column: sourceColumn };
1821
+ }
1822
+ function editorSelectionFromSource(selection) {
1823
+ if (!selection) {
1824
+ return null;
1200
1825
  }
1201
- if (statusCode >= 500) {
1202
- return "red";
1826
+ return {
1827
+ anchor: editorCursorFromSource(selection.anchor),
1828
+ active: editorCursorFromSource(selection.active)
1829
+ };
1830
+ }
1831
+ function sourceSelectionFromEditor(selection) {
1832
+ if (!selection) {
1833
+ return null;
1203
1834
  }
1204
- return "cyan";
1835
+ return {
1836
+ anchor: sourceCursorFromEditor(selection.anchor),
1837
+ active: sourceCursorFromEditor(selection.active)
1838
+ };
1839
+ }
1840
+ function createRegionAwareTokenizer(activeRegion) {
1841
+ return (line, context) => {
1842
+ const gutter = line[0] ?? INACTIVE_GUTTER;
1843
+ const body = line.slice(EDITOR_GUTTER_WIDTH);
1844
+ const highlighted = activeRegion !== null && context.lineNumber >= activeRegion.startLine && context.lineNumber <= activeRegion.endLine;
1845
+ const tokens = [
1846
+ {
1847
+ text: gutter,
1848
+ kind: gutterTokenKind(activeRegion?.method, highlighted && gutter !== INACTIVE_GUTTER)
1849
+ }
1850
+ ];
1851
+ if (body.length > 0) {
1852
+ tokens.push(...tokenizeHttpLine(body, context));
1853
+ }
1854
+ return tokens;
1855
+ };
1205
1856
  }
1206
1857
 
1207
1858
  // src/ui/app-view.ts
1208
- function paneStyle(focused) {
1209
- return focused ? { fg: rgb(180, 220, 255), bold: true } : { fg: rgb(120, 120, 120) };
1859
+ function paneStyle(colors, focused) {
1860
+ return {
1861
+ bg: colors.bgElevated,
1862
+ ...focused ? { fg: colors.paneFocused, bold: true } : { fg: colors.paneMuted }
1863
+ };
1210
1864
  }
1211
- function renderFileTree(state, deps) {
1865
+ function renderFileTree(state, deps, colors) {
1212
1866
  return ui.panel(
1213
1867
  {
1214
1868
  id: "pane-files",
1215
1869
  title: state.ui.focusPane === "files" ? "\u25CF Files" : "Files",
1216
- style: paneStyle(state.ui.focusPane === "files")
1870
+ style: paneStyle(colors, state.ui.focusPane === "files")
1217
1871
  },
1218
1872
  [
1219
1873
  ui.tree({
@@ -1228,7 +1882,7 @@ function renderFileTree(state, deps) {
1228
1882
  onPress: (node) => deps.onTreePress(node),
1229
1883
  renderNode: (node, _depth, nodeState) => ui.row({ gap: 1 }, [
1230
1884
  ui.text(nodeState.selected ? `\u25B8 ${node.name}` : node.name, {
1231
- style: nodeState.selected ? { fg: rgb(255, 220, 120), bold: true } : state.dirty && node.path === state.selectedFilePath ? { fg: rgb(255, 180, 80) } : void 0
1885
+ style: nodeState.selected ? { fg: colors.selected, bold: true } : state.dirty && node.path === state.selectedFilePath ? { fg: colors.dirty } : void 0
1232
1886
  })
1233
1887
  ]),
1234
1888
  flex: 1,
@@ -1237,38 +1891,32 @@ function renderFileTree(state, deps) {
1237
1891
  ]
1238
1892
  );
1239
1893
  }
1240
- function renderEditor(state, deps, readOnly) {
1241
- const activeRegion = state.activeRegion ?? (state.parsedFile ? resolveRegionAtLine(state.parsedFile.regions, state.editor.cursor.line) : null);
1894
+ function renderEditor(state, deps, readOnly, colors) {
1895
+ const activeRegion = resolveActiveRegion(state.parsedFile, state.editor.cursor.line);
1242
1896
  const titleParts = [
1243
1897
  state.ui.focusPane === "editor" ? "\u25CF Editor" : "Editor",
1244
1898
  state.selectedFilePath ? state.selectedFilePath.split("/").pop() : "No file",
1245
1899
  state.dirty ? " \u25CF" : "",
1246
1900
  activeRegion ? ` | ${activeRegion.method ?? "?"} ${activeRegion.name}` : ""
1247
1901
  ];
1248
- const diagnostics = state.parsedFile ? buildRegionDiagnostics(
1249
- state.parsedFile.regions,
1250
- activeRegion?.id ?? null,
1251
- state.fileLines
1252
- ) : [];
1253
1902
  return ui.panel(
1254
1903
  {
1255
1904
  id: "pane-editor",
1256
1905
  title: titleParts.join(""),
1257
- style: paneStyle(state.ui.focusPane === "editor")
1906
+ style: paneStyle(colors, state.ui.focusPane === "editor")
1258
1907
  },
1259
1908
  [
1260
1909
  ui.codeEditor({
1261
1910
  id: "editor",
1262
- lines: state.fileLines,
1263
- cursor: state.editor.cursor,
1264
- selection: state.editor.selection,
1911
+ lines: prefixEditorLines(state.fileLines, activeRegion),
1912
+ cursor: editorCursorFromSource(state.editor.cursor),
1913
+ selection: editorSelectionFromSource(state.editor.selection),
1265
1914
  scrollTop: state.editor.scrollTop,
1266
1915
  scrollLeft: state.editor.scrollLeft,
1267
1916
  readOnly,
1268
1917
  lineNumbers: true,
1269
1918
  syntaxLanguage: "plain",
1270
- tokenizeLine: tokenizeHttpLine,
1271
- diagnostics,
1919
+ tokenizeLine: createRegionAwareTokenizer(activeRegion),
1272
1920
  onChange: (lines, cursor) => deps.onEditorChange(lines, cursor),
1273
1921
  onSelectionChange: deps.onEditorSelection,
1274
1922
  onScroll: deps.onEditorScroll,
@@ -1295,7 +1943,7 @@ function responseCursorProps(state, deps) {
1295
1943
  onSelectionChange: deps.onResponseSelection
1296
1944
  };
1297
1945
  }
1298
- function renderResponseBody(state, deps) {
1946
+ function renderResponseBody(state, deps, colors) {
1299
1947
  const result = state.request.result;
1300
1948
  const scroll = responseScrollProps(state, deps);
1301
1949
  const cursor = responseCursorProps(state, deps);
@@ -1338,7 +1986,7 @@ function renderResponseBody(state, deps) {
1338
1986
  case "variables":
1339
1987
  return ui.codeEditor({
1340
1988
  id: `response-vars-${gen}`,
1341
- lines: [JSON.stringify(state.request.variables, null, 2)],
1989
+ lines: JSON.stringify(state.request.variables, null, 2).split("\n"),
1342
1990
  readOnly: true,
1343
1991
  syntaxLanguage: "json",
1344
1992
  ...cursor,
@@ -1350,16 +1998,20 @@ function renderResponseBody(state, deps) {
1350
1998
  ...result.testResults.map(
1351
1999
  (test) => ui.text(`${test.status === "SUCCESS" ? "\u2713" : "\u2717"} ${test.message}`, {
1352
2000
  style: {
1353
- fg: test.status === "SUCCESS" ? rgb(120, 220, 120) : test.status === "SKIPPED" ? rgb(220, 220, 120) : rgb(220, 120, 120)
2001
+ fg: test.status === "SUCCESS" ? colors.success : test.status === "SKIPPED" ? colors.warning : colors.error
1354
2002
  }
1355
2003
  })
1356
2004
  )
1357
2005
  ]);
1358
2006
  case "pretty":
1359
- default:
2007
+ default: {
2008
+ const prettyView = buildFoldableJsonView(
2009
+ prettyJsonIfPossible(result.prettyBody || result.body),
2010
+ state.responseEditor.foldedJsonPaths
2011
+ );
1360
2012
  return ui.codeEditor({
1361
2013
  id: `response-pretty-${gen}`,
1362
- lines: prettyJsonIfPossible(result.prettyBody || result.body).split("\n"),
2014
+ lines: prettyView.lines,
1363
2015
  readOnly: true,
1364
2016
  lineNumbers: true,
1365
2017
  syntaxLanguage: "json",
@@ -1368,9 +2020,10 @@ function renderResponseBody(state, deps) {
1368
2020
  ...scroll,
1369
2021
  flex: 1
1370
2022
  });
2023
+ }
1371
2024
  }
1372
2025
  }
1373
- function renderResponse(state, deps) {
2026
+ function renderResponse(state, deps, colors) {
1374
2027
  const result = state.request.result;
1375
2028
  const statusLine = state.request.sending || !result ? null : `${result.protocol ?? "HTTP"} ${result.statusCode ?? "?"} ${result.statusMessage ?? ""} \xB7 ${result.durationMs ?? "?"} ms`;
1376
2029
  const tabItems = [
@@ -1384,12 +2037,15 @@ function renderResponse(state, deps) {
1384
2037
  {
1385
2038
  id: "pane-response",
1386
2039
  title: state.ui.focusPane === "response" ? "\u25CF Response" : "Response",
1387
- style: paneStyle(state.ui.focusPane === "response")
2040
+ style: paneStyle(colors, state.ui.focusPane === "response")
1388
2041
  },
1389
2042
  [
1390
2043
  ui.row({ gap: 2 }, [
1391
2044
  state.request.sending ? ui.spinner({ label: "Sending request\u2026" }) : ui.text(statusLine ?? "Ready", {
1392
- style: { fg: rgb(...statusColor(result?.statusCode)), bold: true }
2045
+ style: {
2046
+ fg: statusColorForTone(colors, statusTone(result?.statusCode)),
2047
+ bold: true
2048
+ }
1393
2049
  }),
1394
2050
  result?.error && !state.request.sending ? ui.badge(result.error, { variant: "error" }) : null
1395
2051
  ]),
@@ -1401,43 +2057,42 @@ function renderResponse(state, deps) {
1401
2057
  disabled: state.request.sending,
1402
2058
  onPress: () => deps.onResponseTab(tab.key)
1403
2059
  })
1404
- )
2060
+ ),
2061
+ state.ui.responseTab === "pretty" && result ? ui.button({
2062
+ id: "response-json-fold",
2063
+ label: "Fold/Unfold",
2064
+ disabled: state.request.sending,
2065
+ onPress: deps.onResponseJsonFoldToggle
2066
+ }) : null,
2067
+ state.ui.responseTab === "pretty" && state.responseEditor.foldedJsonPaths.length > 0 ? ui.button({
2068
+ id: "response-json-unfold-all",
2069
+ label: "Unfold all",
2070
+ disabled: state.request.sending,
2071
+ onPress: deps.onResponseJsonUnfoldAll
2072
+ }) : null
1405
2073
  ]),
1406
- renderResponseBody(state, deps)
2074
+ renderResponseBody(state, deps, colors)
1407
2075
  ]
1408
2076
  );
1409
2077
  }
1410
- function statusColor(code) {
1411
- const tone = statusTone(code);
1412
- switch (tone) {
1413
- case "green":
1414
- return [120, 220, 140];
1415
- case "yellow":
1416
- return [240, 200, 100];
1417
- case "red":
1418
- return [240, 120, 120];
1419
- default:
1420
- return [140, 200, 240];
1421
- }
1422
- }
1423
- function renderMainLayout(state, deps) {
2078
+ function renderMainLayout(state, deps, colors) {
1424
2079
  const layoutMode = resolveLayoutMode(state.ui.viewportWidth);
1425
2080
  const zoom = state.ui.zoomPane;
1426
2081
  if (zoom) {
1427
- if (zoom === "files") return renderFileTree(state, deps);
1428
- if (zoom === "editor") return renderEditor(state, deps, false);
1429
- return renderResponse(state, deps);
2082
+ if (zoom === "files") return renderFileTree(state, deps, colors);
2083
+ if (zoom === "editor") return renderEditor(state, deps, false, colors);
2084
+ return renderResponse(state, deps, colors);
1430
2085
  }
1431
2086
  if (layoutMode === "stacked") {
1432
- const pane = state.ui.focusPane === "files" ? renderFileTree(state, deps) : state.ui.focusPane === "response" ? renderResponse(state, deps) : renderEditor(state, deps, false);
2087
+ const pane = state.ui.focusPane === "files" ? renderFileTree(state, deps, colors) : state.ui.focusPane === "response" ? renderResponse(state, deps, colors) : renderEditor(state, deps, false, colors);
1433
2088
  return pane;
1434
2089
  }
1435
2090
  if (layoutMode === "sidebar-overlay") {
1436
2091
  return ui.row({ gap: 1, flex: 1 }, [
1437
- state.ui.sidebarVisible ? ui.box({ width: 28, flex: 0 }, [renderFileTree(state, deps)]) : null,
2092
+ state.ui.sidebarVisible ? ui.box({ width: 28, flex: 0 }, [renderFileTree(state, deps, colors)]) : null,
1438
2093
  ui.column({ gap: 1, flex: 1 }, [
1439
- renderEditor(state, deps, false),
1440
- renderResponse(state, deps)
2094
+ renderEditor(state, deps, false, colors),
2095
+ renderResponse(state, deps, colors)
1441
2096
  ])
1442
2097
  ]);
1443
2098
  }
@@ -1451,38 +2106,53 @@ function renderMainLayout(state, deps) {
1451
2106
  onChange: deps.onSplitChange
1452
2107
  },
1453
2108
  [
1454
- renderFileTree(state, deps),
1455
- renderEditor(state, deps, false),
1456
- renderResponse(state, deps)
2109
+ renderFileTree(state, deps, colors),
2110
+ renderEditor(state, deps, false, colors),
2111
+ renderResponse(state, deps, colors)
1457
2112
  ]
1458
2113
  )
1459
2114
  ]);
1460
2115
  }
1461
- function renderFooter(state) {
2116
+ function renderFooter(state, deps, colors) {
1462
2117
  const env = state.request.activeEnvironment.length > 0 ? state.request.activeEnvironment.join(",") : "none";
1463
- const dirName = path3.basename(state.workspaceRoot);
2118
+ const dirName = path6.basename(state.workspaceRoot);
1464
2119
  const branch = state.ui.gitBranch ?? "\u2014";
1465
- const fileName = state.selectedFilePath ? path3.basename(state.selectedFilePath) : null;
1466
- const hints = footerHints({
2120
+ const fileName = state.selectedFilePath ? path6.basename(state.selectedFilePath) : null;
2121
+ const hints = footerHintItems({
1467
2122
  focusPane: state.ui.focusPane,
1468
2123
  overlay: state.ui.overlay,
1469
2124
  viewportWidth: state.ui.viewportWidth,
1470
- sending: state.request.sending
2125
+ sending: state.request.sending,
2126
+ bindings: state.settings.keybindings,
2127
+ responseTab: state.ui.responseTab,
2128
+ hasResponse: Boolean(state.request.result),
2129
+ hasFoldedJson: state.responseEditor.foldedJsonPaths.length > 0
1471
2130
  });
1472
2131
  return ui.statusBar({
1473
2132
  id: "status-bar",
2133
+ style: { bg: colors.bgSubtle, fg: colors.fgPrimary },
1474
2134
  left: [
1475
- ui.text(dirName, { style: { fg: rgb(180, 220, 255), bold: true } }),
1476
- ui.text(` \u2387 ${branch}`, { style: { fg: rgb(160, 220, 160) } }),
2135
+ ui.text(dirName, { style: { fg: colors.paneFocused, bold: true } }),
2136
+ ui.text(` \u2387 ${branch}`, { style: { fg: colors.success } }),
1477
2137
  fileName ? ui.text(` | ${fileName}`) : null,
1478
- state.dirty ? ui.text(" \u25CF", { style: { fg: rgb(255, 180, 80) } }) : null,
1479
- ui.text(` | env: ${env}`, { style: { fg: rgb(160, 200, 255) } }),
2138
+ state.dirty ? ui.text(" \u25CF", { style: { fg: colors.dirty } }) : null,
2139
+ ui.button({
2140
+ id: "status-env.switcher",
2141
+ label: ` ^E env: ${env}`,
2142
+ onPress: () => deps.onCommand("env.switcher")
2143
+ }),
1480
2144
  state.ui.statusMessage ? ui.text(` | ${state.ui.statusMessage}`) : null
1481
2145
  ].filter(Boolean),
1482
- right: [ui.text(hints)]
2146
+ right: hints.map(
2147
+ (hint) => ui.button({
2148
+ id: `status-${hint.command}`,
2149
+ label: hint.label,
2150
+ onPress: () => deps.onCommand(hint.command)
2151
+ })
2152
+ )
1483
2153
  });
1484
2154
  }
1485
- function renderOverlayContent(state, deps) {
2155
+ function renderOverlayContent(state, deps, colors) {
1486
2156
  switch (state.ui.overlay) {
1487
2157
  case "env":
1488
2158
  return ui.modal({
@@ -1490,12 +2160,18 @@ function renderOverlayContent(state, deps) {
1490
2160
  title: "Environment",
1491
2161
  content: ui.column({ gap: 1 }, [
1492
2162
  ui.text("Select environment (Enter to apply, Esc to close)"),
1493
- ui.text("(none)", {
1494
- style: state.ui.envSelectedIndex === 0 ? { fg: rgb(255, 220, 120), bold: true } : void 0
2163
+ ui.button({
2164
+ id: "env-option-none",
2165
+ label: "(none)",
2166
+ onPress: () => deps.onEnvSelect(0),
2167
+ style: state.ui.envSelectedIndex === 0 ? { fg: colors.selected, bold: true } : void 0
1495
2168
  }),
1496
2169
  ...state.request.environments.map(
1497
- (env, index) => ui.text(env, {
1498
- style: index + 1 === state.ui.envSelectedIndex ? { fg: rgb(255, 220, 120), bold: true } : void 0
2170
+ (env, index) => ui.button({
2171
+ id: `env-option-${index}`,
2172
+ label: env,
2173
+ onPress: () => deps.onEnvSelect(index + 1),
2174
+ style: index + 1 === state.ui.envSelectedIndex ? { fg: colors.selected, bold: true } : void 0
1499
2175
  })
1500
2176
  )
1501
2177
  ]),
@@ -1561,9 +2237,10 @@ function renderOverlayContent(state, deps) {
1561
2237
  }
1562
2238
  }
1563
2239
  function renderApp(state, deps) {
1564
- const base = ui.column({ gap: 1, flex: 1 }, [
1565
- renderMainLayout(state, deps),
1566
- renderFooter(state)
2240
+ const colors = colorsForMode(state.settings.themeMode);
2241
+ const base = ui.column({ gap: 1, flex: 1, style: { bg: colors.bgBase } }, [
2242
+ renderMainLayout(state, deps, colors),
2243
+ renderFooter(state, deps, colors)
1567
2244
  ]);
1568
2245
  if (state.ui.overlay === "none") {
1569
2246
  return base;
@@ -1576,7 +2253,7 @@ function renderApp(state, deps) {
1576
2253
  backdrop: "dim",
1577
2254
  closeOnEscape: true,
1578
2255
  onClose: deps.onOverlayClose,
1579
- content: renderOverlayContent(state, deps)
2256
+ content: renderOverlayContent(state, deps, colors)
1580
2257
  })
1581
2258
  ]);
1582
2259
  }
@@ -1591,6 +2268,35 @@ function focusPaneId(pane) {
1591
2268
  }
1592
2269
  }
1593
2270
 
2271
+ // src/ui/scroll.ts
2272
+ import { routeWheel } from "@rezi-ui/core";
2273
+ function lineNumberWidth(lines, lineNumbers) {
2274
+ if (!lineNumbers) {
2275
+ return 0;
2276
+ }
2277
+ return String(Math.max(1, lines.length)).length + 1;
2278
+ }
2279
+ function maxLineWidth(lines) {
2280
+ return lines.reduce((max, line) => Math.max(max, line.length), 0);
2281
+ }
2282
+ function resolveWheelScroll(event, state, lines, viewport) {
2283
+ const routed = routeWheel(event, {
2284
+ scrollX: state.scrollLeft,
2285
+ scrollY: state.scrollTop,
2286
+ contentWidth: maxLineWidth(lines),
2287
+ contentHeight: Math.max(1, lines.length),
2288
+ viewportWidth: Math.max(0, viewport.width - lineNumberWidth(lines, viewport.lineNumbers ?? true)),
2289
+ viewportHeight: Math.max(0, viewport.height)
2290
+ });
2291
+ if (routed.nextScrollX === void 0 && routed.nextScrollY === void 0) {
2292
+ return null;
2293
+ }
2294
+ return {
2295
+ scrollTop: routed.nextScrollY ?? state.scrollTop,
2296
+ scrollLeft: routed.nextScrollX ?? state.scrollLeft
2297
+ };
2298
+ }
2299
+
1594
2300
  // src/utils/clipboard.ts
1595
2301
  import clipboard from "clipboardy";
1596
2302
  async function copyToClipboard(text) {
@@ -1604,12 +2310,11 @@ async function copyToClipboard(text) {
1604
2310
  return false;
1605
2311
  }
1606
2312
  }
1607
- function disableFlowControl() {
1608
- if (process.stdin.isTTY) {
1609
- try {
1610
- process.stdin.setRawMode?.(true);
1611
- } catch {
1612
- }
2313
+ async function readFromClipboard() {
2314
+ try {
2315
+ return await clipboard.read();
2316
+ } catch {
2317
+ return null;
1613
2318
  }
1614
2319
  }
1615
2320
 
@@ -1631,17 +2336,229 @@ async function getGitBranch(root) {
1631
2336
  }
1632
2337
  }
1633
2338
 
2339
+ // src/utils/terminal-theme.ts
2340
+ import process2 from "process";
2341
+ import { Readable } from "stream";
2342
+ var OSC_11_BACKGROUND_QUERY = "\x1B]11;?\x07";
2343
+ function parseHexComponent(value) {
2344
+ const parsed = Number.parseInt(value, 16);
2345
+ if (Number.isNaN(parsed)) {
2346
+ return null;
2347
+ }
2348
+ if (value.length <= 2) {
2349
+ return parsed;
2350
+ }
2351
+ return Math.round(parsed * 255 / 65535);
2352
+ }
2353
+ function parseOsc11BackgroundColor(sequence) {
2354
+ const rgbMatch = /\x1b\]11;rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})(?:\x07|\x1b\\)/iu.exec(
2355
+ sequence
2356
+ );
2357
+ if (rgbMatch) {
2358
+ const red = parseHexComponent(rgbMatch[1] ?? "");
2359
+ const green = parseHexComponent(rgbMatch[2] ?? "");
2360
+ const blue = parseHexComponent(rgbMatch[3] ?? "");
2361
+ if (red === null || green === null || blue === null) {
2362
+ return null;
2363
+ }
2364
+ return { red, green, blue };
2365
+ }
2366
+ const hexMatch = /\x1b\]11;#([0-9a-f]{6})(?:\x07|\x1b\\)/iu.exec(sequence);
2367
+ if (hexMatch) {
2368
+ const hex = hexMatch[1] ?? "";
2369
+ const red = parseHexComponent(hex.slice(0, 2));
2370
+ const green = parseHexComponent(hex.slice(2, 4));
2371
+ const blue = parseHexComponent(hex.slice(4, 6));
2372
+ if (red === null || green === null || blue === null) {
2373
+ return null;
2374
+ }
2375
+ return { red, green, blue };
2376
+ }
2377
+ return null;
2378
+ }
2379
+ function themeModeForBackgroundColor({ red, green, blue }) {
2380
+ const linear = [red, green, blue].map((component) => {
2381
+ const normalized = component / 255;
2382
+ return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
2383
+ });
2384
+ const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
2385
+ return luminance > 0.5 ? "light" : "dark";
2386
+ }
2387
+ function themeModeFromColorFgbg() {
2388
+ const colorFgbg = process2.env.COLORFGBG;
2389
+ if (!colorFgbg) {
2390
+ return null;
2391
+ }
2392
+ const parts = colorFgbg.split(";");
2393
+ const background = Number.parseInt(parts[parts.length - 1] ?? "", 10);
2394
+ if (Number.isNaN(background)) {
2395
+ return null;
2396
+ }
2397
+ return background > 7 ? "light" : "dark";
2398
+ }
2399
+ async function detectTerminalThemeMode(options = {}) {
2400
+ const input = options.input ?? process2.stdin;
2401
+ const output = options.output ?? process2.stdout;
2402
+ const timeoutMs = options.timeoutMs ?? 150;
2403
+ if (!("isTTY" in input && input.isTTY) || !("isTTY" in output && output.isTTY)) {
2404
+ return null;
2405
+ }
2406
+ const wasRaw = "isRaw" in input ? input.isRaw : void 0;
2407
+ let settled = false;
2408
+ let buffer = "";
2409
+ return await new Promise((resolve) => {
2410
+ const drainInput = () => {
2411
+ if (!(input instanceof Readable)) {
2412
+ return;
2413
+ }
2414
+ while (input.readableLength > 0) {
2415
+ input.read();
2416
+ }
2417
+ input.pause();
2418
+ };
2419
+ const cleanup = () => {
2420
+ if (settled) {
2421
+ return;
2422
+ }
2423
+ settled = true;
2424
+ clearTimeout(timer);
2425
+ input.removeListener("data", onData);
2426
+ if (wasRaw !== void 0 && "setRawMode" in input) {
2427
+ input.setRawMode?.(wasRaw);
2428
+ }
2429
+ drainInput();
2430
+ };
2431
+ const finish = (mode) => {
2432
+ cleanup();
2433
+ resolve(mode);
2434
+ };
2435
+ const timer = setTimeout(() => finish(null), timeoutMs);
2436
+ const onData = (chunk) => {
2437
+ buffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
2438
+ const color = parseOsc11BackgroundColor(buffer);
2439
+ if (color) {
2440
+ finish(themeModeForBackgroundColor(color));
2441
+ }
2442
+ };
2443
+ input.setRawMode?.(true);
2444
+ if (input instanceof Readable) {
2445
+ input.resume();
2446
+ }
2447
+ input.on("data", onData);
2448
+ output.write(OSC_11_BACKGROUND_QUERY);
2449
+ });
2450
+ }
2451
+ async function resolveThemeMode(preference, options = {}) {
2452
+ const { allowProbe = true, fallbackMode = "dark" } = options;
2453
+ if (preference === "light") {
2454
+ return "light";
2455
+ }
2456
+ if (preference === "dark") {
2457
+ return "dark";
2458
+ }
2459
+ if (allowProbe) {
2460
+ const detected = await detectTerminalThemeMode();
2461
+ if (detected) {
2462
+ return detected;
2463
+ }
2464
+ }
2465
+ return themeModeFromColorFgbg() ?? fallbackMode;
2466
+ }
2467
+
1634
2468
  // src/cli.ts
2469
+ function isPointInRect(x, y, rect) {
2470
+ return x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h;
2471
+ }
2472
+ function responseEditorId(state) {
2473
+ switch (state.ui.responseTab) {
2474
+ case "pretty":
2475
+ return `response-pretty-${state.resultGeneration}`;
2476
+ case "raw":
2477
+ return `response-raw-${state.resultGeneration}`;
2478
+ case "variables":
2479
+ return `response-vars-${state.resultGeneration}`;
2480
+ case "headers":
2481
+ case "tests":
2482
+ return null;
2483
+ }
2484
+ }
2485
+ function responseEditorLines(state) {
2486
+ const result = state.request.result;
2487
+ if (!result) {
2488
+ return null;
2489
+ }
2490
+ switch (state.ui.responseTab) {
2491
+ case "pretty":
2492
+ return buildFoldableJsonView(
2493
+ prettyJsonIfPossible(result.prettyBody || result.body),
2494
+ state.responseEditor.foldedJsonPaths
2495
+ ).lines;
2496
+ case "raw":
2497
+ return result.body ? result.body.split("\n") : [""];
2498
+ case "variables":
2499
+ return JSON.stringify(state.request.variables, null, 2).split("\n");
2500
+ case "headers":
2501
+ case "tests":
2502
+ return null;
2503
+ }
2504
+ }
2505
+ function handleWheelEvent(event, app) {
2506
+ if (event.kind !== "mouse" || event.mouseKind !== 5) {
2507
+ return false;
2508
+ }
2509
+ let handled = false;
2510
+ app.update((prev) => {
2511
+ const editorRect = app.measureElement("editor");
2512
+ if (editorRect && isPointInRect(event.x, event.y, editorRect)) {
2513
+ handled = true;
2514
+ const next = resolveWheelScroll(
2515
+ event,
2516
+ prev.editor,
2517
+ prefixEditorLines(prev.fileLines, null),
2518
+ { width: editorRect.w, height: editorRect.h }
2519
+ );
2520
+ return {
2521
+ ...prev,
2522
+ ui: { ...prev.ui, focusPane: "editor" },
2523
+ editor: next ? { ...prev.editor, scrollTop: next.scrollTop, scrollLeft: next.scrollLeft } : prev.editor
2524
+ };
2525
+ }
2526
+ const id = responseEditorId(prev);
2527
+ const responseRect = id ? app.measureElement(id) : null;
2528
+ if (responseRect && isPointInRect(event.x, event.y, responseRect)) {
2529
+ handled = true;
2530
+ const lines = responseEditorLines(prev);
2531
+ const next = lines ? resolveWheelScroll(event, prev.responseEditor, lines, {
2532
+ width: responseRect.w,
2533
+ height: responseRect.h
2534
+ }) : null;
2535
+ return {
2536
+ ...prev,
2537
+ ui: { ...prev.ui, focusPane: "response" },
2538
+ responseEditor: next ? { ...prev.responseEditor, scrollTop: next.scrollTop, scrollLeft: next.scrollLeft } : prev.responseEditor
2539
+ };
2540
+ }
2541
+ return prev;
2542
+ });
2543
+ return handled;
2544
+ }
1635
2545
  async function main() {
1636
2546
  initEngineProviders();
1637
- const workspaceRoot = path4.resolve(process2.argv[2] ?? process2.cwd());
2547
+ const workspaceRoot = path7.resolve(process3.argv[2] ?? process3.cwd());
1638
2548
  const workspace = new Workspace(workspaceRoot);
1639
2549
  const tree = await workspace.open();
2550
+ const config = loadConfig(workspaceRoot);
2551
+ const initialThemeMode = await resolveThemeMode(config.theme);
1640
2552
  let currentState = createInitialState(workspaceRoot);
1641
2553
  currentState = {
1642
2554
  ...currentState,
1643
2555
  fileTree: tree,
1644
2556
  expandedPaths: tree.filter((n) => n.kind === "directory").map((n) => n.path),
2557
+ settings: {
2558
+ ...currentState.settings,
2559
+ theme: config.theme,
2560
+ themeMode: initialThemeMode
2561
+ },
1645
2562
  ui: {
1646
2563
  ...currentState.ui,
1647
2564
  gitBranch: await getGitBranch(workspaceRoot)
@@ -1662,19 +2579,24 @@ async function main() {
1662
2579
  }
1663
2580
  const loaded = loadKeybindings(workspaceRoot);
1664
2581
  app.keys({
1665
- ...buildBindingMap(loaded.bindings, (command) => {
2582
+ ...buildBindingMap(loaded.bindings, (command, ctx) => {
2583
+ currentState = ctx.state;
1666
2584
  if (command === "response.copy") {
1667
2585
  void handleCopy(app, currentState);
1668
2586
  }
1669
2587
  bus.execute(command);
1670
2588
  }),
1671
2589
  enter: {
1672
- handler: () => bus.execute("env.apply"),
2590
+ handler: (ctx) => {
2591
+ currentState = ctx.state;
2592
+ bus.execute("env.apply");
2593
+ },
1673
2594
  when: (ctx) => ctx.state.ui.overlay === "env",
1674
2595
  description: "Apply selected environment"
1675
2596
  },
1676
2597
  up: {
1677
- handler: () => {
2598
+ handler: (ctx) => {
2599
+ currentState = ctx.state;
1678
2600
  if (currentState.ui.overlay === "env") {
1679
2601
  bus.execute("env.selectPrev");
1680
2602
  }
@@ -1682,7 +2604,8 @@ async function main() {
1682
2604
  when: (ctx) => ctx.state.ui.overlay === "env" || ctx.state.ui.overlay === "commandPalette"
1683
2605
  },
1684
2606
  down: {
1685
- handler: () => {
2607
+ handler: (ctx) => {
2608
+ currentState = ctx.state;
1686
2609
  if (currentState.ui.overlay === "env") {
1687
2610
  bus.execute("env.selectNext");
1688
2611
  }
@@ -1693,11 +2616,33 @@ async function main() {
1693
2616
  app.update((state) => ({
1694
2617
  ...state,
1695
2618
  settings: {
2619
+ ...state.settings,
1696
2620
  keymapPreset: loaded.preset,
1697
2621
  keybindings: loaded.bindings
1698
2622
  }
1699
2623
  }));
1700
2624
  };
2625
+ const applyTheme = async (preference) => {
2626
+ if (!app) {
2627
+ return;
2628
+ }
2629
+ const themeMode = await resolveThemeMode(preference, {
2630
+ allowProbe: false,
2631
+ fallbackMode: currentState.settings.themeMode
2632
+ });
2633
+ app.setTheme(themeForMode(themeMode));
2634
+ app.update((state) => ({
2635
+ ...state,
2636
+ settings: {
2637
+ ...state.settings,
2638
+ theme: preference,
2639
+ themeMode
2640
+ }
2641
+ }));
2642
+ };
2643
+ const reloadConfig = () => {
2644
+ void applyTheme(loadConfig(workspaceRoot).theme);
2645
+ };
1701
2646
  bus = createCommandContext({
1702
2647
  workspace,
1703
2648
  getState: () => currentState,
@@ -1708,16 +2653,27 @@ async function main() {
1708
2653
  });
1709
2654
  },
1710
2655
  quit: () => {
1711
- void workspace.close().finally(() => {
1712
- app?.stop().finally(() => process2.exit(0));
2656
+ void workspace.close().finally(async () => {
2657
+ await app?.stop();
2658
+ await app?.dispose();
2659
+ process3.exit(0);
1713
2660
  });
1714
2661
  },
1715
2662
  reloadKeybindings
1716
2663
  });
1717
- app = createNodeApp({ initialState: currentState });
2664
+ app = createNodeApp({ initialState: currentState, theme: themeForMode(initialThemeMode) });
1718
2665
  reloadKeybindings();
1719
- disableFlowControl();
1720
- const stopWatch = watchKeybindings(workspaceRoot, reloadKeybindings);
2666
+ const executeCommand = (command) => {
2667
+ if (!bus || !app) {
2668
+ return;
2669
+ }
2670
+ if (command === "response.copy") {
2671
+ void handleCopy(app, currentState);
2672
+ }
2673
+ bus.execute(command);
2674
+ };
2675
+ const stopKeybindingWatch = watchKeybindings(workspaceRoot, reloadKeybindings);
2676
+ const stopConfigWatch = watchConfig(workspaceRoot, reloadConfig);
1721
2677
  workspace.on("change", () => {
1722
2678
  void bus.refreshWorkspace();
1723
2679
  void refreshGitBranch();
@@ -1726,29 +2682,27 @@ async function main() {
1726
2682
  (state) => renderApp(state, {
1727
2683
  onEditorChange: (lines, cursor) => {
1728
2684
  app?.update((prev) => {
1729
- const activeRegion = prev.parsedFile ? resolveRegionAtLine(prev.parsedFile.regions, cursor.line) : null;
2685
+ const fileLines = stripEditorLines(lines);
2686
+ const sourceCursor = sourceCursorFromEditor(cursor);
1730
2687
  return {
1731
2688
  ...prev,
1732
- fileLines: [...lines],
1733
- dirty: contentFromLines(lines) !== prev.fileContent,
1734
- activeRegion,
2689
+ fileLines,
2690
+ dirty: contentFromLines(fileLines) !== prev.fileContent,
1735
2691
  ui: { ...prev.ui, focusPane: "editor" },
1736
- editor: { ...prev.editor, cursor }
2692
+ editor: { ...prev.editor, cursor: sourceCursor }
1737
2693
  };
1738
2694
  });
1739
2695
  },
1740
2696
  onEditorSelection: (selection) => {
1741
2697
  app?.update((prev) => {
1742
- const cursorLine = selection?.active.line ?? prev.editor.cursor.line;
1743
- const activeRegion = prev.parsedFile ? resolveRegionAtLine(prev.parsedFile.regions, cursorLine) : null;
2698
+ const sourceSelection = sourceSelectionFromEditor(selection);
1744
2699
  return {
1745
2700
  ...prev,
1746
- activeRegion,
1747
2701
  ui: { ...prev.ui, focusPane: "editor" },
1748
2702
  editor: {
1749
2703
  ...prev.editor,
1750
- selection,
1751
- cursor: selection?.active ?? prev.editor.cursor
2704
+ selection: sourceSelection,
2705
+ cursor: sourceSelection?.active ?? prev.editor.cursor
1752
2706
  }
1753
2707
  };
1754
2708
  });
@@ -1812,6 +2766,12 @@ async function main() {
1812
2766
  responseEditor: { ...prev.responseEditor, cursor }
1813
2767
  }));
1814
2768
  },
2769
+ onResponseJsonFoldToggle: () => {
2770
+ bus?.execute("response.jsonFoldToggle");
2771
+ },
2772
+ onResponseJsonUnfoldAll: () => {
2773
+ bus?.execute("response.jsonUnfoldAll");
2774
+ },
1815
2775
  onSplitChange: (sizes) => {
1816
2776
  if (sizes.length === 3) {
1817
2777
  app?.update((prev) => ({
@@ -1849,26 +2809,34 @@ async function main() {
1849
2809
  bus?.execute("overlay.close");
1850
2810
  },
1851
2811
  onEnvSelect: (index) => {
1852
- app?.update((prev) => ({
1853
- ...prev,
1854
- ui: { ...prev.ui, envSelectedIndex: index }
1855
- }));
2812
+ app?.update((prev) => {
2813
+ currentState = {
2814
+ ...prev,
2815
+ ui: { ...prev.ui, envSelectedIndex: index }
2816
+ };
2817
+ return currentState;
2818
+ });
2819
+ bus?.execute("env.apply");
1856
2820
  },
1857
2821
  onResponseSearch: (query) => {
1858
2822
  app?.update((prev) => ({
1859
2823
  ...prev,
1860
2824
  editor: { ...prev.editor, searchQuery: query }
1861
2825
  }));
1862
- }
2826
+ },
2827
+ onCommand: executeCommand
1863
2828
  })
1864
2829
  );
1865
- app.onEvent((event) => handleUiEvent(event, app));
2830
+ app.onEvent((event) => {
2831
+ void handleUiEvent(event, app);
2832
+ });
1866
2833
  const files = flattenFiles(tree).filter((node) => node.kind === "file");
1867
2834
  if (files[0]) {
1868
2835
  await bus.openFile(files[0].path);
1869
2836
  }
1870
2837
  await app.run();
1871
- stopWatch();
2838
+ stopKeybindingWatch();
2839
+ stopConfigWatch();
1872
2840
  }
1873
2841
  async function handleCopy(app, state) {
1874
2842
  const result = state.request.result;
@@ -1887,7 +2855,38 @@ async function handleCopy(app, state) {
1887
2855
  ui: { ...prev.ui, statusMessage: ok ? "Copied" : "Copy failed" }
1888
2856
  }));
1889
2857
  }
1890
- function handleUiEvent(event, app) {
2858
+ async function handlePaste(app) {
2859
+ const text = await readFromClipboard();
2860
+ app.update((prev) => {
2861
+ if (prev.ui.overlay !== "none" || prev.ui.focusPane !== "editor") {
2862
+ return prev;
2863
+ }
2864
+ if (!text) {
2865
+ return {
2866
+ ...prev,
2867
+ ui: { ...prev.ui, statusMessage: text === "" ? "Clipboard empty" : "Paste failed" }
2868
+ };
2869
+ }
2870
+ const next = pasteIntoEditor({
2871
+ lines: prev.fileLines,
2872
+ cursor: prev.editor.cursor,
2873
+ selection: prev.editor.selection,
2874
+ text
2875
+ });
2876
+ return {
2877
+ ...prev,
2878
+ fileLines: next.lines,
2879
+ dirty: contentFromLines(next.lines) !== prev.fileContent,
2880
+ editor: {
2881
+ ...prev.editor,
2882
+ cursor: next.cursor,
2883
+ selection: next.selection
2884
+ },
2885
+ ui: { ...prev.ui, focusPane: "editor", statusMessage: "Pasted" }
2886
+ };
2887
+ });
2888
+ }
2889
+ async function handleUiEvent(event, app) {
1891
2890
  if (event.kind === "engine" && event.event.kind === "resize") {
1892
2891
  const resize = event.event;
1893
2892
  app.update((prev) => ({
@@ -1902,10 +2901,46 @@ function handleUiEvent(event, app) {
1902
2901
  }));
1903
2902
  return;
1904
2903
  }
2904
+ if (event.kind === "engine" && event.event.kind === "key" && event.event.action === "down" && (event.event.mods & ZR_MOD_CTRL) !== 0 && event.event.key === 86) {
2905
+ await handlePaste(app);
2906
+ return;
2907
+ }
1905
2908
  if (event.kind !== "engine" || event.event.kind !== "mouse") {
1906
2909
  return;
1907
2910
  }
1908
- const { x, y } = event.event;
2911
+ if (handleWheelEvent(event.event, app)) {
2912
+ return;
2913
+ }
2914
+ const { x, y, mouseKind, mods } = event.event;
2915
+ if (mouseKind === 3) {
2916
+ const editorRect = app.measureElement("editor");
2917
+ if (editorRect && isPointInRect(x, y, editorRect)) {
2918
+ app.update((prev) => {
2919
+ const cursor = sourceCursorFromEditorPoint({
2920
+ x,
2921
+ y,
2922
+ rect: editorRect,
2923
+ lines: prev.fileLines,
2924
+ scrollTop: prev.editor.scrollTop,
2925
+ scrollLeft: prev.editor.scrollLeft
2926
+ });
2927
+ if (!cursor) {
2928
+ return prev;
2929
+ }
2930
+ const extendSelection = (mods & ZR_MOD_SHIFT) !== 0;
2931
+ return {
2932
+ ...prev,
2933
+ ui: { ...prev.ui, focusPane: "editor" },
2934
+ editor: {
2935
+ ...prev.editor,
2936
+ cursor,
2937
+ selection: extendSelection ? { anchor: prev.editor.cursor, active: cursor } : null
2938
+ }
2939
+ };
2940
+ });
2941
+ return;
2942
+ }
2943
+ }
1909
2944
  const panes = [
1910
2945
  { id: "pane-files", pane: "files" },
1911
2946
  { id: "pane-editor", pane: "editor" },
@@ -1916,7 +2951,7 @@ function handleUiEvent(event, app) {
1916
2951
  if (!rect) {
1917
2952
  continue;
1918
2953
  }
1919
- if (x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h) {
2954
+ if (isPointInRect(x, y, rect)) {
1920
2955
  app.update((prev) => ({
1921
2956
  ...prev,
1922
2957
  ui: { ...prev.ui, focusPane: entry.pane }
@@ -1927,7 +2962,7 @@ function handleUiEvent(event, app) {
1927
2962
  }
1928
2963
  void main().catch((error) => {
1929
2964
  console.error(error);
1930
- process2.exit(1);
2965
+ process3.exit(1);
1931
2966
  });
1932
2967
  export {
1933
2968
  focusPaneId