jade-lang 0.9.0 → 0.10.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +400 -12
  3. data/docs/interop.md +18 -1
  4. data/docs/stdlib.md +3 -1
  5. data/docs/syntax.md +96 -0
  6. data/lib/jade/api.rb +9 -0
  7. data/lib/jade/ast.rb +37 -1
  8. data/lib/jade/cli/eject.rb +132 -0
  9. data/lib/jade/cli.rb +2 -0
  10. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  11. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  12. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  13. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  14. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  15. data/lib/jade/codegen/boundary.rb +12 -2
  16. data/lib/jade/codegen/context.rb +17 -0
  17. data/lib/jade/codegen/error.rb +1 -0
  18. data/lib/jade/codegen/function_call.rb +23 -7
  19. data/lib/jade/codegen/function_declaration.rb +25 -13
  20. data/lib/jade/codegen/helpers.rb +27 -4
  21. data/lib/jade/codegen/implementation.rb +2 -2
  22. data/lib/jade/codegen/inline.rb +1 -1
  23. data/lib/jade/codegen/inlines.rb +6 -1
  24. data/lib/jade/codegen/names.rb +31 -0
  25. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  26. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  27. data/lib/jade/codegen.rb +76 -15
  28. data/lib/jade/compiler.rb +2 -0
  29. data/lib/jade/entry.rb +1 -1
  30. data/lib/jade/formatter/calls.rb +68 -11
  31. data/lib/jade/formatter/collections.rb +4 -4
  32. data/lib/jade/formatter/declarations.rb +13 -15
  33. data/lib/jade/formatter/helper.rb +20 -0
  34. data/lib/jade/formatter/infix_application.rb +19 -4
  35. data/lib/jade/formatter/lambda.rb +24 -12
  36. data/lib/jade/formatter/pattern.rb +3 -0
  37. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  38. data/lib/jade/frontend/desugaring.rb +29 -9
  39. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  40. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  41. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  42. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  43. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  44. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  45. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  46. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  47. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  48. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  49. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  50. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  51. data/lib/jade/frontend/forward_declaration.rb +2 -0
  52. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  53. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  54. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  55. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  56. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  57. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  58. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  59. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  60. data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
  61. data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
  62. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
  63. data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
  64. data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
  65. data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
  66. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
  67. data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
  68. data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
  69. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
  70. data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
  71. data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
  72. data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
  73. data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
  74. data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
  75. data/lib/jade/frontend/semantic_analysis.rb +5 -0
  76. data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
  77. data/lib/jade/frontend/type_checking/cascade.rb +90 -0
  78. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
  79. data/lib/jade/frontend/type_checking/constraints.rb +18 -3
  80. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  81. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  82. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  83. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  84. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  85. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  86. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  87. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  88. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  89. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  90. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  91. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  92. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  93. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  94. data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
  95. data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
  96. data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
  97. data/lib/jade/frontend/type_checking/error.rb +6 -0
  98. data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
  99. data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
  100. data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
  101. data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
  102. data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
  103. data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
  104. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  105. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  106. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  107. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  108. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  109. data/lib/jade/frontend/type_checking.rb +3 -1
  110. data/lib/jade/frontend/usage_analysis.rb +5 -1
  111. data/lib/jade/interop/boundary.rb +64 -24
  112. data/lib/jade/interop/error.rb +35 -7
  113. data/lib/jade/interop/lowering.rb +8 -2
  114. data/lib/jade/lsp/converters.rb +83 -2
  115. data/lib/jade/lsp/handlers.rb +122 -25
  116. data/lib/jade/lsp/snippets.rb +12 -0
  117. data/lib/jade/lsp/state.rb +11 -4
  118. data/lib/jade/module_loader/build.rb +36 -0
  119. data/lib/jade/module_loader.rb +1 -0
  120. data/lib/jade/parsing/combinators.rb +78 -2
  121. data/lib/jade/parsing/error.rb +43 -6
  122. data/lib/jade/parsing/type.rb +5 -3
  123. data/lib/jade/parsing.rb +140 -26
  124. data/lib/jade/runtime.rb +32 -13
  125. data/lib/jade/source.rb +6 -2
  126. data/lib/jade/stdlib/basics.rb +22 -3
  127. data/lib/jade/stdlib/char.rb +2 -0
  128. data/lib/jade/stdlib/clock.rb +6 -2
  129. data/lib/jade/stdlib/decimal.rb +51 -14
  130. data/lib/jade/stdlib/intrinsics.rb +59 -6
  131. data/lib/jade/stdlib/list.rb +37 -1
  132. data/lib/jade/stdlib/number.rb +47 -0
  133. data/lib/jade/stdlib/range.rb +211 -0
  134. data/lib/jade/stdlib.rb +11 -4
  135. data/lib/jade/symbol/alias.rb +11 -0
  136. data/lib/jade/symbol/parser.rb +1 -1
  137. data/lib/jade/symbol.rb +13 -1
  138. data/lib/jade/task.rb +0 -2
  139. data/lib/jade/type/anonymous_record.rb +11 -2
  140. data/lib/jade/type/application.rb +11 -2
  141. data/lib/jade/type/base.rb +26 -0
  142. data/lib/jade/type/function.rb +11 -2
  143. data/lib/jade/type.rb +36 -10
  144. data/lib/jade/version.rb +1 -1
  145. metadata +33 -2
@@ -11,6 +11,50 @@ module Jade
11
11
  # Result wrap/unwrap that user-level Decode.from_value uses is dead
12
12
  # weight at the boundary because failure always raises anyway. Skipping
13
13
  # it removes one allocation per arg per Ruby→Jade call.
14
+ # Names the argument a failure came from. Costs nothing until one
15
+ # does: Ruby only pays for a rescue that fires.
16
+ def arg(where)
17
+ yield
18
+ rescue DecodeError => e
19
+ raise e.under(where)
20
+ end
21
+ alias at arg
22
+
23
+ # Finding the element costs a second pass, which only a failure pays
24
+ # for: the map stays a map.
25
+ def elements(array, where = nil)
26
+ array.map { yield(it) }
27
+ rescue DecodeError => e
28
+ raise e.under("#{where}[#{failing_index(array) { yield(it) }}]")
29
+ end
30
+
31
+ def failing_index(array)
32
+ array.each_with_index do |element, index|
33
+ yield(element)
34
+ rescue DecodeError
35
+ return index
36
+ end
37
+ end
38
+
39
+ # An absent key reads as nil to the field decoder, which then blames
40
+ # the type it wanted. The path says which key that was, so a struct
41
+ # can check whether it was ever there.
42
+ def blame(error, hash, label)
43
+ key = error.where.to_s[/\A\.([^.\[]+)/, 1]
44
+ return error if key.nil? || hash.key?(key)
45
+
46
+ symbol_keys!(label, hash)
47
+ error.as(Jade::Decode::MissingField[key])
48
+ end
49
+
50
+ # Mixed keys are a real shape mismatch; the per-field errors say more
51
+ # than a guess about intent would.
52
+ def symbol_keys!(label, hash)
53
+ return if hash.empty? || hash.keys.any?(::String)
54
+
55
+ hash.keys.grep(::Symbol).then { raise SymbolKeys.new(label, it) unless it.empty? }
56
+ end
57
+
14
58
  def decode_or_raise(decoder, value)
15
59
  Jade::Decode::Runner.run!(decoder, value) do |error|
16
60
  raise Jade::Interop::DecodeError.new(error, value)
@@ -21,54 +65,50 @@ module Jade
21
65
  # argument types in place of the generic `decode_or_raise` path —
22
66
  # avoids constructing a Decoder descriptor and walking the
23
67
  # interpreter for primitives.
24
- def integer(label, v)
25
- ::Integer === v ? v : type_error!(label, v)
68
+ def integer(label, v, where = nil)
69
+ ::Integer === v ? v : type_error!(label, v, where)
26
70
  end
27
71
 
28
- def string(label, v)
29
- ::String === v ? v.dup : type_error!(label, v)
72
+ def string(label, v, where = nil)
73
+ ::String === v ? v.dup : type_error!(label, v, where)
30
74
  end
31
75
 
32
- def bool(label, v)
33
- v == true || v == false ? v : type_error!(label, v)
76
+ def bool(label, v, where = nil)
77
+ v == true || v == false ? v : type_error!(label, v, where)
34
78
  end
35
79
 
36
- def float(label, v)
37
- ::Numeric === v ? v.to_f : type_error!(label, v)
80
+ def float(label, v, where = nil)
81
+ ::Numeric === v ? v.to_f : type_error!(label, v, where)
38
82
  end
39
83
 
40
- def list_of(klass, label, v)
41
- v.is_a?(::Array) && v.all? { klass === _1 } ? v : type_error!(label, v)
84
+ # Copied, because the caller keeps its array and Jade values do not
85
+ # change. The copy is under 1% of the check that precedes it.
86
+ def list_of(klass, label, v, where = nil)
87
+ v.is_a?(::Array) && v.all? { klass === _1 } ? v.dup : type_error!(label, v, where)
42
88
  end
43
89
 
44
90
  # Validates that v is an Array but doesn't check element types — used
45
91
  # when the per-element decoder isn't a simple `is_a?` (e.g. nested
46
92
  # structs). The caller maps a decoder over the result.
47
- def array(label, v)
48
- v.is_a?(::Array) ? v : type_error!(label, v)
93
+ def array(label, v, where = nil)
94
+ v.is_a?(::Array) ? v : type_error!(label, v, where)
49
95
  end
50
96
 
97
+ # Symbol keys are not checked here: every field would then be
98
+ # missing, and the rescue around the fields catches that for free.
51
99
  def hash(label, v)
52
100
  case v
53
- when ::Hash then string_keyed!(label, v)
101
+ when ::Hash then v
54
102
  when ::Data then v.to_h.transform_keys(&:to_s)
55
- else type_error!(label, v)
103
+ else type_error!(label, v, nil)
56
104
  end
57
105
  end
58
106
 
59
- # Mixed keys are a real shape mismatch; the per-field errors say more
60
- # than a guess about intent would.
61
- def string_keyed!(label, v)
62
- return v if v.empty? || v.keys.any?(::String)
63
-
64
- v.keys.grep(::Symbol)
65
- .then { it.empty? ? v : raise(Jade::Interop::SymbolKeys.new(label, it)) }
66
- end
67
-
68
- def type_error!(label, v)
107
+ def type_error!(label, v, where)
69
108
  raise Jade::Interop::DecodeError.new(
70
109
  Jade::Decode::WrongType[label, Jade::Decode.type_name(v)],
71
110
  v,
111
+ where:,
72
112
  )
73
113
  end
74
114
  end
@@ -54,36 +54,48 @@ module Jade
54
54
  end
55
55
 
56
56
  class DecodeError < Error
57
- attr_reader :decode_error, :value, :source
57
+ attr_reader :decode_error, :value, :source, :where
58
58
 
59
59
  SUBJECTS = {
60
60
  argument: 'Ruby passed a value that failed to decode',
61
61
  port_return: 'Port returned a value that failed to decode',
62
62
  }.freeze
63
63
 
64
- def initialize(decode_error, value, source: :argument)
64
+ # Ruby has no null.
65
+ TYPE_NAMES = { 'null' => 'nil' }.freeze
66
+
67
+ def initialize(decode_error, value, source: :argument, where: nil)
65
68
  @decode_error = decode_error
66
69
  @value = value
67
70
  @source = source
71
+ @where = where
68
72
  super(format(decode_error, value))
69
73
  end
70
74
 
75
+ # Frames unwind innermost first, so each one puts itself in front of
76
+ # the path the frames below it built.
77
+ def under(segment)
78
+ self.class.new(decode_error, value, source:, where: "#{segment}#{where}")
79
+ end
80
+
81
+ def as(error)
82
+ self.class.new(error, value, source:, where:)
83
+ end
84
+
71
85
  private
72
86
 
73
87
  def format(error, value)
74
88
  path, leaf = unwind(error)
75
- location = path.empty? ? "value" : path.join
76
- subject = SUBJECTS.fetch(@source)
77
89
 
78
90
  case leaf
79
91
  in Jade::Decode::WrongType[expected, got]
80
- "#{subject} at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
92
+ "#{subject(path)}: expected #{expected}, got #{type_name(got)}#{seen(value)}"
81
93
 
82
94
  in Jade::Decode::MissingField[key]
83
- "#{subject} at #{location}: missing field `#{key}` (#{value.inspect})"
95
+ "#{subject(path)}: missing field `#{key}`#{seen(value)}"
84
96
 
85
97
  in Jade::Decode::Custom[msg]
86
- "#{subject} at #{location}: #{msg} (#{value.inspect})"
98
+ "#{subject(path)}: #{msg}#{seen(value)}"
87
99
 
88
100
  in Jade::Decode::Multiple[errors]
89
101
  errors
@@ -92,6 +104,22 @@ module Jade
92
104
  end
93
105
  end
94
106
 
107
+ # `Shop.price(item).cents` when the caller said where it was, and the
108
+ # old sentence when it did not.
109
+ def subject(path)
110
+ return "#{SUBJECTS.fetch(@source)} at #{path.empty? ? 'value' : path.join}" unless @where
111
+
112
+ "#{@where}#{path.join}"
113
+ end
114
+
115
+ def type_name(got)
116
+ TYPE_NAMES.fetch(got.to_s, got)
117
+ end
118
+
119
+ def seen(value)
120
+ value.nil? ? '' : " (#{value.inspect})"
121
+ end
122
+
95
123
  def unwind(error, path = [])
96
124
  case error
97
125
  in Jade::Decode::AtField[key, inner] then unwind(inner, path + [".#{key}"])
@@ -21,8 +21,9 @@ module Jade
21
21
  in Symbol::FunctionType
22
22
  [FunctionError.new('inline function type')]
23
23
 
24
- in Symbol::TypeApplication(args:)
25
- args.flat_map { validate(it, registry, entry) }
24
+ in Symbol::TypeApplication(constructor:, args:)
25
+ validate(constructor, registry, entry) +
26
+ args.flat_map { validate(it, registry, entry) }
26
27
 
27
28
  in Symbol::RecordType(fields:)
28
29
  fields.values.flat_map { validate(it, registry, entry) }
@@ -34,6 +35,11 @@ module Jade
34
35
  lookup_type(symbol, registry, entry)
35
36
  &.then { validate(it, registry, entry) } || []
36
37
 
38
+ # An alias is its body here as everywhere else, so a function type
39
+ # behind one is still a function type that cannot be lowered.
40
+ in Symbol::Alias(body:)
41
+ body ? validate(body, registry, entry) : []
42
+
37
43
  else
38
44
  []
39
45
  end
@@ -33,6 +33,7 @@ module Jade
33
33
  enum_member: 22,
34
34
  struct: 23,
35
35
  interface: 11,
36
+ type_parameter: 26,
36
37
  }.freeze
37
38
 
38
39
  HOVERABLE_SYMBOLS = [
@@ -44,6 +45,7 @@ module Jade
44
45
  Jade::Symbol::Variant,
45
46
  Jade::Symbol::Union,
46
47
  Jade::Symbol::Struct,
48
+ Jade::Symbol::Alias,
47
49
  ].freeze
48
50
 
49
51
  # LSP CompletionItemKind values we use.
@@ -66,8 +68,8 @@ module Jade
66
68
  nil
67
69
  end
68
70
 
69
- def completion_items
70
- Snippets::ALL.map do |snippet|
71
+ def completion_items(module_name = nil)
72
+ Snippets.for_module(module_name).map do |snippet|
71
73
  {
72
74
  label: snippet.label,
73
75
  kind: COMPLETION_KIND_SNIPPET,
@@ -104,6 +106,21 @@ module Jade
104
106
  end
105
107
  end
106
108
 
109
+ # Enter and `end` are the two moments an editor can indent from what
110
+ # is already written, without the parse it will not get mid-typing.
111
+ def on_type_edits(text, line, character, typed)
112
+ text
113
+ .lines
114
+ .map(&:chomp)
115
+ .then do |lines|
116
+ case typed
117
+ in "\n" then indent_edit(lines, line, indent_after(lines, line))
118
+ in "d" then closing_edit(lines, line, character)
119
+ else []
120
+ end
121
+ end
122
+ end
123
+
107
124
  def whole_document_edit(source, new_text)
108
125
  last_line = source.line_starts.size - 1
109
126
  {
@@ -162,6 +179,9 @@ module Jade
162
179
  in AST::StructDeclaration(name:, range:)
163
180
  document_symbol(name, :struct, source, range)
164
181
 
182
+ in AST::TypeAliasDeclaration(name:, range:)
183
+ document_symbol(name, :type_parameter, source, range)
184
+
165
185
  in AST::InterfaceDeclaration(name:, range:, functions:)
166
186
  document_symbol(
167
187
  name, :interface, source, range,
@@ -374,8 +394,12 @@ module Jade
374
394
 
375
395
  def hover_body(symbol, registry)
376
396
  case symbol
397
+ in Jade::Symbol::Alias
398
+ render_alias(symbol, registry)
399
+
377
400
  in Jade::Symbol::Union | Jade::Symbol::Struct
378
401
  render_type(symbol, registry)
402
+
379
403
  else
380
404
  render_value(symbol, registry)
381
405
  end
@@ -404,6 +428,21 @@ module Jade
404
428
  )
405
429
  end
406
430
 
431
+ # Expansion erases the name everywhere else, so this is the only place
432
+ # left that says what an alias stands for.
433
+ def render_alias(symbol, registry)
434
+ Jade::Type
435
+ .from_symbol(symbol.body, registry, Frontend::TypeChecking::VarGen.new)
436
+ .first
437
+ .then { "type alias #{symbol.name}#{alias_params(symbol)} = #{it}" }
438
+ end
439
+
440
+ def alias_params(symbol)
441
+ symbol.type_params.empty? ?
442
+ '' :
443
+ "(#{symbol.type_params.map(&:name).join(', ')})"
444
+ end
445
+
407
446
  def render_type(symbol, registry)
408
447
  impls_for(symbol, registry)
409
448
  .then { it.empty? ? '' : "\n\nimplements #{it.join(', ')}" }
@@ -471,6 +510,7 @@ module Jade
471
510
  in AST::VariableReference | AST::ConstructorReference |
472
511
  AST::QualifiedAccess | AST::FunctionDeclaration |
473
512
  AST::TypeDeclaration | AST::StructDeclaration |
513
+ AST::TypeAliasDeclaration |
474
514
  AST::InterfaceDeclaration | AST::VariantDeclaration
475
515
  node.symbol.then { resolve_ref(it, registry) }
476
516
 
@@ -518,6 +558,47 @@ module Jade
518
558
  document_symbol(fn.name, :function, source, fn.range)
519
559
  end
520
560
 
561
+ INDENT = 2
562
+
563
+ # A block opens on a header, on a keyword that ends one, or on an
564
+ # unclosed bracket. `in` is a branch head: it sits with its `case`.
565
+ OPENS = /(\A(def|case|implements|interface|uses|if)\b)|((\bthen|\bwith|\belse|[({\[])\z)/
566
+
567
+ def indent_after(lines, line)
568
+ previous(lines, line)
569
+ .then { it ? indent_of(it) + (OPENS.match?(it.strip) ? INDENT : 0) : 0 }
570
+ end
571
+
572
+ # `end` closes the block its opener began, so it lands where the
573
+ # opener sits: one level out from the body above it.
574
+ def closing_edit(lines, line, character)
575
+ return [] unless lines[line].to_s[0...character].strip == 'end'
576
+
577
+ previous(lines, line)
578
+ .then { it ? [indent_of(it) - INDENT, 0].max : 0 }
579
+ .then { indent_edit(lines, line, it) }
580
+ end
581
+
582
+ def previous(lines, line)
583
+ lines[0...line].reverse.find { !it.strip.empty? }
584
+ end
585
+
586
+ def indent_edit(lines, line, width)
587
+ return [] if indent_of(lines[line].to_s) == width
588
+
589
+ [{
590
+ range: {
591
+ start: { line:, character: 0 },
592
+ end: { line:, character: indent_of(lines[line].to_s) },
593
+ },
594
+ newText: ' ' * width,
595
+ }]
596
+ end
597
+
598
+ def indent_of(line)
599
+ line[/\A */].length
600
+ end
601
+
521
602
  def diagnostic_message(diagnostic)
522
603
  [
523
604
  diagnostic.message,
@@ -24,6 +24,7 @@ module Jade
24
24
  when 'textDocument/rename' then on_rename(state, message)
25
25
  when 'textDocument/inlayHint' then on_inlay_hint(state, message)
26
26
  when 'textDocument/formatting' then on_formatting(state, message)
27
+ when 'textDocument/onTypeFormatting' then on_type_formatting(state, message)
27
28
  else on_unknown(state, message)
28
29
  end
29
30
  end
@@ -39,9 +40,13 @@ module Jade
39
40
 
40
41
  def on_initialize(state, message)
41
42
  params = (message['params'] || {})
42
- root = params
43
+ # The editor's root is the project root, not the source root the
44
+ # compiler names modules from. Reading the manifest also loads the
45
+ # extension gems a project imports.
46
+ project = params
43
47
  .then { it['rootUri'] || it.dig('workspaceFolders', 0, 'uri') }
44
48
  .then { it ? it.sub(%r{\Afile://}, '') : Dir.pwd }
49
+ .then { [Project.find(it), it] }
45
50
 
46
51
  {
47
52
  capabilities: {
@@ -55,10 +60,14 @@ module Jade
55
60
  renameProvider: { prepareProvider: true },
56
61
  inlayHintProvider: true,
57
62
  documentFormattingProvider: true,
63
+ documentOnTypeFormattingProvider: {
64
+ firstTriggerCharacter: "\n",
65
+ moreTriggerCharacter: ['d'],
66
+ },
58
67
  },
59
68
  serverInfo: { name: 'jade-lsp', version: '0.1.0' },
60
69
  }
61
- .then { [state.with_root(root), [respond(message['id'], it)]] }
70
+ .then { [state.with_project(*project), [respond(message['id'], it)]] }
62
71
  end
63
72
 
64
73
  def negotiate_encoding(params)
@@ -70,7 +79,7 @@ module Jade
70
79
  def on_did_open(state, params)
71
80
  params['textDocument']
72
81
  .then { state.put_buffer(it['uri'], it['text']) }
73
- .then { recompile_and_publish(it) }
82
+ .then { recompile_and_publish(it, scope: :project) }
74
83
  end
75
84
 
76
85
  def on_did_change(state, params)
@@ -83,7 +92,7 @@ module Jade
83
92
 
84
93
  def on_did_close(state, params)
85
94
  params['textDocument']['uri']
86
- .then { recompile_and_publish(state.close(it), extra_uris: [it]) }
95
+ .then { recompile_and_publish(state.close(it), extra_uris: [it], scope: :project) }
87
96
  end
88
97
 
89
98
  def on_document_symbol(state, message)
@@ -116,7 +125,18 @@ module Jade
116
125
  end
117
126
 
118
127
  def on_completion(state, message)
119
- [state, [respond(message['id'], Converters.completion_items)]]
128
+ message
129
+ .dig('params', 'textDocument', 'uri')
130
+ .then { module_name_for(it, state.source_root) }
131
+ .then { [state, [respond(message['id'], Converters.completion_items(it))]] }
132
+ end
133
+
134
+ def module_name_for(uri, source_root)
135
+ return nil unless uri && source_root
136
+
137
+ Converters
138
+ .relative_path(uri, source_root)
139
+ .then { Source.module_name_for(it) }
120
140
  end
121
141
 
122
142
  def on_prepare_rename(state, message)
@@ -148,6 +168,23 @@ module Jade
148
168
  [state, [respond(message['id'], edits)]]
149
169
  end
150
170
 
171
+ def on_type_formatting(state, message)
172
+ params = message['params']
173
+ state
174
+ .buffers[params.dig('textDocument', 'uri')]
175
+ .then { it ? on_type_edits(it, params) : [] }
176
+ .then { [state, [respond(message['id'], it)]] }
177
+ end
178
+
179
+ def on_type_edits(text, params)
180
+ Converters.on_type_edits(
181
+ text,
182
+ params.dig('position', 'line'),
183
+ params.dig('position', 'character'),
184
+ params['ch'],
185
+ )
186
+ end
187
+
151
188
  def formatting_edits_for(state, uri)
152
189
  text = state.buffers[uri]
153
190
  return nil unless text
@@ -261,43 +298,103 @@ module Jade
261
298
  # extra_uris always receive a publishDiagnostics, but compile output
262
299
  # wins: if a real diagnostic came back for the URI, we send that, not
263
300
  # an empty clear.
264
- def recompile_and_publish(state, extra_uris: [])
265
- if state.buffers.empty? || state.source_root.nil?
266
- return [state, extra_uris.map { publish_for(it, []) }]
267
- end
301
+ def recompile_and_publish(state, extra_uris: [], scope: :buffers)
302
+ return [state, extra_uris.map { publish_for(it, []) }] if state.source_root.nil?
268
303
 
269
304
  overlays = state.buffers
270
305
  .to_h { |uri, text| [Converters.relative_path(uri, state.source_root), text] }
271
306
 
272
- diagnostics_by_uri, registry = compile_each(state.source_root, overlays)
307
+ diagnostics_by_uri, registry =
308
+ compile_each(state, overlays, entries(state, overlays, scope))
273
309
 
274
- uris = (diagnostics_by_uri.keys + state.buffers.keys + extra_uris).uniq
310
+ uris = (diagnostics_by_uri.keys + state.buffers.keys + state.published + extra_uris).uniq
275
311
  messages = uris.map { publish_for(it, diagnostics_by_uri[it] || []) }
276
312
 
277
- next_state = registry ? state.set_registry(registry) : state
313
+ next_state = (registry ? state.set_registry(registry) : state)
314
+ .with_published(diagnostics_by_uri.keys)
278
315
  [next_state, messages]
279
316
  end
280
317
 
281
- # Compile from each open buffer so unrelated modules still produce
282
- # diagnostics. The same module reached twice yields identical
283
- # diagnostics (deterministic over overlays), so Hash#merge is safe.
284
- def compile_each(source_root, overlays)
285
- overlays.keys.reduce([{}, nil]) do |(diag_acc, reg_acc), entry|
286
- registry, diagnostics = compile(source_root, entry, overlays)
287
- [diag_acc.merge(diagnostics), registry || reg_acc]
288
- end
318
+ # Typing recompiles what is open; opening and closing sweep.
319
+ def entries(state, overlays, scope)
320
+ return overlays.keys if scope == :buffers
321
+
322
+ Dir
323
+ .glob('**/*.jd', base: state.source_root)
324
+ .sort
325
+ .then { imported_last(it, state.registry) }
289
326
  end
290
327
 
291
- def compile(source_root, entry_path, overlays)
328
+ # Compiling a module compiles what it imports, so the modules nothing
329
+ # imports cover the project in one pass. A file the last compile never
330
+ # saw goes first, since it may be the new root.
331
+ def imported_last(paths, registry)
332
+ return paths unless registry
333
+
334
+ [imported(registry), module_names(registry)]
335
+ .then { |names, by_path| paths.partition { !names.include?(by_path[it]) } }
336
+ .flatten
337
+ end
338
+
339
+ def imported(registry)
340
+ registry.dependency_graph.nodes.values.flatten.to_set
341
+ end
342
+
343
+ def module_names(registry)
344
+ registry
345
+ .modules
346
+ .each_value
347
+ .filter_map { [it.source.uri, it.name] if it.source }
348
+ .to_h
349
+ end
350
+
351
+ def compile_each(state, overlays, entries)
352
+ entries
353
+ .reduce([{}, nil, []]) { |acc, entry| compile_unless_seen(acc, state, overlays, entry) }
354
+ .first(2)
355
+ end
356
+
357
+ def compile_unless_seen((diagnostics, registry, seen), state, overlays, entry)
358
+ return [diagnostics, registry, seen] if seen.include?(entry)
359
+
360
+ compile(state, entry, overlays)
361
+ .then { |(compiled, found)| [diagnostics.merge(found), compiled || registry, seen + compiled_paths(compiled)] }
362
+ end
363
+
364
+ def compiled_paths(registry)
365
+ return [] unless registry
366
+
367
+ registry
368
+ .modules
369
+ .each_value
370
+ .reject { Stdlib.is_stdlib?(it) }
371
+ .filter_map { it.source&.uri }
372
+ end
373
+
374
+ def compile(state, entry_path, overlays)
292
375
  ModuleLoader
293
- .load(source_root, entry_path, tolerant: true, overlays:)
294
- .then { [it, collect_diagnostics(it, source_root)] }
376
+ .load(state.source_root, entry_path, cache_dir: state.cache_dir, tolerant: true, overlays:)
377
+ .then { [it, collect_diagnostics(it, state.source_root)] }
295
378
  rescue Jade::CompilationError => e
296
- [nil, diagnostics_by_uri(e.diagnostics, source_root)]
379
+ [nil, diagnostics_by_uri(e.diagnostics, state.source_root)]
297
380
  rescue StandardError => e
298
381
  $stderr.puts "[jade-lsp] compile crash: #{e.class}: #{e.message}"
299
382
  $stderr.puts e.backtrace.first(20).join("\n")
300
- [nil, {}]
383
+ [nil, crash_diagnostic(state.source_root, entry_path, overlays, e)]
384
+ end
385
+
386
+ # Publishing nothing renders a crashed compile as a clean file.
387
+ def crash_diagnostic(source_root, entry_path, overlays, error)
388
+ Jade::Source
389
+ .new(uri: entry_path, text: overlays[entry_path] || '')
390
+ .then { Jade::Diagnostics::Label[it, 0...0, nil] }
391
+ .then do
392
+ Jade::Diagnostics::Diagnostic.error(
393
+ "jade-lsp crashed compiling this file: #{error.class}: #{error.message}",
394
+ primary: it,
395
+ )
396
+ end
397
+ .then { { Converters.lsp_uri(entry_path, source_root) => [it] } }
301
398
  end
302
399
 
303
400
  def collect_diagnostics(registry, source_root)
@@ -105,6 +105,18 @@ module Jade
105
105
 
106
106
  module_function
107
107
 
108
+ # The compiler requires the name to match the path, so a placeholder
109
+ # here offers a decision with one right answer.
110
+ def for_module(module_name)
111
+ return ALL unless module_name
112
+
113
+ ALL.map do
114
+ next it unless it.label == 'module'
115
+
116
+ it.with(body: "module #{module_name} exposing (${0})")
117
+ end
118
+ end
119
+
108
120
  # Every form, as plain Jade — what `jade q syntax` serves.
109
121
  def catalog
110
122
  ALL.map { { form: it.label, detail: it.detail, source: it.source } }
@@ -1,12 +1,14 @@
1
1
  module Jade
2
2
  module LSP
3
- State = Data.define(:source_root, :buffers, :registry) do
3
+ State = Data.define(:source_root, :cache_dir, :buffers, :registry, :published) do
4
4
  def self.empty
5
- new(source_root: nil, buffers: {}, registry: nil)
5
+ new(source_root: nil, cache_dir: nil, buffers: {}, registry: nil, published: [])
6
6
  end
7
7
 
8
- def with_root(root)
9
- with(source_root: root)
8
+ # No manifest means no cache directory to share with the CLI, and
9
+ # every compile starts from nothing.
10
+ def with_project(project, root)
11
+ with(source_root: project&.source_root || root, cache_dir: project&.cache_path)
10
12
  end
11
13
 
12
14
  def put_buffer(uri, text)
@@ -20,6 +22,11 @@ module Jade
20
22
  def set_registry(reg)
21
23
  with(registry: reg)
22
24
  end
25
+
26
+ # Cleared next round, whether or not the file is still open.
27
+ def with_published(uris)
28
+ with(published: uris)
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -0,0 +1,36 @@
1
+ require 'fileutils'
2
+
3
+ require 'jade/module_loader/cache'
4
+
5
+ module Jade
6
+ module ModuleLoader
7
+ # The emitted Ruby, as opposed to `Cache`, which holds compiled
8
+ # modules. Generated code calls the runtime by name, so a build left
9
+ # by another version of the compiler can call a helper this one no
10
+ # longer has. Sources are untouched in that case, so mtimes say
11
+ # nothing: keep the fingerprint next to the build and drop the build
12
+ # when it does not match.
13
+ module Build
14
+ extend self
15
+
16
+ FINGERPRINT = '.fingerprint'
17
+
18
+ def discard_foreign(root)
19
+ return unless File.exist?(root)
20
+ return if fingerprint(root) == Cache.compiler_fingerprint
21
+
22
+ FileUtils.rm_rf(root)
23
+ end
24
+
25
+ def stamp(root)
26
+ File.write(File.join(root, FINGERPRINT), Cache.compiler_fingerprint)
27
+ end
28
+
29
+ private
30
+
31
+ def fingerprint(root)
32
+ File.join(root, FINGERPRINT).then { File.read(it) if File.exist?(it) }
33
+ end
34
+ end
35
+ end
36
+ end