redcar 0.10 → 0.11.0dev
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +14 -0
- data/Rakefile +68 -29
- data/lib/redcar.rb +2 -2
- data/plugins/application/lib/application/dialogs/filter_list_dialog.rb +2 -1
- data/plugins/application_swt/spec/application_swt/gradient_spec.rb +3 -12
- data/plugins/core/lib/core/resource.rb +13 -5
- data/plugins/document_search/features/find.feature +366 -214
- data/plugins/document_search/features/incremental_search.feature +351 -0
- data/plugins/document_search/features/replace.feature +16 -16
- data/plugins/document_search/features/step_definitions/find_steps.rb +16 -0
- data/plugins/document_search/features/support/env.rb +11 -0
- data/plugins/document_search/lib/document_search.rb +149 -109
- data/plugins/document_search/lib/document_search/commands.rb +251 -202
- data/plugins/document_search/lib/document_search/find_speedbar.rb +138 -81
- data/plugins/document_search/lib/document_search/incremental_search_speedbar.rb +70 -0
- data/plugins/document_search/lib/document_search/query_options.rb +15 -39
- data/plugins/document_search/plugin.rb +1 -1
- data/plugins/edit_view/features/step_definitions/editing_steps.rb +6 -2
- data/plugins/edit_view_swt/lib/edit_view_swt.rb +2 -2
- data/plugins/file_parser/lib/file_parser.rb +6 -1
- data/plugins/html_view/features/step_definitions/web_view_steps.rb +12 -0
- data/plugins/html_view/features/support/env.rb +16 -0
- data/plugins/html_view/lib/html_view.rb +5 -1
- data/plugins/line_tools/lib/line_tools.rb +16 -0
- data/plugins/project/features/find_file.feature +28 -0
- data/plugins/project/features/open_and_save_files.feature +11 -0
- data/plugins/project/features/step_definitions/file_steps.rb +6 -1
- data/plugins/project/features/support/env.rb +2 -0
- data/plugins/project/lib/project.rb +49 -6
- data/plugins/project/lib/project/commands.rb +18 -6
- data/plugins/project/lib/project/find_file_dialog.rb +19 -8
- data/plugins/project/lib/project/find_recent_dialog.rb +30 -0
- data/plugins/project/lib/project/manager.rb +41 -10
- data/plugins/project/lib/project/recent.rb +64 -0
- data/plugins/project/spec/fixtures/myproject/vendor/bar.rb +0 -0
- data/plugins/project/spec/fixtures/myproject/vendor/plugins/bar.rb +0 -0
- data/plugins/{find-in-project → project_search}/TODO.md +3 -3
- data/plugins/project_search/features/support/env.rb +6 -0
- data/plugins/project_search/features/word_search.feature +34 -0
- data/plugins/project_search/lib/project_search.rb +73 -0
- data/plugins/project_search/lib/project_search/binary_data_detector.rb +46 -0
- data/plugins/project_search/lib/project_search/commands.rb +62 -0
- data/plugins/project_search/lib/project_search/hit.rb +17 -0
- data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/collapsed.png +0 -0
- data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/expanded.png +0 -0
- data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/spinner.gif +0 -0
- data/plugins/project_search/lib/project_search/lucene_index.rb +64 -0
- data/plugins/project_search/lib/project_search/lucene_refresh.rb +22 -0
- data/plugins/project_search/lib/project_search/project.rb +14 -0
- data/plugins/project_search/lib/project_search/query.rb +29 -0
- data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/stylesheets/style.css +14 -3
- data/plugins/project_search/lib/project_search/views/_file.html.erb +60 -0
- data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/views/index.html.erb +12 -9
- data/plugins/project_search/lib/project_search/word_search.rb +105 -0
- data/plugins/project_search/lib/project_search/word_search_controller.rb +207 -0
- data/plugins/project_search/plugin.rb +8 -0
- data/plugins/project_search/spec/fixtures/project/binary_file.bin +0 -0
- data/plugins/project_search/spec/fixtures/project/foo.txt +43 -0
- data/plugins/project_search/spec/fixtures/project/qux.rb +3 -0
- data/plugins/project_search/spec/project_search/binary_data_detector_spec.rb +24 -0
- data/plugins/project_search/spec/project_search/word_search_spec.rb +157 -0
- data/plugins/project_search/spec/spec_helper.rb +27 -0
- data/plugins/redcar/redcar.rb +77 -71
- data/plugins/ruby/lib/ruby/syntax_checker.rb +1 -1
- data/plugins/snippets/features/snippets.feature +12 -0
- data/plugins/snippets/lib/snippets/tab_handler.rb +1 -1
- metadata +46 -25
- data/plugins/document_search/features/find_and_replace.feature +0 -723
- data/plugins/document_search/lib/document_search/find_and_replace_speedbar.rb +0 -142
- data/plugins/find-in-project/lib/find_in_project.rb +0 -35
- data/plugins/find-in-project/lib/find_in_project/commands.rb +0 -30
- data/plugins/find-in-project/lib/find_in_project/controllers.rb +0 -170
- data/plugins/find-in-project/lib/find_in_project/views/_divider.html.erb +0 -4
- data/plugins/find-in-project/lib/find_in_project/views/_file_heading.html.erb +0 -10
- data/plugins/find-in-project/lib/find_in_project/views/_file_line.html.erb +0 -6
- data/plugins/find-in-project/plugin.rb +0 -11
- data/plugins/project/lib/project/recent_directories.rb +0 -54
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
When /^I open the incremental search speedbar$/ do
|
3
|
+
Redcar::DocumentSearch::OpenIncrementalSearchSpeedbarCommand.new.run
|
4
|
+
end
|
5
|
+
|
6
|
+
When /^I open the find speedbar$/ do
|
7
|
+
Redcar::DocumentSearch::OpenFindSpeedbarCommand.new.run
|
8
|
+
end
|
9
|
+
|
10
|
+
Then /^I should see the incremental search speedbar$/ do
|
11
|
+
Then "the Redcar::DocumentSearch::IncrementalSearchSpeedbar speedbar should be open"
|
12
|
+
end
|
13
|
+
|
14
|
+
Then /^I should see the find speedbar$/ do
|
15
|
+
Then "the Redcar::DocumentSearch::FindSpeedbar speedbar should be open"
|
16
|
+
end
|
@@ -1,3 +1,14 @@
|
|
1
1
|
|
2
|
+
def reset_search_settings(options)
|
3
|
+
if options
|
4
|
+
options.is_regex = Redcar::DocumentSearch::QueryOptions::DEFAULT_IS_REGEX
|
5
|
+
options.match_case = Redcar::DocumentSearch::QueryOptions::DEFAULT_MATCH_CASE
|
6
|
+
options.wrap_around = Redcar::DocumentSearch::QueryOptions::DEFAULT_WRAP_AROUND
|
7
|
+
end
|
8
|
+
end
|
2
9
|
|
10
|
+
Before do
|
11
|
+
reset_search_settings(Redcar::DocumentSearch::IncrementalSearchSpeedbar.previous_options)
|
12
|
+
reset_search_settings(Redcar::DocumentSearch::FindSpeedbar.previous_options)
|
13
|
+
end
|
3
14
|
|
@@ -2,150 +2,190 @@ require 'strscan'
|
|
2
2
|
require "document_search/query_options"
|
3
3
|
require "document_search/commands"
|
4
4
|
require "document_search/find_speedbar"
|
5
|
-
require "document_search/
|
6
|
-
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
sub_menu "
|
12
|
-
|
13
|
-
|
5
|
+
require "document_search/incremental_search_speedbar"
|
6
|
+
|
7
|
+
module Redcar
|
8
|
+
module DocumentSearch
|
9
|
+
def self.menus
|
10
|
+
Redcar::Menu::Builder.build do
|
11
|
+
sub_menu "Edit" do
|
12
|
+
sub_menu "Find", :priority => 50 do
|
13
|
+
item "Incremental Search", OpenIncrementalSearchSpeedbarCommand
|
14
|
+
item "Find...", OpenFindSpeedbarCommand
|
15
|
+
separator
|
16
|
+
item "Find Next", DoFindNextCommand
|
17
|
+
item "Find Previous", DoFindPreviousCommand
|
18
|
+
separator
|
19
|
+
item "Replace All", DoReplaceAllCommand
|
20
|
+
item "Replace All in Selection", DoReplaceAllInSelectionCommand
|
21
|
+
item "Replace and Find", DoReplaceAndFindCommand
|
22
|
+
separator
|
23
|
+
item "Use Selection for Find", DoUseSelectionForFindCommand
|
24
|
+
item "Use Selection for Replace", DoUseSelectionForReplaceCommand
|
25
|
+
end
|
14
26
|
separator
|
15
|
-
item "Find Next", FindNextMenuCommand
|
16
|
-
item "Find Previous", FindPreviousMenuCommand
|
17
|
-
item "Replace and Find", ReplaceAndFindMenuCommand
|
18
|
-
separator
|
19
|
-
item "Use Selection for Find", UseSelectionForFindMenuCommand
|
20
|
-
item "Use Selection for Replace", UseSelectionForReplaceMenuCommand
|
21
27
|
end
|
22
|
-
separator
|
23
28
|
end
|
24
29
|
end
|
25
|
-
end
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
def self.keymaps
|
32
|
+
osx = Redcar::Keymap.build("main", :osx) do
|
33
|
+
link "Ctrl+S", DocumentSearch::OpenIncrementalSearchSpeedbarCommand
|
34
|
+
link "Cmd+F", DocumentSearch::OpenFindSpeedbarCommand
|
35
|
+
link "Cmd+G", DocumentSearch::DoFindNextCommand
|
36
|
+
link "Cmd+Shift+G", DocumentSearch::DoFindPreviousCommand
|
37
|
+
link "Cmd+Ctrl+F", DocumentSearch::DoReplaceAllCommand
|
38
|
+
link "Cmd+Ctrl+Shift+F", DocumentSearch::DoReplaceAllInSelectionCommand
|
39
|
+
link "Cmd+Alt+F", DocumentSearch::DoReplaceAndFindCommand
|
40
|
+
link "Cmd+E", DocumentSearch::DoUseSelectionForFindCommand
|
41
|
+
link "Cmd+Shift+E", DocumentSearch::DoUseSelectionForReplaceCommand
|
42
|
+
end
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
linwin = Redcar::Keymap.build("main", [:linux, :windows]) do
|
45
|
+
link "Alt+S", DocumentSearch::OpenIncrementalSearchSpeedbarCommand
|
46
|
+
link "Ctrl+F", DocumentSearch::OpenFindSpeedbarCommand
|
47
|
+
link "Ctrl+G", DocumentSearch::DoFindNextCommand
|
48
|
+
link "Ctrl+Shift+G", DocumentSearch::DoFindPreviousCommand
|
49
|
+
link "Ctrl+Alt+F", DocumentSearch::DoReplaceAndFindCommand
|
50
|
+
link "Ctrl+E", DocumentSearch::DoUseSelectionForFindCommand
|
51
|
+
link "Alt+Shift+E", DocumentSearch::DoUseSelectionForReplaceCommand
|
52
|
+
end
|
46
53
|
|
47
|
-
|
48
|
-
|
54
|
+
[linwin, osx]
|
55
|
+
end
|
49
56
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
57
|
+
def self.toolbars
|
58
|
+
Redcar::ToolBar::Builder.build do
|
59
|
+
item "Find", :command => DocumentSearch::OpenIncrementalSearchSpeedbarCommand, :icon => File.join(Redcar::ICONS_DIRECTORY, "magnifier.png"), :barname => :edit
|
60
|
+
item "Find Next", :command => DocumentSearch::DoFindNextCommand, :icon => File.join(Redcar::ICONS_DIRECTORY, "magnifier--arrow.png"), :barname => :edit
|
61
|
+
end
|
54
62
|
end
|
55
|
-
end
|
56
63
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
class OpenIncrementalSearchSpeedbarCommand < Redcar::EditTabCommand
|
65
|
+
def execute
|
66
|
+
already_open = win.speedbar.is_a? IncrementalSearchSpeedbar
|
67
|
+
@speedbar = IncrementalSearchSpeedbar.new
|
68
|
+
unless already_open
|
69
|
+
# Clear out previous query for new speedbar.
|
70
|
+
IncrementalSearchSpeedbar.previous_query = ''
|
71
|
+
win.open_speedbar(@speedbar)
|
72
|
+
else
|
73
|
+
# If already open, find next match.
|
74
|
+
win.open_speedbar(@speedbar)
|
75
|
+
IncrementalSearchSpeedbar.find_next
|
76
|
+
end
|
62
77
|
end
|
63
|
-
win.open_speedbar(@speedbar)
|
64
78
|
end
|
65
|
-
end
|
66
79
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
80
|
+
class OpenFindSpeedbarCommand < Redcar::EditTabCommand
|
81
|
+
def execute
|
82
|
+
@speedbar = FindSpeedbar.new
|
83
|
+
if doc.selection?
|
84
|
+
@speedbar.initial_query = doc.selected_text
|
85
|
+
end
|
86
|
+
win.open_speedbar(@speedbar)
|
72
87
|
end
|
73
|
-
win.open_speedbar(@speedbar)
|
74
88
|
end
|
75
|
-
end
|
76
89
|
|
77
|
-
|
78
|
-
|
79
|
-
|
90
|
+
class DoFindNextCommand < Redcar::EditTabCommand
|
91
|
+
def execute
|
92
|
+
if win.speedbar.is_a? IncrementalSearchSpeedbar
|
93
|
+
IncrementalSearchSpeedbar.find_next
|
94
|
+
else
|
95
|
+
FindSpeedbar.find_next
|
96
|
+
end
|
97
|
+
end
|
80
98
|
end
|
81
|
-
end
|
82
99
|
|
83
|
-
|
84
|
-
|
85
|
-
|
100
|
+
class DoFindPreviousCommand < Redcar::EditTabCommand
|
101
|
+
def execute
|
102
|
+
if win.speedbar.is_a? IncrementalSearchSpeedbar
|
103
|
+
IncrementalSearchSpeedbar.find_previous
|
104
|
+
else
|
105
|
+
FindSpeedbar.find_previous
|
106
|
+
end
|
107
|
+
end
|
86
108
|
end
|
87
|
-
end
|
88
109
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
110
|
+
class DoReplaceAndFindCommand < Redcar::EditTabCommand
|
111
|
+
def execute
|
112
|
+
FindSpeedbar.replace_and_find(
|
113
|
+
FindSpeedbar.previous_query,
|
114
|
+
FindSpeedbar.previous_replace,
|
115
|
+
FindSpeedbar.previous_options)
|
116
|
+
end
|
95
117
|
end
|
96
|
-
end
|
97
118
|
|
98
|
-
|
99
|
-
|
100
|
-
|
119
|
+
class DoReplaceAllCommand < Redcar::EditTabCommand
|
120
|
+
def execute
|
121
|
+
FindSpeedbar.replace_all(
|
122
|
+
FindSpeedbar.previous_query,
|
123
|
+
FindSpeedbar.previous_replace,
|
124
|
+
FindSpeedbar.previous_options)
|
125
|
+
end
|
101
126
|
end
|
102
|
-
end
|
103
127
|
|
104
|
-
|
105
|
-
|
106
|
-
|
128
|
+
class DoReplaceAllInSelectionCommand < Redcar::EditTabCommand
|
129
|
+
def execute
|
130
|
+
FindSpeedbar.replace_all_in_selection(
|
131
|
+
FindSpeedbar.previous_query,
|
132
|
+
FindSpeedbar.previous_replace,
|
133
|
+
FindSpeedbar.previous_options)
|
134
|
+
end
|
107
135
|
end
|
108
|
-
end
|
109
136
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
@wrap = wrap
|
137
|
+
class DoUseSelectionForFindCommand < Redcar::EditTabCommand
|
138
|
+
def execute
|
139
|
+
FindSpeedbar.use_selection_for_find(doc, win.speedbar)
|
140
|
+
end
|
115
141
|
end
|
116
142
|
|
117
|
-
|
118
|
-
|
143
|
+
class DoUseSelectionForReplaceCommand < Redcar::EditTabCommand
|
144
|
+
def execute
|
145
|
+
FindSpeedbar.use_selection_for_replace(doc, win.speedbar)
|
146
|
+
end
|
119
147
|
end
|
120
148
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
149
|
+
# TODO(yozhipozhi): Figure out if this is still needed.
|
150
|
+
class FindNextRegex < Redcar::DocumentCommand
|
151
|
+
def initialize(re, wrap=nil)
|
152
|
+
@re = re
|
153
|
+
@wrap = wrap
|
154
|
+
end
|
126
155
|
|
127
|
-
|
128
|
-
|
129
|
-
sc.reset
|
130
|
-
sc.scan_until(@re)
|
156
|
+
def to_s
|
157
|
+
"<#{self.class}: @re:#{@re.inspect} wrap:#{!!@wrap}>"
|
131
158
|
end
|
132
159
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
160
|
+
def execute
|
161
|
+
position = doc.cursor_offset
|
162
|
+
sc = StringScanner.new(doc.get_all_text)
|
163
|
+
sc.pos = position
|
164
|
+
sc.scan_until(@re)
|
165
|
+
|
166
|
+
if @wrap and !sc.matched?
|
167
|
+
# No match was found in the remainder of the document, search from beginning
|
168
|
+
sc.reset
|
169
|
+
sc.scan_until(@re)
|
170
|
+
end
|
171
|
+
|
172
|
+
if sc.matched?
|
173
|
+
endoff = sc.pos
|
174
|
+
startoff = sc.pos - sc.matched_size
|
175
|
+
line = doc.line_at_offset(startoff)
|
176
|
+
lineoff = startoff - doc.offset_at_line(line)
|
177
|
+
if lineoff < doc.smallest_visible_horizontal_index
|
178
|
+
horiz = lineoff
|
179
|
+
else
|
180
|
+
horiz = endoff - doc.offset_at_line(line)
|
181
|
+
end
|
182
|
+
doc.set_selection_range(sc.pos, sc.pos - sc.matched_size)
|
183
|
+
doc.scroll_to_line(line)
|
184
|
+
doc.scroll_to_horizontal_offset(horiz) if horiz
|
185
|
+
return true
|
142
186
|
end
|
143
|
-
|
144
|
-
doc.scroll_to_line(line)
|
145
|
-
doc.scroll_to_horizontal_offset(horiz) if horiz
|
146
|
-
return true
|
187
|
+
false
|
147
188
|
end
|
148
|
-
false
|
149
189
|
end
|
150
190
|
end
|
151
191
|
end
|
@@ -1,257 +1,306 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# An instance of a search type method: Plain text search
|
12
|
-
def query_plain(query, options)
|
13
|
-
query_regex(Regexp.escape(query), options)
|
14
|
-
end
|
1
|
+
module Redcar
|
2
|
+
module DocumentSearch
|
3
|
+
# Utilities for extended search commands.
|
4
|
+
module FindCommandMixin
|
5
|
+
### QUERY PATTERNS ###
|
6
|
+
|
7
|
+
# Indicates if the query is valid.
|
8
|
+
def is_valid(query)
|
9
|
+
query.inspect != "//i"
|
10
|
+
end
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# convert the glob pattern to a regex pattern
|
20
|
-
new_query = ""
|
21
|
-
query.each_char do |c|
|
22
|
-
case c
|
23
|
-
when "*"
|
24
|
-
new_query << ".*"
|
25
|
-
when "?"
|
26
|
-
new_query << "."
|
27
|
-
else
|
28
|
-
new_query << Regexp.escape(c)
|
29
|
-
end
|
12
|
+
# An instance of a search type method: Regular expression
|
13
|
+
def make_regex_query(query, options)
|
14
|
+
Regexp.new(query, !options.match_case)
|
30
15
|
end
|
31
|
-
query_regex(new_query, options)
|
32
|
-
end
|
33
16
|
|
34
|
-
|
17
|
+
# An instance of a search type method: Plain text search
|
18
|
+
def make_literal_query(query, options)
|
19
|
+
make_regex_query(Regexp.escape(query), options)
|
20
|
+
end
|
35
21
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
22
|
+
### SELECTION ###
|
23
|
+
|
24
|
+
# Returns the document selection range as byte offsets, adjusting for multi-byte characters.
|
25
|
+
def selection_byte_offsets
|
26
|
+
char_offsets = [doc.cursor_offset, doc.selection_offset]
|
27
|
+
min_char_offset = char_offsets.min
|
28
|
+
max_char_offset = char_offsets.max
|
29
|
+
|
30
|
+
# For the min_byte_offset, get all document text before the selection, and count the bytes.
|
31
|
+
min_byte_offset = doc.get_range(0, min_char_offset).size
|
32
|
+
# If the selection is non-empty, count the bytes in the selection text, too.
|
33
|
+
max_byte_offset = (min_byte_offset +
|
34
|
+
(max_char_offset > min_char_offset ?
|
35
|
+
doc.get_slice(min_char_offset, max_char_offset).size :
|
36
|
+
0))
|
37
|
+
[min_byte_offset, max_byte_offset]
|
38
|
+
end
|
44
39
|
|
45
|
-
|
40
|
+
# Selects the first match of query, starting from the start_pos.
|
41
|
+
def select_next_match(doc, start_pos, query, wrap_around)
|
42
|
+
return false unless is_valid(query)
|
43
|
+
scanner = StringScanner.new(doc.get_all_text)
|
44
|
+
scanner.pos = start_pos
|
46
45
|
if not scanner.scan_until(query)
|
47
|
-
|
46
|
+
if not wrap_around
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
scanner.reset
|
51
|
+
if not scanner.scan_until(query)
|
52
|
+
return false
|
53
|
+
end
|
48
54
|
end
|
55
|
+
|
56
|
+
selection_pos = scanner.pos - scanner.matched_size
|
57
|
+
select_range_bytes(selection_pos, scanner.pos)
|
58
|
+
true
|
49
59
|
end
|
50
60
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
61
|
+
# Selects the match that first precedes the search position.
|
62
|
+
#
|
63
|
+
# The current implementation is brain-dead, but works: the document is scanned from the start
|
64
|
+
# up to the search position, retaining the most recent match. Many smarter, but more
|
65
|
+
# complicated strategies are possible; the best would be full reversal of the query regex, but
|
66
|
+
# that obviously has a lot of tricky aspects to it.
|
67
|
+
def select_previous_match(doc, search_pos, query, wrap_around)
|
68
|
+
return false unless is_valid(query)
|
69
|
+
previous_match = nil
|
70
|
+
scanner = StringScanner.new(doc.get_all_text)
|
71
|
+
scanner.pos = 0
|
72
|
+
while scanner.scan_until(query)
|
73
|
+
start_pos = scanner.pos - scanner.matched_size
|
74
|
+
if start_pos < search_pos
|
75
|
+
previous_match = [start_pos, scanner.pos]
|
76
|
+
elsif previous_match
|
77
|
+
select_range_bytes(*previous_match)
|
78
|
+
return true
|
79
|
+
elsif not wrap_around
|
80
|
+
return false
|
81
|
+
else
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
55
85
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# up to the search position, retaining the most recent match. Many smarter, but more
|
60
|
-
# complicated strategies are possible; the best would be full reversal of the query regex, but
|
61
|
-
# that obviously has a lot of tricky aspects to it.
|
62
|
-
def select_previous_match(doc, search_pos, query, wrap_around)
|
63
|
-
previous_match = nil
|
64
|
-
scanner = StringScanner.new(doc.get_all_text)
|
65
|
-
scanner.pos = 0
|
66
|
-
while scanner.scan_until(query)
|
67
|
-
start_pos = scanner.pos - scanner.matched_size
|
68
|
-
if start_pos < search_pos
|
86
|
+
# Find the last match in the document.
|
87
|
+
while scanner.scan_until(query)
|
88
|
+
start_pos = scanner.pos - scanner.matched_size
|
69
89
|
previous_match = [start_pos, scanner.pos]
|
70
|
-
|
71
|
-
|
90
|
+
end
|
91
|
+
|
92
|
+
if previous_match
|
93
|
+
select_range_bytes(*previous_match)
|
72
94
|
return true
|
73
|
-
elsif not wrap_around
|
74
|
-
return false
|
75
95
|
else
|
76
|
-
|
96
|
+
return false
|
77
97
|
end
|
78
98
|
end
|
79
99
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
100
|
+
# Replaces the current selection, if it matches the query completely.
|
101
|
+
def replace_selection_if_match(doc, start_pos, query, replace)
|
102
|
+
scanner = StringScanner.new(doc.selected_text)
|
103
|
+
scanner.check(query)
|
104
|
+
if (not scanner.matched?) || (scanner.matched_size != doc.selected_text.length)
|
105
|
+
return 0
|
106
|
+
end
|
107
|
+
matched_text = doc.get_range(start_pos, scanner.matched_size)
|
108
|
+
replacement_text = matched_text.gsub(query, replace)
|
109
|
+
doc.replace(start_pos, scanner.matched_size, replacement_text)
|
110
|
+
replacement_text.length
|
91
111
|
end
|
92
|
-
end
|
93
112
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
113
|
+
# Selects the specified range and scrolls to the start.
|
114
|
+
def select_range(start, stop)
|
115
|
+
line = doc.line_at_offset(start)
|
116
|
+
lineoff = start - doc.offset_at_line(line)
|
117
|
+
if lineoff < doc.smallest_visible_horizontal_index
|
118
|
+
horiz = lineoff
|
119
|
+
else
|
120
|
+
horiz = stop - doc.offset_at_line(line)
|
121
|
+
end
|
122
|
+
doc.set_selection_range(start, stop)
|
123
|
+
doc.scroll_to_line(line)
|
124
|
+
doc.scroll_to_horizontal_offset(horiz) if horiz
|
100
125
|
end
|
101
|
-
matched_text = doc.get_range(start_pos, scanner.matched_size)
|
102
|
-
replacement_text = matched_text.gsub(query, replace)
|
103
|
-
doc.replace(start_pos, scanner.matched_size, replacement_text)
|
104
|
-
replacement_text.length
|
105
|
-
end
|
106
126
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
127
|
+
# Selects the specified byte range, mapping to character indices first.
|
128
|
+
#
|
129
|
+
# This method is necessary, because Ruby (1.8) strings really work in terms of bytes, and thus
|
130
|
+
# our regex and scanning matches return byte ranges, while the editor view deals in terms of
|
131
|
+
# character ranges.
|
132
|
+
def select_range_bytes(start, stop)
|
133
|
+
text = doc.get_all_text
|
134
|
+
# Unpack span up to start into array of Unicode chars and count for start_chars.
|
135
|
+
start_chars = text.slice(0, start).unpack('U*').size
|
136
|
+
# Do the same for the span between start and stop, and then use to compute stop_chars.
|
137
|
+
char_span = text.slice(start, stop - start).unpack('U*').size
|
138
|
+
stop_chars = start_chars + char_span
|
139
|
+
select_range(start_chars, stop_chars)
|
115
140
|
end
|
116
|
-
doc.set_selection_range(start, stop)
|
117
|
-
doc.scroll_to_line(line)
|
118
|
-
doc.scroll_to_horizontal_offset(horiz) if horiz
|
119
141
|
end
|
120
|
-
end
|
121
142
|
|
122
143
|
|
123
|
-
|
124
|
-
|
125
|
-
|
144
|
+
# Base class for find commands.
|
145
|
+
class FindCommandBase < Redcar::DocumentCommand
|
146
|
+
include FindCommandMixin
|
126
147
|
|
127
|
-
|
148
|
+
attr_reader :query, :options, :always_start_within_selection
|
128
149
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
150
|
+
# description here
|
151
|
+
def initialize(q, opt)
|
152
|
+
@options = opt
|
153
|
+
@query = opt.is_regex ? make_regex_query(q, opt) : make_literal_query(q, opt)
|
154
|
+
end
|
133
155
|
end
|
134
|
-
end
|
135
156
|
|
136
157
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
if select_next_match(doc, start_pos, query, @options.wrap_around)
|
143
|
-
true
|
144
|
-
else
|
145
|
-
# Clear selection as visual feedback that search failed.
|
146
|
-
doc.set_selection_range(start_pos, start_pos)
|
147
|
-
false
|
158
|
+
# Finds the next match after the current location.
|
159
|
+
class FindNextCommand < FindCommandBase
|
160
|
+
def initialize(q, opt, always_start_within_selection=false)
|
161
|
+
super(q, opt)
|
162
|
+
@always_start_within_selection = always_start_within_selection
|
148
163
|
end
|
149
|
-
end
|
150
|
-
end
|
151
164
|
|
165
|
+
def execute
|
166
|
+
# We first determine where to start the search, either from the begin or end of the current
|
167
|
+
# selection.
|
168
|
+
#
|
169
|
+
# If always_start_within_selection is true, then we always start at the beginning; this is
|
170
|
+
# needed for incremental search.
|
171
|
+
#
|
172
|
+
# Otherwise, we check if the current selection matches the query:
|
173
|
+
# * If it does match, we start after the selection, assuming that the selection matches
|
174
|
+
# because of a prior search, and we want to move on to the next occurrence.
|
175
|
+
# * If it doesn't match, we start at the beginning of the selection, to handle cases where
|
176
|
+
# the selection is actually the start of a match, e.g. the "Foo" portion of "Foobar" is
|
177
|
+
# selected, and the user sets the query to "Foobar"; then we want Find Next to simply
|
178
|
+
# expand the selection to span "Foobar" as the next match.
|
179
|
+
#
|
180
|
+
# TODO(yozhipozhi): Test this behavior!
|
181
|
+
start_within_selection = true
|
182
|
+
if !@always_start_within_selection && (doc.selected_text.length > 0)
|
183
|
+
text = doc.selected_text
|
184
|
+
m = query.match(text)
|
185
|
+
if (m && (m[0].length == text.length))
|
186
|
+
start_within_selection = false
|
187
|
+
end
|
188
|
+
end
|
189
|
+
offsets = selection_byte_offsets
|
190
|
+
start_pos = start_within_selection ? offsets[0] : offsets[1]
|
152
191
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
# Clear selection as visual feedback that search failed.
|
162
|
-
doc.set_selection_range(start_pos, start_pos)
|
163
|
-
false
|
192
|
+
# Do selection.
|
193
|
+
if select_next_match(doc, start_pos, query, options.wrap_around)
|
194
|
+
true
|
195
|
+
else
|
196
|
+
# Clear selection as visual feedback that search failed.
|
197
|
+
select_range_bytes(start_pos, start_pos)
|
198
|
+
false
|
199
|
+
end
|
164
200
|
end
|
165
201
|
end
|
166
|
-
end
|
167
202
|
|
168
203
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
204
|
+
# Finds the previous match before the current location.
|
205
|
+
class FindPreviousCommand < FindCommandBase
|
206
|
+
def execute
|
207
|
+
offsets = selection_byte_offsets
|
208
|
+
start_pos = offsets.min
|
209
|
+
if select_previous_match(doc, start_pos, query, @options.wrap_around)
|
210
|
+
true
|
211
|
+
else
|
212
|
+
# Clear selection as visual feedback that search failed.
|
213
|
+
select_range_bytes(start_pos, start_pos)
|
214
|
+
false
|
215
|
+
end
|
180
216
|
end
|
181
217
|
end
|
182
|
-
end
|
183
218
|
|
184
219
|
|
185
|
-
|
186
|
-
|
187
|
-
|
220
|
+
# Base class for replace commands.
|
221
|
+
class ReplaceCommandBase < Redcar::DocumentCommand
|
222
|
+
include FindCommandMixin
|
188
223
|
|
189
|
-
|
224
|
+
attr_reader :query, :replace
|
190
225
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
226
|
+
# description here
|
227
|
+
def initialize(query, replace, options)
|
228
|
+
@options = options
|
229
|
+
@query =
|
230
|
+
options.is_regex ? make_regex_query(query, options) : make_literal_query(query, options)
|
231
|
+
@replace = replace
|
232
|
+
end
|
196
233
|
end
|
197
|
-
end
|
198
234
|
|
199
235
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
236
|
+
# Replaces the currently selected text, if it matches the search criteria, then finds and
|
237
|
+
# selects the next match in the document.
|
238
|
+
#
|
239
|
+
# This command maintains the invariant that no text is replaced without first being selected,
|
240
|
+
# so the user always knows exactly what change is about to be made. A ramification of this
|
241
|
+
# policy is that, if no text is selected beforehand, or the selected text does not match the
|
242
|
+
# query, then "replace" portion of "replace and find" is essentially skipped, so that two button
|
243
|
+
# presses are required.
|
244
|
+
class ReplaceAndFindCommand < ReplaceCommandBase
|
245
|
+
def execute
|
246
|
+
offsets = selection_byte_offsets
|
247
|
+
start_pos = offsets.min
|
248
|
+
if doc.selected_text.length > 0
|
249
|
+
chars_replaced = replace_selection_if_match(doc, start_pos, query, replace)
|
250
|
+
if chars_replaced > 0
|
251
|
+
start_pos += chars_replaced
|
252
|
+
else
|
253
|
+
start_pos = offsets.max
|
254
|
+
end
|
255
|
+
end
|
256
|
+
if select_next_match(doc, start_pos, query, @options.wrap_around)
|
257
|
+
true
|
216
258
|
else
|
217
|
-
|
259
|
+
# Clear selection as visual feedback that search failed.
|
260
|
+
select_range_bytes(start_pos, start_pos)
|
261
|
+
false
|
218
262
|
end
|
219
263
|
end
|
220
|
-
if select_next_match(doc, start_pos, query, @options.wrap_around)
|
221
|
-
true
|
222
|
-
else
|
223
|
-
# Clear selection as visual feedback that search failed.
|
224
|
-
doc.set_selection_range(start_pos, start_pos)
|
225
|
-
false
|
226
|
-
end
|
227
264
|
end
|
228
|
-
end
|
229
265
|
|
230
266
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
count = 0
|
237
|
-
sc = StringScanner.new(text)
|
238
|
-
while sc.scan_until(query)
|
239
|
-
count += 1
|
240
|
-
|
241
|
-
startoff = sc.pos - sc.matched_size
|
242
|
-
replacement_text = text.slice(startoff, sc.matched_size).gsub(query, replace)
|
243
|
-
endoff = startoff + replacement_text.length
|
244
|
-
|
245
|
-
text[startoff...sc.pos] = replacement_text
|
246
|
-
sc.string = text
|
247
|
-
sc.pos = startoff + replacement_text.length
|
267
|
+
# Replaces all query matches.
|
268
|
+
class ReplaceAllCommand < ReplaceCommandBase
|
269
|
+
def initialize(query, replace, options, selection_only)
|
270
|
+
super(query, replace, options)
|
271
|
+
@selection_only = selection_only
|
248
272
|
end
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
273
|
+
|
274
|
+
def execute
|
275
|
+
startoff, endoff = nil
|
276
|
+
text = @selection_only ? doc.selected_text : doc.get_all_text
|
277
|
+
count = 0
|
278
|
+
sc = StringScanner.new(text)
|
279
|
+
while sc.scan_until(query)
|
280
|
+
count += 1
|
281
|
+
|
282
|
+
startoff = sc.pos - sc.matched_size
|
283
|
+
replacement_text = text.slice(startoff, sc.matched_size).gsub(query, replace)
|
284
|
+
endoff = startoff + replacement_text.length
|
285
|
+
|
286
|
+
text[startoff...sc.pos] = replacement_text
|
287
|
+
sc.string = text
|
288
|
+
sc.pos = startoff + replacement_text.length
|
289
|
+
end
|
290
|
+
if count > 0
|
291
|
+
if @selection_only
|
292
|
+
offsets = selection_byte_offsets
|
293
|
+
startoff = offsets.min
|
294
|
+
doc.replace(startoff, doc.selected_text.length, text)
|
295
|
+
select_range_bytes(startoff, startoff + text.length)
|
296
|
+
else
|
297
|
+
doc.text = text
|
298
|
+
select_range_bytes(startoff, startoff + replacement_text.length)
|
299
|
+
end
|
300
|
+
true
|
301
|
+
else
|
302
|
+
false
|
303
|
+
end
|
255
304
|
end
|
256
305
|
end
|
257
306
|
end
|