redcar 0.10 → 0.11.0dev

Sign up to get free protection for your applications and to get access to all the features.
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