@churivibhav/reqex 0.1.1 → 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,15 +1,93 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/cli.ts
5
- import path4 from "path";
6
- 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";
7
7
  import { createNodeApp } from "@rezi-ui/node";
8
8
 
9
- // src/config/keybindings.ts
9
+ // src/config/config.ts
10
10
  import fs from "fs";
11
+ import path2 from "path";
12
+
13
+ // src/config/paths.ts
11
14
  import os from "os";
12
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";
13
91
  var VSCODE_DEFAULTS = {
14
92
  F5: "request.send",
15
93
  "ctrl+enter": "request.send",
@@ -31,13 +109,14 @@ var VSCODE_DEFAULTS = {
31
109
  F11: "pane.zoom",
32
110
  z: "pane.zoom",
33
111
  F1: "help.show",
34
- "?": "help.show",
35
112
  "ctrl+/": "keybindings.show",
36
113
  escape: "overlay.close",
37
114
  "ctrl+q": "app.quit",
38
115
  "ctrl+x": "request.cancel",
39
116
  "ctrl+shift+c": "response.copy",
40
117
  "ctrl+f": "response.search",
118
+ "ctrl+[": "response.jsonFoldToggle",
119
+ "ctrl+]": "response.jsonUnfoldAll",
41
120
  "ctrl+tab": "response.tab.next",
42
121
  "ctrl+shift+tab": "response.tab.prev"
43
122
  };
@@ -72,6 +151,8 @@ var COMMAND_LABELS = {
72
151
  "response.tab.prev": "Previous response tab",
73
152
  "response.copy": "Copy response",
74
153
  "response.search": "Search response",
154
+ "response.jsonFoldToggle": "Fold/unfold JSON node",
155
+ "response.jsonUnfoldAll": "Unfold all JSON",
75
156
  "editor.searchNext": "Find next in editor"
76
157
  };
77
158
  var CHORD_PART_LABELS = {
@@ -117,38 +198,21 @@ function buildKeybindingsViewLines(bindings, maxLines) {
117
198
  }
118
199
  return rows;
119
200
  }
120
- function getConfigDir() {
121
- if (process.env.REQEX_CONFIG_DIR) {
122
- return process.env.REQEX_CONFIG_DIR;
123
- }
124
- if (process.platform === "win32") {
125
- const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
126
- return path.join(appData, "reqex");
127
- }
128
- if (process.platform === "darwin") {
129
- return path.join(os.homedir(), "Library", "Application Support", "reqex");
130
- }
131
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
132
- return path.join(xdg, "reqex");
133
- }
134
- function getProjectConfigDir(workspaceRoot) {
135
- return path.join(workspaceRoot, ".reqex");
136
- }
137
- function readJsonIfExists(filePath) {
201
+ function readJsonIfExists2(filePath) {
138
202
  try {
139
- if (!fs.existsSync(filePath)) {
203
+ if (!fs2.existsSync(filePath)) {
140
204
  return null;
141
205
  }
142
- const raw = fs.readFileSync(filePath, "utf8");
206
+ const raw = fs2.readFileSync(filePath, "utf8");
143
207
  return JSON.parse(raw);
144
208
  } catch {
145
209
  return null;
146
210
  }
147
211
  }
148
212
  function loadKeybindings(workspaceRoot) {
149
- const userConfig = readJsonIfExists(path.join(getConfigDir(), "keybindings.json"));
150
- const projectConfig = readJsonIfExists(
151
- 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")
152
216
  );
153
217
  const preset = projectConfig?.preset ?? userConfig?.preset ?? "vscode";
154
218
  const defaults = preset === "vim" ? VIM_DEFAULTS : VSCODE_DEFAULTS;
@@ -165,10 +229,10 @@ function watchKeybindings(workspaceRoot, onChange) {
165
229
  const dirs = [getConfigDir(), getProjectConfigDir(workspaceRoot)];
166
230
  const watchers = dirs.map((dir) => {
167
231
  try {
168
- fs.mkdirSync(dir, { recursive: true });
232
+ fs2.mkdirSync(dir, { recursive: true });
169
233
  } catch {
170
234
  }
171
- return fs.watch(dir, { persistent: false }, () => onChange());
235
+ return fs2.watch(dir, { persistent: false }, () => onChange());
172
236
  });
173
237
  return () => {
174
238
  for (const watcher of watchers) {
@@ -176,34 +240,166 @@ function watchKeybindings(workspaceRoot, onChange) {
176
240
  }
177
241
  };
178
242
  }
179
- var FOOTER_HINTS = {
180
- editor: ["F5 Send", "Ctrl+S Save", "Tab Panes", "F2 Palette"],
181
- response: ["Tab Panes", "Ctrl+Shift+C Copy", "F5 Send", "F2 Palette"],
182
- files: ["Enter Open", "Tab Panes", "Ctrl+P Files", "F2 Palette"],
183
- overlay: ["\u2191\u2193 Navigate", "Enter Select", "Esc Close"],
184
- 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"
278
+ };
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": []
185
307
  };
186
- function footerHints(context) {
187
- let hints;
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;
188
330
  if (context.overlay !== "none") {
189
- hints = FOOTER_HINTS.overlay;
331
+ contextual = FOOTER_COMMANDS.overlay;
190
332
  } else if (context.sending) {
191
- hints = FOOTER_HINTS.sending;
333
+ contextual = FOOTER_COMMANDS.sending;
192
334
  } else {
193
- 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
+ );
194
347
  }
348
+ if (!context.hasFoldedJson) {
349
+ return commands.filter((command) => command !== "response.jsonUnfoldAll");
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
+ });
195
374
  const leftBudget = 48;
196
375
  const maxWidth = Math.max(20, context.viewportWidth - leftBudget);
197
- let text = hints.join(" \xB7 ");
198
- if (text.length > maxWidth) {
199
- 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
+ }
390
+ }
391
+ const help = pinned.find((item) => item.command === "help.show");
392
+ if (help && selected[selected.length - 1]?.command !== "help.show") {
393
+ append(help);
200
394
  }
201
- return text;
395
+ return selected.filter(Boolean);
202
396
  }
203
397
  var HELP_HINT_LINES = [
204
398
  "F5 Send request under cursor",
205
399
  "Tab / Shift+Tab Cycle panes",
206
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",
207
403
  "Ctrl+E Environment switcher",
208
404
  "F2 / Ctrl+Shift+P Command palette",
209
405
  "Ctrl+Shift+C Copy response tab",
@@ -271,33 +467,91 @@ function resolveRegionAtLine(regions, line) {
271
467
  return best;
272
468
  });
273
469
  }
470
+ function resolveActiveRegion(parsedFile, cursorLine) {
471
+ if (!parsedFile) {
472
+ return null;
473
+ }
474
+ return resolveRegionAtLine(parsedFile.regions, cursorLine);
475
+ }
274
476
  var HTTP_METHOD_PREFIX = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|GRAPHQL)\b/u;
275
- function markerColumnAfterMethod(line) {
276
- const match = HTTP_METHOD_PREFIX.exec(line);
277
- return match ? match[0].length : 0;
278
- }
279
- function buildRegionDiagnostics(regions, activeRegionId, fileLines) {
280
- const markers = [];
281
- for (const region of regions) {
282
- if (!region.hasRequest || region.isGlobal) {
283
- 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;
284
485
  }
285
- const isActive = region.id === activeRegionId;
286
- const line = fileLines[region.startLine] ?? "";
287
- const markerCol = markerColumnAfterMethod(line);
288
- markers.push({
289
- line: region.startLine,
290
- startColumn: markerCol,
291
- endColumn: markerCol + 1,
292
- severity: isActive ? "hint" : "info",
293
- message: `${region.method ?? "REQ"} ${region.name}`
294
- });
295
486
  }
296
- return markers;
487
+ return firstRegion.startLine;
297
488
  }
298
489
 
299
490
  // src/engine/store.ts
300
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
301
555
  var store = new httpyacStoreModule.HttpFileStore();
302
556
  var versions = /* @__PURE__ */ new Map();
303
557
  function toRegion(region) {
@@ -322,7 +576,9 @@ function getParseVersion(filePath) {
322
576
  }
323
577
  async function parseFile(filePath, getText, workingDir, version) {
324
578
  const parseVersion = version ?? getParseVersion(filePath);
579
+ const config = await loadEnvironmentConfig(filePath, workingDir);
325
580
  const httpFile = await store.getOrCreate(filePath, getText, parseVersion, {
581
+ config,
326
582
  workingDir
327
583
  });
328
584
  const regions = httpFile.httpRegions.map(toRegion);
@@ -404,11 +660,13 @@ async function sendRegion(options) {
404
660
  const logResponse = async (response) => {
405
661
  capturedResponse = response;
406
662
  };
663
+ const config = await loadEnvironmentConfig(options.filePath, options.workingDir);
407
664
  try {
408
665
  await send({
409
666
  httpFile,
410
667
  httpRegion,
411
668
  activeEnvironment: options.activeEnvironment,
669
+ config,
412
670
  variables: options.variables,
413
671
  logResponse
414
672
  });
@@ -433,26 +691,28 @@ async function sendRegion(options) {
433
691
  };
434
692
  }
435
693
  }
436
- async function listEnvironments(filePath) {
694
+ async function listEnvironments(filePath, workingDir) {
437
695
  const httpFile = getHttpFile(filePath);
438
696
  if (!httpFile) {
439
697
  return [];
440
698
  }
441
- return getEnvironments({ httpFile });
699
+ const config = await loadEnvironmentConfig(filePath, workingDir);
700
+ return getEnvironments({ httpFile, config });
442
701
  }
443
- async function listVariables(filePath, activeEnvironment) {
702
+ async function listVariables(filePath, workingDir, activeEnvironment) {
444
703
  const httpFile = getHttpFile(filePath);
445
704
  if (!httpFile) {
446
705
  return {};
447
706
  }
448
- return getVariables({ httpFile, activeEnvironment });
707
+ const config = await loadEnvironmentConfig(filePath, workingDir);
708
+ return getVariables({ httpFile, activeEnvironment, config });
449
709
  }
450
710
 
451
711
  // src/keymap/dispatcher.ts
452
712
  function buildBindingMap(bindings, execute) {
453
713
  const map = {};
454
714
  for (const [key, command] of Object.entries(bindings)) {
455
- map[key] = () => execute(command);
715
+ map[key] = (ctx) => execute(command, ctx);
456
716
  }
457
717
  return map;
458
718
  }
@@ -465,6 +725,8 @@ function commandFromPaletteId(id) {
465
725
  "palette.commands": "palette.commands",
466
726
  "help.show": "help.show",
467
727
  "keybindings.show": "keybindings.show",
728
+ "response.jsonFoldToggle": "response.jsonFoldToggle",
729
+ "response.jsonUnfoldAll": "response.jsonUnfoldAll",
468
730
  "pane.zoom": "pane.zoom",
469
731
  "sidebar.toggle": "sidebar.toggle"
470
732
  };
@@ -476,6 +738,8 @@ var COMMAND_ITEMS = [
476
738
  { id: "file.save", label: "Save File", description: "Write editor to disk", shortcut: "Ctrl+S" },
477
739
  { id: "env.switcher", label: "Switch Environment", description: "Choose active environment", shortcut: "Ctrl+E" },
478
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+]" },
479
743
  { id: "pane.zoom", label: "Zoom Pane", description: "Zoom focused pane", shortcut: "F11" },
480
744
  { id: "help.show", label: "Help", description: "Show quick help", shortcut: "F1" },
481
745
  { id: "keybindings.show", label: "Keybindings", description: "Show all keybindings", shortcut: "Ctrl+/" }
@@ -488,7 +752,7 @@ import { EventEmitter } from "events";
488
752
 
489
753
  // src/workspace/discovery.ts
490
754
  import { readdir, stat } from "fs/promises";
491
- import path2 from "path";
755
+ import path5 from "path";
492
756
  var SKIP_DIRS = /* @__PURE__ */ new Set([
493
757
  ".git",
494
758
  "node_modules",
@@ -503,7 +767,7 @@ var FILE_KINDS = [
503
767
  { ext: ".env.json", kind: "env-json" }
504
768
  ];
505
769
  function classifyFile(filePath) {
506
- const base = path2.basename(filePath);
770
+ const base = path5.basename(filePath);
507
771
  for (const { ext, kind } of FILE_KINDS) {
508
772
  if (base === ext || base.endsWith(ext)) {
509
773
  return kind;
@@ -532,7 +796,7 @@ async function discoverDirectory(rootDir, currentDir) {
532
796
  continue;
533
797
  }
534
798
  }
535
- const fullPath = path2.join(currentDir, entry);
799
+ const fullPath = path5.join(currentDir, entry);
536
800
  let entryStat;
537
801
  try {
538
802
  entryStat = await stat(fullPath);
@@ -623,17 +887,17 @@ var Workspace = class extends EventEmitter {
623
887
  ],
624
888
  awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
625
889
  });
626
- const handle = async (type, path5) => {
890
+ const handle = async (type, path8) => {
627
891
  try {
628
892
  this.tree = await discoverFileTree(this.rootDir);
629
- this.emitChange({ type, path: path5 });
893
+ this.emitChange({ type, path: path8 });
630
894
  } catch (error) {
631
895
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
632
896
  }
633
897
  };
634
- this.watcher.on("add", (path5) => void handle("add", path5));
635
- this.watcher.on("change", (path5) => void handle("change", path5));
636
- 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));
637
901
  this.watcher.on("error", (error) => {
638
902
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
639
903
  });
@@ -663,12 +927,12 @@ function createInitialState(workspaceRoot) {
663
927
  dirty: false,
664
928
  parseVersion: 0,
665
929
  parsedFile: null,
666
- activeRegion: null,
667
930
  responseEditor: {
668
931
  scrollTop: 0,
669
932
  scrollLeft: 0,
670
933
  cursor: { line: 0, column: 0 },
671
- selection: null
934
+ selection: null,
935
+ foldedJsonPaths: []
672
936
  },
673
937
  resultGeneration: 0,
674
938
  editor: {
@@ -704,7 +968,9 @@ function createInitialState(workspaceRoot) {
704
968
  },
705
969
  settings: {
706
970
  keymapPreset: "vscode",
707
- keybindings: {}
971
+ keybindings: {},
972
+ theme: "auto",
973
+ themeMode: "dark"
708
974
  }
709
975
  };
710
976
  }
@@ -761,7 +1027,196 @@ function createSendController() {
761
1027
  };
762
1028
  }
763
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
+
764
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
+ }
765
1220
  function createCommandContext(deps) {
766
1221
  setPromptHandler(async (request) => {
767
1222
  return new Promise((resolve) => {
@@ -792,27 +1247,28 @@ function createCommandContext(deps) {
792
1247
  ui: { ...state.ui, pendingPrompt: null }
793
1248
  }));
794
1249
  };
795
- const openFile = async (path5) => {
796
- const content = await deps.workspace.readFile(path5);
1250
+ const openFile = async (path8) => {
1251
+ const content = await deps.workspace.readFile(path8);
797
1252
  const lines = linesFromContent(content);
798
- const parsed = await parseFile(path5, async () => content, deps.workspace.rootDir);
799
- const environments = await listEnvironments(path5);
800
- const variables = await listVariables(path5, [...deps.getState().request.activeEnvironment]);
801
- 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;
802
1259
  deps.update((state) => ({
803
1260
  ...state,
804
- selectedFilePath: path5,
1261
+ selectedFilePath: path8,
805
1262
  fileContent: content,
806
1263
  fileLines: lines,
807
1264
  dirty: false,
808
1265
  parseVersion: parsed.version,
809
1266
  parsedFile: parsed,
810
- activeRegion,
811
1267
  editor: {
812
1268
  ...state.editor,
813
- cursor: { line: activeRegion?.startLine ?? 0, column: 0 },
1269
+ cursor: { line: initialLine, column: 0 },
814
1270
  selection: null,
815
- scrollTop: activeRegion?.startLine ?? 0
1271
+ scrollTop: initialLine
816
1272
  },
817
1273
  request: {
818
1274
  ...state.request,
@@ -846,22 +1302,37 @@ function createCommandContext(deps) {
846
1302
  deps.workspace.rootDir,
847
1303
  version
848
1304
  );
849
- 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);
850
1305
  deps.update((s) => ({
851
1306
  ...s,
852
1307
  fileContent: content,
853
1308
  dirty: false,
854
1309
  parsedFile: parsed,
855
- activeRegion: activeRegion ?? null,
856
1310
  ui: { ...s.ui, statusMessage: "Saved" }
857
1311
  }));
858
1312
  };
859
1313
  const runSend = async () => {
860
1314
  const state = deps.getState();
861
- if (!state.selectedFilePath || !state.parsedFile) {
1315
+ if (!state.selectedFilePath) {
862
1316
  return;
863
1317
  }
864
- 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);
865
1336
  if (!region) {
866
1337
  deps.update((s) => ({
867
1338
  ...s,
@@ -873,13 +1344,13 @@ function createCommandContext(deps) {
873
1344
  const gen = sendController.beginSend();
874
1345
  deps.update((s) => ({
875
1346
  ...s,
876
- activeRegion: region,
877
1347
  request: { ...s.request, sending: true, error: null, result: null },
878
1348
  responseEditor: {
879
1349
  scrollTop: 0,
880
1350
  scrollLeft: 0,
881
1351
  cursor: { line: 0, column: 0 },
882
- selection: null
1352
+ selection: null,
1353
+ foldedJsonPaths: []
883
1354
  },
884
1355
  resultGeneration: s.resultGeneration + 1,
885
1356
  ui: { ...s.ui, focusPane: "response", responseTab: "pretty" }
@@ -895,9 +1366,11 @@ function createCommandContext(deps) {
895
1366
  if (!sendController.isCurrent(gen)) {
896
1367
  return;
897
1368
  }
898
- const variables = await listVariables(state.selectedFilePath, [
899
- ...deps.getState().request.activeEnvironment
900
- ]);
1369
+ const variables = await listVariables(
1370
+ state.selectedFilePath,
1371
+ deps.workspace.rootDir,
1372
+ [...deps.getState().request.activeEnvironment]
1373
+ );
901
1374
  if (!sendController.isCurrent(gen)) {
902
1375
  return;
903
1376
  }
@@ -970,9 +1443,9 @@ function createCommandContext(deps) {
970
1443
  ui: {
971
1444
  ...s.ui,
972
1445
  overlay: s.ui.overlay === "env" ? "none" : "env",
973
- envSelectedIndex: Math.max(
974
- 0,
975
- s.request.environments.indexOf(s.request.activeEnvironment.join(",")) || 0
1446
+ envSelectedIndex: envSelectedIndexFromActive(
1447
+ s.request.environments,
1448
+ s.request.activeEnvironment
976
1449
  )
977
1450
  }
978
1451
  }));
@@ -1003,10 +1476,13 @@ function createCommandContext(deps) {
1003
1476
  }
1004
1477
  break;
1005
1478
  case "env.apply": {
1006
- const envName = state.request.environments[state.ui.envSelectedIndex];
1479
+ const envName = envNameFromSelectedIndex(
1480
+ state.request.environments,
1481
+ state.ui.envSelectedIndex
1482
+ );
1007
1483
  void (async () => {
1008
1484
  const activeEnvironment = envName ? [envName] : [];
1009
- const variables = state.selectedFilePath ? await listVariables(state.selectedFilePath, activeEnvironment) : {};
1485
+ const variables = state.selectedFilePath ? await listVariables(state.selectedFilePath, deps.workspace.rootDir, activeEnvironment) : {};
1010
1486
  deps.update((s) => ({
1011
1487
  ...s,
1012
1488
  request: { ...s.request, activeEnvironment, variables },
@@ -1096,6 +1572,46 @@ function createCommandContext(deps) {
1096
1572
  ui: { ...s.ui, focusPane: "response", responseTab: "pretty" }
1097
1573
  }));
1098
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;
1099
1615
  case "editor.searchNext":
1100
1616
  break;
1101
1617
  default:
@@ -1118,103 +1634,240 @@ function createCommandContext(deps) {
1118
1634
  return context;
1119
1635
  }
1120
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
+
1121
1755
  // src/ui/app-view.ts
1122
- import path3 from "path";
1756
+ import path6 from "path";
1123
1757
  import {
1124
- rgb,
1125
1758
  ui
1126
1759
  } from "@rezi-ui/core";
1127
1760
 
1128
- // src/utils/http-syntax.ts
1129
- var KEYWORDS = /* @__PURE__ */ new Set([
1130
- "GET",
1131
- "POST",
1132
- "PUT",
1133
- "PATCH",
1134
- "DELETE",
1135
- "HEAD",
1136
- "OPTIONS",
1137
- "CONNECT",
1138
- "TRACE",
1139
- "GRAPHQL"
1140
- ]);
1141
- function tokenizeHttpLine(line, _context) {
1142
- const tokens = [];
1143
- const methodMatch = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|GRAPHQL)\b/u.exec(
1144
- line
1145
- );
1146
- if (methodMatch) {
1147
- const method = methodMatch[1] ?? "";
1148
- tokens.push({ text: method, kind: "keyword" });
1149
- const rest = line.slice(methodMatch[0].length);
1150
- if (rest.length > 0) {
1151
- tokens.push({ text: rest, kind: "string" });
1152
- }
1153
- return tokens;
1154
- }
1155
- if (/^\s*#/u.test(line)) {
1156
- return [{ text: line, kind: "comment" }];
1157
- }
1158
- if (/^\s*\/\//u.test(line)) {
1159
- return [{ text: line, kind: "comment" }];
1160
- }
1161
- if (/^\s*@/u.test(line)) {
1162
- return [{ text: line, kind: "type" }];
1163
- }
1164
- const headerMatch = /^\s*([!#$%&'*+\-.^_`|~0-9A-Za-z]+)(\s*:\s*)(.*)$/u.exec(line);
1165
- if (headerMatch) {
1166
- tokens.push({ text: headerMatch[1] ?? "", kind: "function" });
1167
- tokens.push({ text: headerMatch[2] ?? "", kind: "operator" });
1168
- tokens.push({ text: headerMatch[3] ?? "", kind: "string" });
1169
- return tokens;
1170
- }
1171
- const words = line.split(/(\s+)/u);
1172
- for (const word of words) {
1173
- if (KEYWORDS.has(word)) {
1174
- tokens.push({ text: word, kind: "keyword" });
1175
- } else if (word.length > 0) {
1176
- tokens.push({ text: word, kind: "plain" });
1177
- }
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";
1178
1785
  }
1179
- return tokens.length > 0 ? tokens : [{ text: line, kind: "plain" }];
1180
1786
  }
1181
- function prettyJsonIfPossible(text) {
1182
- const trimmed = text.trim();
1183
- if (!trimmed) {
1184
- return text;
1185
- }
1186
- try {
1187
- return JSON.stringify(JSON.parse(trimmed), null, 2);
1188
- } catch {
1189
- return text;
1190
- }
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
+ });
1191
1792
  }
1192
- function statusTone(statusCode) {
1193
- if (!statusCode) {
1194
- return "cyan";
1195
- }
1196
- if (statusCode >= 200 && statusCode < 300) {
1197
- 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;
1198
1812
  }
1199
- if (statusCode >= 400 && statusCode < 500) {
1200
- 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;
1201
1825
  }
1202
- if (statusCode >= 500) {
1203
- 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;
1204
1834
  }
1205
- 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
+ };
1206
1856
  }
1207
1857
 
1208
1858
  // src/ui/app-view.ts
1209
- function paneStyle(focused) {
1210
- 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
+ };
1211
1864
  }
1212
- function renderFileTree(state, deps) {
1865
+ function renderFileTree(state, deps, colors) {
1213
1866
  return ui.panel(
1214
1867
  {
1215
1868
  id: "pane-files",
1216
1869
  title: state.ui.focusPane === "files" ? "\u25CF Files" : "Files",
1217
- style: paneStyle(state.ui.focusPane === "files")
1870
+ style: paneStyle(colors, state.ui.focusPane === "files")
1218
1871
  },
1219
1872
  [
1220
1873
  ui.tree({
@@ -1229,7 +1882,7 @@ function renderFileTree(state, deps) {
1229
1882
  onPress: (node) => deps.onTreePress(node),
1230
1883
  renderNode: (node, _depth, nodeState) => ui.row({ gap: 1 }, [
1231
1884
  ui.text(nodeState.selected ? `\u25B8 ${node.name}` : node.name, {
1232
- 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
1233
1886
  })
1234
1887
  ]),
1235
1888
  flex: 1,
@@ -1238,38 +1891,32 @@ function renderFileTree(state, deps) {
1238
1891
  ]
1239
1892
  );
1240
1893
  }
1241
- function renderEditor(state, deps, readOnly) {
1242
- 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);
1243
1896
  const titleParts = [
1244
1897
  state.ui.focusPane === "editor" ? "\u25CF Editor" : "Editor",
1245
1898
  state.selectedFilePath ? state.selectedFilePath.split("/").pop() : "No file",
1246
1899
  state.dirty ? " \u25CF" : "",
1247
1900
  activeRegion ? ` | ${activeRegion.method ?? "?"} ${activeRegion.name}` : ""
1248
1901
  ];
1249
- const diagnostics = state.parsedFile ? buildRegionDiagnostics(
1250
- state.parsedFile.regions,
1251
- activeRegion?.id ?? null,
1252
- state.fileLines
1253
- ) : [];
1254
1902
  return ui.panel(
1255
1903
  {
1256
1904
  id: "pane-editor",
1257
1905
  title: titleParts.join(""),
1258
- style: paneStyle(state.ui.focusPane === "editor")
1906
+ style: paneStyle(colors, state.ui.focusPane === "editor")
1259
1907
  },
1260
1908
  [
1261
1909
  ui.codeEditor({
1262
1910
  id: "editor",
1263
- lines: state.fileLines,
1264
- cursor: state.editor.cursor,
1265
- selection: state.editor.selection,
1911
+ lines: prefixEditorLines(state.fileLines, activeRegion),
1912
+ cursor: editorCursorFromSource(state.editor.cursor),
1913
+ selection: editorSelectionFromSource(state.editor.selection),
1266
1914
  scrollTop: state.editor.scrollTop,
1267
1915
  scrollLeft: state.editor.scrollLeft,
1268
1916
  readOnly,
1269
1917
  lineNumbers: true,
1270
1918
  syntaxLanguage: "plain",
1271
- tokenizeLine: tokenizeHttpLine,
1272
- diagnostics,
1919
+ tokenizeLine: createRegionAwareTokenizer(activeRegion),
1273
1920
  onChange: (lines, cursor) => deps.onEditorChange(lines, cursor),
1274
1921
  onSelectionChange: deps.onEditorSelection,
1275
1922
  onScroll: deps.onEditorScroll,
@@ -1296,7 +1943,7 @@ function responseCursorProps(state, deps) {
1296
1943
  onSelectionChange: deps.onResponseSelection
1297
1944
  };
1298
1945
  }
1299
- function renderResponseBody(state, deps) {
1946
+ function renderResponseBody(state, deps, colors) {
1300
1947
  const result = state.request.result;
1301
1948
  const scroll = responseScrollProps(state, deps);
1302
1949
  const cursor = responseCursorProps(state, deps);
@@ -1339,7 +1986,7 @@ function renderResponseBody(state, deps) {
1339
1986
  case "variables":
1340
1987
  return ui.codeEditor({
1341
1988
  id: `response-vars-${gen}`,
1342
- lines: [JSON.stringify(state.request.variables, null, 2)],
1989
+ lines: JSON.stringify(state.request.variables, null, 2).split("\n"),
1343
1990
  readOnly: true,
1344
1991
  syntaxLanguage: "json",
1345
1992
  ...cursor,
@@ -1351,16 +1998,20 @@ function renderResponseBody(state, deps) {
1351
1998
  ...result.testResults.map(
1352
1999
  (test) => ui.text(`${test.status === "SUCCESS" ? "\u2713" : "\u2717"} ${test.message}`, {
1353
2000
  style: {
1354
- 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
1355
2002
  }
1356
2003
  })
1357
2004
  )
1358
2005
  ]);
1359
2006
  case "pretty":
1360
- default:
2007
+ default: {
2008
+ const prettyView = buildFoldableJsonView(
2009
+ prettyJsonIfPossible(result.prettyBody || result.body),
2010
+ state.responseEditor.foldedJsonPaths
2011
+ );
1361
2012
  return ui.codeEditor({
1362
2013
  id: `response-pretty-${gen}`,
1363
- lines: prettyJsonIfPossible(result.prettyBody || result.body).split("\n"),
2014
+ lines: prettyView.lines,
1364
2015
  readOnly: true,
1365
2016
  lineNumbers: true,
1366
2017
  syntaxLanguage: "json",
@@ -1369,9 +2020,10 @@ function renderResponseBody(state, deps) {
1369
2020
  ...scroll,
1370
2021
  flex: 1
1371
2022
  });
2023
+ }
1372
2024
  }
1373
2025
  }
1374
- function renderResponse(state, deps) {
2026
+ function renderResponse(state, deps, colors) {
1375
2027
  const result = state.request.result;
1376
2028
  const statusLine = state.request.sending || !result ? null : `${result.protocol ?? "HTTP"} ${result.statusCode ?? "?"} ${result.statusMessage ?? ""} \xB7 ${result.durationMs ?? "?"} ms`;
1377
2029
  const tabItems = [
@@ -1385,12 +2037,15 @@ function renderResponse(state, deps) {
1385
2037
  {
1386
2038
  id: "pane-response",
1387
2039
  title: state.ui.focusPane === "response" ? "\u25CF Response" : "Response",
1388
- style: paneStyle(state.ui.focusPane === "response")
2040
+ style: paneStyle(colors, state.ui.focusPane === "response")
1389
2041
  },
1390
2042
  [
1391
2043
  ui.row({ gap: 2 }, [
1392
2044
  state.request.sending ? ui.spinner({ label: "Sending request\u2026" }) : ui.text(statusLine ?? "Ready", {
1393
- style: { fg: rgb(...statusColor(result?.statusCode)), bold: true }
2045
+ style: {
2046
+ fg: statusColorForTone(colors, statusTone(result?.statusCode)),
2047
+ bold: true
2048
+ }
1394
2049
  }),
1395
2050
  result?.error && !state.request.sending ? ui.badge(result.error, { variant: "error" }) : null
1396
2051
  ]),
@@ -1402,43 +2057,42 @@ function renderResponse(state, deps) {
1402
2057
  disabled: state.request.sending,
1403
2058
  onPress: () => deps.onResponseTab(tab.key)
1404
2059
  })
1405
- )
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
1406
2073
  ]),
1407
- renderResponseBody(state, deps)
2074
+ renderResponseBody(state, deps, colors)
1408
2075
  ]
1409
2076
  );
1410
2077
  }
1411
- function statusColor(code) {
1412
- const tone = statusTone(code);
1413
- switch (tone) {
1414
- case "green":
1415
- return [120, 220, 140];
1416
- case "yellow":
1417
- return [240, 200, 100];
1418
- case "red":
1419
- return [240, 120, 120];
1420
- default:
1421
- return [140, 200, 240];
1422
- }
1423
- }
1424
- function renderMainLayout(state, deps) {
2078
+ function renderMainLayout(state, deps, colors) {
1425
2079
  const layoutMode = resolveLayoutMode(state.ui.viewportWidth);
1426
2080
  const zoom = state.ui.zoomPane;
1427
2081
  if (zoom) {
1428
- if (zoom === "files") return renderFileTree(state, deps);
1429
- if (zoom === "editor") return renderEditor(state, deps, false);
1430
- 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);
1431
2085
  }
1432
2086
  if (layoutMode === "stacked") {
1433
- 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);
1434
2088
  return pane;
1435
2089
  }
1436
2090
  if (layoutMode === "sidebar-overlay") {
1437
2091
  return ui.row({ gap: 1, flex: 1 }, [
1438
- 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,
1439
2093
  ui.column({ gap: 1, flex: 1 }, [
1440
- renderEditor(state, deps, false),
1441
- renderResponse(state, deps)
2094
+ renderEditor(state, deps, false, colors),
2095
+ renderResponse(state, deps, colors)
1442
2096
  ])
1443
2097
  ]);
1444
2098
  }
@@ -1452,38 +2106,53 @@ function renderMainLayout(state, deps) {
1452
2106
  onChange: deps.onSplitChange
1453
2107
  },
1454
2108
  [
1455
- renderFileTree(state, deps),
1456
- renderEditor(state, deps, false),
1457
- renderResponse(state, deps)
2109
+ renderFileTree(state, deps, colors),
2110
+ renderEditor(state, deps, false, colors),
2111
+ renderResponse(state, deps, colors)
1458
2112
  ]
1459
2113
  )
1460
2114
  ]);
1461
2115
  }
1462
- function renderFooter(state) {
2116
+ function renderFooter(state, deps, colors) {
1463
2117
  const env = state.request.activeEnvironment.length > 0 ? state.request.activeEnvironment.join(",") : "none";
1464
- const dirName = path3.basename(state.workspaceRoot);
2118
+ const dirName = path6.basename(state.workspaceRoot);
1465
2119
  const branch = state.ui.gitBranch ?? "\u2014";
1466
- const fileName = state.selectedFilePath ? path3.basename(state.selectedFilePath) : null;
1467
- const hints = footerHints({
2120
+ const fileName = state.selectedFilePath ? path6.basename(state.selectedFilePath) : null;
2121
+ const hints = footerHintItems({
1468
2122
  focusPane: state.ui.focusPane,
1469
2123
  overlay: state.ui.overlay,
1470
2124
  viewportWidth: state.ui.viewportWidth,
1471
- 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
1472
2130
  });
1473
2131
  return ui.statusBar({
1474
2132
  id: "status-bar",
2133
+ style: { bg: colors.bgSubtle, fg: colors.fgPrimary },
1475
2134
  left: [
1476
- ui.text(dirName, { style: { fg: rgb(180, 220, 255), bold: true } }),
1477
- 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 } }),
1478
2137
  fileName ? ui.text(` | ${fileName}`) : null,
1479
- state.dirty ? ui.text(" \u25CF", { style: { fg: rgb(255, 180, 80) } }) : null,
1480
- 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
+ }),
1481
2144
  state.ui.statusMessage ? ui.text(` | ${state.ui.statusMessage}`) : null
1482
2145
  ].filter(Boolean),
1483
- 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
+ )
1484
2153
  });
1485
2154
  }
1486
- function renderOverlayContent(state, deps) {
2155
+ function renderOverlayContent(state, deps, colors) {
1487
2156
  switch (state.ui.overlay) {
1488
2157
  case "env":
1489
2158
  return ui.modal({
@@ -1491,12 +2160,18 @@ function renderOverlayContent(state, deps) {
1491
2160
  title: "Environment",
1492
2161
  content: ui.column({ gap: 1 }, [
1493
2162
  ui.text("Select environment (Enter to apply, Esc to close)"),
1494
- ui.text("(none)", {
1495
- 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
1496
2168
  }),
1497
2169
  ...state.request.environments.map(
1498
- (env, index) => ui.text(env, {
1499
- 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
1500
2175
  })
1501
2176
  )
1502
2177
  ]),
@@ -1562,9 +2237,10 @@ function renderOverlayContent(state, deps) {
1562
2237
  }
1563
2238
  }
1564
2239
  function renderApp(state, deps) {
1565
- const base = ui.column({ gap: 1, flex: 1 }, [
1566
- renderMainLayout(state, deps),
1567
- 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)
1568
2244
  ]);
1569
2245
  if (state.ui.overlay === "none") {
1570
2246
  return base;
@@ -1577,7 +2253,7 @@ function renderApp(state, deps) {
1577
2253
  backdrop: "dim",
1578
2254
  closeOnEscape: true,
1579
2255
  onClose: deps.onOverlayClose,
1580
- content: renderOverlayContent(state, deps)
2256
+ content: renderOverlayContent(state, deps, colors)
1581
2257
  })
1582
2258
  ]);
1583
2259
  }
@@ -1592,6 +2268,35 @@ function focusPaneId(pane) {
1592
2268
  }
1593
2269
  }
1594
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
+
1595
2300
  // src/utils/clipboard.ts
1596
2301
  import clipboard from "clipboardy";
1597
2302
  async function copyToClipboard(text) {
@@ -1605,12 +2310,11 @@ async function copyToClipboard(text) {
1605
2310
  return false;
1606
2311
  }
1607
2312
  }
1608
- function disableFlowControl() {
1609
- if (process.stdin.isTTY) {
1610
- try {
1611
- process.stdin.setRawMode?.(true);
1612
- } catch {
1613
- }
2313
+ async function readFromClipboard() {
2314
+ try {
2315
+ return await clipboard.read();
2316
+ } catch {
2317
+ return null;
1614
2318
  }
1615
2319
  }
1616
2320
 
@@ -1632,17 +2336,229 @@ async function getGitBranch(root) {
1632
2336
  }
1633
2337
  }
1634
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
+
1635
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
+ }
1636
2545
  async function main() {
1637
2546
  initEngineProviders();
1638
- const workspaceRoot = path4.resolve(process2.argv[2] ?? process2.cwd());
2547
+ const workspaceRoot = path7.resolve(process3.argv[2] ?? process3.cwd());
1639
2548
  const workspace = new Workspace(workspaceRoot);
1640
2549
  const tree = await workspace.open();
2550
+ const config = loadConfig(workspaceRoot);
2551
+ const initialThemeMode = await resolveThemeMode(config.theme);
1641
2552
  let currentState = createInitialState(workspaceRoot);
1642
2553
  currentState = {
1643
2554
  ...currentState,
1644
2555
  fileTree: tree,
1645
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
+ },
1646
2562
  ui: {
1647
2563
  ...currentState.ui,
1648
2564
  gitBranch: await getGitBranch(workspaceRoot)
@@ -1663,19 +2579,24 @@ async function main() {
1663
2579
  }
1664
2580
  const loaded = loadKeybindings(workspaceRoot);
1665
2581
  app.keys({
1666
- ...buildBindingMap(loaded.bindings, (command) => {
2582
+ ...buildBindingMap(loaded.bindings, (command, ctx) => {
2583
+ currentState = ctx.state;
1667
2584
  if (command === "response.copy") {
1668
2585
  void handleCopy(app, currentState);
1669
2586
  }
1670
2587
  bus.execute(command);
1671
2588
  }),
1672
2589
  enter: {
1673
- handler: () => bus.execute("env.apply"),
2590
+ handler: (ctx) => {
2591
+ currentState = ctx.state;
2592
+ bus.execute("env.apply");
2593
+ },
1674
2594
  when: (ctx) => ctx.state.ui.overlay === "env",
1675
2595
  description: "Apply selected environment"
1676
2596
  },
1677
2597
  up: {
1678
- handler: () => {
2598
+ handler: (ctx) => {
2599
+ currentState = ctx.state;
1679
2600
  if (currentState.ui.overlay === "env") {
1680
2601
  bus.execute("env.selectPrev");
1681
2602
  }
@@ -1683,7 +2604,8 @@ async function main() {
1683
2604
  when: (ctx) => ctx.state.ui.overlay === "env" || ctx.state.ui.overlay === "commandPalette"
1684
2605
  },
1685
2606
  down: {
1686
- handler: () => {
2607
+ handler: (ctx) => {
2608
+ currentState = ctx.state;
1687
2609
  if (currentState.ui.overlay === "env") {
1688
2610
  bus.execute("env.selectNext");
1689
2611
  }
@@ -1694,11 +2616,33 @@ async function main() {
1694
2616
  app.update((state) => ({
1695
2617
  ...state,
1696
2618
  settings: {
2619
+ ...state.settings,
1697
2620
  keymapPreset: loaded.preset,
1698
2621
  keybindings: loaded.bindings
1699
2622
  }
1700
2623
  }));
1701
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
+ };
1702
2646
  bus = createCommandContext({
1703
2647
  workspace,
1704
2648
  getState: () => currentState,
@@ -1709,16 +2653,27 @@ async function main() {
1709
2653
  });
1710
2654
  },
1711
2655
  quit: () => {
1712
- void workspace.close().finally(() => {
1713
- app?.stop().finally(() => process2.exit(0));
2656
+ void workspace.close().finally(async () => {
2657
+ await app?.stop();
2658
+ await app?.dispose();
2659
+ process3.exit(0);
1714
2660
  });
1715
2661
  },
1716
2662
  reloadKeybindings
1717
2663
  });
1718
- app = createNodeApp({ initialState: currentState });
2664
+ app = createNodeApp({ initialState: currentState, theme: themeForMode(initialThemeMode) });
1719
2665
  reloadKeybindings();
1720
- disableFlowControl();
1721
- 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);
1722
2677
  workspace.on("change", () => {
1723
2678
  void bus.refreshWorkspace();
1724
2679
  void refreshGitBranch();
@@ -1727,29 +2682,27 @@ async function main() {
1727
2682
  (state) => renderApp(state, {
1728
2683
  onEditorChange: (lines, cursor) => {
1729
2684
  app?.update((prev) => {
1730
- const activeRegion = prev.parsedFile ? resolveRegionAtLine(prev.parsedFile.regions, cursor.line) : null;
2685
+ const fileLines = stripEditorLines(lines);
2686
+ const sourceCursor = sourceCursorFromEditor(cursor);
1731
2687
  return {
1732
2688
  ...prev,
1733
- fileLines: [...lines],
1734
- dirty: contentFromLines(lines) !== prev.fileContent,
1735
- activeRegion,
2689
+ fileLines,
2690
+ dirty: contentFromLines(fileLines) !== prev.fileContent,
1736
2691
  ui: { ...prev.ui, focusPane: "editor" },
1737
- editor: { ...prev.editor, cursor }
2692
+ editor: { ...prev.editor, cursor: sourceCursor }
1738
2693
  };
1739
2694
  });
1740
2695
  },
1741
2696
  onEditorSelection: (selection) => {
1742
2697
  app?.update((prev) => {
1743
- const cursorLine = selection?.active.line ?? prev.editor.cursor.line;
1744
- const activeRegion = prev.parsedFile ? resolveRegionAtLine(prev.parsedFile.regions, cursorLine) : null;
2698
+ const sourceSelection = sourceSelectionFromEditor(selection);
1745
2699
  return {
1746
2700
  ...prev,
1747
- activeRegion,
1748
2701
  ui: { ...prev.ui, focusPane: "editor" },
1749
2702
  editor: {
1750
2703
  ...prev.editor,
1751
- selection,
1752
- cursor: selection?.active ?? prev.editor.cursor
2704
+ selection: sourceSelection,
2705
+ cursor: sourceSelection?.active ?? prev.editor.cursor
1753
2706
  }
1754
2707
  };
1755
2708
  });
@@ -1813,6 +2766,12 @@ async function main() {
1813
2766
  responseEditor: { ...prev.responseEditor, cursor }
1814
2767
  }));
1815
2768
  },
2769
+ onResponseJsonFoldToggle: () => {
2770
+ bus?.execute("response.jsonFoldToggle");
2771
+ },
2772
+ onResponseJsonUnfoldAll: () => {
2773
+ bus?.execute("response.jsonUnfoldAll");
2774
+ },
1816
2775
  onSplitChange: (sizes) => {
1817
2776
  if (sizes.length === 3) {
1818
2777
  app?.update((prev) => ({
@@ -1850,26 +2809,34 @@ async function main() {
1850
2809
  bus?.execute("overlay.close");
1851
2810
  },
1852
2811
  onEnvSelect: (index) => {
1853
- app?.update((prev) => ({
1854
- ...prev,
1855
- ui: { ...prev.ui, envSelectedIndex: index }
1856
- }));
2812
+ app?.update((prev) => {
2813
+ currentState = {
2814
+ ...prev,
2815
+ ui: { ...prev.ui, envSelectedIndex: index }
2816
+ };
2817
+ return currentState;
2818
+ });
2819
+ bus?.execute("env.apply");
1857
2820
  },
1858
2821
  onResponseSearch: (query) => {
1859
2822
  app?.update((prev) => ({
1860
2823
  ...prev,
1861
2824
  editor: { ...prev.editor, searchQuery: query }
1862
2825
  }));
1863
- }
2826
+ },
2827
+ onCommand: executeCommand
1864
2828
  })
1865
2829
  );
1866
- app.onEvent((event) => handleUiEvent(event, app));
2830
+ app.onEvent((event) => {
2831
+ void handleUiEvent(event, app);
2832
+ });
1867
2833
  const files = flattenFiles(tree).filter((node) => node.kind === "file");
1868
2834
  if (files[0]) {
1869
2835
  await bus.openFile(files[0].path);
1870
2836
  }
1871
2837
  await app.run();
1872
- stopWatch();
2838
+ stopKeybindingWatch();
2839
+ stopConfigWatch();
1873
2840
  }
1874
2841
  async function handleCopy(app, state) {
1875
2842
  const result = state.request.result;
@@ -1888,7 +2855,38 @@ async function handleCopy(app, state) {
1888
2855
  ui: { ...prev.ui, statusMessage: ok ? "Copied" : "Copy failed" }
1889
2856
  }));
1890
2857
  }
1891
- 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) {
1892
2890
  if (event.kind === "engine" && event.event.kind === "resize") {
1893
2891
  const resize = event.event;
1894
2892
  app.update((prev) => ({
@@ -1903,10 +2901,46 @@ function handleUiEvent(event, app) {
1903
2901
  }));
1904
2902
  return;
1905
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
+ }
1906
2908
  if (event.kind !== "engine" || event.event.kind !== "mouse") {
1907
2909
  return;
1908
2910
  }
1909
- 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
+ }
1910
2944
  const panes = [
1911
2945
  { id: "pane-files", pane: "files" },
1912
2946
  { id: "pane-editor", pane: "editor" },
@@ -1917,7 +2951,7 @@ function handleUiEvent(event, app) {
1917
2951
  if (!rect) {
1918
2952
  continue;
1919
2953
  }
1920
- if (x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h) {
2954
+ if (isPointInRect(x, y, rect)) {
1921
2955
  app.update((prev) => ({
1922
2956
  ...prev,
1923
2957
  ui: { ...prev.ui, focusPane: entry.pane }
@@ -1928,7 +2962,7 @@ function handleUiEvent(event, app) {
1928
2962
  }
1929
2963
  void main().catch((error) => {
1930
2964
  console.error(error);
1931
- process2.exit(1);
2965
+ process3.exit(1);
1932
2966
  });
1933
2967
  export {
1934
2968
  focusPaneId