@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.cjs CHANGED
@@ -26,9 +26,9 @@ var codemirrorThemeGithub = require('@uiw/codemirror-theme-github');
26
26
  var codemirrorLangSfsql = require('@keboola/codemirror-lang-sfsql');
27
27
  var colors = require('@keboola/tailwind-config/colors');
28
28
  var simpleMode = require('@codemirror/legacy-modes/mode/simple-mode');
29
+ var state = require('@codemirror/state');
29
30
  var commands = require('@codemirror/commands');
30
31
  var codemirrorLangSql = require('@keboola/codemirror-lang-sql');
31
- var state = require('@codemirror/state');
32
32
 
33
33
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
34
34
 
@@ -297,6 +297,21 @@ Tooltip.Trigger = TooltipTrigger;
297
297
 
298
298
  // src/utils/escapeStringRegexp.ts
299
299
  var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
300
+
301
+ // src/utils/isMac.ts
302
+ var getIsMac = () => {
303
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
304
+ const platform = navigator.userAgentData.platform;
305
+ if (typeof platform === "string") {
306
+ return platform.toLowerCase() === "macos";
307
+ }
308
+ }
309
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
310
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
311
+ }
312
+ return false;
313
+ };
314
+ var isMac = getIsMac();
300
315
  var helpVariants = classVarianceAuthority.cva(["tw:m-0", "tw:text-xs"], {
301
316
  defaultVariants: {
302
317
  state: "default"
@@ -747,8 +762,6 @@ var ButtonToolbar = ({ value, children }) => {
747
762
  };
748
763
  var COMMANDS = [
749
764
  "run-operation",
750
- "source freshness",
751
- "docs generate",
752
765
  "run",
753
766
  "test",
754
767
  "seed",
@@ -757,9 +770,13 @@ var COMMANDS = [
757
770
  "ls",
758
771
  "compile",
759
772
  "build",
760
- "deps"
773
+ "deps",
774
+ "docs",
775
+ "source"
761
776
  ];
777
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
762
778
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
779
+ var SELECTORS = ["--select", "--exclude", "-s"];
763
780
  var METHODS = [
764
781
  "path:",
765
782
  "source:",
@@ -786,13 +803,59 @@ var FLAGS = [
786
803
  "--profile",
787
804
  "--full-refresh"
788
805
  ];
789
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
790
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
806
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
807
+ const subcommands = SUBCOMMANDS[command];
808
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
809
+ });
810
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
811
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
812
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
813
+ var WORD = /[\w:-]*$/;
814
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
815
+ var DBT = [{ label: "dbt", type: "keyword" }];
816
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
817
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
818
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
819
+ command,
820
+ asOptions(subcommands, "keyword")
821
+ ])
822
+ );
823
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
824
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
825
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
826
+ var optionsAfter = (words) => {
827
+ const [step, command, ...rest] = words;
828
+ if (step === void 0) return DBT;
829
+ if (step.toLowerCase() !== "dbt") return [];
830
+ if (command === void 0) return COMMAND_OPTIONS;
831
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
832
+ if (subcommands && rest.length === 0) return subcommands;
833
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
834
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
835
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
836
+ };
837
+ var complete = (context) => {
838
+ const typed = context.matchBefore(WORD);
839
+ if (!typed?.text && !context.explicit) return null;
840
+ const from = typed?.from ?? context.pos;
841
+ const line = context.state.doc.lineAt(from);
842
+ const options = optionsAfter(
843
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
844
+ );
845
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
846
+ };
791
847
  var dbt = simpleMode.simpleMode({
848
+ languageData: {
849
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
850
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
851
+ autocomplete: complete
852
+ },
792
853
  start: [
793
854
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
794
- { regex: /dbt/, token: "keyword", sol: true },
795
- { regex: word(COMMANDS), token: "keyword" },
855
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
856
+ // colour — the same thing the completion source asks of it.
857
+ { regex: /dbt\b/, token: "keyword", sol: true },
858
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
796
859
  { regex: word(ARGUMENTS), token: "variableName" },
797
860
  { regex: prefix(METHODS), token: "propertyName" },
798
861
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -840,6 +903,8 @@ var KEYWORDS = {
840
903
  };
841
904
  var jsonnet = {
842
905
  name: "jsonnet",
906
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
907
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
843
908
  startState: () => ({
844
909
  cComment: false,
845
910
  textBlock: false,
@@ -1056,6 +1121,20 @@ var buildCustomTheme = () => codemirrorThemeGithub.githubLightInit({
1056
1121
  selection: resolveToken("primary", "31 143 255")
1057
1122
  }
1058
1123
  });
1124
+ var sfsqlWithLanguageData = {
1125
+ ...codemirrorLangSfsql.sfsql,
1126
+ languageData: {
1127
+ ...codemirrorLangSfsql.sfsql.languageData,
1128
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
1129
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
1130
+ autocomplete: autocomplete.completeFromList(
1131
+ Object.keys(codemirrorLangSfsql.modeSpec.keywords).map((label) => ({
1132
+ label: label.toUpperCase(),
1133
+ type: "keyword"
1134
+ }))
1135
+ )
1136
+ }
1137
+ };
1059
1138
  var getLanguageMode = (mode) => {
1060
1139
  switch (mode) {
1061
1140
  case void 0:
@@ -1072,7 +1151,7 @@ var getLanguageMode = (mode) => {
1072
1151
  case "text/x-mssql":
1073
1152
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.MSSQL });
1074
1153
  case "text/x-sfsql":
1075
- return language.StreamLanguage.define(codemirrorLangSfsql.sfsql);
1154
+ return language.StreamLanguage.define(sfsqlWithLanguageData);
1076
1155
  case "text/x-plsql":
1077
1156
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.PLSQL });
1078
1157
  case "text/x-rsrc":
@@ -1097,6 +1176,112 @@ var getLanguageMode = (mode) => {
1097
1176
  }
1098
1177
  };
1099
1178
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1179
+ var setSearchEffect = state.StateEffect.define();
1180
+ function findMatches(doc, query) {
1181
+ if (!query) return [];
1182
+ const text = doc.toString();
1183
+ try {
1184
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1185
+ return Array.from(text.matchAll(regex)).map((match, index) => {
1186
+ const from = match.index;
1187
+ const to = from + match[0].length;
1188
+ return { from, to, text: match[0], localIndex: index };
1189
+ });
1190
+ } catch (e) {
1191
+ console.warn("Invalid search regex:", e);
1192
+ return [];
1193
+ }
1194
+ }
1195
+ function createDecorations(matches, selectedLocalIndex) {
1196
+ const decorations = matches.map((match) => {
1197
+ const isSelected = match.localIndex === selectedLocalIndex;
1198
+ const decoration = view.Decoration.mark({
1199
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1200
+ });
1201
+ return decoration.range(match.from, match.to);
1202
+ });
1203
+ return view.Decoration.set(decorations, true);
1204
+ }
1205
+ var searchField = state.StateField.define({
1206
+ create() {
1207
+ return {
1208
+ query: "",
1209
+ selectedMatchIndex: void 0,
1210
+ matches: [],
1211
+ decorations: view.Decoration.none
1212
+ };
1213
+ },
1214
+ update(state, tr) {
1215
+ let newState = state;
1216
+ for (const effect of tr.effects) {
1217
+ if (!effect.is(setSearchEffect)) {
1218
+ continue;
1219
+ }
1220
+ const { query, selectedMatchIndex } = effect.value;
1221
+ const matches = findMatches(tr.state.doc, query);
1222
+ const decorations = createDecorations(matches, selectedMatchIndex);
1223
+ newState = {
1224
+ query,
1225
+ selectedMatchIndex,
1226
+ matches,
1227
+ decorations
1228
+ };
1229
+ }
1230
+ if (tr.docChanged && newState.query) {
1231
+ const matches = findMatches(tr.state.doc, newState.query);
1232
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1233
+ newState = {
1234
+ ...newState,
1235
+ matches,
1236
+ decorations
1237
+ };
1238
+ }
1239
+ return newState;
1240
+ },
1241
+ provide: (field) => view.EditorView.decorations.from(field, (state) => state.decorations)
1242
+ });
1243
+ var searchViewPlugin = view.ViewPlugin.fromClass(
1244
+ class {
1245
+ lastSelectedIndex;
1246
+ update(update) {
1247
+ const state = update.state.field(searchField);
1248
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1249
+ const match = state.matches[state.selectedMatchIndex];
1250
+ if (match) {
1251
+ requestAnimationFrame(() => {
1252
+ update.view.dispatch({
1253
+ effects: view.EditorView.scrollIntoView(match.from, { y: "center" })
1254
+ });
1255
+ });
1256
+ }
1257
+ this.lastSelectedIndex = state.selectedMatchIndex;
1258
+ }
1259
+ if (state.selectedMatchIndex === void 0) {
1260
+ this.lastSelectedIndex = void 0;
1261
+ }
1262
+ }
1263
+ }
1264
+ );
1265
+ var searchTheme = view.EditorView.baseTheme({
1266
+ ".cm-searchMatch": {
1267
+ backgroundColor: colors__default.default.blue[100]
1268
+ },
1269
+ ".cm-searchMatch > span": {
1270
+ color: `${colors__default.default.blue[600]} !important`
1271
+ },
1272
+ ".cm-searchMatch-selected": {
1273
+ backgroundColor: colors__default.default.blue[600]
1274
+ },
1275
+ ".cm-searchMatch-selected > span": {
1276
+ color: `${colors__default.default.white} !important`
1277
+ }
1278
+ });
1279
+ function searchExtension() {
1280
+ return [searchField, searchViewPlugin, searchTheme];
1281
+ }
1282
+ function updateSearch(view, query, selectedMatchIndex) {
1283
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1284
+ }
1100
1285
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsxRuntime.jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1101
1286
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1102
1287
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1156,11 +1341,78 @@ Kbd.Group = KbdGroup;
1156
1341
  Kbd.Separator = KbdSeparator;
1157
1342
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1158
1343
  "Use ",
1159
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Ctrl+/" }),
1160
- " or ",
1161
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Command+/" }),
1344
+ /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1162
1345
  " to toggle comments."
1163
1346
  ] });
1347
+ 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);
1348
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1349
+ var findTooltipHost = (from) => {
1350
+ let outermostCropper = null;
1351
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1352
+ const style = getComputedStyle(node);
1353
+ if (crops(style)) {
1354
+ outermostCropper = node;
1355
+ } else if (establishesFixedContainingBlock(style)) {
1356
+ return outermostCropper?.parentElement ?? node;
1357
+ }
1358
+ }
1359
+ return outermostCropper?.parentElement ?? null;
1360
+ };
1361
+ var tooltipsExtension = () => {
1362
+ const layer = new state.Compartment();
1363
+ return [
1364
+ layer.of(view.tooltips()),
1365
+ view.ViewPlugin.define((view$1) => {
1366
+ const scheduled = setTimeout(() => {
1367
+ if (!view$1.dom.isConnected) return;
1368
+ const parent = findTooltipHost(view$1.dom);
1369
+ if (parent) view$1.dispatch({ effects: layer.reconfigure(view.tooltips({ parent })) });
1370
+ });
1371
+ return { destroy: () => clearTimeout(scheduled) };
1372
+ })
1373
+ ];
1374
+ };
1375
+ var useTooltipsExtension = () => react.useMemo(tooltipsExtension, []);
1376
+
1377
+ // src/components/CodeEditor/variableCompletion.ts
1378
+ var BOOST = 2e3;
1379
+ var NAME = String.raw`[\w-]*`;
1380
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1381
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1382
+ var OPENING = /\{{1,2} ?$/;
1383
+ var CLOSING = /^ ?\}{1,2}/;
1384
+ var completeVariables = (variables = []) => {
1385
+ if (variables.length === 0) return () => null;
1386
+ const options = variables.map(({ name, value }) => ({
1387
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1388
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1389
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1390
+ label: name,
1391
+ displayLabel: `{{${name}}}`,
1392
+ detail: value,
1393
+ type: "variable",
1394
+ boost: BOOST,
1395
+ apply: (view, completion, from, to) => {
1396
+ const { doc } = view.state;
1397
+ const line = doc.lineAt(from);
1398
+ const insert = `{{${completion.label}}}`;
1399
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1400
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1401
+ const start = opening ? from - opening[0].length : from;
1402
+ const end = closing ? to + closing[0].length : to;
1403
+ view.dispatch({
1404
+ changes: { from: start, to: end, insert },
1405
+ selection: { anchor: start + insert.length }
1406
+ });
1407
+ }
1408
+ }));
1409
+ return (context) => {
1410
+ const match = context.matchBefore(AT_CARET);
1411
+ if (!match?.text) return null;
1412
+ const [name = ""] = NAME_AT_END.exec(match.text) ?? [];
1413
+ return { from: match.to - name.length, options };
1414
+ };
1415
+ };
1164
1416
  var emptyArray = [];
1165
1417
  var emptyObject = {};
1166
1418
  var CodeEditor = ({
@@ -1170,8 +1422,12 @@ var CodeEditor = ({
1170
1422
  mode = "application/json",
1171
1423
  placeholder = "",
1172
1424
  ariaLabel,
1425
+ searchQuery,
1426
+ searchMatchIndex,
1173
1427
  readOnly = false,
1174
1428
  autocompletions = emptyArray,
1429
+ completionSource,
1430
+ variables,
1175
1431
  extraKeys = emptyObject,
1176
1432
  withToggleCommentHint = false,
1177
1433
  withAutofocus = false,
@@ -1186,6 +1442,26 @@ var CodeEditor = ({
1186
1442
  className
1187
1443
  }) => {
1188
1444
  const handleChange = useStableCallback((next) => onChange?.(next));
1445
+ const withSearch = searchQuery !== void 0;
1446
+ const editorRef = react.useRef(null);
1447
+ react.useEffect(() => {
1448
+ const view = editorRef.current?.view;
1449
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1450
+ }, [withSearch, searchQuery, searchMatchIndex]);
1451
+ const handleCreateEditor = useStableCallback((view) => {
1452
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1453
+ });
1454
+ const extraKeysRef = react.useRef(extraKeys);
1455
+ react.useLayoutEffect(() => {
1456
+ extraKeysRef.current = extraKeys;
1457
+ });
1458
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1459
+ const boundKeys = react.useMemo(
1460
+ () => boundKeysSignature.split("\n").filter(Boolean),
1461
+ [boundKeysSignature]
1462
+ );
1463
+ const tooltipLayer = useTooltipsExtension();
1464
+ const variableCompletion = react.useMemo(() => completeVariables(variables), [variables]);
1189
1465
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1190
1466
  const baseTheme = useThemedExtension(
1191
1467
  "code-editor:chrome",
@@ -1197,31 +1473,43 @@ var CodeEditor = ({
1197
1473
  shouldLintJson(mode, readOnly) ? lint.linter(langJson.jsonParseLinter()) : [],
1198
1474
  withLineWrapping ? view.EditorView.lineWrapping : [],
1199
1475
  CodeMirror.EditorState.languageData.of(() => [
1200
- { autocomplete: completeAny ? autocomplete.completeAnyWord : autocompletions }
1476
+ { autocomplete: completionSource ?? (completeAny ? autocomplete.completeAnyWord : autocompletions) }
1201
1477
  ]),
1478
+ CodeMirror.EditorState.languageData.of(() => [{ autocomplete: variableCompletion }]),
1202
1479
  view.keymap.of(
1203
- Object.entries(extraKeys).map(([key, run]) => ({
1480
+ boundKeys.map((key) => ({
1204
1481
  key,
1205
1482
  run: () => {
1206
- if (typeof run === "string") return run;
1207
- run();
1483
+ extraKeysRef.current[key]?.();
1208
1484
  return true;
1209
1485
  }
1210
1486
  }))
1211
1487
  ),
1212
- readOnly ? [] : autocomplete.autocompletion({ activateOnTyping: completeAny }),
1488
+ readOnly ? [] : [
1489
+ // CodeMirror's own default. Gating it on the props hid completions the
1490
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1491
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1492
+ // popup opens anyway.
1493
+ autocomplete.autocompletion()
1494
+ ],
1213
1495
  ariaLabel ? view.EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1496
+ withSearch ? searchExtension() : [],
1497
+ tooltipLayer,
1214
1498
  baseTheme
1215
1499
  ];
1216
1500
  }, [
1217
1501
  autocompletions,
1502
+ completionSource,
1503
+ variableCompletion,
1218
1504
  completeAny,
1219
- extraKeys,
1505
+ boundKeys,
1220
1506
  mode,
1221
1507
  readOnly,
1222
1508
  withLineWrapping,
1223
1509
  baseTheme,
1224
- ariaLabel
1510
+ ariaLabel,
1511
+ withSearch,
1512
+ tooltipLayer
1225
1513
  ]);
1226
1514
  const extensions = react.useMemo(() => {
1227
1515
  return [
@@ -1240,9 +1528,12 @@ var CodeEditor = ({
1240
1528
  const config = react.useMemo(() => {
1241
1529
  return {
1242
1530
  ...buildBasicSetup(readOnly),
1243
- lineNumbers: withLineNumbers
1531
+ lineNumbers: withLineNumbers,
1532
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1533
+ // second match count over the same document beside the caller's input.
1534
+ ...withSearch ? { searchKeymap: false } : null
1244
1535
  };
1245
- }, [withLineNumbers, readOnly]);
1536
+ }, [withLineNumbers, readOnly, withSearch]);
1246
1537
  const additionalButtons = renderAdditionalButtons?.(value);
1247
1538
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1248
1539
  return (
@@ -1265,6 +1556,8 @@ var CodeEditor = ({
1265
1556
  /* @__PURE__ */ jsxRuntime.jsx(
1266
1557
  CodeMirror__default.default,
1267
1558
  {
1559
+ ref: editorRef,
1560
+ onCreateEditor: handleCreateEditor,
1268
1561
  theme: customTheme,
1269
1562
  value,
1270
1563
  onChange: handleChange,
@@ -1587,112 +1880,6 @@ function highlightingExtension(queryParser) {
1587
1880
  function updateHighlighting(view, lines) {
1588
1881
  view.dispatch({ effects: setHighlightedLinesEffect.of(lines ?? null) });
1589
1882
  }
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
1883
  var OWN_KEYMAPS = {
1697
1884
  defaultKeymap: false,
1698
1885
  completionKeymap: false,
@@ -1758,6 +1945,7 @@ var SqlEditor = ({
1758
1945
  dialect
1759
1946
  }) => {
1760
1947
  const editorRef = react.useRef(null);
1948
+ const tooltipLayer = useTooltipsExtension();
1761
1949
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1762
1950
  const themeExtension = useThemedExtension(
1763
1951
  "sql-editor:chrome",
@@ -1768,9 +1956,16 @@ var SqlEditor = ({
1768
1956
  return [
1769
1957
  CodeMirror.EditorView.lineWrapping,
1770
1958
  CodeMirror.keymap.of(extraKeys ? [...extraKeys, ...baseKeymap] : baseKeymap),
1771
- ...codemirrorLangSql.installExtensions({ schema, defaultDatabase, defaultSchema, variables, dialect }),
1959
+ ...codemirrorLangSql.installExtensions({
1960
+ schema,
1961
+ defaultDatabase,
1962
+ defaultSchema,
1963
+ variableCompletion: completeVariables(variables),
1964
+ dialect
1965
+ }),
1772
1966
  searchExtension(),
1773
1967
  highlightingExtension(queryParser),
1968
+ tooltipLayer,
1774
1969
  themeExtension,
1775
1970
  onBlurEditor ? CodeMirror.EditorView.domEventHandlers({ blur: (_, view) => onBlurEditor(view) }) : [],
1776
1971
  onFocusEditor ? CodeMirror.EditorView.domEventHandlers({ focus: (_, view) => onFocusEditor(view) }) : []
@@ -1785,7 +1980,8 @@ var SqlEditor = ({
1785
1980
  dialect,
1786
1981
  onBlurEditor,
1787
1982
  onFocusEditor,
1788
- themeExtension
1983
+ themeExtension,
1984
+ tooltipLayer
1789
1985
  ]);
1790
1986
  const defaultSetup = react.useMemo(() => {
1791
1987
  return { ...buildBasicSetup(readOnly), ...OWN_KEYMAPS, lineNumbers };