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