@harborclient/sdk 1.0.21 → 1.0.25

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.
Files changed (93) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AsyncListState/index.js +3 -3
  3. package/dist/components/Autocomplete/SuggestionList.js +3 -3
  4. package/dist/components/BackButton/index.js +1 -1
  5. package/dist/components/BusyIndicator/index.js +1 -1
  6. package/dist/components/Button/index.js +1 -1
  7. package/dist/components/CodeEditor/config.d.ts +4 -0
  8. package/dist/components/CodeEditor/config.d.ts.map +1 -1
  9. package/dist/components/CodeEditor/config.js +3 -2
  10. package/dist/components/CodeEditor/editorChrome.d.ts +23 -0
  11. package/dist/components/CodeEditor/editorChrome.d.ts.map +1 -0
  12. package/dist/components/CodeEditor/editorChrome.js +136 -0
  13. package/dist/components/CodeEditor/index.d.ts +133 -1
  14. package/dist/components/CodeEditor/index.d.ts.map +1 -1
  15. package/dist/components/CodeEditor/index.js +389 -134
  16. package/dist/components/CodeEditor/renderHighlightedPlaceholder.d.ts +32 -0
  17. package/dist/components/CodeEditor/renderHighlightedPlaceholder.d.ts.map +1 -0
  18. package/dist/components/CodeEditor/renderHighlightedPlaceholder.js +105 -0
  19. package/dist/components/CodeEditor/slashCommandHighlighter.d.ts +17 -0
  20. package/dist/components/CodeEditor/slashCommandHighlighter.d.ts.map +1 -0
  21. package/dist/components/CodeEditor/slashCommandHighlighter.js +56 -0
  22. package/dist/components/CodeEditor/syntaxHighlightedPlaceholder.d.ts +15 -0
  23. package/dist/components/CodeEditor/syntaxHighlightedPlaceholder.d.ts.map +1 -0
  24. package/dist/components/CodeEditor/syntaxHighlightedPlaceholder.js +99 -0
  25. package/dist/components/EmptyState/index.js +1 -1
  26. package/dist/components/FooterButton/index.js +2 -2
  27. package/dist/components/FooterIcon/index.js +1 -1
  28. package/dist/components/FormDataEditor/index.js +2 -2
  29. package/dist/components/FormGroup/index.d.ts.map +1 -1
  30. package/dist/components/FormGroup/index.js +7 -5
  31. package/dist/components/KeyValueEditor/index.js +2 -2
  32. package/dist/components/Modal/ModalFooter.d.ts.map +1 -1
  33. package/dist/components/Modal/ModalFooter.js +3 -1
  34. package/dist/components/Modal/ModalHeader.d.ts.map +1 -1
  35. package/dist/components/Modal/ModalHeader.js +1 -1
  36. package/dist/components/Modal/index.js +1 -1
  37. package/dist/components/OverlayCloseButton/index.js +1 -1
  38. package/dist/components/Page/index.js +1 -1
  39. package/dist/components/PageHeader/index.d.ts.map +1 -1
  40. package/dist/components/PageHeader/index.js +1 -1
  41. package/dist/components/PageSidebar/index.d.ts.map +1 -1
  42. package/dist/components/PageSidebar/index.js +6 -6
  43. package/dist/components/PanelCloseButton/index.js +1 -1
  44. package/dist/components/Resizable/Resizable.js +2 -2
  45. package/dist/components/Resizable/ResizeHandle.d.ts.map +1 -1
  46. package/dist/components/Resizable/ResizeHandle.js +4 -2
  47. package/dist/components/Resizable/footerPanelUtils.d.ts +1 -1
  48. package/dist/components/Resizable/footerPanelUtils.d.ts.map +1 -1
  49. package/dist/components/Resizable/footerPanelUtils.js +1 -1
  50. package/dist/components/Resizable/useResizable.d.ts +5 -4
  51. package/dist/components/Resizable/useResizable.d.ts.map +1 -1
  52. package/dist/components/Resizable/useResizable.js +19 -10
  53. package/dist/components/ResourceList/index.d.ts.map +1 -1
  54. package/dist/components/ResourceList/index.js +5 -5
  55. package/dist/components/RowActionsMenu/index.d.ts.map +1 -1
  56. package/dist/components/RowActionsMenu/index.js +6 -4
  57. package/dist/components/SegmentedTabs/SegmentedTabPanel.js +1 -1
  58. package/dist/components/SegmentedTabs/SegmentedTabsVisibilityMenu.js +4 -4
  59. package/dist/components/SegmentedTabs/index.d.ts.map +1 -1
  60. package/dist/components/SegmentedTabs/index.js +6 -3
  61. package/dist/components/SegmentedTabs/types.d.ts +3 -1
  62. package/dist/components/SegmentedTabs/types.d.ts.map +1 -1
  63. package/dist/components/SidebarLayout/index.js +1 -1
  64. package/dist/components/Spinner/index.js +1 -1
  65. package/dist/components/TabCloseButton/index.js +1 -1
  66. package/dist/components/Table/index.d.ts.map +1 -1
  67. package/dist/components/Table/index.js +7 -6
  68. package/dist/components/Toolbar/index.d.ts +75 -0
  69. package/dist/components/Toolbar/index.d.ts.map +1 -0
  70. package/dist/components/Toolbar/index.js +38 -0
  71. package/dist/components/VariableInput/index.d.ts.map +1 -1
  72. package/dist/components/VariableInput/index.js +6 -4
  73. package/dist/components/VariableTable/index.d.ts.map +1 -1
  74. package/dist/components/VariableTable/index.js +1 -1
  75. package/dist/components/classes.js +2 -2
  76. package/dist/components/forms/Checkbox.d.ts.map +1 -1
  77. package/dist/components/forms/Checkbox.js +1 -1
  78. package/dist/components/forms/Radio.js +1 -1
  79. package/dist/components/forms/classes.d.ts +4 -4
  80. package/dist/components/forms/classes.d.ts.map +1 -1
  81. package/dist/components/forms/classes.js +4 -4
  82. package/dist/components/index.d.ts +3 -1
  83. package/dist/components/index.d.ts.map +1 -1
  84. package/dist/components/index.js +1 -0
  85. package/dist/styles.css +80 -0
  86. package/dist/styles.css.d.ts +2 -0
  87. package/dist/ui/codeEditorSettings.d.ts +15 -0
  88. package/dist/ui/codeEditorSettings.d.ts.map +1 -1
  89. package/dist/ui/codeEditorSettings.js +40 -0
  90. package/dist/ui/index.d.ts +1 -1
  91. package/dist/ui/index.d.ts.map +1 -1
  92. package/dist/ui/index.js +1 -1
  93. package/package.json +10 -4
@@ -1,122 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
2
- import { autocompletion } from '@codemirror/autocomplete';
2
+ import { autocompletion, closeCompletion } from '@codemirror/autocomplete';
3
3
  import { javascript } from '@codemirror/lang-javascript';
4
4
  import { json } from '@codemirror/lang-json';
5
- import { HighlightStyle, StreamLanguage, syntaxHighlighting } from '@codemirror/language';
5
+ import { StreamLanguage } from '@codemirror/language';
6
6
  import { shell } from '@codemirror/legacy-modes/mode/shell';
7
- import { Decoration, EditorView, MatchDecorator, ViewPlugin, hoverTooltip } from '@codemirror/view';
7
+ import { Prec } from '@codemirror/state';
8
+ import { Decoration, EditorView, MatchDecorator, ViewPlugin, hoverTooltip, keymap } from '@codemirror/view';
8
9
  import CodeMirrorImport from '@uiw/react-codemirror';
9
- import { tags } from '@lezer/highlight';
10
- import { useEffect, useId, useMemo, useRef, useState, createElement } from '@harborclient/sdk/react';
10
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, createElement } from '@harborclient/sdk/react';
11
11
  import { getVariableTooltipContent, VARIABLE_NAME_CHARS } from '../../variables/index.js';
12
+ import { normalizeCodeEditorFontSize } from '../../ui/codeEditorSettings.js';
12
13
  import { useCodeEditorConfig } from './config.js';
14
+ import { createBuiltInSyntaxHighlighting, createEditorTheme } from './editorChrome.js';
15
+ import { createSlashCommandHighlighter } from './slashCommandHighlighter.js';
16
+ import { createSyntaxHighlightedPlaceholder } from './syntaxHighlightedPlaceholder.js';
13
17
  import { getCodeEditorThemeExtension } from './themes.js';
14
18
  export { CODE_EDITOR_THEME_OPTIONS } from './themes.js';
15
- const lightHighlight = HighlightStyle.define([
16
- { tag: tags.propertyName, color: '#881391' },
17
- { tag: tags.string, color: '#c41a16' },
18
- { tag: tags.number, color: '#1c00cf' },
19
- { tag: tags.bool, color: '#1c00cf' },
20
- { tag: tags.null, color: '#1c00cf' },
21
- { tag: tags.keyword, color: '#881391' },
22
- { tag: tags.bracket, color: 'var(--mac-text)' },
23
- { tag: tags.punctuation, color: 'var(--mac-muted)' },
24
- { tag: tags.comment, color: 'var(--mac-muted)', fontStyle: 'italic' }
25
- ]);
26
- const darkHighlight = HighlightStyle.define([
27
- { tag: tags.propertyName, color: '#ff7ab2' },
28
- { tag: tags.string, color: '#ff8170' },
29
- { tag: tags.number, color: '#78dce8' },
30
- { tag: tags.bool, color: '#78dce8' },
31
- { tag: tags.null, color: '#78dce8' },
32
- { tag: tags.keyword, color: '#ff7ab2' },
33
- { tag: tags.bracket, color: 'var(--mac-text)' },
34
- { tag: tags.punctuation, color: 'var(--mac-muted)' },
35
- { tag: tags.comment, color: 'var(--mac-muted)', fontStyle: 'italic' }
36
- ]);
37
- const editorTheme = EditorView.theme({
38
- '&': {
39
- backgroundColor: 'transparent',
40
- color: 'var(--mac-text)'
41
- },
42
- '.cm-scroller': {
43
- overflow: 'auto',
44
- fontFamily: 'var(--font-mono)'
45
- },
46
- '.cm-content': {
47
- padding: '8px 0',
48
- fontFamily: 'var(--font-mono)',
49
- fontSize: '14px',
50
- caretColor: 'var(--mac-accent)'
51
- },
52
- '.cm-line': {
53
- padding: '0 8px'
54
- },
55
- '&.cm-focused': {
56
- outline: 'none'
57
- },
58
- '&.cm-focused .cm-cursor': {
59
- borderLeftColor: 'var(--mac-accent)'
60
- },
61
- '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': {
62
- backgroundColor: 'var(--mac-selection) !important'
63
- },
64
- '.cm-gutters': {
65
- backgroundColor: 'transparent',
66
- color: 'var(--mac-muted)',
67
- border: 'none'
68
- },
69
- '.cm-activeLineGutter': {
70
- backgroundColor: 'var(--mac-selection)'
71
- },
72
- '.cm-activeLine': {
73
- backgroundColor: 'color-mix(in srgb, var(--mac-selection) 45%, transparent)'
74
- },
75
- '.cm-variable-token': {
76
- color: '#32D2E2'
77
- },
78
- '.cm-tooltip.cm-tooltip-hover': {
79
- border: '1px solid var(--mac-separator)',
80
- backgroundColor: 'var(--mac-surface)',
81
- borderRadius: '6px',
82
- boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
83
- },
84
- '.cm-variable-tooltip': {
85
- display: 'flex',
86
- flexDirection: 'column',
87
- gap: '6px',
88
- padding: '8px 12px',
89
- fontSize: '14px',
90
- color: 'var(--mac-text)'
91
- },
92
- '.cm-variable-tooltip-muted': {
93
- color: 'var(--mac-muted)'
94
- },
95
- '.cm-variable-tooltip-edit': {
96
- alignSelf: 'flex-start',
97
- background: 'none',
98
- border: 'none',
99
- padding: '0',
100
- cursor: 'pointer',
101
- fontSize: '14px',
102
- color: 'var(--mac-accent)'
103
- },
104
- '.cm-tooltip.cm-tooltip-autocomplete': {
105
- border: '1px solid var(--mac-separator)',
106
- backgroundColor: 'var(--mac-surface)',
107
- borderRadius: '6px',
108
- boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
109
- fontSize: '14px'
110
- },
111
- '.cm-completionLabel': {
112
- fontFamily: 'var(--font-mono)'
113
- },
114
- '.cm-completionDetail': {
115
- color: 'var(--mac-muted)',
116
- fontStyle: 'normal',
117
- marginLeft: '8px'
118
- }
119
- });
19
+ /**
20
+ * Clamps selection offsets to the current document length.
21
+ *
22
+ * @param docLength - Current document length in characters.
23
+ * @param selection - Selection offsets to clamp.
24
+ * @returns Offsets safe to dispatch into CodeMirror.
25
+ */
26
+ function clampSelection(docLength, selection) {
27
+ const maxOffset = Math.max(0, docLength);
28
+ return {
29
+ anchor: Math.min(Math.max(0, selection.anchor), maxOffset),
30
+ head: Math.min(Math.max(0, selection.head), maxOffset)
31
+ };
32
+ }
33
+ /** Debounce interval for {@link Props.onViewStateChange} notifications. */
34
+ const VIEW_STATE_DEBOUNCE_MS = 300;
120
35
  const variableMatcher = new MatchDecorator({
121
36
  regexp: new RegExp(`\\{\\{\\s*([${VARIABLE_NAME_CHARS}]+)\\s*\\}\\}`, 'g'),
122
37
  decoration: Decoration.mark({ class: 'cm-variable-token' })
@@ -140,6 +55,110 @@ const variableHighlighter = ViewPlugin.fromClass(class {
140
55
  this.decorations = variableMatcher.updateDeco(update, this.decorations);
141
56
  }
142
57
  }, { decorations: (v) => v.decorations });
58
+ /** Matches a slash command at the start of a line with optional arguments. */
59
+ const SLASH_COMMAND_LINE_PATTERN = /^(\s*)\/(\w+)(?:[ \t]+(.*))?$/;
60
+ /**
61
+ * Returns a parsed slash command on a line when the name is registered.
62
+ *
63
+ * @param lineText - Full line text without the trailing newline.
64
+ * @param lineFrom - Document offset of the line start.
65
+ * @param lineNumber - 1-based line number.
66
+ * @param commands - Registered slash commands.
67
+ */
68
+ function parseSlashCommandLine(lineText, lineFrom, lineNumber, commands) {
69
+ const match = lineText.match(SLASH_COMMAND_LINE_PATTERN);
70
+ if (!match) {
71
+ return null;
72
+ }
73
+ const command = match[2];
74
+ if (command == null || !commands.some((entry) => entry.name === command)) {
75
+ return null;
76
+ }
77
+ const leading = match[1] ?? '';
78
+ const slashIndex = leading.length;
79
+ const from = lineFrom + slashIndex;
80
+ const to = lineFrom + lineText.length;
81
+ return {
82
+ command,
83
+ args: (match[3] ?? '').trim(),
84
+ line: lineNumber,
85
+ from,
86
+ to
87
+ };
88
+ }
89
+ /**
90
+ * Builds a CodeMirror completion source for registered slash commands at line start.
91
+ *
92
+ * @param commands - Slash commands to offer after `/`.
93
+ */
94
+ function createSlashCommandCompletionSource(commands) {
95
+ /**
96
+ * Returns slash command completions when the caret follows `/` at line start.
97
+ *
98
+ * @param context - CodeMirror completion context at the cursor.
99
+ */
100
+ return (context) => {
101
+ const word = context.matchBefore(/^\s*\/\w*/);
102
+ if (!word || word.text.length === 0) {
103
+ return null;
104
+ }
105
+ const partial = word.text.replace(/^\s*\//, '');
106
+ const options = commands
107
+ .filter((entry) => entry.name.startsWith(partial))
108
+ .map((entry) => ({
109
+ label: `/${entry.name}`,
110
+ type: 'keyword',
111
+ detail: entry.description,
112
+ apply: `/${entry.name} `
113
+ }));
114
+ if (options.length === 0) {
115
+ return null;
116
+ }
117
+ return { from: word.from, options };
118
+ };
119
+ }
120
+ /**
121
+ * Returns an Enter keymap that triggers registered slash commands before default newline handling.
122
+ *
123
+ * @param commands - Slash commands to recognize.
124
+ * @param onSlashCommand - Host callback invoked with parsed trigger details.
125
+ */
126
+ function slashCommandEnterHandler(commands, onSlashCommand) {
127
+ return Prec.highest(keymap.of([
128
+ {
129
+ key: 'Enter',
130
+ /**
131
+ * Opens host slash-command UI when Enter is pressed on a complete command line.
132
+ *
133
+ * @param view - CodeMirror editor view instance.
134
+ */
135
+ run: (view) => {
136
+ const pos = view.state.selection.main.head;
137
+ const line = view.state.doc.lineAt(pos);
138
+ const parsed = parseSlashCommandLine(line.text, line.from, line.number, commands);
139
+ if (!parsed) {
140
+ return false;
141
+ }
142
+ const coords = view.coordsAtPos(parsed.from);
143
+ if (!coords) {
144
+ return false;
145
+ }
146
+ // Dismiss the autocomplete popup so its Enter binding does not also fire.
147
+ closeCompletion(view);
148
+ onSlashCommand({
149
+ ...parsed,
150
+ coords: {
151
+ top: coords.top,
152
+ left: coords.left,
153
+ bottom: coords.bottom,
154
+ right: coords.right
155
+ }
156
+ });
157
+ return true;
158
+ }
159
+ }
160
+ ]));
161
+ }
143
162
  /**
144
163
  * Finds the {{variable}} token at a document position, if any.
145
164
  *
@@ -274,10 +293,10 @@ function variableTooltipEscapeHandler(isOpen, onDismiss, getValidationDescribedB
274
293
  /**
275
294
  * Builds a hover tooltip extension for {{variable}} tokens.
276
295
  *
277
- * @param variables - Collection-scoped variables for resolution.
278
- * @param onEditVariable - Optional callback to open collection settings.
296
+ * @param getVariables - Returns the current collection-scoped variables.
297
+ * @param getOnEditVariable - Returns the optional edit callback.
279
298
  */
280
- function variableTooltip(variables, onEditVariable) {
299
+ function variableTooltip(getVariables, getOnEditVariable) {
281
300
  return hoverTooltip((view, pos) => {
282
301
  const match = findVariableAtPos(view.state.doc, pos);
283
302
  if (!match)
@@ -287,21 +306,27 @@ function variableTooltip(variables, onEditVariable) {
287
306
  end: match.end,
288
307
  above: true,
289
308
  create() {
290
- return { dom: buildVariableTooltipDom(match.key, variables, onEditVariable) };
309
+ return {
310
+ dom: buildVariableTooltipDom(match.key, getVariables(), getOnEditVariable())
311
+ };
291
312
  }
292
313
  };
293
314
  });
294
315
  }
316
+ /** Size of the native resize-y grip hit target in the bottom-right corner. */
317
+ const RESIZE_GRIP_PX = 16;
295
318
  /**
296
319
  * CodeMirror wrapper for editable request bodies and read-only response views.
297
320
  *
298
321
  * Styling relies on host CSS variables (`--mac-*`, `--font-mono`) and the `.app-no-drag`
299
322
  * class defined in HarborClient `styles.css`.
300
323
  */
301
- export function CodeEditor({ value, onChange, language = 'text', readOnly = false, placeholder, minHeight = '144px', className = '', variables, onEditVariable, completionSource, themeOverride, setupOverride, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy }) {
324
+ export function CodeEditor({ value, onChange, language = 'text', readOnly = false, editable, placeholder, placeholderHighlight = false, minHeight = '144px', height, onHeightChange, initialScrollTop, initialSelection, onViewStateChange, className = '', variables, onEditVariable, completionSource, slashCommands, onSlashCommand, themeOverride, setupOverride, fontSize, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy }) {
302
325
  const config = useCodeEditorConfig();
303
326
  const resolvedTheme = themeOverride ?? config.theme;
304
327
  const resolvedSetup = setupOverride ?? (readOnly ? null : config.setup);
328
+ const resolvedFontSize = normalizeCodeEditorFontSize(fontSize ?? config.fontSize);
329
+ const resolvedEditable = editable ?? !readOnly;
305
330
  const [isDark, setIsDark] = useState(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
306
331
  const [selectionTooltip, setSelectionTooltip] = useState(null);
307
332
  const selectionTooltipRef = useRef(selectionTooltip);
@@ -310,8 +335,177 @@ export function CodeEditor({ value, onChange, language = 'text', readOnly = fals
310
335
  setSelectionTooltipRef.current = setSelectionTooltip;
311
336
  const ariaDescribedByRef = useRef(ariaDescribedBy);
312
337
  ariaDescribedByRef.current = ariaDescribedBy;
338
+ const onSlashCommandRef = useRef(onSlashCommand);
339
+ onSlashCommandRef.current = onSlashCommand;
340
+ const onChangeRef = useRef(onChange);
341
+ onChangeRef.current = onChange;
342
+ const completionSourceRef = useRef(completionSource);
343
+ completionSourceRef.current = completionSource;
344
+ const variablesRef = useRef(variables);
345
+ variablesRef.current = variables;
346
+ const onEditVariableRef = useRef(onEditVariable);
347
+ onEditVariableRef.current = onEditVariable;
348
+ const hasVariables = variables != null;
349
+ const hasCompletionSource = completionSource != null;
313
350
  const tooltipId = useId();
351
+ const wrapperRef = useRef(null);
352
+ const onHeightChangeRef = useRef(onHeightChange);
353
+ onHeightChangeRef.current = onHeightChange;
354
+ const isUserResizingRef = useRef(false);
355
+ const editorViewRef = useRef(null);
356
+ const onViewStateChangeRef = useRef(onViewStateChange);
357
+ onViewStateChangeRef.current = onViewStateChange;
358
+ const initialScrollTopRef = useRef(initialScrollTop);
359
+ initialScrollTopRef.current = initialScrollTop;
360
+ const initialSelectionRef = useRef(initialSelection);
361
+ initialSelectionRef.current = initialSelection;
362
+ const viewStateDebounceRef = useRef(undefined);
363
+ const scrollListenerCleanupRef = useRef(null);
364
+ const scheduleViewStateFlushRef = useRef(() => undefined);
314
365
  const getValidationDescribedBy = () => ariaDescribedByRef.current;
366
+ /**
367
+ * Reads the current scroll and selection snapshot from an editor view.
368
+ *
369
+ * @param view - Live CodeMirror view.
370
+ * @returns Rounded scroll offset and selection offsets.
371
+ */
372
+ const readViewState = useCallback((view) => {
373
+ const selection = view.state.selection.main;
374
+ return {
375
+ scrollTop: Math.max(0, Math.round(view.scrollDOM.scrollTop)),
376
+ selection: { anchor: selection.anchor, head: selection.head }
377
+ };
378
+ }, []);
379
+ /**
380
+ * Notifies the host of the latest scroll/selection snapshot.
381
+ */
382
+ const flushViewState = useCallback(() => {
383
+ const view = editorViewRef.current;
384
+ if (!view || !onViewStateChangeRef.current) {
385
+ return;
386
+ }
387
+ onViewStateChangeRef.current(readViewState(view));
388
+ }, [readViewState]);
389
+ /**
390
+ * Debounces view-state persistence while the user scrolls or changes selection.
391
+ */
392
+ useEffect(() => {
393
+ scheduleViewStateFlushRef.current = () => {
394
+ if (viewStateDebounceRef.current) {
395
+ clearTimeout(viewStateDebounceRef.current);
396
+ }
397
+ viewStateDebounceRef.current = setTimeout(() => {
398
+ flushViewState();
399
+ }, VIEW_STATE_DEBOUNCE_MS);
400
+ };
401
+ }, [flushViewState]);
402
+ /**
403
+ * Restores persisted scroll/selection and wires scroll tracking when the view is created.
404
+ */
405
+ const stableOnCreateEditor = useCallback((view) => {
406
+ editorViewRef.current = view;
407
+ const restoredSelection = initialSelectionRef.current;
408
+ if (restoredSelection) {
409
+ const clamped = clampSelection(view.state.doc.length, restoredSelection);
410
+ view.dispatch({
411
+ selection: { anchor: clamped.anchor, head: clamped.head }
412
+ });
413
+ }
414
+ const restoredScrollTop = initialScrollTopRef.current;
415
+ if (restoredScrollTop != null && Number.isFinite(restoredScrollTop) && restoredScrollTop >= 0) {
416
+ view.scrollDOM.scrollTop = restoredScrollTop;
417
+ }
418
+ /**
419
+ * Schedules persistence when the user scrolls the editor content.
420
+ */
421
+ const handleScroll = () => {
422
+ scheduleViewStateFlushRef.current();
423
+ };
424
+ view.scrollDOM.addEventListener('scroll', handleScroll, { passive: true });
425
+ scrollListenerCleanupRef.current = () => {
426
+ view.scrollDOM.removeEventListener('scroll', handleScroll);
427
+ };
428
+ }, []);
429
+ /**
430
+ * Flushes scroll/selection on unmount so collapsed script rows retain view state.
431
+ */
432
+ useEffect(() => {
433
+ return () => {
434
+ if (viewStateDebounceRef.current) {
435
+ clearTimeout(viewStateDebounceRef.current);
436
+ }
437
+ scrollListenerCleanupRef.current?.();
438
+ scrollListenerCleanupRef.current = null;
439
+ flushViewState();
440
+ editorViewRef.current = null;
441
+ };
442
+ }, [flushViewState]);
443
+ /**
444
+ * Persists wrapper height only after the user finishes a native resize-y drag on the grip.
445
+ * Ignores mount and layout settling so height props do not reconfigure CodeMirror in a loop.
446
+ */
447
+ useEffect(() => {
448
+ if (!onHeightChange) {
449
+ return;
450
+ }
451
+ const wrapper = wrapperRef.current;
452
+ if (!wrapper) {
453
+ return;
454
+ }
455
+ /**
456
+ * Marks resize drags that begin on the native bottom-right grip.
457
+ *
458
+ * @param event - Pointer down on the editor wrapper.
459
+ */
460
+ const handlePointerDown = (event) => {
461
+ const rect = wrapper.getBoundingClientRect();
462
+ const onGrip = event.clientX >= rect.right - RESIZE_GRIP_PX &&
463
+ event.clientY >= rect.bottom - RESIZE_GRIP_PX;
464
+ if (onGrip) {
465
+ isUserResizingRef.current = true;
466
+ }
467
+ };
468
+ /**
469
+ * Reports the final wrapper height after a completed user resize drag.
470
+ */
471
+ const handlePointerUp = () => {
472
+ if (!isUserResizingRef.current) {
473
+ return;
474
+ }
475
+ isUserResizingRef.current = false;
476
+ const nextHeight = Math.round(wrapper.getBoundingClientRect().height);
477
+ onHeightChangeRef.current?.(nextHeight);
478
+ };
479
+ wrapper.addEventListener('pointerdown', handlePointerDown);
480
+ window.addEventListener('pointerup', handlePointerUp);
481
+ return () => {
482
+ wrapper.removeEventListener('pointerdown', handlePointerDown);
483
+ window.removeEventListener('pointerup', handlePointerUp);
484
+ };
485
+ }, [onHeightChange]);
486
+ /**
487
+ * Whether to render a muted syntax-highlighted ghost layer instead of plain placeholder text.
488
+ */
489
+ const useHighlightedPlaceholder = Boolean(placeholderHighlight && placeholder && language === 'javascript');
490
+ /**
491
+ * Stable onChange wrapper so @uiw/react-codemirror does not reconfigure on every parent render.
492
+ */
493
+ const stableOnChange = useCallback((nextValue) => {
494
+ onChangeRef.current?.(nextValue);
495
+ }, []);
496
+ /**
497
+ * Stable completion delegator reading the latest host source from a ref.
498
+ */
499
+ const stableCompletionSource = useMemo(() => (context) => completionSourceRef.current?.(context) ?? null, []);
500
+ /**
501
+ * Stable variable accessors for tooltip extensions.
502
+ */
503
+ const getVariables = useCallback(() => variablesRef.current ?? [], []);
504
+ const getOnEditVariable = useCallback(() => onEditVariableRef.current, []);
505
+ /**
506
+ * Memoized editor chrome theme keyed by font size only.
507
+ */
508
+ const editorThemeExt = useMemo(() => createEditorTheme(resolvedFontSize), [resolvedFontSize]);
315
509
  /**
316
510
  * Tracks system dark mode so syntax highlighting matches the active theme.
317
511
  */
@@ -321,35 +515,65 @@ export function CodeEditor({ value, onChange, language = 'text', readOnly = fals
321
515
  media.addEventListener('change', handleChange);
322
516
  return () => media.removeEventListener('change', handleChange);
323
517
  }, []);
518
+ /**
519
+ * Stable selection listener that debounces view-state persistence.
520
+ */
521
+ const viewStateSelectionListener = useMemo(() => EditorView.updateListener.of((update) => {
522
+ if (!onViewStateChangeRef.current || !update.selectionSet) {
523
+ return;
524
+ }
525
+ scheduleViewStateFlushRef.current();
526
+ }), []);
324
527
  /**
325
528
  * Assembles CodeMirror extensions for language mode, theme, and optional variable tooling.
326
529
  */
327
530
  const extensions = useMemo(() => {
328
- const next = [EditorView.lineWrapping, editorTheme];
531
+ const next = [EditorView.lineWrapping, editorThemeExt];
532
+ if (onViewStateChange != null) {
533
+ next.push(viewStateSelectionListener);
534
+ }
329
535
  const themeExtension = getCodeEditorThemeExtension(resolvedTheme);
330
536
  if (themeExtension) {
331
537
  next.push(themeExtension);
332
538
  }
333
539
  else {
334
- next.push(syntaxHighlighting(isDark ? darkHighlight : lightHighlight));
540
+ next.push(createBuiltInSyntaxHighlighting(isDark));
335
541
  }
336
542
  if (language === 'json') {
337
543
  next.push(json());
338
544
  }
339
545
  if (language === 'javascript') {
340
546
  next.push(javascript());
341
- if (completionSource) {
547
+ // Register the slash Enter handler before autocompletion so that, at equal
548
+ // Prec.highest precedence, this handler wins the tie and fires before the
549
+ // completion keymap's Enter->accept binding.
550
+ if (slashCommands && slashCommands.length > 0) {
551
+ next.push(slashCommandEnterHandler(slashCommands, (trigger) => {
552
+ onSlashCommandRef.current?.(trigger);
553
+ }));
554
+ }
555
+ const completionOverrides = [];
556
+ if (slashCommands && slashCommands.length > 0) {
557
+ completionOverrides.push(createSlashCommandCompletionSource(slashCommands));
558
+ }
559
+ if (hasCompletionSource) {
560
+ completionOverrides.push(stableCompletionSource);
561
+ }
562
+ if (completionOverrides.length > 0) {
342
563
  next.push(autocompletion({
343
564
  activateOnTyping: true,
344
- override: [completionSource]
565
+ override: completionOverrides
345
566
  }));
346
567
  }
568
+ if (slashCommands && slashCommands.length > 0) {
569
+ next.push(createSlashCommandHighlighter(slashCommands));
570
+ }
347
571
  }
348
572
  if (language === 'shell') {
349
573
  next.push(StreamLanguage.define(shell));
350
574
  }
351
- if (variables) {
352
- next.push(variableHighlighter, variableTooltip(variables, onEditVariable), variableSelectionTooltip(tooltipId, (state) => {
575
+ if (hasVariables) {
576
+ next.push(variableHighlighter, variableTooltip(getVariables, getOnEditVariable), variableSelectionTooltip(tooltipId, (state) => {
353
577
  setSelectionTooltipRef.current(state);
354
578
  }, getValidationDescribedBy), variableTooltipEscapeHandler(() => selectionTooltipRef.current != null, () => {
355
579
  setSelectionTooltipRef.current(null);
@@ -369,20 +593,37 @@ export function CodeEditor({ value, onChange, language = 'text', readOnly = fals
369
593
  if (Object.keys(contentAttrs).length > 0) {
370
594
  next.push(EditorView.contentAttributes.of(contentAttrs));
371
595
  }
596
+ if (useHighlightedPlaceholder && placeholder) {
597
+ next.push(...createSyntaxHighlightedPlaceholder(placeholder, {
598
+ fontSize: resolvedFontSize,
599
+ isDark,
600
+ theme: resolvedTheme,
601
+ slashCommands
602
+ }));
603
+ }
372
604
  return next;
373
605
  }, [
606
+ editorThemeExt,
374
607
  resolvedTheme,
375
608
  isDark,
376
609
  language,
377
- variables,
378
- onEditVariable,
379
- completionSource,
610
+ hasVariables,
611
+ hasCompletionSource,
612
+ stableCompletionSource,
613
+ getVariables,
614
+ getOnEditVariable,
615
+ slashCommands,
616
+ useHighlightedPlaceholder,
617
+ placeholder,
618
+ resolvedFontSize,
380
619
  id,
381
620
  ariaLabel,
382
621
  ariaLabelledBy,
383
622
  ariaInvalid,
384
623
  ariaDescribedBy,
385
- tooltipId
624
+ tooltipId,
625
+ onViewStateChange,
626
+ viewStateSelectionListener
386
627
  ]);
387
628
  /**
388
629
  * Resolves CodeMirror basicSetup from persisted settings or read-only defaults.
@@ -412,30 +653,44 @@ export function CodeEditor({ value, onChange, language = 'text', readOnly = fals
412
653
  indentOnInput: false
413
654
  };
414
655
  }
656
+ // Disable the built-in autocompletion and completion keymap: this component
657
+ // installs its own autocompletion() for JavaScript editors, ordered after the
658
+ // slash-command Enter handler. Leaving basicSetup's autocompletion enabled would
659
+ // inject a competing Prec.highest Enter->acceptCompletion binding ahead of ours,
660
+ // swallowing the first Enter on a /ask line.
415
661
  return {
416
662
  lineNumbers: resolvedSetup.lineNumbers,
417
663
  foldGutter: resolvedSetup.foldGutter,
418
664
  highlightActiveLine: resolvedSetup.highlightActiveLine,
419
- highlightActiveLineGutter: resolvedSetup.highlightActiveLineGutter
665
+ highlightActiveLineGutter: resolvedSetup.highlightActiveLineGutter,
666
+ highlightSelectionMatches: false,
667
+ autocompletion: false,
668
+ completionKeymap: false
420
669
  };
421
670
  }, [resolvedSetup, readOnly]);
422
671
  const wrapperClassName = readOnly
423
- ? `hc-code-editor overflow-hidden rounded-md bg-control shadow-[inset_0_0.5px_1px_rgba(0,0,0,0.06)] app-no-drag ${className}`
424
- : `hc-code-editor min-h-36 resize-y overflow-hidden rounded-md border border-separator bg-control shadow-[inset_0_0.5px_1px_rgba(0,0,0,0.06)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--mac-accent)_35%,transparent),inset_0_0.5px_1px_rgba(0,0,0,0.06)] app-no-drag ${className}`;
672
+ ? `hc-code-editor overflow-hidden rounded-lg bg-control shadow-[inset_0_0.5px_1px_rgba(0,0,0,0.06)] app-no-drag ${className}`
673
+ : `hc-code-editor min-h-36 resize-y overflow-hidden rounded-lg border border-separator bg-control shadow-[inset_0_0.5px_1px_rgba(0,0,0,0.06)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--mac-accent)_35%,transparent),inset_0_0.5px_1px_rgba(0,0,0,0.06)] app-no-drag ${className}`;
425
674
  const selectionTooltipContent = selectionTooltip
426
675
  ? getVariableTooltipContent(selectionTooltip.key, variables ?? [])
427
676
  : null;
428
- return (_jsxs("div", { className: wrapperClassName, children: [createElement(CodeMirrorImport, {
677
+ const wrapperStyle = height ? { height } : undefined;
678
+ const shouldTrackViewState = onViewStateChange != null || initialScrollTop != null || initialSelection != null;
679
+ return (_jsxs("div", { ref: wrapperRef, className: wrapperClassName, style: wrapperStyle, children: [createElement(CodeMirrorImport, {
429
680
  value,
430
- onChange: readOnly ? undefined : onChange,
681
+ onChange: readOnly ? undefined : stableOnChange,
431
682
  extensions,
432
683
  theme: 'none',
433
- editable: !readOnly,
684
+ editable: resolvedEditable,
434
685
  readOnly,
435
- placeholder,
686
+ placeholder: useHighlightedPlaceholder ? undefined : placeholder,
436
687
  minHeight,
437
- basicSetup
438
- }), selectionTooltip && selectionTooltipContent && variables ? (_jsxs("div", { id: tooltipId, role: "tooltip", className: "pointer-events-auto fixed z-50 flex max-w-sm -translate-x-1/2 -translate-y-full flex-col gap-1.5 rounded-md border border-separator bg-surface px-3 py-2 text-[14px] text-text shadow-md app-no-drag", style: { top: selectionTooltip.top - 4, left: selectionTooltip.left }, children: [_jsx("span", { className: selectionTooltipContent.muted ? 'text-muted' : undefined, children: selectionTooltipContent.text }), onEditVariable ? (_jsx("button", { type: "button", className: "self-start text-[14px] text-accent hover:underline", "aria-label": `Edit value for ${selectionTooltip.key}`, onMouseDown: (event) => {
688
+ ...(height ? { height: '100%' } : {}),
689
+ basicSetup,
690
+ ...(shouldTrackViewState ? { onCreateEditor: stableOnCreateEditor } : {})
691
+ }), selectionTooltip && selectionTooltipContent && variables ? (_jsxs("div", { id: tooltipId, role: "tooltip", className: "hc-code-editor-tooltip pointer-events-auto fixed z-50 flex max-w-sm -translate-x-1/2 -translate-y-full flex-col gap-1.5 rounded-lg border border-separator bg-surface px-3 py-2 text-[14px] text-text shadow-md app-no-drag", style: { top: selectionTooltip.top - 4, left: selectionTooltip.left }, children: [_jsx("span", { className: selectionTooltipContent.muted
692
+ ? 'hc-code-editor-tooltip-text text-muted'
693
+ : 'hc-code-editor-tooltip-text', children: selectionTooltipContent.text }), onEditVariable ? (_jsx("button", { type: "button", className: "hc-code-editor-tooltip-edit self-start text-[14px] text-accent hover:underline", "aria-label": `Edit value for ${selectionTooltip.key}`, onMouseDown: (event) => {
439
694
  event.preventDefault();
440
695
  onEditVariable();
441
696
  setSelectionTooltip(null);
@@ -0,0 +1,32 @@
1
+ import type { CodeEditorTheme } from '../../types.js';
2
+ import { type SlashCommandSpec } from './slashCommandHighlighter.js';
3
+ /**
4
+ * Inputs for rendering a muted syntax-highlighted placeholder document.
5
+ */
6
+ export interface RenderHighlightedPlaceholderOptions {
7
+ /**
8
+ * Normalized CSS font size matching the parent editor.
9
+ */
10
+ fontSize: string;
11
+ /**
12
+ * Whether the host prefers dark mode.
13
+ */
14
+ isDark: boolean;
15
+ /**
16
+ * Persisted CodeMirror theme selection.
17
+ */
18
+ theme: CodeEditorTheme;
19
+ /**
20
+ * Optional slash commands to highlight inside the placeholder document.
21
+ */
22
+ slashCommands?: SlashCommandSpec[];
23
+ }
24
+ /**
25
+ * Renders highlighted placeholder markup via a hidden offscreen editor, then clones
26
+ * the resulting line nodes into a flat inline placeholder element.
27
+ *
28
+ * @param text - Placeholder source shown until the user focuses or types.
29
+ * @param options - Highlighting and theme options shared with the parent editor.
30
+ */
31
+ export declare function renderHighlightedPlaceholderDom(text: string, options: RenderHighlightedPlaceholderOptions): HTMLElement;
32
+ //# sourceMappingURL=renderHighlightedPlaceholder.d.ts.map