redcar 0.10 → 0.11.0dev

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.
Files changed (77) hide show
  1. data/CHANGES +14 -0
  2. data/Rakefile +68 -29
  3. data/lib/redcar.rb +2 -2
  4. data/plugins/application/lib/application/dialogs/filter_list_dialog.rb +2 -1
  5. data/plugins/application_swt/spec/application_swt/gradient_spec.rb +3 -12
  6. data/plugins/core/lib/core/resource.rb +13 -5
  7. data/plugins/document_search/features/find.feature +366 -214
  8. data/plugins/document_search/features/incremental_search.feature +351 -0
  9. data/plugins/document_search/features/replace.feature +16 -16
  10. data/plugins/document_search/features/step_definitions/find_steps.rb +16 -0
  11. data/plugins/document_search/features/support/env.rb +11 -0
  12. data/plugins/document_search/lib/document_search.rb +149 -109
  13. data/plugins/document_search/lib/document_search/commands.rb +251 -202
  14. data/plugins/document_search/lib/document_search/find_speedbar.rb +138 -81
  15. data/plugins/document_search/lib/document_search/incremental_search_speedbar.rb +70 -0
  16. data/plugins/document_search/lib/document_search/query_options.rb +15 -39
  17. data/plugins/document_search/plugin.rb +1 -1
  18. data/plugins/edit_view/features/step_definitions/editing_steps.rb +6 -2
  19. data/plugins/edit_view_swt/lib/edit_view_swt.rb +2 -2
  20. data/plugins/file_parser/lib/file_parser.rb +6 -1
  21. data/plugins/html_view/features/step_definitions/web_view_steps.rb +12 -0
  22. data/plugins/html_view/features/support/env.rb +16 -0
  23. data/plugins/html_view/lib/html_view.rb +5 -1
  24. data/plugins/line_tools/lib/line_tools.rb +16 -0
  25. data/plugins/project/features/find_file.feature +28 -0
  26. data/plugins/project/features/open_and_save_files.feature +11 -0
  27. data/plugins/project/features/step_definitions/file_steps.rb +6 -1
  28. data/plugins/project/features/support/env.rb +2 -0
  29. data/plugins/project/lib/project.rb +49 -6
  30. data/plugins/project/lib/project/commands.rb +18 -6
  31. data/plugins/project/lib/project/find_file_dialog.rb +19 -8
  32. data/plugins/project/lib/project/find_recent_dialog.rb +30 -0
  33. data/plugins/project/lib/project/manager.rb +41 -10
  34. data/plugins/project/lib/project/recent.rb +64 -0
  35. data/plugins/project/spec/fixtures/myproject/vendor/bar.rb +0 -0
  36. data/plugins/project/spec/fixtures/myproject/vendor/plugins/bar.rb +0 -0
  37. data/plugins/{find-in-project → project_search}/TODO.md +3 -3
  38. data/plugins/project_search/features/support/env.rb +6 -0
  39. data/plugins/project_search/features/word_search.feature +34 -0
  40. data/plugins/project_search/lib/project_search.rb +73 -0
  41. data/plugins/project_search/lib/project_search/binary_data_detector.rb +46 -0
  42. data/plugins/project_search/lib/project_search/commands.rb +62 -0
  43. data/plugins/project_search/lib/project_search/hit.rb +17 -0
  44. data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/collapsed.png +0 -0
  45. data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/expanded.png +0 -0
  46. data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/images/spinner.gif +0 -0
  47. data/plugins/project_search/lib/project_search/lucene_index.rb +64 -0
  48. data/plugins/project_search/lib/project_search/lucene_refresh.rb +22 -0
  49. data/plugins/project_search/lib/project_search/project.rb +14 -0
  50. data/plugins/project_search/lib/project_search/query.rb +29 -0
  51. data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/stylesheets/style.css +14 -3
  52. data/plugins/project_search/lib/project_search/views/_file.html.erb +60 -0
  53. data/plugins/{find-in-project/lib/find_in_project → project_search/lib/project_search}/views/index.html.erb +12 -9
  54. data/plugins/project_search/lib/project_search/word_search.rb +105 -0
  55. data/plugins/project_search/lib/project_search/word_search_controller.rb +207 -0
  56. data/plugins/project_search/plugin.rb +8 -0
  57. data/plugins/project_search/spec/fixtures/project/binary_file.bin +0 -0
  58. data/plugins/project_search/spec/fixtures/project/foo.txt +43 -0
  59. data/plugins/project_search/spec/fixtures/project/qux.rb +3 -0
  60. data/plugins/project_search/spec/project_search/binary_data_detector_spec.rb +24 -0
  61. data/plugins/project_search/spec/project_search/word_search_spec.rb +157 -0
  62. data/plugins/project_search/spec/spec_helper.rb +27 -0
  63. data/plugins/redcar/redcar.rb +77 -71
  64. data/plugins/ruby/lib/ruby/syntax_checker.rb +1 -1
  65. data/plugins/snippets/features/snippets.feature +12 -0
  66. data/plugins/snippets/lib/snippets/tab_handler.rb +1 -1
  67. metadata +46 -25
  68. data/plugins/document_search/features/find_and_replace.feature +0 -723
  69. data/plugins/document_search/lib/document_search/find_and_replace_speedbar.rb +0 -142
  70. data/plugins/find-in-project/lib/find_in_project.rb +0 -35
  71. data/plugins/find-in-project/lib/find_in_project/commands.rb +0 -30
  72. data/plugins/find-in-project/lib/find_in_project/controllers.rb +0 -170
  73. data/plugins/find-in-project/lib/find_in_project/views/_divider.html.erb +0 -4
  74. data/plugins/find-in-project/lib/find_in_project/views/_file_heading.html.erb +0 -10
  75. data/plugins/find-in-project/lib/find_in_project/views/_file_line.html.erb +0 -6
  76. data/plugins/find-in-project/plugin.rb +0 -11
  77. 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/find_and_replace_speedbar"
6
-
7
- module DocumentSearch
8
- def self.menus
9
- Redcar::Menu::Builder.build do
10
- sub_menu "Edit" do
11
- sub_menu "Find", :priority => 50 do
12
- item "Incremental Search", FindMenuCommand
13
- item "Find...", FindAndReplaceSpeedbarCommand
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
- def self.keymaps
28
- osx = Redcar::Keymap.build("main", :osx) do
29
- link "Ctrl+S", DocumentSearch::FindMenuCommand
30
- link "Cmd+F", DocumentSearch::FindAndReplaceSpeedbarCommand
31
- link "Cmd+G", DocumentSearch::FindNextMenuCommand
32
- link "Cmd+Shift+G", DocumentSearch::FindPreviousMenuCommand
33
- link "Cmd+Alt+F", DocumentSearch::ReplaceAndFindMenuCommand
34
- link "Cmd+E", DocumentSearch::UseSelectionForFindMenuCommand
35
- link "Cmd+Shift+E", DocumentSearch::UseSelectionForReplaceMenuCommand
36
- end
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
- linwin = Redcar::Keymap.build("main", [:linux, :windows]) do
39
- link "Alt+S", DocumentSearch::FindMenuCommand
40
- link "Ctrl+F", DocumentSearch::FindAndReplaceSpeedbarCommand
41
- link "Ctrl+G", DocumentSearch::FindNextMenuCommand
42
- link "Ctrl+Shift+G", DocumentSearch::FindPreviousMenuCommand
43
- link "Ctrl+E", DocumentSearch::UseSelectionForFindMenuCommand
44
- link "Alt+E", DocumentSearch::UseSelectionForReplaceMenuCommand
45
- end
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
- [linwin, osx]
48
- end
54
+ [linwin, osx]
55
+ end
49
56
 
50
- def self.toolbars
51
- Redcar::ToolBar::Builder.build do
52
- item "Find", :command => DocumentSearch::FindMenuCommand, :icon => File.join(Redcar::ICONS_DIRECTORY, "magnifier.png"), :barname => :edit
53
- item "Find Next", :command => DocumentSearch::FindNextMenuCommand, :icon => File.join(Redcar::ICONS_DIRECTORY, "magnifier--arrow.png"), :barname => :edit
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
- class FindMenuCommand < Redcar::EditTabCommand
58
- def execute
59
- @speedbar = FindSpeedbar.new
60
- if doc.selection?
61
- @speedbar.initial_query = doc.selected_text
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
- class FindAndReplaceSpeedbarCommand < Redcar::EditTabCommand
68
- def execute
69
- @speedbar = FindAndReplaceSpeedbar.new
70
- if doc.selection?
71
- @speedbar.initial_query = doc.selected_text
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
- class FindNextMenuCommand < Redcar::EditTabCommand
78
- def execute
79
- FindSpeedbar.find_next
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
- class FindPreviousMenuCommand < Redcar::EditTabCommand
84
- def execute
85
- FindSpeedbar.find_previous
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
- class ReplaceAndFindMenuCommand < Redcar::EditTabCommand
90
- def execute
91
- FindAndReplaceSpeedbar.replace_and_find(
92
- FindSpeedbar.previous_query,
93
- FindAndReplaceSpeedbar.previous_replace,
94
- FindSpeedbar.previous_options)
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
- class UseSelectionForFindMenuCommand < Redcar::EditTabCommand
99
- def execute
100
- FindSpeedbar.use_selection_for_find(doc, win.speedbar)
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
- class UseSelectionForReplaceMenuCommand < Redcar::EditTabCommand
105
- def execute
106
- FindAndReplaceSpeedbar.use_selection_for_replace(doc, win.speedbar)
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
- # TODO(yozhipozhi): Figure out if this is still needed.
111
- class FindNextRegex < Redcar::DocumentCommand
112
- def initialize(re, wrap=nil)
113
- @re = re
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
- def to_s
118
- "<#{self.class}: @re:#{@re.inspect} wrap:#{!!@wrap}>"
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
- def execute
122
- position = doc.cursor_offset
123
- sc = StringScanner.new(doc.get_all_text)
124
- sc.pos = position
125
- sc.scan_until(@re)
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
- if @wrap and !sc.matched?
128
- # No match was found in the remainder of the document, search from beginning
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
- if sc.matched?
134
- endoff = sc.pos
135
- startoff = sc.pos - sc.matched_size
136
- line = doc.line_at_offset(startoff)
137
- lineoff = startoff - doc.offset_at_line(line)
138
- if lineoff < doc.smallest_visible_horizontal_index
139
- horiz = lineoff
140
- else
141
- horiz = endoff - doc.offset_at_line(line)
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
- doc.set_selection_range(sc.pos, sc.pos - sc.matched_size)
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 DocumentSearch
2
- # Utilities for extended search commands.
3
- module FindCommandMixin
4
- ### QUERY PATTERNS ###
5
-
6
- # An instance of a search type method: Regular expression
7
- def query_regex(query, options)
8
- Regexp.new(query, !options.match_case)
9
- end
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
- # An instance of a search type method: Glob text search
17
- # Converts a glob pattern (* or ?) into a regex pattern
18
- def query_glob(query, options)
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
- ### SELECTION ###
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
- # Selects the first match of query, starting from the start_pos.
37
- def select_next_match(doc, start_pos, query, wrap_around)
38
- scanner = StringScanner.new(doc.get_all_text)
39
- scanner.pos = start_pos
40
- if not scanner.scan_until(query)
41
- if not wrap_around
42
- return false
43
- end
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
- scanner.reset
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
- return false
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
- selection_pos = scanner.pos - scanner.matched_size
52
- select_range(selection_pos, scanner.pos)
53
- true
54
- end
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
- # Selects the match that first precedes the search position.
57
- #
58
- # The current implementation is brain-dead, but works: the document is scanned from the start
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
- elsif previous_match
71
- select_range(*previous_match)
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
- break
96
+ return false
77
97
  end
78
98
  end
79
99
 
80
- # Find the last match in the document.
81
- while scanner.scan_until(query)
82
- start_pos = scanner.pos - scanner.matched_size
83
- previous_match = [start_pos, scanner.pos]
84
- end
85
-
86
- if previous_match
87
- select_range(*previous_match)
88
- return true
89
- else
90
- return false
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
- # Replaces the current selection, if it matches the query completely.
95
- def replace_selection_if_match(doc, start_pos, query, replace)
96
- scanner = StringScanner.new(doc.selected_text)
97
- scanner.check(query)
98
- if (not scanner.matched?) || (scanner.matched_size != doc.selected_text.length)
99
- return 0
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
- # Selects the specified range and scrolls to the start.
108
- def select_range(start, stop)
109
- line = doc.line_at_offset(start)
110
- lineoff = start - doc.offset_at_line(line)
111
- if lineoff < doc.smallest_visible_horizontal_index
112
- horiz = lineoff
113
- else
114
- horiz = stop - doc.offset_at_line(line)
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
- # Base class for find commands.
124
- class FindCommandBase < Redcar::DocumentCommand
125
- include FindCommandMixin
144
+ # Base class for find commands.
145
+ class FindCommandBase < Redcar::DocumentCommand
146
+ include FindCommandMixin
126
147
 
127
- attr_reader :query
148
+ attr_reader :query, :options, :always_start_within_selection
128
149
 
129
- # description here
130
- def initialize(query, options)
131
- @options = options
132
- @query = send(options.query_type, query, options)
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
- # Finds the next match after the current location.
138
- class FindIncrementalCommand < FindCommandBase
139
- def execute
140
- offsets = [doc.cursor_offset, doc.selection_offset]
141
- start_pos = offsets.min
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
- # Finds the next match after the current location.
154
- class FindNextCommand < FindCommandBase
155
- def execute
156
- offsets = [doc.cursor_offset, doc.selection_offset]
157
- start_pos = offsets.max
158
- if select_next_match(doc, start_pos, query, @options.wrap_around)
159
- true
160
- else
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
- # Finds the previous match before the current location.
170
- class FindPreviousCommand < FindCommandBase
171
- def execute
172
- offsets = [doc.cursor_offset, doc.selection_offset]
173
- start_pos = offsets.min
174
- if select_previous_match(doc, start_pos, query, @options.wrap_around)
175
- true
176
- else
177
- # Clear selection as visual feedback that search failed.
178
- doc.set_selection_range(start_pos, start_pos)
179
- false
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
- # Base class for replace commands.
186
- class ReplaceCommandBase < Redcar::DocumentCommand
187
- include FindCommandMixin
220
+ # Base class for replace commands.
221
+ class ReplaceCommandBase < Redcar::DocumentCommand
222
+ include FindCommandMixin
188
223
 
189
- attr_reader :query, :replace
224
+ attr_reader :query, :replace
190
225
 
191
- # description here
192
- def initialize(query, replace, options)
193
- @options = options
194
- @query = send(options.query_type, query, options)
195
- @replace = replace
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
- # Replaces the currently selected text, if it matches the search criteria, then finds and
201
- # selects the next match in the document.
202
- #
203
- # This command maintains the invariant that no text is replaced without first being selected, so
204
- # the user always knows exactly what change is about to be made. A ramification of this policy
205
- # is that, if no text is selected beforehand, or the selected text does not match the query,
206
- # then "replace" portion of "replace and find" is essentially skipped, so that two button
207
- # presses are required.
208
- class ReplaceAndFindCommand < ReplaceCommandBase
209
- def execute
210
- offsets = [doc.cursor_offset, doc.selection_offset]
211
- start_pos = offsets.min
212
- if doc.selected_text.length > 0
213
- chars_replaced = replace_selection_if_match(doc, start_pos, query, replace)
214
- if chars_replaced > 0
215
- start_pos += chars_replaced
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
- start_pos = offsets.max
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
- # Replaces all query matches.
232
- class ReplaceAllCommand < ReplaceCommandBase
233
- def execute
234
- startoff, endoff = nil
235
- text = doc.get_all_text
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
- if count > 0
250
- doc.text = text
251
- select_range(startoff + replacement_text.length, startoff)
252
- true
253
- else
254
- false
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