elastic-input 0.3.5 → 0.3.7

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/README.md CHANGED
@@ -487,6 +487,7 @@ formatQuery(query, { maxLineLength: 80, indent: '\t' });
487
487
  |--------|------|---------|-------------|
488
488
  | `maxLineLength` | `number` | `60` | Lines shorter than this stay inline |
489
489
  | `indent` | `string` | `' '` (2 spaces) | Indent string per nesting level |
490
+ | `whitespaceOperator` | `string` | — | Replace implicit AND (whitespace) with this operator (e.g. `'AND'`, `'&&'`) |
490
491
 
491
492
  ## Requirements
492
493
 
@@ -1,5 +1,5 @@
1
1
  import { Suggestion } from '../autocomplete/suggestionTypes';
2
- import { ColorConfig, StyleConfig, HistoryEntry, SavedSearch } from '../types';
2
+ import { ColorConfig, StyleConfig, HistoryEntry, SavedSearch, SuggestionItem } from '../types';
3
3
  import { CursorContext } from '../parser/Parser';
4
4
  import * as React from 'react';
5
5
  interface AutocompleteDropdownProps {
@@ -21,6 +21,8 @@ interface AutocompleteDropdownProps {
21
21
  cursorContext?: CursorContext | null;
22
22
  /** Ref callback to expose the dropdown list element for page-size calculations. */
23
23
  listRefCallback?: (el: HTMLDivElement | null) => void;
24
+ /** Controls the type badge in dropdown items. false=hide, true=default, callback=custom. */
25
+ renderType?: boolean | ((type: string, suggestion: SuggestionItem) => React.ReactNode | null | undefined);
24
26
  /** Custom class names for dropdown elements. */
25
27
  classNames?: {
26
28
  dropdown?: string;
@@ -28,5 +30,5 @@ interface AutocompleteDropdownProps {
28
30
  dropdownItem?: string;
29
31
  };
30
32
  }
31
- export declare function AutocompleteDropdown({ suggestions, selectedIndex, onSelect, position, colors, styles, visible, fixedWidth, renderHistoryItem, renderSavedSearchItem, renderDropdownHeader, cursorContext, listRefCallback, classNames, }: AutocompleteDropdownProps): React.ReactPortal | null;
33
+ export declare function AutocompleteDropdown({ suggestions, selectedIndex, onSelect, position, colors, styles, visible, fixedWidth, renderHistoryItem, renderSavedSearchItem, renderDropdownHeader, cursorContext, listRefCallback, renderType, classNames, }: AutocompleteDropdownProps): React.ReactPortal | null;
32
34
  export {};
@@ -444,6 +444,7 @@ class Parser {
444
444
  operator: "AND",
445
445
  left: result,
446
446
  right,
447
+ implicit: true,
447
448
  start: result.start,
448
449
  end: right.end
449
450
  };
@@ -526,6 +527,7 @@ class Parser {
526
527
  operator: "AND",
527
528
  left,
528
529
  right,
530
+ implicit: true,
529
531
  start: left.start,
530
532
  end: right.end
531
533
  };
@@ -1215,6 +1217,7 @@ const DARK_COLORS = {
1215
1217
  const DEFAULT_STYLES = {
1216
1218
  fontFamily: "'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace",
1217
1219
  fontSize: "14px",
1220
+ lineHeight: "1.5",
1218
1221
  inputMinHeight: "40px",
1219
1222
  inputPadding: "8px 12px",
1220
1223
  inputBorderWidth: "2px",
@@ -2125,7 +2128,7 @@ function getEditableStyle(colors, styles) {
2125
2128
  outline: "none",
2126
2129
  fontSize: styles.fontSize,
2127
2130
  fontFamily: styles.fontFamily,
2128
- lineHeight: "1.5",
2131
+ lineHeight: styles.lineHeight,
2129
2132
  backgroundColor: colors.background,
2130
2133
  color: colors.text,
2131
2134
  caretColor: colors.cursor,
@@ -2153,7 +2156,7 @@ function getPlaceholderStyle(colors, styles) {
2153
2156
  pointerEvents: "none",
2154
2157
  fontSize: styles.fontSize,
2155
2158
  fontFamily: styles.fontFamily,
2156
- lineHeight: "1.5",
2159
+ lineHeight: styles.lineHeight,
2157
2160
  userSelect: "none"
2158
2161
  };
2159
2162
  }
@@ -2750,6 +2753,7 @@ function AutocompleteDropdown({
2750
2753
  renderDropdownHeader,
2751
2754
  cursorContext,
2752
2755
  listRefCallback,
2756
+ renderType,
2753
2757
  classNames
2754
2758
  }) {
2755
2759
  const portalRef = React.useRef(null);
@@ -2821,6 +2825,9 @@ function AutocompleteDropdown({
2821
2825
  if (suggestion.type === "error") {
2822
2826
  return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--error", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.8 } }, /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-label", style: { ...getDropdownItemLabelStyle(), color: mergedColors.error } }, suggestion.label || "Error loading suggestions"));
2823
2827
  }
2828
+ if (suggestion.type === "noResults") {
2829
+ return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--no-results", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.7 } }, suggestion.customContent);
2830
+ }
2824
2831
  if (suggestion.type === "loading") {
2825
2832
  return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--loading", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.6, justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-label", style: { ...getDropdownItemLabelStyle(), fontStyle: "italic" } }, suggestion.label || "Searching..."), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-spinner", style: { marginLeft: "6px", display: "inline-block", animation: "elastic-input-spin 1s linear infinite", width: "14px", height: "14px", border: "2px solid", borderColor: `${mergedColors.placeholder} transparent ${mergedColors.placeholder} transparent`, borderRadius: "50%" } }));
2826
2833
  }
@@ -2897,7 +2904,10 @@ function AutocompleteDropdown({
2897
2904
  },
2898
2905
  /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-label", style: getDropdownItemLabelStyle() }, highlightMatch(suggestion.label, suggestion.matchPartial, isSelected)),
2899
2906
  suggestion.description && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-desc", style: getDropdownItemDescStyle() }, suggestion.description),
2900
- suggestion.type && suggestion.type !== "hint" && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: getDropdownItemTypeStyle(isSelected, mergedStyles) }, suggestion.type)
2907
+ renderType !== false && suggestion.type && suggestion.type !== "hint" && (() => {
2908
+ const content2 = typeof renderType === "function" ? renderType(suggestion.type, { text: suggestion.text, label: suggestion.label, description: suggestion.description, type: suggestion.type }) : suggestion.type;
2909
+ return content2 != null ? /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: getDropdownItemTypeStyle(isSelected, mergedStyles) }, content2) : null;
2910
+ })()
2901
2911
  );
2902
2912
  }));
2903
2913
  return ReactDOM.createPortal(content, portalRef.current);
@@ -3626,6 +3636,155 @@ function dedup(ranges) {
3626
3636
  }
3627
3637
  return result;
3628
3638
  }
3639
+ const DEFAULT_MAX_LINE_LENGTH = 60;
3640
+ const DEFAULT_INDENT = " ";
3641
+ function formatQuery(input, options) {
3642
+ const maxLineLength = (options == null ? void 0 : options.maxLineLength) ?? DEFAULT_MAX_LINE_LENGTH;
3643
+ const indent = (options == null ? void 0 : options.indent) ?? DEFAULT_INDENT;
3644
+ const whitespaceOperator = options == null ? void 0 : options.whitespaceOperator;
3645
+ let ast;
3646
+ if (typeof input === "string") {
3647
+ const tokens = new Lexer(input, { savedSearches: true, historySearch: true }).tokenize();
3648
+ const parser = new Parser(tokens);
3649
+ ast = parser.parse();
3650
+ } else {
3651
+ ast = input;
3652
+ }
3653
+ if (!ast) return typeof input === "string" ? input : "";
3654
+ return printNode(ast, 0, maxLineLength, indent, whitespaceOperator);
3655
+ }
3656
+ function resolveOperator(node, whitespaceOperator) {
3657
+ if (node.implicit) return whitespaceOperator ?? "";
3658
+ return node.operator;
3659
+ }
3660
+ function inline(node, whitespaceOperator) {
3661
+ switch (node.type) {
3662
+ case "FieldValue": {
3663
+ const val = node.quoted ? `"${node.value}"` : node.value;
3664
+ const op = node.operator === ":" ? ":" : `:${node.operator}`;
3665
+ let s = `${node.field}${op}${val}`;
3666
+ if (node.fuzzy != null) s += `~${node.fuzzy}`;
3667
+ if (node.proximity != null) s += `~${node.proximity}`;
3668
+ if (node.boost != null) s += `^${node.boost}`;
3669
+ return s;
3670
+ }
3671
+ case "BareTerm": {
3672
+ let s = node.quoted ? `"${node.value}"` : node.value;
3673
+ if (node.fuzzy != null) s += `~${node.fuzzy}`;
3674
+ if (node.proximity != null) s += `~${node.proximity}`;
3675
+ if (node.boost != null) s += `^${node.boost}`;
3676
+ return s;
3677
+ }
3678
+ case "Range": {
3679
+ const lb = node.lowerInclusive ? "[" : "{";
3680
+ const rb = node.upperInclusive ? "]" : "}";
3681
+ const lower = node.lowerQuoted ? `"${node.lower}"` : node.lower;
3682
+ const upper = node.upperQuoted ? `"${node.upper}"` : node.upper;
3683
+ const range = `${lb}${lower} TO ${upper}${rb}`;
3684
+ return node.field ? `${node.field}:${range}` : range;
3685
+ }
3686
+ case "Regex":
3687
+ return `/${node.pattern}/`;
3688
+ case "SavedSearch":
3689
+ return `#${node.name}`;
3690
+ case "HistoryRef":
3691
+ return `!${node.ref}`;
3692
+ case "Not":
3693
+ return `NOT ${inline(node.expression, whitespaceOperator)}`;
3694
+ case "Group": {
3695
+ let s = `(${inline(node.expression, whitespaceOperator)})`;
3696
+ if (node.boost != null) s += `^${node.boost}`;
3697
+ return s;
3698
+ }
3699
+ case "FieldGroup": {
3700
+ let s = `${node.field}:(${inline(node.expression, whitespaceOperator)})`;
3701
+ if (node.boost != null) s += `^${node.boost}`;
3702
+ return s;
3703
+ }
3704
+ case "BooleanExpr": {
3705
+ const op = resolveOperator(node, whitespaceOperator);
3706
+ const sep = op ? ` ${op} ` : " ";
3707
+ return `${inline(node.left, whitespaceOperator)}${sep}${inline(node.right, whitespaceOperator)}`;
3708
+ }
3709
+ case "Error":
3710
+ return node.value;
3711
+ }
3712
+ }
3713
+ function flattenChain(node) {
3714
+ const op = node.operator;
3715
+ const implicit = !!node.implicit;
3716
+ const operands = [];
3717
+ const collect = (n) => {
3718
+ if (n.type === "BooleanExpr" && n.operator === op && !!n.implicit === implicit) {
3719
+ collect(n.left);
3720
+ collect(n.right);
3721
+ } else {
3722
+ operands.push(n);
3723
+ }
3724
+ };
3725
+ collect(node);
3726
+ return { operator: op, implicit, operands };
3727
+ }
3728
+ function containsGroups(node) {
3729
+ if (node.type === "Group" || node.type === "FieldGroup") return true;
3730
+ if (node.type === "BooleanExpr") return containsGroups(node.left) || containsGroups(node.right);
3731
+ if (node.type === "Not") return containsGroups(node.expression);
3732
+ return false;
3733
+ }
3734
+ function shouldBreakGroup(expr, maxLineLength) {
3735
+ const inlined = inline(expr);
3736
+ if (inlined.length > maxLineLength) return true;
3737
+ if (containsGroups(expr)) return true;
3738
+ return false;
3739
+ }
3740
+ function printNode(node, depth, maxLineLength, indent, whitespaceOperator) {
3741
+ const pad = indent.repeat(depth);
3742
+ switch (node.type) {
3743
+ case "BooleanExpr": {
3744
+ const { operator, implicit, operands } = flattenChain(node);
3745
+ const displayOp = implicit ? whitespaceOperator ?? "" : operator;
3746
+ const sep = displayOp ? ` ${displayOp} ` : " ";
3747
+ const inlined = operands.map((o) => inline(o, whitespaceOperator)).join(sep);
3748
+ if (inlined.length <= maxLineLength) {
3749
+ return inlined;
3750
+ }
3751
+ const lines = operands.map((operand, i) => {
3752
+ const printed = printNode(operand, depth, maxLineLength, indent, whitespaceOperator);
3753
+ if (i === 0) return printed;
3754
+ return displayOp ? `${pad}${displayOp} ${printed}` : `${pad}${printed}`;
3755
+ });
3756
+ return lines.join("\n");
3757
+ }
3758
+ case "Group": {
3759
+ if (!shouldBreakGroup(node.expression, maxLineLength)) {
3760
+ let s2 = `(${inline(node.expression, whitespaceOperator)})`;
3761
+ if (node.boost != null) s2 += `^${node.boost}`;
3762
+ return s2;
3763
+ }
3764
+ const inner = printNode(node.expression, depth + 1, maxLineLength, indent, whitespaceOperator);
3765
+ let s = `(
3766
+ ${indentLines(inner, depth + 1, indent)}
3767
+ ${pad})`;
3768
+ if (node.boost != null) s += `^${node.boost}`;
3769
+ return s;
3770
+ }
3771
+ case "FieldGroup": {
3772
+ let s = `${node.field}:(${inline(node.expression, whitespaceOperator)})`;
3773
+ if (node.boost != null) s += `^${node.boost}`;
3774
+ return s;
3775
+ }
3776
+ case "Not":
3777
+ return `NOT ${printNode(node.expression, depth, maxLineLength, indent, whitespaceOperator)}`;
3778
+ default:
3779
+ return inline(node, whitespaceOperator);
3780
+ }
3781
+ }
3782
+ function indentLines(text, depth, indent) {
3783
+ const pad = indent.repeat(depth);
3784
+ return text.split("\n").map((line) => {
3785
+ return line.startsWith(pad) ? line : pad + line;
3786
+ }).join("\n");
3787
+ }
3629
3788
  class UndoStack {
3630
3789
  constructor(maxSize = 100) {
3631
3790
  __publicField(this, "stack", []);
@@ -3806,12 +3965,15 @@ function ElasticInput(props) {
3806
3965
  const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
3807
3966
  const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
3808
3967
  const renderDropdownHeader = dropdownConfig == null ? void 0 : dropdownConfig.renderHeader;
3968
+ const renderNoResults = dropdownConfig == null ? void 0 : dropdownConfig.renderNoResults;
3969
+ const renderType = dropdownConfig == null ? void 0 : dropdownConfig.renderType;
3809
3970
  const autoSelect = (dropdownConfig == null ? void 0 : dropdownConfig.autoSelect) ?? false;
3810
3971
  const homeEndKeys = (dropdownConfig == null ? void 0 : dropdownConfig.homeEndKeys) ?? false;
3811
3972
  const multiline = (featuresConfig == null ? void 0 : featuresConfig.multiline) !== false;
3812
3973
  const smartSelectAll = (featuresConfig == null ? void 0 : featuresConfig.smartSelectAll) ?? false;
3813
3974
  const expandSelection = (featuresConfig == null ? void 0 : featuresConfig.expandSelection) ?? false;
3814
3975
  const wildcardWrap = (featuresConfig == null ? void 0 : featuresConfig.wildcardWrap) ?? false;
3976
+ const enableFormatQuery = (featuresConfig == null ? void 0 : featuresConfig.formatQuery) ?? false;
3815
3977
  const lexerOptions = React.useMemo(() => ({
3816
3978
  savedSearches: enableSavedSearches,
3817
3979
  historySearch: enableHistorySearch
@@ -4044,6 +4206,24 @@ function ElasticInput(props) {
4044
4206
  return { ...s, customContent: custom };
4045
4207
  });
4046
4208
  }, [renderFieldHint]);
4209
+ const tryShowNoResults = React.useCallback((context) => {
4210
+ if (!renderNoResults) return false;
4211
+ const content = renderNoResults({ cursorContext: context, partial: context.partial });
4212
+ if (content == null) return false;
4213
+ const noResultsSuggestion = {
4214
+ text: "",
4215
+ label: "",
4216
+ type: "noResults",
4217
+ customContent: content,
4218
+ replaceStart: 0,
4219
+ replaceEnd: 0
4220
+ };
4221
+ setSuggestions([noResultsSuggestion]);
4222
+ setSelectedSuggestionIndex(-1);
4223
+ setShowDatePicker(false);
4224
+ showDropdownAtPosition(32, 300);
4225
+ return true;
4226
+ }, [renderNoResults]);
4047
4227
  const updateSuggestionsFromTokens = React.useCallback((toks, offset) => {
4048
4228
  var _a, _b, _c;
4049
4229
  const result = engineRef.current.getSuggestions(toks, offset);
@@ -4138,10 +4318,12 @@ function ElasticInput(props) {
4138
4318
  setAutocompleteContext(contextType);
4139
4319
  showDropdownAtPosition(newSuggestions.length * 32, 300);
4140
4320
  } else {
4141
- setShowDropdown(false);
4142
- setShowDatePicker(false);
4143
- setSuggestions([]);
4144
4321
  setAutocompleteContext(contextType);
4322
+ if (!tryShowNoResults(result.context)) {
4323
+ setShowDropdown(false);
4324
+ setShowDatePicker(false);
4325
+ setSuggestions([]);
4326
+ }
4145
4327
  }
4146
4328
  } else {
4147
4329
  const token = result.context.token;
@@ -4237,17 +4419,20 @@ function ElasticInput(props) {
4237
4419
  showDropdownAtPosition(mapped.length * 32, 300);
4238
4420
  } else {
4239
4421
  const syncResult = engineRef.current.getSuggestions(stateRef.current.tokens, stateRef.current.cursorOffset);
4240
- const hintSuggestions = applyFieldHint(
4241
- syncResult.suggestions.filter((s) => s.type === "hint"),
4242
- syncResult.context
4243
- );
4244
- if (hintSuggestions.length > 0) {
4245
- setSuggestions(hintSuggestions);
4246
- setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
4247
- showDropdownAtPosition(hintSuggestions.length * 32, 300);
4422
+ if (partial && tryShowNoResults(syncResult.context)) {
4248
4423
  } else {
4249
- setShowDropdown(false);
4250
- setSuggestions([]);
4424
+ const hintSuggestions = applyFieldHint(
4425
+ syncResult.suggestions.filter((s) => s.type === "hint"),
4426
+ syncResult.context
4427
+ );
4428
+ if (hintSuggestions.length > 0) {
4429
+ setSuggestions(hintSuggestions);
4430
+ setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
4431
+ showDropdownAtPosition(hintSuggestions.length * 32, 300);
4432
+ } else if (!tryShowNoResults(syncResult.context)) {
4433
+ setShowDropdown(false);
4434
+ setSuggestions([]);
4435
+ }
4251
4436
  }
4252
4437
  }
4253
4438
  } catch (e) {
@@ -4272,7 +4457,7 @@ function ElasticInput(props) {
4272
4457
  }
4273
4458
  }, debounceMs);
4274
4459
  }
4275
- }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay, autoSelect]);
4460
+ }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay, autoSelect, tryShowNoResults]);
4276
4461
  updateSuggestionsRef.current = updateSuggestionsFromTokens;
4277
4462
  const closeDropdown = React.useCallback(() => {
4278
4463
  var _a;
@@ -4636,6 +4821,14 @@ function ElasticInput(props) {
4636
4821
  if (onKeyDownProp) onKeyDownProp(e);
4637
4822
  if (e.defaultPrevented) return;
4638
4823
  const s = stateRef.current;
4824
+ if (enableFormatQuery && e.key === "F" && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey) {
4825
+ e.preventDefault();
4826
+ const formatted = formatQuery(currentValueRef.current);
4827
+ if (formatted !== currentValueRef.current) {
4828
+ applyNewValue(formatted, formatted.length);
4829
+ }
4830
+ return;
4831
+ }
4639
4832
  if (editorRef.current && editorRef.current.childNodes.length > 40) {
4640
4833
  const sel = window.getSelection();
4641
4834
  const hasSelection = sel != null && !sel.isCollapsed;
@@ -4932,7 +5125,7 @@ function ElasticInput(props) {
4932
5125
  if (onSearch) onSearch(currentValueRef.current, s.ast);
4933
5126
  return;
4934
5127
  }
4935
- }, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection, homeEndKeys, getDropdownPageSize]);
5128
+ }, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection, homeEndKeys, getDropdownPageSize, enableFormatQuery]);
4936
5129
  const handleKeyUp = React.useCallback((e) => {
4937
5130
  if (keyConsumedByDropdownRef.current) {
4938
5131
  keyConsumedByDropdownRef.current = false;
@@ -5084,6 +5277,7 @@ function ElasticInput(props) {
5084
5277
  listRefCallback: (el) => {
5085
5278
  dropdownListRef.current = el;
5086
5279
  },
5280
+ renderType,
5087
5281
  classNames: classNames ? { dropdown: classNames.dropdown, dropdownHeader: classNames.dropdownHeader, dropdownItem: classNames.dropdownItem } : void 0
5088
5282
  }
5089
5283
  ), showDatePicker && dropdownPosition ? /* @__PURE__ */ React.createElement(
@@ -5175,143 +5369,6 @@ function walk(node, groupField, out) {
5175
5369
  break;
5176
5370
  }
5177
5371
  }
5178
- const DEFAULT_MAX_LINE_LENGTH = 60;
5179
- const DEFAULT_INDENT = " ";
5180
- function formatQuery(input, options) {
5181
- const maxLineLength = (options == null ? void 0 : options.maxLineLength) ?? DEFAULT_MAX_LINE_LENGTH;
5182
- const indent = (options == null ? void 0 : options.indent) ?? DEFAULT_INDENT;
5183
- let ast;
5184
- if (typeof input === "string") {
5185
- const tokens = new Lexer(input, { savedSearches: true, historySearch: true }).tokenize();
5186
- const parser = new Parser(tokens);
5187
- ast = parser.parse();
5188
- } else {
5189
- ast = input;
5190
- }
5191
- if (!ast) return typeof input === "string" ? input : "";
5192
- return printNode(ast, 0, maxLineLength, indent);
5193
- }
5194
- function inline(node) {
5195
- switch (node.type) {
5196
- case "FieldValue": {
5197
- const val = node.quoted ? `"${node.value}"` : node.value;
5198
- const op = node.operator === ":" ? ":" : `:${node.operator}`;
5199
- let s = `${node.field}${op}${val}`;
5200
- if (node.fuzzy != null) s += `~${node.fuzzy}`;
5201
- if (node.proximity != null) s += `~${node.proximity}`;
5202
- if (node.boost != null) s += `^${node.boost}`;
5203
- return s;
5204
- }
5205
- case "BareTerm": {
5206
- let s = node.quoted ? `"${node.value}"` : node.value;
5207
- if (node.fuzzy != null) s += `~${node.fuzzy}`;
5208
- if (node.proximity != null) s += `~${node.proximity}`;
5209
- if (node.boost != null) s += `^${node.boost}`;
5210
- return s;
5211
- }
5212
- case "Range": {
5213
- const lb = node.lowerInclusive ? "[" : "{";
5214
- const rb = node.upperInclusive ? "]" : "}";
5215
- const lower = node.lowerQuoted ? `"${node.lower}"` : node.lower;
5216
- const upper = node.upperQuoted ? `"${node.upper}"` : node.upper;
5217
- const range = `${lb}${lower} TO ${upper}${rb}`;
5218
- return node.field ? `${node.field}:${range}` : range;
5219
- }
5220
- case "Regex":
5221
- return `/${node.pattern}/`;
5222
- case "SavedSearch":
5223
- return `#${node.name}`;
5224
- case "HistoryRef":
5225
- return `!${node.ref}`;
5226
- case "Not":
5227
- return `NOT ${inline(node.expression)}`;
5228
- case "Group": {
5229
- let s = `(${inline(node.expression)})`;
5230
- if (node.boost != null) s += `^${node.boost}`;
5231
- return s;
5232
- }
5233
- case "FieldGroup": {
5234
- let s = `${node.field}:(${inline(node.expression)})`;
5235
- if (node.boost != null) s += `^${node.boost}`;
5236
- return s;
5237
- }
5238
- case "BooleanExpr":
5239
- return `${inline(node.left)} ${node.operator} ${inline(node.right)}`;
5240
- case "Error":
5241
- return node.value;
5242
- }
5243
- }
5244
- function flattenChain(node) {
5245
- const op = node.operator;
5246
- const operands = [];
5247
- const collect = (n) => {
5248
- if (n.type === "BooleanExpr" && n.operator === op) {
5249
- collect(n.left);
5250
- collect(n.right);
5251
- } else {
5252
- operands.push(n);
5253
- }
5254
- };
5255
- collect(node);
5256
- return { operator: op, operands };
5257
- }
5258
- function containsGroups(node) {
5259
- if (node.type === "Group" || node.type === "FieldGroup") return true;
5260
- if (node.type === "BooleanExpr") return containsGroups(node.left) || containsGroups(node.right);
5261
- if (node.type === "Not") return containsGroups(node.expression);
5262
- return false;
5263
- }
5264
- function shouldBreakGroup(expr, maxLineLength) {
5265
- const inlined = inline(expr);
5266
- if (inlined.length > maxLineLength) return true;
5267
- if (containsGroups(expr)) return true;
5268
- return false;
5269
- }
5270
- function printNode(node, depth, maxLineLength, indent) {
5271
- const pad = indent.repeat(depth);
5272
- switch (node.type) {
5273
- case "BooleanExpr": {
5274
- const { operator, operands } = flattenChain(node);
5275
- const inlined = operands.map((o) => inline(o)).join(` ${operator} `);
5276
- if (inlined.length <= maxLineLength) {
5277
- return inlined;
5278
- }
5279
- const lines = operands.map((operand, i) => {
5280
- const printed = printNode(operand, depth, maxLineLength, indent);
5281
- return i === 0 ? printed : `${pad}${operator} ${printed}`;
5282
- });
5283
- return lines.join("\n");
5284
- }
5285
- case "Group": {
5286
- if (!shouldBreakGroup(node.expression, maxLineLength)) {
5287
- let s2 = `(${inline(node.expression)})`;
5288
- if (node.boost != null) s2 += `^${node.boost}`;
5289
- return s2;
5290
- }
5291
- const inner = printNode(node.expression, depth + 1, maxLineLength, indent);
5292
- let s = `(
5293
- ${indentLines(inner, depth + 1, indent)}
5294
- ${pad})`;
5295
- if (node.boost != null) s += `^${node.boost}`;
5296
- return s;
5297
- }
5298
- case "FieldGroup": {
5299
- let s = `${node.field}:(${inline(node.expression)})`;
5300
- if (node.boost != null) s += `^${node.boost}`;
5301
- return s;
5302
- }
5303
- case "Not":
5304
- return `NOT ${printNode(node.expression, depth, maxLineLength, indent)}`;
5305
- default:
5306
- return inline(node);
5307
- }
5308
- }
5309
- function indentLines(text, depth, indent) {
5310
- const pad = indent.repeat(depth);
5311
- return text.split("\n").map((line) => {
5312
- return line.startsWith(pad) ? line : pad + line;
5313
- }).join("\n");
5314
- }
5315
5372
  export {
5316
5373
  AutocompleteEngine,
5317
5374
  DARK_COLORS,
@@ -24,6 +24,8 @@ export interface BooleanExprNode {
24
24
  operator: 'AND' | 'OR';
25
25
  left: ASTNode;
26
26
  right: ASTNode;
27
+ /** True when the operator was inferred from whitespace (no explicit AND/OR token). */
28
+ implicit?: boolean;
27
29
  start: number;
28
30
  end: number;
29
31
  }
package/dist/types.d.ts CHANGED
@@ -158,6 +158,8 @@ export interface StyleConfig {
158
158
  fontFamily?: string;
159
159
  /** Base font size for the input. */
160
160
  fontSize?: string;
161
+ /** Line height for the input text and placeholder. @default '1.5' */
162
+ lineHeight?: string;
161
163
  /** Minimum height of the input element. */
162
164
  inputMinHeight?: string;
163
165
  /** Padding inside the input element. */
@@ -273,6 +275,19 @@ export interface DropdownConfig {
273
275
  * dropdown is open and an item is already selected. When no item is selected
274
276
  * (index = -1), the keys pass through for normal text cursor movement. @default false */
275
277
  homeEndKeys?: boolean;
278
+ /** Called when the engine returns zero suggestions. Return a React element to display
279
+ * in the dropdown (e.g. "No results for …"), or null/undefined to hide the dropdown.
280
+ * Not called during async loading (the spinner handles that). */
281
+ renderNoResults?: (context: {
282
+ cursorContext: CursorContext;
283
+ partial: string;
284
+ }) => React.ReactNode | null | undefined;
285
+ /** Controls the type badge shown in dropdown items.
286
+ * - `false` — hide the badge entirely
287
+ * - `true` (default) — show the raw `suggestion.type` string
288
+ * - callback `(type, suggestion) => ReactNode` — custom render per item; return null to hide
289
+ */
290
+ renderType?: boolean | ((type: string, suggestion: SuggestionItem) => React.ReactNode | null | undefined);
276
291
  }
277
292
  /**
278
293
  * Feature toggles for optional editing behaviors. All default to false except `multiline`.
@@ -286,6 +301,8 @@ export interface FeaturesConfig {
286
301
  expandSelection?: boolean;
287
302
  /** Pressing `*` with a single value token selected wraps it in wildcards. @default false */
288
303
  wildcardWrap?: boolean;
304
+ /** Enable Alt+Shift+F to pretty-print the query in-place using `formatQuery`. @default false */
305
+ formatQuery?: boolean;
289
306
  /** Enable `#name` saved-search syntax and autocomplete. When false, `#` is a regular character. @default false */
290
307
  savedSearches?: boolean;
291
308
  /** Enable `!query` history-search syntax and autocomplete. When false, `!` is a regular character. @default false */
@@ -6,6 +6,10 @@ export interface FormatQueryOptions {
6
6
  maxLineLength?: number;
7
7
  /** Indent string for each nesting level. @default ' ' (2 spaces) */
8
8
  indent?: string;
9
+ /** When set, replaces implicit AND (whitespace between terms) with this operator
10
+ * string in the output. By default, implicit AND is preserved as whitespace.
11
+ * @example 'AND' — turns `status:active name:john` into `status:active AND name:john` */
12
+ whitespaceOperator?: string;
9
13
  }
10
14
  /**
11
15
  * Pretty-print an Elasticsearch query string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elastic-input",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Syntax-aware smart autocomplete input for Elastic query syntax",
5
5
  "license": "MIT",
6
6
  "repository": {