kapusta 0.9.0 → 0.11.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -2
  3. data/bin/check-all +17 -0
  4. data/bin/compile-examples +70 -0
  5. data/bin/fennel-parity +4 -38
  6. data/examples/account-lockout.kap +11 -0
  7. data/examples/accumulator.kap +2 -0
  8. data/examples/bank-account.kap +2 -0
  9. data/examples/bst-iterator.kap +52 -0
  10. data/examples/circle.kap +18 -0
  11. data/examples/convert-temperature.kap +14 -0
  12. data/examples/count-effects.kap +13 -0
  13. data/examples/counter.kap +2 -0
  14. data/examples/falling-drops.kap +12 -0
  15. data/examples/fennel-parity-examples.txt +40 -0
  16. data/examples/hit-counter.kap +2 -0
  17. data/examples/max-achievable.kap +8 -0
  18. data/examples/module-header.kap +4 -2
  19. data/examples/mruby-runtime-examples.txt +92 -0
  20. data/examples/number-of-1-bits.kap +13 -0
  21. data/examples/number-of-steps.kap +15 -0
  22. data/examples/parking-system.kap +2 -0
  23. data/examples/recent-counter.kap +17 -0
  24. data/examples/scopes.kap +2 -0
  25. data/examples/signal-harvest.kap +16 -0
  26. data/examples/stack.kap +2 -0
  27. data/examples/two-sum-hash.kap +11 -14
  28. data/examples/underscore-patterns.kap +1 -1
  29. data/examples/valid-parentheses-1.kap +2 -0
  30. data/lib/kapusta/cli.rb +11 -6
  31. data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
  32. data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
  33. data/lib/kapusta/compiler/emitter/expressions.rb +1 -0
  34. data/lib/kapusta/compiler/emitter/interop.rb +4 -2
  35. data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
  36. data/lib/kapusta/compiler/emitter/support.rb +63 -24
  37. data/lib/kapusta/compiler/emitter.rb +2 -1
  38. data/lib/kapusta/compiler/normalizer.rb +6 -0
  39. data/lib/kapusta/compiler.rb +15 -6
  40. data/lib/kapusta/errors.rb +6 -0
  41. data/lib/kapusta/formatter.rb +1 -2
  42. data/lib/kapusta/lsp/definition.rb +17 -0
  43. data/lib/kapusta/lsp/rename.rb +3 -1
  44. data/lib/kapusta/lsp/scope_walker.rb +40 -11
  45. data/lib/kapusta/lsp.rb +2 -1
  46. data/lib/kapusta/version.rb +1 -1
  47. data/lib/kapusta.rb +2 -2
  48. data/spec/cli_spec.rb +35 -0
  49. data/spec/examples_errors_spec.rb +20 -0
  50. data/spec/examples_spec.rb +136 -0
  51. data/spec/lsp_spec.rb +71 -3
  52. data/spec/spec_helper.rb +9 -0
  53. metadata +14 -1
@@ -10,6 +10,7 @@ module Kapusta
10
10
  bad_set_target: 'bad set target: %{target}',
11
11
  bad_shorthand: 'bad shorthand',
12
12
  bind_table_dots: 'unable to bind table ...',
13
+ cannot_call_constant: 'cannot call constant %{name}; reference it without parentheses',
13
14
  cannot_call_literal: 'cannot call literal value %{value}',
14
15
  cannot_emit_form: 'cannot emit form: %{form}',
15
16
  cannot_set_method_binding: 'cannot set method binding: %{name}',
@@ -24,6 +25,9 @@ module Kapusta
24
25
  dot_no_args: 'expected table argument',
25
26
  each_no_binding: 'expected binding table',
26
27
  empty_call: 'expected a function, macro, or special to call',
28
+ end_outside_header: 'end outside class or module',
29
+ end_with_args: 'end takes no arguments',
30
+ unclosed_header: 'class or module not closed with (end)',
27
31
  expected_var: 'expected var %{name}',
28
32
  fn_no_params: 'expected parameters table',
29
33
  global_arity: 'expected name and value',
@@ -51,6 +55,7 @@ module Kapusta
51
55
  odd_forms_in_hash: 'odd number of forms in hash',
52
56
  rest_not_last: 'expected rest argument before last parameter',
53
57
  shadowed_special: 'local %{name} was overshadowed by a special form or macro',
58
+ target_requires_compile: '--target requires --compile',
54
59
  tset_no_value: 'tset: expected table, key, and value arguments',
55
60
  unclosed_delimiter: "unclosed opening delimiter '%{char}'",
56
61
  undefined_symbol: 'undefined symbol: %{name}',
@@ -58,6 +63,7 @@ module Kapusta
58
63
  unexpected_eof: 'unexpected eof',
59
64
  unexpected_vararg: 'unexpected vararg',
60
65
  unknown_special_form: 'unknown special form: %{name}',
66
+ unknown_target: 'unknown target %{target}; only mruby is supported',
61
67
  unquote_outside_quasiquote: 'unquote outside quasiquote',
62
68
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
63
69
  unterminated_string: 'unterminated string',
@@ -266,7 +266,7 @@ module Kapusta
266
266
  raw_args = list_raw_rest(list)
267
267
 
268
268
  case head_name
269
- when 'fn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
269
+ when 'fn', 'defn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
270
270
  when 'let' then render_let(list, indent)
271
271
  when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
272
272
  when 'try' then render_try(list, indent)
@@ -657,7 +657,6 @@ module Kapusta
657
657
 
658
658
  def render_let_bindings(bindings, indent)
659
659
  return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
660
- return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
661
660
 
662
661
  hanging = render_hanging_pairwise_vec(bindings)
663
662
  hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'rename'
4
+ require_relative '../reader'
5
+ require_relative 'scope_walker'
4
6
 
5
7
  module Kapusta
6
8
  class LSP
@@ -8,6 +10,9 @@ module Kapusta
8
10
  module_function
9
11
 
10
12
  def find(uri, text, line_zero, character, workspace_index:)
13
+ marker = end_marker_at(text, line_zero, character)
14
+ return location_for_binding(uri, marker.target) if marker&.target
15
+
11
16
  target = Rename.locate(text, line_zero, character)
12
17
  return unless target
13
18
 
@@ -23,6 +28,18 @@ module Kapusta
23
28
  end
24
29
  end
25
30
 
31
+ def end_marker_at(text, line_zero, character)
32
+ forms = Reader.read_all(text)
33
+ walker = ScopeWalker.analyze(forms)
34
+ line = line_zero + 1
35
+ col = character + 1
36
+ walker.end_markers.find do |m|
37
+ m.line == line && col >= m.column && col <= m.end_column
38
+ end
39
+ rescue Kapusta::Error
40
+ nil
41
+ end
42
+
26
43
  def locations_for_macro(uri, binding, workspace_index)
27
44
  return unless binding
28
45
 
@@ -344,7 +344,9 @@ module Kapusta
344
344
  end
345
345
 
346
346
  def rename_constant(target, new_name, workspace_index)
347
- return error("invalid constant segment: #{new_name}") unless Identifier.valid_constant_segment?(new_name)
347
+ unless Identifier.valid_constant_segment?(new_name)
348
+ return error("class and module names must start with an uppercase letter (got #{new_name.inspect})")
349
+ end
348
350
 
349
351
  prefix = target.segment_prefix
350
352
  seg_index = target.segment_index
@@ -15,6 +15,7 @@ module Kapusta
15
15
  bindings[name] || parent&.lookup(name)
16
16
  end
17
17
  end
18
+ EndMarker = Struct.new(:line, :column, :end_column, :target, keyword_init: true)
18
19
 
19
20
  SKIPPED_HEADS = %w[macros quasi-sym quasi-list
20
21
  quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
@@ -26,6 +27,7 @@ module Kapusta
26
27
  'global' => :walk_global,
27
28
  'set' => :walk_set,
28
29
  'fn' => :walk_fn,
30
+ 'defn' => :walk_fn,
29
31
  'lambda' => :walk_fn,
30
32
  'λ' => :walk_fn,
31
33
  'for' => :walk_for,
@@ -48,7 +50,7 @@ module Kapusta
48
50
  'gvar' => :walk_sigil_form
49
51
  }.freeze
50
52
 
51
- attr_reader :bindings, :references, :root_scope
53
+ attr_reader :bindings, :references, :root_scope, :end_markers
52
54
 
53
55
  def self.analyze(forms)
54
56
  walker = new
@@ -59,6 +61,7 @@ module Kapusta
59
61
  def initialize
60
62
  @bindings = []
61
63
  @references = []
64
+ @end_markers = []
62
65
  @scope_seq = 0
63
66
  @root_scope = make_scope(nil, :file)
64
67
  @gvar_scope = make_scope(nil, :gvars)
@@ -67,17 +70,43 @@ module Kapusta
67
70
  end
68
71
 
69
72
  def walk_top(forms)
70
- i = 0
73
+ walk_form_run(forms, 0, @root_scope)
74
+ end
75
+
76
+ def walk_form_run(forms, start, scope, header_target: nil)
77
+ i = start
71
78
  while i < forms.length
72
79
  form = forms[i]
80
+ if end_form?(form)
81
+ record_end_marker(form, header_target) if header_target
82
+ return i + 1
83
+ end
84
+
73
85
  if bodyless_header?(form)
74
- walk_bodyless_header(form, forms[(i + 1)..] || [], @root_scope)
75
- break
86
+ i = walk_bodyless_header(form, forms, i + 1, scope)
87
+ next
76
88
  end
77
89
 
78
- walk_form(form, @root_scope)
90
+ walk_form(form, scope)
79
91
  i += 1
80
92
  end
93
+ i
94
+ end
95
+
96
+ def record_end_marker(form, target)
97
+ head = form.head
98
+ return unless head.is_a?(Sym) && head.respond_to?(:line) && head.line
99
+
100
+ @end_markers << EndMarker.new(
101
+ line: head.line,
102
+ column: head.column,
103
+ end_column: head.column + head.name.length,
104
+ target:
105
+ )
106
+ end
107
+
108
+ def end_form?(form)
109
+ form.is_a?(List) && !form.empty? && form.head.is_a?(Sym) && form.head.name == 'end'
81
110
  end
82
111
 
83
112
  def binding_at(line, column)
@@ -120,25 +149,25 @@ module Kapusta
120
149
  end
121
150
  end
122
151
 
123
- def walk_bodyless_header(form, remaining_forms, scope)
152
+ def walk_bodyless_header(form, forms, body_start, scope)
124
153
  case form.head.name
125
154
  when 'module'
126
155
  name_sym = form.items[1]
127
- add_constant_binding(name_sym, scope, :module) if name_sym.is_a?(Sym)
156
+ binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :module) : nil
128
157
  body = form.items[2..] || []
129
158
  inside_module_or_class do
130
159
  if body.length == 1 && bodyless_header?(body[0])
131
- walk_bodyless_header(body[0], remaining_forms, scope)
160
+ walk_bodyless_header(body[0], forms, body_start, scope)
132
161
  else
133
- remaining_forms.each { |item| walk_form(item, scope) }
162
+ walk_form_run(forms, body_start, scope, header_target: binding)
134
163
  end
135
164
  end
136
165
  when 'class'
137
166
  name_sym, supers, = split_class_args(form.items[1..] || [])
138
167
  supers&.items&.each { |item| walk_form(item, scope) }
139
- add_constant_binding(name_sym, scope, :class) if name_sym.is_a?(Sym)
168
+ binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
140
169
  inside_class do
141
- remaining_forms.each { |item| walk_form(item, scope) }
170
+ walk_form_run(forms, body_start, scope, header_target: binding)
142
171
  end
143
172
  end
144
173
  end
data/lib/kapusta/lsp.rb CHANGED
@@ -257,7 +257,8 @@ module Kapusta
257
257
  new_name, workspace_index: @workspace_index)
258
258
  if result[:error]
259
259
  debug("rename error: #{result[:error].inspect}")
260
- reply_error(id, result[:error][:code], result[:error][:message])
260
+ notify('window/showMessage', { type: 1, message: "Rename: #{result[:error][:message]}" })
261
+ reply(id, { documentChanges: [] })
261
262
  else
262
263
  edit = build_workspace_edit(result[:changes])
263
264
  debug("rename ok: files=#{result[:changes].keys.length} edits=#{result[:changes].values.sum(&:length)}")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -23,8 +23,8 @@ module Kapusta
23
23
  self.eval(source, path:)
24
24
  end
25
25
 
26
- def self.compile(source, path: '(eval)', **_opts)
27
- Compiler.compile(source, path:)
26
+ def self.compile(source, path: '(eval)', target: nil, **_opts)
27
+ Compiler.compile(source, path:, target:)
28
28
  end
29
29
 
30
30
  def self.require(feature, relative_to: nil)
data/spec/cli_spec.rb CHANGED
@@ -66,6 +66,19 @@ RSpec.describe Kapusta::CLI do
66
66
  end
67
67
  end
68
68
 
69
+ it 'compiles case and match forms for mruby with --target=mruby' do
70
+ path = File.expand_path('../examples/match.kap', __dir__)
71
+
72
+ ruby = capture_stdout do
73
+ described_class.start(['--compile', '--target=mruby', path])
74
+ end
75
+
76
+ expect(ruby).not_to match(/^\s*in\b/)
77
+ expect(ruby).not_to include('^(')
78
+ expect(ruby).to include("case\n")
79
+ expect(ruby).to include('when ')
80
+ end
81
+
69
82
  it 'rejects extra positional arguments in compile mode' do
70
83
  path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
71
84
 
@@ -77,6 +90,28 @@ RSpec.describe Kapusta::CLI do
77
90
  expect(error_output).to include('usage: kapusta')
78
91
  end
79
92
 
93
+ it 'rejects unsupported targets' do
94
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
95
+
96
+ error_output = capture_stderr do
97
+ expect { described_class.start(['--compile', '--target=mri', path]) }
98
+ .to raise_error(SystemExit)
99
+ end
100
+
101
+ expect(error_output).to include('unknown target "mri"; only mruby is supported')
102
+ end
103
+
104
+ it 'rejects target without compile mode' do
105
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
106
+
107
+ error_output = capture_stderr do
108
+ expect { described_class.start(['--target=mruby', path]) }
109
+ .to raise_error(SystemExit)
110
+ end
111
+
112
+ expect(error_output).to include('--target requires --compile')
113
+ end
114
+
80
115
  it 'passes remaining arguments through to the Kapusta program' do
81
116
  path = File.expand_path('../examples/greet.kap', __dir__)
82
117
 
@@ -92,6 +92,26 @@ RSpec.describe 'examples-errors' do
92
92
  .to eq("destructure-literal-table.kap:4:1: could not destructure literal\n")
93
93
  end
94
94
 
95
+ it 'end-outside-header.kap' do
96
+ expect(run_error_example('end-outside-header.kap'))
97
+ .to eq("end-outside-header.kap:1:1: end outside class or module\n")
98
+ end
99
+
100
+ it 'end-with-args.kap' do
101
+ expect(run_error_example('end-with-args.kap'))
102
+ .to eq("end-with-args.kap:5:1: end takes no arguments\n")
103
+ end
104
+
105
+ it 'extra-end.kap' do
106
+ expect(run_error_example('extra-end.kap'))
107
+ .to eq("extra-end.kap:7:1: end outside class or module\n")
108
+ end
109
+
110
+ it 'unclosed-header.kap' do
111
+ expect(run_error_example('unclosed-header.kap'))
112
+ .to eq("unclosed-header.kap:1:1: class or module not closed with (end)\n")
113
+ end
114
+
95
115
  it 'destructure-rest-as-table.kap' do
96
116
  expect(run_error_example('destructure-rest-as-table.kap'))
97
117
  .to eq("destructure-rest-as-table.kap:6:3: unable to bind table ...\n")
@@ -2,10 +2,21 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'fileutils'
5
+ require 'open3'
5
6
  require 'stringio'
7
+ require 'tempfile'
6
8
 
7
9
  EXAMPLES_DIR = File.expand_path('../examples', __dir__)
8
10
 
11
+ def example_list(name)
12
+ File.readlines(File.join(EXAMPLES_DIR, name), chomp: true)
13
+ .reject(&:empty?)
14
+ .map { |example| "#{example}.kap" }
15
+ .freeze
16
+ end
17
+
18
+ MRUBY_RUNTIME_EXAMPLES = example_list('mruby-runtime-examples.txt')
19
+
9
20
  def run_example(name, argv: [])
10
21
  previous_argv = ARGV.dup
11
22
  previous_stdout = $stdout
@@ -27,6 +38,39 @@ ensure
27
38
  $stdout = previous_stdout
28
39
  end
29
40
 
41
+ def compile_example(name, target: nil)
42
+ path = File.join(EXAMPLES_DIR, name)
43
+ Kapusta.compile(File.read(path), path:, target:)
44
+ end
45
+
46
+ def run_compiled_source(source, path:)
47
+ previous_argv = ARGV.dup
48
+ previous_stdout = $stdout
49
+ ARGV.replace([])
50
+ $stdout = StringIO.new
51
+ TOPLEVEL_BINDING.eval(source, path, 1)
52
+ $stdout.string
53
+ ensure
54
+ $stdout = previous_stdout
55
+ ARGV.replace(previous_argv)
56
+ end
57
+
58
+ def run_mruby_source(source, path:)
59
+ stdout, stderr, status = capture_mruby_source(source, path:)
60
+
61
+ raise "mruby failed for #{path}:\n#{stderr}" unless status.success?
62
+
63
+ stdout
64
+ end
65
+
66
+ def capture_mruby_source(source, path:)
67
+ Tempfile.create([File.basename(path, '.kap'), '.rb']) do |file|
68
+ file.write(source)
69
+ file.close
70
+ Open3.capture3('mruby', file.path)
71
+ end
72
+ end
73
+
30
74
  RSpec.describe 'examples' do
31
75
  it 'ackermann.kap' do
32
76
  expect(run_example('ackermann.kap')).to eq("9\n61\n")
@@ -36,6 +80,18 @@ RSpec.describe 'examples' do
36
80
  expect(run_example('accumulator.kap')).to eq("22\n")
37
81
  end
38
82
 
83
+ it 'account-lockout.kap' do
84
+ expect(run_example('account-lockout.kap')).to eq(<<~OUT)
85
+ :ok
86
+ :locked
87
+ :locked
88
+ OUT
89
+ end
90
+
91
+ it 'circle.kap' do
92
+ expect(run_example('circle.kap')).to eq("78.53975\n31.4159\n")
93
+ end
94
+
39
95
  it 'anagram.kap' do
40
96
  expect(run_example('anagram.kap')).to eq("true\ntrue\nfalse\n")
41
97
  end
@@ -107,6 +163,26 @@ RSpec.describe 'examples' do
107
163
  expect(run_example('happy-number.kap')).to eq("true\nfalse\ntrue\n")
108
164
  end
109
165
 
166
+ it 'number-of-1-bits.kap' do
167
+ expect(run_example('number-of-1-bits.kap')).to eq("3\n1\n31\n")
168
+ end
169
+
170
+ it 'number-of-steps.kap' do
171
+ expect(run_example('number-of-steps.kap')).to eq("6\n4\n12\n")
172
+ end
173
+
174
+ it 'convert-temperature.kap' do
175
+ expect(run_example('convert-temperature.kap')).to eq("309.65\n97.7\n395.26\n251.798\n")
176
+ end
177
+
178
+ it 'count-effects.kap' do
179
+ expect(run_example('count-effects.kap')).to eq("1\n2\n")
180
+ end
181
+
182
+ it 'max-achievable.kap' do
183
+ expect(run_example('max-achievable.kap')).to eq("6\n7\n10\n")
184
+ end
185
+
110
186
  it 'move-zeroes.kap' do
111
187
  expect(run_example('move-zeroes.kap')).to eq(<<~OUT)
112
188
  [1, 3, 12, 0, 0]
@@ -368,6 +444,18 @@ RSpec.describe 'examples' do
368
444
  OUT
369
445
  end
370
446
 
447
+ it 'underscore-patterns.kap on mruby keeps loose nil and strict fallback separate' do
448
+ path = File.join(EXAMPLES_DIR, 'underscore-patterns.kap')
449
+ ruby = compile_example('underscore-patterns.kap', target: :mruby)
450
+
451
+ expect(run_mruby_source(ruby, path:)).to eq(<<~OUT)
452
+ 5
453
+ nil
454
+ 5
455
+ "fallback"
456
+ OUT
457
+ end
458
+
371
459
  it 'scopes.kap' do
372
460
  expect(run_example('scopes.kap')).to eq("5\n9\n9\n9\n")
373
461
  end
@@ -399,6 +487,10 @@ RSpec.describe 'examples' do
399
487
  OUT
400
488
  end
401
489
 
490
+ it 'signal-harvest.kap' do
491
+ expect(run_example('signal-harvest.kap')).to eq("40\ntrue\nfalse\n")
492
+ end
493
+
402
494
  it 'shapes.kap' do
403
495
  expect(run_example('shapes.kap')).to eq("78.5\n9\n8\n0\n")
404
496
  end
@@ -437,6 +529,19 @@ RSpec.describe 'examples' do
437
529
  expect(run_example('two-sum-hash.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
438
530
  end
439
531
 
532
+ it 'bst-iterator.kap' do
533
+ expect(run_example('bst-iterator.kap')).to eq(<<~OUT)
534
+ 3
535
+ 7
536
+ true
537
+ 9
538
+ true
539
+ 15
540
+ 20
541
+ false
542
+ OUT
543
+ end
544
+
440
545
  it 'baseball-game.kap' do
441
546
  expect(run_example('baseball-game.kap')).to eq("30\n27\n")
442
547
  end
@@ -468,6 +573,10 @@ RSpec.describe 'examples' do
468
573
  OUT
469
574
  end
470
575
 
576
+ it 'recent-counter.kap' do
577
+ expect(run_example('recent-counter.kap')).to eq("4\n5\n")
578
+ end
579
+
471
580
  it 'reverse-integer.kap' do
472
581
  expect(run_example('reverse-integer.kap')).to eq("321\n-321\n21\n0\n")
473
582
  end
@@ -577,3 +686,30 @@ RSpec.describe 'examples' do
577
686
  OUT
578
687
  end
579
688
  end
689
+
690
+ RSpec.describe 'mruby runtime examples' do
691
+ MRUBY_RUNTIME_EXAMPLES.each do |name|
692
+ it name do
693
+ path = File.join(EXAMPLES_DIR, name)
694
+ ruby = compile_example(name)
695
+ expected = run_example(name)
696
+ expect(run_compiled_source(ruby, path:)).to eq(expected)
697
+ mruby_stdout, _mruby_stderr, mruby_status = capture_mruby_source(ruby, path:)
698
+
699
+ if mruby_status.success? && mruby_stdout == expected
700
+ expect(run_mruby_source(ruby, path:)).to eq(expected)
701
+ else
702
+ mruby_ruby = compile_example(name, target: :mruby)
703
+
704
+ if mruby_ruby == ruby
705
+ expect(mruby_status).to be_success
706
+ else
707
+ expect(mruby_ruby).not_to match(/^\s*in\b/)
708
+ expect(mruby_ruby).not_to include('^(')
709
+ expect(run_compiled_source(mruby_ruby, path:)).to eq(expected)
710
+ run_mruby_source(mruby_ruby, path:)
711
+ end
712
+ end
713
+ end
714
+ end
715
+ end
data/spec/lsp_spec.rb CHANGED
@@ -268,7 +268,8 @@ RSpec.describe Kapusta::LSP do
268
268
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'foo'), new_name: 'bar')
269
269
  )
270
270
 
271
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
271
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
272
+ expect(message['params']['message']).to include('already defined')
272
273
  end
273
274
  end
274
275
 
@@ -282,7 +283,8 @@ RSpec.describe Kapusta::LSP do
282
283
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
283
284
  )
284
285
 
285
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
286
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
287
+ expect(message['params']['message']).to include('already defined')
286
288
  end
287
289
  end
288
290
 
@@ -304,6 +306,71 @@ RSpec.describe Kapusta::LSP do
304
306
  )
305
307
  end
306
308
 
309
+ it 'rejects renaming a class to a lowercase name with a clear message' do
310
+ text = "(class Accumulator)\n\n(end)\n"
311
+ responses = run(
312
+ frame_initialize,
313
+ frame_did_open('file:///x.kap', text),
314
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'Accumulator'), new_name: 'fff')
315
+ )
316
+
317
+ response = result_for(responses)
318
+ expect(response['error']).to be_nil
319
+ expect(response['result']).to eq('documentChanges' => [])
320
+
321
+ show_message = responses.find { |m| m['method'] == 'window/showMessage' }
322
+ expect(show_message).not_to be_nil
323
+ expect(show_message['params']['type']).to eq(1)
324
+ expect(show_message['params']['message']).to include('uppercase letter')
325
+ end
326
+
327
+ it 'renames a class declared with a bodyless header closed by (end) and its usages after (end)' do
328
+ text = "(class Accumulator)\n\n(fn add! [n] n)\n\n(end)\n\n(let [acc (Accumulator.new 10)]\n (acc.add! 5))\n"
329
+ with_workspace('a.kap' => text) do |root_uri, uri|
330
+ responses = run(
331
+ frame_initialize([root_uri]),
332
+ frame_did_open(uri['a.kap'], text),
333
+ frame_rename(uri: uri['a.kap'], **cursor_at(text, 'Accumulator'), new_name: 'Foo')
334
+ )
335
+ result = result_for(responses)['result']
336
+
337
+ expect(result).not_to be_nil
338
+ edits = result['documentChanges'].first['edits']
339
+ expect(edits.map { |e| e['range']['start']['line'] }).to contain_exactly(0, 6)
340
+ expect(edits.map { |e| e['newText'] }).to all(eq('Foo'))
341
+ end
342
+ end
343
+
344
+ it 'jumps from (end) to the class header that opened the file scope' do
345
+ text = "(class Foo)\n\n(fn hi [] 1)\n\n(end)\n"
346
+ responses = run(
347
+ frame_initialize,
348
+ frame_did_open('file:///x.kap', text),
349
+ frame_definition(uri: 'file:///x.kap', **cursor_at(text, 'end'))
350
+ )
351
+ result = result_for(responses)['result']
352
+
353
+ expect(result).to eq(
354
+ 'uri' => 'file:///x.kap',
355
+ 'range' => {
356
+ 'start' => { 'line' => 0, 'character' => 7 },
357
+ 'end' => { 'line' => 0, 'character' => 10 }
358
+ }
359
+ )
360
+ end
361
+
362
+ it 'jumps from (end) to the matching module header for nested headers' do
363
+ text = "(module Outer)\n\n(module Inner)\n(fn self.go [] 1)\n(end)\n\n(end)\n"
364
+ responses = run(
365
+ frame_initialize,
366
+ frame_did_open('file:///x.kap', text),
367
+ frame_definition(uri: 'file:///x.kap', line: 4, character: 1)
368
+ )
369
+ result = result_for(responses)['result']
370
+
371
+ expect(result['range']['start']).to eq('line' => 2, 'character' => 8)
372
+ end
373
+
307
374
  it 'jumps to a top-level fn definition across files' do
308
375
  text_a = "(fn greet [n] (print n))\n"
309
376
  text_b = "(greet 42)\n"
@@ -583,7 +650,8 @@ RSpec.describe Kapusta::LSP do
583
650
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'swap!'), new_name: 'flip!')
584
651
  )
585
652
 
586
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
653
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
654
+ expect(message['params']['message']).to include('already defined')
587
655
  end
588
656
  end
589
657
 
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,15 @@
3
3
  require 'bundler/setup'
4
4
  require 'kapusta'
5
5
 
6
+ module SilenceConstantRedefinitionWarnings
7
+ def warn(message, category: nil)
8
+ return if /already initialized constant|previous definition of/.match?(message)
9
+
10
+ super
11
+ end
12
+ end
13
+ Warning.singleton_class.prepend(SilenceConstantRedefinitionWarnings)
14
+
6
15
  RSpec.configure do |config|
7
16
  config.disable_monkey_patching!
8
17
  config.example_status_persistence_file_path = '.rspec_status'