spellr 0.9.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 901a89f14415b3993f210ac2f0f94cb82efdbfc55565f8e1c9fd51347790b3d6
4
- data.tar.gz: 113d25fcdfc5d70c9e50f4a061092728c1349452e88e0dd0d6268d10851c47b9
3
+ metadata.gz: cb3c19ba481190944dadf76bbc0d4953e4488aefedb8debfa6f153e752577831
4
+ data.tar.gz: acddcaf1433e9190ad4fdd61e1714fad9623acbd98f951c413ac7bab4ce484b3
5
5
  SHA512:
6
- metadata.gz: adcfec5ea17125517f445fb7e895b5ee049d1540f7626ab5c55d835d600b149d844b9a052f2324784038f5098a392a8f51b16d01c56ace9cffa773b34e27bbca
7
- data.tar.gz: 922c16526b3e0dd8600ca5ef836de2419dda0237e3b3c2d8a2674d1ca7b0f0dcebf1fe0d0deded671522ee526c7e185a338ef98e762c4385e00b25f8e3ae94e8
6
+ metadata.gz: cbb4ef118126f6f04ffbf6316490bb8c6b382339fb34b948b11400ecc1edcf61ff7442e2b59711babcec67b69e56ffe826f64d0f91385ca720fe1139c30463e0
7
+ data.tar.gz: a47bf93a6e524282bd03b282e25cba90279066e045ec5b8a1ee269dc332722d70743b2cc0b95fafd38c4b16fff26ec46c74ea3c2c7a42b7c1e5dfd6e3b55e945
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # v0.11.0
2
+ - Remove explicit did_you_mean dependency
3
+ (if you want the suggestions in ruby 2.5 & 2.6, you'll need to add did_you_mean to your Gemfile directly).
4
+ - require 'set' in Spellr::Reporter
5
+ previously we were just were relying on FastIgnore requiring it, and i don't use it there any longer.
6
+
7
+ # v0.10.1
8
+ - Resolve fast_ignore follow_symlinks deprecation
9
+
10
+ # v0.10.0
11
+ - Drop ruby 2.4 support, to allow for...
12
+ - Spelling suggestions while using `spellr --interactive`
13
+ - And a new, probably frequently wrong, `spellr --autocorrect`
14
+
1
15
  # v0.9.1
2
16
  - Assume all files are utf8, more comprehensively. (Sets ::Encoding.default_external and default_internal while running)
3
17
 
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 'maybe_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,47 @@ 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
- rescue Spellr::InvalidByteSequence
37
- # sometimes files are binary
38
- reporter.warn "Skipped unreadable file: #{aqua file.relative_path}"
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
39
44
  end
40
45
 
41
- def check_file(file, start_at = nil, found_word_proc = wordlist_proc_for(file))
46
+ def check_and_count_file(file, current_reporter)
47
+ check_file(file, current_reporter)
48
+ current_reporter.output.increment(:checked)
49
+ rescue Spellr::InvalidByteSequence, ::Errno::ENOENT, ::Errno::EISDIR, ::Errno::EACCES
50
+ current_reporter.warn "Skipped unreadable file: #{aqua file.relative_path}"
51
+ end
52
+
53
+ def check_file(file, curr_reporter, start_at = nil, wordlist_proc = wordlist_proc_for(file))
54
+ restart_token = catch(:check_file_from) do
55
+ report_file(file, curr_reporter, start_at, wordlist_proc)
56
+ nil
57
+ end
58
+ check_file_from_restart(file, curr_reporter, restart_token, wordlist_proc) if restart_token
59
+ end
60
+
61
+ def report_file(file, curr_reporter, start_at = nil, wordlist_proc = wordlist_proc_for(file))
42
62
  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
63
+ .each_token(skip_term_proc: wordlist_proc) do |token|
64
+ curr_reporter.call(token)
65
+ curr_reporter.output.exit_code = 1
46
66
  end
47
67
  end
48
68
 
69
+ def check_file_from_restart(file, current_reporter, restart_token, wordlist_proc)
70
+ # new wordlist cache when adding a word
71
+ wordlist_proc = wordlist_proc_for(file) unless restart_token.replacement
72
+ check_file(file, current_reporter, restart_token.location, wordlist_proc)
73
+ end
74
+
49
75
  def wordlist_proc_for(file)
50
- wordlists = Spellr.config.wordlists_for(file).sort_by(&:length).reverse
76
+ wordlists = file.wordlists
51
77
 
52
78
  ->(term) { wordlists.any? { |w| w.include?(term) } }
53
79
  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,11 +35,10 @@ 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,
42
- follow_symlinks: true,
43
42
  root: Spellr.pwd_s
44
43
  )
45
44
  end
@@ -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 'maybe_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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require_relative 'suggester'
5
+ # :nocov:
6
+ rescue LoadError
7
+ require_relative 'null_suggester'
8
+ Spellr::Suggester = Spellr::NullSuggester
9
+ # :nocov:
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spellr
4
+ class NullSuggester
5
+ class << self
6
+ def suggestions(_token)
7
+ []
8
+ end
9
+
10
+ def fast_suggestions(_token)
11
+ []
12
+ end
13
+
14
+ def slow?
15
+ true
16
+ end
17
+ end
18
+
19
+ def initialize(_wordlist); end
20
+
21
+ def suggestions(_term)
22
+ []
23
+ end
24
+ end
25
+ end
@@ -2,13 +2,14 @@
2
2
 
3
3
  require_relative 'base_reporter'
4
4
  require 'shellwords'
5
+ require 'set'
5
6
 
6
7
  module Spellr
7
8
  class Reporter < Spellr::BaseReporter
8
9
  def finish
9
10
  puts "\n"
10
- puts "#{pluralize 'file', counts[:checked]} checked"
11
- puts "#{pluralize 'error', counts[:total]} found"
11
+ print_count(:checked, 'file')
12
+ print_count(:total, 'error', 'found')
12
13
 
13
14
  interactive_command if counts[:total].positive?
14
15
  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.11.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 'maybe_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
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.require_paths = ['lib']
35
35
 
36
36
  spec.add_development_dependency 'bundler', '~> 2.0'
37
+ spec.add_development_dependency 'did_you_mean' unless ENV['DID_YOU_MEAN'] == '0'
37
38
  spec.add_development_dependency 'leftovers', '>= 0.4.0'
38
39
  spec.add_development_dependency 'mime-types', '~> 3.3.1'
39
40
  spec.add_development_dependency 'nokogiri'
@@ -48,5 +49,6 @@ Gem::Specification.new do |spec|
48
49
  spec.add_development_dependency 'webmock', '~> 3.8'
49
50
 
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.11.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-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: did_you_mean
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: leftovers
29
43
  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
@@ -259,6 +286,8 @@ files:
259
286
  - lib/spellr/language.rb
260
287
  - lib/spellr/line_location.rb
261
288
  - lib/spellr/line_tokenizer.rb
289
+ - lib/spellr/maybe_suggester.rb
290
+ - lib/spellr/null_suggester.rb
262
291
  - lib/spellr/output.rb
263
292
  - lib/spellr/output_stubbed.rb
264
293
  - lib/spellr/pwd.rb
@@ -266,6 +295,7 @@ files:
266
295
  - lib/spellr/rake_task.rb
267
296
  - lib/spellr/reporter.rb
268
297
  - lib/spellr/string_format.rb
298
+ - lib/spellr/suggester.rb
269
299
  - lib/spellr/token.rb
270
300
  - lib/spellr/token_regexps.rb
271
301
  - lib/spellr/tokenizer.rb
@@ -311,14 +341,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
311
341
  requirements:
312
342
  - - ">="
313
343
  - !ruby/object:Gem::Version
314
- version: '2.4'
344
+ version: '2.5'
315
345
  required_rubygems_version: !ruby/object:Gem::Requirement
316
346
  requirements:
317
347
  - - ">="
318
348
  - !ruby/object:Gem::Version
319
349
  version: '0'
320
350
  requirements: []
321
- rubygems_version: 3.2.15
351
+ rubygems_version: 3.3.7
322
352
  signing_key:
323
353
  specification_version: 4
324
354
  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