pink_spoon 0.1.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.
- checksums.yaml +7 -0
- data/bin/install-addon +34 -0
- data/bin/pink-spoon +8 -0
- data/lib/pink_spoon/constant_resolver.rb +469 -0
- data/lib/pink_spoon/definition_finder.rb +144 -0
- data/lib/pink_spoon/doc_extractor.rb +265 -0
- data/lib/pink_spoon/rbi_index.rb +334 -0
- data/lib/pink_spoon/server.rb +173 -0
- data/lib/pink_spoon/version.rb +5 -0
- data/lib/pink_spoon.rb +7 -0
- data/lib/ruby_lsp/pink_spoon/addon.rb +52 -0
- data/lib/ruby_lsp/pink_spoon/code_lens_listener.rb +57 -0
- data/lib/ruby_lsp/pink_spoon/completion_listener.rb +200 -0
- data/lib/ruby_lsp/pink_spoon/definition_listener.rb +425 -0
- data/lib/ruby_lsp/pink_spoon/hover_listener.rb +94 -0
- metadata +80 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "rbi_index"
|
|
5
|
+
require_relative "constant_resolver"
|
|
6
|
+
require_relative "definition_finder"
|
|
7
|
+
|
|
8
|
+
module PinkSpoon
|
|
9
|
+
# JSON-RPC 2.0 server over stdin/stdout.
|
|
10
|
+
# Implements only initialize, textDocument/definition, and textDocument/hover.
|
|
11
|
+
# Every other request gets a null result so clients don't hang.
|
|
12
|
+
class Server
|
|
13
|
+
def initialize
|
|
14
|
+
@rbi_index = nil # built lazily after initialize gives us the project root
|
|
15
|
+
@constant_resolver = nil
|
|
16
|
+
@definition_finder = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
$stdout.sync = true
|
|
21
|
+
$stderr.sync = true
|
|
22
|
+
|
|
23
|
+
loop do
|
|
24
|
+
header = read_header
|
|
25
|
+
break unless header
|
|
26
|
+
|
|
27
|
+
length = header[/Content-Length:\s*(\d+)/i, 1]&.to_i
|
|
28
|
+
next unless length&.positive?
|
|
29
|
+
|
|
30
|
+
body = $stdin.read(length)
|
|
31
|
+
next unless body
|
|
32
|
+
|
|
33
|
+
request = JSON.parse(body, symbolize_names: true)
|
|
34
|
+
handle(request)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
# Request routing
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def handle(req)
|
|
45
|
+
id = req[:id]
|
|
46
|
+
method = req[:method]
|
|
47
|
+
|
|
48
|
+
case method
|
|
49
|
+
when "initialize"
|
|
50
|
+
on_initialize(id, req[:params])
|
|
51
|
+
when "initialized"
|
|
52
|
+
# notification, no response needed
|
|
53
|
+
when "shutdown"
|
|
54
|
+
write_response(id, nil)
|
|
55
|
+
when "exit"
|
|
56
|
+
exit(0)
|
|
57
|
+
when "textDocument/definition"
|
|
58
|
+
write_response(id, on_definition(req[:params]))
|
|
59
|
+
when "textDocument/hover"
|
|
60
|
+
write_response(id, on_hover(req[:params]))
|
|
61
|
+
else
|
|
62
|
+
# Return null for anything we don't handle so the client moves on.
|
|
63
|
+
write_response(id, nil) if id
|
|
64
|
+
end
|
|
65
|
+
rescue => e
|
|
66
|
+
$stderr.puts "[pink-spoon] error handling #{req[:method]}: #{e.class}: #{e.message}"
|
|
67
|
+
$stderr.puts e.backtrace.first(10).join("\n")
|
|
68
|
+
write_error(id, -32_603, e.message) if id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# Handlers
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def on_initialize(id, params)
|
|
76
|
+
root_uri = params&.dig(:rootUri) || params&.dig(:rootPath)
|
|
77
|
+
root_path = root_uri&.delete_prefix("file://") || Dir.pwd
|
|
78
|
+
|
|
79
|
+
@rbi_index = RbiIndex.new(root_path)
|
|
80
|
+
@constant_resolver = ConstantResolver.new(root_path, @rbi_index)
|
|
81
|
+
@definition_finder = DefinitionFinder.new(root_path)
|
|
82
|
+
|
|
83
|
+
write_response(id, {
|
|
84
|
+
capabilities: {
|
|
85
|
+
definitionProvider: true,
|
|
86
|
+
hoverProvider: true,
|
|
87
|
+
},
|
|
88
|
+
serverInfo: { name: "pink-spoon", version: "0.1.0" },
|
|
89
|
+
})
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def on_definition(params)
|
|
93
|
+
return nil unless @rbi_index
|
|
94
|
+
|
|
95
|
+
file = uri_to_path(params.dig(:textDocument, :uri))
|
|
96
|
+
line = params.dig(:position, :line)
|
|
97
|
+
col = params.dig(:position, :character)
|
|
98
|
+
|
|
99
|
+
resolved = @constant_resolver.resolve_at(file, line, col)
|
|
100
|
+
return nil unless resolved
|
|
101
|
+
|
|
102
|
+
location = @definition_finder.find(resolved[:type], resolved[:method])
|
|
103
|
+
return nil unless location
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
uri: path_to_uri(location[:file]),
|
|
107
|
+
range: {
|
|
108
|
+
start: { line: location[:line] - 1, character: 0 },
|
|
109
|
+
end: { line: location[:line] - 1, character: 0 },
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def on_hover(params)
|
|
115
|
+
return nil unless @rbi_index
|
|
116
|
+
|
|
117
|
+
file = uri_to_path(params.dig(:textDocument, :uri))
|
|
118
|
+
line = params.dig(:position, :line)
|
|
119
|
+
col = params.dig(:position, :character)
|
|
120
|
+
|
|
121
|
+
resolved = @constant_resolver.resolve_at(file, line, col)
|
|
122
|
+
return nil unless resolved
|
|
123
|
+
|
|
124
|
+
sig = @rbi_index.signature_for(resolved[:type], resolved[:method])
|
|
125
|
+
return nil unless sig
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
contents: {
|
|
129
|
+
kind: "markdown",
|
|
130
|
+
value: "```ruby\n#{sig}\n```",
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Protocol helpers
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def read_header
|
|
140
|
+
lines = []
|
|
141
|
+
loop do
|
|
142
|
+
line = $stdin.gets
|
|
143
|
+
return nil if line.nil?
|
|
144
|
+
break if line.strip.empty?
|
|
145
|
+
lines << line.strip
|
|
146
|
+
end
|
|
147
|
+
lines.empty? ? nil : lines.join("\n")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def write_response(id, result)
|
|
151
|
+
return unless id
|
|
152
|
+
body = JSON.generate({ jsonrpc: "2.0", id: id, result: result })
|
|
153
|
+
$stdout.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def write_error(id, code, message)
|
|
157
|
+
body = JSON.generate({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id: id,
|
|
160
|
+
error: { code: code, message: message },
|
|
161
|
+
})
|
|
162
|
+
$stdout.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def uri_to_path(uri)
|
|
166
|
+
uri&.delete_prefix("file://")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def path_to_uri(path)
|
|
170
|
+
"file://#{path}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/pink_spoon.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_lsp/addon"
|
|
4
|
+
|
|
5
|
+
require_relative "../../pink_spoon/version"
|
|
6
|
+
require_relative "../../pink_spoon/rbi_index"
|
|
7
|
+
require_relative "../../pink_spoon/constant_resolver"
|
|
8
|
+
require_relative "../../pink_spoon/doc_extractor"
|
|
9
|
+
require_relative "hover_listener"
|
|
10
|
+
require_relative "definition_listener"
|
|
11
|
+
require_relative "completion_listener"
|
|
12
|
+
require_relative "code_lens_listener"
|
|
13
|
+
|
|
14
|
+
module RubyLsp
|
|
15
|
+
module PinkSpoon
|
|
16
|
+
class Addon < ::RubyLsp::Addon
|
|
17
|
+
def activate(global_state, outgoing_queue)
|
|
18
|
+
root = global_state.workspace_path
|
|
19
|
+
|
|
20
|
+
@rbi_index = ::PinkSpoon::RbiIndex.new(root)
|
|
21
|
+
@constant_resolver = ::PinkSpoon::ConstantResolver.new(root, @rbi_index)
|
|
22
|
+
@doc_extractor = ::PinkSpoon::DocExtractor.new(root)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def deactivate; end
|
|
26
|
+
|
|
27
|
+
def name = "Pink Spoon"
|
|
28
|
+
def version = ::PinkSpoon::VERSION
|
|
29
|
+
|
|
30
|
+
# @override
|
|
31
|
+
def create_hover_listener(response_builder, node_context, dispatcher)
|
|
32
|
+
HoverListener.new(response_builder, node_context, dispatcher, @constant_resolver, @rbi_index, @doc_extractor)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @override
|
|
36
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
37
|
+
DefinitionListener.new(response_builder, uri, node_context, dispatcher, @constant_resolver, @rbi_index, @doc_extractor)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @override
|
|
41
|
+
def create_completion_listener(response_builder, node_context, dispatcher, uri)
|
|
42
|
+
CompletionListener.new(response_builder, node_context, dispatcher, uri, @constant_resolver, @rbi_index)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @override
|
|
46
|
+
def create_code_lens_listener(response_builder, uri, dispatcher)
|
|
47
|
+
CodeLensListener.new(response_builder, uri, dispatcher)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module PinkSpoon
|
|
5
|
+
# Fired on every textDocument/codeLens request.
|
|
6
|
+
# Adds "Run" / "Run in watch mode" code lenses next to RSpec
|
|
7
|
+
# describe/context/it blocks.
|
|
8
|
+
class CodeLensListener
|
|
9
|
+
RSPEC_GROUP_METHODS = %w[describe context feature scenario].freeze
|
|
10
|
+
RSPEC_EXAMPLE_METHODS = %w[it example specify scenario].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(response_builder, uri, dispatcher)
|
|
13
|
+
@response_builder = response_builder
|
|
14
|
+
@uri = uri
|
|
15
|
+
|
|
16
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_call_node_enter(node)
|
|
20
|
+
method_name = node.name.to_s
|
|
21
|
+
return unless RSPEC_GROUP_METHODS.include?(method_name) ||
|
|
22
|
+
RSPEC_EXAMPLE_METHODS.include?(method_name)
|
|
23
|
+
return if node.receiver # only bare calls (RSpec DSL)
|
|
24
|
+
|
|
25
|
+
description = extract_description(node)
|
|
26
|
+
line = node.location.start_line - 1 # 0-based
|
|
27
|
+
|
|
28
|
+
range = Interface::Range.new(
|
|
29
|
+
start: Interface::Position.new(line: line, character: 0),
|
|
30
|
+
end: Interface::Position.new(line: line, character: 0),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@response_builder << Interface::CodeLens.new(
|
|
34
|
+
range: range,
|
|
35
|
+
command: Interface::Command.new(
|
|
36
|
+
title: "▶ Run",
|
|
37
|
+
command: "rubyLsp.runTest",
|
|
38
|
+
arguments: [{ uri: @uri.to_s, name: description, line: line }],
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_description(node)
|
|
46
|
+
first_arg = node.arguments&.arguments&.first
|
|
47
|
+
case first_arg
|
|
48
|
+
when Prism::StringNode then first_arg.unescaped
|
|
49
|
+
when Prism::ConstantReadNode then first_arg.name.to_s
|
|
50
|
+
when Prism::ConstantPathNode then first_arg.slice
|
|
51
|
+
else
|
|
52
|
+
node.name.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module PinkSpoon
|
|
5
|
+
# Fired on every textDocument/completion request.
|
|
6
|
+
#
|
|
7
|
+
# Handles three completion modes:
|
|
8
|
+
# 1. Receiver-qualified call (GAUGE.) → RBI methods for that type.
|
|
9
|
+
# 2. Bare call / partial identifier → locals in scope + class methods
|
|
10
|
+
# from RBI + defs in file + RSpec DSL.
|
|
11
|
+
class CompletionListener
|
|
12
|
+
RSPEC_COMPLETIONS = [
|
|
13
|
+
{ name: "describe", snippet: "describe $1 do\n $0\nend", detail: "RSpec · example group" },
|
|
14
|
+
{ name: "context", snippet: "context \"$1\" do\n $0\nend", detail: "RSpec · example group" },
|
|
15
|
+
{ name: "it", snippet: "it \"$1\" do\n $0\nend", detail: "RSpec · example" },
|
|
16
|
+
{ name: "example", snippet: "example \"$1\" do\n $0\nend", detail: "RSpec · example (alias)" },
|
|
17
|
+
{ name: "specify", snippet: "specify \"$1\" do\n $0\nend", detail: "RSpec · example (alias)" },
|
|
18
|
+
{ name: "let", snippet: "let(:$1) { $0 }", detail: "RSpec · memoized helper" },
|
|
19
|
+
{ name: "let!", snippet: "let!(:$1) { $0 }", detail: "RSpec · eager helper" },
|
|
20
|
+
{ name: "subject", snippet: "subject { $0 }", detail: "RSpec · subject" },
|
|
21
|
+
{ name: "subject!", snippet: "subject!(:$1) { $0 }", detail: "RSpec · named subject" },
|
|
22
|
+
{ name: "before", snippet: "before do\n $0\nend", detail: "RSpec · before hook" },
|
|
23
|
+
{ name: "after", snippet: "after do\n $0\nend", detail: "RSpec · after hook" },
|
|
24
|
+
{ name: "around", snippet: "around do |example|\n $0\n example.run\nend", detail: "RSpec · around hook" },
|
|
25
|
+
{ name: "shared_examples", snippet: "shared_examples \"$1\" do\n $0\nend", detail: "RSpec · shared examples" },
|
|
26
|
+
{ name: "shared_context", snippet: "shared_context \"$1\" do\n $0\nend", detail: "RSpec · shared context" },
|
|
27
|
+
{ name: "include_context", snippet: "include_context \"$1\"", detail: "RSpec · include shared context" },
|
|
28
|
+
{ name: "include_examples", snippet: "include_examples \"$1\"", detail: "RSpec · include shared examples" },
|
|
29
|
+
{ name: "pending", snippet: "pending \"$1\"", detail: "RSpec · mark pending" },
|
|
30
|
+
{ name: "skip", snippet: "skip \"$1\"", detail: "RSpec · skip example" },
|
|
31
|
+
{ name: "aggregate_failures", snippet: "aggregate_failures do\n $0\nend", detail: "RSpec · collect failures" },
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
COMPLETION_KIND_METHOD = 2
|
|
35
|
+
COMPLETION_KIND_VARIABLE = 6
|
|
36
|
+
INSERT_FORMAT_SNIPPET = 2
|
|
37
|
+
INSERT_FORMAT_PLAINTEXT = 1
|
|
38
|
+
|
|
39
|
+
def initialize(response_builder, node_context, dispatcher, uri, resolver, rbi_index)
|
|
40
|
+
@response_builder = response_builder
|
|
41
|
+
@node_context = node_context
|
|
42
|
+
@resolver = resolver
|
|
43
|
+
@rbi_index = rbi_index
|
|
44
|
+
@uri = uri
|
|
45
|
+
|
|
46
|
+
dispatcher.register(
|
|
47
|
+
self,
|
|
48
|
+
:on_call_node_enter,
|
|
49
|
+
:on_local_variable_read_node_enter,
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def on_call_node_enter(node)
|
|
54
|
+
return unless node.equal?(@node_context.node)
|
|
55
|
+
|
|
56
|
+
if node.receiver
|
|
57
|
+
offer_method_completions(node)
|
|
58
|
+
else
|
|
59
|
+
offer_bare_completions(node.name.to_s, node)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Partial identifier with no parens (e.g. `con` → `context`, `cre` → `creditor_city`).
|
|
64
|
+
def on_local_variable_read_node_enter(node)
|
|
65
|
+
return unless node.equal?(@node_context.node)
|
|
66
|
+
offer_bare_completions(node.name.to_s, node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# ----------------------------------------------------------------
|
|
72
|
+
# Receiver-qualified completions: GAUGE.inc → enumerate RBI methods.
|
|
73
|
+
# ----------------------------------------------------------------
|
|
74
|
+
def offer_method_completions(node)
|
|
75
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
76
|
+
program_node = nesting&.find { |n| n.is_a?(Prism::ProgramNode) }
|
|
77
|
+
return unless program_node
|
|
78
|
+
|
|
79
|
+
type = @resolver.resolve_receiver_type(node.receiver, program_node, nesting)
|
|
80
|
+
return unless type
|
|
81
|
+
|
|
82
|
+
range = loc_to_range(node.message_loc || node.location)
|
|
83
|
+
|
|
84
|
+
@rbi_index.methods_for(type).each do |method_name, sig_or_def|
|
|
85
|
+
@response_builder << Interface::CompletionItem.new(
|
|
86
|
+
label: method_name,
|
|
87
|
+
filter_text: method_name,
|
|
88
|
+
kind: COMPLETION_KIND_METHOD,
|
|
89
|
+
detail: sig_or_def&.then { |s| s.start_with?("sig") ? s : nil },
|
|
90
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: method_name),
|
|
91
|
+
insert_text_format: INSERT_FORMAT_PLAINTEXT,
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ----------------------------------------------------------------
|
|
97
|
+
# Bare completions: locals + class methods + file defs + RSpec DSL.
|
|
98
|
+
# ----------------------------------------------------------------
|
|
99
|
+
def offer_bare_completions(partial, node)
|
|
100
|
+
range = loc_to_range(
|
|
101
|
+
node.is_a?(Prism::CallNode) ? (node.message_loc || node.location) : node.location,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
offer_local_var_completions(partial, range)
|
|
105
|
+
offer_class_method_completions(partial, range)
|
|
106
|
+
offer_file_def_completions(partial, range)
|
|
107
|
+
offer_rspec_completions(partial, range)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Local variables in the current scope from NodeContext.
|
|
111
|
+
def offer_local_var_completions(partial, range)
|
|
112
|
+
@node_context.locals_for_scope.each do |sym|
|
|
113
|
+
name = sym.to_s
|
|
114
|
+
next unless partial.empty? || name.start_with?(partial)
|
|
115
|
+
|
|
116
|
+
@response_builder << Interface::CompletionItem.new(
|
|
117
|
+
label: name,
|
|
118
|
+
filter_text: name,
|
|
119
|
+
kind: COMPLETION_KIND_VARIABLE,
|
|
120
|
+
detail: "local variable",
|
|
121
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: name),
|
|
122
|
+
insert_text_format: INSERT_FORMAT_PLAINTEXT,
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Methods of the current class from the RBI index (handles typed mixins/parents).
|
|
128
|
+
def offer_class_method_completions(partial, range)
|
|
129
|
+
type = @node_context.fully_qualified_name
|
|
130
|
+
return unless type && !type.empty?
|
|
131
|
+
|
|
132
|
+
@rbi_index.methods_for(type).each do |method_name, sig_or_def|
|
|
133
|
+
next unless partial.empty? || method_name.start_with?(partial)
|
|
134
|
+
|
|
135
|
+
@response_builder << Interface::CompletionItem.new(
|
|
136
|
+
label: method_name,
|
|
137
|
+
filter_text: method_name,
|
|
138
|
+
kind: COMPLETION_KIND_METHOD,
|
|
139
|
+
detail: sig_or_def&.then { |s| s.start_with?("sig") ? s : nil },
|
|
140
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: method_name),
|
|
141
|
+
insert_text_format: INSERT_FORMAT_PLAINTEXT,
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Scans the current file for `def method_name` lines — catches project-defined
|
|
147
|
+
# methods that aren't in any RBI (e.g. `creditor_city_enabled?`).
|
|
148
|
+
def offer_file_def_completions(partial, range)
|
|
149
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
150
|
+
return unless file && File.exist?(file)
|
|
151
|
+
|
|
152
|
+
seen = {}
|
|
153
|
+
File.foreach(file) do |line|
|
|
154
|
+
m = line.match(/\bdef\s+(?:self\.)?(\w[\w!?]*)/)
|
|
155
|
+
next unless m
|
|
156
|
+
|
|
157
|
+
name = m[1]
|
|
158
|
+
next if seen[name]
|
|
159
|
+
next unless partial.empty? || name.start_with?(partial)
|
|
160
|
+
|
|
161
|
+
seen[name] = true
|
|
162
|
+
@response_builder << Interface::CompletionItem.new(
|
|
163
|
+
label: name,
|
|
164
|
+
filter_text: name,
|
|
165
|
+
kind: COMPLETION_KIND_METHOD,
|
|
166
|
+
detail: "def",
|
|
167
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: name),
|
|
168
|
+
insert_text_format: INSERT_FORMAT_PLAINTEXT,
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
rescue
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# RSpec DSL snippets with tab stops.
|
|
176
|
+
def offer_rspec_completions(partial, range)
|
|
177
|
+
RSPEC_COMPLETIONS.each do |item|
|
|
178
|
+
next unless partial.empty? || item[:name].start_with?(partial)
|
|
179
|
+
|
|
180
|
+
@response_builder << Interface::CompletionItem.new(
|
|
181
|
+
label: item[:name],
|
|
182
|
+
filter_text: item[:name],
|
|
183
|
+
kind: COMPLETION_KIND_METHOD,
|
|
184
|
+
detail: item[:detail],
|
|
185
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: item[:snippet]),
|
|
186
|
+
insert_text_format: INSERT_FORMAT_SNIPPET,
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Converts a Prism location (1-based lines) to an LSP Range (0-based).
|
|
192
|
+
def loc_to_range(loc)
|
|
193
|
+
Interface::Range.new(
|
|
194
|
+
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
|
|
195
|
+
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|