module BELParser::Completion

Public Class Methods

complete(input, spec, search, namespaces, caret_position = input.length, include_invalid_semantics = false) click to toggle source
# File lib/bel_parser/completion.rb, line 14
def self.complete(input, spec, search, namespaces,
                  caret_position = input.length, include_invalid_semantics = false)
  # Algorithm
  # 1. Parse AST using statement_autocomplete ragel FSM.
  # 2. Given cursor find node to complete.
  # 3. Determine completers that should run given node type and surrounding nodes in the AST.
  # 4. Compute completion AST for each suggestion.
  # 5. For each suggestion, transform original AST into full completion.
  # 6. Run semantic validation on each completion AST.
  # 7. Return combined completion AST and semantic details.

  ast, caret_position = BELParser::Parsers::Expression::StatementAutocomplete.parse(input, caret_position)
  completing_node     = find_node(ast, caret_position)
  return [] unless completing_node

  completions =
    case completing_node.type
    when :parameter
      complete_parameter(completing_node, caret_position, ast, spec, search, namespaces)
    when :function
      complete_function(completing_node, caret_position, ast, spec, search, namespaces)
    when :argument
      complete_argument(completing_node, caret_position, ast, spec, search, namespaces)
    when :relationship
      complete_relationship(completing_node, caret_position, ast, spec, search, namespaces)
    else
      []
    end

  will_match_partial = true
  urir               = BELParser::Resource.default_uri_reader
  urlr               = BELParser::Resource.default_url_reader

  validator =
    BELParser::Language::ExpressionValidator.new(
      spec, namespaces, urir, urlr, will_match_partial
    )

  validated_completions =
    completions
      .map { |(completion_ast, completion_result)|

        if completion_result[:type] == :namespace_prefix
          # namespace_prefix completions are always valid
          completion_result[:validation] = {
            expression:      completion_result[:value],
            valid_syntax:    true,
            valid_semantics: true,
            message:         'Valid semantics',
            warnings:        [],
            term_signatures: []
          }
          completion_result
        else
          message             = ''
          terms               = completion_ast.traverse.select { |node| node.type == :term }.to_a
          semantics_functions =
            BELParser::Language::Semantics.semantics_functions.reject { |fun|
              fun == BELParser::Language::Semantics::SignatureMapping
            }

          semantic_warnings =
            completion_ast
              .traverse
              .flat_map { |node|
                semantics_functions.flat_map { |func|
                  func.map(node, spec, namespaces, will_match_partial)
                }
              }
              .compact

          if semantic_warnings.empty?
            valid = true
          else
            valid = false
            message =
              semantic_warnings.reduce('') { |msg, warning|
                msg << "#{warning}\n"
              }
            message << "\n"
          end

          term_semantics =
            terms.map { |term|
              term_result = validator.validate(term)
              valid      &= term_result.valid_semantics?
              bel_term    = serialize(term)

              unless valid
                message << "Term: #{bel_term}\n"
                term_result.invalid_signature_mappings.map { |m|
                  message << "  #{m}\n"
                }
                message << "\n"
              end

              {
                term:               bel_term,
                valid_signatures:   term_result.valid_signature_mappings.map(&:to_s),
                invalid_signatures: term_result.invalid_signature_mappings.map(&:to_s)
              }
            }

          completion_result[:validation] = {
            expression:      completion_result[:value],
            valid_syntax:    true,
            valid_semantics: valid,
            message:         valid ? 'Valid semantics' : message,
            warnings:        semantic_warnings.map(&:to_s),
            term_signatures: term_semantics
          }
          completion_result
        end
      }
      .group_by { |completion_result|
        completion_result[:validation][:valid_semantics]
      }

  if include_invalid_semantics
    (validated_completions[true] || []) + (validated_completions[false] || [])
  else
    validated_completions[true] || []
  end
end
complete_argument( completing_node, caret_position, ast, spec, search, namespaces ) click to toggle source
# File lib/bel_parser/completion.rb, line 374
def self.complete_argument(
  completing_node, caret_position, ast, spec, search, namespaces
)
  if completing_node.child.nil?
    all_prefix_completions = AllNamespacePrefixArgumentCompleter
      .new(spec, search, namespaces)
      .complete(nil, nil)
      .map { |(bel_prefix, completion_ast)|
        completion_ast.character_range = completing_node.character_range

        completion_ast = MergeCompletion.new(completion_ast).process(ast)
        completion     = serialize(completion_ast)

        [
          completion_ast,
          {
            type:           :namespace_prefix,
            id:             bel_prefix,
            label:          bel_prefix,
            value:          completion,
            caret_position: completing_node.range_start + bel_prefix.length + 1
          }
        ]
      }

    all_function_completions = AllFunctionArgumentCompleter
      .new(spec, search, namespaces)
      .complete(nil, nil)
      .map { |(function, completion_ast)|
        short = function.short.to_s
        long  = function.long.to_s

        completion_ast.character_range = [
          completing_node.range_start,
          completing_node.range_start + short.length
        ]

        completion_ast = MergeCompletion.new(completion_ast).process(ast)
        completion     = serialize(completion_ast)

        [
          completion_ast,
          {
            type:           :function,
            id:             long,
            label:          long,
            value:          completion,
            caret_position: short.length + 1
          }
        ]
      }

    all_prefix_completions + all_function_completions
  elsif completing_node.parameter?
    parameter     = completing_node.child
    prefix, value = parameter.children

    if prefix && Range.new(*prefix.character_range, false).include?(caret_position)
      prefix_str = prefix.identifier.string_literal

      prefix_completions = NamespacePrefixArgumentCompleter
        .new(spec, search, namespaces)
        .complete(prefix_str, nil)
        .map { |(bel_prefix, completion_ast)|
          completion_ast.character_range = completing_node.character_range

          completion_ast = MergeCompletion.new(completion_ast).process(ast)
          completion     = serialize(completion_ast)

          [
            completion_ast,
            {
              type:           :namespace_prefix,
              id:             bel_prefix,
              label:          bel_prefix,
              value:          completion,
              caret_position: completing_node.range_start + bel_prefix.length + 1
            }
          ]
        }

      prefix_completions
    else
      # completing value of parameter
      value_str =
        case value.first_child.type
        when :identifier
          value.first_child.string_literal
        when :string
          value.first_child.string_value
        end

      prefix_string      = nil
      prefix_completions =
        if prefix && prefix.identifier
          # ... prefix exists, store it for later value lookup
          prefix_string = prefix.identifier.string_literal
          []
        else
          # ... prefix is nil, try to complete it, lookup values later without prefix
          prefix_string = nil

          NamespacePrefixArgumentCompleter
            .new(spec, search, namespaces)
            .complete(value_str, nil)
            .map { |(bel_prefix, completion_ast)|
              completion_ast.character_range = completing_node.character_range

              completion_ast = MergeCompletion.new(completion_ast).process(ast)
              completion     = serialize(completion_ast)

              [
                completion_ast,
                {
                  type:           :namespace_prefix,
                  id:             bel_prefix,
                  label:          bel_prefix,
                  value:          completion,
                  caret_position: completion_ast.range_start + bel_prefix.length + 1
                }
              ]
            }
        end

      function_completions = []
      if prefix_string.nil?
        completer =
          if ast.subject.term.function.nil? || (!ast.object.nil? && ast.object.term? && ast.object.child.function.nil?)
            FunctionTermCompleter
          else
            FunctionArgumentCompleter
          end
        function_completions = completer
          .new(spec, search, namespaces)
          .complete(value_str, caret_position)
          .map { |(function, completion_ast)|
            short = function.short.to_s
            long  = function.long.to_s

            completion_ast.character_range = [
              completing_node.range_start,
              completing_node.range_start + short.length
            ]

            completion_ast = MergeCompletion.new(completion_ast).process(ast)
            completion     = serialize(completion_ast)

            [
              completion_ast,
              {
                type:           :function,
                id:             long,
                label:          long,
                value:          completion,
                caret_position: completing_node.range_start + short.length + 1
              }
            ]
          }
      end

      exact_match_completions = ExactMatchParameterCompleter
        .new(spec, search, namespaces)
        .complete(value_str, caret_position - value.range_start, prefix: prefix_string)
        .map { |(ns_value, completion_ast)|
          completion_ast.character_range = completing_node.character_range

          completion_ast = MergeCompletion.new(completion_ast).process(ast)
          completion     = serialize(completion_ast)

          [
            completion_ast,
            {
              type:           :namespace_value,
              id:             ns_value,
              label:          ns_value,
              value:          completion,
              caret_position: value.range_start + ns_value.length
            }
          ]
        }

      wildcard_completions = WildcardMatchParameterCompleter
        .new(spec, search, namespaces)
        .complete(value_str, caret_position - value.range_start, prefix: prefix_string)
        .map { |(ns_value, completion_ast)|
          completion_ast.character_range = completing_node.character_range

          completion_ast = MergeCompletion.new(completion_ast).process(ast)
          completion     = serialize(completion_ast)

          [
            completion_ast,
            {
              type:           :namespace_value,
              id:             ns_value,
              label:          ns_value,
              value:          completion,
              caret_position: value.range_start + ns_value.length
            }
          ]
        }

      prefix_completions + function_completions + (exact_match_completions + wildcard_completions).uniq
    end
  else
    # TODO Completing term argument, will we ever get here?
    puts "#{completing_node.type}: child is a term, how do we proceed?"
    []
  end
end
complete_function( completing_node, caret_position, ast, spec, search, namespaces ) click to toggle source
# File lib/bel_parser/completion.rb, line 139
def self.complete_function(
  completing_node, caret_position, ast, spec, search, namespaces
)
  string_literal =
    if completing_node.identifier.nil?
      ''
    else
      completing_node.identifier.string_literal
    end

  FunctionCompleter
    .new(spec, search, namespaces)
    .complete(string_literal, caret_position)
    .map { |(function, completion_ast)|
      short      = function.short.to_s
      long       = function.long.to_s

      completion_ast.character_range = [
        completing_node.range_start,
        completing_node.range_start + short.length
      ]

      completion_ast = MergeCompletion.new(completion_ast).process(ast)
      completion     = serialize(completion_ast)

      [
        completion_ast,
        {
          type:           :function,
          id:             long,
          label:          long,
          value:          completion,
          caret_position: short.length + 1
        }
      ]
    }
end
complete_parameter( completing_node, caret_position, ast, spec, search, namespaces ) click to toggle source
# File lib/bel_parser/completion.rb, line 216
def self.complete_parameter(
  completing_node, caret_position, ast, spec, search, namespaces
)
  prefix, value = completing_node.children

  # Completing prefix
  if Range.new(*prefix.character_range, false).include?(caret_position)
    if prefix.identifier.nil?
      # Provide all namespace prefix completions.
      all_prefix_completions = AllNamespacePrefixCompleter
        .new(spec, search, namespaces)
        .complete(nil, nil)
        .map { |(bel_prefix, completion_ast)|
          completion_ast.character_range = [
            prefix.range_start,
            prefix.range_start + bel_prefix.length + 1
          ]

          completion_ast = MergeCompletion.new(completion_ast).process(ast)
          completion     = serialize(completion_ast)

          [
            completion_ast,
            {
              type:           :namespace_prefix,
              id:             bel_prefix,
              label:          bel_prefix,
              value:          completion,
              caret_position: completing_node.range_start + bel_prefix.length + 1
            }
          ]
        }

      all_prefix_completions
    else
      # Match provided namespace prefix.
      string_literal = prefix.identifier.string_literal

      prefix_completions = NamespacePrefixCompleter
        .new(spec, search, namespaces)
        .complete(string_literal, caret_position)
        .map { |(bel_prefix, completion_ast)|
          completion_ast.character_range = [
            prefix.range_start,
            prefix.range_start + bel_prefix.length + 1
          ]

          completion_ast = MergeCompletion.new(completion_ast).process(ast)
          completion     = serialize(completion_ast)

          [
            completion_ast,
            {
              type:           :namespace_prefix,
              id:             bel_prefix,
              label:          bel_prefix,
              value:          completion,
              caret_position: completing_node.range_start + bel_prefix.length + 1
            }
          ]
        }

      prefix_completions
    end
  else
    string_literal =
      case value.first_child.type
      when :identifier
        value.first_child.string_literal
      when :string
        value.first_child.string_value
      end

    prefix_str =
      if prefix && prefix.identifier
        prefix.identifier.string_literal
      else
        nil
      end

    function_completions = FunctionTermCompleter
      .new(spec, search, namespaces)
      .complete(string_literal, caret_position)
      .map { |(function, completion_ast)|
        short      = function.short.to_s
        long       = function.long.to_s
        completion = serialize(completion_ast)

        [
          completion_ast,
          {
            type:           :function,
            id:             long,
            label:          long,
            value:          completion,
            caret_position: short.length + 1
          }
        ]
      }

    prefix_completions = NamespacePrefixArgumentCompleter
      .new(spec, search, namespaces)
      .complete(string_literal, nil)
      .map { |(bel_prefix, completion_ast)|
        completion = serialize(completion_ast)

        [
          completion_ast,
          {
            type:           :namespace_prefix,
            id:             bel_prefix,
            label:          bel_prefix,
            value:          completion,
            caret_position: completing_node.range_start + bel_prefix.length + 1
          }
        ]
      }

    exact_match_completions = ExactMatchParameterCompleter
      .new(spec, search, namespaces)
      .complete(string_literal, caret_position - value.range_start, prefix: prefix_str)
      .map { |(ns_value, completion_ast)|
        completion = "(#{serialize(completion_ast)})"

        [
          completion_ast,
          {
            type:           :namespace_value,
            id:             ns_value,
            label:          ns_value,
            value:          completion,
            caret_position: 0
          }
        ]
      }

    wildcard_completions = WildcardMatchParameterCompleter
      .new(spec, search, namespaces)
      .complete(string_literal, caret_position - value.range_start, prefix: prefix_str)
      .map { |(ns_value, completion_ast)|
        completion = "(#{serialize(completion_ast)})"

        [
          completion_ast,
          {
            type:           :namespace_value,
            id:             ns_value,
            label:          ns_value,
            value:          completion,
            caret_position: 0
          }
        ]
      }

    function_completions + prefix_completions + (exact_match_completions + wildcard_completions).uniq
  end
end
complete_relationship( completing_node, caret_position, ast, spec, search, namespaces ) click to toggle source
# File lib/bel_parser/completion.rb, line 177
def self.complete_relationship(
  completing_node, caret_position, ast, spec, search, namespaces
)
  string_literal = completing_node.string_literal

  completer =
    if string_literal.nil?
      AllRelationshipCompleter.new(spec, search, namespaces)
    else
      RelationshipCompleter.new(spec, search, namespaces)
    end

  completer
    .complete(string_literal, caret_position)
    .map { |(relationship, completion_ast)|
      short = relationship.short.to_s
      long  = relationship.long.to_s

      completion_ast.character_range = [
        completing_node.range_start,
        completing_node.range_start + short.length
      ]

      completion_ast = MergeCompletion.new(completion_ast).process(ast)
      completion     = serialize(completion_ast)

      [
        completion_ast,
        {
          type:           :relationship,
          id:             long,
          label:          long,
          value:          completion,
          caret_position: short.length + 1
        }
      ]
    }
end
find_node(ast, caret_position) click to toggle source
# File lib/bel_parser/completion.rb, line 585
def self.find_node(ast, caret_position)
  ast.traverse do |node|
    next if
      node.type == :term ||
      caret_position < node.range_start ||
      caret_position > node.range_end

    case node.type
    when :argument
      return node if node.child.nil? || node.parameter?
    when :parameter, :function, :relationship
      return node
    end
  end

  nil
end