howl.file_search
local searchers, tmp_dir, config, searcher
setup ->
searchers = {name, def for name, def in pairs file_search.searchers}
for name in pairs searchers
file_search.unregister_searcher name
tmp_dir = File.tmpdir!
config = howl.config.for_file tmp_dir
teardown ->
for name in pairs file_search.searchers
file_search.unregister_searcher name
for _, def in pairs searchers
file_search.register_searcher def
tmp_dir\delete_all!
before_each ->
searcher = {
name: 'test'
description: 'test'
handler: -> {}
}
register_searcher(def)
raises an error for missing attributes
assert.raises "name", ->
file_search.register_searcher description: 'desc', handler: ->
assert.raises "description", ->
file_search.register_searcher name: 'name', handler: ->
assert.raises "handler", ->
file_search.register_searcher name: 'test', description: 'desc'
search(directory, term, opts)
before_each ->
file_search.register_searcher searcher
config.file_searcher = 'test'
returns matches and the used searcher
matches = {}
searcher.handler = -> matches
_, used_searcher = file_search.search tmp_dir, 'foo'
assert.equal searcher, used_searcher
(when the searcher returns matches directly)
returns matches from the specified searcher
matches = {
{ path: 'urk.txt', file: tmp_dir\join('urk'), line_nr: 1, message: 'foo'}
}
searcher.handler = -> matches
res = file_search.search tmp_dir, 'foo'
assert.same matches, res
raises an error if the searcher omits required match fields
matches = {}
searcher.handler = -> matches
matches[1] = { line_nr: 1, message: 'foo' }
assert.raises 'path', -> file_search.search tmp_dir, 'foo'
matches[1] = { path: 'urk.txt', message: 'foo' }
assert.raises 'line_nr', -> file_search.search tmp_dir, 'foo'
matches[1] = { path: 'urk.txt', line_nr: 1 }
assert.raises 'message', -> file_search.search tmp_dir, 'foo'
sets .file from path if not provided
searcher.handler = -> { { path: 'urk.txt', line_nr: 1, message: 'foo'} }
res = file_search.search tmp_dir, 'foo'
assert.equal tmp_dir\join('urk.txt'), res[1].file
(when the searcher returns a process object)
it "returns matches from the process' output", (done) ->
howl_async ->
searcher.handler = -> Process.open_pipe 'echo "file.ext:10: foo"'
res = file_search.search tmp_dir, 'foo'
assert.same {
{message: 'foo', path: 'file.ext', line_nr: 10, file: tmp_dir\join('file.ext')}
}, res
done!
(when the searcher returns a string)
it 'returns matches from running the string as a command', (done) ->
howl_async ->
searcher.handler = -> 'echo "file.ext:10: foo"'
res = file_search.search tmp_dir, 'foo'
assert.same {
{message: 'foo', path: 'file.ext', line_nr: 10, file: tmp_dir\join('file.ext')}
}, res
done!
(when a search process exits with an exit code of 1)
it 'return zero matches', (done) ->
howl_async ->
searcher.handler = -> 'exit 1'
res = file_search.search tmp_dir, 'foo'
assert.same {}, res
done!
(selecting the searcher)
raises an error if the specified searcher is not available
searcher.is_available = -> false
assert.raises 'unavailable', -> file_search.search tmp_dir, 'foo'
allows passing an explicit searcher using an explicit `searcher` table
my_searcher = {
name: 'custom',
description: 'pass-directly',
handler: -> {
{ line_nr: 1, file: tmp_dir\join('my'), path: 'my', message: 'custom' }
}
}
res = file_search.search tmp_dir, 'foo', searcher: my_searcher
assert.same my_searcher.handler!, res
allows passing an explicit searcher using an explicit `searcher` string
my_searcher = {
name: 'my_searcher',
description: 'pass-directly',
handler: -> {
{ line_nr: 1, file: tmp_dir\join('my'), path: 'my', message: 'custom' }
}
}
file_search.register_searcher my_searcher
res = file_search.search tmp_dir, 'foo', searcher: 'my_searcher'
assert.same my_searcher.handler!, res
sort(matches, context)
match = (message, path, line_nr = 1) ->
{:message, :path, :line_nr, file: tmp_dir\join(path)}
messages = (matches) -> [m.message for m in *matches]
prefers standalone matches to substring matches
sorted = file_search.sort {
match('a fool', 'sub1')
match('bar foo zed', 'alone')
match('food for thought', 'sub2')
}, tmp_dir, 'foo'
assert.equal 'alone', sorted[1].path
prefers matches where the term is included in the match's base name
sorted = file_search.sort {
match('notbase', 'foo/zed.moon')
match('base', 'bar/foo.moon')
}, tmp_dir, 'foo'
assert.same {'base', 'notbase'}, messages(sorted)
penalizes matches in test files
sorted = file_search.sort {
match('spec', 'foo/zed_spec.moon')
match('test', 'foo/zed_test.moon')
match('specd', 'foo/zed-spec.moon')
match('testd', 'foo/zed-test.moon')
match('testp', 'foo/test_test.moon')
match('base', 'foo/zed.moon')
}, tmp_dir, 'foo'
assert.same 'base', messages(sorted)[1]
groups matches by path for same-score matches
sorted = file_search.sort {
match('foo', 'one.moon')
match('foo', 'two.moon')
match('bar', 'one.moon')
match('bar', 'two.moon')
}, tmp_dir, 'xxx'
assert.equal sorted[1].path, sorted[2].path
always orders matches in the same file by line nr
sorted = file_search.sort {
match('3', 'file.moon', 3)
match('1', 'file.moon', 1)
match('2', 'file.moon', 2)
}, tmp_dir, 'xxx'
assert.same {'1', '2', '3'}, messages(sorted)
(when context is provided)
local buffer
before_each ->
buffer = howl.Buffer!
prefers matches close to the current context directory
buffer.file = tmp_dir\join 'first/second/file.txt'
sorted = file_search.sort {
match('twoup', 'twoup.txt') -- distance 3
match('samedir', 'first/second/samedir.txt') -- distance 1
match('same', 'first/second/file.txt') -- distance 0
match('oneup', 'first/oneup.txt') -- distance 2
match('diffroot', 'other/otro/annan.txt') --distance 5
}, tmp_dir, 'foo', buffer\context_at(1)
assert.same {'same', 'samedir', 'oneup', 'twoup', 'diffroot'}, messages(sorted)
prefers matches in files sharing the same name cluster
buffer.file = tmp_dir\join 'foo.moon'
sorted = file_search.sort {
match('notsame', 'food.moon')
match('spec', 'foo_spec.moon')
match('other', 'angry/fools.moon')
}, tmp_dir, 'foo', buffer\context_at(1)
assert.same {'spec', 'notsame', 'other'}, messages(sorted)
buffer.file = tmp_dir\join 'foo_spec.moon'
sorted = file_search.sort {
match('notsame', 'food.moon')
match('main', 'foo.moon')
match('other', 'angry/fools.moon')
}, tmp_dir, 'search', buffer\context_at(1)
assert.same {'main', 'notsame', 'other'}, messages(sorted)
the native searcher
local search
setup ->
search = searchers.native.handler
handles multiple matches in a file correctly
hit = tmp_dir\join('hit.txt')
hit.contents = ([[
food
snafoo
bafoon
]]).stripped
res = search tmp_dir, 'foo'
assert.same {
{path: 'hit.txt', line_nr: 1, column: 1, message: 'food'},
{path: 'hit.txt', line_nr: 2, column: 4, message: 'snafoo'},
{path: 'hit.txt', line_nr: 3, column: 3, message: 'bafoon'},
}, res
handles a match at the end of a file, preceeding an empty line
hit = tmp_dir\join('hit.txt')
hit.contents = 'foo\n'
res = search tmp_dir, 'foo'
assert.same {
{path: 'hit.txt', line_nr: 1, column: 1, message: 'foo'},
}, res
is case insensitive
hit = tmp_dir\join('hit.txt')
hit.contents = 'foo\nFOO'
res = search tmp_dir, 'fOo'
assert.same {
{path: 'hit.txt', line_nr: 1, column: 1, message: 'foo'},
{path: 'hit.txt', line_nr: 2, column: 1, message: 'FOO'},
}, res
only reports the first match for a given line
hit = tmp_dir\join('hit.txt')
hit.contents = 'in barbary there is a bar\n'
res = search tmp_dir, 'bar'
assert.same {
{
path: 'hit.txt',
line_nr: 1,
column: 4,
message: 'in barbary there is a bar'
},
}, res
limits messages to the given max_message_length option
hit = tmp_dir\join('hit.txt')
hit.contents = string.rep 'x', 100
res = search tmp_dir, 'x', max_message_length: 50
assert.equals 50, #res[1].message
handles binary files without issue
ffi = require('ffi')
bin = tmp_dir\join('bin')
data = ffi.new 'char[1024]'
for i = 0, 1023
data[i] = math.random(255)
bin.contents = ffi.string(data, 1024)
res = search tmp_dir, 'notlikely'
assert.equals 0, #res
(when the whole_word option is set)
only finds whole words
hit = tmp_dir\join('hit.txt')
hit.contents = ([[
bar
fubar
barred
barbary
in a bar
]]).stripped
res = search tmp_dir, 'bar', whole_word: true
assert.same {
{path: 'hit.txt', line_nr: 1, column: 1, message: 'bar'},
{path: 'hit.txt', line_nr: 5, column: 6, message: 'in a bar'},
}, res