theme-check 1.7.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 +49 -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 +3 -1
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/checks/TEMPLATE.md.erb +24 -19
- 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/exe/theme-check-language-server +0 -4
- 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 +14 -9
- 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/required_layout_theme_object.rb +9 -4
- 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 -25
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- 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 +34 -23
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +8 -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 +142 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- 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/completion_providers/tag_completion_provider.rb +3 -1
- 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 +80 -0
- 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 +92 -217
- data/lib/theme_check/language_server/io_messenger.rb +112 -0
- data/lib/theme_check/language_server/messenger.rb +12 -42
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +54 -110
- 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 +28 -6
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +48 -10
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
- data/lib/theme_check/shopify_liquid/tag.rb +19 -1
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- 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 +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +31 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class CodeActionProvider
|
|
6
|
+
class << self
|
|
7
|
+
def all
|
|
8
|
+
@all ||= []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def inherited(subclass)
|
|
12
|
+
all << subclass
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def kind(k = nil)
|
|
16
|
+
@kind = k unless k.nil?
|
|
17
|
+
@kind
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :storage
|
|
22
|
+
attr_reader :diagnostics_manager
|
|
23
|
+
|
|
24
|
+
def initialize(storage, diagnostics_manager)
|
|
25
|
+
@storage = storage
|
|
26
|
+
@diagnostics_manager = diagnostics_manager
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def kind
|
|
30
|
+
self.class.kind
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def base_kind
|
|
34
|
+
kind.split('.')[0]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def code_actions(relative_path, range)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class QuickfixCodeActionProvider < CodeActionProvider
|
|
6
|
+
kind "quickfix"
|
|
7
|
+
|
|
8
|
+
def code_actions(relative_path, range)
|
|
9
|
+
correctable_diagnostics = diagnostics_manager
|
|
10
|
+
.diagnostics(relative_path)
|
|
11
|
+
.filter(&:correctable?)
|
|
12
|
+
.reject do |diagnostic|
|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
diagnostics_under_cursor = correctable_diagnostics
|
|
19
|
+
.filter { |diagnostic| diagnostic.offense.in_range?(range) }
|
|
20
|
+
|
|
21
|
+
return [] if diagnostics_under_cursor.empty?
|
|
22
|
+
|
|
23
|
+
(
|
|
24
|
+
quickfix_cursor_code_actions(diagnostics_under_cursor) +
|
|
25
|
+
quickfix_all_of_type_code_actions(diagnostics_under_cursor, correctable_diagnostics) +
|
|
26
|
+
quickfix_all_code_action(correctable_diagnostics)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def quickfix_cursor_code_actions(diagnostics)
|
|
33
|
+
diagnostics.map do |diagnostic|
|
|
34
|
+
{
|
|
35
|
+
title: "Fix this #{diagnostic.code} problem: #{diagnostic.message}",
|
|
36
|
+
kind: kind,
|
|
37
|
+
diagnostics: [diagnostic.to_h],
|
|
38
|
+
isPreferred: true,
|
|
39
|
+
command: {
|
|
40
|
+
title: 'quickfix',
|
|
41
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
42
|
+
arguments: [diagnostic.to_h],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def quickfix_all_of_type_code_actions(cursor_diagnostics, correctable_diagnostics)
|
|
49
|
+
codes = Set.new(cursor_diagnostics.map(&:code))
|
|
50
|
+
correctable_diagnostics_by_code = correctable_diagnostics.group_by(&:code)
|
|
51
|
+
codes.flat_map do |code|
|
|
52
|
+
diagnostics = correctable_diagnostics_by_code[code].map(&:to_h)
|
|
53
|
+
return [] unless diagnostics.size > 1
|
|
54
|
+
{
|
|
55
|
+
title: "Fix all #{code} problems",
|
|
56
|
+
kind: kind,
|
|
57
|
+
diagnostics: diagnostics,
|
|
58
|
+
command: {
|
|
59
|
+
title: 'quickfix',
|
|
60
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
61
|
+
arguments: diagnostics,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def quickfix_all_code_action(diagnostics)
|
|
68
|
+
return [] unless diagnostics.size > 1
|
|
69
|
+
diagnostics = diagnostics.map(&:to_h)
|
|
70
|
+
[{
|
|
71
|
+
title: "Fix all auto-fixable problems",
|
|
72
|
+
kind: kind,
|
|
73
|
+
diagnostics: diagnostics,
|
|
74
|
+
command: {
|
|
75
|
+
title: 'quickfix',
|
|
76
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
77
|
+
arguments: diagnostics,
|
|
78
|
+
},
|
|
79
|
+
}]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class SourceFixAllCodeActionProvider < CodeActionProvider
|
|
6
|
+
kind "source.fixAll"
|
|
7
|
+
|
|
8
|
+
def code_actions(relative_path, _)
|
|
9
|
+
diagnostics = diagnostics_manager
|
|
10
|
+
.diagnostics(relative_path)
|
|
11
|
+
.filter(&:correctable?)
|
|
12
|
+
.reject do |diagnostic|
|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
|
16
|
+
end
|
|
17
|
+
.map(&:to_h)
|
|
18
|
+
diagnostics_to_code_action(diagnostics)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def diagnostics_to_code_action(diagnostics)
|
|
24
|
+
return [] if diagnostics.empty?
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
title: "Fix all Theme Check auto-fixable problems",
|
|
28
|
+
kind: kind,
|
|
29
|
+
diagnostics: diagnostics,
|
|
30
|
+
command: {
|
|
31
|
+
title: 'fixAll.file',
|
|
32
|
+
command: LanguageServer::CorrectionExecuteCommandProvider.command,
|
|
33
|
+
arguments: diagnostics,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -6,7 +6,9 @@ module ThemeCheck
|
|
|
6
6
|
def completions(content, cursor)
|
|
7
7
|
return [] unless can_complete?(content, cursor)
|
|
8
8
|
partial = first_word(content) || ''
|
|
9
|
-
ShopifyLiquid::Tag.labels
|
|
9
|
+
labels = ShopifyLiquid::Tag.labels
|
|
10
|
+
labels += ShopifyLiquid::Tag.end_labels
|
|
11
|
+
labels
|
|
10
12
|
.select { |w| w.start_with?(partial) }
|
|
11
13
|
.map { |tag| tag_to_completion(tag) }
|
|
12
14
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class Configuration
|
|
6
|
+
CHECK_ON_OPEN = :"themeCheck.checkOnOpen"
|
|
7
|
+
CHECK_ON_SAVE = :"themeCheck.checkOnSave"
|
|
8
|
+
CHECK_ON_CHANGE = :"themeCheck.checkOnChange"
|
|
9
|
+
|
|
10
|
+
def initialize(bridge, capabilities)
|
|
11
|
+
@bridge = bridge
|
|
12
|
+
@capabilities = capabilities
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@initialized = false
|
|
15
|
+
@config = {
|
|
16
|
+
CHECK_ON_OPEN => @capabilities.initialization_option(CHECK_ON_OPEN) || true,
|
|
17
|
+
CHECK_ON_SAVE => @capabilities.initialization_option(CHECK_ON_SAVE) || true,
|
|
18
|
+
CHECK_ON_CHANGE => @capabilities.initialization_option(CHECK_ON_CHANGE) || true,
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(force: nil)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
return unless @capabilities.supports_workspace_configuration?
|
|
25
|
+
return if initialized? && !force
|
|
26
|
+
check_on_open, check_on_save, check_on_change = @bridge.send_request(
|
|
27
|
+
"workspace/configuration",
|
|
28
|
+
items: [
|
|
29
|
+
{ section: CHECK_ON_OPEN },
|
|
30
|
+
{ section: CHECK_ON_SAVE },
|
|
31
|
+
{ section: CHECK_ON_CHANGE },
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
@config[CHECK_ON_OPEN] = check_on_open unless check_on_open.nil?
|
|
35
|
+
@config[CHECK_ON_CHANGE] = check_on_change unless check_on_change.nil?
|
|
36
|
+
@config[CHECK_ON_SAVE] = check_on_save unless check_on_save.nil?
|
|
37
|
+
@initialized = true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_did_change_capability
|
|
42
|
+
return unless @capabilities.supports_workspace_did_change_configuration_dynamic_registration?
|
|
43
|
+
@bridge.send_request('client/registerCapability', registrations: [{
|
|
44
|
+
id: "workspace/didChangeConfiguration",
|
|
45
|
+
method: "workspace/didChangeConfiguration",
|
|
46
|
+
}])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialized?
|
|
50
|
+
@initialized
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_on_open?
|
|
54
|
+
fetch # making sure we have an initialized value
|
|
55
|
+
@config[CHECK_ON_OPEN]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_on_save?
|
|
59
|
+
fetch # making sure we have for an initialized value
|
|
60
|
+
@config[CHECK_ON_SAVE]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_on_change?
|
|
64
|
+
fetch # making sure we have for an initialized value
|
|
65
|
+
@config[CHECK_ON_CHANGE]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class Diagnostic
|
|
6
|
+
include URIHelper
|
|
7
|
+
|
|
8
|
+
attr_reader :offense
|
|
9
|
+
|
|
10
|
+
def initialize(offense)
|
|
11
|
+
@offense = offense
|
|
12
|
+
@diagnostic = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ==(other)
|
|
16
|
+
case other
|
|
17
|
+
when Hash, Diagnostic
|
|
18
|
+
to_h == other.to_h
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
return @diagnostic unless @diagnostic.nil?
|
|
26
|
+
@diagnostic = {
|
|
27
|
+
source: "theme-check",
|
|
28
|
+
code: code,
|
|
29
|
+
message: message,
|
|
30
|
+
range: range,
|
|
31
|
+
severity: severity,
|
|
32
|
+
data: data,
|
|
33
|
+
}
|
|
34
|
+
@diagnostic[:codeDescription] = code_description unless offense.doc.nil?
|
|
35
|
+
@diagnostic
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s
|
|
39
|
+
to_h.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def single_file?
|
|
43
|
+
offense.single_file?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def correctable?
|
|
47
|
+
offense.correctable?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def code
|
|
51
|
+
offense.code_name
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def message
|
|
55
|
+
offense.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def code_description
|
|
59
|
+
{
|
|
60
|
+
href: offense.doc,
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def severity
|
|
65
|
+
case offense.severity
|
|
66
|
+
when :error
|
|
67
|
+
1
|
|
68
|
+
when :suggestion
|
|
69
|
+
2
|
|
70
|
+
when :style
|
|
71
|
+
3
|
|
72
|
+
else
|
|
73
|
+
4
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def range
|
|
78
|
+
{
|
|
79
|
+
start: {
|
|
80
|
+
line: offense.start_row,
|
|
81
|
+
character: offense.start_column,
|
|
82
|
+
},
|
|
83
|
+
end: {
|
|
84
|
+
line: offense.end_row,
|
|
85
|
+
character: offense.end_column,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def start_index
|
|
91
|
+
offense.start_index
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def end_index
|
|
95
|
+
offense.end_index
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def absolute_path
|
|
99
|
+
@absolute_path ||= offense&.theme_file&.path
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def relative_path
|
|
103
|
+
@relative_path ||= offense&.theme_file&.relative_path
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def uri
|
|
107
|
+
@uri ||= absolute_path && file_uri(absolute_path)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def file_version
|
|
111
|
+
@version ||= offense&.version
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def data
|
|
115
|
+
{
|
|
116
|
+
absolute_path: absolute_path.to_s,
|
|
117
|
+
relative_path: relative_path.to_s,
|
|
118
|
+
uri: uri,
|
|
119
|
+
version: file_version,
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class DiagnosticsEngine
|
|
6
|
+
include URIHelper
|
|
7
|
+
|
|
8
|
+
attr_reader :storage
|
|
9
|
+
|
|
10
|
+
def initialize(storage, bridge, diagnostics_manager = DiagnosticsManager.new)
|
|
11
|
+
@diagnostics_lock = Mutex.new
|
|
12
|
+
@diagnostics_manager = diagnostics_manager
|
|
13
|
+
@storage = storage
|
|
14
|
+
@bridge = bridge
|
|
15
|
+
@token = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def first_run?
|
|
19
|
+
@diagnostics_manager.first_run?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def analyze_and_send_offenses(absolute_path, config, force: false)
|
|
23
|
+
return unless @diagnostics_lock.try_lock
|
|
24
|
+
@token += 1
|
|
25
|
+
@bridge.send_create_work_done_progress_request(@token)
|
|
26
|
+
theme = ThemeCheck::Theme.new(storage)
|
|
27
|
+
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
|
28
|
+
|
|
29
|
+
if @diagnostics_manager.first_run? || force
|
|
30
|
+
@bridge.send_work_done_progress_begin(@token, "Full theme check")
|
|
31
|
+
@bridge.log("Checking #{storage.root}")
|
|
32
|
+
offenses = nil
|
|
33
|
+
time = Benchmark.measure do
|
|
34
|
+
offenses = analyzer.analyze_theme do |path, i, total|
|
|
35
|
+
@bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
|
|
39
|
+
@bridge.send_work_done_progress_end(@token, end_message)
|
|
40
|
+
send_diagnostics(offenses)
|
|
41
|
+
else
|
|
42
|
+
# Analyze selected files
|
|
43
|
+
relative_path = Pathname.new(storage.relative_path(absolute_path))
|
|
44
|
+
file = theme[relative_path]
|
|
45
|
+
# Skip if not a theme file
|
|
46
|
+
if file
|
|
47
|
+
@bridge.send_work_done_progress_begin(@token, "Partial theme check")
|
|
48
|
+
offenses = nil
|
|
49
|
+
time = Benchmark.measure do
|
|
50
|
+
offenses = analyzer.analyze_files([file]) do |path, i, total|
|
|
51
|
+
@bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
|
55
|
+
@bridge.send_work_done_progress_end(@token, end_message)
|
|
56
|
+
@bridge.log(end_message)
|
|
57
|
+
send_diagnostics(offenses, [relative_path])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
@diagnostics_lock.unlock
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def send_diagnostics(offenses, analyzed_files = nil)
|
|
66
|
+
@diagnostics_manager.build_diagnostics(offenses, analyzed_files: analyzed_files).each do |relative_path, diagnostics|
|
|
67
|
+
send_diagnostic(relative_path, diagnostics)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def send_diagnostic(relative_path, diagnostics)
|
|
72
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
|
73
|
+
@bridge.send_notification('textDocument/publishDiagnostics', {
|
|
74
|
+
uri: file_uri(storage.path(relative_path)),
|
|
75
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
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
|