howl.ui.SearchView
local command_line, search_view, list_widget, editor, buffer, finish_result, search, preview_buffer, error_message, info_message, find_iter_count
keypress = (keystroke) ->
search_view.keymap[keystroke] search_view
howl.app\pump_mainloop! -- allow all timer.asap functions to run
keypress_binding_for = (cmd) ->
search_view.keymap.binding_for[cmd] search_view
howl.app\pump_mainloop! -- allow all timer.asap functions to run
set_text = (sv, text) ->
command_line.text = text
sv\on_text_changed text
if sv.searcher.running
howl.dispatch.wait sv.searcher.running
howl.app\pump_mainloop! -- allow all timer.asap functions to run
list_widget_items = -> list_widget.list._items
list_widget_height = -> list_widget.height
found_count = ->
return unless info_message
_, _, count = info_message\find '(%d+) match'
tonumber count
before_each ->
preview_buffer = nil
editor = {
preview: spy.new (b) => preview_buffer = b
cancel_preview: spy.new =>
line_at_top: 1
line_at_bottom: 1
}
command_line =
add_widget: spy.new (name, w) => list_widget = w if name == 'matches'
notification: NotificationWidget!
finish: spy.new (r) => finish_result = r
info_message = '<not-set>'
error_message = '<not-set>'
command_line.notification.error = spy.new (self, msg) -> error_message = msg
command_line.notification.info = spy.new (self, msg) -> info_message = msg
command_line.notification.clear = spy.new (self, msg) ->
info_message = ''
error_message = ''
buffer = howl.Buffer!
editor.buffer = buffer
search = spy.new (query) ->
return if query.is_empty
start = 1
find_iter_count = 0
text = buffer.text
->
start_pos, end_pos = text\ufind query, start, true
return unless start_pos
start = end_pos + 1
find_iter_count += 1
buffer\chunk start_pos, end_pos
(on initialization)
search_view = SearchView
editor: editor
buffer: buffer
:search
search_view\init command_line, max_height: 100
assert.spy(command_line.add_widget).was_called 1
assert.same "ListWidget", typeof(list_widget)
displays the specified prompt and title
search_view = SearchView
editor: editor
buffer: buffer
:search
prompt: 'hello>'
title: '#hello'
search_view\init command_line, max_height: 100
assert.same 'hello>', command_line.prompt
assert.same '#hello', command_line.title
displays no info message
buffer.text = 'content'
search_view = SearchView
editor: editor
buffer: buffer
:search
search_view\init command_line, max_height: 100
set_text search_view, ''
assert.is_nil found_count!
displays all lines of buffer
buffer.text = 'hello1\nhello2\nhello3'
search_view = SearchView
editor: editor
buffer: buffer
:search
search_view\init command_line, max_height: 100
set_text search_view, ''
items = list_widget_items!
assert.same {'hello1', 'hello2', 'hello3'}, [item[2].text for item in *items]
assert.same {}, [item.item_highlights for item in *items]
does not call search for empty query
search = spy.new ->
search_view = SearchView
editor: editor
buffer: buffer
:search
search_view\init command_line, max_height: 100
set_text search_view, ''
assert.spy(search).was_not_called!
(searching)
does not leak parked dispatches when multiple queries are fired rapidly
buffer.text = 'content-line1\ncontent-line2\n'
search_view = SearchView
:editor
:buffer
:search
search_view\init command_line, max_height: 100
parked_before = howl.dispatch.nr_parked!
for query in *{'l', 'li', 'lin', 'line'}
command_line.text = query
search_view\on_text_changed query
if search_view.searcher.running
howl.dispatch.wait search_view.searcher.running
howl.app\pump_mainloop!
assert.same parked_before, howl.dispatch.nr_parked!
calls search(query) iterator when text is updated
buffer.text = 'buffer-content'
search_view = SearchView
:editor
:buffer
:search
search_view\init command_line, max_height: 100
set_text search_view, 'query-target'
assert.spy(search).was_called_with 'query-target'
(.. when query text is set)
before_each ->
buffer.text = 'cöntent-line1\ncöntent-line12\ncontent-line3'
search_view = SearchView
:editor
:buffer
:search
search_view\init command_line, max_height: 100
iterates over search(query)
set_text search_view, 'line1'
assert.spy(search).was_called 1
assert.spy(search).was_called_with 'line1'
assert.same 2, find_iter_count
displays the matching lines, at most one match per line
set_text search_view, 'line1'
items = list_widget_items!
assert.same {1, 2}, [item[1] for item in *items]
assert.same {'cöntent-line1', 'cöntent-line12'}, [item[2].text for item in *items]
set_text search_view, 'line3'
items = list_widget_items!
assert.same {3}, [item[1] for item in *items]
assert.same {'content-line3'}, [item[2].text for item in *items]
displays an info message with match count
set_text search_view, 'line1'
assert.same 2, found_count!
set_text search_view, 'line12'
assert.same 1, found_count!
set_text search_view, 'line-'
assert.same 0, found_count!
resets info message when query text is cleared
set_text search_view, 'line1'
assert.same 2, found_count!
set_text search_view, ''
assert.is_nil found_count!
set_text search_view, 'li'
height = list_widget_height!
set_text search_view, 'line1'
assert.same height, list_widget_height!
returns selected match on pressing enter
set_text search_view, 'line1'
assert.spy(command_line.finish).was_not_called!
keypress_binding_for 'cursor-down'
keypress 'enter'
assert.spy(command_line.finish).was_called 1
assert.same 23, finish_result.chunk.start_pos
assert.same 27, finish_result.chunk.end_pos
displays error message on pressing enter when no selection
set_text search_view, 'xxxxxxxxxxxxx'
keypress 'enter'
assert.spy(command_line.finish).was_not_called!
assert.spy(command_line.notification.error).was_called 1
assert.same error_message, 'No selection'
(.. selected line)
is centered
set_text search_view, 'line1'
assert.same 1, editor.line_at_center
is changed by keypresses
set_text search_view, 'line1'
keypress_binding_for 'cursor-down'
assert.same 2, editor.line_at_center
keypress_binding_for 'cursor-up'
assert.same 1, editor.line_at_center
(.. when search raises an error)
before_each ->
search_view = SearchView
:editor
:buffer
search: -> error 'search-error', 0
search_view\init command_line, max_height: 100
displays the error message
set_text search_view, 'query'
assert.same 'search-error', error_message
(.. when limit is set)
before_each ->
buffer.text = 'a a a a a a a a'
search_view = SearchView
:editor
:buffer
:search
limit: 3
search_view\init command_line, max_height: 100
does not iterate once limit matches are found
set_text search_view, 'a'
assert.same 3, find_iter_count
(replacing)
local replace
before_each ->
replace = spy.new (_, _, _, replacement)-> replacement
search = spy.new (query) ->
return if query.is_empty
start = 1
find_iter_count = 0
text = buffer.text
->
start_pos, end_pos = text\ufind query, start, true
return unless start_pos
start = end_pos + 1
find_iter_count += 1
buffer\chunk(start_pos, end_pos), 'match-info'
parses query and calls search(query) with the search part
buffer.text = 'buffer-content'
search_view = SearchView
:editor
:buffer
:search
:replace
search_view\init command_line, max_height: 100
set_text search_view, '/search'
assert.spy(search).was_called_with 'search'
displays all matches
buffer.text = 'cöntent-line1\ncöntent-line2\n'
search_view = SearchView
:editor
:buffer
:search
:replace
search_view\init command_line, max_height: 100
set_text search_view, '/t'
assert.same {'cöntent-line1', 'cöntent-line1', 'cöntent-line2', 'cöntent-line2'}, [item[2].text for item in *list_widget_items!]
(.. when the query includes a replacement)
before_each ->
buffer.text = 'content-line1\ncontent-line2\n'
search_view = SearchView
:editor
:buffer
:search
:replace
search_view\init command_line, max_height: 100
calls replace for each match
set_text search_view, '/content/<new>'
assert.spy(replace).was_called 2
assert.spy(replace).was_called_with match._, 'match-info', match._, '<new>'
displays a preview with replacements applied
editor.line_at_top = 1
editor.line_at_bottom = 2
set_text search_view, '/line/<new>'
assert.spy(editor.preview).was_called 1
assert.same 'content-<new>1\ncontent-<new>2\n', preview_buffer.text
assert.same {'search'}, howl.ui.highlight.at_pos preview_buffer, 9
returns text with replacements applied as previewed
set_text search_view, '/line/<new>'
text = preview_buffer.text
keypress 'enter'
assert.same text, finish_result.replacement_text
assert.same 2, finish_result.replacement_count
(.. if no replacement text)
previews deletions using strikeout style
editor.line_at_top = 1
editor.line_at_bottom = 2
set_text search_view, '/line/'
text = preview_buffer.text
assert.same buffer.text, text
assert.same {'replace_strikeout', 'search'}, howl.ui.highlight.at_pos preview_buffer, 9
assert.same {'replace_strikeout', 'search'}, howl.ui.highlight.at_pos preview_buffer, 10
returns text with deletions applied
set_text search_view, '/line/'
keypress 'enter'
assert.same 'content-1\ncontent-2\n', finish_result.replacement_text
(.. if no trailing delimiter)
previews existing buffer
set_text search_view, '/line'
assert.equal preview_buffer, buffer
returns nil
set_text search_view, '/line'
keypress 'enter'
assert.is_nil finish_result