howl.Buffer

buffer = (text) ->
  with Buffer {}
    .text = text

.size returns the size of the buffer text, in bytes

assert.equal buffer('hello').size, 5
assert.equal buffer('åäö').size, 6

.length returns the size of the buffer text, in characters

assert.equal 5, buffer('hello').length
assert.equal 3, buffer('åäö').length

.modified indicates and allows setting the modified status

b = Buffer {}
assert.is_false b.modified
b.text = 'hello'
assert.is_true b.modified
b.modified = false
assert.is_false b.modified
b.modified = true
assert.is_true b.modified
assert.equal b.text, 'hello' -- toggling should not have changed text

.read_only can be set to mark the buffer as read-only

b = buffer 'kept'
b.read_only = true
assert.equal true, b.read_only
assert.raises 'read%-only', -> b\append 'illegal'
assert.raises 'read%-only', -> b\insert 'illegal', 1
assert.raises 'read%-only', -> b.text = 'illegal'
assert.equal 'kept', b.text
b.read_only = false
b\append ' yes'
assert.equal 'kept yes', b.text

.properties is a table

assert.equal 'table', type buffer('').properties

.data is a table

assert.equal 'table', type buffer('').data

.showing is true if the buffer is currently referenced in any view

b = buffer ''
assert.false b.showing
b\add_view_ref!
assert.true b.showing

.can_undo returns true if undo is possible, and false otherwise

b = Buffer {}
assert.is_false b.can_undo
b.text = 'bar'
assert.is_true b.can_undo
b\undo!
assert.is_false b.can_undo

#buffer returns the number of characters in the buffer

assert.equal 5, #buffer('hello')
assert.equal 3, #buffer('åäö')

tostring(buffer) returns the buffer title

b = buffer 'hello'
b.title = 'foo'
assert.equal tostring(b), 'foo'

.text

.text allows setting and retrieving the buffer text

b = Buffer {}
assert.equal b.text, ''
b.text = 'Ipsum'
assert.equal 'Ipsum', b.text

transforms incorrect input

b = Buffer {}
b.text = '|\x80|'
assert.equal '|�|', b.text

.last_changed

sys = howl.sys

is set to the current time for a new buffer

b = buffer 'time'
now = math.floor sys.time!
assert.is_true math.floor(b.last_changed) > now - 1
assert.is_true math.floor(b.last_changed) <= now

is updated whenever the buffer is changed in some way

b = buffer 'time'
cur = b.last_changed

b\insert 'foo', 1
assert.is_true b.last_changed > cur

cur = b.last_changed
b\delete 1, 3
assert.is_true b.last_changed > cur

.file = <file>

b = buffer ''

sets the title to the basename of the file

with_tmpfile (file) ->
  b.file = file
  assert.equal b.title, file.basename

marks the buffer as not modified

with_tmpfile (file) ->
  b.file = file
  assert.is_false b.modified

clears the undo history

with_tmpfile (file) ->
  b.file = file
  assert.is_false b.can_undo

when <file> exists

overwrites any existing buffer text even if the buffer is modified

b.text = 'foo'
with_tmpfile (file) ->
  file.contents = 'yes sir'
  b.file = file
  assert.equal b.text, 'yes sir'

and the buffer is not modified

before_each ->
  config.reset!
  config.define name: 'buf_var', description: 'some var', default: 'def value'
  b.text = 'foo'
  b.modified = false
  b.config.buf_var = 'orig_value'

sets the buffer text to the contents of the file

with_tmpfile (file) ->
  file.contents = 'yes sir'
  b.file = file
  assert.equal b.text, 'yes sir'

preserves the buffer config

with_tmpfile (file) ->
  b.file = file
  assert.equal b.config.buf_var, 'orig_value'

transforms and flags incorrect UTF-8 as input

with_tmpfile (file) ->
  file.contents = '|\x80|'
  b.file = file
  assert.equal b.text, '|�|'
  assert.is_true b.modified

when <file> does not exist

set the buffer text to the empty string

b.text = 'foo'
with_tmpfile (file) ->
  file\delete!
  b.file = file
  assert.equal '', b.text

.eol

is "\\n" by default

assert.equals '\n', buffer('').eol

raises an error if a assignment value is unknown

assert.raises 'Unknown', -> buffer('').eol = 'foo'

is adjusted automatically when a file is loaded

b = buffer('')
with_tmpfile (plain_file) ->
  with_tmpfile (file) ->
    file.contents = 'o hai\r\nDOS'
    b.file = file
    assert.equals '\r\n', b.eol

    b.file = plain_file
    file.contents = 'o hai\nNIX'
    b.file = file
    assert.equals '\n', b.eol

    b.file = plain_file
    file.contents = 'venerable\rMac'
    b.file = file
    assert.equals '\r', b.eol

.multibyte

returns true if the buffer contains multibyte characters

assert.is_false buffer('vanilla').multibyte
assert.is_true buffer('HƏllo').multibyte

is updated whenever text is inserted

b = buffer 'vanilla'
b\append 'Bačon'
assert.is_true b.multibyte

is updated whenever text is deleted

b = buffer 'Bačon'
b\delete 3, 5
assert.is_false b.multibyte

.modified_on_disk

is false for a buffer with no file

assert.is_false Buffer!.modified_on_disk

is true if the file's etag is changed after a load or save

file = contents: 'foo', etag: '1', basename: 'changeable', exists: true, path: '/tmp/changeable'
b = Buffer!
b.file = file
file.etag = '2'
assert.is_true b.modified_on_disk
b\save!
assert.is_false b.modified_on_disk

.config

before_each ->
  config.reset!
  config.define_layer 'mode:config'
  config.define name: 'buf_var', description: 'some var', default: 'def value'

allows reading and writing (local) variables

b = buffer 'config'
assert.equal 'def value', b.config.buf_var
b.config.buf_var = 123
assert.equal 123, b.config.buf_var
assert.equal 'def value', config.buf_var

is chained to the mode config when available

mode_config = config.proxy '', 'mode:config'
mode = config_layer: 'mode:config'
b = buffer 'config'
b.mode = mode
mode_config.buf_var = 'from_mode'
assert.equal 'from_mode', b.config.buf_var

is chained to the global config when mode config is not available

b = buffer 'config'
b.mode = {}
assert.equal 'def value', b.config.buf_var

each new buffer gets a distinct config

b1 = buffer 'config'
b2 = buffer 'config'
b1.config.buf_var = 'b1_value'
b2.config.buf_var = 'b2_value'
assert.equal 'b1_value', b1.config.buf_var
assert.equal 'b2_value', b2.config.buf_var

uses a buffer scoped config for unsaved files

b1 = buffer 'unsaved'
b1.config.buf_var = 'b1_value'
assert.equal 'b1_value', config.get 'buf_var', 'buffer/'..b1.id

uses a file scoped config when associated with a file

b1 = buffer 'saved'
with_tmpfile (file) ->
  b1.file = file
  b1.config.buf_var = 'f1_value'
  assert.equal 'f1_value', config.get 'buf_var', 'file'..file.path
  assert.equal 'def value', config.get 'buf_var', 'buffer'..b1.id

delete(start_pos, end_pos)

deletes the specified range, inclusive

b = buffer 'ño örf'
b\delete 2, 4
assert.equal 'ñrf', b.text

does nothing if end_pos is smaller than start_pos

b = buffer 'hello'
b\delete 2, 1
assert.equal 'hello', b.text

insert(text, pos)

inserts text at pos

b = buffer 'ño señor'
b\insert 'me gusta ', 4
assert.equal 'ño me gusta señor', b.text

returns the position right after the inserted text

b = buffer ''
assert.equal 6, b\insert 'Bačon', 1

transforms incorrect input

b = buffer ''
assert.equal 4, b\insert '|\x80|', 1
assert.equal '|�|', b.text

append(text)

appends the specified text

b = buffer 'hello'
b\append ' world'
assert.equal 'hello world', b.text

returns the position right after the inserted text

b = buffer ''
assert.equal 6, b\append 'Bačon'

transforms incorrect input

b = buffer ''
assert.equal 4, b\append '|\x80|'
assert.equal '|�|', b.text

replace(pattern, replacement)

replaces all occurences of pattern with replacement

b = buffer 'hello\nuñi©ode\nworld\n'
b\replace '[lo]', ''
assert.equal 'he\nuñi©de\nwrd\n', b.text

transforms incorrect replacements

b = buffer 'XX'
b\replace 'X', '\x80'
assert.equal '��', b.text

returns the number of occurences replaced

b = buffer 'hello\nworld\n'
assert.equal 1, b\replace('world', 'editor')

(when pattern contains a leading grouping)

replaces only the match within pattern with replacement

b = buffer 'hello\nworld\n'
b\replace '(hel)lo', ''
assert.equal 'lo\nworld\n', b.text

change(start_pos, end_pos, f)

applies all operations as one undo for the specified region

b = buffer 'ño señor'
b\change 4, 6, -> -- 'señ'
  b\delete 4, 6
  b\insert 'minmin', 4

assert.equal 'ño minminor', b.text
b\undo!
assert.equal 'ño señor', b.text

returns the return value of <f> as its own return value

b = buffer '12345'
ret = b\change 1, 3, ->
  'zed'

assert.equals 'zed', ret

undo

undoes the last operation

b = buffer 'hello'
b\delete 1, 1
b\undo!
assert.equal 'hello', b.text

resets the .modified flag when at last saved file revision

with_tmpfile (file) ->
  b = buffer ''
  b.file = file
  b.text = 'hello'
  b\delete 1, 1
  b\save!
  b\delete 1, 1
  assert.equal true, b.modified
  b\undo!
  assert.equal false, b.modified
  b\undo!
  assert.equal true, b.modified

resets the .modified flag when at originally loaded revision

with_tmpfile (file) ->
  file.contents = 'hello hello'
  b = buffer ''
  b.file = file
  b\delete 1, 1
  assert.equal true, b.modified
  b\undo!
  assert.equal false, b.modified

redo

redoes the last undo operation

b = buffer 'hello'
b\delete 1, 1
b\undo!
b\redo!
assert.equal 'ello', b.text

resets the .modified flag when at synced file revision

with_tmpfile (file) ->
  b = buffer ''
  b.file = file
  b.text = 'hello'
  b\delete 1, 1
  b\save!
  b\delete 1, 1
  b\undo!
  b\undo!
  assert.equal true, b.modified
  b\redo!
  assert.equal false, b.modified
  b\redo!
  assert.equal true, b.modified

.can_undo = <bool>

setting it to false removes any undo history

b = buffer 'hello'
assert.is_true b.can_undo
b.can_undo = false
assert.is_false b.can_undo
b\undo!
assert.equal b.text, 'hello'

setting it to true is a no-op

b = buffer 'hello'
assert.is_true b.can_undo
b.can_undo = true
assert.is_true b.can_undo
b\undo!
b.can_undo = true
assert.is_false b.can_undo

.collect_revisions

(when set to false)

does not collect undo information

b = Buffer {}
b.collect_revisions = false
b\append 'foo!'
assert.is_false b.can_undo

clears existing undo information

b = buffer 'zed'
assert.is_true b.can_undo
b.collect_revisions = false
assert.is_false b.can_undo

as_one_undo(f)

allows for grouping actions as one undo

b = buffer 'hello'
b\as_one_undo ->
  b\delete 1, 1
  b\append 'foo'
b\undo!
assert.equal 'hello', b.text

(when f raises an error)

propagates the error

b = buffer 'hello'
assert.raises 'oh my',  ->
  b\as_one_undo -> error 'oh my'

ends the undo transaction

b = buffer 'hello'
assert.error -> b\as_one_undo ->
  b\delete 1, 1
  error 'oh noes what happened?!?'
b\append 'foo'
b\undo!
assert.equal b.text, 'ello'

save()

(when a file is assigned)

stores the contents of the buffer in the assigned file

text = 'line1\nline2♥\nåäö\n'
b = buffer text
with_tmpfile (file) ->
  b.file = file
  b.text = text
  b\save!
  assert.equal text, file.contents

clears the modified flag

with_tmpfile (file) ->
  b = buffer 'foo'
  b.file = file
  b\append ' bar'
  assert.is_true b.modified
  b\save!
  assert.is_false b.modified

(.. when config.strip_trailing_whitespace is false)

does not strip trailing whitespace before saving

with_tmpfile (file) ->
  config.strip_trailing_whitespace = false
  b = buffer ''
  b.file = file
  b.text = 'blank  \n\nfoo \n'
  b\save!
  assert.equal 'blank  \n\nfoo \n', b.text
  assert.equal file.contents, b.text

(.. when config.strip_trailing_whitespace is true)

strips trailing whitespace at the end of lines before saving

with_tmpfile (file) ->
  config.strip_trailing_whitespace = true
  b = buffer ''
  b.file = file
  b.text = 'åäö  \n\nfoo  \n  '
  b\save!
  assert.equal 'åäö\n\nfoo\n', b.text
  assert.equal file.contents, b.text

(.. when config.ensure_newline_at_eof is true)

appends a newline if necessary

with_tmpfile (file) ->
  config.ensure_newline_at_eof = true
  b = buffer ''
  b.file = file
  b.text = 'look mah no newline!'
  b\save!
  assert.equal 'look mah no newline!\n', b.text
  assert.equal file.contents, b.text

(.. when config.ensure_newline_at_eof is false)

does not appends a newline

with_tmpfile (file) ->
  config.ensure_newline_at_eof = false
  b = buffer ''
  b.file = file
  b.text = 'look mah no newline!'
  b\save!
  assert.equal 'look mah no newline!', b.text
  assert.equal file.contents, b.text

(.. when config.backup_files is true)

backs up files before saving

with_tmpfile (file) ->
  mockfile = {}
  setmetatable mockfile, {
    __index: file
    __newindex: (_, key, value) ->
      assert key != 'contents', 'mock to test backup_files'
      return file[key]
  }

  with_tmpdir (backups) ->
    config.backup_files = true
    config.backup_directory = backups.path

    b = buffer 'hello'
    b.file = file
    b\save!

    b.file = mockfile
    pcall -> b\save!

    assert.equal #backups.children, 1
    assert.not_nil backups.children[1].basename\match file.basename

save_as(file)

associates the buffer with <file> henceforth

with_tmpfile (file) ->
  file.contents = 'orig'
  b = buffer ''
  b.file = file
  with_tmpfile (new_file) ->
    b.text = 'nuevo'
    b\save_as new_file
    assert.equal 'nuevo', new_file.contents
    assert.equal new_file, b.file

(when <file> does not exist)

saves the buffer content in the newly created file

with_tmpfile (file) ->
  file\delete!
  b = buffer 'new'
  b\save_as file
  assert.equal 'new', file.contents

(when <file> exists)

overwrites any previous content with the buffer contents

with_tmpfile (file) ->
  file.contents = 'old'
  b = buffer 'new'
  b\save_as file
  assert.equal 'new', file.contents

byte_offset(char_offset)

returns the byte offset for the given <char_offset>

b = buffer 'äåö'
for p in *{
  {1, 1},
  {3, 2},
  {5, 3},
  {7, 4},
}
  assert.equal p[1], b\byte_offset p[2]

adjusts out-of-bounds offsets

assert.equal 7,  buffer'äåö'\byte_offset 5
assert.equal 7,  buffer'äåö'\byte_offset 10
assert.equal 1,  buffer'äåö'\byte_offset 0
assert.equal 1,  buffer'äåö'\byte_offset -1

char_offset(byte_offset)

returns the character offset for the given <byte_offset>

b = buffer 'äåö'
for p in *{
  {1, 1},
  {3, 2},
  {5, 3},
  {7, 4},
}
  assert.equal p[2], b\char_offset p[1]

adjusts out-of-bounds offsets

assert.equal 3, buffer'ab'\char_offset 4
assert.equal 1, buffer'äåö'\char_offset 0
assert.equal 1, buffer'a'\char_offset -1

sub(start_pos, end_pos)

returns the text between start_pos and end_pos, both inclusive

b = buffer 'hållö\nhållö\n'
assert.equal 'h', b\sub(1, 1)
assert.equal 'å', b\sub(2, 2)
assert.equal 'hållö', b\sub(1, 5)
assert.equal 'hållö\nhållö\n', b\sub(1, 12)
assert.equal 'ållö', b\sub(8, 11)
assert.equal '\n', b\sub(12, 12)
assert.equal '\n', b\sub(12, 13)

handles negative indices by counting from end

b = buffer 'hållö\nhållö\n'
assert.equal '\n', b\sub(-1, -1)
assert.equal 'hållö\n', b\sub(-6, -1)
assert.equal 'hållö\nhållö\n', b\sub(-12, -1)

returns empty string for start_pos > end_pos

b = buffer 'abc'
assert.equal '', b\sub(2, 1)

handles out-of-bounds offsets gracefully

assert.equals '', buffer'abc'\sub 4, 6
assert.equals 'abc', buffer'abc'\sub 1, 6

find(pattern [, init ])

searches forward

b = buffer 'ä öx'
assert.same { 1, 4 }, { b\find 'ä öx' }
assert.same { 2, 3 }, { b\find ' ö' }
assert.same { 3, 4 }, { b\find 'öx' }
assert.same { 4, 4 }, { b\find 'x' }

searches forward from init when specified

b = buffer 'öåååö'
assert.same { 2, 3 }, { b\find 'åå', 2 }
assert.same { 3, 4 }, { b\find 'åå', 3 }
assert.is_nil b\find('åå', 4)

negative init specifies offset from end

b = buffer 'öååååö'
assert.same { 4, 5 }, { b\find 'åå', -3 }
assert.same { 2, 3 }, { b\find 'åå', -5 }
assert.is_nil b\find('åå', -2)

returns nil for out of bounds init

b = buffer 'abcde'
assert.is_nil b\find('a', -6)
assert.is_nil b\find('a', 6)

rfind(pattern [, init ])

searches backward from end

b = buffer 'äöxöx'
assert.same { 1, 3 }, { b\rfind 'äöx' }
assert.same { 4, 5 }, { b\rfind 'öx' }
assert.same { 5, 5 }, { b\rfind 'x' }

searches backward from init when specified

b = buffer 'öååååö'
assert.same { 4, 5 }, { b\rfind 'åå', 5 }
assert.same { 3, 4 }, { b\rfind 'åå', 4 }
assert.is_nil b\rfind('åå', 2)

negative init specifies offset from end

b = buffer 'öååååö'
assert.same { 4, 5 }, { b\rfind 'åå', -2 }
assert.same { 2, 3 }, { b\rfind 'åå', -4 }
assert.is_nil b\rfind('åå', -5)

returns nil for out of bounds init

b = buffer 'abcde'
assert.is_nil b\rfind('a', -6)
assert.is_nil b\rfind('a', 6)

mode_at(pos)

returns the mode at the given offset

b = buffer 'abc def ghi jkl'
b.mode = { comment_syntax: '//' }
mode_at_test = { comment_syntax: '#' }
mode_reg = name: 'mode_at_test', create: -> mode_at_test
howl.mode.register mode_reg

b._buffer\style 1, {
  1, 's1', 3,
  5, { 1, 's3', 3 }, 'mode_at_test|s2',
  9, { 1, 's3', 3 }, 'nonexistent_mode|s2',
  13, 's1', 15,
}

assert.same '//', b\mode_at(1).comment_syntax
assert.same '//', b\mode_at(2).comment_syntax
assert.same '//', b\mode_at(3).comment_syntax
assert.same '//', b\mode_at(4).comment_syntax
assert.same '#', b\mode_at(5).comment_syntax
assert.same '#', b\mode_at(6).comment_syntax
assert.same '#', b\mode_at(7).comment_syntax
assert.same '//', b\mode_at(8).comment_syntax
assert.same '//', b\mode_at(9).comment_syntax
assert.same '//', b\mode_at(10).comment_syntax
assert.same '//', b\mode_at(11).comment_syntax
assert.same '//', b\mode_at(12).comment_syntax
assert.same '//', b\mode_at(13).comment_syntax
assert.same '//', b\mode_at(14).comment_syntax
assert.same '//', b\mode_at(15).comment_syntax

howl.mode.unregister 'mode_at_test'

reload(force = false)

reloads the buffer contents from file and returns true

with_tmpfile (file) ->
  b = buffer ''
  file.contents = 'hello'
  b.file = file
  file.contents = 'there'
  assert.is_true b\reload!
  assert.equal 'there', b.text

raises an error if the buffer is not associated with a file

assert.raises 'file', -> Buffer!\reload!

(when the buffer is modified)

leaves the buffer alone and returns false

with_tmpfile (file) ->
  b = buffer ''
  file.contents = 'hello'
  b.file = file
  b\append ' world'
  file.contents = 'there'
  assert.is_false b\reload!
  assert.equal 'hello world', b.text

specifying <force> as true reloads the buffer anyway

with_tmpfile (file) ->
  b = buffer ''
  file.contents = 'hello'
  b.file = file
  b\append ' world'
  file.contents = 'there'
  assert.is_true b\reload true
  assert.equal 'there', b.text

.add_view_ref()

increments the number of viewers

b = buffer ''
b\add_view_ref!
assert.equal 1, b.viewers

.remove_view_ref()

decrements the number of viewers

b = buffer ''
b\add_view_ref!
b\remove_view_ref!
assert.equal 0, b.viewers

resolve_span(span, line_nr = nil)

local buf

before_each ->
  buf = buffer '123åäö789'

accepts .start_pos and .end_pos

assert.same {4, 7}, {buf\resolve_span start_pos: 4, end_pos: 7}

accepts .byte_start_pos as the start specifier

assert.same {5, 7}, {buf\resolve_span byte_start_pos: 6, end_pos: 7}

accepts .byte_end_pos as the end specifier

assert.same {4, 5}, {buf\resolve_span start_pos: 4, byte_end_pos: 6}

accepts .count as the end specifier

assert.same {4, 7}, {buf\resolve_span start_pos: 4, count: 3}

accepts a sole line_nr argument, returning the entire line's span

assert.same {1, 10}, {buf\resolve_span line_nr: 1}

(with a .line_nr given)

accepts .start_column as the start specifier

assert.same {5, 10}, {buf\resolve_span line_nr: 1, start_column: 5}

accepts .byte_start_column as the start specifier

assert.same {5, 10}, {buf\resolve_span line_nr: 1, byte_start_column: 6}

accepts .end_column as the end specifier

assert.same {1, 5}, {buf\resolve_span line_nr: 1, end_column: 5}

accepts .byte_end_column as the end specifier

assert.same {1, 5}, {buf\resolve_span line_nr: 1, byte_end_column: 6}

(with a line_nr parameter given)

accepts .start_column as the start specifier

assert.same {5, 10}, {buf\resolve_span {start_column: 5}, 1}

accepts .byte_start_column as the start specifier

assert.same {5, 10}, {buf\resolve_span {byte_start_column: 6}, 1}

accepts .end_column as the end specifier

assert.same {1, 5}, {buf\resolve_span {end_column: 5}, 1}

accepts .byte_end_column as the end specifier

assert.same {1, 5}, {buf\resolve_span {byte_end_column: 6}, 1}

get_ptr(start_pos, end_pos)

assert_returns = (s, count, ...) ->
  r_ptr, r_count = ...
  assert.equals count, r_count
  assert.equals s, ffi.string(r_ptr, r_count)

returns a pointer to a char buffer for the span, and a byte count

b = buffer '123456789'
assert_returns '3456', 4, b\get_ptr(3, 6)

b = buffer '1åäö8'
assert_returns 'äö', 4, b\get_ptr(3, 4)

handles boundary conditions

b = buffer '123'
assert_returns '123', 3, b\get_ptr(1, 3)
assert_returns '1', 1, b\get_ptr(1, 1)

returns a zero pointer for an empty range

b = buffer ''
assert_returns '', 0, b\get_ptr(1, 0)

raises errors for illegal span values

b = buffer '123'
assert.raises 'Illegal', -> b\get_ptr -1, 2
assert.raises 'Illegal', -> b\get_ptr 1, 4
assert.raises 'Illegal', -> b\get_ptr 3, 4

returns a read-only pointer

b = buffer '123'
ptr = b\get_ptr 1, 1
assert.raises 'constant', -> ptr[0] = 88

titles

uses file basename as the default title

b = Buffer {}
b.file = File('/path/to/file.ext')
assert.equal b.title, 'file.ext'

setting title explicitly overrides default title

b = Buffer {}
b.file = File('/path/to/file.ext')
b.title = 'be something else'
assert.equal b.title, 'be something else'

signals

buffer-saved is fired whenever a buffer is saved

with_signal_handler 'buffer-saved', nil, (handler) ->
  b = buffer 'foo'
  with_tmpfile (file) ->
    b.file = file
    b\save!

  assert.spy(handler).was_called!

text-inserted is fired whenever text is inserted into a buffer

with_signal_handler 'text-inserted', nil, (handler) ->
  b = buffer 'foo'
  b\append 'bar'
  assert.spy(handler).was_called!

text-deleted is fired whenever text is deleted from buffer

with_signal_handler 'text-inserted', nil, (handler) ->
  b = buffer 'foo'
  b\delete 1, 2
  assert.spy(handler).was_called!

text-changed is fired whenever text is change:d from buffer

with_signal_handler 'text-changed', nil, (handler) ->
  b = buffer 'foo'
  b\change 1, 2, ->
    b\delete 1, 1
  assert.spy(handler).was_called!

buffer-modified is fired whenever a buffer is modified

with_signal_handler 'buffer-modified', nil, (handler) ->
  b = buffer 'foo'
  b\append 'bar'
  assert.spy(handler).was_called!

buffer-reloaded is fired whenever a buffer is reloaded

with_signal_handler 'buffer-reloaded', nil, (handler) ->
  with_tmpfile (file) ->
    b = buffer 'foo'
    b.file = file
    b\reload!
    assert.spy(handler).was_called!

buffer-mode-set is fired whenever a buffer has its mode set

with_signal_handler 'buffer-mode-set', nil, (handler) ->
  b = buffer 'foo'
  b.mode = {}
  assert.spy(handler).was_called!

buffer-title-set is fired whenever a buffer has its title set

with_signal_handler 'buffer-title-set', nil, (handler) ->
  b = buffer 'foo'
  b.title = 'Sir Buffer'
  assert.spy(handler).was_called!

(resource management)

buffers are collected properly

b = buffer 'foobar'
buffers = setmetatable { b }, __mode: 'v'
b = nil
collectgarbage!
assert.is_nil buffers[1]