scryglass 1.1.0 → 2.0.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: 75f5dd1ffed556a9b5d06dc18925e3c728bb2e49742fee1d7b5705c5f8be0aac
4
- data.tar.gz: 3076092cf60d7359fc51552199584af40b23e1a71c4291ee8853738a75bc1c8d
3
+ metadata.gz: 1b02a7943a8abec8175e8585b55127b197fdb7543c9015a2fbef908f29c99fa3
4
+ data.tar.gz: 1bdc489ea5afe33ddc5ffea4cc5fed26980fb158815d483d4a0f8c56a808cf75
5
5
  SHA512:
6
- metadata.gz: cd72f233a5afe293c135ab3ed638d147397dc3400bcd37a7dc6b724f125af7484a8036ee9f1bd0fb492025ff30ee06a9c04cddb92c954912a33d7a17aa71b9f4
7
- data.tar.gz: 3fc0b51496476cf08bc9ef79573198133265a15eeac5cac1fb573e27767aa788f1901be0887dec7dcf1341e8eedd1c0d45c12f3ddf9ba5a60f662a2b94ee0e7d
6
+ metadata.gz: 3a79a7e60525eb2c99a15d581c09c1f13e92b26dd7f4330c0c24ba974012a6a6b2e4605d6e45bec00ab0099dc84eae28e37007bca43bc105d23f278a92d40dcc
7
+ data.tar.gz: fab64fe8ad6d75a1e3d46753be7868ae40107fc9d5a1299291b6d4b03988cb612ce05087ccb35932b31cf75aeb9e20966392f4e34ae20f4a75962b882ff83072
data/.irbrc ADDED
@@ -0,0 +1,9 @@
1
+ Scryglass.load
2
+
3
+ def th
4
+ Scryglass.test_hash
5
+ end
6
+
7
+ def dh
8
+ Scryglass.demo_hash
9
+ end
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## Added
11
+
12
+ ## Changed
13
+
14
+ ## Fixed
15
+
16
+ ## [2.0.0] - 2021-01-13
17
+
18
+ ## Added
19
+
20
+ - Turned on ANSI formatted/colored support with AnsiSliceStringRefinement.
21
+ - Added 'AmazingPrint' lens (colored) and gem.
22
+ - Added color formatting to (beta) method_showcase_for.
23
+ - Added "Smart Open" command 'o', which attempts to create sub-rows of the (next) most helpful type.
24
+ - README and help screen now point out that holding SHIFT will increase up/down step distance.
25
+ - Bottom and right edges of screen now indicate, with dots, when there is more beyond the view's edge.
26
+ - Added the VIM home row keybindings `h`/`j`/`k`/`l` as optional arrow keys.
27
+ - Now if the scry session hits an error, it will first ensure the error and console prompt appear below the present screen.
28
+ - Can now press `=` to give a console instance variable name to current objects without leaving scry session.
29
+ - Added popup messages for when the user attempts to create sub-rows for the current row and no sub-items are found.
30
+ - Added tab functionality to manage multiple scry session tabs for easy reference and comparison.
31
+ - Scryglass version now shows up in top right corner of the tree view when the header has no values to track.
32
+ - Improved and enabled 'Method Showcase' lens by default.
33
+ - Added `[<]`/`[>]` key reminders to Lens View.
34
+
35
+ ## Changed
36
+
37
+ - Changed user_signal timeout period from 0.1sec to 0.3sec to reduce number of coincidentally dropped inputs.
38
+ - Changed AnsiSliceStringRefinement syntax even closer to 'string'[args] (supporting [i, l] syntax).
39
+ - Changed the keys for switching subject type and lens from `L`/`l` to `<`/`>` (To make room for vim h/j/k/l keybindings)
40
+ - Expanded list of "Patient Actions" which won't beep even if that procedure (sometimes user input) took longer than 4 seconds.
41
+ - Improved popup messages QOL (they now stack properly and don't make the user wait for them to disappear).
42
+ - Removed `scry_resume` command; bare `scry` now always resumes last session even if the current console receiver isn't `main`.
43
+ - Made help screen key text blue.
44
+
45
+ ## Fixed
46
+
47
+ - Some more fixes to support (BETA) method_showcase_for:
48
+ - Added method_source gem.
49
+ - Now requiring lens_helper.
50
+ - Changed method to be callable externally
51
+ - Extra view margin no longer producible at far end of ANSI strings
52
+ - Escape true newlines returned by objects with unexpected `.inspect` results, which otherwise messes up the display.
53
+ - Cursor indicators ( `(`/`@`/`·` ) can no longer stall or error on exceptional objects; they now show up as `X` if they error or take too long (0.05s).
54
+ - When quitting from the help screen, cursor and prompt are now set all the way at the bottom of the display, rather than where the content ends on the current *non-help* panel.
55
+
10
56
  ## [1.1.0] - 2020-09-21
11
57
 
12
58
  ## Added
@@ -1,11 +1,27 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- scryglass (1.1.0)
4
+ scryglass (2.0.0)
5
+ amazing_print
6
+ binding_of_caller
7
+ method_source
5
8
 
6
9
  GEM
7
10
  remote: https://rubygems.org/
8
11
  specs:
12
+ amazing_print (1.2.1)
13
+ binding_of_caller (0.8.0)
14
+ debug_inspector (>= 0.0.1)
15
+ coderay (1.1.3)
16
+ debug_inspector (0.0.3)
17
+ interception (0.5)
18
+ method_source (1.0.0)
19
+ pry (0.13.1)
20
+ coderay (~> 1.1)
21
+ method_source (~> 1.0)
22
+ pry-rescue (1.5.2)
23
+ interception (>= 0.5)
24
+ pry (>= 0.12.0)
9
25
  rake (12.3.3)
10
26
 
11
27
  PLATFORMS
@@ -13,6 +29,7 @@ PLATFORMS
13
29
 
14
30
  DEPENDENCIES
15
31
  bundler (~> 2.1)
32
+ pry-rescue
16
33
  rake (~> 12.0)
17
34
  scryglass!
18
35
 
data/README.md CHANGED
@@ -87,9 +87,6 @@ To start a Scry Session, call:
87
87
  **A note about passing an argument without parentheses:**
88
88
  > The arg syntax (`scry my_object`) will get confused if it's given a hash direcly (`scry {a: [1, 2] }`), thinking you're trying to pass a block, unless you use parentheses (`scry({a: [1, 2] })`).
89
89
 
90
- **A note about using the resume session command, while in a pry:**
91
- > The straight resume command, the bare `scry`, relies on the assumption that the method receiver is `main`. When your console is a pry in some other code, `self` is no longer `main`, but some other object, and so you are actually calling `scry` on that, overwriting your previous session. If you still want to resume session in that context, you can use `scry_resume`.
92
-
93
90
  ## Basic Usage
94
91
 
95
92
  Use the arrow keys to move around and open/close known Enumerable types! Hit `'?'` to view all the controls and learn how to do much much more.
@@ -130,40 +127,46 @@ The cursor, movable by arrow keys, is represented by a straight line (`–––
130
127
  | `––––` | ...no secret contents |
131
128
  | `(–––` | ...a non-empty Enumerable of an unknown type (openable with `'('`) |
132
129
  | `–@––` | ...instance variables on the object (openable with `'@'`) |
133
- | `(@––` | ...both! ***Generally* instance variables yield more sub-items with more info.** |
130
+ | `––·–` | ...ActiveRecord associations (openable with `'.'`) |
131
+ | `(@·–` | ...all three! (Note: ***Generally* IVs yield more sub-items with more info than using `'('`).** |
134
132
 
135
- ActiveRecord objects are no secret; you can press `'.'` on them to build their AR Association sub-items.
133
+ An `X` in place of any of these characters indicates an error or a timeout (if the "counting" process takes longer than 0.05 seconds)
136
134
 
137
135
  A single `•` will mark the presence of user-added rows when they are hidden.
138
136
 
139
137
  ### Waiting!
140
138
  Scryglass has two features to make wait time a little easier:
141
- - If any process takes longer than 4 seconds between you pressing a key and the process completing, it **makes a beep sound!** This means if something seems like it might take a bit, you can switch to another tab or window without worry, and it will tell you when to check back.
139
+ - If any process (with a couple exceptions for user input) takes longer than 4 seconds between you pressing a key and the process completing, it **makes a beep sound!** This means if something seems like it might take a bit, you can switch to another tab or window without worry, and it will tell you when to check back.
142
140
  - While there are no time estimates (for a number of reasons), many subprocesses are linked to a **progress bar**, which will display at the bottom of the screen. If multiple nested processes are running one within another, the progress bar will divide itself into parts to show each process. The leftmost bar is the base level iteration task.
143
141
 
144
142
  ## In-Depth Control Rundown
145
143
 
146
- | Key | Help Screen Snippet | Verbose Description |
144
+ | Key | Help_Screen_Snippet | Verbose Description |
147
145
  |:---:|---------------------|---------------------|
148
146
  | `?` | Press '?' for controls | `?` will cycle through the help panels, then back to the scry session. |
149
147
  | `q` | Quit Scry | Exits the scry session, returning nil. The cursor (and exit message) is then placed below the last line console line with content in order to take up no more space than needed. |
150
- | `UP`/`DOWN` | Navigate (You can type a number first) | Moves the cursor one step upward or downward in the tree view (this can be done while in lens view). If a number (of any number of digits) is typed out before pressing `UP` or `DOWN`, then the cursor will move that many steps in that direction. If the number of steps goes past the edge of the list, the cursor will sit safely at that edge. |
151
- | `RIGHT` | Expand current or selected row(s) | If any rows are *selected* this attempts to expand all of them, and will expand the ones it can. If none are selected, then it will attempt to expand the current row where the cursor is. If the current row has preexisting sub-items, but they are hidden because the current ro is collapsed, this will reveal them in the tree view. |
148
+ | `UP`/`DOWN` | Navigate (To move further, type a number first or use SHIFT) | Moves the cursor one step upward or downward in the tree view (this can be done while in lens view). If a number (of any number of digits) is typed out before pressing `UP` or `DOWN`, then the cursor will move that many steps in that direction. If `SHIFT` is held while pressing, then the cursor will move 12 steps in that direction. If the number of steps goes past the edge of the list, the cursor will sit safely at that edge. |
149
+ | `RIGHT` | Expand current or selected row(s) | If any rows are *selected* this attempts to expand all of them, and will expand the ones it can. If none are selected, then it will attempt to expand the current row where the cursor is. If the current row has preexisting sub-items, but they are hidden because the current row is collapsed, this will reveal them in the tree view. |
152
150
  | `LEFT` | Collapse current or selected row(s) | If any rows are *selected* this attempts to collapse all of them, and will collapse the ones it can. If none are selected, then it will attempt to collapse the current row where the cursor is. If the current row either has no sub-items or is already collapsed, this action will collapse its parent row instead and place the cursor there. |
151
+ | `h`/`j`/`k`/`l` | (These keys on the home row can also serve as arrow keys) | This is for those familiar with VIM keybindings! Shift speeds up k/j to 12 steps just the same as UP/DOWN. |
153
152
  | `ENTER` | Close Scry, returning current or selected object(s) (Key or Value) | Returns the subject object (based on current subject type, :value or :key) of the current item, or, if any items are selected (`->`), it returns all of those in an array. The order matches the order in which they were marked as selected. In the case of \| and \*, the order of the array will be top to bottom. If the current Subject Type (toggled by `L`) is :key, rows without "keys" will return `nil`. |
154
153
  | `SPACEBAR` | Toggle Lens View | Switches between Tree View and Lens View. This will not change the view position of the lens view, but the view position of the tree view will still follow the cursor if the cursor moves while in lens view. |
155
- | `l` | Cycle through lens types | Cycles through the different lens types in the lens view. These all take the current row, at either the "key" or the "value" object depending on the current subject type (toggled by `L`), and display a string of it, transformed through that particular lens. New lenses can be written in the config. |
156
- | `L` | Toggle subject (Key/Value of row) | This change is only perceptible in the lens view, but does also change which objects are returned by `ENTER`. Any objects without "keys" will return nil for their :key if they don't have one. |
157
- | `w`/`a`/`s`/`d` | Move view window (ALT increases speed) | The W/A/S/D keys form a second set of arrow keys for moving around the "screen" through which you view the tree view and the lens view, when the contents don't all fit on the screen at once. They move 5 cells in the specificied direction, or 50 if ALT is held before pressing. Can be held down for continuous movement. |
154
+ | `>` | Cycle through lens types | Cycles through the different lens types in the lens view. These all take the current row, at either the "key" or the "value" object depending on the current subject type (toggled by `L`), and display a string of it, transformed through that particular lens. New lenses can be written in the config. |
155
+ | `<` | Toggle subject (Key/Value of row) | This change is only perceptible in the lens view, but does also change which objects are returned by `ENTER`. Any objects without "keys" will return nil for their :key if they don't have one. |
156
+ | `w`/`a`/`s`/`d` | Move view window (ALT increases speed) | The W/A/S/D keys form a second set of arrow keys for moving around the "screen" through which you view the tree view and the lens view, when the contents don't all fit on the screen at once. They move 5 cells in the specified direction, or 50 if ALT is held before pressing. Can be held down for continuous movement. |
158
157
  | `0` | Reset view location (Press again: reset cursor) | This resets/zeros the current view (tree or lens). If you are in the tree view, and the view is in the zero (top left) position, then this will instead move the cursor there. |
159
158
  | `@` | Build instance variable sub-rows for current or selected row(s) | Identifies all instance variables on the object (or value of a key-value pair) of the current or selected rows. Then these instance variables are turned into a list of keys, called on the original object, and then paired with the resulting objects. Known Enumerables are recursively navigable as always. |
160
- | `.` | Build ActiveRecord association sub-rows for current or selected row(s) | If the `ActiveRecord` constant is not defined by the system, this will do nothing. If it is, this will navigate the reflections of the the class of the object (or value of a key-value pair) in order to find its AR Associations and turn them into key-value sub-items. Note: With the default configuration, the way it uses reflections *purposefully ignores `:through` relations and `scope`d relations (e.g. the extraneous `CURRENT_phone_numbers`).* |
159
+ | `.` | Build ActiveRecord association sub-rows for current or selected row(s) | If the `ActiveRecord` constant is not defined by the system, this will do nothing. If it is, this will navigate the reflections of the the class of the object (or value of a key-value pair) in order to find its AR Associations and turn them into key-value sub-items. Note: With the default configuration, the way it uses reflections *purposefully ignores `:through` relations and `scope`d relations (e.g. the extraneous `CURRENT_phone_numbers`).* Note: The `·` cursor indicator does not take the time to traverse all reflections, nor account for all configured filters. It's possible that `.` will produce no sub-rows despite the indicator. |
161
160
  | `(` | Attempt to smart-build sub-rows for current or selected row(s), if Enumerable. Usually '@' is preferable | Attempts a "smart reading" of the object (or value of a key-value pair) of the current or selected rows. If the object is an Enumerable, it will attempt to parse it into sub-items. if the object has keys, it will be parsed as key-value pairs like a hash, otherwise singular objects like an array. This can sometimes create sub-items that would otherwise be more neatly accesible under a single instance variable if instance variables are built instead, so default to trying that first. |
161
+ | `o` | Quick Open: builds the most likely helpful sub-rows ( '.' \|\| '@' \|\| '(' ) | "Quick Open" first tries opening AR Associations. If none are produced, it tries for instance variables. If none are produced, it attempts to open as it would an unknown Enumerable type. Can be pressed repeatedly to produce all types of sub-rows. |
162
162
  | `*` | Select/Deselect ALL rows | This includes hidden rows. If all rows are already selected, they will be unselected, regardless of how they became selected. |
163
163
  | `\|` | Select/Deselect every sibling row under the same parent row | If all siblings under that parent row are already selected, they will be unselected, regardless of how they became selected. |
164
164
  | `-` | Select/Deselect current row | If these objects are later returned, the order in which they were selected will determine their order in the returned array. |
165
+ | `Tab` | Change session tab (to the right) (`Shift`+`Tab` moves left) | Changes which scry session is the current one, and brings up the tab bar for a couple seconds. |
166
+ | `Q` | Close current session tab | Permanently exits the current session tab, but keeps scry running if another tab remains. Switches one tab to left if there is one.
165
167
  | `/` | Begin a text search (in tree view) | Begins a case-sensitive regex search of all items, in a loop, starting with just below the current row. For a matching object to be found, the search must match its *truncated sample string in tree view* (regardless of what is on or off screen) (it must match either the key or the value, not the full line they create) (known enumerable types, like `[•••]`, may count as a match if they contain the string in the backend). |
166
168
  | `n` | Move to next search result | Will, using the most recent search entry, move the cursor on to the next match downward, cycling through all rows. This follows the same matching rules as the original search. |
169
+ | `=` | Open prompt to type a console handle for current or selected row(s) | Gets text from user (not including the '@') and saves the current subject, or array of selected row(s) subjects, under that instance variable name. These variables live in the console itself: the binding of wherever `scry` was called. Care is taken not to allow them to conflict with preexisting IV names or method names. Note: But still, if you are prying inside an object/context, your IVs will be defined on that object. Note: If you switch from one pry context to another and then back, your first pry's instance variables will be there despite not being listed in the IV outro message. |
167
170
  | `Esc` | Resets selection, last search, and number-to-move. (or returns to Tree View) | (Essentially, clears the values represented in the Tree View header if you're in the Tree View; otherwise it returns you to the Tree View) |
168
171
 
169
172
  ## Configuration / Customization (all optional)
@@ -190,6 +193,8 @@ Scryglass.configure do |config|
190
193
  ## UX
191
194
  # config.cursor_tracking = [:flexible_range, :dead_center][0] # Default: [0]
192
195
  # config.lenses = [ # Custom lenses can easily be added as name+lambda hashes! Or comment some out to turn them off.
196
+ # { name: 'Amazing Print (`ap`)',
197
+ # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { ap o } } }, # This has colors!
193
198
  # { name: 'Pretty Print (`pp`)',
194
199
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { pp o } } },
195
200
  # { name: 'Inspect (`.inspect`)',
@@ -198,10 +203,18 @@ Scryglass.configure do |config|
198
203
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { require 'yaml' ; y o } } }, # OR: `puts o.to_yaml`
199
204
  # { name: 'Puts (`puts`)',
200
205
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { puts o } } },
201
- # # { name: 'Method Showcase', # Not included by default
202
- # # lambda: ->(o) { Scryglass::LensHelper.method_showcase_for(o) } },
206
+ # { name: 'Method Showcase',
207
+ # lambda: ->(o) { Scryglass::LensHelper.method_showcase_for(o) } },
203
208
  # ]
204
209
 
210
+ ## AmazingPrint defaults, if the user has not set their own:
211
+ # AmazingPrint.defaults ||= {
212
+ # index: false, # (Don't display array indices).
213
+ # raw: true, # (Recursively format instance variables).
214
+ # }
215
+ # See https://github.com/amazing-print/amazing_print
216
+
217
+
205
218
  ## Building ActiveRecord association sub-rows:
206
219
  # config.include_empty_associations = true # Default: true
207
220
  # config.include_through_associations = false # Default: false
@@ -12,16 +12,26 @@ Scryglass.configure do |config|
12
12
  # config.lenses = [ # Custom lenses can easily be added as name+lambda hashes! Or comment some out to turn them off.
13
13
  # { name: 'Pretty Print (`pp`)',
14
14
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { pp o } } },
15
+ # { name: 'Amazing Print (`ap`)',
16
+ # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { ap o } } }, # This has colors!
15
17
  # { name: 'Inspect (`.inspect`)',
16
18
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { puts o.inspect } } },
17
19
  # { name: 'Yaml Print (`y`)',
18
20
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { require 'yaml' ; y o } } }, # OR: `puts o.to_yaml`
19
21
  # { name: 'Puts (`puts`)',
20
22
  # lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { puts o } } },
21
- # # { name: 'Method Showcase', # Not included by default
22
- # # lambda: ->(o) { Scryglass::LensHelper.method_showcase_for(o) } },
23
+ # { name: 'Method Showcase',
24
+ # lambda: ->(o) { Scryglass::LensHelper.method_showcase_for(o) } },
23
25
  # ]
24
26
 
27
+ ## AmazingPrint defaults, if the user has not set their own:
28
+ # AmazingPrint.defaults ||= {
29
+ # index: false, # (Don't display array indices).
30
+ # raw: true, # (Recursively format instance variables).
31
+ # }
32
+ # See https://github.com/amazing-print/amazing_print
33
+
34
+
25
35
  ## Building ActiveRecord association sub-rows:
26
36
  # config.include_empty_associations = true # Default: true
27
37
  # config.include_through_associations = false # Default: false
@@ -7,5 +7,150 @@ module AnsilessStringRefinement
7
7
  def ansiless_length
8
8
  ansiless.length
9
9
  end
10
+
11
+ ## Splits string into characters, with each ANSI escape code being its own
12
+ ## grouped item, like so:
13
+ ## irb> "PLAIN\e[32mCOLOR\e[0mPLAIN".ansi_string_breakout
14
+ ## => ["P", "L", "A", "I", "N", "\e[32m", "C", "O", "L", "O", "R",
15
+ ## "\e[0m", "P", "L", "A", "I", "N"]
16
+ def ansi_string_breakout
17
+ breakout_array = []
18
+ working_self = self.dup
19
+
20
+ while working_self[0]
21
+ if (working_self =~ /\e\[[\d\;]*m/) == 0 # if begins with
22
+ end_of_escape_code = (working_self.index('m'))
23
+ leading_escape_code = working_self[0..end_of_escape_code]
24
+ breakout_array << leading_escape_code
25
+ working_self = working_self[(end_of_escape_code + 1)..-1]
26
+ else
27
+ leading_character = working_self[0]
28
+ breakout_array << leading_character
29
+ working_self = working_self[1..-1]
30
+ end
31
+ end
32
+
33
+ breakout_array
34
+ end
35
+
36
+ def is_ansi_escape_code?
37
+ (self =~ /^\e\[[\d\;]*m$/) == 0
38
+ end
39
+
40
+ ## Returns the indexed character of the real string, determined by the index
41
+ ## given in reference to its ANSIless display form. e.g.:
42
+ ## > s = "\e[31mTEST\e[00m"
43
+ ## > puts s
44
+ ## TEST
45
+ ## > s.ansiless_pick(1) = 'y'
46
+ ## > s
47
+ ## => "\e[31mTyST\e[00m"
48
+ def ansiless_pick(given_index)
49
+ ansiless_self = self.ansiless
50
+
51
+ return nil if ansiless_self[given_index].nil?
52
+
53
+ given_index = (given_index + ansiless_self.length) if given_index.negative?
54
+
55
+ mock_index = 0 # A scanning index that *doesn't* count ANSI codes
56
+ real_index = 0 # A scanning index that *does* count ANSI codes
57
+
58
+ ansi_string_array = ansi_string_breakout
59
+
60
+ until mock_index == given_index
61
+ while ansi_string_array.first.is_ansi_escape_code?
62
+ real_index += (ansi_string_array.shift.length)
63
+ end
64
+
65
+ ansi_string_array.shift
66
+
67
+ while ansi_string_array.first.is_ansi_escape_code?
68
+ real_index += (ansi_string_array.shift.length)
69
+ end
70
+
71
+ mock_index += 1
72
+ real_index += 1
73
+ end
74
+
75
+ self[real_index]
76
+ end
77
+
78
+ ## Like ansiless_pick, but it can set that found string character instead
79
+ def ansiless_set!(given_index, string)
80
+ raise ArgumentError, 'First argument must be an Integer' unless given_index.is_a?(Integer)
81
+ raise ArgumentError, 'Second argument must be a String' unless string.is_a?(String)
82
+
83
+ ansiless_self = self.ansiless
84
+
85
+ return nil if ansiless_self[given_index].nil?
86
+
87
+ given_index = (given_index + ansiless_self.length) if given_index.negative?
88
+
89
+
90
+ new_string = string.to_s
91
+
92
+ mock_index = 0 # A scanning index that *doesn't* count ANSI codes
93
+ real_index = 0 # A scanning index that *does* count ANSI codes
94
+
95
+ ansi_string_array = ansi_string_breakout
96
+
97
+ until mock_index == given_index
98
+ while ansi_string_array.first.is_ansi_escape_code?
99
+ real_index += (ansi_string_array.shift.length)
100
+ end
101
+
102
+ ansi_string_array.shift
103
+
104
+ while ansi_string_array.first.is_ansi_escape_code?
105
+ real_index += (ansi_string_array.shift.length)
106
+ end
107
+
108
+ mock_index += 1
109
+ real_index += 1
110
+ end
111
+
112
+ self[real_index] = new_string
113
+ end
114
+
115
+ def ansi_slice(arg1, arg2 = nil) # i.e. like 'test'[0..2] and 'test'[2, 1]
116
+ unless (arg1.is_a?(Range) && arg2.nil?) ||
117
+ (arg1.is_a?(Integer) && arg2.is_a?(Integer))
118
+ raise ArgumentError, 'ansi_slice takes either a single Range ' \
119
+ 'or two integers (index and length) as its arguments.'
120
+ end
121
+
122
+ target_range = arg1.is_a?(Range) ? arg1 : (arg1...(arg1 + arg2))
123
+
124
+ if target_range.min.negative? || target_range.max.negative?
125
+ raise ArgumentError, 'Range must be entirely positive!'
126
+ end
127
+
128
+ args = [arg1, arg2].compact
129
+ return self[*args] if (self =~ /\e\[[\d\;]*m/).nil? # No work need be done
130
+
131
+ ## And here we match the normal `:[]` behavior outside of boundaries, e.g:
132
+ ## irb> 'TEST'[4..9]
133
+ ## => ""
134
+ ## irb> 'TEST'[5..9]
135
+ ## => nil
136
+ return nil if target_range.min > self.ansiless_length
137
+
138
+ mock_index = 0 # A scanning index that *doesn't* count ANSI codes
139
+ result_string_array = []
140
+
141
+ ansi_string_breakout.each do |char|
142
+ char_is_ansi_escape_code = char.is_ansi_escape_code?
143
+ within_target_range =
144
+ target_range.include?(mock_index)
145
+
146
+ if within_target_range || char_is_ansi_escape_code
147
+ result_string_array << char
148
+ end
149
+
150
+ mock_index += 1 unless char_is_ansi_escape_code
151
+ end
152
+
153
+ result_string_array.join('')
154
+ end
10
155
  end
11
156
  end
@@ -9,13 +9,19 @@ module ArrayFitToRefinement
9
9
  length_result = string_array.join('').send(length_method)
10
10
 
11
11
  if string_array.count < 2
12
- return nonplural_solution(string_length_goal, length_result, fill: fill)
12
+ return nonplural_solution(string_length_goal,
13
+ length_result,
14
+ fill: fill,
15
+ ignore_ansi_codes: ignore_ansi_codes)
13
16
  end
14
17
 
15
18
  if length_result > string_length_goal
16
- string_array.compress_to(string_length_goal, ignore_ansi_codes: ignore_ansi_codes)
19
+ string_array.compress_to(string_length_goal,
20
+ ignore_ansi_codes: ignore_ansi_codes)
17
21
  elsif length_result < string_length_goal
18
- string_array.expand_to(string_length_goal, ignore_ansi_codes: ignore_ansi_codes, fill: fill)
22
+ string_array.expand_to(string_length_goal,
23
+ ignore_ansi_codes: ignore_ansi_codes,
24
+ fill: fill)
19
25
  else # If it joins to the right length already, we still want to return the expected number of strings.
20
26
  spacers = [''] * (string_array.count - 1)
21
27
  string_array.zip(spacers).flatten.compact
@@ -35,9 +41,12 @@ module ArrayFitToRefinement
35
41
  longest_string_length = working_array.map { |s| s.send(length_method) }.max
36
42
  slider_index = slider % working_array.count
37
43
  if working_array[slider_index].send(length_method) >= longest_string_length
38
- working_array[slider_index] =
39
- working_array[slider_index].clip_at(working_array[slider_index].send(length_method) - 1,
40
- ignore_ansi_codes: ignore_ansi_codes)
44
+ length_less_one = working_array[slider_index].send(length_method) - 1
45
+ clipped_by_one = working_array[slider_index].clip_at(
46
+ length_less_one,
47
+ ignore_ansi_codes: ignore_ansi_codes
48
+ )
49
+ working_array[slider_index] = clipped_by_one
41
50
  end
42
51
  slider += 1
43
52
  end
@@ -69,11 +78,23 @@ module ArrayFitToRefinement
69
78
 
70
79
  private
71
80
 
72
- def nonplural_solution(string_length_goal, length_result, fill:)
81
+ def nonplural_solution(string_length_goal,
82
+ length_result,
83
+ fill:,
84
+ ignore_ansi_codes:)
73
85
  return [fill * string_length_goal] if self.empty?
74
86
 
75
- remaining_space = string_length_goal - length_result
76
- return self.map(&:to_s).append(fill * remaining_space)
87
+ string_array = self.map(&:to_s) # This also acts to dup
88
+
89
+ if length_result > string_length_goal
90
+ string_array.compress_to(string_length_goal,
91
+ ignore_ansi_codes: ignore_ansi_codes)
92
+ elsif length_result < string_length_goal
93
+ remaining_space = string_length_goal - length_result
94
+ string_array.append(fill * remaining_space)
95
+ else # If it joins to the right length already, we still want to return the expected number of strings.
96
+ self + ['']
97
+ end
77
98
  end
78
99
  end
79
100
  end