Przeglądaj źródła

[frontend] Add solr and global search parsers back

These were removed by mistake at some point and this commit reverts that
Johan Åhlén 3 lat temu
rodzic
commit
b30107515d

+ 179 - 0
desktop/core/src/desktop/js/parse/jison/globalSearchParser.jison

@@ -0,0 +1,179 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+%lex
+%x singleQuote doubleQuote
+%%
+
+\s+                                             { return 'WS' }
+
+'\u2020'                                        { parser.yy.cursorFound = yylloc; return 'CURSOR'; }
+
+[a-zA-Z]+[:]                                    { return 'FACET' }
+
+\'                                              { this.begin('singleQuote'); return 'QUOTE'; }
+
+<singleQuote>(?:\\[']|[^'])+                    {
+                                                  if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '\'')) {
+                                                    return 'PARTIAL_VALUE';
+                                                  }
+                                                  return 'VALUE';
+                                                }
+
+<singleQuote>\'                                 { this.popState(); return 'QUOTE'; }
+
+\"                                              { this.begin('doubleQuote'); return 'QUOTE'; }
+
+<doubleQuote>(?:\\["]|[^"])+                    {
+                                                  if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '"')) {
+                                                    return 'PARTIAL_VALUE';
+                                                  }
+                                                  return 'VALUE';
+                                                }
+
+<doubleQuote>\"                                 { this.popState(); return 'QUOTE'; }
+
+[^"'\s\u2020]+                                  { return 'TEXT'; }
+
+<<EOF>>                                         { return 'EOF'; }
+
+/lex
+
+%start GlobalSearchAutocomplete
+
+%%
+
+GlobalSearchAutocomplete
+ : OptionalWhitespace SearchParts OptionalWhitespace 'EOF'
+   {
+     return $2;
+   }
+ | OptionalWhitespace SearchParts_EDIT 'EOF'
+   {
+     if (!$2.facets) {
+       $2.facets = {};
+     }
+     if (!$2.text) {
+       $2.text = [];
+     }
+     return $2;
+   }
+ | OptionalWhitespace 'EOF'
+   {
+     return { facets: {}, text: [] };
+   }
+ ;
+
+SearchParts
+ : SearchPart
+ | SearchParts WS SearchPart
+   {
+     parser.mergeFacets($1, $3);
+     parser.mergeText($1, $3);
+   }
+ ;
+
+SearchParts_EDIT
+ : SearchPart_EDIT
+ | SearchParts WS SearchPart_EDIT
+   {
+     parser.mergeFacets($1, $3);
+     parser.mergeText($1, $3);
+     $$ = $3;
+     $$.text = $1.text;
+     $$.facets = $1.facets;
+   }
+ | SearchPart_EDIT WS SearchParts
+   {
+     $$ = $1;
+     parser.mergeFacets($$, $3);
+     parser.mergeText($$, $3);
+   }
+ | SearchParts WS SearchPart_EDIT WS SearchParts
+   {
+     parser.mergeFacets($1, $3);
+     parser.mergeFacets($1, $5);
+     parser.mergeText($1, $3);
+     parser.mergeText($1, $5);
+     $$ = $3;
+     $$.text = $1.text;
+     $$.facets = $1.facets;
+   }
+ ;
+
+SearchPart
+ : Facet     --> { text: [], facets: $1.facets }
+ | FreeText  --> { text: [$1], facets: {} }
+ ;
+
+SearchPart_EDIT
+ : Facet_EDIT
+ | FreeText_EDIT
+ ;
+
+Facet
+ : 'FACET' OptionalWhitespace FreeText
+   {
+     var facet = {};
+     var facetName = $1.substring(0, $1.length - 1).toLowerCase();
+     facet[facetName] = {};
+     facet[facetName][$3.toLowerCase()] = true;
+     $$ = { facets: facet };
+   }
+ ;
+
+Facet_EDIT
+ : 'FACET' OptionalWhitespace 'CURSOR'           --> { suggestFacetValues: $1.substring(0, $1.length - 1).toLowerCase() }
+ | 'FACET' OptionalWhitespace FreeText 'CURSOR'
+   {
+     var facet = {};
+     var facetName = $1.substring(0, $1.length - 1).toLowerCase();
+     facet[facetName] = {};
+     facet[facetName][$3.toLowerCase()] = true;
+     $$ = { suggestFacetValues: facetName, facets: facet }
+   }
+ ;
+
+FreeText
+ : 'TEXT'
+ | QuotedValue
+ ;
+
+FreeText_EDIT
+ : 'CURSOR'                --> { suggestFacets: true, suggestResults: true }
+ | 'CURSOR' 'TEXT'         --> { suggestFacets: true, suggestResults: true, text: [$2] }
+ | 'TEXT' 'CURSOR' 'TEXT'  --> { suggestFacets: true, suggestResults: true, text: [$1+$3] }
+ | 'TEXT' 'CURSOR'         --> { suggestFacets: true, suggestResults: true, text: [$1] }
+ | QuotedValue_EDIT
+ ;
+
+QuotedValue
+ : 'QUOTE' 'VALUE' 'QUOTE'  --> $2
+ | 'QUOTE' 'QUOTE'          --> ''
+ ;
+
+QuotedValue_EDIT
+ : 'QUOTE' 'PARTIAL_VALUE'  --> $2
+ ;
+
+OptionalWhitespace
+ :
+ | WS
+ ;
+
+%%
+
+SqlParseSupport.initGlobalSearchParser(parser);

+ 265 - 0
desktop/core/src/desktop/js/parse/jison/solrFormulaParser.jison

@@ -0,0 +1,265 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+%lex
+%options case-insensitive
+%%
+
+\s                                         { /* skip whitespace */ }
+'--'.*                                     { /* skip comments */ }
+[\/][*][^*]*[*]+([^\/*][^*]*[*]+)*[\/]     { /* skip comments */ }
+
+'\u2020'                                   { parser.yy.cursorFound = yylloc; return 'CURSOR'; }
+
+[0-9]+(?:[,.][0-9]+)?                      { return 'NUMBER'; }
+
+'-'                                        { return '-'; }
+'+'                                        { return '+'; }
+'*'                                        { return '*'; }
+'/'                                        { return '/'; }
+
+[a-z]+\s*\(                                {
+                                             yy.lexer.unput('(');
+                                             parser.addFunctionLocation({
+                                               first_line: yylloc.first_line,
+                                               first_column: yylloc.first_column,
+                                               last_line: yylloc.first_line,
+                                               last_column: yylloc.first_column + yytext.trim().length
+                                             }, yytext.trim());
+                                             return 'FUNCTION';
+                                           }
+
+','                                        { return ','; }
+'('                                        { return '('; }
+')'                                        { return ')'; }
+
+<<EOF>>                                    { return 'EOF'; }
+
+[^\s\u2020()]+                             { parser.addFieldLocation(yylloc, yytext); return 'IDENTIFIER'; }
+
+/lex
+
+%left '+' '-'
+%left '*' '/'
+
+%start SolrFormulaAutocomplete
+
+%%
+
+SolrFormulaAutocomplete
+ : SolrFormula 'EOF'
+   {
+     return {
+       parsedValue: $1
+     };
+   }
+ | SolrFormula_EDIT 'EOF'
+   {
+     return $1
+   }
+ | 'CURSOR' 'EOF'
+   {
+     return { suggestAggregateFunctions: true }
+   }
+ ;
+
+SolrFormula
+ : NonParenthesizedSolrFormula
+ | '(' NonParenthesizedSolrFormula ')'  -> $1 + $2 + $3
+ ;
+
+SolrFormula_EDIT
+ : NonParenthesizedSolrFormula_EDIT
+ | '(' NonParenthesizedSolrFormula_EDIT RightParenthesisOrError  --> $2
+ ;
+
+NonParenthesizedSolrFormula
+ : 'NUMBER'
+ | 'IDENTIFIER'
+ | 'FUNCTION' '(' ArgumentList ')'    -> $1 + $2 + $3 + $4
+ | SolrFormula '+' SolrFormula  -> 'sum(' + $1 + ',' + $3 + ')'
+ | SolrFormula '-' SolrFormula  -> 'sub(' + $1 + ',' + $3 + ')'
+ | SolrFormula '*' SolrFormula  -> 'mul(' + $1 + ',' + $3 + ')'
+ | SolrFormula '/' SolrFormula  -> 'div(' + $1 + ',' + $3 + ')'
+ | '-' SolrFormula                 -> 'sub(0,' + $2 + ')'
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : 'NUMBER' 'CURSOR'                                             --> { suggestOperators: true }
+ | 'IDENTIFIER' 'CURSOR'                                         --> { suggestOperators: true }
+ | 'CURSOR' 'NUMBER'                                             --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'CURSOR' 'IDENTIFIER'                                         --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : 'FUNCTION' '(' 'CURSOR' RightParenthesisOrError               --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'FUNCTION' '(' ArgumentList_EDIT RightParenthesisOrError      --> $3
+ | 'FUNCTION' '(' ArgumentList ')' 'CURSOR'                      --> { suggestOperators: true }
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : SolrFormula '+' 'CURSOR'                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'CURSOR' '+' SolrFormula                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | SolrFormula_EDIT '+' SolrFormula                              --> $1
+ | SolrFormula '+' SolrFormula_EDIT                              --> $3
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : SolrFormula '-' 'CURSOR'                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'CURSOR' '-' SolrFormula                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | SolrFormula_EDIT '-' SolrFormula                              --> $1
+ | SolrFormula '-' SolrFormula_EDIT                              --> $3
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : SolrFormula '*' 'CURSOR'                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'CURSOR' '*' SolrFormula                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | SolrFormula_EDIT '*' SolrFormula                              --> $1
+ | SolrFormula '*' SolrFormula_EDIT                              --> $3
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : SolrFormula '/' 'CURSOR'                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | 'CURSOR' '/' SolrFormula                                      --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | SolrFormula_EDIT '/' SolrFormula                              --> $1
+ | SolrFormula '/' SolrFormula_EDIT                              --> $3
+ ;
+
+NonParenthesizedSolrFormula_EDIT
+ : '-' 'CURSOR'                                                  --> { suggestAggregateFunctions: true, suggestFunctions: true, suggestFields: true }
+ | '-' SolrFormula_EDIT                                          --> $2
+ ;
+
+ArgumentList
+ : SolrFormula
+ | ArgumentList ',' SolrFormula
+ ;
+
+ArgumentList_EDIT
+ : SolrFormula_EDIT
+ | ArgumentList ',' SolrFormula_EDIT                             --> $3
+ | SolrFormula_EDIT ',' ArgumentList
+ | ArgumentList ',' SolrFormula_EDIT ',' ArgumentList            --> $3
+ ;
+
+
+RightParenthesisOrError
+ : ')'
+ | error
+ ;
+
+%%
+
+parser.yy.parseError = function () { return false; }
+
+parser.identifyPartials = function (beforeCursor, afterCursor) {
+  var beforeMatch = beforeCursor.match(/[^()-*+/,\s]*$/);
+  var afterMatch = afterCursor.match(/^[^()-*+/,\s]*/);
+  return {left: beforeMatch ? beforeMatch[0].length : 0, right: afterMatch ? afterMatch[0].length : 0};
+};
+
+var adjustLocationForCursor = function (location) {
+  // columns are 0-based and lines not, so add 1 to cols
+  var newLocation = {
+    first_line: location.first_line,
+    last_line: location.last_line,
+    first_column: location.first_column + 1,
+    last_column: location.last_column + 1
+  };
+  if (parser.yy.cursorFound) {
+    if (parser.yy.cursorFound.first_line === newLocation.first_line && parser.yy.cursorFound.last_column <= newLocation.first_column) {
+      var additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+      newLocation.first_column = newLocation.first_column + additionalSpace;
+      newLocation.last_column = newLocation.last_column + additionalSpace;
+    }
+  }
+  return newLocation;
+};
+
+parser.addFunctionLocation = function (location, name) {
+  parser.yy.locations.push({ type: 'function', name: name, location: adjustLocationForCursor(location) });
+}
+
+parser.addFieldLocation = function (location, name) {
+  parser.yy.locations.push({ type: 'field', name: name, location: adjustLocationForCursor(location) });
+}
+
+parser.parseSolrFormula = function (formula, debug) {
+  parser.yy.cursorFound = false;
+  parser.yy.locations = [];
+  formula = formula.replace(/\r\n|\n\r/gm, '\n');
+
+  var result;
+  try {
+    result = parser.parse(formula);
+  } catch (err) {
+    if (debug) {
+      console.log(beforeCursor + '\u2020' + afterCursor);
+      console.log(err);
+      console.error(err.stack);
+    }
+  }
+  return result || false;
+}
+
+parser.autocompleteSolrFormula = function (beforeCursor, afterCursor, debug) {
+  parser.yy.cursorFound = false;
+  parser.yy.locations = [];
+
+  beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+  afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+
+  parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+  if (parser.yy.partialLengths.left > 0) {
+    beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+  }
+
+  if (parser.yy.partialLengths.right > 0) {
+    afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+  }
+
+  var result;
+  try {
+    result = parser.parse(beforeCursor + '\u2020' + afterCursor);
+  } catch (err) {
+    // Workaround for too many missing parentheses (it's the only error we handle in the parser)
+    if (err && err.toString().indexOf('Parsing halted while starting to recover from another error') !== -1) {
+      var leftCount = (beforeCursor.match(/\(/g) || []).length;
+      var rightCount = (beforeCursor.match(/\)/g) || []).length;
+      var parenthesisPad = '';
+      while (rightCount < leftCount) {
+        parenthesisPad += ')';
+        rightCount++;
+      }
+      try {
+        result = parser.parse(beforeCursor + '\u2020' + parenthesisPad);
+      } catch (err) {
+        return {}
+      }
+    } else {
+      if (debug) {
+        console.log(beforeCursor + '\u2020' + afterCursor);
+        console.log(err);
+        console.error(err.stack);
+      }
+      return {}
+    }
+  }
+  result.locations = parser.yy.locations;
+  return result;
+};

+ 292 - 0
desktop/core/src/desktop/js/parse/jison/solrQueryParser.jison

@@ -0,0 +1,292 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+%lex
+%options case-insensitive flex
+%x squareBracketRange curlyBracketRange doubleQuotedValue singleQuotedValue
+%%
+
+\s                                             { /* skip whitespace */ }
+'--'.*                                         { /* skip comments */ }
+[\/][*][^*]*[*]+([^\/*][^*]*[*]+)*[\/]         { /* skip comments */ }
+
+'\u2020'                                       { parser.yy.cursorFound = yylloc; return 'CURSOR'; }
+
+'AND'                                          { return 'AND'; }
+'&&'                                           { return 'AND'; }
+'OR'                                           { return 'OR'; }
+'||'                                           { return 'OR'; }
+'NOT'                                          { return 'NOT'; }
+'!'                                            { return 'NOT'; }
+'+'                                            { return '+'; }
+'-'                                            { return '-'; }
+':'                                            { return ':'; }
+'*'                                            { return '*'; }
+
+'('                                            { return '('; }
+')'                                            { return ')'; }
+
+[0-9]+(?:[,.][0-9]+)?                          { return 'NUMBER'; }
+
+'['                                            { this.begin('squareBracketRange'); return '['; }
+<squareBracketRange>(?:\\[\]]|[^\]])+          {
+                                                 if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, ']')) {
+                                                   return 'PARTIAL_VALUE';
+                                                 }
+                                                 return 'VALUE';
+                                               }
+<singleQuotedValue>\]                          { this.popState(); return ']'; }
+
+'{'                                            { this.begin('curlyBracketRange'); return '{'; }
+<curlyBracketRange>(?:\\[\}]|[^\}])+           {
+                                                 if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '}')) {
+                                                   return 'PARTIAL_VALUE';
+                                                 }
+                                                 return 'VALUE';
+                                               }
+<curlyBracketRange>\}                          { this.popState(); return '}'; }
+
+\'                                             { this.begin('singleQuotedValue'); return 'SINGLE_QUOTE'; }
+<singleQuotedValue>(?:\\[']|[^'])+             {
+                                                 if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '\'')) {
+                                                   yytext = yytext.replace(/[\u2020].*/, '');
+                                                   return 'PARTIAL_VALUE';
+                                                 }
+                                                 return 'VALUE';
+                                               }
+<singleQuotedValue>\'                          { this.popState(); return 'SINGLE_QUOTE'; }
+
+\"                                             { this.begin('doubleQuotedValue'); return 'DOUBLE_QUOTE'; }
+<doubleQuotedValue>(?:\\["]|[^"])+             {
+                                                 if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '"')) {
+                                                   yytext = yytext.replace(/[\u2020].*/, '');
+                                                   return 'PARTIAL_VALUE';
+                                                 }
+                                                 return 'VALUE';
+                                               }
+<doubleQuotedValue>\"                          { this.popState(); return 'DOUBLE_QUOTE'; }
+
+[^\s\u3000!():"'^+\-\[\]{}~*?\/\u2020]+        { return 'TERM'; }
+
+<<EOF>>                                        { return 'EOF'; }
+
+/lex
+
+%left 'AND' 'OR' '&&' '||' BooleanOperator
+%left 'CURSOR'  // Cursor precedence needed to not conflict with operators i.e. x 'CURSOR' y vs. x 'AND' y
+
+%start SolrQueryAutocomplete
+
+%%
+
+SolrQueryAutocomplete
+ : SolrQuery 'EOF'
+   {
+     return {}
+   }
+ | SolrQuery_EDIT 'EOF'
+   {
+     return $1;
+   }
+ | 'CURSOR' 'EOF'
+   {
+     return { suggestFields: { appendColon: true } }
+   }
+ ;
+
+SolrQuery
+ : NonParenthesizedSolrQuery
+ | '(' NonParenthesizedSolrQuery ')'                           --> $2
+ ;
+
+SolrQuery_EDIT
+ : NonParenthesizedSolrQuery_EDIT
+ | '(' NonParenthesizedSolrQuery_EDIT RightParenthesisOrError  --> $2
+ ;
+
+NonParenthesizedSolrQuery
+ : 'NUMBER'
+ | 'TERM'
+ | KeywordMatch                                                --> { hasKeywordMatch: true }
+ ;
+
+NonParenthesizedSolrQuery_EDIT
+ : KeywordMatch_EDIT
+ ;
+
+NonParenthesizedSolrQuery
+ : SolrQuery BooleanOperator SolrQuery
+ ;
+
+NonParenthesizedSolrQuery_EDIT
+ : SolrQuery 'CURSOR'
+   {
+     if ($1.hasKeywordMatch) {
+       $$ = { suggestKeywords: ['AND', 'OR'] };
+     } else {
+       $$ = { suggestKeywords: ['AND', 'OR', ':'], suggestValues: { field: $1, prependColon: true } };
+     }
+   }
+ | SolrQuery 'CURSOR' SolrQuery
+   {
+     if ($1.hasKeywordMatch) {
+       $$ = { suggestKeywords: ['AND', 'OR'] };
+     } else {
+       $$ = { suggestKeywords: ['AND', 'OR', ':'], suggestValues: { field: $1, prependColon: true } };
+     }
+   }
+ | 'CURSOR' SolrQuery                                          --> { suggestFields: { appendColon: true } }
+ ;
+
+NonParenthesizedSolrQuery_EDIT
+ : SolrQuery BooleanOperator 'CURSOR'                          --> { suggestFields: { appendColon: true } }
+ | 'CURSOR' BooleanOperator SolrQuery                          --> { suggestFields: { appendColon: true } }
+ | SolrQuery BooleanOperator SolrQuery_EDIT                    --> $3
+ | SolrQuery_EDIT BooleanOperator SolrQuery                    --> $1
+ ;
+
+KeywordMatch
+ : 'TERM' ':' 'TERM'
+ | 'TERM' ':' QuotedValue
+ ;
+
+KeywordMatch_EDIT
+ : 'TERM' ':' 'CURSOR'                                         --> { suggestValues: { field: $1 } }
+ | 'TERM' ':' QuotedValue_EDIT                                 --> { suggestValues: { field: $1, quotePresent: true, partial: $3 } }
+ ;
+
+// ======= Common constructs =======
+
+BooleanOperator
+ : 'AND' | 'OR' | '&&' | '||';
+
+QuotedValue
+ : 'SINGLE_QUOTE' 'VALUE' 'SINGLE_QUOTE'                       --> $2
+ | 'DOUBLE_QUOTE' 'VALUE' 'DOUBLE_QUOTE'                       --> $2
+ ;
+
+QuotedValue_EDIT
+ : 'SINGLE_QUOTE' 'PARTIAL_VALUE'                              --> $2
+ | 'DOUBLE_QUOTE' 'PARTIAL_VALUE'                              --> $2
+ ;
+
+RightParenthesisOrError: ')' | error;
+%%
+
+parser.yy.parseError = function () { return false; }
+
+parser.addFieldLocation = function (location, name) {
+  parser.yy.locations.push({ type: 'field', name: name, location: adjustLocationForCursor(location) });
+}
+
+parser.identifyPartials = function (beforeCursor, afterCursor) {
+  var beforeMatch = beforeCursor.match(/[^()-*+/,:"'\s]*$/);
+  var afterMatch = afterCursor.match(/^[^()-*+/,:"'\s]*/);
+  return { left: beforeMatch ? beforeMatch[0].length : 0, right: afterMatch ? afterMatch[0].length : 0 };
+};
+
+parser.handleQuotedValueWithCursor = function (lexer, yytext, yylloc, quoteChar) {
+  if (yytext.indexOf('\u2020') !== -1) {
+    var cursorIndex = yytext.indexOf('\u2020');
+    parser.yy.cursorFound = {
+      first_line: yylloc.first_line,
+      last_line: yylloc.last_line,
+      first_column: yylloc.first_column + cursorIndex,
+      last_column: yylloc.first_column + cursorIndex + 1
+    };
+    var remainder = yytext.substring(cursorIndex + 1);
+    var remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || []).length;
+    if (remainingQuotes > 0 && remainingQuotes & 1 != 0) {
+      parser.yy.missingEndQuote = false;
+      lexer.input();
+    } else {
+      parser.yy.missingEndQuote = true;
+      lexer.unput(remainder);
+    }
+    lexer.popState();
+    return true;
+  }
+  return false;
+};
+
+var adjustLocationForCursor = function (location) {
+  // columns are 0-based and lines not, so add 1 to cols
+  var newLocation = {
+    first_line: location.first_line,
+    last_line: location.last_line,
+    first_column: location.first_column + 1,
+    last_column: location.last_column + 1
+  };
+  if (parser.yy.cursorFound) {
+    if (parser.yy.cursorFound.first_line === newLocation.first_line && parser.yy.cursorFound.last_column <= newLocation.first_column) {
+      var additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+      newLocation.first_column = newLocation.first_column + additionalSpace;
+      newLocation.last_column = newLocation.last_column + additionalSpace;
+    }
+  }
+  return newLocation;
+};
+
+parser.autocompleteSolrQuery = function (beforeCursor, afterCursor, debug) {
+  parser.yy.cursorFound = false;
+  parser.yy.locations = [];
+
+  beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+  afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+
+  parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+  parser.yy.partialCursor = parser.yy.partialLengths.left > 0;
+
+  if (parser.yy.partialLengths.left > 0) {
+    beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+  }
+
+  if (parser.yy.partialLengths.right > 0) {
+    afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+  }
+
+  var result;
+  try {
+    result = parser.parse(beforeCursor + '\u2020' + afterCursor);
+  } catch (err) {
+    // Workaround for too many missing parentheses (it's the only error we handle in the parser)
+    if (err && err.toString().indexOf('Parsing halted while starting to recover from another error') !== -1) {
+      var leftCount = (beforeCursor.match(/\(/g) || []).length;
+      var rightCount = (beforeCursor.match(/\)/g) || []).length;
+      var parenthesisPad = '';
+      while (rightCount < leftCount) {
+        parenthesisPad += ')';
+        rightCount++;
+      }
+      try {
+        result = parser.parse(beforeCursor + '\u2020' + parenthesisPad);
+      } catch (err) {
+        return { locations: parser.yy.locations }
+      }
+    } else {
+      if (debug) {
+        console.log(beforeCursor + '\u2020' + afterCursor);
+        console.log(err);
+        console.error(err.stack);
+      }
+      return { locations: parser.yy.locations }
+    }
+  }
+  result.locations = parser.yy.locations;
+  return result;
+};