theme-check 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +21 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +2 -1
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +79 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +4 -6
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -13
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +28 -54
- data/lib/theme_check/file_system_storage.rb +4 -3
- data/lib/theme_check/html_node.rb +99 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -0
- data/lib/theme_check/language_server/bridge.rb +1 -1
- data/lib/theme_check/language_server/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +79 -28
- data/lib/theme_check/language_server/io_messenger.rb +9 -1
- data/lib/theme_check/language_server/server.rb +8 -7
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +23 -5
- data/lib/theme_check/liquid_node.rb +249 -39
- data/lib/theme_check/locale_diff.rb +16 -4
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- data/lib/theme_check/storage.rb +4 -0
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +18 -9
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +7 -2
- metadata +26 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -5,31 +5,30 @@ module ThemeCheck
|
|
5
5
|
class DiagnosticsEngine
|
6
6
|
include URIHelper
|
7
7
|
|
8
|
-
|
8
|
+
attr_reader :storage
|
9
|
+
|
10
|
+
def initialize(storage, bridge, diagnostics_manager = DiagnosticsManager.new)
|
9
11
|
@diagnostics_lock = Mutex.new
|
10
|
-
@
|
12
|
+
@diagnostics_manager = diagnostics_manager
|
13
|
+
@storage = storage
|
11
14
|
@bridge = bridge
|
12
15
|
@token = 0
|
13
16
|
end
|
14
17
|
|
15
18
|
def first_run?
|
16
|
-
@
|
19
|
+
@diagnostics_manager.first_run?
|
17
20
|
end
|
18
21
|
|
19
|
-
def analyze_and_send_offenses(absolute_path, config)
|
22
|
+
def analyze_and_send_offenses(absolute_path, config, force: false)
|
20
23
|
return unless @diagnostics_lock.try_lock
|
21
24
|
@token += 1
|
22
25
|
@bridge.send_create_work_done_progress_request(@token)
|
23
|
-
storage = ThemeCheck::FileSystemStorage.new(
|
24
|
-
config.root,
|
25
|
-
ignored_patterns: config.ignored_patterns
|
26
|
-
)
|
27
26
|
theme = ThemeCheck::Theme.new(storage)
|
28
27
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
29
28
|
|
30
|
-
if @
|
29
|
+
if @diagnostics_manager.first_run? || force
|
31
30
|
@bridge.send_work_done_progress_begin(@token, "Full theme check")
|
32
|
-
@bridge.log("Checking #{
|
31
|
+
@bridge.log("Checking #{storage.root}")
|
33
32
|
offenses = nil
|
34
33
|
time = Benchmark.measure do
|
35
34
|
offenses = analyzer.analyze_theme do |path, i, total|
|
@@ -55,7 +54,7 @@ module ThemeCheck
|
|
55
54
|
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
56
55
|
@bridge.send_work_done_progress_end(@token, end_message)
|
57
56
|
@bridge.log(end_message)
|
58
|
-
send_diagnostics(offenses, [
|
57
|
+
send_diagnostics(offenses, [relative_path])
|
59
58
|
end
|
60
59
|
end
|
61
60
|
@diagnostics_lock.unlock
|
@@ -64,62 +63,18 @@ module ThemeCheck
|
|
64
63
|
private
|
65
64
|
|
66
65
|
def send_diagnostics(offenses, analyzed_files = nil)
|
67
|
-
@
|
68
|
-
send_diagnostic(
|
66
|
+
@diagnostics_manager.build_diagnostics(offenses, analyzed_files: analyzed_files).each do |relative_path, diagnostics|
|
67
|
+
send_diagnostic(relative_path, diagnostics)
|
69
68
|
end
|
70
69
|
end
|
71
70
|
|
72
|
-
def send_diagnostic(
|
71
|
+
def send_diagnostic(relative_path, diagnostics)
|
73
72
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
74
73
|
@bridge.send_notification('textDocument/publishDiagnostics', {
|
75
|
-
uri: file_uri(path),
|
76
|
-
diagnostics:
|
74
|
+
uri: file_uri(storage.path(relative_path)),
|
75
|
+
diagnostics: diagnostics.map(&:to_h),
|
77
76
|
})
|
78
77
|
end
|
79
|
-
|
80
|
-
def offense_to_diagnostic(offense)
|
81
|
-
diagnostic = {
|
82
|
-
code: offense.code_name,
|
83
|
-
message: offense.message,
|
84
|
-
range: range(offense),
|
85
|
-
severity: severity(offense),
|
86
|
-
source: "theme-check",
|
87
|
-
}
|
88
|
-
diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
|
89
|
-
diagnostic
|
90
|
-
end
|
91
|
-
|
92
|
-
def code_description(offense)
|
93
|
-
{
|
94
|
-
href: offense.doc,
|
95
|
-
}
|
96
|
-
end
|
97
|
-
|
98
|
-
def severity(offense)
|
99
|
-
case offense.severity
|
100
|
-
when :error
|
101
|
-
1
|
102
|
-
when :suggestion
|
103
|
-
2
|
104
|
-
when :style
|
105
|
-
3
|
106
|
-
else
|
107
|
-
4
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def range(offense)
|
112
|
-
{
|
113
|
-
start: {
|
114
|
-
line: offense.start_line,
|
115
|
-
character: offense.start_column,
|
116
|
-
},
|
117
|
-
end: {
|
118
|
-
line: offense.end_line,
|
119
|
-
character: offense.end_column,
|
120
|
-
},
|
121
|
-
}
|
122
|
-
end
|
123
78
|
end
|
124
79
|
end
|
125
80
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
module LanguageServer
|
6
|
+
class DiagnosticsManager
|
7
|
+
# This class exists to facilitate LanguageServer diagnostics tracking.
|
8
|
+
#
|
9
|
+
# Motivations:
|
10
|
+
# 1. The first time we lint, we want all the errors from all the files.
|
11
|
+
# 2. If we fix all the errors in a file, we have to send an empty array for that file.
|
12
|
+
# 3. If we do a partial check, we should consider the whole theme diagnostics as valid, and return cached results
|
13
|
+
# 4. We should be able to create WorkspaceEdits from diagnostics, so that the ExecuteCommandEngine can do its job
|
14
|
+
# 5. We should clean up diagnostics that were applied by the client
|
15
|
+
def initialize
|
16
|
+
@latest_diagnostics = {} # { [Pathname(relative_path)] => Diagnostic[] }
|
17
|
+
@mutex = Mutex.new
|
18
|
+
@first_run = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_run?
|
22
|
+
@first_run
|
23
|
+
end
|
24
|
+
|
25
|
+
def diagnostics(relative_path)
|
26
|
+
relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
|
27
|
+
@mutex.synchronize { @latest_diagnostics[relative_path] || [] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_diagnostics(offenses, analyzed_files: nil)
|
31
|
+
@mutex.synchronize do
|
32
|
+
full_check = analyzed_files.nil?
|
33
|
+
analyzed_paths = analyzed_files.map { |path| Pathname.new(path) } unless full_check
|
34
|
+
|
35
|
+
# When analyzed_files is nil, contains all offenses.
|
36
|
+
# When analyzed_files is !nil, contains all whole theme offenses and single file offenses of the analyzed_files.
|
37
|
+
current_diagnostics = offenses
|
38
|
+
.select(&:theme_file)
|
39
|
+
.group_by(&:theme_file)
|
40
|
+
.transform_keys { |theme_file| Pathname.new(theme_file.relative_path) }
|
41
|
+
.transform_values do |theme_file_offenses|
|
42
|
+
theme_file_offenses.map { |o| Diagnostic.new(o) }
|
43
|
+
end
|
44
|
+
|
45
|
+
previous_paths = paths(@latest_diagnostics)
|
46
|
+
current_paths = paths(current_diagnostics)
|
47
|
+
|
48
|
+
diagnostics_update = (current_paths + previous_paths).map do |path|
|
49
|
+
# When doing a full_check, we either send the current
|
50
|
+
# diagnostics or an empty array to clear the diagnostics
|
51
|
+
# for that file.
|
52
|
+
if full_check
|
53
|
+
[path, current_diagnostics[path] || []]
|
54
|
+
|
55
|
+
# When doing a partial check, the single file diagnostics
|
56
|
+
# from the previous runs should be sent. Otherwise the
|
57
|
+
# latest results are the good ones.
|
58
|
+
else
|
59
|
+
new_diagnostics = current_diagnostics[path] || []
|
60
|
+
should_use_cached_results = !analyzed_paths.include?(path)
|
61
|
+
old_diagnostics = should_use_cached_results ? single_file_diagnostics(path) : []
|
62
|
+
[path, new_diagnostics + old_diagnostics]
|
63
|
+
end
|
64
|
+
end.to_h
|
65
|
+
|
66
|
+
@latest_diagnostics = diagnostics_update.reject { |_, v| v.empty? }
|
67
|
+
@first_run = false
|
68
|
+
diagnostics_update
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def workspace_edit(diagnostics)
|
73
|
+
diagnostics = sanitize(diagnostics)
|
74
|
+
.select(&:correctable?)
|
75
|
+
|
76
|
+
{
|
77
|
+
documentChanges: document_changes(diagnostics),
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete_applied(diagnostics)
|
82
|
+
diagnostics = sanitize(diagnostics)
|
83
|
+
.select(&:correctable?)
|
84
|
+
|
85
|
+
previous_paths = paths(@latest_diagnostics)
|
86
|
+
|
87
|
+
diagnostics.each do |diagnostic|
|
88
|
+
delete(diagnostic.relative_path, diagnostic)
|
89
|
+
end
|
90
|
+
|
91
|
+
current_paths = paths(@latest_diagnostics)
|
92
|
+
|
93
|
+
(current_paths + previous_paths).map do |path|
|
94
|
+
[path, @latest_diagnostics[path] || []]
|
95
|
+
end.to_h
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def sanitize(diagnostics)
|
101
|
+
diagnostics = diagnostics.map { |hash| find(hash) }.reject(&:nil?) if diagnostics[0]&.is_a?(Hash)
|
102
|
+
diagnostics
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete(relative_path, diagnostic)
|
106
|
+
relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
|
107
|
+
@mutex.synchronize do
|
108
|
+
@latest_diagnostics[relative_path]&.delete(diagnostic)
|
109
|
+
@latest_diagnostics.delete(relative_path) if @latest_diagnostics[relative_path]&.empty?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def find(diagnostic_hash)
|
114
|
+
diagnostics(diagnostic_hash.dig(:data, :relative_path))
|
115
|
+
.find { |d| d == diagnostic_hash }
|
116
|
+
end
|
117
|
+
|
118
|
+
def document_changes(diagnostics)
|
119
|
+
corrector = DocumentChangeCorrector.new
|
120
|
+
diagnostics.each do |diagnostic|
|
121
|
+
offense = diagnostic.offense
|
122
|
+
offense.correct(corrector)
|
123
|
+
end
|
124
|
+
corrector.document_changes
|
125
|
+
end
|
126
|
+
|
127
|
+
def paths(diagnostics)
|
128
|
+
(diagnostics || {}).keys.map { |path| Pathname.new(path) }.to_set
|
129
|
+
end
|
130
|
+
|
131
|
+
def single_file_diagnostics(relative_path)
|
132
|
+
@latest_diagnostics[relative_path]&.select(&:single_file?) || []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class DocumentChangeCorrector
|
6
|
+
include URIHelper
|
7
|
+
include JsonHelpers
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@json_edits = {}
|
11
|
+
@json_file_edits = {}
|
12
|
+
@text_document_edits = {}
|
13
|
+
@create_files = []
|
14
|
+
@rename_files = []
|
15
|
+
@delete_files = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def document_changes
|
19
|
+
apply_json_edits
|
20
|
+
apply_json_file_edits
|
21
|
+
@create_files + @rename_files + @text_document_edits.values + @delete_files
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param node [Node]
|
25
|
+
def insert_before(node, content, character_range = nil)
|
26
|
+
position = character_range_position(node, character_range) if character_range
|
27
|
+
edits(node) << {
|
28
|
+
range: {
|
29
|
+
start: start_location(position || node),
|
30
|
+
end: start_location(position || node),
|
31
|
+
},
|
32
|
+
newText: content,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param node [Node]
|
37
|
+
def insert_after(node, content, character_range = nil)
|
38
|
+
position = character_range_position(node, character_range) if character_range
|
39
|
+
edits(node) << {
|
40
|
+
range: {
|
41
|
+
start: end_location(position || node),
|
42
|
+
end: end_location(position || node),
|
43
|
+
},
|
44
|
+
newText: content,
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def replace(node, content, character_range = nil)
|
49
|
+
position = character_range_position(node, character_range) if character_range
|
50
|
+
edits(node) << {
|
51
|
+
range: range(position || node),
|
52
|
+
newText: content,
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param node [LiquidNode]
|
57
|
+
def remove(node)
|
58
|
+
edits(node) << {
|
59
|
+
range: {
|
60
|
+
start: { line: node.outer_markup_start_row, character: node.outer_markup_start_column },
|
61
|
+
end: { line: node.outer_markup_end_row, character: node.outer_markup_end_column },
|
62
|
+
},
|
63
|
+
newText: '',
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def replace_inner_markup(node, content)
|
68
|
+
edits(node) << {
|
69
|
+
range: {
|
70
|
+
start: {
|
71
|
+
line: node.inner_markup_start_row,
|
72
|
+
character: node.inner_markup_start_column,
|
73
|
+
},
|
74
|
+
end: {
|
75
|
+
line: node.inner_markup_end_row,
|
76
|
+
character: node.inner_markup_end_column,
|
77
|
+
},
|
78
|
+
},
|
79
|
+
newText: content,
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def replace_inner_json(node, json, **pretty_json_opts)
|
84
|
+
# Kind of brittle alert: We're assuming that modifications are
|
85
|
+
# made directly on the same json hash (e.g. schema). As such,
|
86
|
+
# if this assumption is true, then it follows that the
|
87
|
+
# "correct" JSON is the _last_ one that we defined.
|
88
|
+
#
|
89
|
+
# We're going to append those changes to the text edit when
|
90
|
+
# we're done.
|
91
|
+
#
|
92
|
+
# We're doing this because no language client will accept
|
93
|
+
# text modifications that occur on the same range. So we need
|
94
|
+
# to dedup our JSON edits for the client to accept our change.
|
95
|
+
#
|
96
|
+
# What we're doing here is overwriting the json edit for a
|
97
|
+
# node to the latest one that is called. If all the edits
|
98
|
+
# occur on the same hash, this final hash will have all the
|
99
|
+
# edits in it.
|
100
|
+
@json_edits[node] = [json, pretty_json_opts]
|
101
|
+
end
|
102
|
+
|
103
|
+
def wrap(node, insert_before, insert_after)
|
104
|
+
edits(node) << {
|
105
|
+
range: range(node),
|
106
|
+
newText: insert_before + node.markup + insert_after,
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_file(storage, relative_path, contents = nil, overwrite: false)
|
111
|
+
uri = file_uri(storage.path(relative_path))
|
112
|
+
@create_files << create_file_change(uri, overwrite)
|
113
|
+
return if contents.nil?
|
114
|
+
text_document = { uri: uri, version: nil }
|
115
|
+
@text_document_edits[text_document] = {
|
116
|
+
textDocument: text_document,
|
117
|
+
edits: [{
|
118
|
+
range: {
|
119
|
+
start: { line: 0, character: 0 },
|
120
|
+
end: { line: 0, character: 0 },
|
121
|
+
},
|
122
|
+
newText: contents,
|
123
|
+
}],
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def remove_file(storage, relative_path)
|
128
|
+
uri = file_uri(storage.path(relative_path))
|
129
|
+
@delete_files << delete_file_change(uri)
|
130
|
+
end
|
131
|
+
|
132
|
+
def mkdir(storage, relative_path)
|
133
|
+
path = Pathname.new(relative_path).join("tmp").to_s
|
134
|
+
# The LSP doesn't have a concept for directories, so what we
|
135
|
+
# do is create a file and then delete it.
|
136
|
+
#
|
137
|
+
# It does the job :upside_down_smile:.
|
138
|
+
create_file(storage, path)
|
139
|
+
remove_file(storage, path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def add_translation(file, path, value)
|
143
|
+
raise ArgumentError unless file.is_a?(JsonFile)
|
144
|
+
hash = file.content
|
145
|
+
SchemaHelper.set(hash, path, value)
|
146
|
+
@json_file_edits[file] = hash
|
147
|
+
end
|
148
|
+
|
149
|
+
def remove_translation(file, path)
|
150
|
+
raise ArgumentError unless file.is_a?(JsonFile)
|
151
|
+
hash = file.content
|
152
|
+
SchemaHelper.delete(hash, path)
|
153
|
+
@json_file_edits[file] = hash
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def apply_json_edits
|
159
|
+
@json_edits.each do |node, (json, pretty_json_opts)|
|
160
|
+
replace_inner_markup(node, pretty_json(json, **pretty_json_opts))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def apply_json_file_edits
|
165
|
+
@json_file_edits.each do |file, hash|
|
166
|
+
replace_entire_file(file, JSON.pretty_generate(hash))
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def replace_entire_file(file, contents)
|
171
|
+
text_document = to_text_document(file)
|
172
|
+
position = ThemeCheck::StrictPosition.new(file.source, file.source, 0)
|
173
|
+
@text_document_edits[text_document] = {
|
174
|
+
textDocument: text_document,
|
175
|
+
edits: [{
|
176
|
+
range: {
|
177
|
+
start: { line: 0, character: 0 },
|
178
|
+
end: { line: position.end_row, character: position.end_column },
|
179
|
+
},
|
180
|
+
newText: contents,
|
181
|
+
}],
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
# @param node [Node]
|
186
|
+
def text_document_edit(node)
|
187
|
+
text_document = to_text_document(node)
|
188
|
+
@text_document_edits[text_document] ||= {
|
189
|
+
textDocument: text_document,
|
190
|
+
edits: [],
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
def create_file_change(uri, overwrite = false)
|
195
|
+
change = {}
|
196
|
+
change[:kind] = 'create'
|
197
|
+
change[:uri] = uri
|
198
|
+
change[:options] = { overwrite: overwrite } if overwrite
|
199
|
+
change
|
200
|
+
end
|
201
|
+
|
202
|
+
def delete_file_change(uri)
|
203
|
+
{
|
204
|
+
kind: 'delete',
|
205
|
+
uri: uri,
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
def edits(node)
|
210
|
+
text_document_edit(node)[:edits]
|
211
|
+
end
|
212
|
+
|
213
|
+
def to_text_document(thing)
|
214
|
+
case thing
|
215
|
+
when Node
|
216
|
+
{
|
217
|
+
uri: file_uri(thing.theme_file&.path),
|
218
|
+
version: thing.theme_file&.version,
|
219
|
+
}
|
220
|
+
when ThemeFile
|
221
|
+
{
|
222
|
+
uri: file_uri(thing.path),
|
223
|
+
version: thing.version,
|
224
|
+
}
|
225
|
+
else
|
226
|
+
raise ArgumentError
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def absolute_path(node)
|
231
|
+
node.theme_file&.path
|
232
|
+
end
|
233
|
+
|
234
|
+
def character_range_position(node, character_range)
|
235
|
+
return unless character_range
|
236
|
+
source = node.theme_file.source
|
237
|
+
StrictPosition.new(
|
238
|
+
source[character_range],
|
239
|
+
source,
|
240
|
+
character_range.begin,
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
# @param node [ThemeCheck::Node]
|
245
|
+
def range(node)
|
246
|
+
{
|
247
|
+
start: start_location(node),
|
248
|
+
end: end_location(node),
|
249
|
+
}
|
250
|
+
end
|
251
|
+
|
252
|
+
def start_location(node)
|
253
|
+
{
|
254
|
+
line: node.start_row,
|
255
|
+
character: node.start_column,
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
259
|
+
def end_location(node)
|
260
|
+
{
|
261
|
+
line: node.end_row,
|
262
|
+
character: node.end_column,
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
@@ -37,12 +37,12 @@ module ThemeCheck
|
|
37
37
|
|
38
38
|
def document_links(buffer)
|
39
39
|
matches(buffer, partial_regexp).map do |match|
|
40
|
-
|
40
|
+
start_row, start_column = from_index_to_row_column(
|
41
41
|
buffer,
|
42
42
|
match.begin(:partial),
|
43
43
|
)
|
44
44
|
|
45
|
-
|
45
|
+
end_row, end_column = from_index_to_row_column(
|
46
46
|
buffer,
|
47
47
|
match.end(:partial)
|
48
48
|
)
|
@@ -51,12 +51,12 @@ module ThemeCheck
|
|
51
51
|
target: file_link(match[:partial]),
|
52
52
|
range: {
|
53
53
|
start: {
|
54
|
-
line:
|
55
|
-
character:
|
54
|
+
line: start_row,
|
55
|
+
character: start_column,
|
56
56
|
},
|
57
57
|
end: {
|
58
|
-
line:
|
59
|
-
character:
|
58
|
+
line: end_row,
|
59
|
+
character: end_column,
|
60
60
|
},
|
61
61
|
},
|
62
62
|
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class ExecuteCommandEngine
|
6
|
+
def initialize
|
7
|
+
@providers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(provider)
|
11
|
+
@providers[provider.command] = provider
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(command, arguments)
|
15
|
+
@providers[command].execute(arguments)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class ExecuteCommandProvider
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
@all ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def inherited(subclass)
|
12
|
+
all << subclass
|
13
|
+
end
|
14
|
+
|
15
|
+
def command(cmd = nil)
|
16
|
+
@command = cmd unless cmd.nil?
|
17
|
+
@command
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute(arguments)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def command
|
26
|
+
self.class.command
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CorrectionExecuteCommandProvider < ExecuteCommandProvider
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
command "correction"
|
9
|
+
|
10
|
+
attr_reader :storage, :bridge, :diagnostics_manager
|
11
|
+
|
12
|
+
def initialize(storage, bridge, diagnostics_manager)
|
13
|
+
@storage = storage
|
14
|
+
@bridge = bridge
|
15
|
+
@diagnostics_manager = diagnostics_manager
|
16
|
+
end
|
17
|
+
|
18
|
+
# The arguments passed to this method are the ones forwarded
|
19
|
+
# from the selected CodeAction by the client.
|
20
|
+
#
|
21
|
+
# @param diagnostic_hashes [Array] - of diagnostics
|
22
|
+
def execute(diagnostic_hashes)
|
23
|
+
# attempt to apply the document changes
|
24
|
+
workspace_edit = diagnostics_manager.workspace_edit(diagnostic_hashes)
|
25
|
+
result = bridge.send_request('workspace/applyEdit', {
|
26
|
+
label: 'Theme Check correction',
|
27
|
+
edit: workspace_edit,
|
28
|
+
})
|
29
|
+
|
30
|
+
# Bail if unable to apply changes
|
31
|
+
return unless result[:applied]
|
32
|
+
|
33
|
+
# Clean up internal representation of fixed diagnostics
|
34
|
+
diagnostics_update = diagnostics_manager.delete_applied(diagnostic_hashes)
|
35
|
+
|
36
|
+
# Send updated diagnostics to client
|
37
|
+
diagnostics_update
|
38
|
+
.map do |relative_path, diagnostics|
|
39
|
+
bridge.send_notification('textDocument/publishDiagnostics', {
|
40
|
+
uri: file_uri(storage.path(relative_path)),
|
41
|
+
diagnostics: diagnostics.map(&:to_h),
|
42
|
+
})
|
43
|
+
storage.path(relative_path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class RunChecksExecuteCommandProvider < ExecuteCommandProvider
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
command "runChecks"
|
9
|
+
|
10
|
+
def initialize(diagnostics_engine, root_path, root_config)
|
11
|
+
@diagnostics_engine = diagnostics_engine
|
12
|
+
@root_path = root_path
|
13
|
+
@root_config = root_config
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(_args)
|
17
|
+
@diagnostics_engine.analyze_and_send_offenses(@root_path, @root_config, force: true)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|