howl.breadcrumbs
local editor, cursor, old_tolerance
buffer = (text) ->
with Buffer {}
.text = text
setup ->
app.window = Window!
editor = app\new_editor!
cursor = editor.cursor
app.editor = editor
breadcrumbs.init!
old_tolerance = config.breadcrumb_tolerance
teardown ->
config.breadcrumb_tolerance = old_tolerance
app.editor = nil
app.window\destroy!
before_each ->
app.editor.buffer = buffer ''
config.breadcrumb_tolerance = 0
after_each ->
breadcrumbs.clear!
drop(opts)
accepts a file and a pos
File.with_tmpfile (file) ->
breadcrumbs.drop :file, pos: 3
assert.same {:file, pos: 3}, breadcrumbs.previous
accepts a file path and a pos
File.with_tmpfile (file) ->
breadcrumbs.drop :file, pos: 3
assert.same {file: File(file), pos: 3}, breadcrumbs.previous
accepts a buffer and a pos
File.with_tmpfile (file) ->
file.contents = '123456789\nabcdefgh'
b = buffer ''
b.file = file
breadcrumbs.drop buffer: b, pos: 3
assert.equals b.file, breadcrumbs.previous.file
assert.equals 3, breadcrumbs.previous.pos
assert.not_nil breadcrumbs.previous.buffer_marker
(when a buffer is present)
sets a marker in the buffer pointing to the crumb
b = buffer '123456789\nabcdefgh'
breadcrumbs.drop buffer: b, pos: 3
assert.equals 3, breadcrumbs.previous.pos
assert.not_nil breadcrumbs.previous.buffer_marker
m = breadcrumbs.previous.buffer_marker
marker_buffer = m.buffer
markers = marker_buffer.markers\find name: m.name
assert.equals 1, #markers
assert.equals 3, markers[1].start_offset
assert.equals 3, markers[1].end_offset
(when opts is missing)
adds a crumb for the current buffer and position
File.with_tmpfile (file) ->
file.contents = '1234\n6789'
editor.buffer.file = file
cursor.pos = 7
breadcrumbs.drop!
crumb = breadcrumbs.previous
assert.equals 7, crumb.pos
assert.equals editor.buffer, crumb.buffer_marker.buffer
assert.equals file, crumb.file
(when forward crumbs exists)
invalidates all such crumbs and buffer markers
b = buffer '123456789\nabcdefgh'
editor.buffer = b
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 6
breadcrumbs.drop buffer: b, pos: 9
cursor.pos = 10
breadcrumbs.go_back! -- loc 3
assert.equals 4, #breadcrumbs.trail
breadcrumbs.go_back! -- loc 2
assert.equals 4, #breadcrumbs.trail
assert.is_not_nil breadcrumbs.trail[2]
assert.is_not_nil breadcrumbs.trail[3]
assert.is_not_nil breadcrumbs.trail[4]
assert.equals 2, breadcrumbs.location
breadcrumbs.drop buffer: b, pos: 4
assert.equals 4, breadcrumbs.trail[2].pos
assert.is_nil breadcrumbs.trail[3]
markers = [m.start_offset for m in *b.markers\find({})]
table.sort markers
assert.same { 3, 4 }, markers
( context '(tolerance handling)',
)
merges crumbs when their distance is within the tolerance
buf = editor.buffer
buf.text = string.rep('123456789\n', 10)
config.breadcrumb_tolerance = 5
breadcrumbs.drop buffer: buf, pos: 1
breadcrumbs.drop buffer: buf, pos: 6
assert.equals 1, #breadcrumbs.trail
assert.equals 6, breadcrumbs.trail[1].pos
markers = [m.start_offset for m in *buf.markers\find({})]
assert.same { 6 }, markers
breadcrumbs.drop buffer: buf, pos: 12
assert.equals 2, #breadcrumbs.trail
assert.equals 12, breadcrumbs.trail[2].pos
markers = [m.start_offset for m in *buf.markers\find({})]
assert.same { 6, 12 }, markers
(house cleaning according to breadcrumb_limit)
local old_limit
before_each ->
old_limit = config.breadcrumb_limit
config.breadcrumb_limit = 2
after_each ->
config.breadcrumb_limit = old_limit
purges old crumbs according to breadcrumb_limit
b = buffer '123456789\nabcdefgh'
breadcrumbs.drop buffer: b, pos: 1
breadcrumbs.drop buffer: b, pos: 2
breadcrumbs.drop buffer: b, pos: 3
assert.equals 2, #breadcrumbs.trail
assert.equals 3, breadcrumbs.location
assert.same {2, 3}, [c.pos for c in *breadcrumbs.trail]
crumb cleaning
removes duplicate crumbs
b = buffer '123456789'
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 3
assert.equals 1, #breadcrumbs.trail
markers = b.markers\find {}
assert.equals 1, #markers
reduces unnecessary loops
b = buffer '123456789'
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 6
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 6
assert.equals 2, #breadcrumbs.trail
assert.equals 3, breadcrumbs.location
clear
invalidates any existing crumbs (buffer markers and crumbs)
b = buffer '123456789\nabcdefgh'
breadcrumbs.drop buffer: b, pos: 3
crumb = breadcrumbs.previous
breadcrumbs.clear!
assert.equals 1, breadcrumbs.location
assert.same {}, b.markers\find name: crumb.buffer_marker.name
assert.is_nil breadcrumbs.trail[1]
go_back
inserts a crumb if needed before going back
b = buffer '123456789'
editor.buffer = b
cursor.pos = 2
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 5
cursor.pos = 7
breadcrumbs.go_back!
assert.equals 5, cursor.pos -- at pos 5
assert.equals 2, breadcrumbs.location -- at breadcrumbs location 2
assert.equals 3, #breadcrumbs.trail -- with two forward crumbs
assert.equals 5, breadcrumbs.trail[2].pos -- the old one
assert.equals 7, breadcrumbs.trail[3].pos -- and the newly inserted
breadcrumbs.go_back!
assert.equals 3, cursor.pos -- at pos 3
assert.equals 1, breadcrumbs.location -- at breadcrumbs location 1
assert.equals 3, #breadcrumbs.trail -- only three forward crumbs
assert.same {3, 5, 7}, [c.pos for c in *breadcrumbs.trail]
(with a buffer and pos available)
opens the buffer and set the current position
b = buffer '123456789\nabcdefgh'
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.go_back!
assert.equals 3, cursor.pos
assert.equals b, editor.buffer
assert.equals 1, breadcrumbs.location
uses a buffer marker for positioning to account for updates
b = buffer '123456789'
breadcrumbs.drop buffer: b, pos: 6
b\insert 'xx', 2
breadcrumbs.go_back!
assert.equals 8, cursor.pos
(with a file and pos available)
opens the file and sets the current position
File.with_tmpfile (file) ->
file.contents = '123456789\nabcdefgh'
breadcrumbs.drop file: file, pos: 3
breadcrumbs.go_back!
assert.equals 3, cursor.pos
assert.equals file, editor.buffer.file
(when the buffer has been collected)
falls back to the file when available
File.with_tmpfile (file) ->
file.contents = '1234\n6789'
b = buffer ''
b.file = file
breadcrumbs.drop file: file, buffer: b, pos: 3
b = nil
collectgarbage!
breadcrumbs.go_back!
assert.equals 3, cursor.pos
assert.equals file, editor.buffer.file
moves to the crumb before if present
b1 = buffer 'buffer1'
breadcrumbs.drop buffer: b1, pos: 3
b2 = buffer 'buffer2'
breadcrumbs.drop buffer: b2, pos: 5
b2 = nil
collectgarbage!
breadcrumbs.go_back!
assert.equals 3, cursor.pos
assert.equals b1, editor.buffer
assert.equals 1, breadcrumbs.location
(tolerance handling)
moves beyond the previous crumb if it is within the distance
b = editor.buffer
b.text = string.rep('1234567890', 2)
config.breadcrumb_tolerance = 2
breadcrumbs.drop buffer: b, pos: 1
breadcrumbs.drop buffer: b, pos: 4
breadcrumbs.drop buffer: b, pos: 7
breadcrumbs.drop buffer: b, pos: 10
cursor.pos = 12
breadcrumbs.go_back!
assert.equals 7, cursor.pos
breadcrumbs.go_back!
assert.equals 4, cursor.pos
go_forward
inserts a crumb if needed before going forward
b = buffer '123456789'
editor.buffer = b
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 5
breadcrumbs.drop buffer: b, pos: 7
cursor.pos = 7
breadcrumbs.go_back!
breadcrumbs.go_back!
breadcrumbs.go_back!
assert.equals 3, cursor.pos
assert.equals 3, #breadcrumbs.trail -- with three forward crumbs
assert.equals 1, breadcrumbs.location -- at breadcrumbs location 1
cursor.pos = 4
breadcrumbs.go_forward!
assert.equals 5, cursor.pos -- at pos 5
assert.equals 4, #breadcrumbs.trail -- with two forward crumbs
assert.equals 3, breadcrumbs.location -- with location thus = 3
assert.equals 3, breadcrumbs.trail[1].pos -- old back crumb
assert.equals 4, breadcrumbs.trail[2].pos -- newly inserted
assert.equals 5, breadcrumbs.trail[3].pos -- old forward crumb
assert.equals 7, breadcrumbs.trail[4].pos -- old forward crumb
breadcrumbs.go_forward!
assert.equals 7, cursor.pos -- at pos 5
assert.equals 4, #breadcrumbs.trail
assert.equals 4, breadcrumbs.location
(with a buffer and pos available)
opens the buffer and set the current position
b = buffer '123456789\nabcdefgh'
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop buffer: b, pos: 7
breadcrumbs.go_back!
breadcrumbs.go_back!
breadcrumbs.go_forward!
assert.equals 7, cursor.pos
assert.equals b, editor.buffer
uses a buffer marker for positioning to account for updates
b = buffer '123456789'
breadcrumbs.drop buffer: b, pos: 1
breadcrumbs.drop buffer: b, pos: 6
breadcrumbs.go_back!
breadcrumbs.go_back!
b\insert 'xx', 2
breadcrumbs.go_forward!
assert.equals 8, cursor.pos
(with a file and pos available)
opens the file and sets the current position
File.with_tmpfile (file) ->
file.contents = '123456789\nabcdefgh'
breadcrumbs.drop file: file, pos: 3
breadcrumbs.drop file: file, pos: 7
breadcrumbs.go_back!
breadcrumbs.go_back!
breadcrumbs.go_forward!
assert.equals 7, cursor.pos
assert.equals file, editor.buffer.file
(when the buffer has been collected)
falls back to the file when available
File.with_tmpfile (file) ->
file.contents = '1234\n6789'
b = buffer ''
b.file = file
breadcrumbs.drop file: file, buffer: b, pos: 3
breadcrumbs.drop file: file, buffer: b, pos: 7
b = nil
breadcrumbs.go_back!
breadcrumbs.go_back!
collectgarbage!
breadcrumbs.go_forward!
assert.equals 7, cursor.pos
assert.equals file, editor.buffer.file
moves to the crumb after if present
b1 = buffer 'buffer1'
breadcrumbs.drop buffer: b1, pos: 3
breadcrumbs.drop buffer: b1, pos: 4
b2 = buffer 'buffer2'
breadcrumbs.drop buffer: b2, pos: 5
breadcrumbs.go_back!
breadcrumbs.go_back!
b1 = nil
collectgarbage!
breadcrumbs.go_forward!
assert.equals 5, cursor.pos
assert.equals b2, editor.buffer
(tolerance handling)
moves beyond the next crumb if it is within the distance
b = editor.buffer
b.text = string.rep('1234567890', 2)
config.breadcrumb_tolerance = 2
breadcrumbs.drop buffer: b, pos: 1
breadcrumbs.drop buffer: b, pos: 4
breadcrumbs.drop buffer: b, pos: 7
breadcrumbs.go_back!
breadcrumbs.go_back!
breadcrumbs.go_back!
cursor.pos = 2
breadcrumbs.go_forward!
assert.equals 7, cursor.pos
location
points to the current crumb position
b = editor.buffer
b.text = string.rep('1234567890', 2)
assert.equals 1, breadcrumbs.location
breadcrumbs.drop buffer: b, pos: 1
assert.equals 2, breadcrumbs.location
breadcrumbs.drop buffer: b, pos: 4
assert.equals 3, breadcrumbs.location
can be assigned to move to a specific location
b = editor.buffer
b.text = string.rep('1234567890', 2)
breadcrumbs.drop buffer: b, pos: 1
breadcrumbs.drop buffer: b, pos: 4
breadcrumbs.drop buffer: b, pos: 8
breadcrumbs.location = 2
assert.equals 2, breadcrumbs.location
assert.equals 4, cursor.pos
.trail
contains a list of breadcrumbs
b = buffer '123456789'
File.with_tmpfile (file) ->
breadcrumbs.drop buffer: b, pos: 3
breadcrumbs.drop :file, pos: 6
crumbs = breadcrumbs.trail
assert.equals 3, crumbs[1].pos
assert.equals b, crumbs[1].buffer_marker.buffer
assert.same {:file, pos: 6}, crumbs[2]
is automatically cleaned
b1 = buffer '123456789'
b2 = buffer '123456789'
breadcrumbs.drop buffer: b1, pos: 3
breadcrumbs.drop buffer: b2, pos: 6
b1 = nil
collectgarbage!
crumbs = breadcrumbs.trail
assert.equals 1, #crumbs
assert.equals 6, crumbs[1].pos
(when a buffer is closed)
removes any crumbs missing a file reference
b1 = buffer '123456789'
b2 = app\new_buffer!
b2.text = '123456789'
b2.modified = false
breadcrumbs.drop buffer: b1, pos: 3
breadcrumbs.drop buffer: b2, pos: 5
breadcrumbs.drop buffer: b1, pos: 7
assert.equals 3, #breadcrumbs.trail
assert.equals 4, breadcrumbs.location
app\close_buffer b2
assert.equals 2, #breadcrumbs.trail
assert.equals 3, breadcrumbs.location
assert.same {3, 7}, [c.pos for c in *breadcrumbs.trail]
clears any buffer references for crumbs with a file reference
File.with_tmpfile (file) ->
file.contents = '123456789'
b1 = buffer '123456789'
b2 = app\new_buffer!
b2.file = file
breadcrumbs.drop buffer: b1, pos: 3
breadcrumbs.drop buffer: b2, pos: 5
breadcrumbs.drop buffer: b1, pos: 7
assert.equals 3, #breadcrumbs.trail
assert.equals 4, breadcrumbs.location
app\close_buffer b2
assert.is_nil breadcrumbs.trail[2].buffer_marker
assert.equals 3, #breadcrumbs.trail
assert.equals 4, breadcrumbs.location
moves the current location down as necessary
File.with_tmpfile (file) ->
file.contents = '123456789'
b1 = buffer '123456789'
b2 = app\new_buffer!
b2.file = file
breadcrumbs.drop buffer: b1, pos: 3
breadcrumbs.drop buffer: b2, pos: 5
breadcrumbs.drop buffer: b2, pos: 7
assert.equals 4, breadcrumbs.location
app\close_buffer b2
assert.equals 1, breadcrumbs.location
(memory management)
keeps weak references to buffers
holder = setmetatable {
buffer: buffer '123456789\nabcdefgh'
}, __mode: 'v'
breadcrumbs.drop buffer: holder.buffer, pos: 3
collectgarbage!
assert.is_nil holder.buffer