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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f41f8fc46c0ec27d4ac70f84f6e60f9d3ad44f9fb50fafe7b4c13d71f935e071
4
- data.tar.gz: 418a82a7691f2f70927806291379740e789d14795aa6e3afe8e2dbbee1825a74
3
+ metadata.gz: 96ad37bd6ec750e9c26613eddc39034c1ee2b93837d4ef4d9b5427ca72b1072c
4
+ data.tar.gz: e17b0235f5cf7a6142cf5d6042a0f09a75158cf5aa21fb949f1460b0477772e4
5
5
  SHA512:
6
- metadata.gz: 3bfa7e60f2ca9e16b1aa01398a3ef1f44cca62907436cb120e313b3fc79a9fb959f0e560f1a51a4dcc47021371de999a1a4d24755df517e658066815218a3822
7
- data.tar.gz: 2bd0c26fe1062883602fc4ff9f91d17730e6327f40c9f33e0cc739833a313cb03a5384724faf88e459e57605809e2e73ff051270ba06ece03bdf60d94b871b77
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
- - Scoped CSS class definition, hover, completion, and validation in Haml.
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]` lookups, checked against the class map of
13
- # the companion stylesheet and any inline `:css` filters. The map
14
- # comes from the graph's records, collected on demand.
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 filters, so a `:ruby`
69
- # method chain such as `.freeze` is not a class. `ClassNames[:name]`
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
- if !filter_body.include?(index) && (head = TAG_HEAD.match(line_text))
76
- offset = head.begin(:shorthand)
77
- Text.each_match(head[:shorthand], index, SHORTHAND_CLASS, group: :name) do |match, span|
78
- found << Occurrence.new(match[:name], span.with(start_character: span.start_character + offset, end_character: span.end_character + offset))
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
- patterns = region.start_with?("(") ? [HTML_KEY] : HASH_KEYS
86
- inner = region[1...-1]
87
- patterns.each do |pattern|
88
- Text.each_match(inner, line_index, pattern, group: :key) do |match, span|
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Klenod
4
4
  module LSP
5
- VERSION = "0.0.32"
5
+ VERSION = "0.0.33"
6
6
  end
7
7
  end
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.32
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.32
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.32
25
+ version: 0.0.33
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: language_server-protocol
28
28
  requirement: !ruby/object:Gem::Requirement