@keboola/design 8.0.0 → 9.0.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,9 +1,9 @@
1
- import { createContext, useMemo, useRef, useEffect, useCallback, useLayoutEffect, useSyncExternalStore, useState, use, Children, Fragment as Fragment$1 } from 'react';
2
- import { completeAnyWord, autocompletion } from '@codemirror/autocomplete';
1
+ import { createContext, useRef, useEffect, useLayoutEffect, useMemo, useCallback, useSyncExternalStore, useState, use, Children, Fragment as Fragment$1 } from 'react';
2
+ import { completeFromList, 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';
6
+ import { EditorView, Decoration, ViewPlugin, gutter, GutterMarker, keymap, placeholder, tooltips, gutterLineClass } from '@codemirror/view';
7
7
  import CodeMirror, { EditorState, EditorView as EditorView$1, keymap as keymap$1 } from '@uiw/react-codemirror';
8
8
  import { clsx } from 'clsx';
9
9
  import { extendTailwindMerge } from 'tailwind-merge';
@@ -21,12 +21,12 @@ 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 { modeSpec, sfsql } from '@keboola/codemirror-lang-sfsql';
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
29
  import { installExtensions } from '@keboola/codemirror-lang-sql';
29
- import { StateEffect, StateField, RangeSet, RangeSetBuilder } from '@codemirror/state';
30
30
 
31
31
  // src/components/CodeEditor/CodeEditor.tsx
32
32
 
@@ -290,6 +290,21 @@ Tooltip.Trigger = TooltipTrigger;
290
290
 
291
291
  // src/utils/escapeStringRegexp.ts
292
292
  var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
293
+
294
+ // src/utils/isMac.ts
295
+ var getIsMac = () => {
296
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
297
+ const platform = navigator.userAgentData.platform;
298
+ if (typeof platform === "string") {
299
+ return platform.toLowerCase() === "macos";
300
+ }
301
+ }
302
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
303
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
304
+ }
305
+ return false;
306
+ };
307
+ var isMac = getIsMac();
293
308
  var helpVariants = cva(["tw:m-0", "tw:text-xs"], {
294
309
  defaultVariants: {
295
310
  state: "default"
@@ -740,8 +755,6 @@ var ButtonToolbar = ({ value, children }) => {
740
755
  };
741
756
  var COMMANDS = [
742
757
  "run-operation",
743
- "source freshness",
744
- "docs generate",
745
758
  "run",
746
759
  "test",
747
760
  "seed",
@@ -750,9 +763,13 @@ var COMMANDS = [
750
763
  "ls",
751
764
  "compile",
752
765
  "build",
753
- "deps"
766
+ "deps",
767
+ "docs",
768
+ "source"
754
769
  ];
770
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
755
771
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
772
+ var SELECTORS = ["--select", "--exclude", "-s"];
756
773
  var METHODS = [
757
774
  "path:",
758
775
  "source:",
@@ -779,13 +796,59 @@ var FLAGS = [
779
796
  "--profile",
780
797
  "--full-refresh"
781
798
  ];
782
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
783
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
799
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
800
+ const subcommands = SUBCOMMANDS[command];
801
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
802
+ });
803
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
804
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
805
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
806
+ var WORD = /[\w:-]*$/;
807
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
808
+ var DBT = [{ label: "dbt", type: "keyword" }];
809
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
810
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
811
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
812
+ command,
813
+ asOptions(subcommands, "keyword")
814
+ ])
815
+ );
816
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
817
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
818
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
819
+ var optionsAfter = (words) => {
820
+ const [step, command, ...rest] = words;
821
+ if (step === void 0) return DBT;
822
+ if (step.toLowerCase() !== "dbt") return [];
823
+ if (command === void 0) return COMMAND_OPTIONS;
824
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
825
+ if (subcommands && rest.length === 0) return subcommands;
826
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
827
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
828
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
829
+ };
830
+ var complete = (context) => {
831
+ const typed = context.matchBefore(WORD);
832
+ if (!typed?.text && !context.explicit) return null;
833
+ const from = typed?.from ?? context.pos;
834
+ const line = context.state.doc.lineAt(from);
835
+ const options = optionsAfter(
836
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
837
+ );
838
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
839
+ };
784
840
  var dbt = simpleMode({
841
+ languageData: {
842
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
843
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
844
+ autocomplete: complete
845
+ },
785
846
  start: [
786
847
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
787
- { regex: /dbt/, token: "keyword", sol: true },
788
- { regex: word(COMMANDS), token: "keyword" },
848
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
849
+ // colour — the same thing the completion source asks of it.
850
+ { regex: /dbt\b/, token: "keyword", sol: true },
851
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
789
852
  { regex: word(ARGUMENTS), token: "variableName" },
790
853
  { regex: prefix(METHODS), token: "propertyName" },
791
854
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -833,6 +896,8 @@ var KEYWORDS = {
833
896
  };
834
897
  var jsonnet = {
835
898
  name: "jsonnet",
899
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
900
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
836
901
  startState: () => ({
837
902
  cComment: false,
838
903
  textBlock: false,
@@ -1049,6 +1114,20 @@ var buildCustomTheme = () => githubLightInit({
1049
1114
  selection: resolveToken("primary", "31 143 255")
1050
1115
  }
1051
1116
  });
1117
+ var sfsqlWithLanguageData = {
1118
+ ...sfsql,
1119
+ languageData: {
1120
+ ...sfsql.languageData,
1121
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
1122
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
1123
+ autocomplete: completeFromList(
1124
+ Object.keys(modeSpec.keywords).map((label) => ({
1125
+ label: label.toUpperCase(),
1126
+ type: "keyword"
1127
+ }))
1128
+ )
1129
+ }
1130
+ };
1052
1131
  var getLanguageMode = (mode) => {
1053
1132
  switch (mode) {
1054
1133
  case void 0:
@@ -1065,7 +1144,7 @@ var getLanguageMode = (mode) => {
1065
1144
  case "text/x-mssql":
1066
1145
  return sql({ upperCaseKeywords: true, dialect: MSSQL });
1067
1146
  case "text/x-sfsql":
1068
- return StreamLanguage.define(sfsql);
1147
+ return StreamLanguage.define(sfsqlWithLanguageData);
1069
1148
  case "text/x-plsql":
1070
1149
  return sql({ upperCaseKeywords: true, dialect: PLSQL });
1071
1150
  case "text/x-rsrc":
@@ -1090,6 +1169,112 @@ var getLanguageMode = (mode) => {
1090
1169
  }
1091
1170
  };
1092
1171
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1172
+ var setSearchEffect = StateEffect.define();
1173
+ function findMatches(doc, query) {
1174
+ if (!query) return [];
1175
+ const text = doc.toString();
1176
+ try {
1177
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1178
+ return Array.from(text.matchAll(regex)).map((match, index) => {
1179
+ const from = match.index;
1180
+ const to = from + match[0].length;
1181
+ return { from, to, text: match[0], localIndex: index };
1182
+ });
1183
+ } catch (e) {
1184
+ console.warn("Invalid search regex:", e);
1185
+ return [];
1186
+ }
1187
+ }
1188
+ function createDecorations(matches, selectedLocalIndex) {
1189
+ const decorations = matches.map((match) => {
1190
+ const isSelected = match.localIndex === selectedLocalIndex;
1191
+ const decoration = Decoration.mark({
1192
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1193
+ });
1194
+ return decoration.range(match.from, match.to);
1195
+ });
1196
+ return Decoration.set(decorations, true);
1197
+ }
1198
+ var searchField = StateField.define({
1199
+ create() {
1200
+ return {
1201
+ query: "",
1202
+ selectedMatchIndex: void 0,
1203
+ matches: [],
1204
+ decorations: Decoration.none
1205
+ };
1206
+ },
1207
+ update(state, tr) {
1208
+ let newState = state;
1209
+ for (const effect of tr.effects) {
1210
+ if (!effect.is(setSearchEffect)) {
1211
+ continue;
1212
+ }
1213
+ const { query, selectedMatchIndex } = effect.value;
1214
+ const matches = findMatches(tr.state.doc, query);
1215
+ const decorations = createDecorations(matches, selectedMatchIndex);
1216
+ newState = {
1217
+ query,
1218
+ selectedMatchIndex,
1219
+ matches,
1220
+ decorations
1221
+ };
1222
+ }
1223
+ if (tr.docChanged && newState.query) {
1224
+ const matches = findMatches(tr.state.doc, newState.query);
1225
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1226
+ newState = {
1227
+ ...newState,
1228
+ matches,
1229
+ decorations
1230
+ };
1231
+ }
1232
+ return newState;
1233
+ },
1234
+ provide: (field) => EditorView.decorations.from(field, (state) => state.decorations)
1235
+ });
1236
+ var searchViewPlugin = ViewPlugin.fromClass(
1237
+ class {
1238
+ lastSelectedIndex;
1239
+ update(update) {
1240
+ const state = update.state.field(searchField);
1241
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1242
+ const match = state.matches[state.selectedMatchIndex];
1243
+ if (match) {
1244
+ requestAnimationFrame(() => {
1245
+ update.view.dispatch({
1246
+ effects: EditorView.scrollIntoView(match.from, { y: "center" })
1247
+ });
1248
+ });
1249
+ }
1250
+ this.lastSelectedIndex = state.selectedMatchIndex;
1251
+ }
1252
+ if (state.selectedMatchIndex === void 0) {
1253
+ this.lastSelectedIndex = void 0;
1254
+ }
1255
+ }
1256
+ }
1257
+ );
1258
+ var searchTheme = EditorView.baseTheme({
1259
+ ".cm-searchMatch": {
1260
+ backgroundColor: colors.blue[100]
1261
+ },
1262
+ ".cm-searchMatch > span": {
1263
+ color: `${colors.blue[600]} !important`
1264
+ },
1265
+ ".cm-searchMatch-selected": {
1266
+ backgroundColor: colors.blue[600]
1267
+ },
1268
+ ".cm-searchMatch-selected > span": {
1269
+ color: `${colors.white} !important`
1270
+ }
1271
+ });
1272
+ function searchExtension() {
1273
+ return [searchField, searchViewPlugin, searchTheme];
1274
+ }
1275
+ function updateSearch(view, query, selectedMatchIndex) {
1276
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1277
+ }
1093
1278
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1094
1279
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1095
1280
  return /* @__PURE__ */ jsx(
@@ -1149,11 +1334,78 @@ Kbd.Group = KbdGroup;
1149
1334
  Kbd.Separator = KbdSeparator;
1150
1335
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxs("div", { children: [
1151
1336
  "Use ",
1152
- /* @__PURE__ */ jsx(Kbd, { children: "Ctrl+/" }),
1153
- " or ",
1154
- /* @__PURE__ */ jsx(Kbd, { children: "Command+/" }),
1337
+ /* @__PURE__ */ jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1155
1338
  " to toggle comments."
1156
1339
  ] });
1340
+ 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);
1341
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1342
+ var findTooltipHost = (from) => {
1343
+ let outermostCropper = null;
1344
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1345
+ const style = getComputedStyle(node);
1346
+ if (crops(style)) {
1347
+ outermostCropper = node;
1348
+ } else if (establishesFixedContainingBlock(style)) {
1349
+ return outermostCropper?.parentElement ?? node;
1350
+ }
1351
+ }
1352
+ return outermostCropper?.parentElement ?? null;
1353
+ };
1354
+ var tooltipsExtension = () => {
1355
+ const layer = new Compartment();
1356
+ return [
1357
+ layer.of(tooltips()),
1358
+ ViewPlugin.define((view) => {
1359
+ const scheduled = setTimeout(() => {
1360
+ if (!view.dom.isConnected) return;
1361
+ const parent = findTooltipHost(view.dom);
1362
+ if (parent) view.dispatch({ effects: layer.reconfigure(tooltips({ parent })) });
1363
+ });
1364
+ return { destroy: () => clearTimeout(scheduled) };
1365
+ })
1366
+ ];
1367
+ };
1368
+ var useTooltipsExtension = () => useMemo(tooltipsExtension, []);
1369
+
1370
+ // src/components/CodeEditor/variableCompletion.ts
1371
+ var BOOST = 2e3;
1372
+ var NAME = String.raw`[\w-]*`;
1373
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1374
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1375
+ var OPENING = /\{{1,2} ?$/;
1376
+ var CLOSING = /^ ?\}{1,2}/;
1377
+ var completeVariables = (variables = []) => {
1378
+ if (variables.length === 0) return () => null;
1379
+ const options = variables.map(({ name, value }) => ({
1380
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1381
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1382
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1383
+ label: name,
1384
+ displayLabel: `{{${name}}}`,
1385
+ detail: value,
1386
+ type: "variable",
1387
+ boost: BOOST,
1388
+ apply: (view, completion, from, to) => {
1389
+ const { doc } = view.state;
1390
+ const line = doc.lineAt(from);
1391
+ const insert = `{{${completion.label}}}`;
1392
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1393
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1394
+ const start = opening ? from - opening[0].length : from;
1395
+ const end = closing ? to + closing[0].length : to;
1396
+ view.dispatch({
1397
+ changes: { from: start, to: end, insert },
1398
+ selection: { anchor: start + insert.length }
1399
+ });
1400
+ }
1401
+ }));
1402
+ return (context) => {
1403
+ const match = context.matchBefore(AT_CARET);
1404
+ if (!match?.text) return null;
1405
+ const [name = ""] = NAME_AT_END.exec(match.text) ?? [];
1406
+ return { from: match.to - name.length, options };
1407
+ };
1408
+ };
1157
1409
  var emptyArray = [];
1158
1410
  var emptyObject = {};
1159
1411
  var CodeEditor = ({
@@ -1163,8 +1415,12 @@ var CodeEditor = ({
1163
1415
  mode = "application/json",
1164
1416
  placeholder: placeholder$1 = "",
1165
1417
  ariaLabel,
1418
+ searchQuery,
1419
+ searchMatchIndex,
1166
1420
  readOnly = false,
1167
1421
  autocompletions = emptyArray,
1422
+ completionSource,
1423
+ variables,
1168
1424
  extraKeys = emptyObject,
1169
1425
  withToggleCommentHint = false,
1170
1426
  withAutofocus = false,
@@ -1179,6 +1435,26 @@ var CodeEditor = ({
1179
1435
  className
1180
1436
  }) => {
1181
1437
  const handleChange = useStableCallback((next) => onChange?.(next));
1438
+ const withSearch = searchQuery !== void 0;
1439
+ const editorRef = useRef(null);
1440
+ useEffect(() => {
1441
+ const view = editorRef.current?.view;
1442
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1443
+ }, [withSearch, searchQuery, searchMatchIndex]);
1444
+ const handleCreateEditor = useStableCallback((view) => {
1445
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1446
+ });
1447
+ const extraKeysRef = useRef(extraKeys);
1448
+ useLayoutEffect(() => {
1449
+ extraKeysRef.current = extraKeys;
1450
+ });
1451
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1452
+ const boundKeys = useMemo(
1453
+ () => boundKeysSignature.split("\n").filter(Boolean),
1454
+ [boundKeysSignature]
1455
+ );
1456
+ const tooltipLayer = useTooltipsExtension();
1457
+ const variableCompletion = useMemo(() => completeVariables(variables), [variables]);
1182
1458
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1183
1459
  const baseTheme = useThemedExtension(
1184
1460
  "code-editor:chrome",
@@ -1190,31 +1466,43 @@ var CodeEditor = ({
1190
1466
  shouldLintJson(mode, readOnly) ? linter(jsonParseLinter()) : [],
1191
1467
  withLineWrapping ? EditorView.lineWrapping : [],
1192
1468
  EditorState.languageData.of(() => [
1193
- { autocomplete: completeAny ? completeAnyWord : autocompletions }
1469
+ { autocomplete: completionSource ?? (completeAny ? completeAnyWord : autocompletions) }
1194
1470
  ]),
1471
+ EditorState.languageData.of(() => [{ autocomplete: variableCompletion }]),
1195
1472
  keymap.of(
1196
- Object.entries(extraKeys).map(([key, run]) => ({
1473
+ boundKeys.map((key) => ({
1197
1474
  key,
1198
1475
  run: () => {
1199
- if (typeof run === "string") return run;
1200
- run();
1476
+ extraKeysRef.current[key]?.();
1201
1477
  return true;
1202
1478
  }
1203
1479
  }))
1204
1480
  ),
1205
- readOnly ? [] : autocompletion({ activateOnTyping: completeAny }),
1481
+ readOnly ? [] : [
1482
+ // CodeMirror's own default. Gating it on the props hid completions the
1483
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1484
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1485
+ // popup opens anyway.
1486
+ autocompletion()
1487
+ ],
1206
1488
  ariaLabel ? EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1489
+ withSearch ? searchExtension() : [],
1490
+ tooltipLayer,
1207
1491
  baseTheme
1208
1492
  ];
1209
1493
  }, [
1210
1494
  autocompletions,
1495
+ completionSource,
1496
+ variableCompletion,
1211
1497
  completeAny,
1212
- extraKeys,
1498
+ boundKeys,
1213
1499
  mode,
1214
1500
  readOnly,
1215
1501
  withLineWrapping,
1216
1502
  baseTheme,
1217
- ariaLabel
1503
+ ariaLabel,
1504
+ withSearch,
1505
+ tooltipLayer
1218
1506
  ]);
1219
1507
  const extensions = useMemo(() => {
1220
1508
  return [
@@ -1233,9 +1521,12 @@ var CodeEditor = ({
1233
1521
  const config = useMemo(() => {
1234
1522
  return {
1235
1523
  ...buildBasicSetup(readOnly),
1236
- lineNumbers: withLineNumbers
1524
+ lineNumbers: withLineNumbers,
1525
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1526
+ // second match count over the same document beside the caller's input.
1527
+ ...withSearch ? { searchKeymap: false } : null
1237
1528
  };
1238
- }, [withLineNumbers, readOnly]);
1529
+ }, [withLineNumbers, readOnly, withSearch]);
1239
1530
  const additionalButtons = renderAdditionalButtons?.(value);
1240
1531
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1241
1532
  return (
@@ -1258,6 +1549,8 @@ var CodeEditor = ({
1258
1549
  /* @__PURE__ */ jsx(
1259
1550
  CodeMirror,
1260
1551
  {
1552
+ ref: editorRef,
1553
+ onCreateEditor: handleCreateEditor,
1261
1554
  theme: customTheme,
1262
1555
  value,
1263
1556
  onChange: handleChange,
@@ -1580,112 +1873,6 @@ function highlightingExtension(queryParser) {
1580
1873
  function updateHighlighting(view, lines) {
1581
1874
  view.dispatch({ effects: setHighlightedLinesEffect.of(lines ?? null) });
1582
1875
  }
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
1876
  var OWN_KEYMAPS = {
1690
1877
  defaultKeymap: false,
1691
1878
  completionKeymap: false,
@@ -1751,6 +1938,7 @@ var SqlEditor = ({
1751
1938
  dialect
1752
1939
  }) => {
1753
1940
  const editorRef = useRef(null);
1941
+ const tooltipLayer = useTooltipsExtension();
1754
1942
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1755
1943
  const themeExtension = useThemedExtension(
1756
1944
  "sql-editor:chrome",
@@ -1761,9 +1949,16 @@ var SqlEditor = ({
1761
1949
  return [
1762
1950
  EditorView$1.lineWrapping,
1763
1951
  keymap$1.of(extraKeys ? [...extraKeys, ...baseKeymap] : baseKeymap),
1764
- ...installExtensions({ schema, defaultDatabase, defaultSchema, variables, dialect }),
1952
+ ...installExtensions({
1953
+ schema,
1954
+ defaultDatabase,
1955
+ defaultSchema,
1956
+ variableCompletion: completeVariables(variables),
1957
+ dialect
1958
+ }),
1765
1959
  searchExtension(),
1766
1960
  highlightingExtension(queryParser),
1961
+ tooltipLayer,
1767
1962
  themeExtension,
1768
1963
  onBlurEditor ? EditorView$1.domEventHandlers({ blur: (_, view) => onBlurEditor(view) }) : [],
1769
1964
  onFocusEditor ? EditorView$1.domEventHandlers({ focus: (_, view) => onFocusEditor(view) }) : []
@@ -1778,7 +1973,8 @@ var SqlEditor = ({
1778
1973
  dialect,
1779
1974
  onBlurEditor,
1780
1975
  onFocusEditor,
1781
- themeExtension
1976
+ themeExtension,
1977
+ tooltipLayer
1782
1978
  ]);
1783
1979
  const defaultSetup = useMemo(() => {
1784
1980
  return { ...buildBasicSetup(readOnly), ...OWN_KEYMAPS, lineNumbers };