na 1.2.79 → 1.2.81
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 +4 -4
- data/.rubocop.yml +8 -2
- data/.rubocop_todo.yml +33 -538
- data/CHANGELOG.md +55 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +27 -10
- data/README.md +47 -6
- data/Rakefile +6 -0
- data/bin/commands/next.rb +4 -0
- data/bin/commands/scan.rb +84 -0
- data/bin/commands/update.rb +1 -1
- data/bin/na +18 -12
- data/lib/na/action.rb +181 -83
- data/lib/na/actions.rb +91 -66
- data/lib/na/array.rb +11 -7
- data/lib/na/benchmark.rb +57 -0
- data/lib/na/colors.rb +113 -92
- data/lib/na/editor.rb +22 -22
- data/lib/na/hash.rb +32 -9
- data/lib/na/help_monkey_patch.rb +9 -1
- data/lib/na/next_action.rb +327 -248
- data/lib/na/pager.rb +60 -32
- data/lib/na/project.rb +14 -1
- data/lib/na/prompt.rb +25 -3
- data/lib/na/string.rb +91 -130
- data/lib/na/theme.rb +47 -39
- data/lib/na/todo.rb +182 -145
- data/lib/na/version.rb +3 -1
- data/lib/na.rb +4 -1
- data/na.gemspec +4 -2
- data/scripts/generate-fish-completions.rb +18 -21
- data/src/_README.md +14 -4
- data/test_performance.rb +78 -0
- metadata +55 -24
data/lib/na/pager.rb
CHANGED
|
@@ -7,78 +7,98 @@ module NA
|
|
|
7
7
|
module Pager
|
|
8
8
|
class << self
|
|
9
9
|
# Boolean determines whether output is paginated
|
|
10
|
+
#
|
|
11
|
+
# @return [Boolean] true if paginated
|
|
10
12
|
def paginate
|
|
11
13
|
@paginate ||= false
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
# Enable/disable pagination
|
|
15
17
|
#
|
|
16
|
-
# @
|
|
17
|
-
|
|
18
|
-
@paginate = should_paginate
|
|
19
|
-
end
|
|
18
|
+
# @return [void]
|
|
19
|
+
attr_writer :paginate
|
|
20
20
|
|
|
21
|
-
# Page output. If @paginate is false, just dump to
|
|
22
|
-
# STDOUT
|
|
23
|
-
#
|
|
24
|
-
# @param text [String] text to paginate
|
|
21
|
+
# Page output. If @paginate is false, just dump to STDOUT
|
|
25
22
|
#
|
|
23
|
+
# @param text [String] text to paginate
|
|
24
|
+
# @return [Boolean, nil] true if paged, false if not, nil if no pager
|
|
26
25
|
def page(text)
|
|
27
26
|
unless @paginate
|
|
28
27
|
puts text
|
|
29
28
|
return
|
|
30
29
|
end
|
|
31
30
|
|
|
31
|
+
# Skip pagination for small outputs (faster than starting a pager)
|
|
32
|
+
if text.length < 2000 && text.lines.count < 50
|
|
33
|
+
puts text
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
pager = which_pager
|
|
38
|
+
return false unless pager
|
|
33
39
|
|
|
40
|
+
# Optimized pager execution - use spawn instead of fork+exec
|
|
34
41
|
read_io, write_io = IO.pipe
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
# Use spawn for better performance than fork+exec
|
|
44
|
+
pid = spawn(pager, in: read_io, out: $stdout, err: $stderr)
|
|
45
|
+
read_io.close
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
begin
|
|
48
|
+
# Write data to pager
|
|
49
|
+
write_io.write(text)
|
|
39
50
|
write_io.close
|
|
40
|
-
input.reopen(read_io)
|
|
41
|
-
read_io.close
|
|
42
|
-
|
|
43
|
-
# Wait until we have input before we start the pager
|
|
44
|
-
IO.select [input]
|
|
45
51
|
|
|
52
|
+
# Wait for pager to complete
|
|
53
|
+
_, status = Process.waitpid2(pid)
|
|
54
|
+
status.success?
|
|
55
|
+
rescue SystemCallError
|
|
56
|
+
# Clean up on error
|
|
46
57
|
begin
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
raise Errors::DoingStandardError, "Pager error, #{e}"
|
|
58
|
+
write_io.close
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
51
61
|
end
|
|
62
|
+
begin
|
|
63
|
+
Process.kill('TERM', pid)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
begin
|
|
68
|
+
Process.waitpid(pid)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
false
|
|
52
73
|
end
|
|
53
|
-
|
|
54
|
-
begin
|
|
55
|
-
read_io.close
|
|
56
|
-
write_io.write(text)
|
|
57
|
-
write_io.close
|
|
58
|
-
rescue SystemCallError # => e
|
|
59
|
-
# raise Errors::DoingStandardError, "Pager error, #{e}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
_, status = Process.waitpid2(pid)
|
|
63
|
-
status.success?
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
78
|
+
# Get the git pager command if available
|
|
79
|
+
#
|
|
80
|
+
# @return [String, nil] git pager command
|
|
68
81
|
def git_pager
|
|
69
82
|
TTY::Which.exist?('git') ? `#{TTY::Which.which('git')} config --get-all core.pager` : nil
|
|
70
83
|
end
|
|
71
84
|
|
|
85
|
+
# List of possible pager commands
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<String>] pager commands
|
|
72
88
|
def pagers
|
|
73
89
|
[
|
|
74
|
-
ENV
|
|
90
|
+
ENV.fetch('PAGER', nil),
|
|
75
91
|
'less -FXr',
|
|
76
|
-
ENV
|
|
92
|
+
ENV.fetch('GIT_PAGER', nil),
|
|
77
93
|
git_pager,
|
|
78
94
|
'more -r'
|
|
79
95
|
].remove_bad
|
|
80
96
|
end
|
|
81
97
|
|
|
98
|
+
# Find the first available executable pager command
|
|
99
|
+
#
|
|
100
|
+
# @param commands [Array<String>] commands to check
|
|
101
|
+
# @return [String, nil] first available command
|
|
82
102
|
def find_executable(*commands)
|
|
83
103
|
execs = commands.empty? ? pagers : commands
|
|
84
104
|
execs
|
|
@@ -86,9 +106,17 @@ module NA
|
|
|
86
106
|
.find { |cmd| TTY::Which.exist?(cmd.split.first) }
|
|
87
107
|
end
|
|
88
108
|
|
|
109
|
+
# Determine which pager to use
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil] pager command
|
|
89
112
|
def which_pager
|
|
90
113
|
@which_pager ||= find_executable(*pagers)
|
|
91
114
|
end
|
|
115
|
+
|
|
116
|
+
# Clear pager cache (useful for testing)
|
|
117
|
+
def clear_pager_cache
|
|
118
|
+
@which_pager = nil
|
|
119
|
+
end
|
|
92
120
|
end
|
|
93
121
|
end
|
|
94
122
|
end
|
data/lib/na/project.rb
CHANGED
|
@@ -4,6 +4,13 @@ module NA
|
|
|
4
4
|
class Project < Hash
|
|
5
5
|
attr_accessor :project, :indent, :line, :last_line
|
|
6
6
|
|
|
7
|
+
# Initialize a Project object
|
|
8
|
+
#
|
|
9
|
+
# @param project [String] Project name
|
|
10
|
+
# @param indent [Integer] Indentation level
|
|
11
|
+
# @param line [Integer] Starting line number
|
|
12
|
+
# @param last_line [Integer] Ending line number
|
|
13
|
+
# @return [void]
|
|
7
14
|
def initialize(project, indent = 0, line = 0, last_line = 0)
|
|
8
15
|
super()
|
|
9
16
|
@project = project
|
|
@@ -12,17 +19,23 @@ module NA
|
|
|
12
19
|
@last_line = last_line
|
|
13
20
|
end
|
|
14
21
|
|
|
22
|
+
# String representation of the project
|
|
23
|
+
#
|
|
24
|
+
# @return [String]
|
|
15
25
|
def to_s
|
|
16
26
|
{ project: @project, indent: @indent, line: @line, last_line: @last_line }.to_s
|
|
17
27
|
end
|
|
18
28
|
|
|
29
|
+
# Inspect the project object
|
|
30
|
+
#
|
|
31
|
+
# @return [String]
|
|
19
32
|
def inspect
|
|
20
33
|
[
|
|
21
34
|
"@project: #{@project}",
|
|
22
35
|
"@indent: #{@indent}",
|
|
23
36
|
"@line: #{@line}",
|
|
24
37
|
"@last_line: #{@last_line}"
|
|
25
|
-
].join(
|
|
38
|
+
].join(' ')
|
|
26
39
|
end
|
|
27
40
|
end
|
|
28
41
|
end
|
data/lib/na/prompt.rb
CHANGED
|
@@ -4,6 +4,10 @@ module NA
|
|
|
4
4
|
# Prompt Hooks
|
|
5
5
|
module Prompt
|
|
6
6
|
class << self
|
|
7
|
+
# Generate the shell prompt hook script for na
|
|
8
|
+
#
|
|
9
|
+
# @param shell [Symbol] Shell type (:zsh, :fish, :bash)
|
|
10
|
+
# @return [String] Shell script for prompt hook
|
|
7
11
|
def prompt_hook(shell)
|
|
8
12
|
case shell
|
|
9
13
|
when :zsh
|
|
@@ -14,7 +18,9 @@ module NA
|
|
|
14
18
|
when :tag
|
|
15
19
|
'na tagged $(basename "$PWD")'
|
|
16
20
|
else
|
|
17
|
-
NA.notify(
|
|
21
|
+
NA.notify(
|
|
22
|
+
"#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1
|
|
23
|
+
)
|
|
18
24
|
end
|
|
19
25
|
else
|
|
20
26
|
'na next'
|
|
@@ -31,7 +37,9 @@ module NA
|
|
|
31
37
|
when :tag
|
|
32
38
|
'na tagged (basename "$PWD")'
|
|
33
39
|
else
|
|
34
|
-
NA.notify(
|
|
40
|
+
NA.notify(
|
|
41
|
+
"#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1
|
|
42
|
+
)
|
|
35
43
|
end
|
|
36
44
|
else
|
|
37
45
|
'na next'
|
|
@@ -50,7 +58,9 @@ module NA
|
|
|
50
58
|
when :tag
|
|
51
59
|
'na tagged $(basename "$PWD")'
|
|
52
60
|
else
|
|
53
|
-
NA.notify(
|
|
61
|
+
NA.notify(
|
|
62
|
+
"#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1
|
|
63
|
+
)
|
|
54
64
|
end
|
|
55
65
|
else
|
|
56
66
|
'na next'
|
|
@@ -70,6 +80,10 @@ module NA
|
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
82
|
|
|
83
|
+
# Get the configuration file path for the given shell
|
|
84
|
+
#
|
|
85
|
+
# @param shell [Symbol] Shell type
|
|
86
|
+
# @return [String] Path to shell config file
|
|
73
87
|
def prompt_file(shell)
|
|
74
88
|
files = {
|
|
75
89
|
zsh: '~/.zshrc',
|
|
@@ -80,6 +94,10 @@ module NA
|
|
|
80
94
|
files[shell]
|
|
81
95
|
end
|
|
82
96
|
|
|
97
|
+
# Display the prompt hook script and notify user of config file
|
|
98
|
+
#
|
|
99
|
+
# @param shell [Symbol] Shell type
|
|
100
|
+
# @return [void]
|
|
83
101
|
def show_prompt_hook(shell)
|
|
84
102
|
file = prompt_file(shell)
|
|
85
103
|
|
|
@@ -87,6 +105,10 @@ module NA
|
|
|
87
105
|
puts prompt_hook(shell)
|
|
88
106
|
end
|
|
89
107
|
|
|
108
|
+
# Install the prompt hook script into the shell config file
|
|
109
|
+
#
|
|
110
|
+
# @param shell [Symbol] Shell type
|
|
111
|
+
# @return [void]
|
|
90
112
|
def install_prompt_hook(shell)
|
|
91
113
|
file = prompt_file(shell)
|
|
92
114
|
|
data/lib/na/string.rb
CHANGED
|
@@ -6,30 +6,20 @@ REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze
|
|
|
6
6
|
|
|
7
7
|
# String helpers
|
|
8
8
|
class ::String
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
def comment(char = "#")
|
|
15
|
-
split(/\n/).map { |l| "# #{l}" }.join("\n")
|
|
9
|
+
# Insert a comment character at the start of every line
|
|
10
|
+
# @param char [String] The character to insert (default #)
|
|
11
|
+
def comment(_char = '#')
|
|
12
|
+
split("\n").map { |l| "# #{l}" }.join("\n")
|
|
16
13
|
end
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
## @return [Boolean] true if object is defined and
|
|
22
|
-
## has content
|
|
23
|
-
##
|
|
15
|
+
# Tests if object is nil or empty
|
|
16
|
+
# @return [Boolean] true if object is defined and has content
|
|
24
17
|
def good?
|
|
25
18
|
!strip.empty?
|
|
26
19
|
end
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
##
|
|
31
|
-
## @return [Boolean] line is empty or comment
|
|
32
|
-
##
|
|
21
|
+
# Test if line should be ignored
|
|
22
|
+
# @return [Boolean] line is empty or comment
|
|
33
23
|
def ignore?
|
|
34
24
|
line = self
|
|
35
25
|
line =~ /^#/ || line.strip.empty?
|
|
@@ -50,19 +40,16 @@ class ::String
|
|
|
50
40
|
end
|
|
51
41
|
|
|
52
42
|
# IO.read(file).force_encoding('ASCII-8BIT').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
53
|
-
|
|
43
|
+
File.read(file).force_encoding('utf-8')
|
|
54
44
|
end
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
##
|
|
59
|
-
## @return [Number] number of indents detected
|
|
60
|
-
##
|
|
46
|
+
# Determine indentation level of line
|
|
47
|
+
# @return [Number] number of indents detected
|
|
61
48
|
def indent_level
|
|
62
49
|
prefix = match(/(^[ \t]+)/)
|
|
63
50
|
return 0 if prefix.nil?
|
|
64
51
|
|
|
65
|
-
prefix[1].gsub(
|
|
52
|
+
prefix[1].gsub(' ', "\t").scan("\t").count
|
|
66
53
|
end
|
|
67
54
|
|
|
68
55
|
def action?
|
|
@@ -94,31 +81,22 @@ class ::String
|
|
|
94
81
|
self =~ /@#{NA.na_tag}\b/
|
|
95
82
|
end
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
##
|
|
100
|
-
## @return Colorized string
|
|
101
|
-
##
|
|
84
|
+
# Colorize the dirname and filename of a path
|
|
85
|
+
# @return [String] Colorized string
|
|
102
86
|
def highlight_filename
|
|
103
87
|
dir = File.dirname(self).shorten_path.trunc_middle(TTY::Screen.columns / 3)
|
|
104
88
|
file = NA.include_ext ? File.basename(self) : File.basename(self, ".#{NA.extension}")
|
|
105
89
|
"#{NA.theme[:dirname]}#{dir}/#{NA.theme[:filename]}#{file}{x}"
|
|
106
90
|
end
|
|
107
91
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
## @param last_color [String] Color to restore after
|
|
117
|
-
## tag highlight
|
|
118
|
-
##
|
|
119
|
-
## @return [String] string with @tags highlighted
|
|
120
|
-
##
|
|
121
|
-
def highlight_tags(color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens], last_color: NA.theme[:action])
|
|
92
|
+
# Colorize @tags with ANSI escapes
|
|
93
|
+
# @param color [String] color (see #Color)
|
|
94
|
+
# @param value [String] The value color template
|
|
95
|
+
# @param parens [String] The parens color template
|
|
96
|
+
# @param last_color [String] Color to restore after tag highlight
|
|
97
|
+
# @return [String] string with @tags highlighted
|
|
98
|
+
def highlight_tags(color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens],
|
|
99
|
+
last_color: NA.theme[:action])
|
|
122
100
|
tag_color = NA::Color.template(color)
|
|
123
101
|
paren_color = NA::Color.template(parens)
|
|
124
102
|
value_color = NA::Color.template(value)
|
|
@@ -132,21 +110,16 @@ class ::String
|
|
|
132
110
|
end
|
|
133
111
|
end
|
|
134
112
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
## search
|
|
140
|
-
## @param color [String] The highlight color
|
|
141
|
-
## template
|
|
142
|
-
## @param last_color [String] Color to restore after
|
|
143
|
-
## highlight
|
|
144
|
-
##
|
|
113
|
+
# Highlight search results
|
|
114
|
+
# @param regexes [Array] The regexes for the search
|
|
115
|
+
# @param color [String] The highlight color template
|
|
116
|
+
# @param last_color [String] Color to restore after highlight
|
|
145
117
|
def highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action])
|
|
146
118
|
string = dup
|
|
147
119
|
color = NA::Color.template(color.dup)
|
|
148
120
|
regexes.each do |rx|
|
|
149
121
|
next if rx.nil?
|
|
122
|
+
|
|
150
123
|
rx = Regexp.new(rx, Regexp::IGNORECASE) if rx.is_a?(String)
|
|
151
124
|
|
|
152
125
|
string.gsub!(rx) do
|
|
@@ -158,16 +131,23 @@ class ::String
|
|
|
158
131
|
string
|
|
159
132
|
end
|
|
160
133
|
|
|
134
|
+
# Truncate the string in the middle, replacing the removed section with '[...]'.
|
|
135
|
+
# @param max [Integer] Maximum allowed length of the string
|
|
136
|
+
# @return [String] Truncated string with middle replaced if necessary
|
|
161
137
|
def trunc_middle(max)
|
|
162
138
|
return self unless length > max
|
|
163
139
|
|
|
164
140
|
half = (max / 2).floor - 3
|
|
165
|
-
chars =
|
|
141
|
+
chars = chars
|
|
166
142
|
pre = chars.slice(0, half)
|
|
167
143
|
post = chars.reverse.slice(0, half).reverse
|
|
168
|
-
"#{pre.join
|
|
144
|
+
"#{pre.join}[...]#{post.join}"
|
|
169
145
|
end
|
|
170
146
|
|
|
147
|
+
# Wrap the string to a given width, indenting each line and preserving tag formatting.
|
|
148
|
+
# @param width [Integer] The maximum line width
|
|
149
|
+
# @param indent [Integer] Number of spaces to indent each line
|
|
150
|
+
# @return [String] Wrapped string
|
|
171
151
|
def wrap(width, indent)
|
|
172
152
|
return "\n#{self}" if width <= 80
|
|
173
153
|
|
|
@@ -188,40 +168,33 @@ class ::String
|
|
|
188
168
|
end
|
|
189
169
|
end
|
|
190
170
|
output << line.join(' ')
|
|
191
|
-
output.join("\n
|
|
171
|
+
output.join("\n#{' ' * (indent + 2)}").gsub(/†/, ' ')
|
|
192
172
|
end
|
|
193
173
|
|
|
194
174
|
# Returns the last escape sequence from a string.
|
|
195
|
-
#
|
|
196
|
-
# @
|
|
197
|
-
# assumption that the result of inserting them
|
|
198
|
-
# will generate the same color as was set at
|
|
199
|
-
# the end of the string. Because you can send
|
|
200
|
-
# modifiers like dark and bold separate from
|
|
201
|
-
# color codes, only using the last code may
|
|
202
|
-
# not render the same style.
|
|
203
|
-
#
|
|
204
|
-
# @return [String] All escape codes in string
|
|
205
|
-
#
|
|
175
|
+
# @note Actually returns all escape codes, with the assumption that the result of inserting them will generate the same color as was set at end of the string. Because you can send modifiers like dark and bold separate from color codes, only using the last code may not render the same style.
|
|
176
|
+
# @return [String] All escape codes in string
|
|
206
177
|
def last_color
|
|
207
|
-
scan(/\e\[[\d;]+m/).join
|
|
178
|
+
scan(/\e\[[\d;]+m/).join.gsub("\e[0m", '')
|
|
208
179
|
end
|
|
209
180
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
## between characters, joins segments with
|
|
215
|
-
## slashes and requires that last segment
|
|
216
|
-
## match last segment of target path
|
|
217
|
-
##
|
|
218
|
-
## @param distance The distance allowed between characters
|
|
219
|
-
## @param require_last Require match to be last element in path
|
|
220
|
-
##
|
|
181
|
+
# Convert a directory path to a regular expression
|
|
182
|
+
# @note Splits at / or :, adds variable distance between characters, joins segments with slashes and requires that last segment match last segment of target path
|
|
183
|
+
# @param distance [Integer] The distance allowed between characters
|
|
184
|
+
# @param require_last [Boolean] Require match to be last element in path
|
|
221
185
|
def dir_to_rx(distance: 1, require_last: true)
|
|
222
|
-
"#{split(%r{[/:]}).map
|
|
186
|
+
"#{split(%r{[/:]}).map do |comp|
|
|
187
|
+
comp.chars.join(".{0,#{distance}}").gsub('*', '[^ ]*?')
|
|
188
|
+
end.join('.*?/.*?')}#{require_last ? '[^/]*?$' : ''}"
|
|
223
189
|
end
|
|
224
190
|
|
|
191
|
+
# Check if the string matches directory patterns using any, all, and none criteria.
|
|
192
|
+
# @param any [Array] Patterns where any match is sufficient
|
|
193
|
+
# @param all [Array] Patterns where all must match
|
|
194
|
+
# @param none [Array] Patterns where none must match
|
|
195
|
+
# @param require_last [Boolean] Require last segment match
|
|
196
|
+
# @param distance [Integer] Allowed character distance in regex
|
|
197
|
+
# @return [Boolean] True if matches criteria
|
|
225
198
|
def dir_matches(any: [], all: [], none: [], require_last: true, distance: 1)
|
|
226
199
|
any_rx = any.map { |q| q.dir_to_rx(distance: distance, require_last: require_last) }
|
|
227
200
|
all_rx = all.map { |q| q.dir_to_rx(distance: distance, require_last: require_last) }
|
|
@@ -229,29 +202,29 @@ class ::String
|
|
|
229
202
|
matches_any(any_rx) && matches_all(all_rx) && matches_none(none_rx)
|
|
230
203
|
end
|
|
231
204
|
|
|
205
|
+
# Check if the string matches any, all, and none regex patterns.
|
|
206
|
+
# @param any [Array] Patterns where any match is sufficient
|
|
207
|
+
# @param all [Array] Patterns where all must match
|
|
208
|
+
# @param none [Array] Patterns where none must match
|
|
209
|
+
# @return [Boolean] True if matches criteria
|
|
232
210
|
def matches(any: [], all: [], none: [])
|
|
233
211
|
matches_any(any) && matches_all(all) && matches_none(none)
|
|
234
212
|
end
|
|
235
213
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
##
|
|
239
|
-
## @return [String] Regex string
|
|
240
|
-
##
|
|
214
|
+
# Convert wildcard characters to regular expressions
|
|
215
|
+
# @return [String] Regex string
|
|
241
216
|
def wildcard_to_rx
|
|
242
|
-
gsub(
|
|
217
|
+
gsub('.', '\\.').gsub('?', '.').gsub('*', '[^ ]*?')
|
|
243
218
|
end
|
|
244
219
|
|
|
220
|
+
# Capitalize the first character of the string in place.
|
|
221
|
+
# @return [String] The modified string
|
|
245
222
|
def cap_first!
|
|
246
223
|
replace cap_first
|
|
247
224
|
end
|
|
248
225
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
## capitalization in place
|
|
252
|
-
##
|
|
253
|
-
## @return [String] capitalized string
|
|
254
|
-
##
|
|
226
|
+
# Capitalize first character, leaving other capitalization in place
|
|
227
|
+
# @return [String] capitalized string
|
|
255
228
|
def cap_first
|
|
256
229
|
sub(/^([a-z])(.*)$/) do
|
|
257
230
|
m = Regexp.last_match
|
|
@@ -259,24 +232,14 @@ class ::String
|
|
|
259
232
|
end
|
|
260
233
|
end
|
|
261
234
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
##
|
|
265
|
-
## @return [String] shortened path
|
|
266
|
-
##
|
|
235
|
+
# Replace home directory with tilde
|
|
236
|
+
# @return [String] shortened path
|
|
267
237
|
def shorten_path
|
|
268
|
-
sub(/^#{
|
|
238
|
+
sub(/^#{Dir.home}/, '~')
|
|
269
239
|
end
|
|
270
240
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
## within configured date tags (tags whose value is
|
|
274
|
-
## expected to be a date). Modifies string in place.
|
|
275
|
-
##
|
|
276
|
-
## @param additional_tags [Array] An array of
|
|
277
|
-
## additional tags to
|
|
278
|
-
## consider date_tags
|
|
279
|
-
##
|
|
241
|
+
# Convert (chronify) natural language dates within configured date tags (tags whose value is expected to be a date). Modifies string in place.
|
|
242
|
+
# @param additional_tags [Array] An array of additional tags to consider date_tags
|
|
280
243
|
def expand_date_tags(additional_tags = nil)
|
|
281
244
|
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
|
282
245
|
|
|
@@ -312,27 +275,14 @@ class ::String
|
|
|
312
275
|
end
|
|
313
276
|
end
|
|
314
277
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
## PDT'
|
|
324
|
-
##
|
|
325
|
-
## @param options Additional options
|
|
326
|
-
##
|
|
327
|
-
## @option options :future [Boolean] assume future date
|
|
328
|
-
## (default: false)
|
|
329
|
-
##
|
|
330
|
-
## @option options :guess [Symbol] :begin or :end to
|
|
331
|
-
## assume beginning or end of
|
|
332
|
-
## arbitrary time range
|
|
333
|
-
##
|
|
334
|
-
## @return [DateTime] result
|
|
335
|
-
##
|
|
278
|
+
# Converts input string into a Time object when input takes on the following formats:
|
|
279
|
+
# - interval format e.g. '1d2h30m', '45m' etc.
|
|
280
|
+
# - a semantic phrase e.g. 'yesterday 5:30pm'
|
|
281
|
+
# - a strftime e.g. '2016-03-15 15:32:04 PDT'
|
|
282
|
+
# @param options [Hash] Additional options
|
|
283
|
+
# @option options :future [Boolean] assume future date (default: false)
|
|
284
|
+
# @option options :guess [Symbol] :begin or :end to assume beginning or end of arbitrary time range
|
|
285
|
+
# @return [DateTime] result
|
|
336
286
|
def chronify(**options)
|
|
337
287
|
now = Time.now
|
|
338
288
|
raise StandardError, "Invalid time expression #{inspect}" if to_s.strip == ''
|
|
@@ -353,8 +303,9 @@ class ::String
|
|
|
353
303
|
else
|
|
354
304
|
date_string = dup
|
|
355
305
|
date_string = 'today' if date_string.match(REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
|
|
356
|
-
date_string = "#{options[:context]
|
|
306
|
+
date_string = "#{options[:context]} #{date_string}" if date_string =~ REGEX_TIME && options[:context]
|
|
357
307
|
|
|
308
|
+
require 'chronic' unless defined?(Chronic)
|
|
358
309
|
res = Chronic.parse(date_string, {
|
|
359
310
|
guess: options.fetch(:guess, :begin),
|
|
360
311
|
context: options.fetch(:future, false) ? :future : :past,
|
|
@@ -367,8 +318,12 @@ class ::String
|
|
|
367
318
|
res
|
|
368
319
|
end
|
|
369
320
|
|
|
321
|
+
# Private helper methods for pattern matching
|
|
370
322
|
private
|
|
371
323
|
|
|
324
|
+
# Returns true if none of the regexes match the string.
|
|
325
|
+
# @param regexes [Array] Array of regex patterns
|
|
326
|
+
# @return [Boolean] True if none match
|
|
372
327
|
def matches_none(regexes)
|
|
373
328
|
regexes.each do |rx|
|
|
374
329
|
return false if match(Regexp.new(rx, Regexp::IGNORECASE))
|
|
@@ -376,6 +331,9 @@ class ::String
|
|
|
376
331
|
true
|
|
377
332
|
end
|
|
378
333
|
|
|
334
|
+
# Returns true if any of the regexes match the string.
|
|
335
|
+
# @param regexes [Array] Array of regex patterns
|
|
336
|
+
# @return [Boolean] True if any match
|
|
379
337
|
def matches_any(regexes)
|
|
380
338
|
regexes.each do |rx|
|
|
381
339
|
return true if match(Regexp.new(rx, Regexp::IGNORECASE))
|
|
@@ -383,6 +341,9 @@ class ::String
|
|
|
383
341
|
false
|
|
384
342
|
end
|
|
385
343
|
|
|
344
|
+
# Returns true if all of the regexes match the string.
|
|
345
|
+
# @param regexes [Array] Array of regex patterns
|
|
346
|
+
# @return [Boolean] True if all match
|
|
386
347
|
def matches_all(regexes)
|
|
387
348
|
regexes.each do |rx|
|
|
388
349
|
return false unless match(Regexp.new(rx, Regexp::IGNORECASE))
|