klenod-lsp 0.0.32 → 0.0.33
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/README.md +5 -2
- data/lib/klenod/lsp/languages/haml/classes.rb +91 -12
- data/lib/klenod/lsp/languages/haml/props.rb +29 -12
- data/lib/klenod/lsp/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96ad37bd6ec750e9c26613eddc39034c1ee2b93837d4ef4d9b5427ca72b1072c
|
|
4
|
+
data.tar.gz: e17b0235f5cf7a6142cf5d6042a0f09a75158cf5aa21fb949f1460b0477772e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a933f96338104e6e505e1a28aecf759215b989c86487c6bb173628d97d52a0898c73e4bedff36270aa6d9fd35d31d91256b103ab5398b9650d781768b956e3dd
|
|
7
|
+
data.tar.gz: 497c4009b5f27f79c08a18f3b9022cb55376f3184152eacfa8f44fe3c039fcc986ba77a5f62dd4daba557fcc5cafcfc4d9ca7b69f76d5aa7444438efe5da6e92
|
data/README.md
CHANGED
|
@@ -17,8 +17,11 @@ application code.
|
|
|
17
17
|
- Symbol rename within a Haml or Ruby file and import-preserving edits when
|
|
18
18
|
files or directories move.
|
|
19
19
|
- Prop and slot information for Haml components, including spelling suggestions
|
|
20
|
-
for unknown props.
|
|
21
|
-
|
|
20
|
+
for unknown props. Prop checks inspect the outer attribute hash and leave
|
|
21
|
+
nested hash keys and text inside values alone.
|
|
22
|
+
- Scoped CSS class definition, hover, completion, and validation in Haml, for
|
|
23
|
+
`.name` shorthand, symbols in `class:` attributes such as
|
|
24
|
+
`class: [:name, { active: open? }]`, and `ClassNames[:name]` lookups.
|
|
22
25
|
- Route lenses and hovers when `RouterPlugin` is configured.
|
|
23
26
|
|
|
24
27
|
Ruby support is intentionally limited to Klenod imports and their bound
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "language_server-protocol"
|
|
4
|
+
require "prism"
|
|
4
5
|
|
|
5
6
|
require_relative "../../text"
|
|
7
|
+
require_relative "ruby_regions"
|
|
6
8
|
|
|
7
9
|
module Klenod
|
|
8
10
|
module LSP
|
|
9
11
|
module Languages
|
|
10
12
|
class Haml
|
|
11
|
-
# CSS class names in a Haml component: the `.name` shorthand on tags
|
|
12
|
-
# and `ClassNames[:name]`
|
|
13
|
-
#
|
|
14
|
-
# comes from the graph's
|
|
13
|
+
# CSS class names in a Haml component: the `.name` shorthand on tags,
|
|
14
|
+
# symbols in a tag's `class:` attribute, and `ClassNames[:name]`
|
|
15
|
+
# lookups, checked against the class map of the companion stylesheet
|
|
16
|
+
# and any inline `:css` filters. The map comes from the graph's
|
|
17
|
+
# records, collected on demand.
|
|
15
18
|
module Classes
|
|
16
19
|
Interface = LanguageServer::Protocol::Interface
|
|
17
20
|
Constant = LanguageServer::Protocol::Constant
|
|
@@ -21,6 +24,10 @@ module Klenod
|
|
|
21
24
|
LOOKUP = /ClassNames\[:(?<name>[\w-]+)\]/
|
|
22
25
|
SHORTHAND_PREFIX = /\A\s*(?:%[A-Za-z][\w:-]*)?(?:[.#][\w-]+)*\.(?<partial>[\w-]*)\z/
|
|
23
26
|
LOOKUP_PREFIX = /ClassNames\[:(?<partial>[\w-]*)\z/
|
|
27
|
+
CLASS_KEY_PREFIX = /(?:(?<![\w:])class:|:class\s*=>|["']class["']\s*(?::|=>))/
|
|
28
|
+
SYMBOL_PREFIX = /(?:\A|[\s\[,]):(?<partial>[\w-]*)\z/
|
|
29
|
+
OPENERS = "[{("
|
|
30
|
+
CLOSERS = "]})"
|
|
24
31
|
STYLE_KINDS = %i[companion_style inline_style].freeze
|
|
25
32
|
FILTER_HEAD = /\A\s*:(?<name>\w+)/
|
|
26
33
|
|
|
@@ -65,24 +72,76 @@ module Klenod
|
|
|
65
72
|
end
|
|
66
73
|
end
|
|
67
74
|
|
|
68
|
-
# Shorthand classes are only read outside
|
|
69
|
-
# method chain such as `.freeze` is not a
|
|
70
|
-
# lookups count everywhere.
|
|
75
|
+
# Shorthand and `class:` attribute classes are only read outside
|
|
76
|
+
# filters, so a `:ruby` method chain such as `.freeze` is not a
|
|
77
|
+
# class. `ClassNames[:name]` lookups count everywhere.
|
|
71
78
|
def occurrences(lines)
|
|
72
79
|
filter_body = filter_body_lines(lines)
|
|
73
80
|
lines.each_with_index.flat_map do |line_text, index|
|
|
74
81
|
found = []
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
unless filter_body.include?(index)
|
|
83
|
+
if (head = TAG_HEAD.match(line_text))
|
|
84
|
+
offset = head.begin(:shorthand)
|
|
85
|
+
Text.each_match(head[:shorthand], index, SHORTHAND_CLASS, group: :name) do |match, span|
|
|
86
|
+
found << Occurrence.new(match[:name], span.with(start_character: span.start_character + offset, end_character: span.end_character + offset))
|
|
87
|
+
end
|
|
79
88
|
end
|
|
89
|
+
found.concat(attribute_occurrences(line_text, index))
|
|
80
90
|
end
|
|
81
91
|
Text.each_match(line_text, index, LOOKUP, group: :name) { |match, span| found << Occurrence.new(match[:name], span) }
|
|
82
92
|
found
|
|
83
93
|
end
|
|
84
94
|
end
|
|
85
95
|
|
|
96
|
+
# Symbols in the `class:` value of an attribute hash closed on the
|
|
97
|
+
# tag line: `class: :name`, `:class => :name`, symbols in an array,
|
|
98
|
+
# and symbol keys of a conditional hash such as `{ active: open? }`.
|
|
99
|
+
# Computed values are left alone, and strings are unscoped classes.
|
|
100
|
+
def attribute_occurrences(line_text, line_index)
|
|
101
|
+
head = RubyRegions::TAG_HEAD.match(line_text)
|
|
102
|
+
return [] unless head
|
|
103
|
+
|
|
104
|
+
RubyRegions.attribute_regions(line_text[head.end(0)..].to_s).flat_map do |region_offset, region|
|
|
105
|
+
next [] unless region.start_with?("{")
|
|
106
|
+
|
|
107
|
+
result = Prism.parse(region)
|
|
108
|
+
hash = result.value.statements.body.first
|
|
109
|
+
next [] unless result.success? && hash.is_a?(Prism::HashNode)
|
|
110
|
+
|
|
111
|
+
offset = head.end(0) + region_offset
|
|
112
|
+
hash.elements.flat_map do |element|
|
|
113
|
+
next [] unless element.is_a?(Prism::AssocNode) && class_key?(element.key)
|
|
114
|
+
|
|
115
|
+
class_symbols(element.value).map do |symbol|
|
|
116
|
+
location = symbol.value_loc
|
|
117
|
+
Occurrence.new(
|
|
118
|
+
symbol.unescaped,
|
|
119
|
+
Text::Span.new(line_index, offset + location.start_character_offset, offset + location.end_character_offset)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def class_key?(node)
|
|
127
|
+
(node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)) && node.unescaped == "class"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def class_symbols(node)
|
|
131
|
+
case node
|
|
132
|
+
when Prism::SymbolNode
|
|
133
|
+
node.value_loc ? [node] : []
|
|
134
|
+
when Prism::ArrayNode
|
|
135
|
+
node.elements.flat_map { class_symbols(it) }
|
|
136
|
+
when Prism::HashNode, Prism::KeywordHashNode
|
|
137
|
+
node.elements.flat_map do |element|
|
|
138
|
+
(element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)) ? class_symbols(element.key) : []
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
[]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
86
145
|
def occurrence_at(lines, position)
|
|
87
146
|
return nil unless lines[position.line]
|
|
88
147
|
|
|
@@ -148,7 +207,7 @@ module Klenod
|
|
|
148
207
|
|
|
149
208
|
def completion_items(prefix, position, analysis, workspace, index)
|
|
150
209
|
in_filter = filter_body_lines(analysis.lines).include?(position.line)
|
|
151
|
-
match = (!in_filter && SHORTHAND_PREFIX.match(prefix)) || LOOKUP_PREFIX.match(prefix)
|
|
210
|
+
match = (!in_filter && (SHORTHAND_PREFIX.match(prefix) || class_symbol_prefix(prefix))) || LOOKUP_PREFIX.match(prefix)
|
|
152
211
|
return nil unless match
|
|
153
212
|
|
|
154
213
|
map = map(analysis, workspace, index)
|
|
@@ -168,6 +227,26 @@ module Klenod
|
|
|
168
227
|
end
|
|
169
228
|
end
|
|
170
229
|
|
|
230
|
+
# A symbol being typed in a tag's `class:` value, such as
|
|
231
|
+
# `%p{ class: [:lead, :k`. A comma outside brackets ends the value,
|
|
232
|
+
# so the next attribute's symbols are not completed as classes.
|
|
233
|
+
def class_symbol_prefix(prefix)
|
|
234
|
+
return nil unless RubyRegions::TAG_HEAD.match?(prefix)
|
|
235
|
+
|
|
236
|
+
key = prefix.to_enum(:scan, CLASS_KEY_PREFIX).map { Regexp.last_match }.last
|
|
237
|
+
return nil unless key
|
|
238
|
+
|
|
239
|
+
value = prefix[key.end(0)..]
|
|
240
|
+
depth = 0
|
|
241
|
+
value.each_char do |char|
|
|
242
|
+
depth += 1 if OPENERS.include?(char)
|
|
243
|
+
depth -= 1 if CLOSERS.include?(char)
|
|
244
|
+
return nil if depth.negative? || (depth.zero? && char == ",")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
SYMBOL_PREFIX.match(value)
|
|
248
|
+
end
|
|
249
|
+
|
|
171
250
|
def style_record(module_id, workspace, index)
|
|
172
251
|
index.record(module_id) || workspace.context.graph.collect_module(module_id)
|
|
173
252
|
rescue StandardError, ScriptError
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "did_you_mean"
|
|
4
4
|
require "language_server-protocol"
|
|
5
|
+
require "prism"
|
|
5
6
|
|
|
6
7
|
require_relative "../../text"
|
|
7
8
|
require_relative "../imports"
|
|
@@ -25,13 +26,6 @@ module Klenod
|
|
|
25
26
|
# `key="value"`, `key=value`, or a bare boolean `key`; a consumed
|
|
26
27
|
# value cannot be mistaken for the next key.
|
|
27
28
|
HTML_KEY = /(?<![\w-])(?<key>[A-Za-z_][\w-]*)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s()]+))?/
|
|
28
|
-
HASH_KEYS = [
|
|
29
|
-
/(?<=[{,]|\A)\s*(?<key>[A-Za-z_]\w*):(?!:)/,
|
|
30
|
-
/(?<=[{,]|\A)\s*"(?<key>[^"]+)":/,
|
|
31
|
-
/(?<=[{,]|\A)\s*:(?<key>\w+)\s*=>/,
|
|
32
|
-
/(?<=[{,]|\A)\s*"(?<key>[^"]+)"\s*=>/
|
|
33
|
-
].freeze
|
|
34
|
-
SPLAT = /\*\*/
|
|
35
29
|
CLOSERS = {"{" => "}", "(" => ")", "[" => "]"}.freeze
|
|
36
30
|
# Props every component accepts: Haml passes `children`, `%Foo[item]`
|
|
37
31
|
# passes `key`, and `slot` places the component in its parent's slot.
|
|
@@ -79,13 +73,15 @@ module Klenod
|
|
|
79
73
|
def explicit_keys(rest, line_index, offset)
|
|
80
74
|
keys = []
|
|
81
75
|
RubyRegions.attribute_regions(rest).each do |region_offset, region|
|
|
82
|
-
return nil if region.start_with?("{") && region.match?(SPLAT)
|
|
83
76
|
return nil unless region.end_with?(CLOSERS.fetch(region[0]))
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
if region.start_with?("{")
|
|
79
|
+
hash_keys = explicit_hash_keys(region, line_index, offset + region_offset)
|
|
80
|
+
return nil unless hash_keys
|
|
81
|
+
|
|
82
|
+
keys.concat(hash_keys)
|
|
83
|
+
elsif region.start_with?("(")
|
|
84
|
+
Text.each_match(region[1...-1], line_index, HTML_KEY, group: :key) do |match, span|
|
|
89
85
|
start = offset + region_offset + 1 + span.start_character
|
|
90
86
|
keys << Key.new(match[:key], Text::Span.new(line_index, start, start + match[:key].length))
|
|
91
87
|
end
|
|
@@ -94,6 +90,27 @@ module Klenod
|
|
|
94
90
|
keys.uniq { |key| key.span.start_character }
|
|
95
91
|
end
|
|
96
92
|
|
|
93
|
+
# Only the outer hash supplies props. Keys in nested hashes, blocks,
|
|
94
|
+
# and strings belong to values, and only an outer splat prevents us
|
|
95
|
+
# from knowing which props the component receives.
|
|
96
|
+
def explicit_hash_keys(source, line_index, offset)
|
|
97
|
+
result = Prism.parse(source, partial_script: true)
|
|
98
|
+
hash = result.value.statements.body.first
|
|
99
|
+
return nil unless result.success? && hash.is_a?(Prism::HashNode)
|
|
100
|
+
return nil if hash.elements.any? { |element| element.is_a?(Prism::AssocSplatNode) }
|
|
101
|
+
|
|
102
|
+
hash.elements.filter_map do |element|
|
|
103
|
+
name, location =
|
|
104
|
+
case (key = element.key)
|
|
105
|
+
when Prism::SymbolNode then [key.value, key.value_loc]
|
|
106
|
+
when Prism::StringNode then [key.unescaped, key.content_loc]
|
|
107
|
+
else next
|
|
108
|
+
end
|
|
109
|
+
start = offset + source.byteslice(0, location.start_offset).length
|
|
110
|
+
Key.new(name, Text::Span.new(line_index, start, start + location.slice.length))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
97
114
|
def component_props(specifier, analysis, workspace, index)
|
|
98
115
|
module_id = analysis.resolved_module_id_for(specifier) || workspace.resolve(specifier, importer_id: analysis.module_id)
|
|
99
116
|
return nil unless module_id
|
data/lib/klenod/lsp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klenod-lsp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.33
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrés Alin
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.33
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.33
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: language_server-protocol
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|