howl.ui.Editor

local buffer, lines
editor = Editor Buffer {}
cursor = editor.cursor
selection = editor.selection
window = Gtk.OffscreenWindow!
window\add editor\to_gobject!
window\show_all!
howl.app\pump_mainloop!

before_each ->
  buffer = Buffer howl.mode.by_name 'default'
  buffer.config.indent = 2
  lines = buffer.lines
  editor.buffer = buffer
  selection\remove!

.current_line is a shortcut for the current buffer line

buffer.text = 'hƏllo\nworld'
cursor.pos = 2
assert.equal editor.current_line, buffer.lines[1]

.current_context returns the buffer context at the current position

buffer.text = 'hƏllo\nwʘrld'
cursor.pos = 2
context = editor.current_context
assert.equal 'Context', typeof context
assert.equal 2, context.pos

.newline() adds a newline at the current position

buffer.text = 'hƏllo'
cursor.pos = 3
editor\newline!
assert.equal buffer.text, 'hƏ\nllo'

insert(text) inserts the text at the cursor, and moves cursor after text

buffer.text = 'hƏllo'
cursor.pos = 6
editor\insert ' world'
assert.equal 'hƏllo world', buffer.text
assert.equal 12, cursor.pos, 12

delete_line() deletes the current line

buffer.text = 'hƏllo\nworld!'
cursor.pos = 3
editor\delete_line!
assert.equal 'world!', buffer.text

copy_line() copies the current line

buffer.text = 'hƏllo\n'
cursor.pos = 3
editor\copy_line!
cursor.pos = 1
editor\paste!
assert.equal 'hƏllo\nhƏllo\n', buffer.text

join_lines joins the current line with the one after

buffer.text = 'hƏllo\n    world!\n'
cursor.pos = 1
editor\join_lines!
assert.equal 'hƏllo world!\n', buffer.text
assert.equal 6, cursor.pos

forward_to_match(string) moves the cursor to next occurence of <string>, if found in the line

buffer.text = 'hƏll\to\n    world!'
cursor.pos = 1
editor\forward_to_match 'l'
assert.equal 3, cursor.pos
editor\forward_to_match 'l'
assert.equal 4, cursor.pos
editor\forward_to_match 'o'
assert.equal 6, cursor.pos
editor\forward_to_match 'w'
assert.equal 6, cursor.pos

backward_to_match(string) moves the cursor back to previous occurence of <string>, if found in the line

buffer.text = 'h\tƏllo\n    world!'
cursor.pos = 6
editor\backward_to_match 'l'
assert.equal 5, cursor.pos
editor\backward_to_match 'l'
assert.equal 4, cursor.pos
editor\backward_to_match 'h'
assert.equal 1, cursor.pos
editor\backward_to_match 'w'
assert.equal 1, cursor.pos

.active_lines

(with no selection active)

is a table containing .current_line

buffer.text = 'hƏllo\nworld'
lines = editor.active_lines
assert.equals 1, #lines
assert.equals editor.current_line, lines[1]

(with a selection active)

is a table of lines involved in the selection

buffer.text = 'hƏllo\nworld'
selection\set 3, 8
active_lines = editor.active_lines
assert.equals 2, #active_lines
assert.equals lines[1], active_lines[1]
assert.equals lines[2], active_lines[2]

.active_chunk

is a chunk

assert.equals 'Chunk', typeof editor.active_chunk

(with no selection active)

is a chunk encompassing the entire buffer text

buffer.text = 'hƏllo\nworld'
assert.equals 'hƏllo\nworld', editor.active_chunk.text

(with a selection active)

is a chunk containing the current the selection

buffer.text = 'hƏllo\nworld'
selection\set 3, 8
assert.equals 'llo\nw', editor.active_chunk.text

indent()

(when mode does not provide a indent method)

does nothing

text = buffer.text
editor[method] editor
assert.equal text, buffer.text

(when mode provides a indent method)

calls that passing itself a parameter

buffer.mode = [method]: spy.new ->
editor[method] editor
assert.spy(buffer.mode[method]).was_called_with match.is_ref(buffer.mode), match.is_ref(editor)

      if method == 'toggle_comment'

uses the buffer mode when the one to use is ambiguous

mode1 = comment_syntax: '//'
mode2 = comment_syntax: '#'

mode1_reg = name: 'toggle_comment_test1', create: -> mode1
mode2_reg = name: 'toggle_comment_test2', create: -> mode2
howl.mode.register mode1_reg
howl.mode.register mode2_reg
buffer.mode = howl.mode.by_name 'toggle_comment_test1'

buffer.text = 'ab\nc'
buffer._buffer.styling\apply 1, {
  1, 'whitespace', 2,
  2, { 1, 's1', 3 }, 'toggle_comment_test|s1',
}

selection\set 1, 5
editor[method] editor
assert.equal '// ab\n// c', buffer.text

selection\set 1, 11
editor[method] editor
assert.equal 'ab\nc', buffer.text

howl.mode.unregister 'toggle_comment_test1'
howl.mode.unregister 'toggle_comment_test2'

comment()

(when mode does not provide a comment method)

does nothing

text = buffer.text
editor[method] editor
assert.equal text, buffer.text

(when mode provides a comment method)

calls that passing itself a parameter

buffer.mode = [method]: spy.new ->
editor[method] editor
assert.spy(buffer.mode[method]).was_called_with match.is_ref(buffer.mode), match.is_ref(editor)

      if method == 'toggle_comment'

uses the buffer mode when the one to use is ambiguous

mode1 = comment_syntax: '//'
mode2 = comment_syntax: '#'

mode1_reg = name: 'toggle_comment_test1', create: -> mode1
mode2_reg = name: 'toggle_comment_test2', create: -> mode2
howl.mode.register mode1_reg
howl.mode.register mode2_reg
buffer.mode = howl.mode.by_name 'toggle_comment_test1'

buffer.text = 'ab\nc'
buffer._buffer.styling\apply 1, {
  1, 'whitespace', 2,
  2, { 1, 's1', 3 }, 'toggle_comment_test|s1',
}

selection\set 1, 5
editor[method] editor
assert.equal '// ab\n// c', buffer.text

selection\set 1, 11
editor[method] editor
assert.equal 'ab\nc', buffer.text

howl.mode.unregister 'toggle_comment_test1'
howl.mode.unregister 'toggle_comment_test2'

uncomment()

(when mode does not provide a uncomment method)

does nothing

text = buffer.text
editor[method] editor
assert.equal text, buffer.text

(when mode provides a uncomment method)

calls that passing itself a parameter

buffer.mode = [method]: spy.new ->
editor[method] editor
assert.spy(buffer.mode[method]).was_called_with match.is_ref(buffer.mode), match.is_ref(editor)

      if method == 'toggle_comment'

uses the buffer mode when the one to use is ambiguous

mode1 = comment_syntax: '//'
mode2 = comment_syntax: '#'

mode1_reg = name: 'toggle_comment_test1', create: -> mode1
mode2_reg = name: 'toggle_comment_test2', create: -> mode2
howl.mode.register mode1_reg
howl.mode.register mode2_reg
buffer.mode = howl.mode.by_name 'toggle_comment_test1'

buffer.text = 'ab\nc'
buffer._buffer.styling\apply 1, {
  1, 'whitespace', 2,
  2, { 1, 's1', 3 }, 'toggle_comment_test|s1',
}

selection\set 1, 5
editor[method] editor
assert.equal '// ab\n// c', buffer.text

selection\set 1, 11
editor[method] editor
assert.equal 'ab\nc', buffer.text

howl.mode.unregister 'toggle_comment_test1'
howl.mode.unregister 'toggle_comment_test2'

toggle_comment()

(when mode does not provide a toggle_comment method)

does nothing

text = buffer.text
editor[method] editor
assert.equal text, buffer.text

(when mode provides a toggle_comment method)

calls that passing itself a parameter

buffer.mode = [method]: spy.new ->
editor[method] editor
assert.spy(buffer.mode[method]).was_called_with match.is_ref(buffer.mode), match.is_ref(editor)

      if method == 'toggle_comment'

uses the buffer mode when the one to use is ambiguous

mode1 = comment_syntax: '//'
mode2 = comment_syntax: '#'

mode1_reg = name: 'toggle_comment_test1', create: -> mode1
mode2_reg = name: 'toggle_comment_test2', create: -> mode2
howl.mode.register mode1_reg
howl.mode.register mode2_reg
buffer.mode = howl.mode.by_name 'toggle_comment_test1'

buffer.text = 'ab\nc'
buffer._buffer.styling\apply 1, {
  1, 'whitespace', 2,
  2, { 1, 's1', 3 }, 'toggle_comment_test|s1',
}

selection\set 1, 5
editor[method] editor
assert.equal '// ab\n// c', buffer.text

selection\set 1, 11
editor[method] editor
assert.equal 'ab\nc', buffer.text

howl.mode.unregister 'toggle_comment_test1'
howl.mode.unregister 'toggle_comment_test2'

with_position_restored(f)

before_each ->
  buffer.text = '  yowser!\n  yikes!'
  cursor.line = 2
  cursor.column = 4

calls <f> passing itself a parameter

f = spy.new -> nil
editor\with_position_restored f
assert.spy(f).was_called_with match.is_ref(editor)

restores the cursor position afterwards

editor\with_position_restored -> cursor.pos = 2
assert.equals 2, cursor.line
assert.equals 4, cursor.column

adjusts the position should the indentation have changed

editor\with_position_restored ->
  lines[1].indentation = 0
  lines[2].indentation = 0

assert.equals 2, cursor.line
assert.equals 2, cursor.column

editor\with_position_restored ->
  lines[1].indentation = 3
  lines[2].indentation = 3

assert.equals 2, cursor.line
assert.equals 5, cursor.column

(when <f> raises an error)

propagates the error

assert.raises 'ARGH!', -> editor\with_position_restored -> error 'ARGH!'

still restores the position

cursor.pos = 4

pcall editor.with_position_restored, editor, ->
  cursor.pos = 2
  error 'ARGH!'

assert.equals 4, cursor.pos

with_selection_preserved(f)

before_each ->
  buffer.text = '\nhello hello hello\n'
  selection\set 10, 8

calls <f> passing itself a parameter

f = spy.new -> nil
editor\with_selection_preserved f
assert.spy(f).was_called_with(match.is_ref(editor))

restores the selected region

editor\with_selection_preserved ->
  selection\set 1, 2
assert.equals 10, selection.anchor
assert.equals 8, selection.cursor

(when buffer is modified outside the selection)

preserves the selected text

editor\with_selection_preserved ->
  buffer\insert 'abc', 1
  buffer\insert 'abc', 14
assert.equals 13, selection.anchor
assert.equals 11, selection.cursor

(when no selection present)

preserves relative position of cursor

selection\set 1, 0
editor.cursor.pos = 5
editor\with_selection_preserved ->
  buffer\insert 'abc\n', 1
  buffer\insert 'abc\n', 10
assert.equals 9, editor.cursor.pos

paste(opts = {})

pastes the current clip of the clipboard at the current position

buffer.text = 'hƏllo'
clipboard.push ' wörld'
cursor\eof!
editor\paste!
assert.equal 'hƏllo wörld', buffer.text

(when opts.clip is specified)

pastes that clip at the current position

clipboard.push 'hello'
clip = clipboard.current
clipboard.push 'wörld'
buffer.text = 'well '
cursor\eof!
editor\paste :clip
assert.equal 'well hello', buffer.text

(when opts.where is set to "after")

pastes the clip to the right of the current position

buffer.text = 'hƏllo\n'
clipboard.push 'yo'
cursor\move_to line: 1, column: 6
editor\paste where: 'after'
assert.equal 'hƏllo yo\n', buffer.text
cursor\eof!
editor\paste where: 'after'
assert.equal 'hƏllo yo\n yo', buffer.text

(when the clip item has .whole_lines set)

pastes the clip on a newly opened line above the current

buffer.text = 'hƏllo\nworld'
clipboard.push text: 'cruel', whole_lines: true
cursor.line = 2
cursor.column = 3
editor\paste!
assert.equal 'hƏllo\ncruel\nworld', buffer.text

pastes the clip at the start of a line if ends with a newline separator

buffer.text = 'hƏllo\nworld'
clipboard.push text: 'cruel\n', whole_lines: true
cursor.line = 2
cursor.column = 3
editor\paste!
assert.equal 'hƏllo\ncruel\nworld', buffer.text

positions the cursor at the start of the pasted clip

buffer.text = 'paste'
clipboard.push text: 'much', whole_lines: true
cursor.column = 3
editor\paste!
assert.equal 1, cursor.pos

(.. when opts.where is set to "after")

pastes the clip on a newly opened line below the current

buffer.text = 'hƏllo\nworld'
clipboard.push text: 'cruel', whole_lines: true
cursor.line = 1
cursor.column = 3
editor\paste where: 'after'
assert.equal 'hƏllo\ncruel\nworld', buffer.text

accounts for trailing newline separators

buffer.text = 'hƏllo\nworld'
clipboard.push text: 'cruel\n', whole_lines: true
cursor.line = 1
cursor.column = 3
editor\paste where: 'after'
assert.equal 'hƏllo\ncruel\nworld', buffer.text

handles pasting at the end of the buffer

buffer.text = 'at'
clipboard.push text: 'last\n', whole_lines: true
cursor.line = 1
cursor.column = 3
editor\paste where: 'after'
assert.equal 'at\nlast\n', buffer.text

(when a selection is present)

deletes the selection before pasting

buffer.text = 'hƏllo\nwonderful\nworld'
clipboard.push text: 'cruel'
selection\select lines[2].start_pos, lines[2].end_pos - 1
editor\paste!
assert.equal 'hƏllo\ncruel\nworld', buffer.text
assert.equal 'cruel', clipboard.current.text

delete_to_end_of_line(opts)

cuts text from cursor up to end of line

buffer.text = 'hƏllo world!\nnext'
cursor.pos = 6
editor\delete_to_end_of_line!
assert.equal buffer.text, 'hƏllo\nnext'
editor\paste!
assert.equal buffer.text, 'hƏllo world!\nnext'

handles lines without EOLs

buffer.text = 'abc'
cursor.pos = 3
editor\delete_to_end_of_line!
assert.equal 'ab', buffer.text

cursor.pos = 3
editor\delete_to_end_of_line!
assert.equal 'ab', buffer.text

deletes without copying if no_copy is specified

buffer.text = 'hƏllo world!'
cursor.pos = 3
editor\delete_to_end_of_line no_copy: true
assert.equal buffer.text, 'hƏ'
editor\paste!
assert.not_equal 'hƏllo world!', buffer.text

(buffer switching)

restores the position from buffer.properties.position if present

buf1 = Buffer {}
buf1.text = 'a whole different whale'
buf1.properties.position = pos: 10
editor.buffer = buf1
assert.equal 10, cursor.pos

buf2 = Buffer {}
buf2.text = '123\n567'
buf2.properties.position = line: 2, column: 2
editor.buffer = buf2
assert.equal 6, cursor.pos

remembers the position for different buffers

buffer.text = 'hƏllo\n    world!'
cursor.pos = 8
buffer2 = Buffer {}
buffer2.text = 'a whole different whale'
editor.buffer = buffer2
cursor.pos = 15
editor.buffer = buffer
assert.equal 8, cursor.pos
editor.buffer = buffer2
assert.equal 15, cursor.pos

updates .last_shown for buffer switched out

time =  sys.time
now = time!
sys.time = -> now
pcall ->
  editor.buffer = Buffer {}
sys.time = time
assert.same now, buffer.last_shown

(previewing)

does not update last_shown for previewed buffer

new_buffer = Buffer {}
new_buffer.last_shown = 2
editor\preview new_buffer
editor.buffer = Buffer {}
assert.same 2, new_buffer.last_shown

updates .last_shown for original buffer switched out

time = sys.time
now = time!
sys.time = -> now
pcall ->
  editor\preview Buffer {}
sys.time= time
assert.same now, buffer.last_shown

(indentation, tabs, spaces and backspace)

defines a "tab_width" config variable, defaulting to 4

assert.equal config.tab_width, 4

defines a "use_tabs" config variable, defaulting to false

assert.equal config.use_tabs, false

defines a "indent" config variable, defaulting to 2

assert.equal config.indent, 2

defines a "tab_indents" config variable, defaulting to true

assert.equal config.tab_indents, true

defines a "backspace_unindents" config variable, defaulting to true

assert.equal config.backspace_unindents, true

smart_tab()

inserts a tab character if use_tabs is true

config.use_tabs = true
buffer.text = 'hƏllo'
cursor.pos = 2
editor\smart_tab!
assert.equal buffer.text, 'h\tƏllo'

inserts spaces to move to the next tab if use_tabs is false

config.use_tabs = false
buffer.text = 'hƏllo'
cursor.pos = 1
editor\smart_tab!
assert.equal string.rep(' ', config.indent) .. 'hƏllo', buffer.text

inserts a tab to move to the next tab stop if use_tabs is true

config.use_tabs = true
config.tab_width = config.indent
buffer.text = 'hƏllo'
cursor.pos = 1
editor\smart_tab!
assert.equal '\thƏllo', buffer.text

(when in whitespace and tab_indents is true)

before_each ->
  config.tab_indents = true
  config.use_tabs = false
  config.indent = 2

indents the current line if in whitespace and tab_indents is true

indent = string.rep ' ', config.indent
buffer.text = indent .. 'hƏllo'
cursor.pos = 2
editor\smart_tab!
assert.equal buffer.text, string.rep(indent, 2) .. 'hƏllo'

moves the cursor to the beginning of the text

buffer.text = '  hƏllo'
cursor.pos = 1
editor\smart_tab!
assert.equal 5, cursor.pos

corrects any half-off indentation

buffer.text = '   hƏllo'
cursor.pos = 1
editor\smart_tab!
assert.equal 5, cursor.pos
assert.equal '    hƏllo', buffer.text

(when a selection is active)

right-shifts the lines included in a selection if any

config.indent = 2
buffer.text = 'hƏllo\nselected\nworld!'
selection\set 2, 10
editor\smart_tab!
assert.equal '  hƏllo\n  selected\nworld!', buffer.text

(when in whitespace and tab_indents is false)

just inserts the corresponding tab or spaces

config.tab_indents = false
config.indent = 2
buffer.text = '  hƏllo'
cursor.pos = 1

config.use_tabs = false
editor\smart_tab!
assert.equal '    hƏllo', buffer.text
assert.equal 3, cursor.pos

config.use_tabs = true
editor\smart_tab!
assert.equal '  \t  hƏllo', buffer.text
assert.equal 4, cursor.pos

smart_back_tab()

(when tab_indents is false)

moves the cursor back to the previous tab position

config.tab_indents = false
config.tab_width = 4
buffer.text = '  hƏ567890'
cursor.pos = 10

editor\smart_back_tab!
assert.equal 9, cursor.pos

editor\smart_back_tab!
assert.equal 5, cursor.pos

editor\smart_back_tab!
assert.equal 1, cursor.pos

editor\smart_back_tab!
assert.equal 1, cursor.pos

cursor.pos = 2
editor\smart_back_tab!
assert.equal 1, cursor.pos

(when tab_indents is true)

unindents when in whitespace

config.tab_indents = true
config.tab_width = 4
buffer.text = '    567890'
cursor.pos = 10

editor\smart_back_tab!
assert.equal 9, cursor.pos

editor\smart_back_tab!
assert.equal 5, cursor.pos

editor\smart_back_tab!
assert.equal 3, cursor.pos
assert.equal '  567890', buffer.text

editor\smart_back_tab!
assert.equal 1, cursor.pos
assert.equal '567890', buffer.text

(when a selection is active)

left-shifts the lines included in a selection if any

config.indent = 2
buffer.text = '  hƏllo\n  selected\nworld!'
selection\set 4, 12
editor\smart_back_tab!
assert.equal 'hƏllo\nselected\nworld!', buffer.text

.delete_back()

deletes back by one character

buffer.text = 'hƏllo'
cursor.pos = 2
editor\delete_back!
assert.equal buffer.text, 'Əllo'

deletes previous newline when at start of line

config.backspace_unindents = false
buffer.text = 'hƏllo\nworld'
cursor.pos = 7
editor\delete_back!
assert.equal 'hƏlloworld', buffer.text

config.backspace_unindents = true
buffer.text = 'hƏllo\nworld'
cursor.pos = 7
editor\delete_back!
assert.equal 'hƏlloworld', buffer.text

unindents if in whitespace and backspace_unindents is true

config.indent = 2
buffer.text = '  hƏllo'
cursor.pos = 3
config.backspace_unindents = true
editor\delete_back!
assert.equal buffer.text, 'hƏllo'
assert.equal 1, cursor.pos

deletes back if in whitespace and backspace_unindents is false

config.indent = 2
buffer.text = '  hƏllo'
cursor.pos = 3
config.backspace_unindents = false
editor\delete_back!
assert.equal buffer.text, ' hƏllo'

(with a selection)

deletes the selection

buffer.text = ' 2\n 5'
cursor.pos = 5
selection\set 1, 5
editor\delete_back!
assert.equal buffer.text, '5'

.delete_back_word()

deletes back by one word

buffer.text = 'hello world'
cursor.pos = 12
editor\delete_back_word!
assert.equal buffer.text, 'hello '

(with a selection)

deletes the selection

buffer.text = ' 2\n 5'
cursor.pos = 5
selection\set 1, 5
editor\delete_back_word!
assert.equal buffer.text, '5'

.delete_forward()

deletes the character at cursor

buffer.text = 'hƏllo'
cursor.pos = 2
editor\delete_forward!
assert.equal 'hllo', buffer.text

(when a selection is active)

deletes the selection

buffer.text = 'hƏllo'
editor.selection\set 2, 5
editor\delete_forward!
assert.equal 'ho', buffer.text
assert.not_equal 'Əll', clipboard.current.text

(when at the end of a line)

deletes the line break

buffer.text = 'hƏllo\nworld'
cursor\move_to line: 1, column: 6
editor\delete_forward!
assert.equal 'hƏlloworld', buffer.text

(when at the end of the buffer)

does nothing

buffer.text = 'hƏllo'
cursor\eof!
editor\delete_forward!
assert.equal 'hƏllo', buffer.text

.delete_forward_word()

deletes forward by one word

buffer.text = 'hello world'
cursor.pos = 1
editor\delete_forward_word!
assert.equal buffer.text, 'world'

(when a selection is active)

deletes the selection

buffer.text = 'hƏllo'
editor.selection\set 2, 5
editor\delete_forward_word!
assert.equal 'ho', buffer.text
assert.not_equal 'Əll', clipboard.current.text

.shift_right()

before_each ->
  config.use_tabs = false
  config.indent = 2

right-shifts the current line when nothing is selected, remembering column

buffer.text = 'hƏllo\nworld!'
cursor.pos = 3
editor\shift_right!
assert.equal '  hƏllo\nworld!', buffer.text
assert.equal 5, cursor.pos

(with a selection)

right-shifts the lines included in the selection

buffer.text = 'hƏllo\nselected\nworld!'
selection\set 2, 10
editor\shift_right!
assert.equal '  hƏllo\n  selected\nworld!', buffer.text

adjusts and keeps the selection

buffer.text = '  xx\nyy zz'
selection\set 3, 8 -- 'xx\nyy'
editor\shift_right!
assert.equal 'xx\n  yy', selection.text
assert.same { 5, 12 }, { selection\range! }

.shift_left()

left-shifts the current line when nothing is selected, remembering column

config.indent = 2
buffer.text = '    hƏllo\nworld!'
cursor.pos = 4
editor\shift_left!
assert.equal '  hƏllo\nworld!', buffer.text
assert.equal 2, cursor.pos

(with a selection)

left-shifts the lines included in the selection

config.indent = 2
buffer.text = '  hƏllo\n  selected\nworld!'
selection\set 4, 12
editor\shift_left!
assert.equal 'hƏllo\nselected\nworld!', buffer.text

adjusts and keeps the selection

buffer.text = '    xx\n  yy zz'
selection\set 3, 12 -- '  xx\nyy'
editor\shift_left!
assert.equal '  xx\nyy', selection.text
assert.same { 1, 8 }, { selection\range! }

cycle_case()

(with a selection active)

changes all lowercase selection to all uppercase

buffer.text = 'hello selectëd #world'
selection\set 7, 22
editor\cycle_case!
assert.equals 'hello SELECTËD #WORLD', buffer.text

changes all uppercase selection to titlecase

buffer.text = 'hello SELECTËD #WORLD HELLO'
selection\set 7, 28
editor\cycle_case!
assert.equals 'hello Selectëd #world Hello', buffer.text

changes mixed case selection to all lowercase

buffer.text = 'hello SelectËD #WorLd'
selection\set 7, 22
editor\cycle_case!
assert.equals 'hello selectëd #world', buffer.text

preserves selection

buffer.text = 'select'
selection\set 3, 5
editor\cycle_case!
assert.equals 3, selection.anchor
assert.equals 5, selection.cursor

(with no selection active)

changes all lowercase word to all uppercase

buffer.text = 'hello wörld'
cursor.pos = 7
editor\cycle_case!
assert.equals 'hello WÖRLD', buffer.text

changes all uppercase word to titlecase

buffer.text = 'hello WÖRLD'
cursor.pos = 7
editor\cycle_case!
assert.equals 'hello Wörld', buffer.text

changes mixed case word to all lowercase

buffer.text = 'hello WörLd'
cursor.pos = 7
editor\cycle_case!
assert.equals 'hello wörld', buffer.text

duplicate_current

(with an active selection)

duplicates the selection

buffer.text = 'hello\nwörld'
cursor.pos = 2
selection\set 2, 5 -- 'ell'
editor\duplicate_current!
assert.equals 'hellello\nwörld', buffer.text

keeps the cursor and current selection

buffer.text = '123456'
selection\set 5, 2
editor\duplicate_current!
assert.equals 2, cursor.pos
assert.equals 2, selection.cursor
assert.equals 5, selection.anchor

(with no active selection)

duplicates the current line

buffer.text = 'hello\nwörld'
cursor.pos = 3
editor\duplicate_current!
assert.equals 'hello\nhello\nwörld', buffer.text

cut

(with an active selection)

cuts the selection

buffer.text = 'hello\nwörld'
cursor.pos = 2
selection\set 2, 5 -- 'ell'
editor\cut!
assert.equals 'ho\nwörld', buffer.text
cursor.pos = 1
editor\paste!
assert.equal 'ellho\nwörld', buffer.text

(with no active selection)

cuts the current line

buffer.text = 'hello\nwörld'
cursor.pos = 3
editor\cut!
assert.equals 'wörld', buffer.text
cursor.pos = 1
editor\paste!
assert.equal 'hello\nwörld', buffer.text

cuts the empty line

buffer.text = '\nwörld'
cursor.pos = 1
editor\cut!
assert.equals 'wörld', buffer.text
editor\paste!
assert.equal '\nwörld', buffer.text

copy

(with an active selection)

copies the selection

buffer.text = 'hello\nwörld'
cursor.pos = 2
selection\set 2, 5 -- 'ell'
editor\copy!

cursor.pos = 1
editor\paste!
assert.equals 'ellhello\nwörld', buffer.text

(with no active selection)

copies the current line

buffer.text = 'hello\nwörld'
cursor.pos = 3
editor\copy!

cursor.pos = 1
editor\paste!
assert.equals 'hello\nhello\nwörld', buffer.text

(resource management)

editors are collected as they should

e = Editor Buffer {}
editors = setmetatable {e}, __mode: 'v'
e\to_gobject!\destroy!
e = nil
collect_memory!
assert.is_true editors[1] == nil

releases resources after buffer switching

b1 = Buffer {}
b2 = Buffer {}
e = Editor b1
buffers = setmetatable { b1, b2 }, __mode: 'v'
editors = setmetatable { e }, __mode: 'v'
e.buffer = b2
e.buffer = b1
e\to_gobject!\destroy!
e = nil
b1 = nil
b2 = nil
collectgarbage!
assert.is_nil editors[1]
assert.is_nil buffers[1]
assert.is_nil buffers[2]

(get_matching_brace)

finds position of matching opending/closing brace

editor.buffer.mode.auto_pairs = {'[': ']'}

editor.buffer.text = '[]'
assert.same 2, editor\get_matching_brace 1
assert.same 1, editor\get_matching_brace 2
assert.same nil, editor\get_matching_brace 3

editor.buffer.text = '[Ü]'
assert.same 3, editor\get_matching_brace 1
assert.same 1, editor\get_matching_brace 3
assert.same nil, editor\get_matching_brace 2

editor.buffer.text = '1ÜÜ4[6ÜÜ9]---'
assert.same 10, editor\get_matching_brace 5
assert.same 5, editor\get_matching_brace 10
assert.same nil, editor\get_matching_brace 1
assert.same nil, editor\get_matching_brace 4
assert.same nil, editor\get_matching_brace 6
assert.same nil, editor\get_matching_brace 11

returns nil for unmatched/mismatched braces

editor.buffer.mode.auto_pairs = {'[': ']'}
editor.buffer.text = ']['
assert.same nil, editor\get_matching_brace 1
assert.same nil, editor\get_matching_brace 2

editor.buffer.text = '([]]'
assert.same nil, editor\get_matching_brace 4

(config updates)

local editor2
before_each ->
  editor2 = Editor Buffer {}

buffer config updates affect containing editor only

editor.buffer.config.line_numbers = true
editor2.buffer.config.line_numbers = true
assert.true editor.line_numbers
assert.true editor2.line_numbers

editor2.buffer.config.line_numbers = false
assert.true editor.line_numbers
assert.false editor2.line_numbers

buffer mode change triggers config refresh for containing editor

mode1 = {}
mode2 = {}

howl.mode.register name: 'test_mode1', create: -> mode1
howl.mode.register name: 'test_mode2', create: -> mode2
howl.mode.configure 'test_mode1',
  line_numbers: false

howl.mode.configure 'test_mode2',
  line_numbers: true

buffer.mode = howl.mode.by_name 'test_mode1'
assert.false editor.line_numbers

buffer.mode = howl.mode.by_name 'test_mode2'
assert.true editor.line_numbers

howl.mode.unregister 'test_mode1'
howl.mode.unregister 'test_mode2'