klenod-lsp 0.0.30 → 0.0.32
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/check.rb +71 -17
- data/lib/klenod/lsp/languages/haml/classes.rb +6 -4
- data/lib/klenod/lsp/languages/haml/props.rb +25 -11
- data/lib/klenod/lsp/languages/haml/rename.rb +7 -3
- data/lib/klenod/lsp/languages/haml/ruby_regions.rb +33 -4
- data/lib/klenod/lsp/languages/haml.rb +10 -1
- data/lib/klenod/lsp/languages/imports.rb +20 -3
- 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: f41f8fc46c0ec27d4ac70f84f6e60f9d3ad44f9fb50fafe7b4c13d71f935e071
|
|
4
|
+
data.tar.gz: 418a82a7691f2f70927806291379740e789d14795aa6e3afe8e2dbbee1825a74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bfa7e60f2ca9e16b1aa01398a3ef1f44cca62907436cb120e313b3fc79a9fb959f0e560f1a51a4dcc47021371de999a1a4d24755df517e658066815218a3822
|
|
7
|
+
data.tar.gz: 2bd0c26fe1062883602fc4ff9f91d17730e6327f40c9f33e0cc739833a313cb03a5384724faf88e459e57605809e2e73ff051270ba06ece03bdf60d94b871b77
|
data/README.md
CHANGED
|
@@ -54,14 +54,17 @@ bundle exec klenod check src/components src/pages/Home.haml
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```
|
|
57
|
-
src/pages/Home.haml
|
|
58
|
-
|
|
57
|
+
src/pages/Home.haml
|
|
58
|
+
4:3 warning `Card` is imported but never used
|
|
59
|
+
9:12 error Could not resolve "./Missing"
|
|
60
|
+
|
|
59
61
|
1 error, 1 warning, 42 files checked.
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
It collects the whole module graph first, so cross-file diagnostics such as
|
|
63
65
|
unknown props and scoped classes are included. Paths limit which files are
|
|
64
66
|
reported. The command exits with status 1 when it reports any diagnostic.
|
|
67
|
+
Output is colored in a terminal; set `NO_COLOR` to turn colors off.
|
|
65
68
|
|
|
66
69
|
Configure the editor to run that command for `.haml` files. Ruby and CSS files
|
|
67
70
|
can also use it alongside their usual language servers.
|
data/lib/klenod/lsp/check.rb
CHANGED
|
@@ -26,6 +26,20 @@ module Klenod
|
|
|
26
26
|
Constant::DiagnosticSeverity::HINT => "hint"
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
+
COLORS = {
|
|
30
|
+
path: "\e[4m",
|
|
31
|
+
location: "\e[2m",
|
|
32
|
+
error: "\e[31m",
|
|
33
|
+
warning: "\e[33m",
|
|
34
|
+
info: "\e[1;34m",
|
|
35
|
+
hint: "\e[2m",
|
|
36
|
+
detail: "\e[2m",
|
|
37
|
+
code: "\e[36m",
|
|
38
|
+
success: "\e[1;32m"
|
|
39
|
+
}.freeze
|
|
40
|
+
RESET = "\e[0m"
|
|
41
|
+
CODE = /`([^`\n]+)`/
|
|
42
|
+
|
|
29
43
|
# One checked file and its diagnostics, which may be empty.
|
|
30
44
|
Result = Data.define(:path, :diagnostics)
|
|
31
45
|
|
|
@@ -58,38 +72,78 @@ module Klenod
|
|
|
58
72
|
end
|
|
59
73
|
end
|
|
60
74
|
|
|
61
|
-
# Prints
|
|
62
|
-
# with paths relative to `root`, then a summary
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
# Prints the diagnostics grouped by file, like ESLint's stylish format,
|
|
76
|
+
# with paths relative to `root`, then a summary:
|
|
77
|
+
#
|
|
78
|
+
# src/pages/Home.haml
|
|
79
|
+
# 4:3 warning `Card` is imported but never used
|
|
80
|
+
# 9:12 error Could not resolve "./Missing"
|
|
81
|
+
#
|
|
82
|
+
# 1 error, 1 warning, 42 files checked.
|
|
83
|
+
#
|
|
84
|
+
# Columns share one width across all files, so every row lines up.
|
|
85
|
+
# Returns the number of diagnostics. Colors default to on for a terminal
|
|
86
|
+
# unless NO_COLOR is set.
|
|
87
|
+
def self.report(results, output:, root: Dir.pwd, color: color?(output))
|
|
88
|
+
paint = color ? ->(name, text) { "#{COLORS.fetch(name)}#{text}#{RESET}" } : ->(_name, text) { text }
|
|
89
|
+
files = results.reject { |result| result.diagnostics.empty? }.map do |result|
|
|
90
|
+
rows = result.diagnostics
|
|
67
91
|
.sort_by { |diagnostic| [diagnostic.range.start.line, diagnostic.range.start.character] }
|
|
68
|
-
.map
|
|
92
|
+
.map do |diagnostic|
|
|
93
|
+
start = diagnostic.range.start
|
|
94
|
+
[start.line + 1, start.character + 1, SEVERITY_NAMES.fetch(diagnostic.severity), diagnostic.message]
|
|
95
|
+
end
|
|
96
|
+
[result.path, rows]
|
|
69
97
|
end
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
rows = files.flat_map(&:last)
|
|
99
|
+
|
|
100
|
+
# Line numbers align right and columns left, so the colons line up.
|
|
101
|
+
line_width = rows.map { |line, _, _, _| line.to_s.length }.max
|
|
102
|
+
column_width = rows.map { |_, column, _, _| column.to_s.length }.max
|
|
103
|
+
severity_width = rows.map { |_, _, severity, _| severity.length }.max
|
|
104
|
+
indent = " " * (2 + line_width.to_i + 1 + column_width.to_i + 2 + severity_width.to_i + 2)
|
|
105
|
+
|
|
106
|
+
files.each do |path, file_rows|
|
|
107
|
+
output.puts paint.call(:path, display_path(path, root))
|
|
108
|
+
file_rows.each do |line, column, severity, message|
|
|
109
|
+
location = "#{line.to_s.rjust(line_width)}:#{column.to_s.ljust(column_width)}"
|
|
110
|
+
first_line, *rest = message.lines(chomp: true)
|
|
111
|
+
first_line = highlight(first_line) if color
|
|
112
|
+
output.puts " #{paint.call(:location, location)} #{paint.call(severity.to_sym, severity.ljust(severity_width))} #{first_line}"
|
|
113
|
+
rest.each { |text| output.puts "#{indent}#{color ? highlight(text, COLORS.fetch(:detail)) : text}" }
|
|
114
|
+
end
|
|
115
|
+
output.puts
|
|
76
116
|
end
|
|
77
117
|
|
|
78
|
-
|
|
118
|
+
diagnostics = results.flat_map(&:diagnostics)
|
|
119
|
+
output.puts summary(results, diagnostics, paint)
|
|
79
120
|
diagnostics.length
|
|
80
121
|
end
|
|
81
122
|
|
|
82
|
-
def self.summary(results, diagnostics)
|
|
123
|
+
def self.summary(results, diagnostics, paint = ->(_name, text) { text })
|
|
83
124
|
files = "#{results.length} #{(results.length == 1) ? "file" : "files"} checked"
|
|
84
|
-
return "No problems found, #{files}." if diagnostics.empty?
|
|
125
|
+
return "#{paint.call(:success, "No problems found")}, #{files}." if diagnostics.empty?
|
|
85
126
|
|
|
86
127
|
counts = diagnostics.group_by(&:severity).sort.map do |severity, group|
|
|
87
128
|
name = SEVERITY_NAMES.fetch(severity)
|
|
88
|
-
"#{group.length} #{(group.length == 1) ? name : "#{name}s"}"
|
|
129
|
+
paint.call(name.to_sym, "#{group.length} #{(group.length == 1) ? name : "#{name}s"}")
|
|
89
130
|
end
|
|
90
131
|
"#{counts.join(", ")}, #{files}."
|
|
91
132
|
end
|
|
92
133
|
|
|
134
|
+
# Colors backticked names such as `Card` in place of their backticks.
|
|
135
|
+
# `base` is the color of the surrounding text, restored after each name.
|
|
136
|
+
def self.highlight(text, base = nil)
|
|
137
|
+
highlighted = text.gsub(CODE) { "#{COLORS.fetch(:code)}#{Regexp.last_match(1)}#{RESET}#{base}" }
|
|
138
|
+
base ? "#{base}#{highlighted}#{RESET}" : highlighted
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.color?(output, env = ENV)
|
|
142
|
+
return false if env.key?("NO_COLOR")
|
|
143
|
+
|
|
144
|
+
output.respond_to?(:tty?) && output.tty?
|
|
145
|
+
end
|
|
146
|
+
|
|
93
147
|
def self.display_path(path, root)
|
|
94
148
|
relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
|
|
95
149
|
relative.start_with?("../") ? path : relative
|
|
@@ -22,7 +22,7 @@ module Klenod
|
|
|
22
22
|
SHORTHAND_PREFIX = /\A\s*(?:%[A-Za-z][\w:-]*)?(?:[.#][\w-]+)*\.(?<partial>[\w-]*)\z/
|
|
23
23
|
LOOKUP_PREFIX = /ClassNames\[:(?<partial>[\w-]*)\z/
|
|
24
24
|
STYLE_KINDS = %i[companion_style inline_style].freeze
|
|
25
|
-
FILTER_HEAD = /\A\s
|
|
25
|
+
FILTER_HEAD = /\A\s*:(?<name>\w+)/
|
|
26
26
|
|
|
27
27
|
Occurrence = Data.define(:name, :span)
|
|
28
28
|
Definition = Data.define(:module_id, :generated)
|
|
@@ -92,14 +92,16 @@ module Klenod
|
|
|
92
92
|
# Indexes of the lines inside filters: every line after a `:name`
|
|
93
93
|
# line that is blank or indented deeper than it, as Haml reads them.
|
|
94
94
|
# Indentation alone also works while the document does not parse.
|
|
95
|
-
|
|
95
|
+
# Filters named in `except` are left out.
|
|
96
|
+
def filter_body_lines(lines, except: [])
|
|
96
97
|
filter_indent = nil
|
|
97
98
|
lines.each_with_index.with_object(Set.new) do |(line_text, index), body|
|
|
98
99
|
indent = line_text[/\A\s*/].length
|
|
99
100
|
if filter_indent && (line_text.strip.empty? || indent > filter_indent)
|
|
100
101
|
body << index
|
|
101
102
|
else
|
|
102
|
-
|
|
103
|
+
head = FILTER_HEAD.match(line_text)
|
|
104
|
+
filter_indent = (head && !except.include?(head[:name])) ? indent : nil
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
end
|
|
@@ -117,7 +119,7 @@ module Klenod
|
|
|
117
119
|
range: occurrence.span.to_range,
|
|
118
120
|
severity: Constant::DiagnosticSeverity::WARNING,
|
|
119
121
|
source: "klenod",
|
|
120
|
-
message: "Unknown CSS class
|
|
122
|
+
message: "Unknown CSS class `#{occurrence.name}`: not defined in #{style_names(map, analysis).map { |name| "`#{name}`" }.join(" or ")}"
|
|
121
123
|
)
|
|
122
124
|
end
|
|
123
125
|
end
|
|
@@ -5,6 +5,7 @@ require "language_server-protocol"
|
|
|
5
5
|
|
|
6
6
|
require_relative "../../text"
|
|
7
7
|
require_relative "../imports"
|
|
8
|
+
require_relative "classes"
|
|
8
9
|
require_relative "ruby_regions"
|
|
9
10
|
|
|
10
11
|
module Klenod
|
|
@@ -13,9 +14,9 @@ module Klenod
|
|
|
13
14
|
class Haml
|
|
14
15
|
# The props a `%Component` tag passes, checked against the props the
|
|
15
16
|
# component reads. Only keys written out on the tag line count:
|
|
16
|
-
# `%Foo(bar="1"){ baz: 2 }`. A splat
|
|
17
|
-
#
|
|
18
|
-
# is left alone.
|
|
17
|
+
# `%Foo(bar="1"){ baz: 2 }`. A splat or attributes spanning several
|
|
18
|
+
# lines make the tag unverifiable, and a component that reads no
|
|
19
|
+
# `$props`, or reads them all with `$*`, is left alone.
|
|
19
20
|
module Props
|
|
20
21
|
Interface = LanguageServer::Protocol::Interface
|
|
21
22
|
Constant = LanguageServer::Protocol::Constant
|
|
@@ -31,7 +32,10 @@ module Klenod
|
|
|
31
32
|
/(?<=[{,]|\A)\s*"(?<key>[^"]+)"\s*=>/
|
|
32
33
|
].freeze
|
|
33
34
|
SPLAT = /\*\*/
|
|
34
|
-
|
|
35
|
+
CLOSERS = {"{" => "}", "(" => ")", "[" => "]"}.freeze
|
|
36
|
+
# Props every component accepts: Haml passes `children`, `%Foo[item]`
|
|
37
|
+
# passes `key`, and `slot` places the component in its parent's slot.
|
|
38
|
+
IMPLICIT = %w[children key slot].freeze
|
|
35
39
|
|
|
36
40
|
Key = Data.define(:name, :span)
|
|
37
41
|
|
|
@@ -39,10 +43,13 @@ module Klenod
|
|
|
39
43
|
|
|
40
44
|
def diagnostics(analysis, workspace, index)
|
|
41
45
|
lines = analysis.lines
|
|
42
|
-
bindings =
|
|
46
|
+
bindings = Haml.bindings(lines)
|
|
43
47
|
props_cache = {}
|
|
48
|
+
filter_body = Classes.filter_body_lines(lines)
|
|
44
49
|
|
|
45
50
|
lines.each_with_index.flat_map do |line_text, line_index|
|
|
51
|
+
next [] if filter_body.include?(line_index)
|
|
52
|
+
|
|
46
53
|
tag = TAG_LINE.match(line_text)
|
|
47
54
|
next [] unless tag
|
|
48
55
|
|
|
@@ -57,16 +64,23 @@ module Klenod
|
|
|
57
64
|
next [] unless props && !props.splat && !props.names.empty?
|
|
58
65
|
|
|
59
66
|
allowed = props.names + IMPLICIT
|
|
60
|
-
keys.reject { |key| allowed.include?(key.name) }.map { |key| diagnostic(key, tag[:name], allowed) }
|
|
67
|
+
keys.reject { |key| allowed.include?(prop_name(key.name)) }.map { |key| diagnostic(key, tag[:name], allowed) }
|
|
61
68
|
end
|
|
62
69
|
end
|
|
63
70
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
71
|
+
# Components receive `video-id` as `$video_id`, like `merge_props`
|
|
72
|
+
# in the Haml plugin's helper source.
|
|
73
|
+
def prop_name(key)
|
|
74
|
+
key.tr("-", "_")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Keys from the tag's attribute regions, or nil when a splat or an
|
|
78
|
+
# attribute list continuing on the next lines makes them unknowable.
|
|
66
79
|
def explicit_keys(rest, line_index, offset)
|
|
67
80
|
keys = []
|
|
68
81
|
RubyRegions.attribute_regions(rest).each do |region_offset, region|
|
|
69
82
|
return nil if region.start_with?("{") && region.match?(SPLAT)
|
|
83
|
+
return nil unless region.end_with?(CLOSERS.fetch(region[0]))
|
|
70
84
|
|
|
71
85
|
patterns = region.start_with?("(") ? [HTML_KEY] : HASH_KEYS
|
|
72
86
|
inner = region[1...-1]
|
|
@@ -93,9 +107,9 @@ module Klenod
|
|
|
93
107
|
end
|
|
94
108
|
|
|
95
109
|
def diagnostic(key, tag_name, allowed)
|
|
96
|
-
suggestion = DidYouMean::SpellChecker.new(dictionary: allowed).correct(key.name).first
|
|
97
|
-
message = "Unknown prop
|
|
98
|
-
message += "; did you mean
|
|
110
|
+
suggestion = DidYouMean::SpellChecker.new(dictionary: allowed).correct(prop_name(key.name)).first
|
|
111
|
+
message = "Unknown prop `#{key.name}` for `#{tag_name}`"
|
|
112
|
+
message += "; did you mean `#{suggestion}`?" if suggestion
|
|
99
113
|
|
|
100
114
|
Interface::Diagnostic.new(
|
|
101
115
|
range: key.span.to_range,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../text"
|
|
4
4
|
require_relative "../imports"
|
|
5
|
+
require_relative "classes"
|
|
5
6
|
require_relative "ruby_regions"
|
|
6
7
|
|
|
7
8
|
module Klenod
|
|
@@ -9,9 +10,9 @@ module Klenod
|
|
|
9
10
|
module Languages
|
|
10
11
|
class Haml
|
|
11
12
|
# Where a bound constant appears in a Haml document: the `%Name` tags
|
|
12
|
-
# and the whole-word uses inside Ruby, meaning `:ruby`
|
|
13
|
-
# lines, tag attributes, and printed tag content.
|
|
14
|
-
# its words.
|
|
13
|
+
# outside filters and the whole-word uses inside Ruby, meaning `:ruby`
|
|
14
|
+
# filters, script lines, tag attributes, and printed tag content.
|
|
15
|
+
# Plain text keeps its words.
|
|
15
16
|
module Rename
|
|
16
17
|
module_function
|
|
17
18
|
|
|
@@ -25,7 +26,10 @@ module Klenod
|
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
filter_body = Classes.filter_body_lines(lines)
|
|
28
30
|
lines.each_with_index do |line_text, index|
|
|
31
|
+
next if filter_body.include?(index)
|
|
32
|
+
|
|
29
33
|
Text.each_match(line_text, index, Haml::COMPONENT_TAG, group: :name) do |match, span|
|
|
30
34
|
next unless match[:name].split("::").first == name
|
|
31
35
|
|
|
@@ -9,7 +9,8 @@ module Klenod
|
|
|
9
9
|
# Executable Ruby regions in a Haml document: Ruby filters, script
|
|
10
10
|
# lines, tag attributes, and printed tag content.
|
|
11
11
|
module RubyRegions
|
|
12
|
-
|
|
12
|
+
# `%name.class#id`, or `.class#id` for an implicit `%div`.
|
|
13
|
+
TAG_HEAD = /\A\s*(?:%[A-Za-z][\w:-]*(?:[.#][\w-]+)*|(?:[.#][\w-]+)+)/
|
|
13
14
|
|
|
14
15
|
module_function
|
|
15
16
|
|
|
@@ -35,22 +36,50 @@ module Klenod
|
|
|
35
36
|
when :script, :silent_script
|
|
36
37
|
yield line_index, 0, lines[line_index].to_s
|
|
37
38
|
when :tag
|
|
38
|
-
tag(node, lines
|
|
39
|
+
tag(node, lines, line_index, &block)
|
|
39
40
|
end
|
|
40
41
|
each_node(node.children, lines, &block) unless node.type == :filter
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
def tag(node,
|
|
45
|
+
def tag(node, lines, line_index, &block)
|
|
46
|
+
line_text = lines[line_index].to_s
|
|
45
47
|
head = TAG_HEAD.match(line_text)
|
|
46
48
|
return unless head
|
|
47
49
|
|
|
48
50
|
rest_start = head.end(0)
|
|
49
51
|
rest = line_text[rest_start..].to_s
|
|
52
|
+
regions = attribute_regions(rest)
|
|
50
53
|
if node.value[:parse]
|
|
51
54
|
yield line_index, rest_start, rest
|
|
52
55
|
else
|
|
53
|
-
|
|
56
|
+
regions.each { |offset, text| yield line_index, rest_start + offset, text }
|
|
57
|
+
end
|
|
58
|
+
continuation(lines, line_index + 1, regions.last&.last, parse: node.value[:parse], &block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The lines of an attribute region left open on the tag line, up to
|
|
62
|
+
# and including its closer. A tag printing Ruby content keeps the
|
|
63
|
+
# rest of the closing line too: `}= value`.
|
|
64
|
+
def continuation(lines, line_index, region, parse: false)
|
|
65
|
+
return unless region
|
|
66
|
+
|
|
67
|
+
opener = region[0]
|
|
68
|
+
closer = {"{" => "}", "(" => ")", "[" => "]"}.fetch(opener)
|
|
69
|
+
depth = region.count(opener) - region.count(closer)
|
|
70
|
+
while depth.positive? && line_index < lines.length
|
|
71
|
+
line_text = lines[line_index].to_s
|
|
72
|
+
length = line_text.length
|
|
73
|
+
line_text.each_char.with_index do |char, index|
|
|
74
|
+
depth += 1 if char == opener
|
|
75
|
+
depth -= 1 if char == closer
|
|
76
|
+
next unless depth.zero?
|
|
77
|
+
|
|
78
|
+
length = index + 1 unless parse
|
|
79
|
+
break
|
|
80
|
+
end
|
|
81
|
+
yield line_index, 0, line_text[0...length]
|
|
82
|
+
line_index += 1
|
|
54
83
|
end
|
|
55
84
|
end
|
|
56
85
|
|
|
@@ -68,13 +68,22 @@ module Klenod
|
|
|
68
68
|
|
|
69
69
|
# `%Details` compiles to the constant `Details`, bound by an import in
|
|
70
70
|
# the leading `:ruby` filter such as `Details = import("/components/Details")`.
|
|
71
|
+
# Imports in other filters, such as code samples in `:markdown`, are
|
|
72
|
+
# text.
|
|
71
73
|
def self.bindings(lines)
|
|
72
|
-
|
|
74
|
+
text = Classes.filter_body_lines(lines, except: %w[ruby])
|
|
75
|
+
Imports.bindings(lines.reject.with_index { |_line_text, index| text.include?(index) })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bindings(lines)
|
|
79
|
+
self.class.bindings(lines)
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
private
|
|
76
83
|
|
|
77
84
|
def component_target_at(line_text, position, lines)
|
|
85
|
+
return nil if Classes.filter_body_lines(lines).include?(position.line)
|
|
86
|
+
|
|
78
87
|
Text.each_match(line_text, position.line, COMPONENT_TAG, group: :name) do |match, span|
|
|
79
88
|
next unless span.include?(position)
|
|
80
89
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "language_server-protocol"
|
|
4
|
+
require "prism"
|
|
4
5
|
|
|
5
6
|
require "klenod/build/module_id"
|
|
6
7
|
|
|
@@ -116,6 +117,7 @@ module Klenod
|
|
|
116
117
|
tokens = []
|
|
117
118
|
slots = []
|
|
118
119
|
Haml::RubyRegions.each(source, lines) do |_line_index, _start_character, ruby_source|
|
|
120
|
+
ruby_source = without_comment(ruby_source)
|
|
119
121
|
tokens.concat(ruby_source.scan(PROP_TOKEN).flatten)
|
|
120
122
|
slots.concat(ruby_source.scan(SLOT_TOKEN).flatten)
|
|
121
123
|
end
|
|
@@ -126,6 +128,13 @@ module Klenod
|
|
|
126
128
|
)
|
|
127
129
|
end
|
|
128
130
|
|
|
131
|
+
# The Ruby before a trailing `# comment`, so commented-out code reads
|
|
132
|
+
# no props. Prism tells a comment from `#` inside a string.
|
|
133
|
+
def without_comment(ruby_source)
|
|
134
|
+
comment = Prism.lex(ruby_source).value.find { |token, _state| token.type == :COMMENT }&.first
|
|
135
|
+
comment ? ruby_source.byteslice(0, comment.location.start_offset) : ruby_source
|
|
136
|
+
end
|
|
137
|
+
|
|
129
138
|
# Completion items for the path being typed inside an import literal,
|
|
130
139
|
# or nil when the line prefix is not inside one.
|
|
131
140
|
def completion_items(prefix, position, analysis, workspace, syntax: Syntax::Ruby)
|
|
@@ -209,6 +218,11 @@ module Klenod
|
|
|
209
218
|
Syntax::Ruby
|
|
210
219
|
end
|
|
211
220
|
|
|
221
|
+
# Constant names bound to imports in a document, keyed by name.
|
|
222
|
+
def bindings(lines)
|
|
223
|
+
Imports.bindings(lines)
|
|
224
|
+
end
|
|
225
|
+
|
|
212
226
|
def definition(analysis, position, workspace, _index = nil)
|
|
213
227
|
target = target_at(analysis, position)
|
|
214
228
|
module_id = target && Imports.resolve(target, analysis, workspace)
|
|
@@ -367,11 +381,14 @@ module Klenod
|
|
|
367
381
|
def usage_spans(importer_id, lines, specifiers)
|
|
368
382
|
return [] unless importer_id.extname == ".haml"
|
|
369
383
|
|
|
370
|
-
names =
|
|
384
|
+
names = Languages::Haml.bindings(lines).select { |_name, specifier| specifiers.include?(specifier) }.keys
|
|
371
385
|
return [] if names.empty?
|
|
372
386
|
|
|
373
387
|
spans = []
|
|
388
|
+
filter_body = Languages::Haml::Classes.filter_body_lines(lines)
|
|
374
389
|
lines.each_with_index do |line_text, index|
|
|
390
|
+
next if filter_body.include?(index)
|
|
391
|
+
|
|
375
392
|
Text.each_match(line_text, index, Languages::Haml::COMPONENT_TAG, group: :name) do |match, span|
|
|
376
393
|
spans << span if names.include?(match[:name].split("::").first)
|
|
377
394
|
end
|
|
@@ -382,7 +399,7 @@ module Klenod
|
|
|
382
399
|
# Bindings whose constant is never used in the file. Languages give
|
|
383
400
|
# the spans a constant occupies, the binding included.
|
|
384
401
|
def unused_binding_diagnostics(analysis)
|
|
385
|
-
|
|
402
|
+
bindings(analysis.lines).filter_map do |name, _specifier|
|
|
386
403
|
spans = rename_spans(analysis, name)
|
|
387
404
|
next if spans.length > 1
|
|
388
405
|
|
|
@@ -391,7 +408,7 @@ module Klenod
|
|
|
391
408
|
range: span.to_range,
|
|
392
409
|
severity: Imports::Constant::DiagnosticSeverity::WARNING,
|
|
393
410
|
source: "klenod",
|
|
394
|
-
message: "
|
|
411
|
+
message: "`#{name}` is imported but never used",
|
|
395
412
|
tags: [Imports::Constant::DiagnosticTag::UNNECESSARY]
|
|
396
413
|
)
|
|
397
414
|
end
|
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.32
|
|
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.32
|
|
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.32
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: language_server-protocol
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|