howl.Buffer

local sci

before_each ->
  sci = Scintilla!

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

.text allows setting and retrieving the buffer text

b = Buffer {}
assert.equal b.text, ''
b.text = 'Ipsum'
assert.equal 'Ipsum', b.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!
b.read_only = true
assert.equal true, b.read_only
b\append 'illegal'
assert.equal '', b.text
b.read_only = false
b\append 'yes'
assert.equal 'yes', b.text

.eol returns the current line ending

b = buffer ''

b.sci\set_eolmode Scintilla.SC_EOL_CRLF
assert.equal b.eol, '\r\n'

b.sci\set_eolmode Scintilla.SC_EOL_LF
assert.equal b.eol, '\n'

b.sci\set_eolmode Scintilla.SC_EOL_CR
assert.equal b.eol, '\r'

.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 sci

b = buffer ''
assert.false b.showing
b\add_sci_ref sci
assert.true b.showing

.last_shown returns a timestamp indicating when it was last shown

b = buffer ''
assert.is_nil b.last_shown
ts = os.time!
b\add_sci_ref sci
assert.is_true b.last_shown >= ts
b\remove_sci_ref sci
assert.is_true b.last_shown <= os.time!

.destroyed is true if the buffer is destroyed and false otherwise

b = buffer 'shoot_me'
assert.is_false b.destroyed
b\destroy!
assert.is_true b.destroyed

undo undoes the last operation

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

.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'

creation

(when sci parameter is specified)

attaches .sci and .doc to the Scintilla instance

sci.get_doc_pointer = -> 'docky'
b = Buffer {}, sci
assert.equal b.doc, 'docky'
assert.equal b.sci, sci

.mode = <mode>

(when <mode> has a lexer)

updates all embedding scis to use container lexing

b = Buffer!
b\add_sci_ref sci
assert.equal Scintilla.SCLEX_NULL, sci\get_lexer!
b.mode = lexer: -> {}
assert.equal Scintilla.SCLEX_CONTAINER, sci\get_lexer!

(when <mode> does not have a lexer)

updates all embedding scis to use null lexing

b = Buffer lexer: -> {}
b\add_sci_ref sci
assert.equal Scintilla.SCLEX_CONTAINER, sci\get_lexer!
b.mode = {}
assert.equal Scintilla.SCLEX_NULL, sci\get_lexer!

.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

keeps the existing buffer text if the file does not exist

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

when <file> exists

keeps the existing buffer text if the buffer is modified

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

and the buffer is not modified

before_each ->
  b.text = 'foo'
  b.modified = false

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'

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

.eol = <string>

set the the current line ending

b = buffer ''

b.eol = '\n'
assert.equal b.sci\get_eolmode!, Scintilla.SC_EOL_LF

b.eol = '\r\n'
assert.equal b.sci\get_eolmode!, Scintilla.SC_EOL_CRLF

b.eol = '\r'
assert.equal b.sci\get_eolmode!, Scintilla.SC_EOL_CR

raises an error if the eol is unknown

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

.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 unset whenever a previously multibyte buffer has its length calculated

b = buffer('HƏllo')
b\delete 2, 2
b.length
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
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

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.local_proxy!
mode = config: 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

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

append(text)

appends the specified text

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

returns the position right after the inserted text

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

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

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

destroy()

raises an error if the buffer is currently showing

b = buffer 'not yet'
b\add_sci_ref sci
assert.raises 'showing', -> b\destroy!

a destroyed buffer raises an error upon subsequent operations

b = buffer 'reap_me'
b\destroy!
assert.raises 'destroyed', -> b.size
assert.raises 'destroyed', -> b.lines
assert.raises 'destroyed', -> b\append 'foo'

(when no sci is passed and a doc is created in the constructor)

releases the scintilla document

b = buffer 'reap_me'
rawset b, 'sci', Spy as_null_object: true
b\destroy!
assert.is_true b.sci.release_document.called

(when a sci is passed and a doc is provided in the constructor)

an error is raised since the buffer is considered as currently showing

sci.get_doc_pointer = -> 'doc'
sci.release_document = spy.new -> nil
b = Buffer {}, sci
assert.raises 'showing', -> b\destroy!
assert.spy(sci.release_document).was_not.called!

.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

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 b.text, 'hello'

(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

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]

raises an error for an out-of-bounds <char_offset>

assert.has_error -> buffer'äåö'\byte_offset 5
assert.has_error -> buffer'äåö'\byte_offset 0
assert.has_error -> buffer'a'\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]

raises error for out-of-bounds offsets

assert.has_error -> buffer'ab'\char_offset 4
assert.has_error -> buffer'äåö'\char_offset 0
assert.has_error -> 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 b\sub(1, 1), 'h'
assert.equal b\sub(2, 2), 'å'
assert.equal b\sub(1, 5), 'hållö'
assert.equal b\sub(1, 12), 'hållö\nhållö\n'
assert.equal b\sub(8, 11), 'ållö'

handles negative indices by counting from end

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

returns empty string for start_pos > end_pos

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

raises error for out of bounds offsets

assert.has_error -> buffer'abc'\sub 1, 4
assert.has_error -> buffer'abc'\sub 5, 6

reload()

reloads the buffer contents from file

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

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

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

.add_sci_ref(sci)

adds the specified sci to .scis

b = buffer ''
b\add_sci_ref sci
assert.same b.scis, { sci }

sets .sci to the specified sci

b = buffer ''
b\add_sci_ref sci
assert.equal b.sci, sci

sets the sci lexer to container if the mode has a lexer

b = buffer ''
b.mode.lexer = -> {}
sci\set_lexer Scintilla.SCLEX_NULL
b\add_sci_ref sci
assert.equal Scintilla.SCLEX_CONTAINER, sci\get_lexer!

sets the sci lexer to null if mode has no lexer

b = buffer ''
sci\set_lexer Scintilla.SCLEX_CONTAINER
b\add_sci_ref sci
assert.equal Scintilla.SCLEX_NULL, sci\get_lexer!

.remove_sci_ref(sci)

removes the specified sci from .scis

b = buffer ''
b\add_sci_ref sci
b\remove_sci_ref sci
assert.same b.scis, {}

sets .sci to some other sci if they were previously the same

sci2 = Scintilla!
b = buffer ''
b\add_sci_ref sci
b\add_sci_ref sci2
assert.equal b.sci, sci2
b\remove_sci_ref sci2
assert.equal b.sci, sci

ensuring that buffer titles are globally unique

(when setting a file for a buffer)

prepends to the title as many parent directories as needed for uniqueness

b1 = Buffer {}
b2 = Buffer {}
b3 = Buffer {}
with_tmpdir (dir) ->
  sub1 = dir\join('sub1')
  sub1\mkdir!
  sub2 = dir\join('sub2')
  sub2\mkdir!
  f1 = sub1\join('file.foo')
  f2 = sub2\join('file.foo')
  f1\touch!
  f2\touch!
  b1.file = f1
  b2.file = f2
  assert.equal b2.title, 'sub2' .. File.separator .. 'file.foo'

  sub_sub = sub1\join('sub2')
  sub_sub\mkdir!
  f3 = sub_sub\join('file.foo')
  f3\touch!
  b3.file = f3
  assert.equal b3.title, 'sub1' .. File.separator .. b2.title

does not unneccesarily transform the title when setting the same file for a buffer

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

(when setting the title explicitly)

appends a counter number in the format <number> to the title

b1 = Buffer {}
b2 = Buffer {}
b1.title = 'Title'
b2.title = 'Title'
assert.equal b2.title, 'Title<2>'

resource management

scintilla documents are released whenever the buffer is garbage collected

release = Spy!
orig_release = Scintilla.release_document
Scintilla.release_document = release
b = Buffer {}
doc = b.doc
b = nil
collectgarbage!
Scintilla.release_document = orig_release
assert.equal release.called_with[2], doc

buffers are collected as they should

b = Buffer {}
bufs = setmetatable {}, __mode: 'v'
append bufs, b
b = nil
collectgarbage!
assert.is_nil bufs[1]

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!

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!