@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.cjs CHANGED
@@ -23,12 +23,11 @@ var r = require('@codemirror/legacy-modes/mode/r');
23
23
  var shell = require('@codemirror/legacy-modes/mode/shell');
24
24
  var toml = require('@codemirror/legacy-modes/mode/toml');
25
25
  var codemirrorThemeGithub = require('@uiw/codemirror-theme-github');
26
- var codemirrorLangSfsql = require('@keboola/codemirror-lang-sfsql');
26
+ var codemirrorLangSql = require('@keboola/codemirror-lang-sql');
27
27
  var colors = require('@keboola/tailwind-config/colors');
28
28
  var simpleMode = require('@codemirror/legacy-modes/mode/simple-mode');
29
- var commands = require('@codemirror/commands');
30
- var codemirrorLangSql = require('@keboola/codemirror-lang-sql');
31
29
  var state = require('@codemirror/state');
30
+ var commands = require('@codemirror/commands');
32
31
 
33
32
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
34
33
 
@@ -297,6 +296,21 @@ Tooltip.Trigger = TooltipTrigger;
297
296
 
298
297
  // src/utils/escapeStringRegexp.ts
299
298
  var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
299
+
300
+ // src/utils/isMac.ts
301
+ var getIsMac = () => {
302
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
303
+ const platform = navigator.userAgentData.platform;
304
+ if (typeof platform === "string") {
305
+ return platform.toLowerCase() === "macos";
306
+ }
307
+ }
308
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
309
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
310
+ }
311
+ return false;
312
+ };
313
+ var isMac = getIsMac();
300
314
  var helpVariants = classVarianceAuthority.cva(["tw:m-0", "tw:text-xs"], {
301
315
  defaultVariants: {
302
316
  state: "default"
@@ -747,8 +761,6 @@ var ButtonToolbar = ({ value, children }) => {
747
761
  };
748
762
  var COMMANDS = [
749
763
  "run-operation",
750
- "source freshness",
751
- "docs generate",
752
764
  "run",
753
765
  "test",
754
766
  "seed",
@@ -757,9 +769,13 @@ var COMMANDS = [
757
769
  "ls",
758
770
  "compile",
759
771
  "build",
760
- "deps"
772
+ "deps",
773
+ "docs",
774
+ "source"
761
775
  ];
776
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
762
777
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
778
+ var SELECTORS = ["--select", "--exclude", "-s"];
763
779
  var METHODS = [
764
780
  "path:",
765
781
  "source:",
@@ -786,13 +802,59 @@ var FLAGS = [
786
802
  "--profile",
787
803
  "--full-refresh"
788
804
  ];
789
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
790
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
805
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
806
+ const subcommands = SUBCOMMANDS[command];
807
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
808
+ });
809
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
810
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
811
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
812
+ var WORD = /[\w:-]*$/;
813
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
814
+ var DBT = [{ label: "dbt", type: "keyword" }];
815
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
816
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
817
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
818
+ command,
819
+ asOptions(subcommands, "keyword")
820
+ ])
821
+ );
822
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
823
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
824
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
825
+ var optionsAfter = (words) => {
826
+ const [step, command, ...rest] = words;
827
+ if (step === void 0) return DBT;
828
+ if (step.toLowerCase() !== "dbt") return [];
829
+ if (command === void 0) return COMMAND_OPTIONS;
830
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
831
+ if (subcommands && rest.length === 0) return subcommands;
832
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
833
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
834
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
835
+ };
836
+ var complete = (context) => {
837
+ const typed = context.matchBefore(WORD);
838
+ if (!typed?.text && !context.explicit) return null;
839
+ const from = typed?.from ?? context.pos;
840
+ const line = context.state.doc.lineAt(from);
841
+ const options = optionsAfter(
842
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
843
+ );
844
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
845
+ };
791
846
  var dbt = simpleMode.simpleMode({
847
+ languageData: {
848
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
849
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
850
+ autocomplete: complete
851
+ },
792
852
  start: [
793
853
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
794
- { regex: /dbt/, token: "keyword", sol: true },
795
- { regex: word(COMMANDS), token: "keyword" },
854
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
855
+ // colour — the same thing the completion source asks of it.
856
+ { regex: /dbt\b/, token: "keyword", sol: true },
857
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
796
858
  { regex: word(ARGUMENTS), token: "variableName" },
797
859
  { regex: prefix(METHODS), token: "propertyName" },
798
860
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -840,6 +902,8 @@ var KEYWORDS = {
840
902
  };
841
903
  var jsonnet = {
842
904
  name: "jsonnet",
905
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
906
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
843
907
  startState: () => ({
844
908
  cComment: false,
845
909
  textBlock: false,
@@ -972,6 +1036,7 @@ var LANGUAGE_MODES = [
972
1036
  "text/x-mysql",
973
1037
  "text/x-pgsql",
974
1038
  "text/x-mssql",
1039
+ // Snowflake, under a historical name — these strings are a published prop contract.
975
1040
  "text/x-sfsql",
976
1041
  "text/x-plsql",
977
1042
  "text/x-rsrc",
@@ -1033,8 +1098,6 @@ var ALWAYS_OFF = {
1033
1098
  var buildBasicSetup = (readOnly) => ({
1034
1099
  ...ALWAYS_OFF,
1035
1100
  ...readOnly ? {
1036
- history: false,
1037
- historyKeymap: false,
1038
1101
  closeBrackets: false,
1039
1102
  closeBracketsKeymap: false,
1040
1103
  lintKeymap: false,
@@ -1072,7 +1135,7 @@ var getLanguageMode = (mode) => {
1072
1135
  case "text/x-mssql":
1073
1136
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.MSSQL });
1074
1137
  case "text/x-sfsql":
1075
- return language.StreamLanguage.define(codemirrorLangSfsql.sfsql);
1138
+ return langSql.sql({ upperCaseKeywords: true, dialect: codemirrorLangSql.getSqlDialect("snowflake") });
1076
1139
  case "text/x-plsql":
1077
1140
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.PLSQL });
1078
1141
  case "text/x-rsrc":
@@ -1097,6 +1160,112 @@ var getLanguageMode = (mode) => {
1097
1160
  }
1098
1161
  };
1099
1162
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1163
+ var setSearchEffect = state.StateEffect.define();
1164
+ function findMatches(doc, query) {
1165
+ if (!query) return [];
1166
+ const text = doc.toString();
1167
+ try {
1168
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1169
+ return Array.from(text.matchAll(regex)).map((match, index) => {
1170
+ const from = match.index;
1171
+ const to = from + match[0].length;
1172
+ return { from, to, text: match[0], localIndex: index };
1173
+ });
1174
+ } catch (e) {
1175
+ console.warn("Invalid search regex:", e);
1176
+ return [];
1177
+ }
1178
+ }
1179
+ function createDecorations(matches, selectedLocalIndex) {
1180
+ const decorations = matches.map((match) => {
1181
+ const isSelected = match.localIndex === selectedLocalIndex;
1182
+ const decoration = view.Decoration.mark({
1183
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1184
+ });
1185
+ return decoration.range(match.from, match.to);
1186
+ });
1187
+ return view.Decoration.set(decorations, true);
1188
+ }
1189
+ var searchField = state.StateField.define({
1190
+ create() {
1191
+ return {
1192
+ query: "",
1193
+ selectedMatchIndex: void 0,
1194
+ matches: [],
1195
+ decorations: view.Decoration.none
1196
+ };
1197
+ },
1198
+ update(state, tr) {
1199
+ let newState = state;
1200
+ for (const effect of tr.effects) {
1201
+ if (!effect.is(setSearchEffect)) {
1202
+ continue;
1203
+ }
1204
+ const { query, selectedMatchIndex } = effect.value;
1205
+ const matches = findMatches(tr.state.doc, query);
1206
+ const decorations = createDecorations(matches, selectedMatchIndex);
1207
+ newState = {
1208
+ query,
1209
+ selectedMatchIndex,
1210
+ matches,
1211
+ decorations
1212
+ };
1213
+ }
1214
+ if (tr.docChanged && newState.query) {
1215
+ const matches = findMatches(tr.state.doc, newState.query);
1216
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1217
+ newState = {
1218
+ ...newState,
1219
+ matches,
1220
+ decorations
1221
+ };
1222
+ }
1223
+ return newState;
1224
+ },
1225
+ provide: (field) => view.EditorView.decorations.from(field, (state) => state.decorations)
1226
+ });
1227
+ var searchViewPlugin = view.ViewPlugin.fromClass(
1228
+ class {
1229
+ lastSelectedIndex;
1230
+ update(update) {
1231
+ const state = update.state.field(searchField);
1232
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1233
+ const match = state.matches[state.selectedMatchIndex];
1234
+ if (match) {
1235
+ requestAnimationFrame(() => {
1236
+ update.view.dispatch({
1237
+ effects: view.EditorView.scrollIntoView(match.from, { y: "center" })
1238
+ });
1239
+ });
1240
+ }
1241
+ this.lastSelectedIndex = state.selectedMatchIndex;
1242
+ }
1243
+ if (state.selectedMatchIndex === void 0) {
1244
+ this.lastSelectedIndex = void 0;
1245
+ }
1246
+ }
1247
+ }
1248
+ );
1249
+ var searchTheme = view.EditorView.baseTheme({
1250
+ ".cm-searchMatch": {
1251
+ backgroundColor: colors__default.default.blue[100]
1252
+ },
1253
+ ".cm-searchMatch > span": {
1254
+ color: `${colors__default.default.blue[600]} !important`
1255
+ },
1256
+ ".cm-searchMatch-selected": {
1257
+ backgroundColor: colors__default.default.blue[600]
1258
+ },
1259
+ ".cm-searchMatch-selected > span": {
1260
+ color: `${colors__default.default.white} !important`
1261
+ }
1262
+ });
1263
+ function searchExtension() {
1264
+ return [searchField, searchViewPlugin, searchTheme];
1265
+ }
1266
+ function updateSearch(view, query, selectedMatchIndex) {
1267
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1268
+ }
1100
1269
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsxRuntime.jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1101
1270
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1102
1271
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1156,12 +1325,79 @@ Kbd.Group = KbdGroup;
1156
1325
  Kbd.Separator = KbdSeparator;
1157
1326
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1158
1327
  "Use ",
1159
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Ctrl+/" }),
1160
- " or ",
1161
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Command+/" }),
1328
+ /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1162
1329
  " to toggle comments."
1163
1330
  ] });
1164
- var emptyArray = [];
1331
+ 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);
1332
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1333
+ var findTooltipHost = (from) => {
1334
+ let outermostCropper = null;
1335
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1336
+ const style = getComputedStyle(node);
1337
+ if (crops(style)) {
1338
+ outermostCropper = node;
1339
+ } else if (establishesFixedContainingBlock(style)) {
1340
+ return outermostCropper?.parentElement ?? node;
1341
+ }
1342
+ }
1343
+ return outermostCropper?.parentElement ?? null;
1344
+ };
1345
+ var tooltipsExtension = () => {
1346
+ const layer = new state.Compartment();
1347
+ return [
1348
+ layer.of(view.tooltips()),
1349
+ view.ViewPlugin.define((view$1) => {
1350
+ const scheduled = setTimeout(() => {
1351
+ if (!view$1.dom.isConnected) return;
1352
+ const parent = findTooltipHost(view$1.dom);
1353
+ if (parent) view$1.dispatch({ effects: layer.reconfigure(view.tooltips({ parent })) });
1354
+ });
1355
+ return { destroy: () => clearTimeout(scheduled) };
1356
+ })
1357
+ ];
1358
+ };
1359
+ var useTooltipsExtension = () => react.useMemo(tooltipsExtension, []);
1360
+
1361
+ // src/components/CodeEditor/variableCompletion.ts
1362
+ var BOOST = 2e3;
1363
+ var NAME = String.raw`[\w-]*`;
1364
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1365
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1366
+ var OPENING = /\{{1,2} ?$/;
1367
+ var CLOSING = /^ ?\}{1,2}/;
1368
+ var completeVariables = (variables = []) => {
1369
+ if (variables.length === 0) return () => null;
1370
+ const options = variables.map(({ name, value }) => ({
1371
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1372
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1373
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1374
+ label: name,
1375
+ displayLabel: `{{${name}}}`,
1376
+ detail: value,
1377
+ type: "variable",
1378
+ boost: BOOST,
1379
+ apply: (view, completion, from, to) => {
1380
+ const { doc } = view.state;
1381
+ const line = doc.lineAt(from);
1382
+ const insert = `{{${completion.label}}}`;
1383
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1384
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1385
+ const start = opening ? from - opening[0].length : from;
1386
+ const end = closing ? to + closing[0].length : to;
1387
+ view.dispatch({
1388
+ changes: { from: start, to: end, insert },
1389
+ selection: { anchor: start + insert.length }
1390
+ });
1391
+ }
1392
+ }));
1393
+ return (context) => {
1394
+ const match = context.matchBefore(AT_CARET);
1395
+ if (!match?.text) return null;
1396
+ const [name = ""] = NAME_AT_END.exec(match.text) ?? [];
1397
+ return { from: match.to - name.length, options };
1398
+ };
1399
+ };
1400
+ var useVariableCompletion = (variables = []) => useStableCallback(completeVariables(variables));
1165
1401
  var emptyObject = {};
1166
1402
  var CodeEditor = ({
1167
1403
  value,
@@ -1170,8 +1406,13 @@ var CodeEditor = ({
1170
1406
  mode = "application/json",
1171
1407
  placeholder = "",
1172
1408
  ariaLabel,
1409
+ searchQuery,
1410
+ searchMatchIndex,
1173
1411
  readOnly = false,
1174
- autocompletions = emptyArray,
1412
+ inputLocked = false,
1413
+ autocompletions,
1414
+ completionSource,
1415
+ variables,
1175
1416
  extraKeys = emptyObject,
1176
1417
  withToggleCommentHint = false,
1177
1418
  withAutofocus = false,
@@ -1186,49 +1427,86 @@ var CodeEditor = ({
1186
1427
  className
1187
1428
  }) => {
1188
1429
  const handleChange = useStableCallback((next) => onChange?.(next));
1430
+ const withSearch = searchQuery !== void 0;
1431
+ const editorRef = react.useRef(null);
1432
+ react.useEffect(() => {
1433
+ const view = editorRef.current?.view;
1434
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1435
+ }, [withSearch, searchQuery, searchMatchIndex]);
1436
+ const handleCreateEditor = useStableCallback((view) => {
1437
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1438
+ });
1439
+ const extraKeysRef = react.useRef(extraKeys);
1440
+ react.useLayoutEffect(() => {
1441
+ extraKeysRef.current = extraKeys;
1442
+ });
1443
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1444
+ const boundKeys = react.useMemo(
1445
+ () => boundKeysSignature.split("\n").filter(Boolean),
1446
+ [boundKeysSignature]
1447
+ );
1448
+ const tooltipLayer = useTooltipsExtension();
1449
+ const variableCompletion = useVariableCompletion(variables);
1189
1450
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1190
1451
  const baseTheme = useThemedExtension(
1191
1452
  "code-editor:chrome",
1192
1453
  () => view.EditorView.theme(buildBaseThemeStyles())
1193
1454
  );
1194
1455
  const commonExtensions = react.useMemo(() => {
1456
+ const callerCompletions = completionSource ?? (completeAny ? autocomplete.completeAnyWord : autocompletions?.length ? autocompletions : null);
1457
+ const completionEntries = [
1458
+ ...callerCompletions ? [{ autocomplete: callerCompletions }] : [],
1459
+ { autocomplete: variableCompletion }
1460
+ ];
1195
1461
  return [
1196
1462
  getLanguageMode(mode),
1197
1463
  shouldLintJson(mode, readOnly) ? lint.linter(langJson.jsonParseLinter()) : [],
1198
1464
  withLineWrapping ? view.EditorView.lineWrapping : [],
1199
- CodeMirror.EditorState.languageData.of(() => [
1200
- { autocomplete: completeAny ? autocomplete.completeAnyWord : autocompletions }
1201
- ]),
1465
+ CodeMirror.EditorState.languageData.of(() => completionEntries),
1202
1466
  view.keymap.of(
1203
- Object.entries(extraKeys).map(([key, run]) => ({
1467
+ boundKeys.map((key) => ({
1204
1468
  key,
1205
1469
  run: () => {
1206
- if (typeof run === "string") return run;
1207
- run();
1470
+ extraKeysRef.current[key]?.();
1208
1471
  return true;
1209
1472
  }
1210
1473
  }))
1211
1474
  ),
1212
- readOnly ? [] : autocomplete.autocompletion({ activateOnTyping: completeAny }),
1475
+ readOnly ? [] : [
1476
+ // CodeMirror's own default. Gating it on the props hid completions the
1477
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1478
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1479
+ // popup opens anyway.
1480
+ autocomplete.autocompletion()
1481
+ ],
1213
1482
  ariaLabel ? view.EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1483
+ withSearch ? searchExtension() : [],
1484
+ tooltipLayer,
1214
1485
  baseTheme
1215
1486
  ];
1216
1487
  }, [
1217
1488
  autocompletions,
1489
+ completionSource,
1490
+ variableCompletion,
1218
1491
  completeAny,
1219
- extraKeys,
1492
+ boundKeys,
1220
1493
  mode,
1221
1494
  readOnly,
1222
1495
  withLineWrapping,
1223
1496
  baseTheme,
1224
- ariaLabel
1497
+ ariaLabel,
1498
+ withSearch,
1499
+ tooltipLayer
1225
1500
  ]);
1226
1501
  const extensions = react.useMemo(() => {
1227
1502
  return [
1228
1503
  ...commonExtensions,
1229
1504
  view.placeholder(placeholder),
1230
1505
  view.EditorView.editable.of(!readOnly),
1231
- CodeMirror.EditorState.readOnly.of(readOnly),
1506
+ // The lock is this facet and nothing else. `contenteditable` is what the
1507
+ // read-only wash keys off, and `basicSetup` is what holds the undo history,
1508
+ // so leaving both alone is what makes the lock invisible.
1509
+ CodeMirror.EditorState.readOnly.of(readOnly || inputLocked),
1232
1510
  previousValue ? merge.unifiedMergeView({
1233
1511
  original: previousValue,
1234
1512
  mergeControls: false,
@@ -1236,13 +1514,16 @@ var CodeEditor = ({
1236
1514
  collapseUnchanged: withCollapseUnchanged ? {} : void 0
1237
1515
  }) : []
1238
1516
  ];
1239
- }, [commonExtensions, placeholder, readOnly, previousValue, withCollapseUnchanged]);
1517
+ }, [commonExtensions, placeholder, readOnly, inputLocked, previousValue, withCollapseUnchanged]);
1240
1518
  const config = react.useMemo(() => {
1241
1519
  return {
1242
1520
  ...buildBasicSetup(readOnly),
1243
- lineNumbers: withLineNumbers
1521
+ lineNumbers: withLineNumbers,
1522
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1523
+ // second match count over the same document beside the caller's input.
1524
+ ...withSearch ? { searchKeymap: false } : null
1244
1525
  };
1245
- }, [withLineNumbers, readOnly]);
1526
+ }, [withLineNumbers, readOnly, withSearch]);
1246
1527
  const additionalButtons = renderAdditionalButtons?.(value);
1247
1528
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1248
1529
  return (
@@ -1265,6 +1546,8 @@ var CodeEditor = ({
1265
1546
  /* @__PURE__ */ jsxRuntime.jsx(
1266
1547
  CodeMirror__default.default,
1267
1548
  {
1549
+ ref: editorRef,
1550
+ onCreateEditor: handleCreateEditor,
1268
1551
  theme: customTheme,
1269
1552
  value,
1270
1553
  onChange: handleChange,
@@ -1457,8 +1740,6 @@ var activeQueryHighlight = (splitQueries) => {
1457
1740
  })
1458
1741
  ];
1459
1742
  };
1460
-
1461
- // src/components/CodeEditor/highlighting.ts
1462
1743
  var HighlightMarker = class extends view.GutterMarker {
1463
1744
  toDOM() {
1464
1745
  const marker = document.createElement("div");
@@ -1574,125 +1855,18 @@ var highlightedLinesTheme = view.EditorView.baseTheme({
1574
1855
  backgroundColor: `${colors__default.default.blue[100]} !important`
1575
1856
  }
1576
1857
  });
1577
- function highlightingExtension(queryParser) {
1858
+ function highlightingExtension() {
1578
1859
  return [
1579
1860
  highlightedLinesField,
1580
1861
  highlightGutter,
1581
1862
  errorLineDecorations,
1582
1863
  processingLineDecorations,
1583
- highlightedLinesTheme,
1584
- ...queryParser ? activeQueryHighlight(queryParser) : []
1864
+ highlightedLinesTheme
1585
1865
  ];
1586
1866
  }
1587
1867
  function updateHighlighting(view, lines) {
1588
1868
  view.dispatch({ effects: setHighlightedLinesEffect.of(lines ?? null) });
1589
1869
  }
1590
- var setSearchEffect = state.StateEffect.define();
1591
- function findMatches(doc, query) {
1592
- if (!query) return [];
1593
- const text = doc.toString();
1594
- try {
1595
- const regex = new RegExp(escapeStringRegexp(query), "gi");
1596
- return Array.from(text.matchAll(regex)).map((match, index) => {
1597
- const from = match.index;
1598
- const to = from + match[0].length;
1599
- return { from, to, text: match[0], localIndex: index };
1600
- });
1601
- } catch (e) {
1602
- console.warn("Invalid search regex:", e);
1603
- return [];
1604
- }
1605
- }
1606
- function createDecorations(matches, selectedLocalIndex) {
1607
- const decorations = matches.map((match) => {
1608
- const isSelected = match.localIndex === selectedLocalIndex;
1609
- const decoration = view.Decoration.mark({
1610
- class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1611
- });
1612
- return decoration.range(match.from, match.to);
1613
- });
1614
- return view.Decoration.set(decorations, true);
1615
- }
1616
- var searchField = state.StateField.define({
1617
- create() {
1618
- return {
1619
- query: "",
1620
- selectedMatchIndex: void 0,
1621
- matches: [],
1622
- decorations: view.Decoration.none
1623
- };
1624
- },
1625
- update(state, tr) {
1626
- let newState = state;
1627
- for (const effect of tr.effects) {
1628
- if (!effect.is(setSearchEffect)) {
1629
- continue;
1630
- }
1631
- const { query, selectedMatchIndex } = effect.value;
1632
- const matches = findMatches(tr.state.doc, query);
1633
- const decorations = createDecorations(matches, selectedMatchIndex);
1634
- newState = {
1635
- query,
1636
- selectedMatchIndex,
1637
- matches,
1638
- decorations
1639
- };
1640
- }
1641
- if (tr.docChanged && newState.query) {
1642
- const matches = findMatches(tr.state.doc, newState.query);
1643
- const decorations = createDecorations(matches, newState.selectedMatchIndex);
1644
- newState = {
1645
- ...newState,
1646
- matches,
1647
- decorations
1648
- };
1649
- }
1650
- return newState;
1651
- },
1652
- provide: (field) => view.EditorView.decorations.from(field, (state) => state.decorations)
1653
- });
1654
- var searchViewPlugin = view.ViewPlugin.fromClass(
1655
- class {
1656
- lastSelectedIndex;
1657
- update(update) {
1658
- const state = update.state.field(searchField);
1659
- if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1660
- const match = state.matches[state.selectedMatchIndex];
1661
- if (match) {
1662
- requestAnimationFrame(() => {
1663
- update.view.dispatch({
1664
- effects: view.EditorView.scrollIntoView(match.from, { y: "center" })
1665
- });
1666
- });
1667
- }
1668
- this.lastSelectedIndex = state.selectedMatchIndex;
1669
- }
1670
- if (state.selectedMatchIndex === void 0) {
1671
- this.lastSelectedIndex = void 0;
1672
- }
1673
- }
1674
- }
1675
- );
1676
- var searchTheme = view.EditorView.baseTheme({
1677
- ".cm-searchMatch": {
1678
- backgroundColor: colors__default.default.blue[100]
1679
- },
1680
- ".cm-searchMatch > span": {
1681
- color: `${colors__default.default.blue[600]} !important`
1682
- },
1683
- ".cm-searchMatch-selected": {
1684
- backgroundColor: colors__default.default.blue[600]
1685
- },
1686
- ".cm-searchMatch-selected > span": {
1687
- color: `${colors__default.default.white} !important`
1688
- }
1689
- });
1690
- function searchExtension() {
1691
- return [searchField, searchViewPlugin, searchTheme];
1692
- }
1693
- function updateSearch(view, query, selectedMatchIndex) {
1694
- view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1695
- }
1696
1870
  var OWN_KEYMAPS = {
1697
1871
  defaultKeymap: false,
1698
1872
  completionKeymap: false,
@@ -1708,6 +1882,7 @@ var CUSTOM_KEYMAP = [
1708
1882
  }
1709
1883
  }
1710
1884
  ];
1885
+ var OWN_KEYMAP_EXTENSION = CodeMirror.keymap.of([...commands.defaultKeymap, ...CUSTOM_KEYMAP]);
1711
1886
  var buildTheme = () => ({
1712
1887
  ...buildBaseThemeStyles(),
1713
1888
  "&.cm-editor": {
@@ -1745,7 +1920,6 @@ var SqlEditor = ({
1745
1920
  searchQuery = "",
1746
1921
  searchMatchIndex,
1747
1922
  readOnly = false,
1748
- extraKeys,
1749
1923
  queryParser,
1750
1924
  autoFocus = false,
1751
1925
  lineNumbers = true,
@@ -1758,34 +1932,57 @@ var SqlEditor = ({
1758
1932
  dialect
1759
1933
  }) => {
1760
1934
  const editorRef = react.useRef(null);
1935
+ const handleChange = useStableCallback((next) => onChange?.(next));
1936
+ const parseQueries = useStableCallback(async (sql2) => await queryParser?.(sql2) ?? []);
1937
+ const handleBlur = useStableCallback((view) => onBlurEditor?.(view));
1938
+ const handleFocus = useStableCallback((view) => onFocusEditor?.(view));
1939
+ const handleUpdate = useStableCallback((update) => {
1940
+ if (update.selectionSet) {
1941
+ const { main } = update.state.selection;
1942
+ onSelectionChange?.(update.state.sliceDoc(main.from, main.to));
1943
+ }
1944
+ });
1945
+ const variableCompletion = useVariableCompletion(variables);
1946
+ const withQueryParser = queryParser !== void 0;
1947
+ const tooltipLayer = useTooltipsExtension();
1761
1948
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1762
1949
  const themeExtension = useThemedExtension(
1763
1950
  "sql-editor:chrome",
1764
1951
  () => CodeMirror.EditorView.theme(buildTheme())
1765
1952
  );
1766
1953
  const extensions = react.useMemo(() => {
1767
- const baseKeymap = [...commands.defaultKeymap, ...CUSTOM_KEYMAP];
1768
1954
  return [
1769
1955
  CodeMirror.EditorView.lineWrapping,
1770
- CodeMirror.keymap.of(extraKeys ? [...extraKeys, ...baseKeymap] : baseKeymap),
1771
- ...codemirrorLangSql.installExtensions({ schema, defaultDatabase, defaultSchema, variables, dialect }),
1956
+ OWN_KEYMAP_EXTENSION,
1957
+ ...codemirrorLangSql.installExtensions({
1958
+ schema,
1959
+ defaultDatabase,
1960
+ defaultSchema,
1961
+ variableCompletion,
1962
+ dialect
1963
+ }),
1772
1964
  searchExtension(),
1773
- highlightingExtension(queryParser),
1965
+ highlightingExtension(),
1966
+ withQueryParser ? activeQueryHighlight(parseQueries) : [],
1967
+ tooltipLayer,
1774
1968
  themeExtension,
1775
- onBlurEditor ? CodeMirror.EditorView.domEventHandlers({ blur: (_, view) => onBlurEditor(view) }) : [],
1776
- onFocusEditor ? CodeMirror.EditorView.domEventHandlers({ focus: (_, view) => onFocusEditor(view) }) : []
1969
+ CodeMirror.EditorView.domEventHandlers({
1970
+ blur: (_, view) => handleBlur(view),
1971
+ focus: (_, view) => handleFocus(view)
1972
+ })
1777
1973
  ];
1778
1974
  }, [
1779
- extraKeys,
1780
- queryParser,
1975
+ withQueryParser,
1976
+ parseQueries,
1781
1977
  schema,
1782
1978
  defaultDatabase,
1783
1979
  defaultSchema,
1784
- variables,
1980
+ variableCompletion,
1785
1981
  dialect,
1786
- onBlurEditor,
1787
- onFocusEditor,
1788
- themeExtension
1982
+ handleBlur,
1983
+ handleFocus,
1984
+ themeExtension,
1985
+ tooltipLayer
1789
1986
  ]);
1790
1987
  const defaultSetup = react.useMemo(() => {
1791
1988
  return { ...buildBasicSetup(readOnly), ...OWN_KEYMAPS, lineNumbers };
@@ -1800,34 +1997,17 @@ var SqlEditor = ({
1800
1997
  updateSearch(editorRef.current.view, searchQuery, searchMatchIndex);
1801
1998
  }
1802
1999
  }, [searchQuery, searchMatchIndex]);
1803
- react.useEffect(() => {
1804
- if (!onDestroyEditor) return;
1805
- return () => {
1806
- onDestroyEditor();
1807
- };
1808
- }, [onDestroyEditor]);
1809
- const handleOnCreate = react.useCallback(
1810
- (view, state) => {
1811
- onCreateEditor?.(view, state);
1812
- if (highlightLines) {
1813
- updateHighlighting(view, highlightLines);
1814
- }
1815
- if (searchQuery) {
1816
- updateSearch(view, searchQuery, searchMatchIndex);
1817
- }
1818
- },
1819
- [onCreateEditor, highlightLines, searchQuery, searchMatchIndex]
1820
- );
1821
- const handleOnUpdate = react.useCallback(
1822
- (update) => {
1823
- if (update.selectionSet) {
1824
- const { main } = update.state.selection;
1825
- const text = update.state.sliceDoc(main.from, main.to);
1826
- onSelectionChange?.(text);
1827
- }
1828
- },
1829
- [onSelectionChange]
1830
- );
2000
+ const handleDestroy = useStableCallback(() => onDestroyEditor?.());
2001
+ react.useEffect(() => handleDestroy, [handleDestroy]);
2002
+ const handleCreate = useStableCallback((view, state) => {
2003
+ onCreateEditor?.(view, state);
2004
+ if (highlightLines) {
2005
+ updateHighlighting(view, highlightLines);
2006
+ }
2007
+ if (searchQuery) {
2008
+ updateSearch(view, searchQuery, searchMatchIndex);
2009
+ }
2010
+ });
1831
2011
  return /* @__PURE__ */ jsxRuntime.jsx(
1832
2012
  CodeMirror__default.default,
1833
2013
  {
@@ -1835,9 +2015,9 @@ var SqlEditor = ({
1835
2015
  theme: customTheme,
1836
2016
  extensions,
1837
2017
  value,
1838
- onChange,
1839
- onUpdate: handleOnUpdate,
1840
- onCreateEditor: handleOnCreate,
2018
+ onChange: handleChange,
2019
+ onUpdate: handleUpdate,
2020
+ onCreateEditor: handleCreate,
1841
2021
  autoFocus,
1842
2022
  placeholder,
1843
2023
  className,