reen 1.0.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c32624b91f88178dd9a68dc09229916804beffff846e04074e4f76a5eb299739
4
- data.tar.gz: ef4cde36ed7202532276d761da152004dcb2f322814d7ed8d82c6de78ba4c920
3
+ metadata.gz: d84332445053fed05a469540f848b194b7ddc94374b5ae42e805e84681e53f9b
4
+ data.tar.gz: 966507f2ef7e0d07c273c758dd9242535acba82414876376d5f73432a4868c54
5
5
  SHA512:
6
- metadata.gz: e139aa0f8f927448c3e566a37868b5c29697e91b46f9fa3043d12dce53d015e64b1791474e36178d40b1b937b76032ab1fcce8caebfa5c45081a79251cee1c56
7
- data.tar.gz: f9e20a1defcf449536eb2e8ca160ef7ee6eb26831527015df85cb2839b88cc97fb3ca2300ab27d00e388ea22af95c978c0d972932a80e2881638390f2c4b3c81
6
+ metadata.gz: 245fbb2118dd45f596c4794f9e59df822a7bf91ce1c5dc38d7ecfdf356d6ca4333cf8360b490e3d01ff184c741069934b499b3d61f7792b26f9733624427cfb2
7
+ data.tar.gz: 6c4e07c278d6ba23a9930f0f04f334004b80caba6bdb53389f2f495b5f4ccbf8e15ce46c7f9d883967700b423791cb1dc2dffc5b1fb39be3a9300a59fc6d3c77
data/README.md CHANGED
@@ -103,7 +103,7 @@ Use Reen programmatically using the `reen` gem. In the example below, we specify
103
103
  require 'reen'
104
104
 
105
105
  glob = Dir.glob("*")
106
- reen = Reen::Reen.new(editor: nil)
106
+ reen = Reen::Renamer.new(editor: nil)
107
107
 
108
108
  reen.execute(glob) do |file|
109
109
  # Rename LICENSE.txt -> LICENSE.md (gsub works on the path portion)
data/bin/reen CHANGED
@@ -3,90 +3,9 @@
3
3
 
4
4
  $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5
5
  require "reen"
6
- require "optparse"
7
-
8
- # Reen command line application
9
- class ReenCLI
10
- EDITOR_MSG = "Editor not set in $VISUAL or $EDITOR -- please set one of those environment variables"
11
- Options = Struct.new(:editor, :review) do
12
- def initialize(editor: nil, review: false)
13
- editor ||= ENV["VISUAL"] || ENV["EDITOR"] # rubocop:disable Style/FetchEnvVar
14
- super(editor, review)
15
- end
16
- end
17
-
18
- attr_reader :options, :files
19
-
20
- def initialize(args)
21
- @options = Options.new
22
- optparse = OptionParser.new { |parser| setup_options(parser) }
23
- optparse.parse!(args, into: @options)
24
-
25
- exit_with_msg(EDITOR_MSG) unless options.editor
26
-
27
- @files = args.empty? ? Dir.glob("*") : args
28
- rescue OptionParser::InvalidOption => e
29
- puts "#{e.message}\n\n"
30
- exit_with_msg(optparse)
31
- end
32
-
33
- def setup_options(parser) # rubocop:disable Metrics/MethodLength
34
- parser.banner = "Usage: reen files [options]"
35
- parser.version = Reen::VERSION
36
-
37
- parser.on("-e", "--editor [EDITOR]", "Use EDITOR (default: $EDITOR)") do |ed|
38
- @options.editor = ed || ENV["EDITOR"] # rubocop:disable Style/FetchEnvVar
39
- end
40
- parser.on("-v", "--visual [EDITOR]", "Use EDITOR (default: $VISUAL)") do |ed|
41
- @options.editor = ed || ENV["VISUAL"] # rubocop:disable Style/FetchEnvVar
42
- end
43
- parser.on("-r", "--review", "Require review and confirmation of changes")
44
-
45
- parser.on("-h", "--help", "Show help for options") do
46
- exit_with_msg(parser)
47
- end
48
- end
49
-
50
- def exit_with_msg(message)
51
- puts message
52
- exit(false)
53
- end
54
-
55
- def review_changes
56
- puts
57
- puts @requests.change_requested.summarize
58
- print "\nContinue? (y/n) "
59
- confirmation = %w[y yes].include?($stdin.gets.chomp.downcase)
60
- exit_with_msg("Nothing changed") unless confirmation
61
- end
62
-
63
- def check_inputs
64
- @files = files.empty? ? Dir.glob("*") : files
65
- exit_with_msg(EDITOR_MSG) unless options.editor
66
- end
67
-
68
- def user_review?
69
- @options.review && @requests.changes_requested?
70
- end
71
-
72
- def call
73
- @requests = Reen::Reen.new(editor: options.editor).request(files)
74
- review_changes if user_review?
75
-
76
- changes = @requests
77
- .execute_all
78
- .change_requested
79
-
80
- if user_review? && changes.all_executed?
81
- puts "Changes made"
82
- else
83
- puts changes.summarize
84
- end
85
- end
86
- end
87
6
 
88
7
  begin
89
- ReenCLI.new(ARGV).call
8
+ Reen::ReenCLI.new(ARGV).call
90
9
  rescue Reen::Error => e
91
10
  puts "#{e.message}\n"
92
11
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Reen
6
+ # Reen command line application
7
+ class ReenCLI
8
+ EDITOR_MSG = "Editor not set in $VISUAL or $EDITOR -- please set one of those environment variables"
9
+ Options = Struct.new(:editor, :review) do
10
+ def initialize(editor: nil, review: false)
11
+ editor ||= ENV["VISUAL"] || ENV["EDITOR"] # rubocop:disable Style/FetchEnvVar
12
+ super(editor, review)
13
+ end
14
+ end
15
+
16
+ attr_reader :options, :files
17
+
18
+ def initialize(args)
19
+ @options = Options.new
20
+ optparse = OptionParser.new { |parser| setup_options(parser) }
21
+ optparse.parse!(args, into: @options)
22
+
23
+ exit_with_msg(EDITOR_MSG) unless options.editor
24
+
25
+ @files = args.empty? ? Dir.glob("*") : args
26
+ rescue OptionParser::InvalidOption => e
27
+ puts "#{e.message}\n\n"
28
+ exit_with_msg(optparse)
29
+ end
30
+
31
+ def setup_options(parser) # rubocop:disable Metrics/MethodLength
32
+ parser.banner = "Usage: reen files [options]"
33
+ parser.version = Reen::VERSION
34
+
35
+ parser.on("-e", "--editor [EDITOR]", "Use EDITOR (default: $EDITOR)") do |ed|
36
+ @options.editor = ed || ENV["EDITOR"] # rubocop:disable Style/FetchEnvVar
37
+ end
38
+ parser.on("-v", "--visual [EDITOR]", "Use EDITOR (default: $VISUAL)") do |ed|
39
+ @options.editor = ed || ENV["VISUAL"] # rubocop:disable Style/FetchEnvVar
40
+ end
41
+ parser.on("-r", "--review", "Require review and confirmation of changes")
42
+
43
+ parser.on("-h", "--help", "Show help for options") do
44
+ exit_with_msg(parser)
45
+ end
46
+ end
47
+
48
+ def exit_with_msg(message)
49
+ puts message
50
+ exit(false)
51
+ end
52
+
53
+ def review_changes
54
+ puts
55
+ puts @changes.summarize
56
+ print "\nContinue? (y/n) "
57
+ confirmation = %w[y yes].include?($stdin.gets.chomp.downcase)
58
+ exit_with_msg("Nothing changed") unless confirmation
59
+ end
60
+
61
+ def check_inputs
62
+ @files = files.empty? ? Dir.glob("*") : files
63
+ exit_with_msg(EDITOR_MSG) unless options.editor
64
+ end
65
+
66
+ def user_wants_review?
67
+ @options.review && @requests.changes_requested?
68
+ end
69
+
70
+ def call
71
+ @service = Renamer.new(editor: options.editor)
72
+ @requests = @service.request(files)
73
+ @changes = @requests.change_requested
74
+
75
+ review_changes if user_wants_review?
76
+ @service.execute_changes
77
+
78
+ if user_wants_review? && @changes.all_executed?
79
+ puts "Changes made"
80
+ else
81
+ puts @changes.summarize
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../infrastructure/actions/delete"
4
+ require_relative "../infrastructure/actions/force_delete"
5
+ require_relative "../infrastructure/actions/rename"
6
+ require_relative "../infrastructure/actions/nothing"
7
+
8
+ module Reen
9
+ # Renames pattern of files with given editor
10
+ # Examples:
11
+ # Reen::Renamer.new(editor: "code -w").call("spec/fixtures/example/*")
12
+ # Reen::Renamer.new(editor: nil).call("spec/fixtures/example/*") { ... }
13
+ class Renamer
14
+ DEL_ERROR = "Do not remove any file/folder names (no changes made)"
15
+
16
+ ACTION_HANDLER = {
17
+ Change::CHANGE::NONE => Actions::DoNothing,
18
+ Change::CHANGE::DELETE => Actions::Delete,
19
+ Change::CHANGE::FORCE_DELETE => Actions::ForceDelete,
20
+ Change::CHANGE::RENAME => Actions::Rename
21
+ }.freeze
22
+
23
+ attr_reader :changes
24
+
25
+ def initialize(editor: "emacs", options: {})
26
+ @editor = editor
27
+ @options = options
28
+ end
29
+
30
+ def request(original_list, &block)
31
+ @entry_list = PathEntryList.new(original_list)
32
+ changed_list = ChangesFile.new(@entry_list).allow_changes(@editor, &block)
33
+
34
+ raise(Error, DEL_ERROR) if changed_list.size != @entry_list.count
35
+
36
+ @changes = detect_changes(@entry_list, changed_list)
37
+ end
38
+
39
+ def execute(original_list, &block)
40
+ @changes ||= request(original_list, &block)
41
+ execute_changes
42
+ @changes
43
+ end
44
+
45
+ def execute_changes
46
+ @changes.list.each do |change|
47
+ next if change.not_accepted?
48
+
49
+ error = ACTION_HANDLER[change.change].new(change.original, change.requested).call
50
+ error ? change.mark_failed(error) : change.mark_executed
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def detect_changes(entry_list, changed_list)
57
+ changed_by_number = PathEntryList.from_numbered(changed_list)
58
+ entries = entry_list.to_a
59
+
60
+ changes = changed_by_number.map do |number, revised|
61
+ Change.new(entries[number - 1], revised)
62
+ end
63
+
64
+ Changes.new(changes)
65
+ end
66
+ end
67
+ end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "actions/delete"
4
- require_relative "actions/force_delete"
5
- require_relative "actions/rename"
6
- require_relative "actions/nothing"
7
-
8
3
  module Reen
9
4
  # Change to an orignal file
10
5
  class Change
@@ -29,13 +24,6 @@ module Reen
29
24
  FOLDER = :folder
30
25
  end
31
26
 
32
- ACTION_HANDLER = {
33
- CHANGE::NONE => Actions::DoNothing,
34
- CHANGE::DELETE => Actions::Delete,
35
- CHANGE::FORCE_DELETE => Actions::ForceDelete,
36
- CHANGE::RENAME => Actions::Rename
37
- }.freeze
38
-
39
27
  CHANGES_DESC = {
40
28
  CHANGE::NONE => "Nothing",
41
29
  CHANGE::DELETE => "Deleting",
@@ -87,18 +75,13 @@ module Reen
87
75
  end
88
76
  end
89
77
 
90
- def execute
91
- return self if not_accepted?
92
-
78
+ def mark_executed
93
79
  @status = STATUS::EXECUTED
94
- error = ACTION_HANDLER[@change].new(@original, @requested).call
95
-
96
- if error
97
- @status = STATUS::FAILED
98
- @reason = error
99
- end
80
+ end
100
81
 
101
- self
82
+ def mark_failed(reason)
83
+ @status = STATUS::FAILED
84
+ @reason = reason
102
85
  end
103
86
 
104
87
  # Predicates
@@ -11,11 +11,6 @@ module Reen
11
11
  @list = changes_list
12
12
  end
13
13
 
14
- def execute_all
15
- @list.map(&:execute)
16
- self
17
- end
18
-
19
14
  # Queries
20
15
 
21
16
  def rename_requested
@@ -27,6 +27,9 @@ module Reen
27
27
  end
28
28
  end
29
29
 
30
+ # Parses "[NN] path" lines into { number => path } hash.
31
+ # Preserves line order (Ruby hash insertion order), so callers get editor order.
32
+ # Silently skips lines that don't match the [NN] pattern.
30
33
  def self.from_numbered(lines)
31
34
  lines.each_with_object({}) do |line, hash|
32
35
  match = line.match(/^\[(\d+)\] (.*)/)
@@ -24,6 +24,8 @@ module Reen
24
24
  @list_file.close
25
25
  end
26
26
 
27
+ # Returns only valid "[NN] path" entry lines (comments and blanks stripped).
28
+ # Line order reflects the user's editor arrangement.
27
29
  def allow_changes(editor, &block)
28
30
  await_editor(editor) if editor
29
31
  @list = File.read(path).split("\n").map(&:strip)
data/lib/reen/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reen
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/reen.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "reen/version"
4
- require_relative "reen/path_entry"
5
- require_relative "reen/path_entry_list"
6
- require_relative "reen/changes_file"
7
- require_relative "reen/change"
8
- require_relative "reen/changes"
9
- require_relative "reen/reen"
4
+ require_relative "reen/domain/path_entry"
5
+ require_relative "reen/domain/path_entry_list"
6
+ require_relative "reen/domain/change"
7
+ require_relative "reen/domain/changes"
8
+ require_relative "reen/infrastructure/changes_file"
9
+ require_relative "reen/application/renamer"
10
+ require_relative "reen/application/reen_cli"
10
11
 
11
12
  module Reen
12
13
  class Error < StandardError; end
data/reen.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/reen/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "reen"
7
+ spec.version = Reen::VERSION
8
+ spec.authors = ["Soumya Ray"]
9
+ spec.email = ["soumya.ray@gmail.com"]
10
+
11
+ spec.summary = "Renames or deletes a pattern of files using your favorite editor"
12
+ spec.description = "Renames or deletes a pattern of files using your favorite editor"
13
+ spec.homepage = "https://github.com/soumyaray/reen"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0"
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["rubygems_mfa_required"] = "true"
22
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
27
+ end
28
+ end
29
+ spec.bindir = "bin"
30
+ spec.executables << "reen"
31
+ spec.require_paths = ["lib"]
32
+
33
+ # For more information and examples about making a new gem, check out our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
@@ -1,4 +1,4 @@
1
- module Reenrb
1
+ module Reen
2
2
  VERSION: String
3
3
  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reen
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Soumya Ray
@@ -21,25 +21,26 @@ files:
21
21
  - ".markdownlint.json"
22
22
  - ".rubocop.yml"
23
23
  - CHANGELOG.md
24
- - CLAUDE.feature-numbered-lists.md
25
24
  - Gemfile
26
25
  - LICENSE.txt
27
26
  - README.md
28
27
  - Rakefile
29
28
  - bin/reen
30
29
  - lib/reen.rb
31
- - lib/reen/actions/delete.rb
32
- - lib/reen/actions/force_delete.rb
33
- - lib/reen/actions/nothing.rb
34
- - lib/reen/actions/rename.rb
35
- - lib/reen/change.rb
36
- - lib/reen/changes.rb
37
- - lib/reen/changes_file.rb
38
- - lib/reen/path_entry.rb
39
- - lib/reen/path_entry_list.rb
40
- - lib/reen/reen.rb
30
+ - lib/reen/application/reen_cli.rb
31
+ - lib/reen/application/renamer.rb
32
+ - lib/reen/domain/change.rb
33
+ - lib/reen/domain/changes.rb
34
+ - lib/reen/domain/path_entry.rb
35
+ - lib/reen/domain/path_entry_list.rb
36
+ - lib/reen/infrastructure/actions/delete.rb
37
+ - lib/reen/infrastructure/actions/force_delete.rb
38
+ - lib/reen/infrastructure/actions/nothing.rb
39
+ - lib/reen/infrastructure/actions/rename.rb
40
+ - lib/reen/infrastructure/changes_file.rb
41
41
  - lib/reen/version.rb
42
- - sig/reenrb.rbs
42
+ - reen.gemspec
43
+ - sig/reen.rbs
43
44
  homepage: https://github.com/soumyaray/reen
44
45
  licenses:
45
46
  - MIT
@@ -1,145 +0,0 @@
1
- # Numbered List Prefixes for Editor
2
-
3
- > **IMPORTANT**: This plan must be kept up-to-date at all times. Assume context can be cleared at any time — this file is the single source of truth for the current state of this work. Update this plan before and after task and subtask implementations.
4
-
5
- ## Branch
6
-
7
- `feature-numbered-lists`
8
-
9
- ## Goal
10
-
11
- Add numbered prefixes (e.g., `[01]`, `[002]`) to the file list shown in the editor so users can freely reorder entries without losing track of which original file each line corresponds to. Matching changes back to originals uses the number, not line position. Also fix the dash-operator ambiguity by requiring a space after `-`/`--` to distinguish delete operations from literal filenames starting with dashes. Introduce `PathEntry` and `PathEntryList` domain objects to replace raw path strings — `PathEntry` wraps a file path and answers filesystem queries, `PathEntryList` is a collection that owns numbered serialization for the editor.
12
-
13
- ## Strategy: Vertical Slice
14
-
15
- 1. **Refactor** — Introduce `PathEntry`/`PathEntryList` domain objects, migrate existing code (all tests pass)
16
- 2. **Fix** — Require space after `-`/`--` for delete operators (dash ambiguity fix)
17
- 3. **Feature** — Add numbered prefixes and number-based matching
18
- 4. **Verify** — Manual test with real editor confirms behavior
19
-
20
- ## Current State
21
-
22
- - [x] Plan created
23
- - [x] Slice 0: PathEntry/PathEntryList refactor
24
- - [x] Slice 1: Dash-operator ambiguity fix
25
- - [x] Slice 2: Numbered prefixes
26
- - [x] Slice 3: Number-based matching
27
- - [x] Slice 4: Documentation
28
- - [ ] Slice 5: Verification
29
-
30
- ## Key Findings
31
-
32
- - `Change` currently owns filesystem queries (`request_dir?`, `request_file?`, `request_empty_dir?`, `request_full_dir?`) by checking `@original` path directly. These should move to `PathEntry`.
33
- - `Change` will hold an `PathEntry` instead of a raw `@original` string. `Change#original` delegates to `PathEntry#path`.
34
- - `PathEntryList` wraps an array of `PathEntry` objects. Constructed from `Dir.glob` results (array of strings). Owns numbered serialization (writing `[NN] path` lines) and parsing (stripping prefixes, returning number-keyed entries).
35
- - `ChangesFile#initialize` currently writes `requested_list.join("\n")` — will instead take an `PathEntryList` and call its serialization.
36
- - `ChangesFile#allow_changes` reads back lines, strips comments/blanks — will delegate prefix parsing to `PathEntryList`.
37
- - `Reen#request` does positional matching via `original_list.zip(changed_list)` — changes to number-based matching via `PathEntryList`.
38
- - `Reen#request` raises if list sizes differ (line deletion guard) — still valid with numbered approach.
39
- - `Change#extract_request` parses operator prefixes (`-`, `--`) greedily, which means renaming to `-` or `--` is impossible. Fix: require a space after `-`/`--` to trigger delete.
40
- - Tests manipulate `file.list` directly (mock editor) — the list exposed to the block will include number prefixes so test blocks simulate what a real user sees/edits.
41
- - Number width is auto-sized: `format("%0#{width}d")` where `width = list.size.to_s.length`.
42
-
43
- ## Questions
44
-
45
- - ~~Should `Change` know about number prefixes?~~ No — `ChangesFile` handles writing and stripping prefixes. `Change` and `Reen` see clean filenames. `Reen#compare_lists` changes from positional zip to number-keyed matching.
46
- - ~~Should dashes go before or after number prefixes?~~ After. Dashes are operators on the filename, numbers are stable anchors: `[01] - file` = delete, `[01] -file` = rename to `-file`. Require space after `-`/`--` to distinguish.
47
- - [ ] Should reordering be reflected in the output summary? (Probably not needed for v1.)
48
-
49
- ## Scope
50
-
51
- **In scope**:
52
-
53
- - `PathEntry` (`lib/reenrb/path_entry.rb`): wraps a file path, provides `file?`, `dir?`, `empty_dir?`, `full_dir?`, `path`
54
- - `PathEntryList` (`lib/reenrb/path_entry_list.rb`): collection of `PathEntry`, constructed from string array, owns numbered serialization/parsing
55
- - `Change`: refactor to hold `PathEntry` instead of raw string, delegate filesystem queries to `PathEntry`
56
- - `Change#extract_request`: require space after `-`/`--` for delete operators (fixes dash-name ambiguity)
57
- - `ChangesFile`: use `PathEntryList` for writing/reading, numbered prefixes, updated instructions
58
- - `Reen`: accept `PathEntryList` (or build one from string array), number-based matching
59
- - Tests for: `PathEntry`, `PathEntryList`, dash-space fix, prefix formatting/stripping, reordered matching
60
-
61
- **Out of scope**:
62
-
63
- - Reporting reorder information to the user
64
- - Any CLI flag to disable numbering
65
- - `Changes` refactor (stays as-is, wraps array of `Change`)
66
- - File/folder creation via unnumbered lines (touch/mkdir) — future feature, separate branch
67
-
68
- ## Tasks
69
-
70
- > **Strict TDD cycle**: For each slice — (1) run full suite to confirm green baseline, (2) write failing tests (RED), (3) run tests to confirm they fail, (4) implement (GREEN), (5) run full suite to confirm all pass. **Check off each step immediately after completing it.**
71
-
72
- ### Slice 0: Introduce PathEntry and PathEntryList domain objects (refactor)
73
-
74
- - [x] 0.0 Run full suite — confirm green baseline (11 tests, 0 failures)
75
- - [x] 0.1 RED — Write failing tests in `spec/spec_path_entry.rb`:
76
- - [x] 0.1a `PathEntry` wraps a path and delegates `file?`, `dir?`, `empty_dir?`, `full_dir?`
77
- - [x] 0.1b `PathEntryList` constructs from array of strings, iterates as `PathEntry` objects
78
- - [x] 0.1c `PathEntryList#paths` returns array of path strings
79
- - [x] 0.2 Run new tests — confirm RED (6 errors, uninitialized constant)
80
- - [x] 0.3 GREEN — Implement `PathEntry` (`lib/reenrb/path_entry.rb`)
81
- - [x] 0.4 GREEN — Implement `PathEntryList` (`lib/reenrb/path_entry_list.rb`)
82
- - [x] 0.5 Run new tests — confirm GREEN (6 runs, 27 assertions, 0 failures)
83
- - [x] 0.6 Refactor `Change` to hold `PathEntry`, delegate filesystem queries
84
- - [x] 0.7 Refactor `Reen` and `ChangesFile` to accept/use `PathEntryList`
85
- - [x] 0.8 Run full suite — confirm all tests pass (17 runs, 58 assertions, 0 failures)
86
-
87
- ### Slice 1: Fix dash-operator ambiguity in Change
88
-
89
- - [x] 1.0 Run full suite — confirm green baseline (17 runs, 0 failures)
90
- - [x] 1.1 RED — Write failing tests in `spec/spec_change.rb`:
91
- - [x] 1.1a `- filename` (with space) triggers delete
92
- - [x] 1.1b `-filename` (no space) triggers rename to `-filename`
93
- - [x] 1.1c `-- filename` (with space) triggers force delete
94
- - [x] 1.1d `--filename` (no space) triggers rename to `--filename`
95
- - [x] 1.2 Run new tests — confirm RED (1.1b and 1.1d fail as expected)
96
- - [x] 1.3 GREEN — Updated `Change#extract_request` regex: `^(--|-)·(?<name>.*)` with fallback `^()(?<name>.*)`
97
- - [x] 1.4 Run full suite — confirm GREEN (21 runs, 64 assertions, 0 failures)
98
-
99
- ### Slice 2: Numbered prefixes in PathEntryList and ChangesFile
100
-
101
- - [x] 2.0 Run full suite — confirm green baseline (21 runs, 0 failures)
102
- - [x] 2.1 RED — Write failing tests:
103
- - [x] 2.1a `PathEntryList#to_numbered` serializes as `[NN] path` lines (auto-sized width)
104
- - [x] 2.1b `PathEntryList.from_numbered` parses `[NN] path` lines back to number-keyed entries
105
- - [x] 2.1c (deferred to Slice 4 — documentation)
106
- - [x] 2.2 Run new tests — confirm RED (3 errors, undefined method `to_numbered`)
107
- - [x] 2.3 GREEN — Implement `PathEntryList#to_numbered` and `PathEntryList.from_numbered`
108
- - [x] 2.4 GREEN — Update `ChangesFile` to use numbered serialization
109
- - [x] 2.5 GREEN — Update `INSTRUCTIONS` constant with reorder/number/dash-space guidance
110
- - [x] 2.6 Run full suite — deferred to Slice 3 (existing tests broke as expected, fixed there)
111
-
112
- ### Slice 3: Number-based matching in Reen
113
-
114
- - [x] 3.0 (combined with Slice 2 — baseline was 21 runs before numbered output broke existing tests)
115
- - [x] 3.1 RED — Write failing tests in `spec/spec_reorder.rb`:
116
- - [x] 3.1a `Reen#request` correctly matches reordered entries to originals by number (reverse, rename+move)
117
- - [x] 3.1b `Reen#request` still raises error when lines are removed
118
- - [x] 3.2 Run new tests — confirm RED (3.1a failed, 3.1b passed)
119
- - [x] 3.3 GREEN — Changed `Reen#compare_lists` to use `PathEntryList.from_numbered` for number-keyed matching
120
- - [x] 3.4 GREEN — Updated existing tests: delete/force-delete use `.sub("] ", "] - ")` pattern
121
- - [x] 3.5 Run full suite — confirm all pass (27 runs, 110 assertions, 0 failures)
122
-
123
- ### Slice 4: Documentation
124
-
125
- - [x] 4.1 Update README.md: documented numbered prefixes, dash-space syntax, reordering, programmatic examples
126
- - [x] 4.2 CLI help in `bin/reen` — no changes needed (uses banner/options only, editing syntax is in INSTRUCTIONS)
127
- - [x] 4.3 `ChangesFile::INSTRUCTIONS` — updated in Slice 2.5, verified
128
-
129
- ### Slice 5: Verification
130
-
131
- - [x] 5.1 Run full suite (`bundle exec rake`) — 29 tests, 115 assertions, 0 failures. RuboCop: only pre-existing offenses, no new ones.
132
- - [ ] 5.2 Manual verification with real editor (user to perform)
133
-
134
- ## Completed
135
-
136
- - Slice 0: PathEntry/PathEntryList refactor — `PathEntry` wraps paths with filesystem queries, `PathEntryList` is an Enumerable collection. `Change` delegates to `PathEntry`. `Reen` and `ChangesFile` accept `PathEntryList`. All 17 tests pass.
137
- - Slice 1: Dash-operator ambiguity fix — `Change#extract_request` now requires space after `-`/`--` for delete operators. `-file` is rename to `-file`, `- file` is delete. All 21 tests pass.
138
- - Slice 2: Numbered prefixes — `PathEntryList#to_numbered` and `.from_numbered` implemented. `ChangesFile` writes `[NN] path` lines and updated `INSTRUCTIONS`.
139
- - Slice 3: Number-based matching — `Reen#compare_lists` uses `from_numbered` for number-keyed matching instead of positional zip. Reordering works. All 27 tests pass (110 assertions).
140
- - Slice 4: Documentation — README updated with numbered prefixes, dash-space syntax, reordering docs, programmatic examples. CLI help unchanged (banner only). INSTRUCTIONS verified.
141
- - Slice 5: Verification — 29 tests, 115 assertions, 0 failures. No new RuboCop offenses. Manual editor test pending.
142
-
143
- ---
144
-
145
- Last updated: 2026-03-10
data/lib/reen/reen.rb DELETED
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Reen
4
- # Renames pattern of files with given editor
5
- # Examples:
6
- # Reen::Reen.new(editor: "code -w").call("spec/fixtures/example/*")
7
- # Reen::Reen.new(editor: nil).call("spec/fixtures/example/*") { ... }
8
- class Reen
9
- DEL_ERROR = "Do not remove any file/folder names (no changes made)"
10
-
11
- attr_reader :changes
12
-
13
- def initialize(editor: "emacs", options: {})
14
- @editor = editor
15
- @options = options
16
- end
17
-
18
- def request(original_list, &block)
19
- @entry_list = PathEntryList.new(original_list)
20
- changed_list = ChangesFile.new(@entry_list).allow_changes(@editor, &block)
21
-
22
- raise(Error, DEL_ERROR) if changed_list.size != @entry_list.count
23
-
24
- @changes = compare_lists(@entry_list, changed_list)
25
- .then { |change_array| Changes.new(change_array) }
26
- end
27
-
28
- def execute(original_list, &block)
29
- @changes ||= request(original_list, &block)
30
- @changes = @changes.execute_all
31
- end
32
-
33
- private
34
-
35
- def compare_lists(entry_list, changed_list)
36
- changed_by_number = PathEntryList.from_numbered(changed_list)
37
- entries = entry_list.to_a
38
-
39
- entries.each_with_index.map do |entry, i|
40
- number = i + 1
41
- revised = changed_by_number[number]
42
- Change.new(entry, revised)
43
- end
44
- end
45
- end
46
- end
File without changes