standard 0.1.0 → 1.51.1

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/test.yml +24 -0
  4. data/.github/workflows/update.yml +54 -0
  5. data/.gitignore +3 -0
  6. data/.standard.yml +1 -1
  7. data/CHANGELOG.md +703 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +71 -37
  10. data/LICENSE.txt +3 -4
  11. data/README.md +459 -264
  12. data/Rakefile +5 -4
  13. data/bin/console +0 -4
  14. data/bin/rake +27 -0
  15. data/bin/run +9 -0
  16. data/config/base.yml +1065 -193
  17. data/config/default.yml +8 -0
  18. data/config/ruby-1.8.yml +10 -2
  19. data/config/ruby-1.9.yml +11 -1
  20. data/config/ruby-2.0.yml +4 -0
  21. data/config/ruby-2.1.yml +4 -0
  22. data/config/ruby-2.2.yml +13 -5
  23. data/config/ruby-2.3.yml +10 -0
  24. data/config/ruby-2.4.yml +10 -0
  25. data/config/ruby-2.5.yml +10 -0
  26. data/config/ruby-2.6.yml +13 -0
  27. data/config/ruby-2.7.yml +10 -0
  28. data/config/ruby-3.0.yml +13 -0
  29. data/config/ruby-3.1.yml +11 -0
  30. data/config/ruby-3.2.yml +4 -0
  31. data/config/ruby-3.3.yml +7 -0
  32. data/docs/ARCHITECTURE.md +33 -0
  33. data/docs/RELEASE.md +41 -0
  34. data/docs/RUBY_VERSIONS.md +51 -0
  35. data/docs/UPGRADING.md +31 -0
  36. data/lib/ruby_lsp/standard/addon.rb +58 -0
  37. data/lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb +44 -0
  38. data/lib/standard/base/plugin.rb +69 -0
  39. data/lib/standard/base.rb +8 -0
  40. data/lib/standard/builds_config.rb +11 -1
  41. data/lib/standard/cli.rb +1 -7
  42. data/lib/standard/creates_config_store/assigns_rubocop_yaml.rb +2 -18
  43. data/lib/standard/creates_config_store/configures_ignored_paths.rb +2 -2
  44. data/lib/standard/creates_config_store/merges_user_config_extensions.rb +37 -0
  45. data/lib/standard/creates_config_store/sets_target_ruby_version.rb +21 -8
  46. data/lib/standard/creates_config_store.rb +5 -0
  47. data/lib/standard/formatter.rb +92 -37
  48. data/lib/standard/loads_runner.rb +17 -3
  49. data/lib/standard/loads_yaml_config.rb +18 -11
  50. data/lib/standard/lsp/diagnostic.rb +174 -0
  51. data/lib/standard/lsp/kills_server.rb +10 -0
  52. data/lib/standard/lsp/logger.rb +21 -0
  53. data/lib/standard/lsp/routes.rb +175 -0
  54. data/lib/standard/lsp/server.rb +37 -0
  55. data/lib/standard/lsp/standardizer.rb +34 -0
  56. data/lib/standard/lsp/stdin_rubocop_runner.rb +71 -0
  57. data/lib/standard/merges_settings.rb +22 -11
  58. data/lib/standard/plugin/combines_plugin_configs.rb +15 -0
  59. data/lib/standard/plugin/creates_runner_context.rb +15 -0
  60. data/lib/standard/plugin/determines_class_constant.rb +56 -0
  61. data/lib/standard/plugin/initializes_plugins.rb +23 -0
  62. data/lib/standard/plugin/merges_plugins_into_rubocop_config.rb +177 -0
  63. data/lib/standard/plugin/standardizes_configured_plugins.rb +37 -0
  64. data/lib/standard/plugin.rb +11 -0
  65. data/lib/standard/railtie.rb +1 -1
  66. data/lib/standard/rake.rb +8 -1
  67. data/lib/standard/{parses_cli_option.rb → resolves_yaml_option.rb} +9 -2
  68. data/lib/standard/rubocop/ext.rb +17 -0
  69. data/lib/standard/runners/genignore.rb +44 -0
  70. data/lib/standard/runners/help.rb +9 -5
  71. data/lib/standard/runners/lsp.rb +11 -0
  72. data/lib/standard/runners/rubocop.rb +14 -18
  73. data/lib/standard/runners/verbose_version.rb +14 -0
  74. data/lib/standard/version.rb +1 -1
  75. data/lib/standard.rb +6 -4
  76. data/standard.gemspec +22 -20
  77. metadata +72 -73
  78. data/.circleci/config.yml +0 -35
  79. data/lib/standard/cop/semantic_blocks.rb +0 -162
  80. data/lib/standard/detects_fixability.rb +0 -20
@@ -1,23 +1,36 @@
1
1
  class Standard::CreatesConfigStore
2
2
  class SetsTargetRubyVersion
3
+ # This is minimum version that Rubocop can parse, not the minimum
4
+ # version it can run on (e.g. TargetRubyVersion). See the following
5
+ # for more details:
6
+ #
7
+ # https://docs.rubocop.org/rubocop/configuration.html#setting-the-target-ruby-version
8
+ #
9
+ # https://github.com/rubocop/rubocop/blob/master/lib/rubocop/target_ruby.rb
10
+ MIN_TARGET_RUBY_VERSION = "2.0"
11
+
3
12
  def call(options_config, standard_config)
4
- options_config["AllCops"]["TargetRubyVersion"] = floatify_version(
5
- max_rubocop_supported_version(standard_config[:ruby_version])
13
+ options_config["AllCops"]["TargetRubyVersion"] = normalize_version(
14
+ min_target_ruby_version_supported(standard_config[:ruby_version])
6
15
  )
7
16
  end
8
17
 
9
18
  private
10
19
 
11
- def max_rubocop_supported_version(desired_version)
12
- rubocop_supported_version = Gem::Version.new("2.3")
13
- if desired_version < rubocop_supported_version
14
- rubocop_supported_version
20
+ def min_target_ruby_version_supported(desired_target_ruby_version)
21
+ return desired_target_ruby_version unless Gem::Version.correct?(desired_target_ruby_version)
22
+
23
+ min_target_ruby_version = Gem::Version.new(MIN_TARGET_RUBY_VERSION)
24
+ if desired_target_ruby_version < min_target_ruby_version
25
+ min_target_ruby_version
15
26
  else
16
- desired_version
27
+ desired_target_ruby_version
17
28
  end
18
29
  end
19
30
 
20
- def floatify_version(version)
31
+ def normalize_version(version)
32
+ return version unless Gem::Version.correct?(version)
33
+
21
34
  major, minor = version.segments
22
35
  "#{major}.#{minor}".to_f # lol
23
36
  end
@@ -3,6 +3,7 @@ require "rubocop"
3
3
  require_relative "creates_config_store/assigns_rubocop_yaml"
4
4
  require_relative "creates_config_store/sets_target_ruby_version"
5
5
  require_relative "creates_config_store/configures_ignored_paths"
6
+ require_relative "creates_config_store/merges_user_config_extensions"
6
7
 
7
8
  module Standard
8
9
  class CreatesConfigStore
@@ -10,12 +11,16 @@ module Standard
10
11
  @assigns_rubocop_yaml = AssignsRubocopYaml.new
11
12
  @sets_target_ruby_version = SetsTargetRubyVersion.new
12
13
  @configures_ignored_paths = ConfiguresIgnoredPaths.new
14
+ @combines_plugin_configs = Plugin::CombinesPluginConfigs.new
15
+ @merges_user_config_extensions = MergesUserConfigExtensions.new
13
16
  end
14
17
 
15
18
  def call(standard_config)
16
19
  RuboCop::ConfigStore.new.tap do |config_store|
17
20
  options_config = @assigns_rubocop_yaml.call(config_store, standard_config)
18
21
  @sets_target_ruby_version.call(options_config, standard_config)
22
+ @combines_plugin_configs.call(options_config, standard_config)
23
+ @merges_user_config_extensions.call(options_config, standard_config)
19
24
  @configures_ignored_paths.call(options_config, standard_config)
20
25
  end
21
26
  end
@@ -1,35 +1,42 @@
1
+ require "yaml"
1
2
  require "rubocop"
2
- require_relative "detects_fixability"
3
3
 
4
4
  module Standard
5
5
  class Formatter < RuboCop::Formatter::BaseFormatter
6
- CALL_TO_ACTION_MESSAGE = <<-CALL_TO_ACTION.gsub(/^ {6}/, "")
7
- Notice: Disagree with these rules? While StandardRB is pre-1.0.0, feel free to submit suggestions to:
8
- https://github.com/testdouble/standard/issues/new
9
- CALL_TO_ACTION
10
-
11
- def initialize(*args)
12
- super
13
- @detects_fixability = DetectsFixability.new
6
+ STANDARD_GREETING = <<~MSG
7
+ standard: Use Ruby Standard Style (https://github.com/standardrb/standard)
8
+ MSG
9
+
10
+ def started(_target_files)
14
11
  @header_printed_already = false
15
- @fix_suggestion_printed_already = false
16
- @any_uncorrected_offenses = false
12
+ @total_correction_count = 0
13
+ @total_correctable_count = 0
14
+ @total_uncorrected_count = 0
15
+ print_todo_warning
17
16
  end
18
17
 
19
18
  def file_finished(file, offenses)
20
- return unless (uncorrected_offenses = offenses.reject(&:corrected?)).any?
21
- @any_uncorrected_offenses = true
19
+ track_stats(offenses)
22
20
 
23
- print_header_once
24
- print_fix_suggestion_once(uncorrected_offenses)
21
+ if (uncorrected_offenses = offenses.reject(&:corrected?)).any?
22
+ print_header_once
25
23
 
26
- uncorrected_offenses.each do |o|
27
- output.printf(" %s:%d:%d: %s\n", path_to(file), o.line, o.real_column, o.message.tr("\n", " "))
24
+ uncorrected_offenses.each do |o|
25
+ output.printf(" %s:%d:%d: %s\n", path_to(file), o.line, o.real_column, o.message.tr("\n", " "))
26
+ end
28
27
  end
29
28
  end
30
29
 
31
- def finished(_)
32
- print_call_for_feedback if @any_uncorrected_offenses
30
+ def track_stats(offenses)
31
+ corrected = offenses.count(&:corrected?)
32
+ @total_correction_count += corrected
33
+ @total_correctable_count += offenses.count(&:correctable?) - corrected
34
+ @total_uncorrected_count += offenses.count - corrected
35
+ end
36
+
37
+ def finished(_inspected_files)
38
+ print_fix_suggestion
39
+ print_todo_congratulations
33
40
  end
34
41
 
35
42
  private
@@ -37,25 +44,48 @@ module Standard
37
44
  def print_header_once
38
45
  return if @header_printed_already
39
46
 
40
- output.print <<-HEADER.gsub(/^ {8}/, "")
41
- standard: Use Ruby Standard Style (https://github.com/testdouble/standard)
42
- HEADER
47
+ output.print STANDARD_GREETING
43
48
 
44
49
  @header_printed_already = true
45
50
  end
46
51
 
47
- def print_fix_suggestion_once(offenses)
48
- if !@fix_suggestion_printed_already && should_suggest_fix?(offenses)
49
- command = if File.split($PROGRAM_NAME).last == "rake"
50
- "rake standard:fix"
52
+ def print_fix_suggestion
53
+ if (fix_mode = potential_fix_mode)
54
+ run_mode = determine_run_mode
55
+
56
+ command = if run_mode == :rake
57
+ "rake standard:#{fix_mode}"
51
58
  else
52
- "standardrb --fix"
59
+ "standardrb --#{fix_mode.to_s.tr("_", "-")}"
53
60
  end
54
61
 
55
- output.print <<-HEADER.gsub(/^ {10}/, "")
56
- standard: Run `#{command}` to automatically fix some problems.
62
+ output.print fixable_error_message(command)
63
+ end
64
+ end
65
+
66
+ def print_todo_warning
67
+ todo_file = options[:todo_file]
68
+ return unless todo_file
69
+
70
+ todo_ignore_files = options[:todo_ignore_files]
71
+ return unless todo_ignore_files&.any?
72
+
73
+ output.print <<~HEADER
74
+ WARNING: this project is being migrated to standard gradually via `#{todo_file}` and is ignoring these files:
75
+ HEADER
76
+
77
+ todo_ignore_files.each do |f|
78
+ output.printf(" %s\n", f)
79
+ end
80
+ end
81
+
82
+ def print_todo_congratulations
83
+ if @total_uncorrected_count == 0 &&
84
+ options[:todo_file] &&
85
+ options[:todo_ignore_files]&.none?
86
+ output.print <<~HEADER
87
+ Congratulations, you've successfully migrated this project to Standard! Delete `#{options[:todo_file]}` in celebration.
57
88
  HEADER
58
- @fix_suggestion_printed_already = true
59
89
  end
60
90
  end
61
91
 
@@ -63,17 +93,42 @@ module Standard
63
93
  Pathname.new(file).relative_path_from(Pathname.new(Dir.pwd))
64
94
  end
65
95
 
66
- def print_call_for_feedback
67
- output.print "\n"
68
- output.print CALL_TO_ACTION_MESSAGE
96
+ def potential_fix_mode
97
+ return nil unless @total_correctable_count > 0
98
+
99
+ if !options[:autocorrect]
100
+ :fix
101
+ elsif options[:autocorrect] && options[:safe_autocorrect]
102
+ :fix_unsafely
103
+ end
69
104
  end
70
105
 
71
- def auto_correct_option_provided?
72
- options[:auto_correct] || options[:safe_auto_correct]
106
+ def determine_run_mode
107
+ if File.split($PROGRAM_NAME).last == "rake"
108
+ :rake
109
+ else
110
+ :cli
111
+ end
73
112
  end
74
113
 
75
- def should_suggest_fix?(offenses)
76
- !auto_correct_option_provided? && @detects_fixability.call(offenses)
114
+ def fixable_error_message(command)
115
+ sales_pitch = if !options[:autocorrect]
116
+ if @total_correctable_count > 1
117
+ "fix up to #{@total_correctable_count} problems"
118
+ else
119
+ "potentially fix one problem"
120
+ end
121
+ elsif options[:autocorrect] && options[:safe_autocorrect]
122
+ if @total_correctable_count > 1
123
+ "DANGEROUSLY fix #{@total_correctable_count} problems"
124
+ else
125
+ "DANGEROUSLY fix one problem"
126
+ end
127
+ end
128
+
129
+ <<~MSG
130
+ standard: Run `#{command}` to #{sales_pitch}.
131
+ MSG
77
132
  end
78
133
  end
79
134
  end
@@ -1,9 +1,23 @@
1
+ require_relative "runners/rubocop"
2
+ require_relative "runners/version"
3
+ require_relative "runners/verbose_version"
4
+ require_relative "runners/lsp"
5
+ require_relative "runners/genignore"
6
+ require_relative "runners/help"
7
+
1
8
  module Standard
2
9
  class LoadsRunner
3
- # Warning: clever metaprogramming. 99% of the time this is Runners::Rubocop
10
+ RUNNERS = {
11
+ rubocop: ::Standard::Runners::Rubocop,
12
+ version: ::Standard::Runners::Version,
13
+ verbose_version: ::Standard::Runners::VerboseVersion,
14
+ lsp: ::Standard::Runners::Lsp,
15
+ genignore: ::Standard::Runners::Genignore,
16
+ help: ::Standard::Runners::Help
17
+ }.freeze
18
+
4
19
  def call(command)
5
- require_relative "runners/#{command}"
6
- ::Standard::Runners.const_get(command.to_s.capitalize).new
20
+ RUNNERS[command].new
7
21
  end
8
22
  end
9
23
  end
@@ -1,18 +1,15 @@
1
1
  require "yaml"
2
2
  require "pathname"
3
3
  require_relative "file_finder"
4
- require_relative "parses_cli_option"
4
+ require_relative "resolves_yaml_option"
5
5
 
6
6
  module Standard
7
7
  class LoadsYamlConfig
8
- def initialize
9
- @parses_cli_option = ParsesCliOption.new
10
- end
8
+ def call(standard_yaml_path, todo_yaml_path)
9
+ standard_yaml = load_standard_yaml(standard_yaml_path)
10
+ todo_yaml = load_standard_yaml(todo_yaml_path)
11
11
 
12
- def call(argv, search_path)
13
- yaml_path = @parses_cli_option.call(argv, "--config") ||
14
- FileFinder.new.call(".standard.yml", search_path)
15
- construct_config(yaml_path, load_standard_yaml(yaml_path))
12
+ construct_config(standard_yaml_path, standard_yaml, todo_yaml_path, todo_yaml)
16
13
  end
17
14
 
18
15
  private
@@ -25,18 +22,28 @@ module Standard
25
22
  end
26
23
  end
27
24
 
28
- def construct_config(yaml_path, standard_yaml)
25
+ def construct_config(yaml_path, standard_yaml, todo_path, todo_yaml)
29
26
  {
30
- ruby_version: Gem::Version.new((standard_yaml["ruby_version"] || RUBY_VERSION)),
27
+ ruby_version: normalized_ruby_version(standard_yaml["ruby_version"]),
31
28
  fix: !!standard_yaml["fix"],
32
29
  format: standard_yaml["format"],
33
30
  parallel: !!standard_yaml["parallel"],
34
- ignore: expand_ignore_config(standard_yaml["ignore"]),
31
+ ignore: expand_ignore_config(standard_yaml["ignore"]) + expand_ignore_config(todo_yaml["ignore"]),
35
32
  default_ignores: standard_yaml.key?("default_ignores") ? !!standard_yaml["default_ignores"] : true,
36
33
  config_root: yaml_path ? Pathname.new(yaml_path).dirname.to_s : nil,
34
+ todo_file: todo_path,
35
+ todo_ignore_files: Array(todo_yaml["ignore"]).map { |f| (Hash === f) ? f.keys.first : f },
36
+ plugins: Array(standard_yaml["plugins"]),
37
+ extend_config: Array(standard_yaml["extend_config"])
37
38
  }
38
39
  end
39
40
 
41
+ def normalized_ruby_version(version)
42
+ return version if version && !Gem::Version.correct?(version)
43
+
44
+ Gem::Version.new(version || RUBY_VERSION)
45
+ end
46
+
40
47
  def expand_ignore_config(ignore_config)
41
48
  arrayify(ignore_config).map { |rule|
42
49
  if rule.is_a?(String)
@@ -0,0 +1,174 @@
1
+ module Standard
2
+ module Lsp
3
+ class Diagnostic
4
+ Constant = LanguageServer::Protocol::Constant
5
+ Interface = LanguageServer::Protocol::Interface
6
+
7
+ RUBOCOP_TO_LSP_SEVERITY = {
8
+ info: Constant::DiagnosticSeverity::HINT,
9
+ refactor: Constant::DiagnosticSeverity::INFORMATION,
10
+ convention: Constant::DiagnosticSeverity::INFORMATION,
11
+ warning: Constant::DiagnosticSeverity::WARNING,
12
+ error: Constant::DiagnosticSeverity::ERROR,
13
+ fatal: Constant::DiagnosticSeverity::ERROR
14
+ }.freeze
15
+
16
+ def initialize(document_encoding, offense, uri, cop_class)
17
+ @document_encoding = document_encoding
18
+ @offense = offense
19
+ @uri = uri
20
+ @cop_class = cop_class
21
+ end
22
+
23
+ def to_lsp_code_actions
24
+ code_actions = []
25
+
26
+ code_actions << autocorrect_action if correctable?
27
+ code_actions << disable_line_action
28
+
29
+ code_actions
30
+ end
31
+
32
+ def to_lsp_diagnostic(config)
33
+ highlighted = @offense.highlighted_area
34
+ Interface::Diagnostic.new(
35
+ message: message,
36
+ source: "Standard Ruby",
37
+ code: @offense.cop_name,
38
+ code_description: code_description(config),
39
+ severity: severity,
40
+ range: Interface::Range.new(
41
+ start: Interface::Position.new(
42
+ line: @offense.line - 1,
43
+ character: highlighted.begin_pos
44
+ ),
45
+ end: Interface::Position.new(
46
+ line: @offense.line - 1,
47
+ character: highlighted.end_pos
48
+ )
49
+ ),
50
+ data: {
51
+ correctable: correctable?,
52
+ code_actions: to_lsp_code_actions
53
+ }
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def message
60
+ message = @offense.message
61
+ message += "\n\nThis offense is not auto-correctable.\n" unless correctable?
62
+ message
63
+ end
64
+
65
+ def severity
66
+ RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name]
67
+ end
68
+
69
+ def code_description(config)
70
+ return unless @cop_class
71
+
72
+ if (doc_url = @cop_class.documentation_url(config))
73
+ Interface::CodeDescription.new(href: doc_url)
74
+ end
75
+ end
76
+
77
+ def autocorrect_action
78
+ Interface::CodeAction.new(
79
+ title: "Autocorrect #{@offense.cop_name}",
80
+ kind: Constant::CodeActionKind::QUICK_FIX,
81
+ edit: Interface::WorkspaceEdit.new(
82
+ document_changes: [
83
+ Interface::TextDocumentEdit.new(
84
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
85
+ uri: ensure_uri_scheme(@uri.to_s).to_s,
86
+ version: nil
87
+ ),
88
+ edits: correctable? ? offense_replacements : []
89
+ )
90
+ ]
91
+ ),
92
+ is_preferred: true
93
+ )
94
+ end
95
+
96
+ def offense_replacements
97
+ @offense.corrector.as_replacements.map do |range, replacement|
98
+ Interface::TextEdit.new(
99
+ range: Interface::Range.new(
100
+ start: Interface::Position.new(line: range.line - 1, character: range.column),
101
+ end: Interface::Position.new(line: range.last_line - 1, character: range.last_column)
102
+ ),
103
+ new_text: replacement
104
+ )
105
+ end
106
+ end
107
+
108
+ def disable_line_action
109
+ Interface::CodeAction.new(
110
+ title: "Disable #{@offense.cop_name} for this line",
111
+ kind: Constant::CodeActionKind::QUICK_FIX,
112
+ edit: Interface::WorkspaceEdit.new(
113
+ document_changes: [
114
+ Interface::TextDocumentEdit.new(
115
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
116
+ uri: ensure_uri_scheme(@uri.to_s).to_s,
117
+ version: nil
118
+ ),
119
+ edits: line_disable_comment
120
+ )
121
+ ]
122
+ )
123
+ )
124
+ end
125
+
126
+ def line_disable_comment
127
+ new_text = if @offense.source_line.include?(" # standard:disable ")
128
+ ",#{@offense.cop_name}"
129
+ else
130
+ " # standard:disable #{@offense.cop_name}"
131
+ end
132
+
133
+ eol = Interface::Position.new(
134
+ line: @offense.line - 1,
135
+ character: length_of_line(@offense.source_line)
136
+ )
137
+
138
+ # TODO: fails for multiline strings - may be preferable to use block
139
+ # comments to disable some offenses
140
+ inline_comment = Interface::TextEdit.new(
141
+ range: Interface::Range.new(start: eol, end: eol),
142
+ new_text: new_text
143
+ )
144
+
145
+ [inline_comment]
146
+ end
147
+
148
+ def length_of_line(line)
149
+ if @document_encoding == Encoding::UTF_16LE
150
+ line_length = 0
151
+ line.codepoints.each do |codepoint|
152
+ line_length += 1
153
+ if codepoint > RubyLsp::Document::Scanner::SURROGATE_PAIR_START
154
+ line_length += 1
155
+ end
156
+ end
157
+ line_length
158
+ else
159
+ line.length
160
+ end
161
+ end
162
+
163
+ def correctable?
164
+ !@offense.corrector.nil?
165
+ end
166
+
167
+ def ensure_uri_scheme(uri)
168
+ uri = URI.parse(uri)
169
+ uri.scheme = "file" if uri.scheme.nil?
170
+ uri
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,10 @@
1
+ module Standard
2
+ module Lsp
3
+ class KillsServer
4
+ def call(&blk)
5
+ at_exit(&blk) unless blk.nil?
6
+ exit 0
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ module Standard
2
+ module Lsp
3
+ class Logger
4
+ def initialize(prefix: "[server]")
5
+ @prefix = prefix
6
+ @puts_onces = []
7
+ end
8
+
9
+ def puts(message)
10
+ warn [@prefix, message].compact.join(" ")
11
+ end
12
+
13
+ def puts_once(message)
14
+ return if @puts_onces.include?(message)
15
+
16
+ @puts_onces << message
17
+ puts(message)
18
+ end
19
+ end
20
+ end
21
+ end