spellr 0.9.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 901a89f14415b3993f210ac2f0f94cb82efdbfc55565f8e1c9fd51347790b3d6
4
- data.tar.gz: 113d25fcdfc5d70c9e50f4a061092728c1349452e88e0dd0d6268d10851c47b9
3
+ metadata.gz: 80439d65c02b1e49b28e0def2c52f45b103b7c1282a95940473dc37e90d4f3a6
4
+ data.tar.gz: 9028a62ab2ff949810ff64801ffa42752360bd2e0b45ad507975d9c9d839ef68
5
5
  SHA512:
6
- metadata.gz: adcfec5ea17125517f445fb7e895b5ee049d1540f7626ab5c55d835d600b149d844b9a052f2324784038f5098a392a8f51b16d01c56ace9cffa773b34e27bbca
7
- data.tar.gz: 922c16526b3e0dd8600ca5ef836de2419dda0237e3b3c2d8a2674d1ca7b0f0dcebf1fe0d0deded671522ee526c7e185a338ef98e762c4385e00b25f8e3ae94e8
6
+ metadata.gz: 5592e135615d089882db57f881a1d3745d06880611cfe181d36a21f4b718b5e91e799d7afe9f045e81629eb222db07046d072b0a88424d7bcc7526ba99f5fd44
7
+ data.tar.gz: aca7014f20d2638ee676499621765ab00f7e862d7be81d446685049ea350c42f25609bac9e998e5f2f4f2399372191c30c46ddd0f192293cafe45f0a6f7c65ff
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v0.10.0
2
+ - Drop ruby 2.4 support, to allow for...
3
+ - Spelling suggestions while using `spellr --interactive`
4
+ - And a new, probably frequently wrong, `spellr --autocorrect`
5
+
1
6
  # v0.9.1
2
7
  - Assume all files are utf8, more comprehensively. (Sets ::Encoding.default_external and default_internal while running)
3
8
 
data/README.md CHANGED
@@ -27,7 +27,7 @@ However, in a programming context spelling things _consistently_ is useful, wher
27
27
 
28
28
  ## Installation
29
29
 
30
- This is tested against ruby 2.4-3.0.
30
+ This is tested against ruby 2.5-3.0.
31
31
 
32
32
  ### With Bundler
33
33
 
@@ -66,6 +66,7 @@ $ spellr # will run the spell checker
66
66
  $ spellr --interactive # will run the spell checker, interactively
67
67
  $ spellr --wordlist # will output all words that fail the spell checker in spellr wordlist format
68
68
  $ spellr --quiet # will suppress all output
69
+ $ spellr --autocorrect # for if you're feeling lucky
69
70
  ```
70
71
 
71
72
  To check a single file or subset of files, just add paths or globs:
@@ -123,14 +124,16 @@ To start an interactive spell checking session:
123
124
  $ spellr --interactive
124
125
  ```
125
126
 
126
- You'll be shown each word that's not found in a dictionary, it's location (path:line:column), along with a prompt.
127
+ You'll be shown each word that's not found in a dictionary, it's location (path:line:column), along with suggestions, and a prompt.
127
128
  ```
128
129
  file.rb:1:0 notaword
130
+ Did you mean: [1] notwork, [2] nonword
129
131
  [a]dd, [r]eplace, [s]kip, [h]elp, [^C] to exit: [ ]
130
132
  ```
131
133
 
132
134
  Type `h` for this list of what each letter command does
133
135
  ```
136
+ [1]...[2] Replace notaword with the numbered suggestion
134
137
  [a] Add notaword to a word list
135
138
  [r] Replace notaword
136
139
  [R] Replace this and all future instances of notaword
@@ -144,9 +147,20 @@ What do you want to do? [ ]
144
147
 
145
148
  ---
146
149
 
150
+ If you type a numeral the word will be replaced with that numbered suggestion
151
+ ```
152
+ file.txt:1:0 notaword
153
+ Did you mean: [1] notwork, [2] nonword
154
+ [a]dd, [r]eplace, [s]kip, [h]elp, [^C] to exit: [2]
155
+ Replaced notaword with nonword
156
+ ```
157
+
158
+ ---
159
+
147
160
  If you type `r` or `R` you'll be shown a prompt with the original word and it prefilled ready for correcting:
148
161
  ```
149
162
  file.txt:1:0 notaword
163
+ Did you mean: [1] notwork, [2] nonword
150
164
  [a]dd, [r]eplace, [s]kip, [h]elp, [^C] to exit: [r]
151
165
 
152
166
  [^C] to go back
@@ -167,6 +181,7 @@ Lowercase `s` will skip this particular use of the word, uppercase `S` will also
167
181
  If you instead type `a` you'll be shown a list of possible wordlists to add to. This list is based on the file path, and is configurable in `.spellr.yml`.
168
182
  ```
169
183
  file.txt:1:0 notaword
184
+ Did you mean: [1] notwork, [2] nonword
170
185
  [a]dd, [r]eplace, [s]kip, [h]elp, [^C] to exit: [a]
171
186
 
172
187
  [e] english
@@ -247,7 +262,7 @@ languages:
247
262
  - path/to/logstash/file
248
263
  ```
249
264
 
250
- ## Rake and Travis
265
+ ## Rake
251
266
 
252
267
  Create or open a file in the root of your project named `Rakefile`.
253
268
  adding the following lines
@@ -260,7 +275,27 @@ Spellr::RakeTask.generate_task
260
275
  This will add the `rake spellr` task. To provide arguments like the cli, use square brackets. (ensure you escape the `[]` if you're using zsh)
261
276
  `rake 'spellr[--interactive]'`
262
277
 
263
- To have this automatically run on travis, add `:spellr` to the default task.
278
+ To provide default cli arguments, the first argument is the name, and subsequent arguments are the cli arguments.
279
+ ```ruby
280
+ # Rakefile
281
+ require 'spellr/rake_task'
282
+ Spellr::RakeTask.generate_task(:spellr_quiet, '--quiet')
283
+
284
+ task default: :spellr_quiet
285
+ ```
286
+ or `rake spellr` will be in interactive mode unless the CI env variable is set.
287
+ ```ruby
288
+ # Rakefile
289
+ require 'spellr/rake_task'
290
+ spellr_arguments = ENV['CI'] ? [] : ['--interactive']
291
+ Spellr::RakeTask.generate_task(:spellr, **spellr_arguments)
292
+
293
+ task default: :spellr
294
+ ```
295
+
296
+ ## Travis
297
+
298
+ To have this automatically run on travis, add `:spellr` to the default rake task.
264
299
  ```ruby
265
300
  # Rakefile
266
301
  require 'spellr/rake_task'
@@ -284,28 +319,10 @@ sudo: false
284
319
  language: ruby
285
320
  cache: bundler
286
321
  rvm:
287
- - 2.5
322
+ - 3.0
288
323
  before_install: gem install bundler
289
324
  ```
290
325
 
291
- To provide default cli arguments, the first argument is the name, and subsequent arguments are the cli arguments.
292
- ```ruby
293
- # Rakefile
294
- require 'spellr/rake_task'
295
- Spellr::RakeTask.generate_task(:spellr_quiet, '--quiet')
296
-
297
- task default: :spellr_quiet
298
- ```
299
- or `rake spellr` will be in interactive mode unless the CI env variable is set.
300
- ```ruby
301
- # Rakefile
302
- require 'spellr/rake_task'
303
- spellr_arguments = ENV['CI'] ? [] : ['--interactive']
304
- Spellr::RakeTask.generate_task(:spellr, **spellr_arguments)
305
-
306
- task default: :spellr
307
- ```
308
-
309
326
  ## Ignoring the configured patterns
310
327
 
311
328
  Sometimes you'll want to spell check something that would usually be ignored,
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spellr'
4
+ require_relative 'base_reporter'
5
+ require_relative 'suggester'
6
+
7
+ module Spellr
8
+ class AutocorrectReporter < BaseReporter
9
+ def finish
10
+ puts "\n"
11
+ print_count(:checked, 'file')
12
+ print_value(total, 'error', 'found')
13
+ print_count(:total_fixed, 'error', 'fixed', hide_zero: true)
14
+ print_count(:total_unfixed, 'error', 'unfixed', hide_zero: true)
15
+ end
16
+
17
+ def call(token)
18
+ super
19
+
20
+ handle_replace(token)
21
+ end
22
+
23
+ private
24
+
25
+ def total
26
+ counts[:total_unfixed] + counts[:total_fixed]
27
+ end
28
+
29
+ def handle_replace(token)
30
+ replacement = ::Spellr::Suggester.suggestions(token).first
31
+ return increment(:total_unfixed) unless replacement
32
+
33
+ token.replace(replacement)
34
+ increment(:total_fixed)
35
+ puts "Replaced #{red(token)} with #{green(replacement)}"
36
+ throw :check_file_from, token
37
+ end
38
+ end
39
+ end
@@ -1,50 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spellr
4
- ruby_version = Gem::Version.new(RUBY_VERSION)
5
- unless ruby_version >= Gem::Version.new('2.5')
6
- module HashSlice
7
- refine Hash do
8
- def slice!(*keys)
9
- delete_if { |k| !keys.include?(k) }
10
- end
11
-
12
- def slice(*keys)
13
- dup.slice!(*keys)
14
- end
15
- end
16
- end
17
-
18
- require 'yaml'
19
- module YAMLSymbolizeNames
20
- refine YAML.singleton_class do
21
- alias_method :safe_load_without_symbolize_names, :safe_load
22
- def safe_load(path, *args, symbolize_names: false, **kwargs)
23
- if symbolize_names
24
- symbolize_names!(safe_load_without_symbolize_names(path, *args, **kwargs))
25
- else
26
- safe_load_without_symbolize_names(path, *args, **kwargs)
27
- end
28
- end
29
-
30
- private
31
-
32
- def symbolize_names!(obj) # rubocop:disable Metrics/MethodLength
33
- case obj
34
- when Hash
35
- obj.keys.each do |key| # rubocop:disable Style/HashEachMethods # each_key never finishes.
36
- obj[key.to_sym] = symbolize_names!(obj.delete(key))
37
- end
38
- when Array
39
- obj.map! { |ea| symbolize_names!(ea) }
40
- end
41
- obj
42
- end
43
- end
44
- end
45
- end
46
-
47
- unless ruby_version >= Gem::Version.new('2.6')
4
+ unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
48
5
  require 'yaml'
49
6
  module YAMLPermittedClasses
50
7
  refine YAML.singleton_class do
@@ -46,5 +46,13 @@ module Spellr
46
46
  def counts
47
47
  output.counts
48
48
  end
49
+
50
+ def print_count(stat, noun, verb = stat, hide_zero: false)
51
+ print_value(counts[stat], noun, verb, hide_zero: hide_zero)
52
+ end
53
+
54
+ def print_value(value, noun, verb, hide_zero: false)
55
+ puts "#{pluralize noun, value} #{verb}" if !hide_zero || value.positive?
56
+ end
49
57
  end
50
58
  end
data/lib/spellr/check.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative '../spellr'
4
4
  require_relative 'tokenizer'
5
5
  require_relative 'string_format'
6
+ require_relative 'output_stubbed'
6
7
 
7
8
  module Spellr
8
9
  class Check
@@ -21,8 +22,10 @@ module Spellr
21
22
  end
22
23
 
23
24
  def check
24
- files.each do |file|
25
- check_and_count_file(file)
25
+ if Spellr.config.parallel
26
+ parallel_check
27
+ else
28
+ files.each { |file| check_and_count_file(file, reporter) }
26
29
  end
27
30
 
28
31
  reporter.finish
@@ -30,24 +33,48 @@ module Spellr
30
33
 
31
34
  private
32
35
 
33
- def check_and_count_file(file)
34
- check_file(file)
35
- reporter.output.increment(:checked)
36
+ def parallel_check
37
+ require 'parallel'
38
+
39
+ Parallel.each(files, finish: ->(_, _, result) { reporter.output << result }) do |file|
40
+ sub_reporter = reporter.class.new(Spellr::OutputStubbed.new)
41
+ check_and_count_file(file, sub_reporter)
42
+ sub_reporter.output
43
+ end
44
+ end
45
+
46
+ def check_and_count_file(file, current_reporter)
47
+ check_file(file, current_reporter)
48
+ current_reporter.output.increment(:checked)
36
49
  rescue Spellr::InvalidByteSequence
37
50
  # sometimes files are binary
38
- reporter.warn "Skipped unreadable file: #{aqua file.relative_path}"
51
+ current_reporter.warn "Skipped unreadable file: #{aqua file.relative_path}"
39
52
  end
40
53
 
41
- def check_file(file, start_at = nil, found_word_proc = wordlist_proc_for(file))
54
+ def check_file(file, curr_reporter, start_at = nil, wordlist_proc = wordlist_proc_for(file))
55
+ restart_token = catch(:check_file_from) do
56
+ report_file(file, curr_reporter, start_at, wordlist_proc)
57
+ nil
58
+ end
59
+ check_file_from_restart(file, curr_reporter, restart_token, wordlist_proc) if restart_token
60
+ end
61
+
62
+ def report_file(file, curr_reporter, start_at = nil, wordlist_proc = wordlist_proc_for(file))
42
63
  Spellr::Tokenizer.new(file, start_at: start_at)
43
- .each_token(skip_term_proc: found_word_proc) do |token|
44
- reporter.call(token)
45
- reporter.output.exit_code = 1
64
+ .each_token(skip_term_proc: wordlist_proc) do |token|
65
+ curr_reporter.call(token)
66
+ curr_reporter.output.exit_code = 1
46
67
  end
47
68
  end
48
69
 
70
+ def check_file_from_restart(file, current_reporter, restart_token, wordlist_proc)
71
+ # new wordlist cache when adding a word
72
+ wordlist_proc = wordlist_proc_for(file) unless restart_token.replacement
73
+ check_file(file, current_reporter, restart_token.location, wordlist_proc)
74
+ end
75
+
49
76
  def wordlist_proc_for(file)
50
- wordlists = Spellr.config.wordlists_for(file).sort_by(&:length).reverse
77
+ wordlists = file.wordlists
51
78
 
52
79
  ->(term) { wordlists.any? { |w| w.include?(term) } }
53
80
  end
@@ -9,8 +9,6 @@ module Spellr
9
9
  class Options
10
10
  class << self
11
11
  def parse(argv)
12
- @parallel_option = false
13
-
14
12
  options.parse!(argv)
15
13
  end
16
14
 
@@ -25,6 +23,7 @@ module Spellr
25
23
  opts.on('-w', '--wordlist', 'Outputs errors in wordlist format', &method(:wordlist_option))
26
24
  opts.on('-q', '--quiet', 'Silences output', &method(:quiet_option))
27
25
  opts.on('-i', '--interactive', 'Runs the spell check interactively', &method(:interactive_option))
26
+ opts.on('-a', '--autocorrect', 'Autocorrect errors', &method(:autocorrect_option))
28
27
  opts.separator('')
29
28
  opts.on('--[no-]parallel', 'Run in parallel or not, default --parallel', &method(:parallel_option))
30
29
  opts.on('-d', '--dry-run', 'List files to be checked', &method(:dry_run_option))
@@ -54,9 +53,12 @@ module Spellr
54
53
 
55
54
  def interactive_option(_)
56
55
  require_relative 'interactive'
57
- require_relative 'check_interactive'
58
56
  Spellr.config.reporter = Spellr::Interactive.new
59
- Spellr.config.checker = Spellr::CheckInteractive unless @parallel_option
57
+ end
58
+
59
+ def autocorrect_option(_)
60
+ require_relative 'autocorrect_reporter'
61
+ Spellr.config.reporter = Spellr::AutocorrectReporter.new
60
62
  end
61
63
 
62
64
  def suppress_file_rules(_)
@@ -73,15 +75,8 @@ module Spellr
73
75
  Spellr.config.config_file = file
74
76
  end
75
77
 
76
- def parallel_option(parallel) # rubocop:disable Metrics/MethodLength
77
- @parallel_option = true
78
- Spellr.config.checker = if parallel
79
- require_relative 'check_parallel'
80
- Spellr::CheckParallel
81
- else
82
- require_relative 'check'
83
- Spellr::Check
84
- end
78
+ def parallel_option(parallel)
79
+ Spellr.config.parallel = parallel
85
80
  end
86
81
 
87
82
  def dry_run_option(_)
data/lib/spellr/config.rb CHANGED
@@ -10,7 +10,7 @@ require 'pathname'
10
10
 
11
11
  module Spellr
12
12
  class Config
13
- attr_writer :reporter, :checker
13
+ attr_writer :reporter, :parallel
14
14
 
15
15
  attr_accessor :suppress_file_rules, :dry_run
16
16
 
@@ -68,6 +68,10 @@ module Spellr
68
68
  @reporter ||= default_reporter
69
69
  end
70
70
 
71
+ def parallel
72
+ defined?(@parallel) ? @parallel : default_parallel
73
+ end
74
+
71
75
  def checker
72
76
  return dry_run_checker if dry_run?
73
77
 
@@ -101,8 +105,12 @@ module Spellr
101
105
  end
102
106
 
103
107
  def default_checker
104
- require_relative 'check_parallel'
105
- Spellr::CheckParallel
108
+ require_relative 'check'
109
+ Spellr::Check
110
+ end
111
+
112
+ def default_parallel
113
+ reporter.class.name != 'Spellr::Interactive'
106
114
  end
107
115
  end
108
116
  end
@@ -8,7 +8,7 @@ module Spellr
8
8
  class ConfigValidator
9
9
  include Spellr::Validations
10
10
 
11
- validate :checker_and_reporter_coexist
11
+ validate :not_interactive_and_parallel
12
12
  validate :interactive_is_interactive
13
13
  validate :only_has_one_key_per_language
14
14
  validate :languages_with_conflicting_keys
@@ -31,11 +31,11 @@ module Spellr
31
31
  nil
32
32
  end
33
33
 
34
- def checker_and_reporter_coexist
35
- if Spellr.config.reporter.class.name == 'Spellr::Interactive' &&
36
- Spellr.config.checker.name == 'Spellr::CheckParallel'
37
- errors << 'CLI error: --interactive is incompatible with --parallel'
38
- end
34
+ def not_interactive_and_parallel
35
+ return unless Spellr.config.reporter.class.name == 'Spellr::Interactive' &&
36
+ Spellr.config.parallel
37
+
38
+ errors << 'CLI error: --interactive is incompatible with --parallel'
39
39
  end
40
40
 
41
41
  def only_has_one_key_per_language
data/lib/spellr/file.rb CHANGED
@@ -36,5 +36,9 @@ module Spellr
36
36
  def read_write
37
37
  write(yield read)
38
38
  end
39
+
40
+ def wordlists
41
+ ::Spellr.config.wordlists_for(self).sort_by { |wordlist| -wordlist.length }
42
+ end
39
43
  end
40
44
  end
@@ -35,7 +35,7 @@ module Spellr
35
35
  }
36
36
  end
37
37
 
38
- def fast_ignore # rubocop:disable Metrics/MethodLength
38
+ def fast_ignore
39
39
  FastIgnore.new(
40
40
  **configured_rules,
41
41
  argv_rules: @patterns,
@@ -5,18 +5,17 @@ require_relative '../spellr'
5
5
  require_relative 'interactive_add'
6
6
  require_relative 'interactive_replacement'
7
7
  require_relative 'base_reporter'
8
+ require_relative 'suggester'
8
9
 
9
10
  module Spellr
10
11
  class Interactive < BaseReporter # rubocop:disable Metrics/ClassLength
11
- def finish # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
12
+ def finish
12
13
  puts "\n"
13
- puts "#{pluralize 'file', counts[:checked]} checked"
14
- puts "#{pluralize 'error', total} found"
15
- if counts[:total_skipped].positive?
16
- puts "#{pluralize 'error', counts[:total_skipped]} skipped"
17
- end
18
- puts "#{pluralize 'error', counts[:total_fixed]} fixed" if counts[:total_fixed].positive?
19
- puts "#{pluralize 'word', counts[:total_added]} added" if counts[:total_added].positive?
14
+ print_count(:checked, 'file')
15
+ print_value(total, 'error', 'found')
16
+ print_count(:total_skipped, 'error', 'skipped', hide_zero: true)
17
+ print_count(:total_fixed, 'error', 'fixed', hide_zero: true)
18
+ print_count(:total_added, 'word', 'added', hide_zero: true)
20
19
  end
21
20
 
22
21
  def global_replacements
@@ -27,22 +26,25 @@ module Spellr
27
26
  @global_skips ||= counts[:global_skips] = []
28
27
  end
29
28
 
30
- def call(token)
29
+ def call(token, only_prompt: false)
31
30
  # if attempt_global_replacement succeeds, then it throws,
32
31
  # it acts like a guard clause all by itself.
33
32
  attempt_global_replacement(token)
34
33
  return if attempt_global_skip(token)
35
34
 
36
- super
35
+ super(token) unless only_prompt
36
+
37
+ suggestions = ::Spellr::Suggester.fast_suggestions(token)
38
+ print_suggestions(suggestions) unless only_prompt
37
39
 
38
- prompt(token)
40
+ prompt(token, suggestions)
39
41
  end
40
42
 
41
43
  def prompt_for_key
42
44
  print "[ ]\e[2D"
43
45
  end
44
46
 
45
- def loop_within(seconds) # rubocop:disable Metrics/MethodLength
47
+ def loop_within(seconds)
46
48
  # timeout is just because it gets stuck sometimes
47
49
  Timeout.timeout(seconds * 10) do
48
50
  start_time = monotonic_time
@@ -84,11 +86,21 @@ module Spellr
84
86
  "^#{char.tr(CTRL_STR, ALPHABET)}"
85
87
  end
86
88
 
87
- def prompt(token)
89
+ def print_suggestions(suggestions)
90
+ return if suggestions.empty?
91
+
92
+ puts "Did you mean: #{number_suggestions(suggestions)}"
93
+ end
94
+
95
+ def number_suggestions(suggestions)
96
+ suggestions.map.with_index(1) { |word, i| "#{key i.to_s} #{word}" }.join(', ')
97
+ end
98
+
99
+ def prompt(token, suggestions)
88
100
  print "#{key 'add'}, #{key 'replace'}, #{key 'skip'}, #{key 'help'}, [^#{bold 'C'}] to exit: "
89
101
  prompt_for_key
90
102
 
91
- handle_response(token)
103
+ handle_response(token, suggestions)
92
104
  end
93
105
 
94
106
  def clear_line(lines = 1)
@@ -121,9 +133,17 @@ module Spellr
121
133
  throw :check_file_from, token
122
134
  end
123
135
 
124
- def handle_response(token) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
136
+ def suggestions_options(suggestions)
137
+ return suggestions if suggestions.empty?
138
+
139
+ ('1'..(suggestions.length.to_s)).to_a
140
+ end
141
+
142
+ def handle_response(token, suggestions) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
143
+ numbers = suggestions_options(suggestions)
125
144
  # :nocov:
126
- case stdin_getch("qaAsSrR?h\u0003\u0004")
145
+ letter = stdin_getch("qaAsSrR?h\u0003\u0004#{numbers.join}")
146
+ case letter
127
147
  # :nocov:
128
148
  when 'q', "\u0003" # ctrl c
129
149
  Spellr.exit 1
@@ -137,20 +157,37 @@ module Spellr
137
157
  Spellr::InteractiveReplacement.new(token, self).global_replace
138
158
  when 'r'
139
159
  Spellr::InteractiveReplacement.new(token, self).replace
160
+ when *numbers
161
+ handle_replace_with_suggestion(token, suggestions, letter)
140
162
  when '?', 'h'
141
- handle_help(token)
163
+ handle_help(token, suggestions)
142
164
  end
143
165
  end
144
166
 
167
+ def handle_replace_with_suggestion(token, suggestions, letter)
168
+ replacement = suggestions[letter.to_i - 1].chomp
169
+
170
+ token.replace(replacement)
171
+ increment(:total_fixed)
172
+ puts "Replaced #{red(token)} with #{green(replacement)}"
173
+ throw :check_file_from, token
174
+ end
175
+
145
176
  def handle_skip(token)
146
177
  increment(:total_skipped)
147
178
  yield token if block_given?
148
179
  puts "Skipped #{red(token)}"
149
180
  end
150
181
 
151
- def handle_help(token) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
182
+ def handle_help(token, suggestions) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
152
183
  clear_line(2)
153
184
  puts ''
185
+ if suggestions.length > 1
186
+ puts "#{key '1'}...#{key suggestions.length.to_s} "\
187
+ "Replace #{red token} with the numbered suggestion"
188
+ elsif suggestions.length == 1
189
+ puts "#{key '1'} Replace #{red token} with the numbered suggestion"
190
+ end
154
191
  puts "#{key 'a'} Add #{red token} to a word list"
155
192
  puts "#{key 'r'} Replace #{red token}"
156
193
  puts "#{key 'R'} Replace this and all future instances of #{red token}"
@@ -160,7 +197,7 @@ module Spellr
160
197
  puts "[ctrl] + #{key 'C'} Exit spellr"
161
198
  puts ''
162
199
  print "What do you want to do? [ ]\e[2D"
163
- handle_response(token)
200
+ handle_response(token, suggestions)
164
201
  end
165
202
  end
166
203
  end
@@ -39,8 +39,8 @@ module Spellr
39
39
  end
40
40
 
41
41
  def handle_ctrl_c
42
- reporter.clear_line(language_keys.length + 6)
43
- reporter.call(token)
42
+ reporter.clear_line(language_keys.length + 5)
43
+ reporter.call(token, only_prompt: true)
44
44
  end
45
45
 
46
46
  def handle_wordlist_choice
@@ -70,8 +70,8 @@ module Spellr
70
70
 
71
71
  def handle_ctrl_c
72
72
  print "\e[0m"
73
- reporter.clear_line(5)
74
- reporter.call(token)
73
+ reporter.clear_line(4)
74
+ reporter.call(token, only_prompt: true)
75
75
  end
76
76
 
77
77
  private
@@ -79,7 +79,7 @@ class PossibleKey # rubocop:disable Metrics/ClassLength
79
79
  letter_frequency_difference.slice(*FEATURE_LETTERS)
80
80
  end
81
81
 
82
- def character_set # rubocop:disable Metrics/MethodLength
82
+ def character_set
83
83
  @character_set ||= case string
84
84
  when /^[a-fA-F0-9\-]+$/ then :hex
85
85
  when /^[a-z0-9]+$/ then :lower36
@@ -27,7 +27,7 @@ module Stats
27
27
  yield values.max_by(&block)
28
28
  end
29
29
 
30
- def variance(values, &block) # rubocop:disable Metrics/MethodLength
30
+ def variance(values, &block)
31
31
  return 0 if values.empty?
32
32
 
33
33
  mean = mean(values, &block)
@@ -37,7 +37,7 @@ module Spellr
37
37
  end
38
38
  end
39
39
 
40
- def each_token(skip_term_proc: nil) # rubocop:disable Metrics/MethodLength
40
+ def each_token(skip_term_proc: nil)
41
41
  until eos?
42
42
  term = next_term
43
43
  next unless term
@@ -7,8 +7,8 @@ module Spellr
7
7
  class Reporter < Spellr::BaseReporter
8
8
  def finish
9
9
  puts "\n"
10
- puts "#{pluralize 'file', counts[:checked]} checked"
11
- puts "#{pluralize 'error', counts[:total]} found"
10
+ print_count(:checked, 'file')
11
+ print_count(:total, 'error', 'found')
12
12
 
13
13
  interactive_command if counts[:total].positive?
14
14
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'did_you_mean'
4
+ require 'jaro_winkler'
5
+
6
+ ::DidYouMean.send(:remove_const, :JaroWinkler)
7
+ ::DidYouMean::JaroWinkler = ::JaroWinkler
8
+
9
+ module Spellr
10
+ class Suggester
11
+ class << self
12
+ def suggestions(token)
13
+ wordlists = token.location.file.wordlists
14
+ term = token.spellr_normalize.chomp
15
+ words = wordlists.flat_map { |wordlist| wordlist.suggestions(token) }.uniq
16
+ words = ::DidYouMean::SpellChecker.new(dictionary: words).correct(term)
17
+ words = reduce_suggestions(words, term)
18
+
19
+ words.map { |word| word.send(token.case_method) }
20
+ end
21
+
22
+ def slow?
23
+ return @slow if defined?(@slow)
24
+
25
+ @slow = ::JaroWinkler.method(:distance).source_location
26
+ end
27
+
28
+ def fast_suggestions(token)
29
+ if slow?
30
+ []
31
+ else
32
+ suggestions(token)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def reduce_suggestions(words, term)
39
+ return words unless words.length > 1
40
+
41
+ threshold = ::JaroWinkler.distance(term, words.first) * 0.98
42
+ words.select! { |word| ::JaroWinkler.distance(term, word) > threshold }
43
+ words.sort_by! { |word| [-::JaroWinkler.distance(term, word), word] }
44
+ words.take(5)
45
+ end
46
+ end
47
+
48
+ def initialize(wordlist)
49
+ @did_you_mean = ::DidYouMean::SpellChecker.new(dictionary: wordlist.to_a)
50
+ @suggestions = {}
51
+ end
52
+
53
+ def suggestions(term)
54
+ term = term.spellr_normalize
55
+ @suggestions.fetch(term) do
56
+ @suggestions[term] = @did_you_mean.correct(term).map(&:chomp)
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/spellr/token.rb CHANGED
@@ -66,5 +66,14 @@ module Spellr
66
66
  @replacement = replacement
67
67
  location.file.insert(replacement, file_char_range)
68
68
  end
69
+
70
+ def case_method
71
+ @case_method ||= case self
72
+ when /\A[[:lower:]]+\z/ then :downcase
73
+ when /\A[[:upper:]]+\z/ then :upcase
74
+ when /\A[[:upper:]][[:lower:]]*\z/ then :capitalize
75
+ else :itself
76
+ end
77
+ end
69
78
  end
70
79
  end
@@ -34,7 +34,7 @@ module Spellr
34
34
  file.close
35
35
  end
36
36
 
37
- def each_token(skip_term_proc: nil) # rubocop:disable Metrics/MethodLength
37
+ def each_token(skip_term_proc: nil)
38
38
  each_line_with_stats do |line, line_number, char_offset, byte_offset|
39
39
  prepare_tokenizer_for_line(line)&.each_token(skip_term_proc: skip_term_proc) do |token|
40
40
  token.line = prepare_line(line, line_number, char_offset, byte_offset)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spellr
4
- VERSION = '0.9.1'
4
+ VERSION = '0.10.0'
5
5
  end
@@ -78,8 +78,20 @@ module Spellr
78
78
  to_a.length
79
79
  end
80
80
 
81
+ def suggestions(term)
82
+ suggester.suggestions(term)
83
+ end
84
+
81
85
  private
82
86
 
87
+ def suggester
88
+ @suggester ||= begin
89
+ require_relative 'suggester'
90
+
91
+ ::Spellr::Suggester.new(self)
92
+ end
93
+ end
94
+
83
95
  def insert_sorted(term)
84
96
  insert_at = words.bsearch_index { |value| value >= term }
85
97
  insert_at ? words.insert(insert_at, term) : words.push(term)
@@ -87,6 +99,7 @@ module Spellr
87
99
 
88
100
  def clear_cache
89
101
  @words = nil
102
+ @suggester = nil
90
103
  @include = {}
91
104
  remove_instance_variable(:@exist) if defined?(@exist)
92
105
  end
data/spellr.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
21
21
  end
22
22
 
23
- spec.required_ruby_version = '>= 2.4'
23
+ spec.required_ruby_version = '>= 2.5'
24
24
 
25
25
  spec.files = Dir.glob('{lib,exe,wordlists}/**/{*,.*}') + %w{
26
26
  CHANGELOG.md
@@ -47,6 +47,8 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency 'tty_string', '>= 1.1.0'
48
48
  spec.add_development_dependency 'webmock', '~> 3.8'
49
49
 
50
+ spec.add_dependency 'did_you_mean'
50
51
  spec.add_dependency 'fast_ignore', '>= 0.11.0'
52
+ spec.add_dependency 'jaro_winkler'
51
53
  spec.add_dependency 'parallel', '~> 1.0'
52
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spellr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dana Sherson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-10 00:00:00.000000000 Z
11
+ date: 2022-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
194
  version: '3.8'
195
+ - !ruby/object:Gem::Dependency
196
+ name: did_you_mean
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: fast_ignore
197
211
  requirement: !ruby/object:Gem::Requirement
@@ -206,6 +220,20 @@ dependencies:
206
220
  - - ">="
207
221
  - !ruby/object:Gem::Version
208
222
  version: 0.11.0
223
+ - !ruby/object:Gem::Dependency
224
+ name: jaro_winkler
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
209
237
  - !ruby/object:Gem::Dependency
210
238
  name: parallel
211
239
  requirement: !ruby/object:Gem::Requirement
@@ -235,12 +263,11 @@ files:
235
263
  - exe/spellr
236
264
  - lib/.spellr.yml
237
265
  - lib/spellr.rb
266
+ - lib/spellr/autocorrect_reporter.rb
238
267
  - lib/spellr/backports.rb
239
268
  - lib/spellr/base_reporter.rb
240
269
  - lib/spellr/check.rb
241
270
  - lib/spellr/check_dry_run.rb
242
- - lib/spellr/check_interactive.rb
243
- - lib/spellr/check_parallel.rb
244
271
  - lib/spellr/cli.rb
245
272
  - lib/spellr/cli_options.rb
246
273
  - lib/spellr/column_location.rb
@@ -266,6 +293,7 @@ files:
266
293
  - lib/spellr/rake_task.rb
267
294
  - lib/spellr/reporter.rb
268
295
  - lib/spellr/string_format.rb
296
+ - lib/spellr/suggester.rb
269
297
  - lib/spellr/token.rb
270
298
  - lib/spellr/token_regexps.rb
271
299
  - lib/spellr/tokenizer.rb
@@ -311,14 +339,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
311
339
  requirements:
312
340
  - - ">="
313
341
  - !ruby/object:Gem::Version
314
- version: '2.4'
342
+ version: '2.5'
315
343
  required_rubygems_version: !ruby/object:Gem::Requirement
316
344
  requirements:
317
345
  - - ">="
318
346
  - !ruby/object:Gem::Version
319
347
  version: '0'
320
348
  requirements: []
321
- rubygems_version: 3.2.15
349
+ rubygems_version: 3.1.6
322
350
  signing_key:
323
351
  specification_version: 4
324
352
  summary: Spell check your source code
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../spellr'
4
- require_relative 'check'
5
-
6
- module Spellr
7
- class CheckInteractive < Check
8
- private
9
-
10
- def check_file_from_restart(file, restart_token, wordlist_proc)
11
- # new wordlist cache when adding a word
12
- wordlist_proc = wordlist_proc_for(file) unless restart_token.replacement
13
- check_file(file, restart_token.location, wordlist_proc)
14
- end
15
-
16
- def check_file(file, start_at = nil, wordlist_proc = wordlist_proc_for(file))
17
- restart_token = catch(:check_file_from) do
18
- super(file, start_at, wordlist_proc)
19
- nil
20
- end
21
- check_file_from_restart(file, restart_token, wordlist_proc) if restart_token
22
- end
23
- end
24
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../spellr'
4
- require_relative 'check'
5
- require_relative 'output_stubbed'
6
- require 'parallel'
7
-
8
- module Spellr
9
- class CheckParallel < Check
10
- def check # rubocop:disable Metrics/MethodLength
11
- acc_reporter = @reporter
12
-
13
- Parallel.each(files, finish: ->(_, _, result) { acc_reporter.output << result }) do |file|
14
- @reporter = acc_reporter.class.new(Spellr::OutputStubbed.new)
15
- check_and_count_file(file)
16
- reporter.output
17
- end
18
- @reporter = acc_reporter
19
-
20
- reporter.finish
21
- end
22
- end
23
- end