kapusta 0.13.2 → 0.14.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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -11
  3. data/bin/check-all +6 -1
  4. data/bin/compile-examples +7 -2
  5. data/examples/anagram.kap +3 -3
  6. data/examples/app/args.kap +9 -0
  7. data/examples/arrange-coins.kap +3 -3
  8. data/examples/baseball-game.kap +5 -5
  9. data/examples/best-time-to-buy-sell-stock.kap +2 -2
  10. data/examples/binary-search.kap +2 -2
  11. data/examples/binary-to-decimal.kap +1 -1
  12. data/examples/blocks-and-kwargs.kap +2 -2
  13. data/examples/count-effects.kap +1 -1
  14. data/examples/count-items-matching-rule.kap +18 -0
  15. data/examples/doto-hygiene.kap +2 -2
  16. data/examples/doto.kap +2 -2
  17. data/examples/egg-count.kap +1 -1
  18. data/examples/exceptions.kap +1 -1
  19. data/examples/falling-drops.kap +7 -7
  20. data/examples/fennel-parity-examples.txt +3 -6
  21. data/examples/good-pairs.kap +18 -0
  22. data/examples/greet.kap +1 -1
  23. data/examples/happy-number.kap +2 -2
  24. data/examples/left-right-difference.kap +60 -0
  25. data/examples/length-of-last-word.kap +4 -4
  26. data/examples/manhattan-distance.kap +1 -1
  27. data/examples/maximum-subarray.kap +23 -7
  28. data/examples/minimum-start-value.kap +19 -0
  29. data/examples/move-zeroes.kap +2 -2
  30. data/examples/mruby-runtime-examples.txt +8 -2
  31. data/examples/non-constant-local.kap +1 -1
  32. data/examples/number-of-1-bits.kap +1 -1
  33. data/examples/number-of-steps.kap +1 -1
  34. data/examples/palindrome.kap +2 -2
  35. data/examples/pangram.kap +4 -4
  36. data/examples/pcall.kap +3 -3
  37. data/examples/pipeline.kap +4 -4
  38. data/examples/plus-one.kap +3 -3
  39. data/examples/raindrops.kap +1 -1
  40. data/examples/range-width.kap +14 -0
  41. data/examples/recent-counter.kap +6 -6
  42. data/examples/require-local-args.kap +9 -0
  43. data/examples/require-local.kap +7 -0
  44. data/examples/require-module-local.kap +4 -0
  45. data/examples/require-module.kap +4 -0
  46. data/examples/reverse-integer.kap +1 -1
  47. data/examples/roman-to-integer.kap +3 -3
  48. data/examples/running-sum.kap +20 -0
  49. data/examples/safe-lookup.kap +2 -2
  50. data/examples/single-number.kap +1 -1
  51. data/examples/stack.kap +14 -14
  52. data/examples/subtract-product-sum.kap +1 -1
  53. data/examples/summary-ranges.kap +45 -0
  54. data/examples/threading.kap +5 -5
  55. data/examples/tset.kap +1 -1
  56. data/examples/two-sum-hash.kap +3 -3
  57. data/examples/two-sum.kap +1 -1
  58. data/examples/ugly-number.kap +1 -1
  59. data/examples/underground-system.kap +39 -0
  60. data/examples/valid-parentheses-1.kap +6 -6
  61. data/exe/kapusta-ls +49 -2
  62. data/lib/kapusta/ast.rb +8 -0
  63. data/lib/kapusta/compiler/emitter/bindings.rb +111 -89
  64. data/lib/kapusta/compiler/emitter/collections.rb +32 -40
  65. data/lib/kapusta/compiler/emitter/control_flow.rb +33 -31
  66. data/lib/kapusta/compiler/emitter/expressions.rb +21 -5
  67. data/lib/kapusta/compiler/emitter/interop.rb +168 -48
  68. data/lib/kapusta/compiler/emitter/patterns.rb +12 -14
  69. data/lib/kapusta/compiler/emitter/support.rb +63 -81
  70. data/lib/kapusta/compiler/language.rb +522 -0
  71. data/lib/kapusta/compiler/lua_compat.rb +23 -28
  72. data/lib/kapusta/compiler/macro_expander.rb +30 -30
  73. data/lib/kapusta/compiler/macro_lowerer.rb +12 -24
  74. data/lib/kapusta/compiler/normalizer.rb +25 -17
  75. data/lib/kapusta/compiler.rb +3 -24
  76. data/lib/kapusta/env.rb +2 -2
  77. data/lib/kapusta/errors.rb +2 -1
  78. data/lib/kapusta/formatter/ast_helpers.rb +78 -0
  79. data/lib/kapusta/formatter/cli.rb +125 -0
  80. data/lib/kapusta/formatter/line_helpers.rb +44 -0
  81. data/lib/kapusta/formatter/validator.rb +32 -0
  82. data/lib/kapusta/formatter.rb +354 -325
  83. data/lib/kapusta/lsp/identifier.rb +1 -1
  84. data/lib/kapusta/lsp/rename.rb +21 -11
  85. data/lib/kapusta/lsp/scope_walker.rb +122 -212
  86. data/lib/kapusta/lsp/workspace_index.rb +17 -5
  87. data/lib/kapusta/reader.rb +4 -2
  88. data/lib/kapusta/version.rb +1 -1
  89. data/lib/kapusta.rb +39 -6
  90. data/spec/cli_spec.rb +13 -0
  91. data/spec/examples_errors_spec.rb +3 -1
  92. data/spec/examples_spec.rb +67 -15
  93. data/spec/formatter_spec.rb +246 -0
  94. data/spec/lsp_spec.rb +69 -0
  95. data/spec/require_spec.rb +294 -0
  96. metadata +20 -2
  97. data/examples/describe.kap +0 -9
@@ -65,7 +65,7 @@ module Kapusta
65
65
  result = []
66
66
  @entries.each do |uri, entry|
67
67
  entry.walker.bindings.each do |b|
68
- next unless %i[module class].include?(b.kind)
68
+ next unless Compiler::Language.header_scope?(b.kind)
69
69
 
70
70
  segs = b.sym.dotted? ? b.sym.segments : [b.sym.name]
71
71
  result << [uri, b] if segs == prefix
@@ -104,7 +104,7 @@ module Kapusta
104
104
  def constant_definition_with_prefix?(prefix, except_prefix: nil)
105
105
  @entries.any? do |_uri, entry|
106
106
  entry.walker.bindings.any? do |b|
107
- next false unless %i[module class].include?(b.kind)
107
+ next false unless Compiler::Language.header_scope?(b.kind)
108
108
  next false if except_prefix && matches_prefix?(b.sym, except_prefix)
109
109
 
110
110
  matches_prefix?(b.sym, prefix)
@@ -147,7 +147,7 @@ module Kapusta
147
147
  @entries.each do |uri, entry|
148
148
  occs = []
149
149
  entry.walker.bindings.each do |b|
150
- next unless %i[module class].include?(b.kind)
150
+ next unless Compiler::Language.header_scope?(b.kind)
151
151
 
152
152
  occs << b if matches_prefix?(b.sym, prefix)
153
153
  end
@@ -171,7 +171,13 @@ module Kapusta
171
171
  end
172
172
 
173
173
  def matches_prefix?(sym, prefix)
174
- segs = sym.dotted? ? sym.segments : [sym.name]
174
+ segs = if sym.dotted?
175
+ sym.segments
176
+ elsif sym.colonized?
177
+ sym.colon_segments
178
+ else
179
+ [sym.name]
180
+ end
175
181
  return false if segs.length < prefix.length
176
182
 
177
183
  segs[0...prefix.length] == prefix
@@ -194,7 +200,13 @@ module Kapusta
194
200
  end
195
201
 
196
202
  def first_segment_capitalized?(sym)
197
- first = sym.dotted? ? sym.segments.first : sym.name
203
+ first = if sym.dotted?
204
+ sym.segments.first
205
+ elsif sym.colonized?
206
+ sym.colon_segments.first
207
+ else
208
+ sym.name
209
+ end
198
210
  first.match?(/\A[A-Z]/)
199
211
  end
200
212
 
@@ -286,7 +286,7 @@ module Kapusta
286
286
  break unless token.start_with?('.') && token.length > 1
287
287
 
288
288
  token[1..].split('.').each do |name|
289
- current = List.new([Sym.new(':'), current, Kapusta.kebab_to_snake(name).to_sym])
289
+ current = List.new([Sym.new('.'), current, Kapusta.kebab_to_snake(name).to_sym])
290
290
  end
291
291
  end
292
292
 
@@ -345,7 +345,9 @@ module Kapusta
345
345
  raise reader_error(:bad_shorthand, source_position) unless value.is_a?(Sym)
346
346
 
347
347
  key = Kapusta.kebab_to_snake(value.name).to_sym
348
- [key, value]
348
+ pair = [key, value]
349
+ pair.define_singleton_method(:shorthand?) { true }
350
+ pair
349
351
  else
350
352
  [item, value]
351
353
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.13.2'
4
+ VERSION = '0.14.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -11,6 +11,8 @@ require_relative 'kapusta/compiler'
11
11
 
12
12
  module Kapusta
13
13
  @loaded_kapusta_features = {}
14
+ LOADING_KAPUSTA_FEATURE = Object.new
15
+ private_constant :LOADING_KAPUSTA_FEATURE
14
16
 
15
17
  def self.eval(source, path: '(eval)', **_opts)
16
18
  install!
@@ -29,12 +31,15 @@ module Kapusta
29
31
 
30
32
  def self.require(feature, relative_to: nil)
31
33
  install!
32
- feature = feature.to_s
34
+ feature = require_feature_name(feature)
33
35
  local_path = resolve_require_path(feature, relative_to:)
34
36
 
35
37
  return require_kapusta_file(local_path) if local_path&.end_with?('.kap')
36
38
  return Kernel.require(local_path) if local_path
37
39
 
40
+ kap_path = resolve_load_path_kapusta_feature(feature, relative_to:)
41
+ return require_kapusta_file(kap_path) if kap_path
42
+
38
43
  Kernel.require(feature)
39
44
  end
40
45
 
@@ -99,19 +104,47 @@ module Kapusta
99
104
  candidates.find { |candidate| File.file?(candidate) }
100
105
  end
101
106
 
107
+ def self.require_feature_name(feature)
108
+ feature.is_a?(Symbol) ? feature.to_s.tr('.', '/') : feature.to_s
109
+ end
110
+
111
+ def self.resolve_load_path_kapusta_feature(feature, relative_to:)
112
+ return if local_feature?(feature)
113
+
114
+ load_paths = [require_base_dir(relative_to), *$LOAD_PATH].uniq
115
+ load_paths.each do |load_path|
116
+ candidate = existing_kapusta_feature_path(File.expand_path(feature, load_path))
117
+ return candidate if candidate
118
+ end
119
+ nil
120
+ end
121
+
122
+ def self.existing_kapusta_feature_path(path)
123
+ candidates = File.extname(path).empty? ? ["#{path}.kap"] : [path]
124
+ candidates.find { |candidate| File.file?(candidate) && candidate.end_with?('.kap') }
125
+ end
126
+
102
127
  def self.require_kapusta_file(path)
103
128
  expanded = File.realpath(path)
104
- return false if @loaded_kapusta_features[expanded]
129
+ if @loaded_kapusta_features.key?(expanded)
130
+ cached = @loaded_kapusta_features[expanded]
131
+ return false if cached.equal?(LOADING_KAPUSTA_FEATURE)
132
+
133
+ return cached
134
+ end
105
135
 
106
- @loaded_kapusta_features[expanded] = true
107
- dofile(expanded)
136
+ @loaded_kapusta_features[expanded] = LOADING_KAPUSTA_FEATURE
137
+ value = dofile(expanded)
138
+ @loaded_kapusta_features[expanded] = value
108
139
  $LOADED_FEATURES << expanded unless $LOADED_FEATURES.include?(expanded)
109
- true
140
+ value
110
141
  rescue StandardError, ScriptError
111
142
  @loaded_kapusta_features.delete(expanded) if expanded
112
143
  raise
113
144
  end
114
145
 
115
146
  private_class_method :resolve_require_path, :local_feature?, :require_base_dir,
116
- :existing_feature_path, :require_kapusta_file, :resolve_kap_relative
147
+ :existing_feature_path, :require_feature_name,
148
+ :resolve_load_path_kapusta_feature, :existing_kapusta_feature_path,
149
+ :require_kapusta_file, :resolve_kap_relative
117
150
  end
data/spec/cli_spec.rb CHANGED
@@ -138,4 +138,17 @@ RSpec.describe Kapusta::CLI do
138
138
  expect(status.success?).to eq(true), stderr
139
139
  expect(stdout).to eq("kapusta #{Kapusta::VERSION}\n")
140
140
  end
141
+
142
+ it 'reports kapusta-ls lint diagnostics for an unknown class body form' do
143
+ Dir.mktmpdir do |dir|
144
+ path = File.join(dir, 'broken.kap')
145
+ File.write(path, "(class BankAccount)\n\n(n initialize [owner balance]\n owner)\n\n(end)\n")
146
+
147
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, File.expand_path('../exe/kapusta-ls', __dir__),
148
+ '--lint', path)
149
+
150
+ expect(status.success?).to eq(false), stdout
151
+ expect(stderr).to include('class body form must be a declaration or known special form: n')
152
+ end
153
+ end
141
154
  end
@@ -34,7 +34,9 @@ RSpec.describe 'examples-errors' do
34
34
 
35
35
  it 'bad-multisym.kap' do
36
36
  expect(run_error_example('bad-multisym.kap'))
37
- .to eq("bad-multisym.kap:1:8: bad multisym: unbound.foo\n")
37
+ .to eq('bad-multisym.kap:1:8: bad multisym: unbound.foo; unresolved root unbound; ' \
38
+ 'bind unbound first, use a capitalized constant path, ' \
39
+ "or call a method on an explicit receiver\n")
38
40
  end
39
41
 
40
42
  it 'bad-set-target.kap' do
@@ -226,6 +226,10 @@ RSpec.describe 'examples' do
226
226
  expect(run_example('count-effects.kap')).to eq("1\n2\n")
227
227
  end
228
228
 
229
+ it 'count-items-matching-rule.kap' do
230
+ expect(run_example('count-items-matching-rule.kap')).to eq("2\n1\n1\n")
231
+ end
232
+
229
233
  it 'max-achievable.kap' do
230
234
  expect(run_example('max-achievable.kap')).to eq("6\n7\n10\n")
231
235
  end
@@ -250,21 +254,6 @@ RSpec.describe 'examples' do
250
254
  OUT
251
255
  end
252
256
 
253
- it 'describe.kap' do
254
- expect(run_example('describe.kap')).to eq(<<~OUT)
255
- -3
256
- "negative"
257
- 0
258
- "zero"
259
- 1
260
- "one"
261
- 2
262
- "many"
263
- 99
264
- "many"
265
- OUT
266
- end
267
-
268
257
  it 'destructure.kap' do
269
258
  expect(run_example('destructure.kap')).to eq(<<~OUT)
270
259
  6
@@ -352,6 +341,10 @@ RSpec.describe 'examples' do
352
341
  expect(run_example('gcd.kap')).to eq("12\n6\n")
353
342
  end
354
343
 
344
+ it 'good-pairs.kap' do
345
+ expect(run_example('good-pairs.kap')).to eq("4\n6\n0\n")
346
+ end
347
+
355
348
  it 'greet.kap' do
356
349
  expect(run_example('greet.kap', argv: ['Ada'])).to eq(<<~OUT)
357
350
  "Hello, Ada!"
@@ -379,6 +372,10 @@ RSpec.describe 'examples' do
379
372
  expect(run_example('leap-year.kap')).to eq("true\n")
380
373
  end
381
374
 
375
+ it 'left-right-difference.kap' do
376
+ expect(run_example('left-right-difference.kap')).to eq("true\n")
377
+ end
378
+
382
379
  it 'length-of-last-word.kap' do
383
380
  expect(run_example('length-of-last-word.kap')).to eq("5\n4\n6\n")
384
381
  end
@@ -390,6 +387,17 @@ RSpec.describe 'examples' do
390
387
  OUT
391
388
  end
392
389
 
390
+ it 'minimum-start-value.kap' do
391
+ expect(run_example('minimum-start-value.kap')).to eq(<<~OUT)
392
+ 5
393
+ 2
394
+ 1
395
+ 3
396
+ 5
397
+ -4
398
+ OUT
399
+ end
400
+
393
401
  it 'module-header.kap' do
394
402
  expect(run_example('module-header.kap')).to eq(<<~OUT)
395
403
  "Hello, Ada!"
@@ -426,6 +434,14 @@ RSpec.describe 'examples' do
426
434
  OUT
427
435
  end
428
436
 
437
+ it 'range-width.kap' do
438
+ expect(run_example('range-width.kap')).to eq(<<~OUT)
439
+ 8
440
+ 0
441
+ 11
442
+ OUT
443
+ end
444
+
429
445
  it 'record.kap' do
430
446
  expect(run_example('record.kap')).to eq(<<~OUT)
431
447
  "Ada / engineer / ruby, lisp"
@@ -449,6 +465,10 @@ RSpec.describe 'examples' do
449
465
  OUT
450
466
  end
451
467
 
468
+ it 'running-sum.kap' do
469
+ expect(run_example('running-sum.kap')).to eq("true\ntrue\ntrue\n")
470
+ end
471
+
452
472
  it 'kwargs.kap' do
453
473
  expect(run_example('kwargs.kap')).to eq(<<~OUT)
454
474
  "Ada has 3 tasks"
@@ -558,6 +578,14 @@ RSpec.describe 'examples' do
558
578
  expect(run_example('sum.kap')).to eq("100\n")
559
579
  end
560
580
 
581
+ it 'summary-ranges.kap' do
582
+ expect(run_example('summary-ranges.kap')).to eq(<<~OUT)
583
+ "0->2|4->5|7"
584
+ "0|2->4|6|8->9"
585
+ "empty="
586
+ OUT
587
+ end
588
+
561
589
  it 'tset.kap' do
562
590
  person = { name: 'Ada', city: 'Amsterdam' }
563
591
  expect(run_example('tset.kap')).to eq("#{person.inspect}\n#{'Amsterdam'.inspect}\n")
@@ -571,6 +599,10 @@ RSpec.describe 'examples' do
571
599
  expect(run_example('two-sum-hash.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
572
600
  end
573
601
 
602
+ it 'underground-system.kap' do
603
+ expect(run_example('underground-system.kap')).to eq("11\n14\n")
604
+ end
605
+
574
606
  it 'bst-iterator.kap' do
575
607
  expect(run_example('bst-iterator.kap')).to eq(<<~OUT)
576
608
  3
@@ -715,6 +747,26 @@ RSpec.describe 'examples' do
715
747
  expect(run_example('macros-import-whole.kap')).to eq("7\n")
716
748
  end
717
749
 
750
+ it 'require-local.kap' do
751
+ expect(run_example('require-local.kap')).to eq(<<~OUT)
752
+ "serve"
753
+ "--port 3000"
754
+ "usage: kapusta <command> [options]"
755
+ OUT
756
+ end
757
+
758
+ it 'require-module.kap' do
759
+ expect(run_example('require-module.kap')).to eq(<<~OUT)
760
+ "deploy -> production"
761
+ OUT
762
+ end
763
+
764
+ it 'require-module-local.kap' do
765
+ expect(run_example('require-module-local.kap')).to eq(<<~OUT)
766
+ "deploy -> production"
767
+ OUT
768
+ end
769
+
718
770
  it 'parking-system.kap' do
719
771
  expect(run_example('parking-system.kap')).to eq("true\ntrue\nfalse\nfalse\n")
720
772
  end
@@ -98,6 +98,29 @@ RSpec.describe Kapusta::Formatter do
98
98
  end
99
99
  end
100
100
 
101
+ it 'checks kapm macro modules with quasiquoted function templates' do
102
+ Dir.mktmpdir do |dir|
103
+ path = File.join(dir, 'sample.kapm')
104
+ File.write(path, <<~KAP)
105
+ (fn defcommand [name args runtime-args body1 & body]
106
+ `(fn ,name
107
+ ,args
108
+
109
+ (fn ,runtime-args
110
+ ,body1
111
+ ,(unpack body))))
112
+
113
+ {: defcommand}
114
+ KAP
115
+
116
+ error_output = capture_stderr do
117
+ expect(described_class.new(['--check', path]).run).to eq(0)
118
+ end
119
+
120
+ expect(error_output).to eq('')
121
+ end
122
+ end
123
+
101
124
  it 'reads stdin when the input path is -' do
102
125
  output = with_stdin("(let [name (or (. ARGV 0) \"world\")](puts (.. \"Hello, \" name \"!\")))\n") do
103
126
  capture_stdout do
@@ -271,6 +294,229 @@ RSpec.describe Kapusta::Formatter do
271
294
  end
272
295
  end
273
296
 
297
+ it 'keeps short vector call arguments on the call line' do
298
+ Dir.mktmpdir do |dir|
299
+ path = File.join(dir, 'sample.kap')
300
+ File.write(path, <<~KAP)
301
+ (fn range-label [lo hi]
302
+ (join ""
303
+ [(.. lo) "->" (.. hi)]))
304
+ KAP
305
+
306
+ output = capture_stdout do
307
+ expect(described_class.new([path]).run).to eq(0)
308
+ end
309
+
310
+ expect(output).to eq(<<~KAP)
311
+ (fn range-label [lo hi]
312
+ (join "" [(.. lo) "->" (.. hi)]))
313
+ KAP
314
+ end
315
+ end
316
+
317
+ it 'keeps short local hash values on the local line' do
318
+ Dir.mktmpdir do |dir|
319
+ path = File.join(dir, 'sample.kap')
320
+ File.write(path, <<~KAP)
321
+ (local rule-keys
322
+ {"type" :type
323
+ "color" :color
324
+ "name" :name})
325
+ KAP
326
+
327
+ output = capture_stdout do
328
+ expect(described_class.new([path]).run).to eq(0)
329
+ end
330
+
331
+ expect(output).to eq(<<~KAP)
332
+ (local rule-keys {"type" :type "color" :color "name" :name})
333
+ KAP
334
+ end
335
+ end
336
+
337
+ it 'hangs hash-first call arguments under the first argument' do
338
+ Dir.mktmpdir do |dir|
339
+ path = File.join(dir, 'sample.kap')
340
+ File.write(path, <<~KAP)
341
+ (local check (fn [expected actual] (= expected actual)))
342
+ (local scroll-state (fn [lines visible offset] [lines visible offset]))
343
+
344
+ (check {:preview-offset 0 :preview-total 3 :preview-visible 5}
345
+ (scroll-state ["a" "b" "c"] 5 10))
346
+ KAP
347
+
348
+ output = capture_stdout do
349
+ expect(described_class.new([path]).run).to eq(0)
350
+ end
351
+
352
+ expect(output).to eq(<<~KAP)
353
+ (local check (fn [expected actual] (= expected actual)))
354
+ (local scroll-state (fn [lines visible offset] [lines visible offset]))
355
+
356
+ (check {:preview-offset 0 :preview-total 3 :preview-visible 5}
357
+ (scroll-state ["a" "b" "c"] 5 10))
358
+ KAP
359
+ end
360
+ end
361
+
362
+ it 'packs overflowing call arguments before hanging the rest' do
363
+ Dir.mktmpdir do |dir|
364
+ path = File.join(dir, 'sample.kap')
365
+ File.write(path, <<~KAP)
366
+ (local preview {})
367
+ (local state {})
368
+
369
+ (preview.apply-horizontal-scroll-limit state ["abcdefghijklmnopqrstuvwxyz"] 20 true)
370
+ KAP
371
+
372
+ output = capture_stdout do
373
+ expect(described_class.new([path]).run).to eq(0)
374
+ end
375
+
376
+ expect(output).to eq(<<~KAP)
377
+ (local preview {})
378
+ (local state {})
379
+
380
+ (preview.apply-horizontal-scroll-limit state ["abcdefghijklmnopqrstuvwxyz"] 20
381
+ true)
382
+ KAP
383
+ end
384
+ end
385
+
386
+ it 'wraps long call values in let bindings with hanging arguments' do
387
+ Dir.mktmpdir do |dir|
388
+ path = File.join(dir, 'sample.kap')
389
+ File.write(path, <<~KAP)
390
+ (local entries [])
391
+ (local update {})
392
+
393
+ (let [state (update.init "HEAD" entries {:version 1 :reviews {}} "scope-long" "src-long-name")]
394
+ state)
395
+ KAP
396
+
397
+ output = capture_stdout do
398
+ expect(described_class.new([path]).run).to eq(0)
399
+ end
400
+
401
+ expect(output).to eq(<<~KAP)
402
+ (local entries [])
403
+ (local update {})
404
+
405
+ (let [state (update.init "HEAD" entries {:version 1 :reviews {}} "scope-long"
406
+ "src-long-name")]
407
+ state)
408
+ KAP
409
+ end
410
+ end
411
+
412
+ it 'keeps multiline hash call arguments hanging in let bindings' do
413
+ Dir.mktmpdir do |dir|
414
+ path = File.join(dir, 'sample.kap')
415
+ File.write(path, <<~KAP)
416
+ (local preview-key {})
417
+
418
+ (let [old-key (preview-key.for-entry "HEAD" {:status "M" :kind "M" :path "old.rb" :reviewed false})]
419
+ old-key)
420
+ KAP
421
+
422
+ output = capture_stdout do
423
+ expect(described_class.new([path]).run).to eq(0)
424
+ end
425
+
426
+ expect(output).to eq(<<~KAP)
427
+ (local preview-key {})
428
+
429
+ (let [old-key (preview-key.for-entry "HEAD"
430
+ {:status "M"
431
+ :kind "M"
432
+ :path "old.rb"
433
+ :reviewed false})]
434
+ old-key)
435
+ KAP
436
+ end
437
+ end
438
+
439
+ it 'separates compound items in multiline vectors' do
440
+ Dir.mktmpdir do |dir|
441
+ path = File.join(dir, 'sample.kap')
442
+ File.write(path, <<~KAP)
443
+ (local tree {})
444
+
445
+ (fn entry [status path]
446
+ [status path])
447
+
448
+ (fn check [expected actual]
449
+ [expected actual])
450
+
451
+ (fn test-tree []
452
+ (let [rows (tree.rows [(entry "A" "script/shorthand_branch.sh") (entry "M"
453
+ "spec/lib/epoxy/version_branch_validation_spec.rb")
454
+ (entry "M" "spec/lib/direct_spec.rb") (entry "M"
455
+ "spec/lib/tasks/helpers/commit_validator_spec.rb")])]
456
+ (check [{:type :folder :depth 0 :name "script/" :path "script"}
457
+ {:type :file :depth 1 :name "shorthand_branch.sh" :entry-index 1}
458
+ {:type :folder :depth 1 :name "epoxy/" :path "spec/lib/epoxy"} {:type :file
459
+ :depth 2
460
+ :name "version_branch_validation_spec.rb"
461
+ :entry-index 2}]
462
+ rows)))
463
+ KAP
464
+
465
+ output = capture_stdout do
466
+ expect(described_class.new([path]).run).to eq(0)
467
+ end
468
+
469
+ expect(output).to eq(<<~KAP)
470
+ (local tree {})
471
+
472
+ (fn entry [status path]
473
+ [status path])
474
+
475
+ (fn check [expected actual]
476
+ [expected actual])
477
+
478
+ (fn test-tree []
479
+ (let [rows (tree.rows [(entry "A" "script/shorthand_branch.sh")
480
+ (entry "M"
481
+ "spec/lib/epoxy/version_branch_validation_spec.rb")
482
+ (entry "M" "spec/lib/direct_spec.rb")
483
+ (entry "M"
484
+ "spec/lib/tasks/helpers/commit_validator_spec.rb")])]
485
+ (check [{:type :folder :depth 0 :name "script/" :path "script"}
486
+ {:type :file :depth 1 :name "shorthand_branch.sh" :entry-index 1}
487
+ {:type :folder :depth 1 :name "epoxy/" :path "spec/lib/epoxy"}
488
+ {:type :file
489
+ :depth 2
490
+ :name "version_branch_validation_spec.rb"
491
+ :entry-index 2}]
492
+ rows)))
493
+ KAP
494
+ end
495
+ end
496
+
497
+ it 'hangs function values in set forms' do
498
+ Dir.mktmpdir do |dir|
499
+ path = File.join(dir, 'sample.kap')
500
+ File.write(path, <<~KAP)
501
+ (local loader {})
502
+ (set loader.preview-output
503
+ (fn [...]
504
+ (error "scroll should not load preview lines")))
505
+ KAP
506
+
507
+ output = capture_stdout do
508
+ expect(described_class.new([path]).run).to eq(0)
509
+ end
510
+
511
+ expect(output).to eq(<<~KAP)
512
+ (local loader {})
513
+ (set loader.preview-output
514
+ (fn [...]
515
+ (error "scroll should not load preview lines")))
516
+ KAP
517
+ end
518
+ end
519
+
274
520
  it 'preserves nil-valued let bindings before function bindings' do
275
521
  Dir.mktmpdir do |dir|
276
522
  path = File.join(dir, 'sample.kap')
data/spec/lsp_spec.rb CHANGED
@@ -111,6 +111,16 @@ RSpec.describe Kapusta::LSP do
111
111
  expect(responses.last.dig('params', 'diagnostics')).to be_empty
112
112
  end
113
113
 
114
+ it 'publishes diagnostics for an unknown class body form' do
115
+ responses = run(
116
+ frame_initialize,
117
+ frame_did_open('file:///x.kap', "(class BankAccount)\n\n(n initialize [owner balance]\n owner)\n\n(end)\n")
118
+ )
119
+
120
+ expect(responses.last.dig('params', 'diagnostics', 0, 'message'))
121
+ .to eq('class body form must be a declaration or known special form: n')
122
+ end
123
+
114
124
  it 'returns a TextEdit for formatting' do
115
125
  responses = run(
116
126
  frame_initialize,
@@ -123,6 +133,18 @@ RSpec.describe Kapusta::LSP do
123
133
  expect(edits.first).to include('range', 'newText')
124
134
  end
125
135
 
136
+ it 'formats through the LSP without collapsing full hash pairs into shorthand' do
137
+ text = "(let [active [] id 7]\n{:active active : id})\n"
138
+ responses = run(
139
+ frame_initialize,
140
+ frame_did_open('file:///x.kap', text),
141
+ frame_formatting(uri: 'file:///x.kap')
142
+ )
143
+ edits = result_for(responses)['result']
144
+
145
+ expect(edits.first['newText']).to include('{:active active : id}')
146
+ end
147
+
126
148
  it 'rejects requests sent before initialize' do
127
149
  responses = run(
128
150
  frame(jsonrpc: '2.0', id: 1, method: 'textDocument/formatting',
@@ -145,6 +167,35 @@ RSpec.describe Kapusta::LSP do
145
167
  expect(changes.first['edits'].map { |e| e['newText'] }).to eq(%w[y y y])
146
168
  end
147
169
 
170
+ it 'renames only the base binding in a colon hash lookup shorthand' do
171
+ text = "(let [user {:name \"Ada\"}]\n (print user:name))\n"
172
+ responses = run(
173
+ frame_initialize,
174
+ frame_did_open('file:///x.kap', text),
175
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'user:name'), new_name: 'person')
176
+ )
177
+ edits = result_for(responses)['result']['documentChanges'].first['edits']
178
+
179
+ expect(edits.map { |e| e['newText'] }).to eq(%w[person person])
180
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
181
+ .to contain_exactly([0, 6], [1, 9])
182
+ end
183
+
184
+ it 'does not offer rename on a colon hash key segment' do
185
+ text = "(let [user {:name \"Ada\"}]\n (print user:name))\n"
186
+ key_index = text.index('user:name') + 'user:'.length
187
+ prefix = text[0...key_index]
188
+ last_nl = prefix.rindex("\n")
189
+ position = { line: prefix.count("\n"), character: last_nl ? key_index - last_nl - 1 : key_index }
190
+ responses = run(
191
+ frame_initialize,
192
+ frame_did_open('file:///x.kap', text),
193
+ frame_prepare_rename(uri: 'file:///x.kap', **position)
194
+ )
195
+
196
+ expect(result_for(responses)['result']).to be_nil
197
+ end
198
+
148
199
  it 'renames a let binding referenced inside an accumulate iterator with multiple binders' do
149
200
  text = "(let [xs [1 2 3]\n total (accumulate [s 0 _ x (ipairs xs)] (+ s x))]\n (print total))\n"
150
201
  idx = text.index('(ipairs xs)') + 'ipairs '.length + 1
@@ -359,6 +410,24 @@ RSpec.describe Kapusta::LSP do
359
410
  )
360
411
  end
361
412
 
413
+ it 'jumps from a colon hash lookup base to its local binding' do
414
+ text = "(let [user {:name \"Ada\"}]\n user:name)\n"
415
+ responses = run(
416
+ frame_initialize,
417
+ frame_did_open('file:///x.kap', text),
418
+ frame_definition(uri: 'file:///x.kap', **cursor_at(text, 'user:name'))
419
+ )
420
+ result = result_for(responses)['result']
421
+
422
+ expect(result).to eq(
423
+ 'uri' => 'file:///x.kap',
424
+ 'range' => {
425
+ 'start' => { 'line' => 0, 'character' => 6 },
426
+ 'end' => { 'line' => 0, 'character' => 10 }
427
+ }
428
+ )
429
+ end
430
+
362
431
  it 'rejects renaming a class to a lowercase name with a clear message' do
363
432
  text = "(class Accumulator)\n\n(end)\n"
364
433
  responses = run(