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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +400 -12
- data/docs/interop.md +18 -1
- data/docs/stdlib.md +3 -1
- data/docs/syntax.md +96 -0
- data/lib/jade/api.rb +9 -0
- data/lib/jade/ast.rb +37 -1
- data/lib/jade/cli/eject.rb +132 -0
- data/lib/jade/cli.rb +2 -0
- data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
- data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
- data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
- data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
- data/lib/jade/codegen/boundary/specialized.rb +13 -5
- data/lib/jade/codegen/boundary.rb +12 -2
- data/lib/jade/codegen/context.rb +17 -0
- data/lib/jade/codegen/error.rb +1 -0
- data/lib/jade/codegen/function_call.rb +23 -7
- data/lib/jade/codegen/function_declaration.rb +25 -13
- data/lib/jade/codegen/helpers.rb +27 -4
- data/lib/jade/codegen/implementation.rb +2 -2
- data/lib/jade/codegen/inline.rb +1 -1
- data/lib/jade/codegen/inlines.rb +6 -1
- data/lib/jade/codegen/names.rb +31 -0
- data/lib/jade/codegen/pattern/constructor.rb +2 -2
- data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
- data/lib/jade/codegen.rb +76 -15
- data/lib/jade/compiler.rb +2 -0
- data/lib/jade/entry.rb +1 -1
- data/lib/jade/formatter/calls.rb +68 -11
- data/lib/jade/formatter/collections.rb +4 -4
- data/lib/jade/formatter/declarations.rb +13 -15
- data/lib/jade/formatter/helper.rb +20 -0
- data/lib/jade/formatter/infix_application.rb +19 -4
- data/lib/jade/formatter/lambda.rb +24 -12
- data/lib/jade/formatter/pattern.rb +3 -0
- data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
- data/lib/jade/frontend/desugaring.rb +29 -9
- data/lib/jade/frontend/fixity_fixer.rb +6 -4
- data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
- data/lib/jade/frontend/forward_declaration/error.rb +3 -0
- data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
- data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
- data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
- data/lib/jade/frontend/forward_declaration/module.rb +4 -0
- data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
- data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration.rb +2 -0
- data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
- data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
- data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
- data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
- data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
- data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
- data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
- data/lib/jade/frontend/pattern_analysis.rb +5 -38
- data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
- data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
- data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
- data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
- data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
- data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
- data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
- data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
- data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
- data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
- data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis.rb +5 -0
- data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
- data/lib/jade/frontend/type_checking/cascade.rb +90 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
- data/lib/jade/frontend/type_checking/constraints.rb +18 -3
- data/lib/jade/frontend/type_checking/error/display.rb +49 -0
- data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
- data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
- data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
- data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
- data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
- data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
- data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
- data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
- data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
- data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
- data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
- data/lib/jade/frontend/type_checking/error.rb +6 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
- data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
- data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
- data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
- data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
- data/lib/jade/frontend/type_checking/inference.rb +2 -0
- data/lib/jade/frontend/type_checking/loader.rb +11 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
- data/lib/jade/frontend/type_checking/substitution.rb +2 -2
- data/lib/jade/frontend/type_checking.rb +3 -1
- data/lib/jade/frontend/usage_analysis.rb +5 -1
- data/lib/jade/interop/boundary.rb +64 -24
- data/lib/jade/interop/error.rb +35 -7
- data/lib/jade/interop/lowering.rb +8 -2
- data/lib/jade/lsp/converters.rb +83 -2
- data/lib/jade/lsp/handlers.rb +122 -25
- data/lib/jade/lsp/snippets.rb +12 -0
- data/lib/jade/lsp/state.rb +11 -4
- data/lib/jade/module_loader/build.rb +36 -0
- data/lib/jade/module_loader.rb +1 -0
- data/lib/jade/parsing/combinators.rb +78 -2
- data/lib/jade/parsing/error.rb +43 -6
- data/lib/jade/parsing/type.rb +5 -3
- data/lib/jade/parsing.rb +140 -26
- data/lib/jade/runtime.rb +32 -13
- data/lib/jade/source.rb +6 -2
- data/lib/jade/stdlib/basics.rb +22 -3
- data/lib/jade/stdlib/char.rb +2 -0
- data/lib/jade/stdlib/clock.rb +6 -2
- data/lib/jade/stdlib/decimal.rb +51 -14
- data/lib/jade/stdlib/intrinsics.rb +59 -6
- data/lib/jade/stdlib/list.rb +37 -1
- data/lib/jade/stdlib/number.rb +47 -0
- data/lib/jade/stdlib/range.rb +211 -0
- data/lib/jade/stdlib.rb +11 -4
- data/lib/jade/symbol/alias.rb +11 -0
- data/lib/jade/symbol/parser.rb +1 -1
- data/lib/jade/symbol.rb +13 -1
- data/lib/jade/task.rb +0 -2
- data/lib/jade/type/anonymous_record.rb +11 -2
- data/lib/jade/type/application.rb +11 -2
- data/lib/jade/type/base.rb +26 -0
- data/lib/jade/type/function.rb +11 -2
- data/lib/jade/type.rb +36 -10
- data/lib/jade/version.rb +1 -1
- 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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/jade/interop/error.rb
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
92
|
+
"#{subject(path)}: expected #{expected}, got #{type_name(got)}#{seen(value)}"
|
|
81
93
|
|
|
82
94
|
in Jade::Decode::MissingField[key]
|
|
83
|
-
"#{subject}
|
|
95
|
+
"#{subject(path)}: missing field `#{key}`#{seen(value)}"
|
|
84
96
|
|
|
85
97
|
in Jade::Decode::Custom[msg]
|
|
86
|
-
"#{subject}
|
|
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
|
-
|
|
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
|
data/lib/jade/lsp/converters.rb
CHANGED
|
@@ -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
|
|
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,
|
data/lib/jade/lsp/handlers.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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)
|
data/lib/jade/lsp/snippets.rb
CHANGED
|
@@ -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 } }
|
data/lib/jade/lsp/state.rb
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|