kapusta 0.7.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/bin/fennel-parity +9 -4
- 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/shared-macros.kapm +4 -0
- data/lib/kapusta/compiler/macro_expander.rb +54 -142
- 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/errors.rb +6 -1
- 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 +102 -48
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +17 -1
- data/spec/examples_spec.rb +12 -0
- data/spec/lsp_spec.rb +535 -15
- metadata +16 -1
data/spec/lsp_spec.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'spec_helper'
|
|
|
4
4
|
require 'kapusta/lsp'
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'stringio'
|
|
7
|
+
require 'tmpdir'
|
|
7
8
|
|
|
8
9
|
RSpec.describe Kapusta::LSP do
|
|
9
10
|
def frame(payload)
|
|
@@ -30,29 +31,81 @@ RSpec.describe Kapusta::LSP do
|
|
|
30
31
|
parse_responses(output.string)
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def frame_initialize(folders = [])
|
|
35
|
+
workspace_folders = folders.map { |uri| { uri:, name: 'tmp' } }
|
|
36
|
+
frame(jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
37
|
+
params: { workspaceFolders: workspace_folders })
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def frame_did_open(uri, text, version: 1)
|
|
41
|
+
frame(jsonrpc: '2.0', method: 'textDocument/didOpen',
|
|
42
|
+
params: { textDocument: { uri:, version:, text: } })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def frame_rename(uri:, line:, character:, new_name:, id: 2)
|
|
46
|
+
frame(jsonrpc: '2.0', id:, method: 'textDocument/rename',
|
|
47
|
+
params: { textDocument: { uri: }, position: { line:, character: }, newName: new_name })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def frame_prepare_rename(uri:, line:, character:, id: 2)
|
|
51
|
+
frame(jsonrpc: '2.0', id:, method: 'textDocument/prepareRename',
|
|
52
|
+
params: { textDocument: { uri: }, position: { line:, character: } })
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def frame_definition(uri:, line:, character:, id: 2)
|
|
56
|
+
frame(jsonrpc: '2.0', id:, method: 'textDocument/definition',
|
|
57
|
+
params: { textDocument: { uri: }, position: { line:, character: } })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def frame_formatting(uri:, id: 2)
|
|
61
|
+
frame(jsonrpc: '2.0', id:, method: 'textDocument/formatting',
|
|
62
|
+
params: { textDocument: { uri: } })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cursor_at(text, marker)
|
|
66
|
+
idx = text.index(marker) or raise "marker #{marker.inspect} not found in text"
|
|
67
|
+
prefix = text[0...idx]
|
|
68
|
+
last_nl = prefix.rindex("\n")
|
|
69
|
+
{ line: prefix.count("\n"), character: last_nl ? idx - last_nl - 1 : idx }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def result_for(responses, id = 2)
|
|
73
|
+
responses.find { |m| m['id'] == id }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def with_workspace(files)
|
|
77
|
+
Dir.mktmpdir do |dir|
|
|
78
|
+
uris = files.to_h do |name, text|
|
|
79
|
+
path = File.join(dir, name)
|
|
80
|
+
File.write(path, text)
|
|
81
|
+
[name, "file://#{File.expand_path(path)}"]
|
|
82
|
+
end
|
|
83
|
+
root_uri = "file://#{File.expand_path(dir)}"
|
|
84
|
+
yield(root_uri, uris)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
33
88
|
it 'advertises diagnostics and formatting capabilities on initialize' do
|
|
34
|
-
responses = run(
|
|
89
|
+
responses = run(frame_initialize)
|
|
35
90
|
capabilities = responses.first.dig('result', 'capabilities')
|
|
36
91
|
|
|
37
92
|
expect(capabilities).to include('textDocumentSync', 'documentFormattingProvider')
|
|
93
|
+
expect(capabilities['definitionProvider']).to be(true)
|
|
38
94
|
end
|
|
39
95
|
|
|
40
96
|
it 'publishes diagnostics for invalid source' do
|
|
41
97
|
responses = run(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
params: { textDocument: { uri: 'file:///x.kap', version: 1, text: '(let [x 1] (+ x ()))' } })
|
|
98
|
+
frame_initialize,
|
|
99
|
+
frame_did_open('file:///x.kap', '(let [x 1] (+ x ()))')
|
|
45
100
|
)
|
|
46
|
-
diagnostics = responses.last.dig('params', 'diagnostics')
|
|
47
101
|
|
|
48
|
-
expect(diagnostics).not_to be_empty
|
|
102
|
+
expect(responses.last.dig('params', 'diagnostics')).not_to be_empty
|
|
49
103
|
end
|
|
50
104
|
|
|
51
105
|
it 'publishes no diagnostics for valid source' do
|
|
52
106
|
responses = run(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
params: { textDocument: { uri: 'file:///x.kap', version: 1, text: '(print "hi")' } })
|
|
107
|
+
frame_initialize,
|
|
108
|
+
frame_did_open('file:///x.kap', '(print "hi")')
|
|
56
109
|
)
|
|
57
110
|
|
|
58
111
|
expect(responses.last.dig('params', 'diagnostics')).to be_empty
|
|
@@ -60,13 +113,11 @@ RSpec.describe Kapusta::LSP do
|
|
|
60
113
|
|
|
61
114
|
it 'returns a TextEdit for formatting' do
|
|
62
115
|
responses = run(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
frame(jsonrpc: '2.0', id: 2, method: 'textDocument/formatting',
|
|
67
|
-
params: { textDocument: { uri: 'file:///x.kap' } })
|
|
116
|
+
frame_initialize,
|
|
117
|
+
frame_did_open('file:///x.kap', "(fn greet [x] (print x))\n"),
|
|
118
|
+
frame_formatting(uri: 'file:///x.kap')
|
|
68
119
|
)
|
|
69
|
-
edits = responses
|
|
120
|
+
edits = result_for(responses)['result']
|
|
70
121
|
|
|
71
122
|
expect(edits).to be_an(Array).and(be_one)
|
|
72
123
|
expect(edits.first).to include('range', 'newText')
|
|
@@ -80,4 +131,473 @@ RSpec.describe Kapusta::LSP do
|
|
|
80
131
|
|
|
81
132
|
expect(responses.first.dig('error', 'code')).to eq(-32_002)
|
|
82
133
|
end
|
|
134
|
+
|
|
135
|
+
it 'renames a let binding within a single file' do
|
|
136
|
+
text = '(let [x 1] (+ x x))'
|
|
137
|
+
responses = run(
|
|
138
|
+
frame_initialize,
|
|
139
|
+
frame_did_open('file:///x.kap', text),
|
|
140
|
+
frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'x'), new_name: 'y')
|
|
141
|
+
)
|
|
142
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
143
|
+
|
|
144
|
+
expect(changes.length).to eq(1)
|
|
145
|
+
expect(changes.first['edits'].map { |e| e['newText'] }).to eq(%w[y y y])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'renames a let binding referenced inside an accumulate iterator with multiple binders' do
|
|
149
|
+
text = "(let [xs [1 2 3]\n total (accumulate [s 0 _ x (ipairs xs)] (+ s x))]\n (print total))\n"
|
|
150
|
+
idx = text.index('(ipairs xs)') + 'ipairs '.length + 1
|
|
151
|
+
prefix = text[0...idx]
|
|
152
|
+
last_nl = prefix.rindex("\n")
|
|
153
|
+
position = { line: prefix.count("\n"), character: last_nl ? idx - last_nl - 1 : idx }
|
|
154
|
+
responses = run(
|
|
155
|
+
frame_initialize,
|
|
156
|
+
frame_did_open('file:///x.kap', text),
|
|
157
|
+
frame_rename(uri: 'file:///x.kap', **position, new_name: 'ys')
|
|
158
|
+
)
|
|
159
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
160
|
+
|
|
161
|
+
expect(changes.length).to eq(1)
|
|
162
|
+
expect(changes.first['edits'].map { |e| e['newText'] }).to eq(%w[ys ys])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'renames a for-loop counter referenced inside &until' do
|
|
166
|
+
text = "(for [d 2 10 &until (>= d 5)] (print d))\n"
|
|
167
|
+
responses = run(
|
|
168
|
+
frame_initialize,
|
|
169
|
+
frame_did_open('file:///x.kap', text),
|
|
170
|
+
frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'd'), new_name: 'k')
|
|
171
|
+
)
|
|
172
|
+
edits = result_for(responses)['result']['documentChanges'].first['edits']
|
|
173
|
+
|
|
174
|
+
expect(edits.map { |e| e['newText'] }).to eq(%w[k k k])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'renames a top-level fn across files' do
|
|
178
|
+
text_a = "(fn greet [n] (print n))\n(greet \"x\")\n"
|
|
179
|
+
text_b = "(greet 42)\n"
|
|
180
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
181
|
+
responses = run(
|
|
182
|
+
frame_initialize([root_uri]),
|
|
183
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
184
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'greet'), new_name: 'hello')
|
|
185
|
+
)
|
|
186
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
187
|
+
|
|
188
|
+
expect(changes.map { |c| c['textDocument']['uri'] }).to include(uri['a.kap'], uri['b.kap'])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'renames a module constant rewriting only the matching segment in dotted references' do
|
|
193
|
+
text_a = "(module Foo (fn bar [] 1))\n"
|
|
194
|
+
text_b = "(Foo.bar)\n(Foo.new)\n"
|
|
195
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
196
|
+
responses = run(
|
|
197
|
+
frame_initialize([root_uri]),
|
|
198
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
199
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
|
|
200
|
+
)
|
|
201
|
+
b_edits = result_for(responses)['result']['documentChanges']
|
|
202
|
+
.find { |c| c['textDocument']['uri'] == uri['b.kap'] }['edits']
|
|
203
|
+
|
|
204
|
+
expect(b_edits.map { |e| [e['range']['start']['character'], e['range']['end']['character'], e['newText']] })
|
|
205
|
+
.to contain_exactly([1, 4, 'Bar'], [1, 4, 'Bar'])
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'returns null from prepareRename on a dotted method segment' do
|
|
210
|
+
text = '(Foo.bar 1)'
|
|
211
|
+
responses = run(
|
|
212
|
+
frame_initialize,
|
|
213
|
+
frame_did_open('file:///x.kap', text),
|
|
214
|
+
frame_prepare_rename(uri: 'file:///x.kap', **cursor_at(text, 'bar'))
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
expect(result_for(responses)['result']).to be_nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'renames a constant when the workspace contains hash patterns and the .. special form' do
|
|
221
|
+
text_a = "(class Foo)\n"
|
|
222
|
+
text_b = "(let [{:x x} {:x 1}] (print (.. \"value=\" x)))\n(Foo.new)\n"
|
|
223
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
224
|
+
responses = run(
|
|
225
|
+
frame_initialize([root_uri]),
|
|
226
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
227
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
|
|
228
|
+
)
|
|
229
|
+
response = result_for(responses)
|
|
230
|
+
|
|
231
|
+
expect(response['error']).to be_nil
|
|
232
|
+
uris = response['result']['documentChanges'].map { |c| c['textDocument']['uri'] }
|
|
233
|
+
expect(uris).to include(uri['a.kap'], uri['b.kap'])
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'does not offer rename for method definitions after a bodyless class header' do
|
|
238
|
+
text = "(class Counter)\n(fn initialize [start] start)\n"
|
|
239
|
+
responses = run(
|
|
240
|
+
frame_initialize,
|
|
241
|
+
frame_did_open('file:///x.kap', text),
|
|
242
|
+
frame_prepare_rename(uri: 'file:///x.kap', **cursor_at(text, 'initialize'))
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
expect(result_for(responses)['result']).to be_nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'does not let class methods shadow top-level function references' do
|
|
249
|
+
text = "(fn helper [] 2)\n(class Foo (fn helper [] 1))\n(fn main [] (helper))\n"
|
|
250
|
+
responses = run(
|
|
251
|
+
frame_initialize,
|
|
252
|
+
frame_did_open('file:///x.kap', text),
|
|
253
|
+
frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'helper'), new_name: 'renamed')
|
|
254
|
+
)
|
|
255
|
+
edits = result_for(responses)['result']['documentChanges'].first['edits']
|
|
256
|
+
|
|
257
|
+
expect(edits.map { |e| e['range']['start']['line'] }).to contain_exactly(0, 2)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'rejects top-level rename when the new name exists elsewhere in the workspace' do
|
|
261
|
+
text_a = "(fn foo [] 1)\n"
|
|
262
|
+
text_b = "(foo)\n"
|
|
263
|
+
text_c = "(fn bar [] 2)\n"
|
|
264
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b, 'c.kap' => text_c) do |root_uri, uri|
|
|
265
|
+
responses = run(
|
|
266
|
+
frame_initialize([root_uri]),
|
|
267
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
268
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'foo'), new_name: 'bar')
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
expect(result_for(responses).dig('error', 'message')).to include('already defined')
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'rejects constant rename when the new constant prefix exists' do
|
|
276
|
+
text_a = "(class Foo)\n"
|
|
277
|
+
text_b = "(class Bar)\n"
|
|
278
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
279
|
+
responses = run(
|
|
280
|
+
frame_initialize([root_uri]),
|
|
281
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
282
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
expect(result_for(responses).dig('error', 'message')).to include('already defined')
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'jumps to a let binder from a usage in the same file' do
|
|
290
|
+
text = '(let [x 1] (+ x x))'
|
|
291
|
+
responses = run(
|
|
292
|
+
frame_initialize,
|
|
293
|
+
frame_did_open('file:///x.kap', text),
|
|
294
|
+
frame_definition(uri: 'file:///x.kap', line: 0, character: 14)
|
|
295
|
+
)
|
|
296
|
+
result = result_for(responses)['result']
|
|
297
|
+
|
|
298
|
+
expect(result).to eq(
|
|
299
|
+
'uri' => 'file:///x.kap',
|
|
300
|
+
'range' => {
|
|
301
|
+
'start' => { 'line' => 0, 'character' => 6 },
|
|
302
|
+
'end' => { 'line' => 0, 'character' => 7 }
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'jumps to a top-level fn definition across files' do
|
|
308
|
+
text_a = "(fn greet [n] (print n))\n"
|
|
309
|
+
text_b = "(greet 42)\n"
|
|
310
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
311
|
+
responses = run(
|
|
312
|
+
frame_initialize([root_uri]),
|
|
313
|
+
frame_did_open(uri['b.kap'], text_b),
|
|
314
|
+
frame_definition(uri: uri['b.kap'], **cursor_at(text_b, 'greet'))
|
|
315
|
+
)
|
|
316
|
+
result = result_for(responses)['result']
|
|
317
|
+
|
|
318
|
+
expect(result).to be_an(Array).and(be_one)
|
|
319
|
+
expect(result.first['uri']).to eq(uri['a.kap'])
|
|
320
|
+
expect(result.first['range']['start']).to eq('line' => 0, 'character' => 4)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'jumps to a module definition from a dotted reference across files' do
|
|
325
|
+
text_a = "(module Foo (fn bar [] 1))\n"
|
|
326
|
+
text_b = "(Foo.bar)\n"
|
|
327
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
328
|
+
responses = run(
|
|
329
|
+
frame_initialize([root_uri]),
|
|
330
|
+
frame_did_open(uri['b.kap'], text_b),
|
|
331
|
+
frame_definition(uri: uri['b.kap'], **cursor_at(text_b, 'Foo'))
|
|
332
|
+
)
|
|
333
|
+
result = result_for(responses)['result']
|
|
334
|
+
|
|
335
|
+
expect(result).to be_an(Array).and(be_one)
|
|
336
|
+
expect(result.first['uri']).to eq(uri['a.kap'])
|
|
337
|
+
expect(result.first['range']['start']).to eq('line' => 0, 'character' => 8)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
it 'jumps to a macro definition from a call site in the same file' do
|
|
342
|
+
text = "(macro unless [c & body] `(if (not ,c) (do ,(unpack body))))\n(unless false (print 1))\n"
|
|
343
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
344
|
+
responses = run(
|
|
345
|
+
frame_initialize([root_uri]),
|
|
346
|
+
frame_did_open(uri['a.kap'], text),
|
|
347
|
+
frame_definition(uri: uri['a.kap'], line: 1, character: 1)
|
|
348
|
+
)
|
|
349
|
+
result = result_for(responses)['result']
|
|
350
|
+
|
|
351
|
+
expect(result).to eq(
|
|
352
|
+
'uri' => uri['a.kap'],
|
|
353
|
+
'range' => {
|
|
354
|
+
'start' => { 'line' => 0, 'character' => 7 },
|
|
355
|
+
'end' => { 'line' => 0, 'character' => 13 }
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'jumps from a call site through (import-macros) to the macro definition file' do
|
|
362
|
+
text_a = "(macro swap! [a b] `(let [tmp# ,a] (set ,a ,b) (set ,b tmp#)))\n"
|
|
363
|
+
text_b = "(import-macros {: swap!} :a)\n(var x 1)\n(var y 2)\n(swap! x y)\n"
|
|
364
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
365
|
+
responses = run(
|
|
366
|
+
frame_initialize([root_uri]),
|
|
367
|
+
frame_did_open(uri['b.kap'], text_b),
|
|
368
|
+
frame_definition(uri: uri['b.kap'], line: 3, character: 1)
|
|
369
|
+
)
|
|
370
|
+
result = result_for(responses)['result']
|
|
371
|
+
|
|
372
|
+
expect(result['uri']).to eq(uri['a.kap'])
|
|
373
|
+
expect(result['range']['start']).to eq('line' => 0, 'character' => 7)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'returns null definition when the symbol has no known binding' do
|
|
378
|
+
text = "(foo)\n"
|
|
379
|
+
responses = run(
|
|
380
|
+
frame_initialize,
|
|
381
|
+
frame_did_open('file:///x.kap', text),
|
|
382
|
+
frame_definition(uri: 'file:///x.kap', **cursor_at(text, 'foo'))
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
expect(result_for(responses)['result']).to be_nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
it 'renames a macro definition and its call sites within a file' do
|
|
389
|
+
text = "(macro swap! [a b]\n `(let [tmp# ,a]\n (set ,a ,b)\n (set ,b tmp#)))\n(swap! x y)\n"
|
|
390
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
391
|
+
responses = run(
|
|
392
|
+
frame_initialize([root_uri]),
|
|
393
|
+
frame_did_open(uri['a.kap'], text),
|
|
394
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text, 'swap!'), new_name: 'flip!')
|
|
395
|
+
)
|
|
396
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
397
|
+
|
|
398
|
+
expect(changes.length).to eq(1)
|
|
399
|
+
edits = changes.first['edits']
|
|
400
|
+
expect(edits.map { |e| e['newText'] }).to eq(%w[flip! flip!])
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it 'renames a macro across files when imported via shorthand destructure' do
|
|
405
|
+
text_a = "(macro swap! [a b]\n `(let [tmp# ,a] (set ,a ,b) (set ,b tmp#)))\n"
|
|
406
|
+
text_b = "(import-macros {: swap!} :a)\n(var x 1)\n(var y 2)\n(swap! x y)\n"
|
|
407
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
408
|
+
responses = run(
|
|
409
|
+
frame_initialize([root_uri]),
|
|
410
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
411
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'swap!'), new_name: 'flip!')
|
|
412
|
+
)
|
|
413
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
414
|
+
|
|
415
|
+
uris = changes.map { |c| c['textDocument']['uri'] }
|
|
416
|
+
expect(uris).to include(uri['a.kap'], uri['b.kap'])
|
|
417
|
+
|
|
418
|
+
b_edits = changes.find { |c| c['textDocument']['uri'] == uri['b.kap'] }['edits']
|
|
419
|
+
expect(b_edits.map { |e| e['newText'] }).to all(eq('flip!'))
|
|
420
|
+
expect(b_edits.length).to eq(2)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it 'renames a macro across files when starting from an imported call site' do
|
|
425
|
+
text_a = "(macro swap! [a b]\n `(let [tmp# ,a] (set ,a ,b) (set ,b tmp#)))\n"
|
|
426
|
+
text_b = "(import-macros {: swap!} :a)\n(var x 1)\n(var y 2)\n(swap! x y)\n"
|
|
427
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
428
|
+
responses = run(
|
|
429
|
+
frame_initialize([root_uri]),
|
|
430
|
+
frame_did_open(uri['b.kap'], text_b),
|
|
431
|
+
frame_rename(uri: uri['b.kap'],
|
|
432
|
+
line: 3, character: cursor_at(text_b.lines[3], 'swap!')[:character],
|
|
433
|
+
new_name: 'flip!')
|
|
434
|
+
)
|
|
435
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
436
|
+
|
|
437
|
+
uris = changes.map { |c| c['textDocument']['uri'] }
|
|
438
|
+
expect(uris).to include(uri['a.kap'], uri['b.kap'])
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'renames a user macro that shadows a core special form name' do
|
|
443
|
+
text = "(macro unless [c & body] `(if (not ,c) (do ,(unpack body))))\n" \
|
|
444
|
+
"(unless false (print \"x\"))\n(unless true (print \"y\"))\n"
|
|
445
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
446
|
+
responses = run(
|
|
447
|
+
frame_initialize([root_uri]),
|
|
448
|
+
frame_did_open(uri['a.kap'], text),
|
|
449
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text, 'unless'), new_name: 'except')
|
|
450
|
+
)
|
|
451
|
+
edits = result_for(responses)['result']['documentChanges'].first['edits']
|
|
452
|
+
|
|
453
|
+
expect(edits.map { |e| e['newText'] }).to eq(%w[except except except])
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it 'offers prepareRename on a macro call site shadowing a special form' do
|
|
458
|
+
text = "(macro unless [c & body] `(if (not ,c) (do ,(unpack body))))\n(unless false 1)\n"
|
|
459
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
460
|
+
responses = run(
|
|
461
|
+
frame_initialize([root_uri]),
|
|
462
|
+
frame_did_open(uri['a.kap'], text),
|
|
463
|
+
frame_prepare_rename(uri: uri['a.kap'], line: 1, character: 1)
|
|
464
|
+
)
|
|
465
|
+
result = result_for(responses)['result']
|
|
466
|
+
|
|
467
|
+
expect(result).to include('range', 'placeholder')
|
|
468
|
+
expect(result['placeholder']).to eq('unless')
|
|
469
|
+
expect(result['range']['start']).to eq('line' => 1, 'character' => 1)
|
|
470
|
+
expect(result['range']['end']).to eq('line' => 1, 'character' => 7)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it 'returns null prepareRename for an unshadowed special form call' do
|
|
475
|
+
text = "(unless false 1)\n"
|
|
476
|
+
responses = run(
|
|
477
|
+
frame_initialize,
|
|
478
|
+
frame_did_open('file:///x.kap', text),
|
|
479
|
+
frame_prepare_rename(uri: 'file:///x.kap', line: 0, character: 1)
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
expect(result_for(responses)['result']).to be_nil
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it 'renames only the targeted import when multiple macros are destructured together' do
|
|
486
|
+
text_a = "(macro foo [x] `,x)\n(macro bar [x] `,x)\n"
|
|
487
|
+
text_b = "(import-macros {: foo : bar} :a)\n(foo 1)\n(bar 2)\n"
|
|
488
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b) do |root_uri, uri|
|
|
489
|
+
responses = run(
|
|
490
|
+
frame_initialize([root_uri]),
|
|
491
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
492
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'foo'), new_name: 'qux')
|
|
493
|
+
)
|
|
494
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
495
|
+
b_edits = changes.find { |c| c['textDocument']['uri'] == uri['b.kap'] }['edits']
|
|
496
|
+
|
|
497
|
+
expect(b_edits.map { |e| e['newText'] }).to all(eq('qux'))
|
|
498
|
+
expect(b_edits.length).to eq(2)
|
|
499
|
+
expect(b_edits.flat_map { |e| [e['range']['start']['line'], e['range']['end']['line']] })
|
|
500
|
+
.to all(satisfy { |n| [0, 1].include?(n) })
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'propagates a macro rename to multiple importing files' do
|
|
505
|
+
text_a = "(macro tap [x] `(do (print ,x) ,x))\n"
|
|
506
|
+
text_b = "(import-macros {: tap} :a)\n(tap 1)\n"
|
|
507
|
+
text_c = "(import-macros {: tap} :a)\n(tap 2)\n(tap 3)\n"
|
|
508
|
+
with_workspace('a.kap' => text_a, 'b.kap' => text_b, 'c.kap' => text_c) do |root_uri, uri|
|
|
509
|
+
responses = run(
|
|
510
|
+
frame_initialize([root_uri]),
|
|
511
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
512
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'tap'), new_name: 'spy')
|
|
513
|
+
)
|
|
514
|
+
changes = result_for(responses)['result']['documentChanges']
|
|
515
|
+
|
|
516
|
+
expect(changes.map { |c| c['textDocument']['uri'] })
|
|
517
|
+
.to contain_exactly(uri['a.kap'], uri['b.kap'], uri['c.kap'])
|
|
518
|
+
|
|
519
|
+
changes.each do |c|
|
|
520
|
+
expect(c['edits'].map { |e| e['newText'] }).to all(eq('spy'))
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
c_edits = changes.find { |c| c['textDocument']['uri'] == uri['c.kap'] }['edits']
|
|
524
|
+
expect(c_edits.length).to eq(3)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it 'jumps to itself when invoked on a macro definition name' do
|
|
529
|
+
text = "(macro tap [x] `(do (print ,x) ,x))\n"
|
|
530
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
531
|
+
responses = run(
|
|
532
|
+
frame_initialize([root_uri]),
|
|
533
|
+
frame_did_open(uri['a.kap'], text),
|
|
534
|
+
frame_definition(uri: uri['a.kap'], **cursor_at(text, 'tap'))
|
|
535
|
+
)
|
|
536
|
+
result = result_for(responses)['result']
|
|
537
|
+
|
|
538
|
+
expect(result).to eq(
|
|
539
|
+
'uri' => uri['a.kap'],
|
|
540
|
+
'range' => {
|
|
541
|
+
'start' => { 'line' => 0, 'character' => 7 },
|
|
542
|
+
'end' => { 'line' => 0, 'character' => 10 }
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
it 'jumps through (import-macros) to a fn-defined macro template in a .kapm module' do
|
|
549
|
+
text_helper = "(fn scaled [x] `(* ,x 10))\n{: scaled}\n"
|
|
550
|
+
text_user = "(import-macros {: scaled} :import-helpers)\n(print (scaled 3))\n"
|
|
551
|
+
with_workspace('import-helpers.kapm' => text_helper,
|
|
552
|
+
'macros-import-helpers.kap' => text_user) do |root_uri, uri|
|
|
553
|
+
responses = run(
|
|
554
|
+
frame_initialize([root_uri]),
|
|
555
|
+
frame_did_open(uri['macros-import-helpers.kap'], text_user),
|
|
556
|
+
frame_definition(uri: uri['macros-import-helpers.kap'],
|
|
557
|
+
**cursor_at(text_user.lines[1], 'scaled').merge(line: 1))
|
|
558
|
+
)
|
|
559
|
+
result = result_for(responses)['result']
|
|
560
|
+
|
|
561
|
+
expect(result['uri']).to eq(uri['import-helpers.kapm'])
|
|
562
|
+
expect(result['range']['start']).to eq('line' => 0, 'character' => 4)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it 'returns null definition for a special-form call with no shadowing macro' do
|
|
567
|
+
text = "(unless false 1)\n"
|
|
568
|
+
responses = run(
|
|
569
|
+
frame_initialize,
|
|
570
|
+
frame_did_open('file:///x.kap', text),
|
|
571
|
+
frame_definition(uri: 'file:///x.kap', line: 0, character: 1)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
expect(result_for(responses)['result']).to be_nil
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
it 'rejects macro rename when the new name conflicts with an existing macro definition' do
|
|
578
|
+
text_a = "(macro swap! [a b] `(do ,a ,b))\n(macro flip! [a b] `(do ,b ,a))\n"
|
|
579
|
+
with_workspace('a.kap' => text_a) do |root_uri, uri|
|
|
580
|
+
responses = run(
|
|
581
|
+
frame_initialize([root_uri]),
|
|
582
|
+
frame_did_open(uri['a.kap'], text_a),
|
|
583
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'swap!'), new_name: 'flip!')
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
expect(result_for(responses).dig('error', 'message')).to include('already defined')
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it 'escapes file URIs built during workspace scans' do
|
|
591
|
+
Dir.mktmpdir do |dir|
|
|
592
|
+
nested = File.join(dir, 'space dir')
|
|
593
|
+
Dir.mkdir(nested)
|
|
594
|
+
File.write(File.join(nested, 'a b#c.kap'), "(fn greet [] 1)\n")
|
|
595
|
+
|
|
596
|
+
index = Kapusta::LSP::WorkspaceIndex.new(roots: [nested]).scan!
|
|
597
|
+
uri = index.toplevel_fn_occurrences('greet').keys.first
|
|
598
|
+
|
|
599
|
+
expect(uri).to include('space%20dir')
|
|
600
|
+
expect(uri).to include('a%20b%23c.kap')
|
|
601
|
+
end
|
|
602
|
+
end
|
|
83
603
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kapusta
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evgenii Morozov
|
|
@@ -57,10 +57,14 @@ files:
|
|
|
57
57
|
- examples/greet.kap
|
|
58
58
|
- examples/happy-number.kap
|
|
59
59
|
- examples/hashfn.kap
|
|
60
|
+
- examples/import-helpers.kapm
|
|
60
61
|
- examples/kwargs.kap
|
|
61
62
|
- examples/leap-year.kap
|
|
62
63
|
- examples/length-of-last-word.kap
|
|
63
64
|
- examples/macros-dbg.kap
|
|
65
|
+
- examples/macros-import-helpers.kap
|
|
66
|
+
- examples/macros-import-whole.kap
|
|
67
|
+
- examples/macros-import.kap
|
|
64
68
|
- examples/macros-multi.kap
|
|
65
69
|
- examples/macros-swap.kap
|
|
66
70
|
- examples/macros-thrice-if.kap
|
|
@@ -93,6 +97,7 @@ files:
|
|
|
93
97
|
- examples/safe-lookup.kap
|
|
94
98
|
- examples/scopes.kap
|
|
95
99
|
- examples/shapes.kap
|
|
100
|
+
- examples/shared-macros.kapm
|
|
96
101
|
- examples/single-number.kap
|
|
97
102
|
- examples/squares.kap
|
|
98
103
|
- examples/stack.kap
|
|
@@ -128,12 +133,22 @@ files:
|
|
|
128
133
|
- lib/kapusta/compiler/emitter/support.rb
|
|
129
134
|
- lib/kapusta/compiler/lua_compat.rb
|
|
130
135
|
- lib/kapusta/compiler/macro_expander.rb
|
|
136
|
+
- lib/kapusta/compiler/macro_gensym.rb
|
|
137
|
+
- lib/kapusta/compiler/macro_importer.rb
|
|
138
|
+
- lib/kapusta/compiler/macro_lowerer.rb
|
|
131
139
|
- lib/kapusta/compiler/normalizer.rb
|
|
132
140
|
- lib/kapusta/env.rb
|
|
133
141
|
- lib/kapusta/error.rb
|
|
134
142
|
- lib/kapusta/errors.rb
|
|
135
143
|
- lib/kapusta/formatter.rb
|
|
136
144
|
- lib/kapusta/lsp.rb
|
|
145
|
+
- lib/kapusta/lsp/definition.rb
|
|
146
|
+
- lib/kapusta/lsp/diagnostics.rb
|
|
147
|
+
- lib/kapusta/lsp/formatting.rb
|
|
148
|
+
- lib/kapusta/lsp/identifier.rb
|
|
149
|
+
- lib/kapusta/lsp/rename.rb
|
|
150
|
+
- lib/kapusta/lsp/scope_walker.rb
|
|
151
|
+
- lib/kapusta/lsp/workspace_index.rb
|
|
137
152
|
- lib/kapusta/reader.rb
|
|
138
153
|
- lib/kapusta/support.rb
|
|
139
154
|
- lib/kapusta/version.rb
|