kapusta 0.5.0 → 0.8.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 +4 -4
- data/README.md +24 -6
- data/bin/fennel-parity +11 -4
- data/examples/classify-wallet.kap +11 -0
- data/examples/import-helpers.kapm +9 -0
- data/examples/macros-import-helpers.kap +3 -0
- data/examples/macros-import-whole.kap +5 -0
- data/examples/macros-import.kap +6 -0
- data/examples/power-of-three.kap +12 -0
- data/examples/shared-macros.kapm +4 -0
- data/exe/kapusta-ls +14 -0
- data/kapusta.gemspec +2 -2
- data/lib/kapusta/compiler/emitter/bindings.rb +38 -4
- data/lib/kapusta/compiler/emitter/collections.rb +51 -59
- data/lib/kapusta/compiler/emitter/control_flow.rb +24 -2
- data/lib/kapusta/compiler/emitter/expressions.rb +0 -2
- data/lib/kapusta/compiler/emitter/interop.rb +2 -1
- data/lib/kapusta/compiler/emitter/patterns.rb +52 -4
- data/lib/kapusta/compiler/emitter/support.rb +1 -1
- data/lib/kapusta/compiler/emitter.rb +1 -1
- data/lib/kapusta/compiler/lua_compat.rb +149 -0
- data/lib/kapusta/compiler/macro_expander.rb +55 -141
- data/lib/kapusta/compiler/macro_gensym.rb +21 -0
- data/lib/kapusta/compiler/macro_importer.rb +81 -0
- data/lib/kapusta/compiler/macro_lowerer.rb +184 -0
- data/lib/kapusta/compiler/normalizer.rb +4 -19
- data/lib/kapusta/compiler.rb +4 -2
- data/lib/kapusta/errors.rb +9 -3
- data/lib/kapusta/formatter.rb +4 -0
- data/lib/kapusta/lsp/definition.rb +67 -0
- data/lib/kapusta/lsp/diagnostics.rb +42 -0
- data/lib/kapusta/lsp/formatting.rb +30 -0
- data/lib/kapusta/lsp/identifier.rb +28 -0
- data/lib/kapusta/lsp/rename.rb +417 -0
- data/lib/kapusta/lsp/scope_walker.rb +643 -0
- data/lib/kapusta/lsp/workspace_index.rb +225 -0
- data/lib/kapusta/lsp.rb +312 -0
- data/lib/kapusta/reader.rb +0 -2
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +142 -1
- data/spec/examples_spec.rb +12 -0
- data/spec/lsp_spec.rb +603 -0
- metadata +23 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require_relative '../reader'
|
|
5
|
+
require_relative 'scope_walker'
|
|
6
|
+
|
|
7
|
+
module Kapusta
|
|
8
|
+
class LSP
|
|
9
|
+
class WorkspaceIndex
|
|
10
|
+
Entry = Struct.new(:uri, :text, :forms, :walker, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
MACRO_MODULE_EXTENSIONS = %w[kapm kap].freeze
|
|
13
|
+
SCAN_EXTENSIONS = %w[kap kapm].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(roots: [])
|
|
16
|
+
@roots = Array(roots)
|
|
17
|
+
@entries = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scan!
|
|
21
|
+
@roots.each do |root|
|
|
22
|
+
SCAN_EXTENSIONS.each do |ext|
|
|
23
|
+
Dir.glob(File.join(root, '**', "*.#{ext}")).each do |path|
|
|
24
|
+
uri = path_to_uri(path)
|
|
25
|
+
text = File.read(path)
|
|
26
|
+
store(uri, text)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def refresh(uri, text)
|
|
34
|
+
store(uri, text)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def remove(uri)
|
|
38
|
+
path = uri_to_path(uri)
|
|
39
|
+
if path && File.file?(path)
|
|
40
|
+
store(uri, File.read(path))
|
|
41
|
+
else
|
|
42
|
+
@entries.delete(uri)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def entry(uri)
|
|
47
|
+
@entries[uri]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def entry_count
|
|
51
|
+
@entries.length
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def toplevel_fn_definitions(name)
|
|
55
|
+
result = []
|
|
56
|
+
@entries.each do |uri, entry|
|
|
57
|
+
entry.walker.bindings.each do |b|
|
|
58
|
+
result << [uri, b] if b.kind == :toplevel_fn && b.name == name
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def constant_definitions_with_prefix(prefix)
|
|
65
|
+
result = []
|
|
66
|
+
@entries.each do |uri, entry|
|
|
67
|
+
entry.walker.bindings.each do |b|
|
|
68
|
+
next unless %i[module class].include?(b.kind)
|
|
69
|
+
|
|
70
|
+
segs = b.sym.dotted? ? b.sym.segments : [b.sym.name]
|
|
71
|
+
result << [uri, b] if segs == prefix
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def toplevel_fn_occurrences(name)
|
|
78
|
+
result = {}
|
|
79
|
+
@entries.each do |uri, entry|
|
|
80
|
+
occs = entry.walker.bindings.select do |b|
|
|
81
|
+
b.kind == :toplevel_fn && b.name == name
|
|
82
|
+
end
|
|
83
|
+
occs += entry.walker.references.select do |r|
|
|
84
|
+
next false unless r.sym.is_a?(Sym) && !r.sym.dotted? && r.name == name
|
|
85
|
+
|
|
86
|
+
r.target.nil? || (r.target.kind == :toplevel_fn && r.target.name == name)
|
|
87
|
+
end
|
|
88
|
+
result[uri] = occs unless occs.empty?
|
|
89
|
+
end
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def toplevel_definition?(name, except_name: nil)
|
|
94
|
+
@entries.any? do |_uri, entry|
|
|
95
|
+
entry.walker.bindings.any? do |b|
|
|
96
|
+
next false unless file_toplevel_binding?(b)
|
|
97
|
+
next false if except_name && b.name == except_name
|
|
98
|
+
|
|
99
|
+
b.name == name
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def constant_definition_with_prefix?(prefix, except_prefix: nil)
|
|
105
|
+
@entries.any? do |_uri, entry|
|
|
106
|
+
entry.walker.bindings.any? do |b|
|
|
107
|
+
next false unless %i[module class].include?(b.kind)
|
|
108
|
+
next false if except_prefix && matches_prefix?(b.sym, except_prefix)
|
|
109
|
+
|
|
110
|
+
matches_prefix?(b.sym, prefix)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def each_entry(&)
|
|
116
|
+
@entries.each(&)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def find_macro_definition(importing_uri, module_label, import_key)
|
|
120
|
+
target_name = import_key.to_s.tr('_', '-')
|
|
121
|
+
resolve_module_uris(importing_uri, module_label).each do |uri|
|
|
122
|
+
entry = @entries[uri]
|
|
123
|
+
next unless entry
|
|
124
|
+
|
|
125
|
+
binding = entry.walker.bindings.find do |b|
|
|
126
|
+
%i[macro toplevel_fn].include?(b.kind) && b.name == target_name
|
|
127
|
+
end
|
|
128
|
+
return [uri, binding] if binding
|
|
129
|
+
end
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def import_resolves_to?(importing_uri, module_label, target_uri)
|
|
134
|
+
resolve_module_uris(importing_uri, module_label).include?(target_uri)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def macro_definition_anywhere?(name, except_uri: nil)
|
|
138
|
+
@entries.any? do |uri, entry|
|
|
139
|
+
next false if except_uri && uri == except_uri
|
|
140
|
+
|
|
141
|
+
entry.walker.bindings.any? { |b| b.kind == :macro && b.name == name }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def constant_occurrences(prefix)
|
|
146
|
+
result = {}
|
|
147
|
+
@entries.each do |uri, entry|
|
|
148
|
+
occs = []
|
|
149
|
+
entry.walker.bindings.each do |b|
|
|
150
|
+
next unless %i[module class].include?(b.kind)
|
|
151
|
+
|
|
152
|
+
occs << b if matches_prefix?(b.sym, prefix)
|
|
153
|
+
end
|
|
154
|
+
entry.walker.references.each do |r|
|
|
155
|
+
sym = r.sym
|
|
156
|
+
next unless sym.is_a?(Sym)
|
|
157
|
+
next unless r.target.nil?
|
|
158
|
+
next unless first_segment_capitalized?(sym)
|
|
159
|
+
|
|
160
|
+
occs << r if matches_prefix?(sym, prefix)
|
|
161
|
+
end
|
|
162
|
+
result[uri] = occs unless occs.empty?
|
|
163
|
+
end
|
|
164
|
+
result
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def file_toplevel_binding?(binding)
|
|
170
|
+
binding.scope.kind == :file && %i[toplevel_fn local var].include?(binding.kind)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def matches_prefix?(sym, prefix)
|
|
174
|
+
segs = sym.dotted? ? sym.segments : [sym.name]
|
|
175
|
+
return false if segs.length < prefix.length
|
|
176
|
+
|
|
177
|
+
segs[0...prefix.length] == prefix
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def resolve_module_uris(importing_uri, module_label)
|
|
181
|
+
importing_path = uri_to_path(importing_uri)
|
|
182
|
+
return [] unless importing_path
|
|
183
|
+
|
|
184
|
+
base_dir = File.dirname(importing_path)
|
|
185
|
+
snake_stem = module_label.to_s.tr('-', '_')
|
|
186
|
+
kebab_stem = module_label.to_s.tr('_', '-')
|
|
187
|
+
uris = []
|
|
188
|
+
[kebab_stem, snake_stem].uniq.each do |stem|
|
|
189
|
+
MACRO_MODULE_EXTENSIONS.each do |ext|
|
|
190
|
+
uris << path_to_uri(File.expand_path("#{stem}.#{ext}", base_dir))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
uris
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def first_segment_capitalized?(sym)
|
|
197
|
+
first = sym.dotted? ? sym.segments.first : sym.name
|
|
198
|
+
first.match?(/\A[A-Z]/)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def store(uri, text)
|
|
202
|
+
forms = Reader.read_all(text)
|
|
203
|
+
walker = ScopeWalker.analyze(forms)
|
|
204
|
+
@entries[uri] = Entry.new(uri:, text:, forms:, walker:)
|
|
205
|
+
rescue Kapusta::Error
|
|
206
|
+
@entries[uri] = Entry.new(uri:, text:, forms: [], walker: ScopeWalker.analyze([]))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def path_to_uri(path)
|
|
210
|
+
"file://#{URI::DEFAULT_PARSER.escape(File.expand_path(path))}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def uri_to_path(uri)
|
|
214
|
+
return unless uri.is_a?(String)
|
|
215
|
+
|
|
216
|
+
parsed = URI.parse(uri)
|
|
217
|
+
return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
|
|
218
|
+
|
|
219
|
+
uri
|
|
220
|
+
rescue URI::InvalidURIError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
data/lib/kapusta/lsp.rb
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require_relative '../kapusta'
|
|
6
|
+
require_relative 'lsp/definition'
|
|
7
|
+
require_relative 'lsp/diagnostics'
|
|
8
|
+
require_relative 'lsp/formatting'
|
|
9
|
+
require_relative 'lsp/rename'
|
|
10
|
+
require_relative 'lsp/workspace_index'
|
|
11
|
+
|
|
12
|
+
module Kapusta
|
|
13
|
+
class LSP
|
|
14
|
+
NOT_INITIALIZED = -32_002
|
|
15
|
+
METHOD_NOT_FOUND = -32_601
|
|
16
|
+
FULL_SYNC = 1
|
|
17
|
+
|
|
18
|
+
def self.start(input: $stdin, output: $stdout, log: $stderr)
|
|
19
|
+
new(input:, output:, log:).run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(input:, output:, log:)
|
|
23
|
+
@input = input.binmode
|
|
24
|
+
@output = output.binmode
|
|
25
|
+
@log = log
|
|
26
|
+
@debug = %w[1 true yes on].include?(ENV['KAPUSTA_LS_DEBUG'].to_s.downcase)
|
|
27
|
+
@sources = {}
|
|
28
|
+
@workspace_index = WorkspaceIndex.new
|
|
29
|
+
@initialized = false
|
|
30
|
+
@shutdown = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
until (message = read_message).nil?
|
|
35
|
+
handle(message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def read_message
|
|
42
|
+
headers = read_headers
|
|
43
|
+
return if headers.nil?
|
|
44
|
+
|
|
45
|
+
raw_length = headers['Content-Length']
|
|
46
|
+
return if raw_length.nil?
|
|
47
|
+
|
|
48
|
+
length = Integer(raw_length, 10, exception: false)
|
|
49
|
+
return if length.nil? || length.negative?
|
|
50
|
+
|
|
51
|
+
body = @input.read(length)
|
|
52
|
+
return if body.nil?
|
|
53
|
+
|
|
54
|
+
JSON.parse(body.force_encoding(Encoding::UTF_8))
|
|
55
|
+
rescue JSON::ParserError => e
|
|
56
|
+
log("parse error: #{e.message}")
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_headers
|
|
61
|
+
headers = {}
|
|
62
|
+
loop do
|
|
63
|
+
line = @input.gets
|
|
64
|
+
return if line.nil?
|
|
65
|
+
break if line.chomp.empty?
|
|
66
|
+
|
|
67
|
+
name, value = line.chomp.split(': ', 2)
|
|
68
|
+
headers[name] = value if name && value
|
|
69
|
+
end
|
|
70
|
+
headers
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write_message(payload)
|
|
74
|
+
body = JSON.generate(payload)
|
|
75
|
+
@output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
76
|
+
@output.flush
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle(message)
|
|
80
|
+
method = message['method']
|
|
81
|
+
id = message['id']
|
|
82
|
+
params = message['params'] || {}
|
|
83
|
+
|
|
84
|
+
return handle_pre_init(method, id, params) unless @initialized || method == 'initialize' || method == 'exit'
|
|
85
|
+
|
|
86
|
+
dispatch(method, id, params)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
log("#{e.class}: #{e.message}")
|
|
89
|
+
log(e.backtrace.first(5).join("\n"))
|
|
90
|
+
reply_error(id, METHOD_NOT_FOUND, e.message)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def handle_pre_init(method, id, _params)
|
|
94
|
+
return if id.nil?
|
|
95
|
+
|
|
96
|
+
reply_error(id, NOT_INITIALIZED, "received #{method} before initialize")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def dispatch(method, id, params)
|
|
100
|
+
case method
|
|
101
|
+
when 'initialize'
|
|
102
|
+
on_initialize(params)
|
|
103
|
+
@initialized = true
|
|
104
|
+
reply(id, initialize_result)
|
|
105
|
+
when 'initialized' then nil
|
|
106
|
+
when 'shutdown'
|
|
107
|
+
@shutdown = true
|
|
108
|
+
reply(id, nil)
|
|
109
|
+
when 'exit' then exit(@shutdown ? 0 : 1)
|
|
110
|
+
when 'textDocument/didOpen' then on_did_open(params)
|
|
111
|
+
when 'textDocument/didChange' then on_did_change(params)
|
|
112
|
+
when 'textDocument/didSave' then on_did_save(params)
|
|
113
|
+
when 'textDocument/didClose' then on_did_close(params)
|
|
114
|
+
when 'textDocument/formatting' then reply(id, formatting(params))
|
|
115
|
+
when 'textDocument/definition' then reply(id, definition(params))
|
|
116
|
+
when 'textDocument/prepareRename' then reply(id, prepare_rename(params))
|
|
117
|
+
when 'textDocument/rename' then handle_rename(id, params)
|
|
118
|
+
else
|
|
119
|
+
reply_error(id, METHOD_NOT_FOUND, "method not found: #{method}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reply(id, result)
|
|
124
|
+
return if id.nil?
|
|
125
|
+
|
|
126
|
+
write_message(jsonrpc: '2.0', id:, result:)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reply_error(id, code, message)
|
|
130
|
+
return if id.nil?
|
|
131
|
+
|
|
132
|
+
write_message(jsonrpc: '2.0', id:, error: { code:, message: })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def notify(method, params)
|
|
136
|
+
write_message(jsonrpc: '2.0', method:, params:)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def initialize_result
|
|
140
|
+
{
|
|
141
|
+
capabilities: {
|
|
142
|
+
textDocumentSync: { openClose: true, change: FULL_SYNC, save: { includeText: false } },
|
|
143
|
+
documentFormattingProvider: true,
|
|
144
|
+
definitionProvider: true,
|
|
145
|
+
renameProvider: { prepareProvider: true }
|
|
146
|
+
},
|
|
147
|
+
serverInfo: { name: 'kapusta-ls', version: Kapusta::VERSION }
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def on_initialize(params)
|
|
152
|
+
folders = params['workspaceFolders'] || []
|
|
153
|
+
roots = folders.filter_map { |f| uri_to_path(f['uri']) }
|
|
154
|
+
roots << uri_to_path(params['rootUri']) if params['rootUri']
|
|
155
|
+
roots.compact!
|
|
156
|
+
roots.uniq!
|
|
157
|
+
debug("initialize: roots=#{roots.inspect}")
|
|
158
|
+
@workspace_index = WorkspaceIndex.new(roots:)
|
|
159
|
+
@workspace_index.scan!
|
|
160
|
+
debug("workspace scan complete: #{@workspace_index.entry_count} files")
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
log("workspace scan failed: #{e.class}: #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def on_did_open(params)
|
|
166
|
+
doc = params['textDocument'] || {}
|
|
167
|
+
uri = doc['uri']
|
|
168
|
+
return unless uri
|
|
169
|
+
|
|
170
|
+
version = doc['version']
|
|
171
|
+
text = doc['text'] || ''
|
|
172
|
+
debug("didOpen: uri=#{uri} version=#{version} bytes=#{text.bytesize}")
|
|
173
|
+
store(uri, text, version)
|
|
174
|
+
@workspace_index.refresh(uri, text)
|
|
175
|
+
publish_diagnostics(uri, text, version)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def on_did_change(params)
|
|
179
|
+
uri = params.dig('textDocument', 'uri')
|
|
180
|
+
version = params.dig('textDocument', 'version')
|
|
181
|
+
changes = params['contentChanges'] || []
|
|
182
|
+
return if uri.nil? || changes.empty?
|
|
183
|
+
|
|
184
|
+
text = changes.last['text']
|
|
185
|
+
debug("didChange: uri=#{uri} version=#{version} bytes=#{text.bytesize}")
|
|
186
|
+
store(uri, text, version)
|
|
187
|
+
@workspace_index.refresh(uri, text)
|
|
188
|
+
publish_diagnostics(uri, text, version)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def on_did_save(params)
|
|
192
|
+
uri = params.dig('textDocument', 'uri')
|
|
193
|
+
entry = @sources[uri]
|
|
194
|
+
return unless entry
|
|
195
|
+
|
|
196
|
+
debug("didSave: uri=#{uri}")
|
|
197
|
+
@workspace_index.refresh(uri, entry[:text])
|
|
198
|
+
publish_diagnostics(uri, entry[:text], entry[:version])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def on_did_close(params)
|
|
202
|
+
uri = params.dig('textDocument', 'uri')
|
|
203
|
+
return unless uri
|
|
204
|
+
|
|
205
|
+
debug("didClose: uri=#{uri}")
|
|
206
|
+
@sources.delete(uri)
|
|
207
|
+
@workspace_index.remove(uri)
|
|
208
|
+
notify('textDocument/publishDiagnostics', { uri:, diagnostics: [] })
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def formatting(params)
|
|
212
|
+
uri = params.dig('textDocument', 'uri')
|
|
213
|
+
entry = @sources[uri]
|
|
214
|
+
return [] unless entry
|
|
215
|
+
|
|
216
|
+
Formatting.text_edits(entry[:text], uri_to_path(uri))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def definition(params)
|
|
220
|
+
uri = params.dig('textDocument', 'uri')
|
|
221
|
+
pos = params['position'] || {}
|
|
222
|
+
entry = @sources[uri]
|
|
223
|
+
return unless entry
|
|
224
|
+
|
|
225
|
+
result = Definition.find(uri, entry[:text], pos['line'] || 0, pos['character'] || 0,
|
|
226
|
+
workspace_index: @workspace_index)
|
|
227
|
+
debug("definition: uri=#{uri} pos=#{pos.inspect} result=#{result.inspect}")
|
|
228
|
+
result
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def prepare_rename(params)
|
|
232
|
+
uri = params.dig('textDocument', 'uri')
|
|
233
|
+
pos = params['position'] || {}
|
|
234
|
+
entry = @sources[uri]
|
|
235
|
+
unless entry
|
|
236
|
+
debug("prepareRename: no source for uri=#{uri.inspect}; tracked=#{@sources.keys.inspect}")
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
result = Rename.prepare(entry[:text], pos['line'] || 0, pos['character'] || 0)
|
|
241
|
+
debug("prepareRename: uri=#{uri} pos=#{pos.inspect} result=#{result.inspect}")
|
|
242
|
+
result
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def handle_rename(id, params)
|
|
246
|
+
uri = params.dig('textDocument', 'uri')
|
|
247
|
+
pos = params['position'] || {}
|
|
248
|
+
new_name = params['newName']
|
|
249
|
+
entry = @sources[uri]
|
|
250
|
+
unless entry
|
|
251
|
+
debug("rename: no source for uri=#{uri.inspect}; tracked=#{@sources.keys.inspect}")
|
|
252
|
+
return reply(id, nil)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
debug("rename: uri=#{uri} pos=#{pos.inspect} newName=#{new_name.inspect}")
|
|
256
|
+
result = Rename.perform(uri, entry[:text], pos['line'] || 0, pos['character'] || 0,
|
|
257
|
+
new_name, workspace_index: @workspace_index)
|
|
258
|
+
if result[:error]
|
|
259
|
+
debug("rename error: #{result[:error].inspect}")
|
|
260
|
+
reply_error(id, result[:error][:code], result[:error][:message])
|
|
261
|
+
else
|
|
262
|
+
edit = build_workspace_edit(result[:changes])
|
|
263
|
+
debug("rename ok: files=#{result[:changes].keys.length} edits=#{result[:changes].values.sum(&:length)}")
|
|
264
|
+
reply(id, edit)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def build_workspace_edit(changes_by_uri)
|
|
269
|
+
document_changes = changes_by_uri.map do |uri, edits|
|
|
270
|
+
sorted = edits.sort_by { |e| [-e[:range][:start][:line], -e[:range][:start][:character]] }
|
|
271
|
+
version = @sources.dig(uri, :version)
|
|
272
|
+
{
|
|
273
|
+
textDocument: { uri:, version: },
|
|
274
|
+
edits: sorted
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
{ documentChanges: document_changes }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def store(uri, text, version)
|
|
281
|
+
@sources[uri] = { text:, version: }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def publish_diagnostics(uri, text, version)
|
|
285
|
+
diagnostics = Diagnostics.collect(text, uri_to_path(uri))
|
|
286
|
+
params = { uri:, diagnostics: }
|
|
287
|
+
params[:version] = version unless version.nil?
|
|
288
|
+
notify('textDocument/publishDiagnostics', params)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def uri_to_path(uri)
|
|
292
|
+
return unless uri
|
|
293
|
+
|
|
294
|
+
parsed = URI.parse(uri)
|
|
295
|
+
return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
|
|
296
|
+
|
|
297
|
+
uri
|
|
298
|
+
rescue URI::InvalidURIError
|
|
299
|
+
uri
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def log(message)
|
|
303
|
+
@log.puts "kapusta-ls: #{message}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def debug(message)
|
|
307
|
+
return unless @debug
|
|
308
|
+
|
|
309
|
+
@log.puts "kapusta-ls[debug]: #{message}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
data/lib/kapusta/reader.rb
CHANGED
data/lib/kapusta/version.rb
CHANGED