class TypeProf::LSP::Text

Attributes

caller_table[R]
definition_table[RW]
sigs[R]
text[R]
version[R]

Public Class Methods

new(server, uri, text, version) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 64
def initialize(server, uri, text, version)
  @server = server
  @uri = uri
  @text = text
  @version = version
  @sigs = nil

  @last_analysis_cancel_token = nil
  @analysis_queue = Queue.new
  @analysis_thread = Thread.new do
    loop do
      work = @analysis_queue.pop
      begin
        work.call
      rescue Exception
        puts "Rescued exception:"
        puts $!.full_message
        puts
      end
    end
  end

  # analyze synchronously to respond the first codeLens request
  res, def_table, caller_table = self.analyze(uri, text)
  on_text_changed_analysis(res, def_table, caller_table)
end

Public Instance Methods

analyze(uri, text, cancel_token: nil, signature_help_loc: nil) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 275
def analyze(uri, text, cancel_token: nil, signature_help_loc: nil)
  config = @server.typeprof_config.dup
  path = URI(uri).path
  config.rb_files = [[path, text]]
  config.rbs_files = ["typeprof.rbs"] # XXX
  config.verbose = 0
  config.max_sec = 1
  config.options[:show_errors] = true
  config.options[:show_indicator] = false
  config.options[:lsp] = true
  config.options[:signature_help_loc] = [path, signature_help_loc] if signature_help_loc

  TypeProf.analyze(config, cancel_token)
rescue SyntaxError
end
apply_changes(changes, version) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 98
def apply_changes(changes, version)
  @definition_table = nil
  text = @text.empty? ? [] : @text.lines
  changes.each do |change|
    case change
    in {
      range: {
          start: { line: start_row, character: start_col },
          end:   { line: end_row  , character: end_col   }
      },
      text: change_text,
    }
    else
      raise
    end
    text << "" if start_row == text.size
    text << "" if end_row == text.size
    if start_row == end_row
      text[start_row][start_col...end_col] = change_text
    else
      text[start_row][start_col..] = ""
      text[end_row][...end_col] = ""
      change_text = change_text.lines
      case change_text.size
      when 0
        text[start_row] += text[end_row]
        text[start_row + 1 .. end_row] = []
      when 1
        text[start_row] += change_text.first + text[end_row]
        text[start_row + 1 .. end_row] = []
      else
        text[start_row] += change_text.shift
        text[end_row].prepend(change_text.pop)
        text[start_row + 1 ... end_row - 1] = change_text
      end
    end
  end
  @text = text.join
  @version = version

  on_text_changed
end
code_complete(loc, trigger_kind) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 159
def code_complete(loc, trigger_kind)
  case loc
  in { line: row, character: col }
  end
  unless row < @text.lines.length && col >= 1 && @text.lines[row][0, col] =~ /\.\w*$/
    return nil
  end
  start_offset = $~.begin(0)
  end_offset = $&.size

  case trigger_kind
  when LSP::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS
    unless @current_completion_session&.reusable?(row, start_offset)
      puts "no reusable completion session but got TRIGGER_FOR_INCOMPLETE_COMPLETIONS"
      @current_completion_session = new_code_completion_session(row, start_offset, end_offset)
    end
    return @current_completion_session.results
  else
    @current_completion_session = new_code_completion_session(row, start_offset, end_offset)
    return @current_completion_session&.results
  end
end
lines() click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 94
def lines
  @text.lines
end
new_code_completion_session(row, start_offset, end_offset) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 141
def new_code_completion_session(row, start_offset, end_offset)
  lines = @text.lines
  lines[row][start_offset, end_offset] = ".__typeprof_lsp_completion"
  tmp_text = lines.join
  res, = analyze(@uri, tmp_text)
  if res && res[:completion]
    results = res[:completion].keys.map do |name|
      {
        label: name,
        kind: 2, # Method
      }
    end
    return CompletionSession.new(results, row, start_offset)
  else
    nil
  end
end
on_text_changed() click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 295
def on_text_changed
  cancel_token = AnalysisToken.new
  @last_analysis_cancel_token&.cancel
  @last_analysis_cancel_token = cancel_token

  uri = @uri
  text = @text
  self.push_analysis_queue do
    if cancel_token.cancelled?
      next
    end
    res, def_table, caller_table = self.analyze(uri, text, cancel_token: cancel_token)
    unless cancel_token.cancelled?
      on_text_changed_analysis(res, def_table, caller_table)
    end
  end
end
on_text_changed_analysis(res, definition_table, caller_table) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 313
def on_text_changed_analysis(res, definition_table, caller_table)
  @definition_table = definition_table
  @caller_table = caller_table
  return unless res

  @sigs = []
  res[:sigs].each do |file, lineno, sig_str, rbs_code_range, class_kind, class_name|
    uri0 = "file://" + file
    if @uri == uri0
      command = { title: sig_str }
      if rbs_code_range
        command[:command] = "typeprof.jumpToRBS"
        command[:arguments] = [uri0, { line: lineno - 1, character: 0 }, @server.root_uri + "/" + rbs_code_range[0], rbs_code_range[1].to_lsp]
      else
        command[:command] = "typeprof.createPrototypeRBS"
        command[:arguments] = [class_kind, class_name, sig_str]
      end
      @sigs << {
        range: {
          start: { line: lineno - 1, character: 0 },
          end: { line: lineno - 1, character: 1 },
        },
        command: command,
      }
    end
  end

  diagnostics = {}
  res[:errors]&.each do |(file, code_range), msg|
    next unless file and code_range
    uri0 = "file://" + file
    diagnostics[uri0] ||= []
    diagnostics[uri0] << {
      range: code_range.to_lsp,
      severity: 1,
      source: "TypeProf",
      message: msg,
    }
  end

  @server.send_request("workspace/codeLens/refresh")

  @server.send_notification(
    "textDocument/publishDiagnostics",
    {
      uri: @uri,
      version: version,
      diagnostics: diagnostics[@uri] || [],
    }
  )
end
push_analysis_queue(&work) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 291
def push_analysis_queue(&work)
  @analysis_queue.push(work)
end
signature_help(loc, trigger_kind) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 248
def signature_help(loc, trigger_kind)
  loc = CodeLocation.from_lsp(loc)

  res, = analyze(@uri, @text, signature_help_loc: loc)

  if res
    res[:signature_help].filter_map do |sig_str, sig_help, node_id|
      node = ISeq.find_node_by_id(@text, node_id)
      if node && ISeq.code_range_from_node(node).contain_loc?(loc)
        idx = locate_arg_index_in_signature_help(node, loc, sig_help)

        {
          label: sig_str,
          parameters: sig_help.values.map do |r|
            {
              label: [r.begin, r.end],
            }
          end,
          activeParameter: idx,
        }
      end
    end
  else
    nil
  end
end

Private Instance Methods

locate_arg_index_in_signature_help(node, loc, sig_help) click to toggle source
# File typeprof-0.21.2/lib/typeprof/lsp.rb, line 182
        def locate_arg_index_in_signature_help(node, loc, sig_help)
  case node.type
  when :FCALL
    _mid, args_node = node.children
  when :CALL
    _recv, _mid, args_node = node.children
  end

  idx = 0

  if args_node
    arg_nodes = args_node.children.compact

    arg_indexes = {}
    hash = arg_nodes.pop if arg_nodes.last&.type == :HASH

    arg_nodes.each_with_index do |node, i|
      # Ingore arguments after rest argument
      break if node.type == :LIST || node.type == :ARGSCAT

      arg_indexes[i] = ISeq.code_range_from_node(node)
    end

    # Handle keyword arguments
    if hash
      hash.children.last.children.compact.each_slice(2) do |node1, node2|
        # key:  expression
        # ^^^^  ^^^^^^^^^^
        # node1 node2
        key = node1.children.first
        arg_indexes[key] =
          CodeRange.new(
            CodeLocation.new(node1.first_lineno, node1.first_lineno),
            CodeLocation.new(node2.last_lineno, node2.last_lineno),
          )
      end
    end

    if arg_indexes.size >= 1 && arg_indexes.values.last.last < loc
      # There is the cursor after the last argument: "foo(111, 222,|)"
      idx = arg_indexes.size - 1
      prev_cr = arg_indexes.values.last
      if prev_cr.last.lineno == loc.lineno
        line = @text.lines[prev_cr.last.lineno - 1]
        idx += 1 if line[prev_cr.last.column..loc.column].include?(",")
      end
    else
      # There is the cursor within any argument: "foo(111,|222)" or foo(111, 22|2)"
      prev_cr = nil
      arg_indexes.each do |i, cr|
        idx = sig_help.keys.index(i)
        if loc < cr.first
          break if !prev_cr || prev_cr.last.lineno != loc.lineno
          line = @text.lines[prev_cr.last.lineno - 1]
          idx -= 1 unless line[prev_cr.last.column..loc.column].include?(",")
          break
        end
        break if loc <= cr.last
        prev_cr = cr
      end
    end
  end

  idx
end