theme-check 1.8.0 → 1.9.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/.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
|