@harborclient/sdk 1.0.23 → 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.
- 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 +133 -1
- package/dist/components/CodeEditor/index.d.ts.map +1 -1
- package/dist/components/CodeEditor/index.js +387 -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/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 +1 -1
|
@@ -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 {
|
|
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';
|
|
13
17
|
import { getCodeEditorThemeExtension } from './themes.js';
|
|
14
18
|
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
|
-
});
|
|
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
|
|
278
|
-
* @param
|
|
296
|
+
* @param getVariables - Returns the current collection-scoped variables.
|
|
297
|
+
* @param getOnEditVariable - Returns the optional edit callback.
|
|
279
298
|
*/
|
|
280
|
-
function variableTooltip(
|
|
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 {
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
352
|
-
next.push(variableHighlighter, variableTooltip(
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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,42 @@ 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-
|
|
424
|
-
: `hc-code-editor min-h-36 resize-y overflow-hidden rounded-
|
|
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
|
-
|
|
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 :
|
|
681
|
+
onChange: readOnly ? undefined : stableOnChange,
|
|
431
682
|
extensions,
|
|
432
683
|
theme: 'none',
|
|
433
|
-
editable:
|
|
684
|
+
editable: resolvedEditable,
|
|
434
685
|
readOnly,
|
|
435
|
-
placeholder,
|
|
686
|
+
placeholder: useHighlightedPlaceholder ? undefined : placeholder,
|
|
436
687
|
minHeight,
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
439
692
|
? 'hc-code-editor-tooltip-text text-muted'
|
|
440
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) => {
|
|
441
694
|
event.preventDefault();
|
|
@@ -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
|