@keboola/design 8.0.0 → 9.1.0

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/editors.js CHANGED
@@ -1,10 +1,10 @@
1
- import { createContext, useMemo, useRef, useEffect, useCallback, useLayoutEffect, useSyncExternalStore, useState, use, Children, Fragment as Fragment$1 } from 'react';
1
+ import { createContext, useRef, useEffect, useLayoutEffect, useMemo, useCallback, useSyncExternalStore, useState, use, Children, Fragment as Fragment$1 } from 'react';
2
2
  import { completeAnyWord, autocompletion } from '@codemirror/autocomplete';
3
3
  import { jsonParseLinter, json } from '@codemirror/lang-json';
4
4
  import { linter } from '@codemirror/lint';
5
5
  import { unifiedMergeView } from '@codemirror/merge';
6
- import { Decoration, gutter, EditorView, ViewPlugin, GutterMarker, keymap, placeholder, gutterLineClass } from '@codemirror/view';
7
- import CodeMirror, { EditorState, EditorView as EditorView$1, keymap as keymap$1 } from '@uiw/react-codemirror';
6
+ import { EditorView, Decoration, ViewPlugin, gutter, GutterMarker, keymap as keymap$1, placeholder, tooltips, gutterLineClass } from '@codemirror/view';
7
+ import CodeMirror, { keymap, EditorState, EditorView as EditorView$1 } from '@uiw/react-codemirror';
8
8
  import { clsx } from 'clsx';
9
9
  import { extendTailwindMerge } from 'tailwind-merge';
10
10
  import { Root as Root$1, Trigger, Portal, Content } from '@radix-ui/react-tooltip';
@@ -21,12 +21,11 @@ import { r } from '@codemirror/legacy-modes/mode/r';
21
21
  import { shell } from '@codemirror/legacy-modes/mode/shell';
22
22
  import { toml } from '@codemirror/legacy-modes/mode/toml';
23
23
  import { githubLightInit } from '@uiw/codemirror-theme-github';
24
- import { sfsql } from '@keboola/codemirror-lang-sfsql';
24
+ import { installExtensions, getSqlDialect } from '@keboola/codemirror-lang-sql';
25
25
  import colors from '@keboola/tailwind-config/colors';
26
26
  import { simpleMode } from '@codemirror/legacy-modes/mode/simple-mode';
27
+ import { StateEffect, StateField, RangeSet, Compartment, RangeSetBuilder } from '@codemirror/state';
27
28
  import { defaultKeymap } from '@codemirror/commands';
28
- import { installExtensions } from '@keboola/codemirror-lang-sql';
29
- import { StateEffect, StateField, RangeSet, RangeSetBuilder } from '@codemirror/state';
30
29
 
31
30
  // src/components/CodeEditor/CodeEditor.tsx
32
31
 
@@ -290,6 +289,21 @@ Tooltip.Trigger = TooltipTrigger;
290
289
 
291
290
  // src/utils/escapeStringRegexp.ts
292
291
  var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
292
+
293
+ // src/utils/isMac.ts
294
+ var getIsMac = () => {
295
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
296
+ const platform = navigator.userAgentData.platform;
297
+ if (typeof platform === "string") {
298
+ return platform.toLowerCase() === "macos";
299
+ }
300
+ }
301
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
302
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
303
+ }
304
+ return false;
305
+ };
306
+ var isMac = getIsMac();
293
307
  var helpVariants = cva(["tw:m-0", "tw:text-xs"], {
294
308
  defaultVariants: {
295
309
  state: "default"
@@ -740,8 +754,6 @@ var ButtonToolbar = ({ value, children }) => {
740
754
  };
741
755
  var COMMANDS = [
742
756
  "run-operation",
743
- "source freshness",
744
- "docs generate",
745
757
  "run",
746
758
  "test",
747
759
  "seed",
@@ -750,9 +762,13 @@ var COMMANDS = [
750
762
  "ls",
751
763
  "compile",
752
764
  "build",
753
- "deps"
765
+ "deps",
766
+ "docs",
767
+ "source"
754
768
  ];
769
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
755
770
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
771
+ var SELECTORS = ["--select", "--exclude", "-s"];
756
772
  var METHODS = [
757
773
  "path:",
758
774
  "source:",
@@ -779,13 +795,59 @@ var FLAGS = [
779
795
  "--profile",
780
796
  "--full-refresh"
781
797
  ];
782
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
783
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
798
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
799
+ const subcommands = SUBCOMMANDS[command];
800
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
801
+ });
802
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
803
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
804
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
805
+ var WORD = /[\w:-]*$/;
806
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
807
+ var DBT = [{ label: "dbt", type: "keyword" }];
808
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
809
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
810
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
811
+ command,
812
+ asOptions(subcommands, "keyword")
813
+ ])
814
+ );
815
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
816
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
817
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
818
+ var optionsAfter = (words) => {
819
+ const [step, command, ...rest] = words;
820
+ if (step === void 0) return DBT;
821
+ if (step.toLowerCase() !== "dbt") return [];
822
+ if (command === void 0) return COMMAND_OPTIONS;
823
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
824
+ if (subcommands && rest.length === 0) return subcommands;
825
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
826
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
827
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
828
+ };
829
+ var complete = (context) => {
830
+ const typed = context.matchBefore(WORD);
831
+ if (!typed?.text && !context.explicit) return null;
832
+ const from = typed?.from ?? context.pos;
833
+ const line = context.state.doc.lineAt(from);
834
+ const options = optionsAfter(
835
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
836
+ );
837
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
838
+ };
784
839
  var dbt = simpleMode({
840
+ languageData: {
841
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
842
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
843
+ autocomplete: complete
844
+ },
785
845
  start: [
786
846
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
787
- { regex: /dbt/, token: "keyword", sol: true },
788
- { regex: word(COMMANDS), token: "keyword" },
847
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
848
+ // colour — the same thing the completion source asks of it.
849
+ { regex: /dbt\b/, token: "keyword", sol: true },
850
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
789
851
  { regex: word(ARGUMENTS), token: "variableName" },
790
852
  { regex: prefix(METHODS), token: "propertyName" },
791
853
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -833,6 +895,8 @@ var KEYWORDS = {
833
895
  };
834
896
  var jsonnet = {
835
897
  name: "jsonnet",
898
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
899
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
836
900
  startState: () => ({
837
901
  cComment: false,
838
902
  textBlock: false,
@@ -965,6 +1029,7 @@ var LANGUAGE_MODES = [
965
1029
  "text/x-mysql",
966
1030
  "text/x-pgsql",
967
1031
  "text/x-mssql",
1032
+ // Snowflake, under a historical name — these strings are a published prop contract.
968
1033
  "text/x-sfsql",
969
1034
  "text/x-plsql",
970
1035
  "text/x-rsrc",
@@ -1026,8 +1091,6 @@ var ALWAYS_OFF = {
1026
1091
  var buildBasicSetup = (readOnly) => ({
1027
1092
  ...ALWAYS_OFF,
1028
1093
  ...readOnly ? {
1029
- history: false,
1030
- historyKeymap: false,
1031
1094
  closeBrackets: false,
1032
1095
  closeBracketsKeymap: false,
1033
1096
  lintKeymap: false,
@@ -1065,7 +1128,7 @@ var getLanguageMode = (mode) => {
1065
1128
  case "text/x-mssql":
1066
1129
  return sql({ upperCaseKeywords: true, dialect: MSSQL });
1067
1130
  case "text/x-sfsql":
1068
- return StreamLanguage.define(sfsql);
1131
+ return sql({ upperCaseKeywords: true, dialect: getSqlDialect("snowflake") });
1069
1132
  case "text/x-plsql":
1070
1133
  return sql({ upperCaseKeywords: true, dialect: PLSQL });
1071
1134
  case "text/x-rsrc":
@@ -1090,6 +1153,112 @@ var getLanguageMode = (mode) => {
1090
1153
  }
1091
1154
  };
1092
1155
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1156
+ var setSearchEffect = StateEffect.define();
1157
+ function findMatches(doc, query) {
1158
+ if (!query) return [];
1159
+ const text = doc.toString();
1160
+ try {
1161
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1162
+ return Array.from(text.matchAll(regex)).map((match, index) => {
1163
+ const from = match.index;
1164
+ const to = from + match[0].length;
1165
+ return { from, to, text: match[0], localIndex: index };
1166
+ });
1167
+ } catch (e) {
1168
+ console.warn("Invalid search regex:", e);
1169
+ return [];
1170
+ }
1171
+ }
1172
+ function createDecorations(matches, selectedLocalIndex) {
1173
+ const decorations = matches.map((match) => {
1174
+ const isSelected = match.localIndex === selectedLocalIndex;
1175
+ const decoration = Decoration.mark({
1176
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1177
+ });
1178
+ return decoration.range(match.from, match.to);
1179
+ });
1180
+ return Decoration.set(decorations, true);
1181
+ }
1182
+ var searchField = StateField.define({
1183
+ create() {
1184
+ return {
1185
+ query: "",
1186
+ selectedMatchIndex: void 0,
1187
+ matches: [],
1188
+ decorations: Decoration.none
1189
+ };
1190
+ },
1191
+ update(state, tr) {
1192
+ let newState = state;
1193
+ for (const effect of tr.effects) {
1194
+ if (!effect.is(setSearchEffect)) {
1195
+ continue;
1196
+ }
1197
+ const { query, selectedMatchIndex } = effect.value;
1198
+ const matches = findMatches(tr.state.doc, query);
1199
+ const decorations = createDecorations(matches, selectedMatchIndex);
1200
+ newState = {
1201
+ query,
1202
+ selectedMatchIndex,
1203
+ matches,
1204
+ decorations
1205
+ };
1206
+ }
1207
+ if (tr.docChanged && newState.query) {
1208
+ const matches = findMatches(tr.state.doc, newState.query);
1209
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1210
+ newState = {
1211
+ ...newState,
1212
+ matches,
1213
+ decorations
1214
+ };
1215
+ }
1216
+ return newState;
1217
+ },
1218
+ provide: (field) => EditorView.decorations.from(field, (state) => state.decorations)
1219
+ });
1220
+ var searchViewPlugin = ViewPlugin.fromClass(
1221
+ class {
1222
+ lastSelectedIndex;
1223
+ update(update) {
1224
+ const state = update.state.field(searchField);
1225
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1226
+ const match = state.matches[state.selectedMatchIndex];
1227
+ if (match) {
1228
+ requestAnimationFrame(() => {
1229
+ update.view.dispatch({
1230
+ effects: EditorView.scrollIntoView(match.from, { y: "center" })
1231
+ });
1232
+ });
1233
+ }
1234
+ this.lastSelectedIndex = state.selectedMatchIndex;
1235
+ }
1236
+ if (state.selectedMatchIndex === void 0) {
1237
+ this.lastSelectedIndex = void 0;
1238
+ }
1239
+ }
1240
+ }
1241
+ );
1242
+ var searchTheme = EditorView.baseTheme({
1243
+ ".cm-searchMatch": {
1244
+ backgroundColor: colors.blue[100]
1245
+ },
1246
+ ".cm-searchMatch > span": {
1247
+ color: `${colors.blue[600]} !important`
1248
+ },
1249
+ ".cm-searchMatch-selected": {
1250
+ backgroundColor: colors.blue[600]
1251
+ },
1252
+ ".cm-searchMatch-selected > span": {
1253
+ color: `${colors.white} !important`
1254
+ }
1255
+ });
1256
+ function searchExtension() {
1257
+ return [searchField, searchViewPlugin, searchTheme];
1258
+ }
1259
+ function updateSearch(view, query, selectedMatchIndex) {
1260
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1261
+ }
1093
1262
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1094
1263
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1095
1264
  return /* @__PURE__ */ jsx(
@@ -1149,12 +1318,79 @@ Kbd.Group = KbdGroup;
1149
1318
  Kbd.Separator = KbdSeparator;
1150
1319
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxs("div", { children: [
1151
1320
  "Use ",
1152
- /* @__PURE__ */ jsx(Kbd, { children: "Ctrl+/" }),
1153
- " or ",
1154
- /* @__PURE__ */ jsx(Kbd, { children: "Command+/" }),
1321
+ /* @__PURE__ */ jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1155
1322
  " to toggle comments."
1156
1323
  ] });
1157
- var emptyArray = [];
1324
+ var establishesFixedContainingBlock = (style) => style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" || style.backdropFilter !== "none" || /\b(paint|layout|strict|content)\b/.test(style.contain) || /\b(transform|perspective|filter|contain)\b/.test(style.willChange);
1325
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1326
+ var findTooltipHost = (from) => {
1327
+ let outermostCropper = null;
1328
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1329
+ const style = getComputedStyle(node);
1330
+ if (crops(style)) {
1331
+ outermostCropper = node;
1332
+ } else if (establishesFixedContainingBlock(style)) {
1333
+ return outermostCropper?.parentElement ?? node;
1334
+ }
1335
+ }
1336
+ return outermostCropper?.parentElement ?? null;
1337
+ };
1338
+ var tooltipsExtension = () => {
1339
+ const layer = new Compartment();
1340
+ return [
1341
+ layer.of(tooltips()),
1342
+ ViewPlugin.define((view) => {
1343
+ const scheduled = setTimeout(() => {
1344
+ if (!view.dom.isConnected) return;
1345
+ const parent = findTooltipHost(view.dom);
1346
+ if (parent) view.dispatch({ effects: layer.reconfigure(tooltips({ parent })) });
1347
+ });
1348
+ return { destroy: () => clearTimeout(scheduled) };
1349
+ })
1350
+ ];
1351
+ };
1352
+ var useTooltipsExtension = () => useMemo(tooltipsExtension, []);
1353
+
1354
+ // src/components/CodeEditor/variableCompletion.ts
1355
+ var BOOST = 2e3;
1356
+ var NAME = String.raw`[\w-]*`;
1357
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1358
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1359
+ var OPENING = /\{{1,2} ?$/;
1360
+ var CLOSING = /^ ?\}{1,2}/;
1361
+ var completeVariables = (variables = []) => {
1362
+ if (variables.length === 0) return () => null;
1363
+ const options = variables.map(({ name, value }) => ({
1364
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1365
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1366
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1367
+ label: name,
1368
+ displayLabel: `{{${name}}}`,
1369
+ detail: value,
1370
+ type: "variable",
1371
+ boost: BOOST,
1372
+ apply: (view, completion, from, to) => {
1373
+ const { doc } = view.state;
1374
+ const line = doc.lineAt(from);
1375
+ const insert = `{{${completion.label}}}`;
1376
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1377
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1378
+ const start = opening ? from - opening[0].length : from;
1379
+ const end = closing ? to + closing[0].length : to;
1380
+ view.dispatch({
1381
+ changes: { from: start, to: end, insert },
1382
+ selection: { anchor: start + insert.length }
1383
+ });
1384
+ }
1385
+ }));
1386
+ return (context) => {
1387
+ const match = context.matchBefore(AT_CARET);
1388
+ if (!match?.text) return null;
1389
+ const [name = ""] = NAME_AT_END.exec(match.text) ?? [];
1390
+ return { from: match.to - name.length, options };
1391
+ };
1392
+ };
1393
+ var useVariableCompletion = (variables = []) => useStableCallback(completeVariables(variables));
1158
1394
  var emptyObject = {};
1159
1395
  var CodeEditor = ({
1160
1396
  value,
@@ -1163,8 +1399,13 @@ var CodeEditor = ({
1163
1399
  mode = "application/json",
1164
1400
  placeholder: placeholder$1 = "",
1165
1401
  ariaLabel,
1402
+ searchQuery,
1403
+ searchMatchIndex,
1166
1404
  readOnly = false,
1167
- autocompletions = emptyArray,
1405
+ inputLocked = false,
1406
+ autocompletions,
1407
+ completionSource,
1408
+ variables,
1168
1409
  extraKeys = emptyObject,
1169
1410
  withToggleCommentHint = false,
1170
1411
  withAutofocus = false,
@@ -1179,49 +1420,86 @@ var CodeEditor = ({
1179
1420
  className
1180
1421
  }) => {
1181
1422
  const handleChange = useStableCallback((next) => onChange?.(next));
1423
+ const withSearch = searchQuery !== void 0;
1424
+ const editorRef = useRef(null);
1425
+ useEffect(() => {
1426
+ const view = editorRef.current?.view;
1427
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1428
+ }, [withSearch, searchQuery, searchMatchIndex]);
1429
+ const handleCreateEditor = useStableCallback((view) => {
1430
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1431
+ });
1432
+ const extraKeysRef = useRef(extraKeys);
1433
+ useLayoutEffect(() => {
1434
+ extraKeysRef.current = extraKeys;
1435
+ });
1436
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1437
+ const boundKeys = useMemo(
1438
+ () => boundKeysSignature.split("\n").filter(Boolean),
1439
+ [boundKeysSignature]
1440
+ );
1441
+ const tooltipLayer = useTooltipsExtension();
1442
+ const variableCompletion = useVariableCompletion(variables);
1182
1443
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1183
1444
  const baseTheme = useThemedExtension(
1184
1445
  "code-editor:chrome",
1185
1446
  () => EditorView.theme(buildBaseThemeStyles())
1186
1447
  );
1187
1448
  const commonExtensions = useMemo(() => {
1449
+ const callerCompletions = completionSource ?? (completeAny ? completeAnyWord : autocompletions?.length ? autocompletions : null);
1450
+ const completionEntries = [
1451
+ ...callerCompletions ? [{ autocomplete: callerCompletions }] : [],
1452
+ { autocomplete: variableCompletion }
1453
+ ];
1188
1454
  return [
1189
1455
  getLanguageMode(mode),
1190
1456
  shouldLintJson(mode, readOnly) ? linter(jsonParseLinter()) : [],
1191
1457
  withLineWrapping ? EditorView.lineWrapping : [],
1192
- EditorState.languageData.of(() => [
1193
- { autocomplete: completeAny ? completeAnyWord : autocompletions }
1194
- ]),
1195
- keymap.of(
1196
- Object.entries(extraKeys).map(([key, run]) => ({
1458
+ EditorState.languageData.of(() => completionEntries),
1459
+ keymap$1.of(
1460
+ boundKeys.map((key) => ({
1197
1461
  key,
1198
1462
  run: () => {
1199
- if (typeof run === "string") return run;
1200
- run();
1463
+ extraKeysRef.current[key]?.();
1201
1464
  return true;
1202
1465
  }
1203
1466
  }))
1204
1467
  ),
1205
- readOnly ? [] : autocompletion({ activateOnTyping: completeAny }),
1468
+ readOnly ? [] : [
1469
+ // CodeMirror's own default. Gating it on the props hid completions the
1470
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1471
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1472
+ // popup opens anyway.
1473
+ autocompletion()
1474
+ ],
1206
1475
  ariaLabel ? EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1476
+ withSearch ? searchExtension() : [],
1477
+ tooltipLayer,
1207
1478
  baseTheme
1208
1479
  ];
1209
1480
  }, [
1210
1481
  autocompletions,
1482
+ completionSource,
1483
+ variableCompletion,
1211
1484
  completeAny,
1212
- extraKeys,
1485
+ boundKeys,
1213
1486
  mode,
1214
1487
  readOnly,
1215
1488
  withLineWrapping,
1216
1489
  baseTheme,
1217
- ariaLabel
1490
+ ariaLabel,
1491
+ withSearch,
1492
+ tooltipLayer
1218
1493
  ]);
1219
1494
  const extensions = useMemo(() => {
1220
1495
  return [
1221
1496
  ...commonExtensions,
1222
1497
  placeholder(placeholder$1),
1223
1498
  EditorView.editable.of(!readOnly),
1224
- EditorState.readOnly.of(readOnly),
1499
+ // The lock is this facet and nothing else. `contenteditable` is what the
1500
+ // read-only wash keys off, and `basicSetup` is what holds the undo history,
1501
+ // so leaving both alone is what makes the lock invisible.
1502
+ EditorState.readOnly.of(readOnly || inputLocked),
1225
1503
  previousValue ? unifiedMergeView({
1226
1504
  original: previousValue,
1227
1505
  mergeControls: false,
@@ -1229,13 +1507,16 @@ var CodeEditor = ({
1229
1507
  collapseUnchanged: withCollapseUnchanged ? {} : void 0
1230
1508
  }) : []
1231
1509
  ];
1232
- }, [commonExtensions, placeholder$1, readOnly, previousValue, withCollapseUnchanged]);
1510
+ }, [commonExtensions, placeholder$1, readOnly, inputLocked, previousValue, withCollapseUnchanged]);
1233
1511
  const config = useMemo(() => {
1234
1512
  return {
1235
1513
  ...buildBasicSetup(readOnly),
1236
- lineNumbers: withLineNumbers
1514
+ lineNumbers: withLineNumbers,
1515
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1516
+ // second match count over the same document beside the caller's input.
1517
+ ...withSearch ? { searchKeymap: false } : null
1237
1518
  };
1238
- }, [withLineNumbers, readOnly]);
1519
+ }, [withLineNumbers, readOnly, withSearch]);
1239
1520
  const additionalButtons = renderAdditionalButtons?.(value);
1240
1521
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1241
1522
  return (
@@ -1258,6 +1539,8 @@ var CodeEditor = ({
1258
1539
  /* @__PURE__ */ jsx(
1259
1540
  CodeMirror,
1260
1541
  {
1542
+ ref: editorRef,
1543
+ onCreateEditor: handleCreateEditor,
1261
1544
  theme: customTheme,
1262
1545
  value,
1263
1546
  onChange: handleChange,
@@ -1450,8 +1733,6 @@ var activeQueryHighlight = (splitQueries) => {
1450
1733
  })
1451
1734
  ];
1452
1735
  };
1453
-
1454
- // src/components/CodeEditor/highlighting.ts
1455
1736
  var HighlightMarker = class extends GutterMarker {
1456
1737
  toDOM() {
1457
1738
  const marker = document.createElement("div");
@@ -1567,125 +1848,18 @@ var highlightedLinesTheme = EditorView.baseTheme({
1567
1848
  backgroundColor: `${colors.blue[100]} !important`
1568
1849
  }
1569
1850
  });
1570
- function highlightingExtension(queryParser) {
1851
+ function highlightingExtension() {
1571
1852
  return [
1572
1853
  highlightedLinesField,
1573
1854
  highlightGutter,
1574
1855
  errorLineDecorations,
1575
1856
  processingLineDecorations,
1576
- highlightedLinesTheme,
1577
- ...queryParser ? activeQueryHighlight(queryParser) : []
1857
+ highlightedLinesTheme
1578
1858
  ];
1579
1859
  }
1580
1860
  function updateHighlighting(view, lines) {
1581
1861
  view.dispatch({ effects: setHighlightedLinesEffect.of(lines ?? null) });
1582
1862
  }
1583
- var setSearchEffect = StateEffect.define();
1584
- function findMatches(doc, query) {
1585
- if (!query) return [];
1586
- const text = doc.toString();
1587
- try {
1588
- const regex = new RegExp(escapeStringRegexp(query), "gi");
1589
- return Array.from(text.matchAll(regex)).map((match, index) => {
1590
- const from = match.index;
1591
- const to = from + match[0].length;
1592
- return { from, to, text: match[0], localIndex: index };
1593
- });
1594
- } catch (e) {
1595
- console.warn("Invalid search regex:", e);
1596
- return [];
1597
- }
1598
- }
1599
- function createDecorations(matches, selectedLocalIndex) {
1600
- const decorations = matches.map((match) => {
1601
- const isSelected = match.localIndex === selectedLocalIndex;
1602
- const decoration = Decoration.mark({
1603
- class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1604
- });
1605
- return decoration.range(match.from, match.to);
1606
- });
1607
- return Decoration.set(decorations, true);
1608
- }
1609
- var searchField = StateField.define({
1610
- create() {
1611
- return {
1612
- query: "",
1613
- selectedMatchIndex: void 0,
1614
- matches: [],
1615
- decorations: Decoration.none
1616
- };
1617
- },
1618
- update(state, tr) {
1619
- let newState = state;
1620
- for (const effect of tr.effects) {
1621
- if (!effect.is(setSearchEffect)) {
1622
- continue;
1623
- }
1624
- const { query, selectedMatchIndex } = effect.value;
1625
- const matches = findMatches(tr.state.doc, query);
1626
- const decorations = createDecorations(matches, selectedMatchIndex);
1627
- newState = {
1628
- query,
1629
- selectedMatchIndex,
1630
- matches,
1631
- decorations
1632
- };
1633
- }
1634
- if (tr.docChanged && newState.query) {
1635
- const matches = findMatches(tr.state.doc, newState.query);
1636
- const decorations = createDecorations(matches, newState.selectedMatchIndex);
1637
- newState = {
1638
- ...newState,
1639
- matches,
1640
- decorations
1641
- };
1642
- }
1643
- return newState;
1644
- },
1645
- provide: (field) => EditorView.decorations.from(field, (state) => state.decorations)
1646
- });
1647
- var searchViewPlugin = ViewPlugin.fromClass(
1648
- class {
1649
- lastSelectedIndex;
1650
- update(update) {
1651
- const state = update.state.field(searchField);
1652
- if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1653
- const match = state.matches[state.selectedMatchIndex];
1654
- if (match) {
1655
- requestAnimationFrame(() => {
1656
- update.view.dispatch({
1657
- effects: EditorView.scrollIntoView(match.from, { y: "center" })
1658
- });
1659
- });
1660
- }
1661
- this.lastSelectedIndex = state.selectedMatchIndex;
1662
- }
1663
- if (state.selectedMatchIndex === void 0) {
1664
- this.lastSelectedIndex = void 0;
1665
- }
1666
- }
1667
- }
1668
- );
1669
- var searchTheme = EditorView.baseTheme({
1670
- ".cm-searchMatch": {
1671
- backgroundColor: colors.blue[100]
1672
- },
1673
- ".cm-searchMatch > span": {
1674
- color: `${colors.blue[600]} !important`
1675
- },
1676
- ".cm-searchMatch-selected": {
1677
- backgroundColor: colors.blue[600]
1678
- },
1679
- ".cm-searchMatch-selected > span": {
1680
- color: `${colors.white} !important`
1681
- }
1682
- });
1683
- function searchExtension() {
1684
- return [searchField, searchViewPlugin, searchTheme];
1685
- }
1686
- function updateSearch(view, query, selectedMatchIndex) {
1687
- view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1688
- }
1689
1863
  var OWN_KEYMAPS = {
1690
1864
  defaultKeymap: false,
1691
1865
  completionKeymap: false,
@@ -1701,6 +1875,7 @@ var CUSTOM_KEYMAP = [
1701
1875
  }
1702
1876
  }
1703
1877
  ];
1878
+ var OWN_KEYMAP_EXTENSION = keymap.of([...defaultKeymap, ...CUSTOM_KEYMAP]);
1704
1879
  var buildTheme = () => ({
1705
1880
  ...buildBaseThemeStyles(),
1706
1881
  "&.cm-editor": {
@@ -1738,7 +1913,6 @@ var SqlEditor = ({
1738
1913
  searchQuery = "",
1739
1914
  searchMatchIndex,
1740
1915
  readOnly = false,
1741
- extraKeys,
1742
1916
  queryParser,
1743
1917
  autoFocus = false,
1744
1918
  lineNumbers = true,
@@ -1751,34 +1925,57 @@ var SqlEditor = ({
1751
1925
  dialect
1752
1926
  }) => {
1753
1927
  const editorRef = useRef(null);
1928
+ const handleChange = useStableCallback((next) => onChange?.(next));
1929
+ const parseQueries = useStableCallback(async (sql2) => await queryParser?.(sql2) ?? []);
1930
+ const handleBlur = useStableCallback((view) => onBlurEditor?.(view));
1931
+ const handleFocus = useStableCallback((view) => onFocusEditor?.(view));
1932
+ const handleUpdate = useStableCallback((update) => {
1933
+ if (update.selectionSet) {
1934
+ const { main } = update.state.selection;
1935
+ onSelectionChange?.(update.state.sliceDoc(main.from, main.to));
1936
+ }
1937
+ });
1938
+ const variableCompletion = useVariableCompletion(variables);
1939
+ const withQueryParser = queryParser !== void 0;
1940
+ const tooltipLayer = useTooltipsExtension();
1754
1941
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1755
1942
  const themeExtension = useThemedExtension(
1756
1943
  "sql-editor:chrome",
1757
1944
  () => EditorView$1.theme(buildTheme())
1758
1945
  );
1759
1946
  const extensions = useMemo(() => {
1760
- const baseKeymap = [...defaultKeymap, ...CUSTOM_KEYMAP];
1761
1947
  return [
1762
1948
  EditorView$1.lineWrapping,
1763
- keymap$1.of(extraKeys ? [...extraKeys, ...baseKeymap] : baseKeymap),
1764
- ...installExtensions({ schema, defaultDatabase, defaultSchema, variables, dialect }),
1949
+ OWN_KEYMAP_EXTENSION,
1950
+ ...installExtensions({
1951
+ schema,
1952
+ defaultDatabase,
1953
+ defaultSchema,
1954
+ variableCompletion,
1955
+ dialect
1956
+ }),
1765
1957
  searchExtension(),
1766
- highlightingExtension(queryParser),
1958
+ highlightingExtension(),
1959
+ withQueryParser ? activeQueryHighlight(parseQueries) : [],
1960
+ tooltipLayer,
1767
1961
  themeExtension,
1768
- onBlurEditor ? EditorView$1.domEventHandlers({ blur: (_, view) => onBlurEditor(view) }) : [],
1769
- onFocusEditor ? EditorView$1.domEventHandlers({ focus: (_, view) => onFocusEditor(view) }) : []
1962
+ EditorView$1.domEventHandlers({
1963
+ blur: (_, view) => handleBlur(view),
1964
+ focus: (_, view) => handleFocus(view)
1965
+ })
1770
1966
  ];
1771
1967
  }, [
1772
- extraKeys,
1773
- queryParser,
1968
+ withQueryParser,
1969
+ parseQueries,
1774
1970
  schema,
1775
1971
  defaultDatabase,
1776
1972
  defaultSchema,
1777
- variables,
1973
+ variableCompletion,
1778
1974
  dialect,
1779
- onBlurEditor,
1780
- onFocusEditor,
1781
- themeExtension
1975
+ handleBlur,
1976
+ handleFocus,
1977
+ themeExtension,
1978
+ tooltipLayer
1782
1979
  ]);
1783
1980
  const defaultSetup = useMemo(() => {
1784
1981
  return { ...buildBasicSetup(readOnly), ...OWN_KEYMAPS, lineNumbers };
@@ -1793,34 +1990,17 @@ var SqlEditor = ({
1793
1990
  updateSearch(editorRef.current.view, searchQuery, searchMatchIndex);
1794
1991
  }
1795
1992
  }, [searchQuery, searchMatchIndex]);
1796
- useEffect(() => {
1797
- if (!onDestroyEditor) return;
1798
- return () => {
1799
- onDestroyEditor();
1800
- };
1801
- }, [onDestroyEditor]);
1802
- const handleOnCreate = useCallback(
1803
- (view, state) => {
1804
- onCreateEditor?.(view, state);
1805
- if (highlightLines) {
1806
- updateHighlighting(view, highlightLines);
1807
- }
1808
- if (searchQuery) {
1809
- updateSearch(view, searchQuery, searchMatchIndex);
1810
- }
1811
- },
1812
- [onCreateEditor, highlightLines, searchQuery, searchMatchIndex]
1813
- );
1814
- const handleOnUpdate = useCallback(
1815
- (update) => {
1816
- if (update.selectionSet) {
1817
- const { main } = update.state.selection;
1818
- const text = update.state.sliceDoc(main.from, main.to);
1819
- onSelectionChange?.(text);
1820
- }
1821
- },
1822
- [onSelectionChange]
1823
- );
1993
+ const handleDestroy = useStableCallback(() => onDestroyEditor?.());
1994
+ useEffect(() => handleDestroy, [handleDestroy]);
1995
+ const handleCreate = useStableCallback((view, state) => {
1996
+ onCreateEditor?.(view, state);
1997
+ if (highlightLines) {
1998
+ updateHighlighting(view, highlightLines);
1999
+ }
2000
+ if (searchQuery) {
2001
+ updateSearch(view, searchQuery, searchMatchIndex);
2002
+ }
2003
+ });
1824
2004
  return /* @__PURE__ */ jsx(
1825
2005
  CodeMirror,
1826
2006
  {
@@ -1828,9 +2008,9 @@ var SqlEditor = ({
1828
2008
  theme: customTheme,
1829
2009
  extensions,
1830
2010
  value,
1831
- onChange,
1832
- onUpdate: handleOnUpdate,
1833
- onCreateEditor: handleOnCreate,
2011
+ onChange: handleChange,
2012
+ onUpdate: handleUpdate,
2013
+ onCreateEditor: handleCreate,
1834
2014
  autoFocus,
1835
2015
  placeholder,
1836
2016
  className,