1-- Copyright 2020-2021 Mitchell. See LICENSE.
2
3local _tostring = tostring
4-- Overloads tostring() to print more user-friendly output for `assert_equal()`.
5function tostring(value)
6  if type(value) == 'table' then
7    return string.format('{%s}', table.concat(value, ', '))
8  elseif type(value) == 'string' then
9    return string.format('%q', value)
10  else
11    return _tostring(value)
12  end
13end
14
15-- Asserts that values *v1* and *v2* are equal.
16-- Tables are compared by value, not by reference.
17function assert_equal(v1, v2)
18  if v1 == v2 then return end
19  if type(v1) == 'table' and type(v2) == 'table' then
20    if #v1 == #v2 then
21      for k, v in pairs(v1) do if v2[k] ~= v then goto continue end end
22      for k, v in pairs(v2) do if v1[k] ~= v then goto continue end end
23      return
24    end
25    ::continue::
26    v1 = string.format('{%s}', table.concat(v1, ', '))
27    v2 = string.format('{%s}', table.concat(v2, ', '))
28  end
29  error(string.format('%s ~= %s', v1, v2), 2)
30end
31
32
33-- Asserts that function *f* raises an error whose error message contains string
34-- *expected_errmsg*.
35-- @param f Function to call.
36-- @param expected_errmsg String the error message should contain.
37function assert_raises(f, expected_errmsg)
38  local ok, errmsg = pcall(f)
39  if ok then error('error expected', 2) end
40  if expected_errmsg ~= errmsg and
41     not tostring(errmsg):find(expected_errmsg, 1, true) then
42    error(string.format(
43      'error message %q expected, was %q', expected_errmsg, errmsg), 2)
44  end
45end
46
47local expected_failures = {}
48function expected_failure(f) expected_failures[f] = true end
49
50--------------------------------------------------------------------------------
51
52function test_assert()
53  assert_equal(assert(true, 'okay'), true)
54  assert_raises(function() assert(false, 'not okay') end, 'not okay')
55  assert_raises(function() assert(false, 'not okay: %s', false) end, 'not okay: false')
56  assert_raises(function() assert(false, 'not okay: %s') end, 'no value')
57  assert_raises(function() assert(false, 1234) end, '1234')
58  assert_raises(function() assert(false) end, 'assertion failed!')
59end
60
61function test_assert_types()
62  function foo(bar, baz, quux)
63    assert_type(bar, 'string', 1)
64    assert_type(baz, 'boolean/nil', 2)
65    assert_type(quux, 'string/table/nil', 3)
66    return bar
67  end
68  assert_equal(foo('bar'), 'bar')
69  assert_raises(function() foo(1) end, "bad argument #1 to 'foo' (string expected, got number")
70  assert_raises(function() foo('bar', 'baz') end, "bad argument #2 to 'foo' (boolean/nil expected, got string")
71  assert_raises(function() foo('bar', true, 1) end, "bad argument #3 to 'foo' (string/table/nil expected, got number")
72
73  function foo(bar) assert_type(bar, string) end
74  assert_raises(function() foo(1) end, "bad argument #2 to 'assert_type' (string expected, got table")
75  function foo(bar) assert_type(bar, 'string') end
76  assert_raises(function() foo(1) end, "bad argument #3 to 'assert_type' (value expected, got nil")
77end
78
79function test_events_basic()
80  local emitted = false
81  local event, handler = 'test_basic', function() emitted = true end
82  events.connect(event, handler)
83  events.emit(event)
84  assert(emitted, 'event not emitted or handled')
85  emitted = false
86  events.disconnect(event, handler)
87  events.emit(event)
88  assert(not emitted, 'event still handled')
89
90  assert_raises(function() events.connect(nil) end, 'string expected')
91  assert_raises(function() events.connect(event, nil) end, 'function expected')
92  assert_raises(function() events.connect(event, function() end, 'bar') end, 'number/nil expected')
93  assert_raises(function() events.disconnect() end, 'expected, got nil')
94  assert_raises(function() events.disconnect(event, nil) end, 'function expected')
95  assert_raises(function() events.emit(nil) end, 'string expected')
96end
97
98function test_events_single_handle()
99  local count = 0
100  local event, handler = 'test_single_handle', function() count = count + 1 end
101  events.connect(event, handler)
102  events.connect(event, handler) -- should disconnect first
103  events.emit(event)
104  assert_equal(count, 1)
105end
106
107function test_events_insert()
108  local foo = {}
109  local event = 'test_insert'
110  events.connect(event, function() foo[#foo + 1] = 2 end)
111  events.connect(event, function() foo[#foo + 1] = 1 end, 1)
112  events.emit(event)
113  assert_equal(foo, {1, 2})
114end
115
116function test_events_short_circuit()
117  local emitted = false
118  local event = 'test_short_circuit'
119  events.connect(event, function() return true end)
120  events.connect(event, function() emitted = true end)
121  assert_equal(events.emit(event), true)
122  assert_equal(emitted, false)
123end
124
125function test_events_disconnect_during_handle()
126  local foo = {}
127  local event, handlers = 'test_disconnect_during_handle', {}
128  for i = 1, 3 do
129    handlers[i] = function()
130      foo[#foo + 1] = i
131      events.disconnect(event, handlers[i])
132    end
133    events.connect(event, handlers[i])
134  end
135  events.emit(event)
136  assert_equal(foo, {1, 2, 3})
137end
138
139function test_events_error()
140  local errmsg
141  local event, handler = 'test_error', function(message)
142    errmsg = message
143    return false -- halt propagation
144  end
145  events.connect(events.ERROR, handler, 1)
146  events.connect(event, function() error('foo') end)
147  events.emit(event)
148  events.disconnect(events.ERROR, handler)
149  assert(errmsg:find('foo'), 'error handler did not run')
150end
151
152function test_events_value_passing()
153  local event = 'test_value_passing'
154  events.connect(event, function() return end)
155  events.connect(event, function() return {1, 2, 3} end) -- halts propagation
156  events.connect(event, function() return 'foo' end)
157  assert_equal(events.emit(event), {1, 2, 3})
158end
159
160local locales = {}
161-- Load localizations from *locale_conf* and return them in a table.
162-- @param locale_conf String path to a local file to load.
163local function load_locale(locale_conf)
164  if locales[locale_conf] then return locales[locale_conf] end
165  print(string.format('Loading locale "%s"', locale_conf))
166  local L = {}
167  for line in io.lines(locale_conf) do
168    if not line:find('^%s*[^%w_%[]') then
169      local id, str = line:match('^(.-)%s*=%s*(.+)$')
170      if id and str and assert(not L[id], 'duplicate locale id "%s"', id) then
171        L[id] = str
172      end
173    end
174  end
175  locales[locale_conf] = L
176  return L
177end
178
179-- Looks for use of localization in the given Lua file and verifies that each
180-- use is okay.
181-- @param filename String filename of the Lua file to check.
182-- @param L Table of localizations to read from.
183local function check_localizations(filename, L)
184  print(string.format('Processing file "%s"', filename:gsub(_HOME, '')))
185  local count = 0
186  for line in io.lines(filename) do
187    for id in line:gmatch([=[_L%[['"]([^'"]+)['"]%]]=]) do
188      assert(L[id], 'locale missing id "%s"', id)
189      count = count + 1
190    end
191  end
192  print(string.format('Checked %d localizations.', count))
193end
194
195local loaded_extra = {}
196-- Records localization assignments in the given Lua file for use in subsequent
197-- checks.
198-- @param L Table of localizations to add to.
199local function load_extra_localizations(filename, L)
200  if loaded_extra[filename] then return end
201  print(string.format('Processing file "%s"', filename:gsub(_HOME, '')))
202  local count = 0
203  for line in io.lines(filename) do
204    if line:find('_L%b[]%s*=') then
205      for id in line:gmatch([=[_L%[['"]([^'"]+)['"]%]%s*=]=]) do
206        assert(not L[id], 'duplicate locale id "%s"', id)
207        L[id], count = true, count + 1
208      end
209    end
210  end
211  loaded_extra[filename] = true
212  print(string.format('Added %d localizations.', count))
213end
214
215local LOCALE_CONF = _HOME .. '/core/locale.conf'
216local LOCALE_DIR = _HOME .. '/core/locales'
217
218function test_locale_load()
219  local L = load_locale(LOCALE_CONF)
220  for locale_conf in lfs.walk(LOCALE_DIR) do
221    local l = load_locale(locale_conf)
222    for id in pairs(L) do assert(l[id], 'locale missing id "%s"', id) end
223    for id in pairs(l) do assert(L[id], 'locale has extra id "%s"', id) end
224  end
225end
226
227function test_locale_use_core()
228  local L = load_locale(LOCALE_CONF)
229  local ta_dirs = {'core', 'modules/ansi_c', 'modules/lua', 'modules/textadept'}
230  for _, dir in ipairs(ta_dirs) do
231    dir = _HOME .. '/' .. dir
232    for filename in lfs.walk(dir, '.lua') do
233      check_localizations(filename, L)
234    end
235  end
236  check_localizations(_HOME .. '/init.lua', L)
237end
238
239function test_locale_use_extra()
240  local L = load_locale(LOCALE_CONF)
241  for filename in lfs.walk(_HOME, '.lua') do
242    load_extra_localizations(filename, L)
243  end
244  for filename in lfs.walk(_HOME, '.lua') do
245    check_localizations(filename, L)
246  end
247end
248
249function test_locale_use_userhome()
250  local L = load_locale(LOCALE_CONF)
251  for filename in lfs.walk(_HOME, '.lua') do
252    load_extra_localizations(filename, L)
253  end
254  for filename in lfs.walk(_USERHOME, '.lua') do
255    load_extra_localizations(filename, L)
256  end
257  L['%1'] = true -- snippet
258  for filename in lfs.walk(_USERHOME, '.lua') do
259    check_localizations(filename, L)
260  end
261end
262
263function test_file_io_open_file_detect_encoding()
264  io.recent_files = {} -- clear
265  local recent_files = {}
266  local files = {
267    [_HOME .. '/test/file_io/utf8'] = 'UTF-8',
268    [_HOME .. '/test/file_io/cp1252'] = 'CP1252',
269    [_HOME .. '/test/file_io/utf16'] = 'UTF-16',
270    [_HOME .. '/test/file_io/binary'] = '',
271  }
272  for filename, encoding in pairs(files) do
273    print(string.format('Opening file %s', filename))
274    io.open_file(filename)
275    assert_equal(buffer.filename, filename)
276    local f = io.open(filename, 'rb')
277    local contents = f:read('a')
278    f:close()
279    if encoding ~= '' then
280      --assert_equal(buffer:get_text():iconv(encoding, 'UTF-8'), contents)
281      assert_equal(buffer.encoding, encoding)
282      assert_equal(buffer.code_page, buffer.CP_UTF8)
283    else
284      assert_equal(buffer:get_text(), contents)
285      assert_equal(buffer.encoding, nil)
286      assert_equal(buffer.code_page, 0)
287    end
288    buffer:close()
289    table.insert(recent_files, 1, filename)
290  end
291  assert_equal(io.recent_files, recent_files)
292
293  assert_raises(function() io.open_file(1) end, 'string/table/nil expected, got number')
294  assert_raises(function() io.open_file('/tmp/foo', true) end, 'string/table/nil expected, got boolean')
295  -- TODO: encoding failure
296end
297
298function test_file_io_open_file_detect_newlines()
299  local files = {
300    [_HOME .. '/test/file_io/lf'] = buffer.EOL_LF,
301    [_HOME .. '/test/file_io/crlf'] = buffer.EOL_CRLF,
302  }
303  for filename, mode in pairs(files) do
304    io.open_file(filename)
305    assert_equal(buffer.eol_mode, mode)
306    buffer:close()
307  end
308end
309
310function test_file_io_open_file_with_encoding()
311  local num_buffers = #_BUFFERS
312  local files = {
313    _HOME .. '/test/file_io/utf8',
314    _HOME .. '/test/file_io/cp1252',
315    _HOME .. '/test/file_io/utf16'
316  }
317  local encodings = {nil, 'CP1252', 'UTF-16'}
318  io.open_file(files, encodings)
319  assert_equal(#_BUFFERS, num_buffers + #files)
320  for i = #files, 1, -1 do
321    view:goto_buffer(_BUFFERS[num_buffers + i])
322    assert_equal(buffer.filename, files[i])
323    if encodings[i] then assert_equal(buffer.encoding, encodings[i]) end
324    buffer:close()
325  end
326end
327
328function test_file_io_open_file_already_open()
329  local filename = _HOME .. '/test/file_io/utf8'
330  io.open_file(filename)
331  buffer.new()
332  local num_buffers = #_BUFFERS
333  io.open_file(filename)
334  assert_equal(buffer.filename, filename)
335  assert_equal(#_BUFFERS, num_buffers)
336  view:goto_buffer(1)
337  buffer:close() -- untitled
338  buffer:close() -- filename
339end
340
341function test_file_io_open_file_interactive()
342  local num_buffers = #_BUFFERS
343  io.open_file()
344  if #_BUFFERS > num_buffers then buffer:close() end
345end
346
347function test_file_io_open_file_errors()
348  if LINUX then
349    assert_raises(function() io.open_file('/etc/group-') end, 'cannot open /etc/group-: Permission denied')
350  end
351  -- TODO: find a case where the file can be opened, but not read
352end
353
354function test_file_io_reload_file()
355  io.open_file(_HOME .. '/test/file_io/utf8')
356  local pos = 10
357  buffer:goto_pos(pos)
358  local text = buffer:get_text()
359  buffer:append_text('foo')
360  assert(buffer:get_text() ~= text, 'buffer text is unchanged')
361  buffer:reload()
362  assert_equal(buffer:get_text(), text)
363  assert_equal(buffer.current_pos, pos)
364  buffer:close()
365end
366
367function test_file_io_set_encoding()
368  io.open_file(_HOME .. '/test/file_io/utf8')
369  local pos = 10
370  buffer:goto_pos(pos)
371  local text = buffer:get_text()
372  buffer:set_encoding('CP1252')
373  assert_equal(buffer.encoding, 'CP1252')
374  assert_equal(buffer.code_page, buffer.CP_UTF8)
375  assert_equal(buffer:get_text(), text) -- fundamentally the same
376  assert_equal(buffer.current_pos, pos)
377  buffer:reload()
378  buffer:close()
379
380  assert_raises(function() buffer:set_encoding(true) end, 'string/nil expected, got boolean')
381end
382
383function test_file_io_save_file()
384  buffer.new()
385  buffer._type = '[Foo Buffer]'
386  buffer:append_text('foo')
387  local filename = os.tmpname()
388  buffer:save_as(filename)
389  local f = assert(io.open(filename))
390  local contents = f:read('a')
391  f:close()
392  assert_equal(buffer:get_text(), contents)
393  assert(not buffer._type, 'still has a type')
394  buffer:append_text('bar')
395  io.save_all_files()
396  f = assert(io.open(filename))
397  contents = f:read('a')
398  f:close()
399  assert_equal(buffer:get_text(), contents)
400  buffer:close()
401  os.remove(filename)
402
403  assert_raises(function() buffer:save_as(1) end, 'string/nil expected, got number')
404end
405
406function test_file_io_non_global_buffer_functions()
407  local filename = os.tmpname()
408  local buf = buffer.new()
409  buf:append_text('foo')
410  view:goto_buffer(-1)
411  assert(buffer ~= buf, 'still in untitled buffer')
412  assert_equal(buf:get_text(), 'foo')
413  assert(buffer ~= buf, 'jumped to untitled buffer')
414  buf:save_as(filename)
415  assert(buffer ~= buf, 'jumped to untitled buffer')
416  view:goto_buffer(1)
417  assert(buffer == buf, 'not in saved buffer')
418  assert_equal(buffer.filename, filename)
419  assert(not buffer.modify, 'saved buffer still marked modified')
420  local f = io.open(filename, 'rb')
421  local contents = f:read('a')
422  f:close()
423  assert_equal(buffer:get_text(), contents)
424  buffer:append_text('bar')
425  view:goto_buffer(-1)
426  assert(buffer ~= buf, 'still in saved buffer')
427  buf:save()
428  assert(buffer ~= buf, 'jumped to untitled buffer')
429  f = io.open(filename, 'rb')
430  contents = f:read('a')
431  f:close()
432  assert_equal(buf:get_text(), contents)
433  buf:append_text('baz')
434  assert_equal(buf:get_text(), contents .. 'baz')
435  assert(buf.modify, 'buffer not marked modified')
436  buf:reload()
437  assert_equal(buf:get_text(), contents)
438  assert(not buf.modify, 'buffer still marked modified')
439  buf:append_text('baz')
440  buf:close(true)
441  assert(buffer ~= buf, 'closed the wrong buffer')
442  os.remove(filename)
443end
444
445function test_file_io_file_detect_modified()
446  local modified = false
447  local handler = function(filename)
448    assert_type(filename, 'string', 1)
449    modified = true
450    return false -- halt propagation
451  end
452  events.connect(events.FILE_CHANGED, handler, 1)
453  local filename = os.tmpname()
454  local f = assert(io.open(filename, 'w'))
455  f:write('foo\n'):flush()
456  io.open_file(filename)
457  assert_equal(buffer:get_text(), 'foo\n')
458  view:goto_buffer(-1)
459  os.execute('sleep 1') -- filesystem mod time has 1-second granularity
460  f:write('bar\n'):flush()
461  view:goto_buffer(1)
462  assert_equal(modified, true)
463  buffer:close()
464  f:close()
465  os.remove(filename)
466  events.disconnect(events.FILE_CHANGED, handler)
467end
468
469function test_file_io_file_detect_modified_interactive()
470  local filename = os.tmpname()
471  local f = assert(io.open(filename, 'w'))
472  f:write('foo\n'):flush()
473  io.open_file(filename)
474  assert_equal(buffer:get_text(), 'foo\n')
475  view:goto_buffer(-1)
476  os.execute('sleep 1') -- filesystem mod time has 1-second granularity
477  f:write('bar\n'):flush()
478  view:goto_buffer(1)
479  assert_equal(buffer:get_text(), 'foo\nbar\n')
480  buffer:close()
481  f:close()
482  os.remove(filename)
483end
484
485function test_file_io_recent_files()
486  io.recent_files = {} -- clear
487  local recent_files = {}
488  local files = {
489    _HOME .. '/test/file_io/utf8',
490    _HOME .. '/test/file_io/cp1252',
491    _HOME .. '/test/file_io/utf16',
492    _HOME .. '/test/file_io/binary'
493  }
494  for _, filename in ipairs(files) do
495    io.open_file(filename)
496    buffer:close()
497    table.insert(recent_files, 1, filename)
498  end
499  assert_equal(io.recent_files, recent_files)
500end
501
502function test_file_io_open_recent_interactive()
503  local filename = _HOME .. '/test/file_io/utf8'
504  io.open_file(filename)
505  buffer:close()
506  local tmpfile = os.tmpname()
507  io.open_file(tmpfile)
508  buffer:close()
509  os.remove(tmpfile)
510  io.open_recent_file()
511  assert_equal(buffer.filename, filename)
512  buffer:close()
513end
514
515function test_file_io_get_project_root()
516  local cwd = lfs.currentdir()
517  lfs.chdir(_HOME)
518  assert_equal(io.get_project_root(), _HOME)
519  lfs.chdir(cwd)
520  assert_equal(io.get_project_root(_HOME), _HOME)
521  assert_equal(io.get_project_root(_HOME .. '/core'), _HOME)
522  assert_equal(io.get_project_root(_HOME .. '/core/init.lua'), _HOME)
523  assert_equal(io.get_project_root('/tmp'), nil)
524  lfs.chdir(cwd)
525
526  -- Test git submodules.
527  local dir = os.tmpname()
528  os.remove(dir)
529  lfs.mkdir(dir)
530  lfs.mkdir(dir .. '/.git')
531  lfs.mkdir(dir .. '/foo')
532  io.open(dir .. '/foo/.git', 'w'):write():close() -- simulate submodule
533  assert_equal(io.get_project_root(dir .. '/foo/bar.txt'), dir)
534  io.open_file(dir .. '/foo/bar.txt')
535  assert_equal(io.get_project_root(true), dir .. '/foo')
536  buffer:close()
537  os.execute('rm -r ' .. dir)
538
539  assert_raises(function() io.get_project_root(1) end, 'string/nil expected, got number')
540end
541
542function test_file_io_quick_open_interactive()
543  local num_buffers = #_BUFFERS
544  local cwd = lfs.currentdir()
545  local dir = _HOME .. '/core'
546  lfs.chdir(dir)
547  io.quick_open_filters[dir] = '.lua'
548  io.quick_open(dir)
549  if #_BUFFERS > num_buffers then
550    assert(buffer.filename:find('%.lua$'), '.lua file filter did not work')
551    buffer:close()
552  end
553  io.quick_open_filters[dir] = true
554  assert_raises(function() io.quick_open(dir) end, 'string/table/nil expected, got boolean')
555  io.quick_open_filters[_HOME] = '.lua'
556  io.quick_open()
557  if #_BUFFERS > num_buffers then
558    assert(buffer.filename:find('%.lua$'), '.lua file filter did not work')
559    buffer:close()
560  end
561  local quick_open_max = io.quick_open_max
562  io.quick_open_max = 10
563  io.quick_open(_HOME)
564  assert(#_BUFFERS > num_buffers, 'File limit exceeded notification did not occur')
565  buffer:close()
566  io.quick_open_max = quick_open_max -- restore
567  lfs.chdir(cwd)
568
569  assert_raises(function() io.quick_open(1) end, 'string/table/nil expected, got number')
570  assert_raises(function() io.quick_open(_HOME, true) end, 'string/table/nil expected, got boolean')
571  assert_raises(function() io.quick_open(_HOME, nil, 1) end, 'table/nil expected, got number')
572end
573
574function test_keys_keychain()
575  local ctrl_a = keys['ctrl+a']
576  local foo = false
577  keys['ctrl+a'] = {a = function() foo = true end}
578  events.emit(events.KEYPRESS, string.byte('a'))
579  assert(not foo, 'foo set outside keychain')
580  events.emit(events.KEYPRESS, string.byte('a'), false, true)
581  assert_equal(#keys.keychain, 1)
582  assert_equal(keys.keychain[1], 'ctrl+a')
583  events.emit(events.KEYPRESS, not CURSES and 0xFF1B or 7) -- esc
584  assert_equal(#keys.keychain, 0, 'keychain not canceled')
585  events.emit(events.KEYPRESS, string.byte('a'))
586  assert(not foo, 'foo set outside keychain')
587  events.emit(events.KEYPRESS, string.byte('a'), false, true)
588  events.emit(events.KEYPRESS, string.byte('a'))
589  assert(foo, 'foo not set')
590  keys['ctrl+a'] = ctrl_a -- restore
591end
592
593function test_keys_propagation()
594  buffer:new()
595  local foo, bar, baz = false, false, false
596  keys.a = function() foo = true end
597  keys.b = function() bar = true end
598  keys.c = function() baz = true end
599  keys.cpp.a = function() end -- halt
600  keys.cpp.b = function() return false end -- propagate
601  keys.cpp.c = function()
602    keys.mode = 'test_mode'
603    return false -- propagate
604  end
605  buffer:set_lexer('cpp')
606  events.emit(events.KEYPRESS, string.byte('a'))
607  assert(not foo, 'foo set')
608  events.emit(events.KEYPRESS, string.byte('b'))
609  assert(bar, 'bar set')
610  events.emit(events.KEYPRESS, string.byte('c'))
611  assert(not baz, 'baz set') -- mode changed, so cannot propagate to keys.c
612  assert_equal(keys.mode, 'test_mode')
613  keys.mode = nil
614  keys.a, keys.b, keys.c, keys.cpp.a, keys.cpp.b, keys.cpp.c = nil, nil, nil, nil, nil, nil -- reset
615  buffer:close()
616end
617
618function test_keys_modes()
619  buffer.new()
620  local foo, bar = false, false
621  keys.a = function() foo = true end
622  keys.test_mode = {a = function()
623    bar = true
624    keys.mode = nil
625    return false -- propagate
626  end}
627  keys.cpp.a = function() keys.mode = 'test_mode' end
628  events.emit(events.KEYPRESS, string.byte('a'))
629  assert(foo, 'foo not set')
630  assert(not keys.mode, 'key mode entered')
631  assert(not bar, 'bar set outside mode')
632  foo = false
633  buffer:set_lexer('cpp')
634  events.emit(events.KEYPRESS, string.byte('a'))
635  assert_equal(keys.mode, 'test_mode')
636  assert(not foo, 'foo set outside mode')
637  assert(not bar, 'bar set outside mode')
638  events.emit(events.KEYPRESS, string.byte('a'))
639  assert(bar, 'bar not set')
640  assert(not keys.mode, 'key mode still active')
641  assert(not foo, 'foo set') -- TODO: should this propagate?
642  keys.a, keys.test_mode, keys.cpp.a = nil, nil, nil -- reset
643  buffer:close()
644end
645
646function test_lfs_ext_walk()
647  local files, directories = 0, 0
648  for filename in lfs.walk(_HOME .. '/core', nil, nil, true) do
649    if not filename:find('/$') then
650      files = files + 1
651    else
652      directories = directories + 1
653    end
654  end
655  assert(files > 0, 'no files found')
656  assert(directories > 0, 'no directories found')
657
658  assert_raises(function() lfs.walk() end, 'string expected, got nil')
659  assert_raises(function() lfs.walk(_HOME, 1) end, 'string/table/nil expected, got number')
660  assert_raises(function() lfs.walk(_HOME, nil, true) end, 'number/nil expected, got boolean')
661end
662
663function test_lfs_ext_walk_filter_lua()
664  local count = 0
665  for filename in lfs.walk(_HOME .. '/core', '.lua') do
666    assert(filename:find('%.lua$'), '"%s" not a Lua file', filename)
667    count = count + 1
668  end
669  assert(count > 0, 'no Lua files found')
670end
671
672function test_lfs_ext_walk_filter_exclusive()
673  local count = 0
674  for filename in lfs.walk(_HOME .. '/core', '!.lua') do
675    assert(not filename:find('%.lua$'), '"%s" is a Lua file', filename)
676    count = count + 1
677  end
678  assert(count > 0, 'no non-Lua files found')
679end
680
681function test_lfs_ext_walk_filter_dir()
682  local count = 0
683  for filename in lfs.walk(_HOME, '/core') do
684    assert(filename:find('/core/'), '"%s" is not in core/', filename)
685    count = count + 1
686  end
687  assert(count > 0, 'no core files found')
688end
689expected_failure(test_lfs_ext_walk_filter_dir)
690
691function test_lfs_ext_walk_filter_mixed()
692  local count = 0
693  for filename in lfs.walk(_HOME .. '/core', {'!/locales', '.lua'}) do
694    assert(not filename:find('/locales/') and filename:find('%.lua$'), '"%s" should not match', filename)
695    count = count + 1
696  end
697  assert(count > 0, 'no matching files found')
698end
699
700function test_lfs_ext_walk_max_depth()
701  local count = 0
702  for filename in lfs.walk(_HOME, '.lua', 0) do count = count + 1 end
703  assert_equal(count, 1) -- init.lua
704end
705
706function test_lfs_ext_walk_halt()
707  local count, count_at_halt = 0, 0
708  for filename in lfs.walk(_HOME .. '/core') do
709    count = count + 1
710    if filename:find('/locales/.') then
711      count_at_halt = count
712      break
713    end
714  end
715  assert_equal(count, count_at_halt)
716
717  for filename in lfs.walk(_HOME .. '/core', nil, nil, true) do
718    count = count + 1
719    if filename:find('[/\\]$') then
720      count_at_halt = count
721      break
722    end
723  end
724  assert_equal(count, count_at_halt)
725end
726
727function test_lfs_ext_walk_win32()
728  local win32 = _G.WIN32
729  _G.WIN32 = true
730  local count = 0
731  for filename in lfs.walk(_HOME, {'/core'}) do
732    assert(not filename:find('/'), '"%s" has /', filename)
733    if filename:find('\\core') then count = count + 1 end
734  end
735  assert(count > 0, 'no core files found')
736  _G.WIN32 = win32 -- reset just in case
737end
738
739function test_lfs_ext_walk_symlinks()
740  local dir = os.tmpname()
741  os.remove(dir)
742  lfs.mkdir(dir)
743  lfs.mkdir(dir .. '/1')
744  io.open(dir .. '/1/foo', 'w'):close()
745  lfs.mkdir(dir .. '/1/bar')
746  io.open(dir .. '/1/bar/baz', 'w'):close()
747  lfs.link(dir .. '/1/', dir .. '/1/bar/quux', true) -- trailing '/' on purpose
748  lfs.mkdir(dir .. '/2')
749  io.open(dir .. '/2/foobar', 'w'):close()
750  lfs.link(dir .. '/2/foobar', dir .. '/2/foobaz', true)
751  lfs.link(dir .. '/2', dir .. '/1/2', true)
752  local files = {}
753  for filename in lfs.walk(dir .. '/1/') do -- trailing '/' on purpose
754    files[#files + 1] = filename
755  end
756  table.sort(files)
757  local expected_files = {dir .. '/1/foo', dir .. '/1/bar/baz', dir .. '/1/2/foobar', dir .. '/1/2/foobaz'}
758  table.sort(expected_files)
759  assert_equal(files, expected_files)
760  os.execute('rm -r ' .. dir)
761
762  lfs.mkdir(dir)
763  io.open(dir .. '/foo', 'w'):close()
764  local cwd = lfs.currentdir()
765  lfs.chdir(dir)
766  lfs.link('.', 'bar', true)
767  lfs.mkdir(dir .. '/baz')
768  lfs.mkdir(dir .. '/baz/quux')
769  lfs.chdir(dir .. '/baz/quux')
770  lfs.link('../../baz/', 'foobar', true)
771  lfs.chdir(cwd)
772  local count = 0
773  for filename in lfs.walk(dir) do count = count + 1 end
774  assert_equal(count, 1)
775  os.execute('rm -r ' .. dir)
776end
777
778function test_lfs_ext_walk_root()
779  local filename = lfs.walk('/', nil, 0, true)()
780  assert(not filename:find('lfs_ext.lua:'), 'coroutine error')
781end
782
783function test_lfs_ext_abs_path()
784  assert_equal(lfs.abspath('bar', '/foo'), '/foo/bar')
785  assert_equal(lfs.abspath('./bar', '/foo'), '/foo/bar')
786  assert_equal(lfs.abspath('../bar', '/foo'), '/bar')
787  assert_equal(lfs.abspath('/bar', '/foo'), '/bar')
788  assert_equal(lfs.abspath('../../././baz', '/foo/bar'), '/baz')
789  local win32 = _G.WIN32
790  _G.WIN32 = true
791  assert_equal(lfs.abspath('bar', 'C:\\foo'), 'C:\\foo\\bar')
792  assert_equal(lfs.abspath('.\\bar', 'C:\\foo'), 'C:\\foo\\bar')
793  assert_equal(lfs.abspath('..\\bar', 'C:\\foo'), 'C:\\bar')
794  assert_equal(lfs.abspath('C:\\bar', 'C:\\foo'), 'C:\\bar')
795  assert_equal(lfs.abspath('c:\\bar', 'c:\\foo'), 'C:\\bar')
796  assert_equal(lfs.abspath('..\\../.\\./baz', 'C:\\foo\\bar'), 'C:\\baz')
797  _G.WIN32 = win32 -- reset just in case
798
799  assert_raises(function() lfs.abspath() end, 'string expected, got nil')
800  assert_raises(function() lfs.abspath('foo', 1) end, 'string/nil expected, got number')
801end
802
803function test_ui_print()
804  local tabs = ui.tabs
805  local silent_print = ui.silent_print
806
807  ui.tabs = true
808  ui.silent_print = false
809  ui.print('foo')
810  assert_equal(buffer._type, _L['[Message Buffer]'])
811  assert_equal(#_VIEWS, 1)
812  assert_equal(buffer:get_text(), 'foo\n')
813  assert(buffer:line_from_position(buffer.current_pos) > 1, 'still on first line')
814  ui.print('bar', 'baz')
815  assert_equal(buffer:get_text(), 'foo\nbar\tbaz\n')
816  buffer:close()
817
818  ui.tabs = false
819  ui.print(1, 2, 3)
820  assert_equal(buffer._type, _L['[Message Buffer]'])
821  assert_equal(#_VIEWS, 2)
822  assert_equal(buffer:get_text(), '1\t2\t3\n')
823  ui.goto_view(-1) -- first view
824  assert(buffer._type ~= _L['[Message Buffer]'], 'still in message buffer')
825  ui.print(4, 5, 6) -- should jump to second view
826  assert_equal(buffer._type, _L['[Message Buffer]'])
827  assert_equal(buffer:get_text(), '1\t2\t3\n4\t5\t6\n')
828  ui.goto_view(-1) -- first view
829  assert(buffer._type ~= _L['[Message Buffer]'], 'still in message buffer')
830  ui.silent_print = true
831  ui.print(7, 8, 9) -- should stay in first view
832  assert(buffer._type ~= _L['[Message Buffer]'], 'switched to message buffer')
833  assert_equal(_BUFFERS[#_BUFFERS]:get_text(), '1\t2\t3\n4\t5\t6\n7\t8\t9\n')
834  ui.silent_print = false
835  ui.goto_view(1) -- second view
836  assert_equal(buffer._type, _L['[Message Buffer]'])
837  view:goto_buffer(-1)
838  assert(buffer._type ~= _L['[Message Buffer]'], 'message buffer still visible')
839  ui.print()
840  assert_equal(buffer._type, _L['[Message Buffer]'])
841  assert_equal(buffer:get_text(), '1\t2\t3\n4\t5\t6\n7\t8\t9\n\n')
842  view:unsplit()
843
844  buffer:close()
845  ui.tabs = tabs
846  ui.silent_print = silent_print
847end
848
849function test_ui_print_to_other_view()
850  local silent_print = ui.silent_print
851
852  ui.silent_print = false
853  view:split()
854  ui.goto_view(-1)
855  assert_equal(_VIEWS[view], 1)
856  ui.print('foo') -- should print to other view, not split again
857  assert_equal(#_VIEWS, 2)
858  assert_equal(_VIEWS[view], 2)
859  buffer:close()
860  ui.goto_view(-1)
861  view:unsplit()
862
863  ui.silent_print = silent_print
864end
865
866function test_ui_dialogs_colorselect_interactive()
867  local color = ui.dialogs.colorselect{title = 'Blue', color = 0xFF0000}
868  assert_equal(color, 0xFF0000)
869  color = ui.dialogs.colorselect{
870    title = 'Red', color = '#FF0000', palette = {'#FF0000', 0x00FF00},
871    string_output = true
872  }
873  assert_equal(color, '#FF0000')
874
875  assert_raises(function() ui.dialogs.colorselect{title = function() end} end, "bad argument #title to 'colorselect' (string/number/table/boolean expected, got function")
876  assert_raises(function() ui.dialogs.colorselect{palette = {true}} end, "bad argument #palette[1] to 'colorselect' (string/number expected, got boolean")
877end
878
879function test_ui_dialogs_dropdown_interactive()
880  local dropdowns = {'dropdown', 'standard_dropdown'}
881  for _, dropdown in ipairs(dropdowns) do
882    print('Running ' .. dropdown)
883    local button, i = ui.dialogs[dropdown]{items = {'foo', 'bar', 'baz'}}
884    assert_equal(type(button), 'number')
885    assert_equal(i, 1)
886    button, i = ui.dialogs[dropdown]{
887      text = 'foo', items = {'bar', 'baz', 'quux'}, select = 2,
888      no_cancel = true, width = 400, height = 400
889    }
890    assert_equal(i, 2)
891  end
892
893  assert_raises(function() ui.dialogs.dropdown{items = {'foo', 'bar', 'baz'}, select = true} end, "bad argument #select to 'dropdown' (number expected, got boolean")
894  assert_raises(function() ui.dialogs.dropdown{items = {'foo', 'bar', 'baz', true}} end, "bad argument #items[4] to 'dropdown' (string/number expected, got boolean")
895end
896
897function test_ui_dialogs_filesave_fileselect_interactive()
898  local test_filename = _HOME .. '/test/ui/empty'
899  local test_dir, test_file = test_filename:match('^(.+[/\\])([^/\\]+)$')
900  local filename = ui.dialogs.filesave{
901    with_directory = test_dir, with_file = test_file,
902    no_create_directories = true
903  }
904  assert_equal(filename, test_filename)
905  filename = ui.dialogs.fileselect{
906    with_directory = test_dir, with_file = test_file, select_multiple = true
907  }
908  assert_equal(filename, {test_filename})
909  filename = ui.dialogs.fileselect{
910    with_directory = test_dir, select_only_directories = true
911  }
912  assert_equal(filename, test_dir:match('^(.+)/$'))
913end
914
915function test_ui_dialogs_filteredlist_interactive()
916  local _, i = ui.dialogs.filteredlist{
917    informative_text = 'foo', columns = '1', items = {'bar', 'baz', 'quux'},
918    text = 'b z'
919  }
920  assert_equal(i, 2)
921  local _, text = ui.dialogs.filteredlist{
922    columns = {'1', '2'},
923    items = {'foo', 'foobar', 'bar', 'barbaz', 'baz', 'bazfoo'},
924    search_column = 2, text = 'baz', output_column = 2, string_output = true,
925    select_multiple = true, button1 = _L['OK'], button2 = _L['Cancel'],
926    button3 = 'Other', width = ui.size[1] / 2
927  }
928  assert_equal(text, {'barbaz'})
929end
930
931function test_ui_dialogs_fontselect_interactive()
932  local font = ui.dialogs.fontselect{
933    font_name = 'Monospace', font_size = 14, font_style = 'Bold'
934  }
935  assert_equal(font, 'Monospace Bold 14')
936end
937
938function test_ui_dialogs_inputbox_interactive()
939  local inputboxes = {
940    'inputbox', 'secure_inputbox', 'standard_inputbox',
941    'secure_standard_inputbox'
942  }
943  for _, inputbox in ipairs(inputboxes) do
944    print('Running ' .. inputbox)
945    local button, text = ui.dialogs[inputbox]{text = 'foo'}
946    assert_equal(type(button), 'number')
947    assert_equal(text, 'foo')
948    button, text = ui.dialogs[inputbox]{
949      text = 'foo', string_output = true, no_cancel = true
950    }
951    assert_equal(type(button), 'string')
952    assert_equal(text, 'foo')
953  end
954
955  local button, text = ui.dialogs.inputbox{
956    informative_text = {'info', 'foo', 'baz'}, text = {'bar', 'quux'}
957  }
958  assert_equal(type(button), 'number')
959  assert_equal(text, {'bar', 'quux'})
960  button = ui.dialogs.inputbox{
961    informative_text = {'info', 'foo', 'baz'}, text = {'bar', 'quux'},
962    string_output = true
963  }
964  assert_equal(type(button), 'string')
965end
966
967function test_ui_dialogs_msgbox_interactive()
968  local msgboxes = {'msgbox', 'ok_msgbox', 'yesno_msgbox'}
969  local icons = {'gtk-dialog-info', 'gtk-dialog-warning', 'gtk-dialog-question'}
970  for i, msgbox in ipairs(msgboxes) do
971    print('Running ' .. msgbox)
972    local button = ui.dialogs[msgbox]{icon = icons[i]}
973    assert_equal(type(button), 'number')
974    button = ui.dialogs[msgbox]{
975      icon_file = _HOME .. '/core/images/ta_32x32.png', string_output = true,
976      no_cancel = true
977    }
978    assert_equal(type(button), 'string')
979  end
980end
981
982function test_ui_dialogs_optionselect_interactive()
983  local _, selected = ui.dialogs.optionselect{items = 'foo', select = 1}
984  assert_equal(selected, {1})
985  _, selected = ui.dialogs.optionselect{
986    items = {'foo', 'bar', 'baz'}, select = {1, 3}, string_output = true
987  }
988  assert_equal(selected, {'foo', 'baz'})
989
990  assert_raises(function() ui.dialogs.optionselect{items = {'foo', 'bar', 'baz'}, select = {1, 'bar'}} end, "bad argument #select[2] to 'optionselect' (number expected, got string")
991end
992
993function test_ui_dialogs_progressbar_interactive()
994  local i = 0
995  ui.dialogs.progressbar({title = 'foo'}, function()
996    os.execute('sleep 0.1')
997    i = i + 10
998    if i > 100 then return nil end
999    return i, i .. '%'
1000  end)
1001
1002  local stopped = ui.dialogs.progressbar({
1003    title = 'foo', indeterminite = true, stoppable = true
1004  }, function()
1005    os.execute('sleep 0.1')
1006    return 50
1007  end)
1008  assert(stopped, 'progressbar not stopped')
1009
1010  ui.update() -- allow GTK to remove callback for previous function
1011  i = 0
1012  ui.dialogs.progressbar({title = 'foo', stoppable = true}, function()
1013    os.execute('sleep 0.1')
1014    i = i + 10
1015    if i > 100 then return nil end
1016    return i, i <= 50 and "stop disable" or "stop enable"
1017  end)
1018
1019  local errmsg
1020  local handler = function(message)
1021    errmsg = message
1022    return false -- halt propagation
1023  end
1024  events.connect(events.ERROR, handler, 1)
1025  ui.dialogs.progressbar({}, function() error('foo') end)
1026  assert(errmsg:find('foo'), 'error handler did not run')
1027  ui.dialogs.progressbar({}, function() return true end)
1028  assert(errmsg:find('invalid return values'), 'error handler did not run')
1029  events.disconnect(events.ERROR, handler)
1030end
1031
1032function test_ui_dialogs_textbox_interactive()
1033  ui.dialogs.textbox{
1034    text = 'foo', editable = true, selected = true, monospaced_font = true
1035  }
1036  ui.dialogs.textbox{text_from_file = _HOME .. '/LICENSE', scroll_to = 'bottom'}
1037end
1038
1039function test_ui_switch_buffer_interactive()
1040  buffer.new()
1041  buffer:append_text('foo')
1042  buffer.new()
1043  buffer:append_text('bar')
1044  buffer:new()
1045  buffer:append_text('baz')
1046  ui.switch_buffer() -- back to [Test Output]
1047  local text = buffer:get_text()
1048  assert(text ~= 'foo' and text ~= 'bar' and text ~= 'baz')
1049  for i = 1, 3 do view:goto_buffer(1) end -- cycle back to baz
1050  ui.switch_buffer(true)
1051  assert_equal(buffer:get_text(), 'bar')
1052  for i = 1, 3 do buffer:close(true) end
1053end
1054
1055function test_ui_goto_file()
1056  local dir1_file1 = _HOME .. '/core/ui/dir1/file1'
1057  local dir1_file2 = _HOME .. '/core/ui/dir1/file2'
1058  local dir2_file1 = _HOME .. '/core/ui/dir2/file1'
1059  local dir2_file2 = _HOME .. '/core/ui/dir2/file2'
1060  ui.goto_file(dir1_file1) -- current view
1061  assert_equal(#_VIEWS, 1)
1062  assert_equal(buffer.filename, dir1_file1)
1063  ui.goto_file(dir1_file2, true) -- split view
1064  assert_equal(#_VIEWS, 2)
1065  assert_equal(buffer.filename, dir1_file2)
1066  assert_equal(_VIEWS[1].buffer.filename, dir1_file1)
1067  ui.goto_file(dir1_file1) -- should go back to first view
1068  assert_equal(buffer.filename, dir1_file1)
1069  assert_equal(_VIEWS[2].buffer.filename, dir1_file2)
1070  ui.goto_file(dir2_file2, true, nil, true) -- should sloppily go back to second view
1071  assert_equal(buffer.filename, dir1_file2) -- sloppy
1072  assert_equal(_VIEWS[1].buffer.filename, dir1_file1)
1073  ui.goto_file(dir2_file1) -- should go back to first view
1074  assert_equal(buffer.filename, dir2_file1)
1075  assert_equal(_VIEWS[2].buffer.filename, dir1_file2)
1076  ui.goto_file(dir2_file2, false, _VIEWS[1]) -- should go to second view
1077  assert_equal(#_VIEWS, 2)
1078  assert_equal(buffer.filename, dir2_file2)
1079  assert_equal(_VIEWS[1].buffer.filename, dir2_file1)
1080  view:unsplit()
1081  assert_equal(#_VIEWS, 1)
1082  for i = 1, 4 do buffer:close() end
1083end
1084
1085function test_ui_uri_drop()
1086  local filename = _HOME .. '/test/ui/uri drop'
1087  local uri = 'file://' .. _HOME .. '/test/ui/uri%20drop'
1088  events.emit(events.URI_DROPPED, uri)
1089  assert_equal(buffer.filename, filename)
1090  buffer:close()
1091  local buffer = buffer
1092  events.emit(events.URI_DROPPED, 'file://' .. _HOME)
1093  assert_equal(buffer, _G.buffer) -- do not open directory
1094
1095  -- TODO: WIN32
1096  -- TODO: OSX
1097end
1098
1099function test_ui_buffer_switch_save_restore_properties()
1100  local filename = _HOME .. '/test/ui/test.lua'
1101  io.open_file(filename)
1102  buffer:goto_pos(10)
1103  view:fold_line(
1104    buffer:line_from_position(buffer.current_pos), view.FOLDACTION_CONTRACT)
1105  view.margin_width_n[1] = 0 -- hide line numbers
1106  view:goto_buffer(-1)
1107  assert(view.margin_width_n[1] > 0, 'line numbers are still hidden')
1108  view:goto_buffer(1)
1109  assert_equal(buffer.current_pos, 10)
1110  assert_equal(view.fold_expanded[buffer:line_from_position(buffer.current_pos)], false)
1111  assert_equal(view.margin_width_n[1], 0)
1112  buffer:close()
1113end
1114
1115if CURSES then
1116  -- TODO: clipboard, mouse events, etc.
1117end
1118
1119function test_spawn_cwd()
1120  assert_equal(os.spawn('pwd'):read('a'), lfs.currentdir() .. '\n')
1121  assert_equal(os.spawn('pwd', '/tmp'):read('a'), '/tmp\n')
1122end
1123
1124function test_spawn_env()
1125  assert(not os.spawn('env'):read('a'):find('^%s*$'), 'empty env')
1126  assert(os.spawn('env', {FOO = 'bar'}):read('a'):find('FOO=bar\n'), 'env not set')
1127  local output = os.spawn('env', {FOO = 'bar', 'BAR=baz', [true] = 'false'}):read('a')
1128  assert(output:find('FOO=bar\n'), 'env not set properly')
1129  assert(output:find('BAR=baz\n'), 'env not set properly')
1130  assert(not output:find('true=false\n'), 'env not set properly')
1131end
1132
1133function test_spawn_stdin()
1134  local p = os.spawn('lua -e "print(io.read())"')
1135  p:write('foo\n')
1136  p:close()
1137  assert_equal(p:read('l'), 'foo')
1138  assert_equal(p:read('a'), '')
1139end
1140
1141function test_spawn_callbacks()
1142  local exit_status = -1
1143  local p = os.spawn('echo foo', ui.print, nil, function(status) exit_status = status end)
1144  os.execute('sleep 0.1')
1145  ui.update()
1146  assert_equal(buffer._type, _L['[Message Buffer]'])
1147  assert(buffer:get_text():find('^foo'), 'no spawn stdout')
1148  assert_equal(exit_status, 0)
1149  buffer:close(true)
1150  view:unsplit()
1151  -- Verify stdout is not read as stderr.
1152  p = os.spawn('echo foo', nil, ui.print)
1153  os.execute('sleep 0.1')
1154  ui.update()
1155  assert_equal(#_BUFFERS, 1)
1156end
1157
1158function test_spawn_wait()
1159  local exit_status = -1
1160  local p = os.spawn('sleep 0.1', nil, nil, function(status) exit_status = status end)
1161  assert_equal(p:status(), "running")
1162  assert_equal(p:wait(), 0)
1163  assert_equal(exit_status, 0)
1164  assert_equal(p:status(), "terminated")
1165  -- Verify call to wait again returns previous exit status.
1166  assert_equal(p:wait(), exit_status)
1167end
1168
1169function test_spawn_kill()
1170  local p = os.spawn('sleep 1')
1171  p:kill()
1172  assert(p:wait() ~= 0)
1173  assert_equal(p:status(), "terminated")
1174end
1175
1176if WIN32 and CURSES then
1177  function test_spawn()
1178    -- TODO:
1179  end
1180end
1181
1182function test_buffer_text_range()
1183  buffer.new()
1184  buffer:set_text('foo\nbar\nbaz')
1185  buffer:set_target_range(5, 8)
1186  assert_equal(buffer.target_text, 'bar')
1187  assert_equal(buffer:text_range(1, buffer.length + 1), 'foo\nbar\nbaz')
1188  assert_equal(buffer:text_range(-1, 4), 'foo')
1189  assert_equal(buffer:text_range(9, 16), 'baz')
1190  assert_equal(buffer.target_text, 'bar') -- assert target range is unchanged
1191  buffer:close(true)
1192
1193  assert_raises(function() buffer:text_range() end, 'number expected, got nil')
1194  assert_raises(function() buffer:text_range(5) end, 'number expected, got nil')
1195end
1196
1197function test_bookmarks()
1198  local function has_bookmark(line)
1199    return buffer:marker_get(line) & 1 << textadept.bookmarks.MARK_BOOKMARK - 1 > 0
1200  end
1201
1202  buffer.new()
1203  buffer:new_line()
1204  assert(buffer:line_from_position(buffer.current_pos) > 1, 'still on first line')
1205  textadept.bookmarks.toggle()
1206  assert(has_bookmark(2), 'no bookmark')
1207  textadept.bookmarks.toggle()
1208  assert(not has_bookmark(2), 'bookmark still there')
1209
1210  buffer:goto_pos(buffer:position_from_line(1))
1211  textadept.bookmarks.toggle()
1212  buffer:goto_pos(buffer:position_from_line(2))
1213  textadept.bookmarks.toggle()
1214  textadept.bookmarks.goto_mark(true)
1215  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
1216  textadept.bookmarks.goto_mark(true)
1217  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
1218  textadept.bookmarks.goto_mark(false)
1219  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
1220  textadept.bookmarks.goto_mark(false)
1221  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
1222  textadept.bookmarks.clear()
1223  assert(not has_bookmark(1), 'bookmark still there')
1224  assert(not has_bookmark(2), 'bookmark still there')
1225  buffer:close(true)
1226end
1227
1228function test_bookmarks_interactive()
1229  buffer.new()
1230  buffer:new_line()
1231  textadept.bookmarks.toggle()
1232  buffer:line_up()
1233  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
1234  textadept.bookmarks.goto_mark()
1235  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
1236  buffer:close(true)
1237end
1238
1239function test_bookmarks_reload()
1240  local function has_bookmark(line)
1241    return buffer:marker_get(line) & 1 << textadept.bookmarks.MARK_BOOKMARK - 1 > 0
1242  end
1243
1244  io.open_file(_HOME .. '/test/modules/textadept/bookmarks/foo')
1245  buffer:line_down()
1246  textadept.bookmarks.toggle()
1247  buffer:line_down()
1248  buffer:line_down()
1249  textadept.bookmarks.toggle()
1250  assert(has_bookmark(2), 'line not bookmarked')
1251  assert(has_bookmark(4), 'line not bookmarked')
1252  buffer:reload()
1253  assert(has_bookmark(2), 'bookmark not restored')
1254  assert(has_bookmark(4), 'bookmark not restored')
1255  buffer:close(true)
1256end
1257
1258function test_command_entry_run()
1259  local command_run, tab_pressed = false, false
1260  ui.command_entry.run(function(command) command_run = command end, {
1261    ['\t'] = function() tab_pressed = true end
1262  }, nil, 2)
1263  ui.update() -- redraw command entry
1264  assert_equal(ui.command_entry.active, true)
1265  assert_equal(ui.command_entry:get_lexer(), 'text')
1266  assert(ui.command_entry.height > ui.command_entry:text_height(0), 'height < 2 lines')
1267  ui.command_entry:set_text('foo')
1268  events.emit(events.KEYPRESS, string.byte('\t'))
1269  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1270  assert_equal(command_run, 'foo')
1271  assert(tab_pressed, '\\t not registered')
1272  assert_equal(ui.command_entry.active, false)
1273
1274  assert_raises(function() ui.command_entry.run(function() end, 1) end, 'table/string/nil expected, got number')
1275  assert_raises(function() ui.command_entry.run(function() end, {}, 1) end, 'string/nil expected, got number')
1276  assert_raises(function() ui.command_entry.run(function() end, {}, 'lua', true) end, 'number/nil expected, got boolean')
1277  assert_raises(function() ui.command_entry.run(function() end, 'lua', true) end, 'number/nil expected, got boolean')
1278end
1279
1280local function run_lua_command(command)
1281  ui.command_entry.run()
1282  ui.command_entry:set_text(command)
1283  assert_equal(ui.command_entry:get_lexer(), 'lua')
1284  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1285end
1286
1287function test_command_entry_run_lua()
1288  run_lua_command('print(_HOME)')
1289  assert_equal(buffer._type, _L['[Message Buffer]'])
1290  assert_equal(buffer:get_text(), _HOME .. '\n')
1291  run_lua_command('{key="value"}')
1292  assert(buffer:get_text():find('{key = value}'), 'table not pretty-printed')
1293  -- TODO: multi-line table pretty print.
1294  if #_VIEWS > 1 then view:unsplit() end
1295  buffer:close()
1296end
1297
1298function test_command_entry_run_lua_abbreviated_env()
1299  -- buffer get/set.
1300  run_lua_command('length')
1301  assert(buffer:get_text():find('%d+%s*$'), 'buffer.length result not a number')
1302  run_lua_command('auto_c_active')
1303  assert(buffer:get_text():find('false%s*$'), 'buffer:auto_c_active() result not false')
1304  run_lua_command('view_eol=true')
1305  assert_equal(view.view_eol, true)
1306  -- view get/set.
1307  if #_VIEWS > 1 then view:unsplit() end
1308  run_lua_command('split')
1309  assert_equal(#_VIEWS, 2)
1310  run_lua_command('size=1')
1311  assert_equal(view.size, 1)
1312  run_lua_command('unsplit')
1313  assert_equal(#_VIEWS, 1)
1314  -- ui get/set.
1315  run_lua_command('dialogs')
1316  assert(buffer:get_text():find('%b{}%s*$'), 'ui.dialogs result not a table')
1317  run_lua_command('statusbar_text="foo"')
1318  -- _G get/set.
1319  run_lua_command('foo="bar"')
1320  run_lua_command('foo')
1321  assert(buffer:get_text():find('bar%s*$'), 'foo result not "bar"')
1322  -- textadept get/set.
1323  run_lua_command('editing')
1324  assert(buffer:get_text():find('%b{}%s*$'), 'textadept.editing result not a table')
1325  run_lua_command('editing.select_paragraph')
1326  assert(buffer.selection_start ~= buffer.selection_end, 'textadept.editing.select_paragraph() did not select paragraph')
1327  buffer:close()
1328end
1329
1330local function assert_lua_autocompletion(text, first_item)
1331  ui.command_entry:set_text(text)
1332  ui.command_entry:goto_pos(ui.command_entry.length + 1)
1333  events.emit(events.KEYPRESS, string.byte('\t'))
1334  assert_equal(ui.command_entry:auto_c_active(), true)
1335  assert_equal(ui.command_entry.auto_c_current_text, first_item)
1336  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1337  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1338  assert_equal(ui.command_entry:get_text(), text) -- no history cycling
1339  assert_equal(ui.command_entry:auto_c_active(), true)
1340  assert_equal(ui.command_entry.auto_c_current_text, first_item)
1341  ui.command_entry:auto_c_cancel()
1342end
1343
1344function test_command_entry_complete_lua()
1345  ui.command_entry.run()
1346  assert_lua_autocompletion('string.', 'byte')
1347  assert_lua_autocompletion('auto', 'auto_c_active')
1348  assert_lua_autocompletion('MARK', 'MARKER_MAX')
1349  assert_lua_autocompletion('buffer.auto', 'auto_c_auto_hide')
1350  assert_lua_autocompletion('buffer:auto', 'auto_c_active')
1351  assert_lua_autocompletion('caret', 'caret_fore')
1352  assert_lua_autocompletion('ANNO', 'ANNOTATION_BOXED')
1353  assert_lua_autocompletion('view.margin', 'margin_back_n')
1354  assert_lua_autocompletion('view:call', 'call_tip_active')
1355  assert_lua_autocompletion('goto', 'goto_buffer')
1356  assert_lua_autocompletion('_', '_BUFFERS')
1357  assert_lua_autocompletion('fi', 'file_types')
1358  -- TODO: textadept.editing.show_documentation key binding.
1359  ui.command_entry:focus() -- hide
1360end
1361
1362function test_command_entry_history()
1363  local one, two = function() end, function() end
1364
1365  ui.command_entry.run(one)
1366  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1367  assert_equal(ui.command_entry:get_text(), '') -- no prior history
1368  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1369  assert_equal(ui.command_entry:get_text(), '') -- no further history
1370  ui.command_entry:add_text('foo')
1371  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1372
1373  ui.command_entry.run(two)
1374  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1375  assert_equal(ui.command_entry:get_text(), '') -- no prior history
1376  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1377  assert_equal(ui.command_entry:get_text(), '') -- no further history
1378  ui.command_entry:add_text('bar')
1379  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1380
1381  ui.command_entry.run(one)
1382  assert_equal(ui.command_entry:get_text(), 'foo')
1383  assert_equal(ui.command_entry.selection_start, 1)
1384  assert_equal(ui.command_entry.selection_end, 4)
1385  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1386  assert_equal(ui.command_entry:get_text(), 'foo') -- no prior history
1387  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1388  assert_equal(ui.command_entry:get_text(), 'foo') -- no further history
1389  ui.command_entry:set_text('baz')
1390  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1391
1392  ui.command_entry.run(one)
1393  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1394  assert_equal(ui.command_entry:get_text(), 'foo')
1395  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1396  assert_equal(ui.command_entry:get_text(), 'baz')
1397  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up, 'foo'
1398  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1399
1400  ui.command_entry.run(one)
1401  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1402  assert_equal(ui.command_entry:get_text(), 'baz')
1403  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1404  assert_equal(ui.command_entry:get_text(), 'foo')
1405  events.emit(events.KEYPRESS, not CURSES and 0xFF1B or 7) -- esc
1406
1407  ui.command_entry.run(two)
1408  assert_equal(ui.command_entry:get_text(), 'bar')
1409  events.emit(events.KEYPRESS, not CURSES and 0xFF1B or 7) -- esc
1410end
1411
1412function test_command_entry_history_append()
1413  local f, keys = function() end, {['\n'] = ui.command_entry.focus}
1414
1415  ui.command_entry.run(f, keys)
1416  ui.command_entry:set_text('foo')
1417  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1418
1419  ui.command_entry.run(f, keys)
1420  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1421  assert_equal(ui.command_entry:get_text(), '') -- no prior history
1422  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1423  assert_equal(ui.command_entry:get_text(), '') -- no further history
1424  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1425  ui.command_entry.append_history('bar')
1426
1427  ui.command_entry.run(f, keys)
1428  assert_equal(ui.command_entry:get_text(), 'bar')
1429  assert_equal(ui.command_entry.selection_start, 1)
1430  assert_equal(ui.command_entry.selection_end, 4)
1431  events.emit(events.KEYPRESS, not CURSES and 0xFF52 or 301) -- up
1432  assert_equal(ui.command_entry:get_text(), 'bar') -- no prior history
1433  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
1434  assert_equal(ui.command_entry:get_text(), 'bar') -- no further history
1435  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1436
1437  -- Verify no previous mode or history is needed for adding history.
1438  local f2 = function() end
1439  ui.command_entry.append_history(f2, 'baz')
1440  ui.command_entry.run(f2, keys)
1441  assert_equal(ui.command_entry:get_text(), 'baz')
1442  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1443
1444  assert_raises(function() ui.command_entry.append_history(1) end, 'string expected, got number')
1445  assert_raises(function() ui.command_entry:append_history('text') end, 'function expected, got table')
1446  assert_raises(function() ui.command_entry.append_history(function() end, true) end, 'string/nil expected, got boolean')
1447end
1448
1449function test_command_entry_mode_restore()
1450  local mode = 'test_mode'
1451  keys.mode = mode
1452  ui.command_entry.run(nil)
1453  assert(keys.mode ~= mode)
1454  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
1455  assert_equal(keys.mode, mode)
1456  keys.mode = nil
1457end
1458
1459function test_command_entry_text_changed_event()
1460  local changed = false
1461  ui.command_entry.run()
1462  events.connect(events.COMMAND_TEXT_CHANGED, function() changed = true end)
1463  ui.command_entry:set_text('foo')
1464  assert(changed, 'changed event not emitted')
1465  changed = false
1466  ui.command_entry:undo()
1467  assert(changed, 'changed event not emitted')
1468  ui.command_entry:focus() -- hide
1469end
1470
1471function test_editing_auto_pair()
1472  buffer.new()
1473  -- Single selection.
1474  buffer:add_text('foo(')
1475  events.emit(events.CHAR_ADDED, string.byte('('))
1476  assert_equal(buffer:get_text(), 'foo()')
1477  events.emit(events.KEYPRESS, string.byte(')'))
1478  assert_equal(buffer.current_pos, buffer.line_end_position[1])
1479  buffer:char_left()
1480  -- Note: cannot check for brace highlighting; indicator search does not work.
1481  events.emit(events.KEYPRESS, not CURSES and 0xFF08 or 263) -- \b
1482  assert_equal(buffer:get_text(), 'foo')
1483  -- Multi-selection.
1484  buffer:set_text('foo(\nfoo(')
1485  local pos1 = buffer.line_end_position[1]
1486  local pos2 = buffer.line_end_position[2]
1487  buffer:set_selection(pos1, pos1)
1488  buffer:add_selection(pos2, pos2)
1489  events.emit(events.CHAR_ADDED, string.byte('('))
1490  assert_equal(buffer:get_text(), 'foo()\nfoo()')
1491  assert_equal(buffer.selections, 2)
1492  assert_equal(buffer.selection_n_start[1], buffer.selection_n_end[1])
1493  assert_equal(buffer.selection_n_start[1], pos1)
1494  assert_equal(buffer.selection_n_start[2], buffer.selection_n_end[2])
1495  assert_equal(buffer.selection_n_start[2], pos2 + 1)
1496  -- TODO: typeover.
1497  events.emit(events.KEYPRESS, not CURSES and 0xFF08 or 263) -- \b
1498  assert_equal(buffer:get_text(), 'foo\nfoo')
1499  -- Verify atomic undo for multi-select.
1500  buffer:undo() -- simulated backspace
1501  buffer:undo() -- normal undo that a user would perform
1502  assert_equal(buffer:get_text(), 'foo()\nfoo()')
1503  buffer:undo()
1504  assert_equal(buffer:get_text(), 'foo(\nfoo(')
1505  buffer:close(true)
1506end
1507
1508function test_editing_auto_indent()
1509  buffer.new()
1510  buffer:add_text('foo')
1511  buffer:new_line()
1512  assert_equal(buffer.line_indentation[2], 0)
1513  buffer:tab()
1514  buffer:add_text('bar')
1515  buffer:new_line()
1516  assert_equal(buffer.line_indentation[3], buffer.tab_width)
1517  assert_equal(buffer.current_pos, buffer.line_indent_position[3])
1518  buffer:new_line()
1519  buffer:back_tab()
1520  assert_equal(buffer.line_indentation[4], 0)
1521  assert_equal(buffer.current_pos, buffer:position_from_line(4))
1522  buffer:new_line() -- should indent since previous line is blank
1523  assert_equal(buffer.line_indentation[5], buffer.tab_width)
1524  assert_equal(buffer.current_pos, buffer.line_indent_position[5])
1525  buffer:goto_pos(buffer:position_from_line(2)) -- "\tbar"
1526  buffer:new_line() -- should not change indentation
1527  assert_equal(buffer.line_indentation[3], buffer.tab_width)
1528  assert_equal(buffer.current_pos, buffer:position_from_line(3))
1529  buffer:close(true)
1530end
1531
1532function test_editing_strip_trailing_spaces()
1533  local strip = textadept.editing.strip_trailing_spaces
1534  textadept.editing.strip_trailing_spaces = true
1535  buffer.new()
1536  local text = table.concat({
1537    'foo ',
1538    '  bar\t\r',
1539    'baz\t '
1540  }, '\n')
1541  buffer:set_text(text)
1542  buffer:goto_pos(buffer.line_end_position[2])
1543  events.emit(events.FILE_BEFORE_SAVE)
1544  assert_equal(buffer:get_text(), table.concat({
1545    'foo',
1546    '  bar',
1547    'baz',
1548    ''
1549  }, '\n'))
1550  assert_equal(buffer.current_pos, buffer.line_end_position[2])
1551  buffer:undo()
1552  assert_equal(buffer:get_text(), text)
1553  buffer.encoding = nil -- treat as a binary file
1554  events.emit(events.FILE_BEFORE_SAVE)
1555  assert_equal(buffer:get_text(), text)
1556  buffer:close(true)
1557  textadept.editing.strip_trailing_spaces = strip -- restore
1558end
1559
1560function test_editing_paste_reindent_tabs_to_tabs()
1561  ui.clipboard_text = table.concat({
1562    '\tfoo',
1563    '',
1564    '\t\tbar',
1565    '\tbaz'
1566  }, '\n')
1567  buffer.new()
1568  buffer.use_tabs, buffer.eol_mode = true, buffer.EOL_CRLF
1569  buffer:add_text('quux\r\n')
1570  textadept.editing.paste_reindent()
1571  assert_equal(buffer:get_text(), table.concat({
1572    'quux',
1573    'foo',
1574    '',
1575    '\tbar',
1576    'baz'
1577  }, '\r\n'))
1578  buffer:clear_all()
1579  buffer:add_text('\t\tquux\r\n\r\n') -- no auto-indent
1580  assert_equal(buffer.line_indentation[2], 0)
1581  assert_equal(buffer.line_indentation[3], 0)
1582  textadept.editing.paste_reindent()
1583  assert_equal(buffer:get_text(), table.concat({
1584    '\t\tquux',
1585    '',
1586    '\t\tfoo',
1587    '\t\t',
1588    '\t\t\tbar',
1589    '\t\tbaz'
1590  }, '\r\n'))
1591  buffer:clear_all()
1592  buffer:add_text('\t\tquux\r\n')
1593  assert_equal(buffer.line_indentation[2], 0)
1594  buffer:new_line() -- auto-indent
1595  assert_equal(buffer.line_indentation[3], 2 * buffer.tab_width)
1596  textadept.editing.paste_reindent()
1597  assert_equal(buffer:get_text(), table.concat({
1598    '\t\tquux',
1599    '',
1600    '\t\tfoo',
1601    '\t\t',
1602    '\t\t\tbar',
1603    '\t\tbaz'
1604  }, '\r\n'))
1605  buffer:close(true)
1606end
1607expected_failure(test_editing_paste_reindent_tabs_to_tabs)
1608
1609function test_editing_paste_reindent_spaces_to_spaces()
1610  ui.clipboard_text = table.concat({
1611    '    foo',
1612    '',
1613    '        bar',
1614    '            baz',
1615    '    quux'
1616  }, '\n')
1617  buffer.new()
1618  buffer.use_tabs, buffer.tab_width = false, 2
1619  buffer:add_text('foobar\n')
1620  textadept.editing.paste_reindent()
1621  assert_equal(buffer:get_text(), table.concat({
1622    'foobar',
1623    'foo',
1624    '',
1625    '  bar',
1626    '    baz',
1627    'quux'
1628  }, '\n'))
1629  buffer:clear_all()
1630  buffer:add_text('    foobar\n\n') -- no auto-indent
1631  assert_equal(buffer.line_indentation[2], 0)
1632  assert_equal(buffer.line_indentation[3], 0)
1633  textadept.editing.paste_reindent()
1634  assert_equal(buffer:get_text(), table.concat({
1635    '    foobar',
1636    '',
1637    '    foo',
1638    '    ',
1639    '      bar',
1640    '        baz',
1641    '    quux'
1642  }, '\n'))
1643  buffer:clear_all()
1644  buffer:add_text('    foobar\n')
1645  assert_equal(buffer.line_indentation[2], 0)
1646  buffer:new_line() -- auto-indent
1647  assert_equal(buffer.line_indentation[3], 4)
1648  textadept.editing.paste_reindent()
1649  assert_equal(buffer:get_text(), table.concat({
1650    '    foobar',
1651    '',
1652    '    foo',
1653    '    ',
1654    '      bar',
1655    '        baz',
1656    '    quux'
1657  }, '\n'))
1658  buffer:close(true)
1659end
1660expected_failure(test_editing_paste_reindent_spaces_to_spaces)
1661
1662function test_editing_paste_reindent_spaces_to_tabs()
1663  ui.clipboard_text = table.concat({
1664    '  foo',
1665    '    bar',
1666    '  baz'
1667  }, '\n')
1668  buffer.new()
1669  buffer.use_tabs, buffer.tab_width = true, 4
1670  buffer:add_text('\tquux')
1671  buffer:new_line()
1672  textadept.editing.paste_reindent()
1673  assert_equal(buffer:get_text(), table.concat({
1674    '\tquux',
1675    '\tfoo',
1676    '\t\tbar',
1677    '\tbaz'
1678  }, '\n'))
1679  buffer:close(true)
1680end
1681
1682function test_editing_paste_reindent_tabs_to_spaces()
1683  ui.clipboard_text = table.concat({
1684    '\tif foo and',
1685    '\t   bar then',
1686    '\t\tbaz()',
1687    '\tend',
1688    ''
1689  }, '\n')
1690  buffer.new()
1691  buffer.use_tabs, buffer.tab_width = false, 2
1692  buffer:set_lexer('lua')
1693  buffer:add_text('function quux()')
1694  buffer:new_line()
1695  buffer:insert_text(-1, 'end')
1696  buffer:colorize(1, -1) -- first line should be a fold header
1697  textadept.editing.paste_reindent()
1698  assert_equal(buffer:get_text(), table.concat({
1699    'function quux()',
1700    '  if foo and',
1701    '     bar then',
1702    '    baz()',
1703    '  end',
1704    'end'
1705  }, '\n'))
1706  buffer:close(true)
1707end
1708expected_failure(test_editing_paste_reindent_tabs_to_spaces)
1709
1710function test_editing_toggle_comment_lines()
1711  buffer.new()
1712  buffer:add_text('foo')
1713  textadept.editing.toggle_comment()
1714  assert_equal(buffer:get_text(), 'foo')
1715  buffer:set_lexer('lua')
1716  local text = table.concat({
1717    '',
1718    'local foo = "bar"',
1719    '  local baz = "quux"',
1720    ''
1721  }, '\n')
1722  buffer:set_text(text)
1723  buffer:goto_pos(buffer:position_from_line(2))
1724  textadept.editing.toggle_comment()
1725  assert_equal(buffer:get_text(), table.concat({
1726    '',
1727    '--local foo = "bar"',
1728    '  local baz = "quux"',
1729    ''
1730  }, '\n'))
1731  assert_equal(buffer.current_pos, buffer:position_from_line(2) + 2)
1732  textadept.editing.toggle_comment() -- uncomment
1733  assert_equal(buffer:get_line(2), 'local foo = "bar"\n')
1734  assert_equal(buffer.current_pos, buffer:position_from_line(2))
1735  local offset = 5
1736  buffer:set_sel(buffer:position_from_line(2) + offset, buffer:position_from_line(4) - offset)
1737  textadept.editing.toggle_comment()
1738  assert_equal(buffer:get_text(), table.concat({
1739    '',
1740    '--local foo = "bar"',
1741    '--  local baz = "quux"',
1742    ''
1743  }, '\n'))
1744  assert_equal(buffer.selection_start, buffer:position_from_line(2) + offset + 2)
1745  assert_equal(buffer.selection_end, buffer:position_from_line(4) - offset)
1746  textadept.editing.toggle_comment() -- uncomment
1747  assert_equal(buffer:get_text(), table.concat({
1748    '',
1749    'local foo = "bar"',
1750    '  local baz = "quux"',
1751    ''
1752  }, '\n'))
1753  assert_equal(buffer.selection_start, buffer:position_from_line(2) + offset)
1754  assert_equal(buffer.selection_end, buffer:position_from_line(4) - offset)
1755  buffer:undo() -- comment
1756  buffer:undo() -- uncomment
1757  assert_equal(buffer:get_text(), text) -- verify atomic undo
1758  buffer:close(true)
1759end
1760
1761function test_editing_toggle_comment()
1762  buffer.new()
1763  buffer:set_lexer('ansi_c')
1764  buffer:set_text(table.concat({
1765    '',
1766    '  const char *foo = "bar";',
1767    'const char *baz = "quux";',
1768    ''
1769  }, '\n'))
1770  buffer:set_sel(buffer:position_from_line(2), buffer:position_from_line(4))
1771  textadept.editing.toggle_comment()
1772  assert_equal(buffer:get_text(), table.concat({
1773    '',
1774    '  /*const char *foo = "bar";*/',
1775    '/*const char *baz = "quux";*/',
1776    ''
1777  }, '\n'))
1778  assert_equal(buffer.selection_start, buffer:position_from_line(2) + 2)
1779  assert_equal(buffer.selection_end, buffer:position_from_line(4))
1780  textadept.editing.toggle_comment() -- uncomment
1781  assert_equal(buffer:get_text(), table.concat({
1782    '',
1783    '  const char *foo = "bar";',
1784    'const char *baz = "quux";',
1785    ''
1786  }, '\n'))
1787  assert_equal(buffer.selection_start, buffer:position_from_line(2))
1788  assert_equal(buffer.selection_end, buffer:position_from_line(4))
1789  buffer:close(true)
1790end
1791
1792function test_editing_goto_line()
1793  buffer.new()
1794  buffer:new_line()
1795  textadept.editing.goto_line(1)
1796  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
1797  textadept.editing.goto_line(2)
1798  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
1799  buffer:close(true)
1800
1801  assert_raises(function() textadept.editing.goto_line(true) end, 'number/nil expected, got boolean')
1802end
1803
1804-- TODO: test_editing_goto_line_interactive
1805
1806function test_editing_transpose_chars()
1807  buffer.new()
1808  buffer:add_text('foobar')
1809  textadept.editing.transpose_chars()
1810  assert_equal(buffer:get_text(), 'foobra')
1811  buffer:char_left()
1812  textadept.editing.transpose_chars()
1813  assert_equal(buffer:get_text(), 'foobar')
1814  buffer:clear_all()
1815  buffer:add_text('⌘⇧⌥')
1816  textadept.editing.transpose_chars()
1817  assert_equal(buffer:get_text(), '⌘⌥⇧')
1818  buffer:char_left()
1819  textadept.editing.transpose_chars()
1820  assert_equal(buffer:get_text(), '⌘⇧⌥')
1821  buffer:clear_all()
1822  textadept.editing.transpose_chars()
1823  assert_equal(buffer:get_text(), '')
1824  buffer:add_text('a')
1825  textadept.editing.transpose_chars()
1826  assert_equal(buffer:get_text(), 'a')
1827  -- TODO: multiple selection?
1828  buffer:close(true)
1829end
1830
1831function test_editing_join_lines()
1832  buffer.new()
1833  buffer:append_text('foo\nbar\n  baz\nquux\n')
1834  textadept.editing.join_lines()
1835  assert_equal(buffer:get_text(), 'foo bar\n  baz\nquux\n')
1836  assert_equal(buffer.current_pos, 4)
1837  buffer:set_sel(buffer:position_from_line(2) + 5, buffer:position_from_line(4) - 5)
1838  textadept.editing.join_lines()
1839  assert_equal(buffer:get_text(), 'foo bar\n  baz quux\n')
1840  buffer:close(true)
1841end
1842
1843function test_editing_enclose()
1844  buffer.new()
1845  buffer.add_text('foo bar')
1846  textadept.editing.enclose('"', '"')
1847  assert_equal(buffer:get_text(), 'foo "bar"')
1848  buffer:undo()
1849  buffer:select_all()
1850  textadept.editing.enclose('(', ')')
1851  assert_equal(buffer:get_text(), '(foo bar)')
1852  buffer:undo()
1853  buffer:append_text('\nfoo bar')
1854  buffer:set_selection(buffer.line_end_position[1], buffer.line_end_position[1])
1855  buffer:add_selection(buffer.line_end_position[2], buffer.line_end_position[2])
1856  textadept.editing.enclose('<', '>')
1857  assert_equal(buffer:get_text(), 'foo <bar>\nfoo <bar>')
1858  buffer:undo()
1859  assert_equal(buffer:get_text(), 'foo bar\nfoo bar') -- verify atomic undo
1860  buffer:set_selection(buffer:position_from_line(1), buffer.line_end_position[1])
1861  buffer:add_selection(buffer:position_from_line(2), buffer.line_end_position[2])
1862  textadept.editing.enclose('-', '-')
1863  assert_equal(buffer:get_text(), '-foo bar-\n-foo bar-')
1864  buffer:close(true)
1865
1866  assert_raises(function() textadept.editing.enclose() end, 'string expected, got nil')
1867  assert_raises(function() textadept.editing.enclose('<', 1) end, 'string expected, got number')
1868end
1869
1870function test_editing_auto_enclose()
1871  local auto_enclose = textadept.editing.auto_enclose
1872  buffer.new()
1873  buffer:add_text('foo bar')
1874  buffer:word_left_extend()
1875  textadept.editing.auto_enclose = false
1876  events.emit(events.KEYPRESS, string.byte('*')) -- simulate typing
1877  assert(buffer:get_text() ~= 'foo *bar*')
1878  textadept.editing.auto_enclose = true
1879  events.emit(events.KEYPRESS, string.byte('*')) -- simulate typing
1880  assert_equal(buffer:get_text(), 'foo *bar*')
1881  buffer:undo()
1882  buffer:select_all()
1883  events.emit(events.KEYPRESS, string.byte('(')) -- simulate typing
1884  assert_equal(buffer:get_text(), '(foo bar)')
1885  buffer:close(true)
1886  textadept.editing.auto_enclose = auto_enclose -- restore
1887end
1888
1889function test_editing_select_enclosed()
1890  buffer.new()
1891  buffer:add_text('("foo bar")')
1892  buffer:goto_pos(6)
1893  textadept.editing.select_enclosed()
1894  assert_equal(buffer:get_sel_text(), 'foo bar')
1895  textadept.editing.select_enclosed()
1896  assert_equal(buffer:get_sel_text(), '"foo bar"')
1897  textadept.editing.select_enclosed()
1898  assert_equal(buffer:get_sel_text(), 'foo bar')
1899  buffer:goto_pos(6)
1900  textadept.editing.select_enclosed('("', '")')
1901  assert_equal(buffer:get_sel_text(), 'foo bar')
1902  textadept.editing.select_enclosed('("', '")')
1903  assert_equal(buffer:get_sel_text(), '("foo bar")')
1904  textadept.editing.select_enclosed('("', '")')
1905  assert_equal(buffer:get_sel_text(), 'foo bar')
1906  buffer:append_text('"baz"')
1907  buffer:goto_pos(10) -- last " on first line
1908  textadept.editing.select_enclosed()
1909  assert_equal(buffer:get_sel_text(), 'foo bar')
1910  buffer:close(true)
1911
1912  assert_raises(function() textadept.editing.select_enclosed('"') end, 'string expected, got nil')
1913end
1914expected_failure(test_editing_select_enclosed)
1915
1916function test_editing_select_word()
1917  buffer.new()
1918  buffer:append_text(table.concat({
1919    'foo',
1920    'foobar',
1921    'bar foo',
1922    'baz foo bar',
1923    'fooquux',
1924    'foo'
1925  }, '\n'))
1926  textadept.editing.select_word()
1927  assert_equal(buffer:get_sel_text(), 'foo')
1928  textadept.editing.select_word()
1929  assert_equal(buffer.selections, 2)
1930  assert_equal(buffer:get_sel_text(), 'foofoo') -- Scintilla stores it this way
1931  textadept.editing.select_word(true)
1932  assert_equal(buffer.selections, 4)
1933  assert_equal(buffer:get_sel_text(), 'foofoofoofoo')
1934  local lines = {}
1935  for i = 1, buffer.selections do
1936    lines[#lines + 1] = buffer:line_from_position(buffer.selection_n_start[i])
1937  end
1938  table.sort(lines)
1939  assert_equal(lines, {1, 3, 4, 6})
1940  buffer:close(true)
1941end
1942
1943function test_editing_select_line()
1944  buffer.new()
1945  buffer:add_text('foo\n  bar')
1946  textadept.editing.select_line()
1947  assert_equal(buffer:get_sel_text(), '  bar')
1948  buffer:close(true)
1949end
1950
1951function test_editing_select_paragraph()
1952  buffer.new()
1953  buffer:set_text(table.concat({
1954    'foo',
1955    '',
1956    'bar',
1957    'baz',
1958    '',
1959    'quux'
1960  }, '\n'))
1961  buffer:goto_pos(buffer:position_from_line(3))
1962  textadept.editing.select_paragraph()
1963  assert_equal(buffer:get_sel_text(), 'bar\nbaz\n\n')
1964  buffer:close(true)
1965end
1966
1967function test_editing_convert_indentation()
1968  buffer.new()
1969  local text = table.concat({
1970    '\tfoo',
1971    '  bar',
1972    '\t    baz',
1973    '    \tquux'
1974  }, '\n')
1975  buffer:set_text(text)
1976  buffer.use_tabs, buffer.tab_width = true, 4
1977  textadept.editing.convert_indentation()
1978  assert_equal(buffer:get_text(), table.concat({
1979    '\tfoo',
1980    '  bar',
1981    '\t\tbaz',
1982    '\t\tquux'
1983  }, '\n'))
1984  buffer:undo()
1985  assert_equal(buffer:get_text(), text) -- verify atomic undo
1986  buffer.use_tabs, buffer.tab_width = false, 2
1987  textadept.editing.convert_indentation()
1988  assert_equal(buffer:get_text(), table.concat({
1989    '  foo',
1990    '  bar',
1991    '      baz',
1992    '      quux'
1993  }, '\n'))
1994  buffer:close(true)
1995end
1996
1997function test_editing_highlight_word()
1998  local function verify(indics)
1999    local bit = 1 << textadept.editing.INDIC_HIGHLIGHT - 1
2000    for _, pos in ipairs(indics) do
2001      local mask = buffer:indicator_all_on_for(pos)
2002      assert(mask & bit > 0, 'no indicator on line %d', buffer:line_from_position(pos))
2003    end
2004  end
2005  local function update()
2006    ui.update()
2007    if CURSES then events.emit(events.UPDATE_UI, buffer.UPDATE_SELECTION) end
2008  end
2009
2010  local highlight = textadept.editing.highlight_words
2011  textadept.editing.highlight_words = textadept.editing.HIGHLIGHT_SELECTED
2012  buffer.new()
2013  buffer:append_text(table.concat({
2014    'foo',
2015    'foobar',
2016    'bar  foo',
2017    'baz foo bar',
2018    'fooquux',
2019    'foo'
2020  }, '\n'))
2021  local function verify_foo()
2022    verify{
2023      buffer:position_from_line(1),
2024      buffer:position_from_line(3) + 5,
2025      buffer:position_from_line(4) + 4,
2026      buffer:position_from_line(6)
2027    }
2028  end
2029  textadept.editing.select_word()
2030  update()
2031  verify_foo()
2032  events.emit(events.KEYPRESS, not CURSES and 0xFF1B or 7) -- esc
2033  local pos = buffer:indicator_end(textadept.editing.INDIC_HIGHLIGHT, 1)
2034  assert_equal(pos, 1) -- highlights cleared
2035  -- Verify turning off word highlighting.
2036  textadept.editing.highlight_words = textadept.editing.HIGHLIGHT_NONE
2037  textadept.editing.select_word()
2038  update()
2039  pos = buffer:indicator_end(textadept.editing.INDIC_HIGHLIGHT, 2)
2040  assert_equal(pos, 1) -- no highlights
2041  textadept.editing.highlight_words = textadept.editing.HIGHLIGHT_SELECTED
2042  -- Verify partial word selections do not highlight words.
2043  buffer:set_sel(1, 3)
2044  pos = buffer:indicator_end(textadept.editing.INDIC_HIGHLIGHT, 2)
2045  assert_equal(pos, 1) -- no highlights
2046  -- Verify multi-word selections do not highlight words.
2047  buffer:set_sel(buffer:position_from_line(3), buffer.line_end_position[3])
2048  assert(buffer:is_range_word(buffer.selection_start, buffer.selection_end))
2049  pos = buffer:indicator_end(textadept.editing.INDIC_HIGHLIGHT, 2)
2050  assert_equal(pos, 1) -- no highlights
2051  -- Verify current word highlighting.
2052  textadept.editing.highlight_words = textadept.editing.HIGHLIGHT_CURRENT
2053  buffer:goto_pos(1)
2054  update()
2055  verify_foo()
2056  buffer:line_down()
2057  update()
2058  verify{buffer:position_from_line(2)}
2059  buffer:line_down()
2060  update()
2061  verify{buffer:position_from_line(3), buffer:position_from_line(4) + 9}
2062  buffer:word_right()
2063  update()
2064  verify_foo()
2065  buffer:char_left()
2066  update()
2067  pos = buffer:indicator_end(textadept.editing.INDIC_HIGHLIGHT, 2)
2068  assert_equal(pos, 1) -- no highlights
2069  buffer:close(true)
2070  textadept.editing.highlight_words = highlight -- reset
2071end
2072
2073function test_editing_filter_through()
2074  buffer.new()
2075  buffer:set_text('3|baz\n1|foo\n5|foobar\n1|foo\n4|quux\n2|bar\n')
2076  textadept.editing.filter_through('sort')
2077  assert_equal(buffer:get_text(), '1|foo\n1|foo\n2|bar\n3|baz\n4|quux\n5|foobar\n')
2078  buffer:undo()
2079  textadept.editing.filter_through('sort | uniq|cut -d "|" -f2')
2080  assert_equal(buffer:get_text(), 'foo\nbar\nbaz\nquux\nfoobar\n')
2081  buffer:undo()
2082  buffer:set_sel(buffer:position_from_line(2) + 2, buffer.line_end_position[2])
2083  textadept.editing.filter_through('sed "s/o/O/g;"')
2084  assert_equal(buffer:get_text(), '3|baz\n1|fOO\n5|foobar\n1|foo\n4|quux\n2|bar\n')
2085  buffer:undo()
2086  buffer:set_sel(buffer:position_from_line(2), buffer:position_from_line(5))
2087  textadept.editing.filter_through('sort')
2088  assert_equal(buffer:get_text(), '3|baz\n1|foo\n1|foo\n5|foobar\n4|quux\n2|bar\n')
2089  buffer:undo()
2090  buffer:set_sel(buffer:position_from_line(2), buffer:position_from_line(5) + 1)
2091  textadept.editing.filter_through('sort')
2092  assert_equal(buffer:get_text(), '3|baz\n1|foo\n1|foo\n4|quux\n5|foobar\n2|bar\n')
2093  buffer:close(true)
2094
2095  assert_raises(function() textadept.editing.filter_through() end, 'string expected, got nil')
2096end
2097
2098function test_editing_autocomplete()
2099  assert_raises(function() textadept.editing.autocomplete() end, 'string expected, got nil')
2100end
2101
2102function test_editing_autocomplete_word()
2103  local all_words = textadept.editing.autocomplete_all_words
2104  textadept.editing.autocomplete_all_words = false
2105  buffer.new()
2106  buffer:add_text('foo f')
2107  textadept.editing.autocomplete('word')
2108  assert_equal(buffer:get_text(), 'foo foo')
2109  buffer:add_text('bar f')
2110  textadept.editing.autocomplete('word')
2111  assert(buffer:auto_c_active(), 'autocomplete list not shown')
2112  buffer:auto_c_select('foob')
2113  buffer:auto_c_complete()
2114  assert_equal(buffer:get_text(), 'foo foobar foobar')
2115  local ignore_case = buffer.auto_c_ignore_case
2116  buffer.auto_c_ignore_case = false
2117  buffer:add_text(' Bar b')
2118  textadept.editing.autocomplete('word')
2119  assert_equal(buffer:get_text(), 'foo foobar foobar Bar b')
2120  buffer.auto_c_ignore_case = true
2121  textadept.editing.autocomplete('word')
2122  assert_equal(buffer:get_text(), 'foo foobar foobar Bar Bar')
2123  buffer.auto_c_ignore_case = ignore_case
2124  buffer.new()
2125  buffer:add_text('foob')
2126  textadept.editing.autocomplete_all_words = true
2127  textadept.editing.autocomplete('word')
2128  textadept.editing.autocomplete_all_words = all_words
2129  assert_equal(buffer:get_text(), 'foobar')
2130  buffer:close(true)
2131  buffer:close(true)
2132end
2133
2134function test_editing_show_documentation()
2135  buffer.new()
2136  textadept.editing.api_files['text'] = {
2137    _HOME .. '/test/modules/textadept/editing/api',
2138    function() return _HOME .. '/test/modules/textadept/editing/api2' end
2139  }
2140  buffer:add_text('foo')
2141  textadept.editing.show_documentation()
2142  assert(view:call_tip_active(), 'documentation not found')
2143  view:call_tip_cancel()
2144  buffer:add_text('2')
2145  textadept.editing.show_documentation()
2146  assert(view:call_tip_active(), 'documentation not found')
2147  view:call_tip_cancel()
2148  buffer:add_text('bar')
2149  textadept.editing.show_documentation()
2150  assert(not view:call_tip_active(), 'documentation found')
2151  buffer:clear_all()
2152  buffer:add_text('FOO')
2153  textadept.editing.show_documentation(nil, true)
2154  assert(view:call_tip_active(), 'documentation not found')
2155  view:call_tip_cancel()
2156  buffer:add_text('(')
2157  textadept.editing.show_documentation(nil, true)
2158  assert(view:call_tip_active(), 'documentation not found')
2159  view:call_tip_cancel()
2160  buffer:add_text('bar')
2161  textadept.editing.show_documentation(nil, true)
2162  assert(view:call_tip_active(), 'documentation not found')
2163  events.emit(events.CALL_TIP_CLICK, 1)
2164  -- TODO: test calltip cycling.
2165  buffer:close(true)
2166  textadept.editing.api_files['text'] = nil
2167
2168  assert_raises(function() textadept.editing.show_documentation(true) end, 'number/nil expected, got boolean')
2169end
2170
2171function test_file_types_get_lexer()
2172  buffer.new()
2173  buffer:set_lexer('html')
2174  buffer:set_text(table.concat({
2175    '<html><head><style type="text/css">',
2176    'h1 {}',
2177    '</style></head></html>'
2178  }, '\n'))
2179  buffer:colorize(1, -1)
2180  buffer:goto_pos(buffer:position_from_line(2))
2181  assert_equal(buffer:get_lexer(), 'html')
2182  assert_equal(buffer:get_lexer(true), 'css')
2183  assert_equal(buffer:name_of_style(buffer.style_at[buffer.current_pos]), 'identifier')
2184  buffer:close(true)
2185end
2186
2187function test_file_types_set_lexer()
2188  local lexer_loaded
2189  local handler = function(name) lexer_loaded = name end
2190  events.connect(events.LEXER_LOADED, handler)
2191  buffer.new()
2192  buffer.filename = 'foo.lua'
2193  buffer:set_lexer()
2194  assert_equal(buffer:get_lexer(), 'lua')
2195  assert_equal(lexer_loaded, 'lua')
2196  buffer.filename = 'foo'
2197  buffer:set_text('#!/bin/sh')
2198  buffer:set_lexer()
2199  assert_equal(buffer:get_lexer(), 'bash')
2200  buffer:undo()
2201  buffer.filename = 'Makefile'
2202  buffer:set_lexer()
2203  assert_equal(buffer:get_lexer(), 'makefile')
2204  -- Verify lexer after certain events.
2205  buffer.filename = 'foo.c'
2206  events.emit(events.FILE_AFTER_SAVE, nil, true)
2207  assert_equal(buffer:get_lexer(), 'ansi_c')
2208  buffer.filename = 'foo.cpp'
2209  events.emit(events.FILE_OPENED)
2210  assert_equal(buffer:get_lexer(), 'cpp')
2211  view:goto_buffer(1)
2212  view:goto_buffer(-1)
2213  assert_equal(buffer:get_lexer(), 'cpp')
2214  events.disconnect(events.LEXER_LOADED, handler)
2215  buffer:close(true)
2216
2217  assert_raises(function() buffer:set_lexer(true) end, 'string/nil expected, got boolean')
2218end
2219
2220function test_file_types_select_lexer_interactive()
2221  buffer.new()
2222  local name = buffer:get_lexer()
2223  textadept.file_types.select_lexer()
2224  assert(buffer:get_lexer() ~= name, 'lexer unchanged')
2225  buffer:close()
2226end
2227
2228function test_file_types_load_lexers()
2229  local lexers = {}
2230  for name in buffer:private_lexer_call(_SCINTILLA.functions.property_names[1]):gmatch('[^\n]+') do
2231    lexers[#lexers + 1] = name
2232  end
2233  print('Loading lexers...')
2234  if #_VIEWS > 1 then view:unsplit() end
2235  view:goto_buffer(-1)
2236  ui.silent_print = true
2237  buffer.new()
2238  for _, name in ipairs(lexers) do
2239    print('Loading lexer ' .. name)
2240    buffer:set_lexer(name)
2241  end
2242  buffer:close()
2243  ui.silent_print = false
2244end
2245
2246function test_ui_find_find_text()
2247  local wrapped = false
2248  local handler = function() wrapped = true end
2249  buffer.new()
2250  buffer:set_text(table.concat({
2251    ' foo',
2252    'foofoo',
2253    'FOObar',
2254    'foo bar baz',
2255  }, '\n'))
2256  ui.find.find_entry_text = 'foo'
2257  ui.find.find_next()
2258  assert_equal(buffer.selection_start, 1 + 1)
2259  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2260  ui.find.whole_word = true
2261  ui.find.find_next()
2262  assert_equal(buffer.selection_start, buffer:position_from_line(4))
2263  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2264  events.connect(events.FIND_WRAPPED, handler)
2265  ui.find.find_next()
2266  assert(wrapped, 'search did not wrap')
2267  events.disconnect(events.FIND_WRAPPED, handler)
2268  assert_equal(buffer.selection_start, 1 + 1)
2269  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2270  ui.find.find_prev()
2271  assert_equal(buffer.selection_start, buffer:position_from_line(4))
2272  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2273  ui.find.match_case, ui.find.whole_word = true, false
2274  ui.find.find_entry_text = 'FOO'
2275  ui.find.find_next()
2276  assert_equal(buffer.selection_start, buffer:position_from_line(3))
2277  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2278  ui.find.find_next()
2279  assert_equal(buffer.selection_start, buffer:position_from_line(3))
2280  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2281  ui.find.regex = true
2282  ui.find.find_entry_text = 'f(.)\\1'
2283  ui.find.find_next()
2284  assert_equal(buffer.selection_start, buffer:position_from_line(4))
2285  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2286  ui.find.find_entry_text = 'quux'
2287  ui.find.find_next()
2288  assert_equal(buffer.selection_start, buffer.selection_end) -- no match
2289  assert_equal(events.emit(events.FIND, 'not found'), -1) -- simulate Find Next
2290  ui.find.match_case, ui.find.regex = false, false
2291  ui.find.find_entry_text = ''
2292  buffer:close(true)
2293end
2294
2295function test_ui_find_highlight_results()
2296  local function assert_indics(indics)
2297    local bit = 1 << ui.find.INDIC_FIND - 1
2298    for _, pos in ipairs(indics) do
2299      local mask = buffer:indicator_all_on_for(pos)
2300      assert(mask & bit > 0, 'no indicator on line %d', buffer:line_from_position(pos))
2301    end
2302  end
2303
2304  local highlight_all_matches = ui.find.highlight_all_matches
2305  ui.find.highlight_all_matches = true
2306  buffer.new()
2307  buffer:append_text(table.concat({
2308    'foo',
2309    'foobar',
2310    'bar foo',
2311    'baz foo bar',
2312    'fooquux',
2313    'foo'
2314  }, '\n'))
2315  -- Normal search.
2316  ui.find.find_entry_text = 'foo'
2317  ui.find.find_next()
2318  assert_indics{
2319    buffer:position_from_line(1),
2320    buffer:position_from_line(3) + 4,
2321    buffer:position_from_line(4) + 4,
2322    buffer:position_from_line(6)
2323  }
2324  -- Regex search.
2325  ui.find.find_entry_text = 'ba.'
2326  ui.find.regex = true
2327  ui.find.find_next()
2328  assert_indics{
2329    buffer:position_from_line(2) + 3,
2330    buffer:position_from_line(3),
2331    buffer:position_from_line(4),
2332    buffer:position_from_line(4) + 8,
2333  }
2334  ui.find.regex = false
2335  -- Do not highlight short searches (potential performance issue).
2336  ui.find.find_entry_text = 'f'
2337  ui.find.find_next()
2338  local pos = buffer:indicator_end(ui.find.INDIC_FIND, 2)
2339  assert_equal(pos, 1)
2340  -- Verify turning off match highlighting works.
2341  ui.find.highlight_all_matches = false
2342  ui.find.find_entry_text = 'foo'
2343  ui.find.find_next()
2344  pos = buffer:indicator_end(ui.find.INDIC_FIND, 2)
2345  assert_equal(pos, 1)
2346  ui.find.find_entry_text = ''
2347  ui.find.highlight_all_matches = highlight_all_matches -- reset
2348  buffer:close(true)
2349end
2350
2351function test_ui_find_incremental()
2352  buffer.new()
2353  buffer:set_text(table.concat({
2354    ' foo',
2355    'foobar',
2356    'FOObaz',
2357    'FOOquux'
2358  }, '\n'))
2359  assert_equal(buffer.current_pos, 1)
2360  ui.find.incremental = true
2361  ui.find.find_entry_text = 'f' -- simulate 'f' keypress
2362  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2363  assert_equal(buffer.selection_start, 1 + 1)
2364  assert_equal(buffer.selection_end, buffer.selection_start + 1)
2365  ui.find.find_entry_text = 'fo' -- simulate 'o' keypress
2366  ui.find.find_entry_text = 'foo' -- simulate 'o' keypress
2367  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2368  assert_equal(buffer.selection_start, 1 + 1)
2369  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2370  events.emit(events.FIND, ui.find.find_entry_text, true) -- simulate Find Next
2371  assert_equal(buffer.selection_start, buffer:position_from_line(2))
2372  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2373  ui.find.find_entry_text = 'fooq' -- simulate 'q' keypress
2374  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2375  assert_equal(buffer.selection_start, buffer:position_from_line(4))
2376  assert_equal(buffer.selection_end, buffer.selection_start + 4)
2377  ui.find.find_entry_text = 'foo' -- simulate backspace
2378  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2379  assert_equal(buffer.selection_start, buffer:position_from_line(2))
2380  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2381  events.emit(events.FIND, ui.find.find_entry_text, true) -- simulate Find Next
2382  assert_equal(buffer.selection_start, buffer:position_from_line(3))
2383  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2384  ui.find.match_case = true
2385  events.emit(events.FIND, ui.find.find_entry_text, true) -- simulate Find Next, wrap
2386  assert_equal(buffer.selection_start, 1 + 1)
2387  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2388  ui.find.match_case = false
2389  ui.find.whole_word = true
2390  ui.find.find_entry_text = 'foob'
2391  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2392  assert(buffer.selection_empty, 'no text should be found')
2393  ui.find.find_entry_text = 'foobar'
2394  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2395  assert_equal(buffer:get_sel_text(), 'foobar')
2396  ui.find.whole_word = false
2397  ui.find.find_entry_text = ''
2398  ui.find.incremental = false
2399  buffer:close(true)
2400end
2401
2402function test_ui_find_incremental_highlight()
2403  local highlight_all_matches = ui.find.highlight_all_matches
2404  ui.find.highlight_all_matches = true
2405  buffer.new()
2406  buffer:set_text(table.concat({
2407    ' foo',
2408    'foobar',
2409    'FOObaz',
2410    'FOOquux'
2411  }, '\n'))
2412  ui.find.incremental = true
2413  ui.find.find_entry_text = 'f' -- simulate 'f' keypress
2414  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2415  local pos = buffer:indicator_end(ui.find.INDIC_FIND, 2)
2416  assert_equal(pos, 1) -- too short
2417  ui.find.find_entry_text = 'fo' -- simulate 'o' keypress
2418  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2419  local indics = {
2420    buffer:position_from_line(1) + 1,
2421    buffer:position_from_line(2),
2422    buffer:position_from_line(3),
2423    buffer:position_from_line(4)
2424  }
2425  local bit = 1 << ui.find.INDIC_FIND - 1
2426  for _, pos in ipairs(indics) do
2427    local mask = buffer:indicator_all_on_for(pos)
2428    assert(mask & bit > 0, 'no indicator on line %d', buffer:line_from_position(pos))
2429  end
2430  ui.find.find_entry_text = ''
2431  ui.find.incremental = false
2432  ui.find.highlight_all_matches = highlight_all_matches
2433  buffer:close(true)
2434end
2435
2436function test_ui_find_incremental_not_found()
2437  buffer.new()
2438  buffer:set_text('foobar')
2439  ui.find.incremental = true
2440  ui.find.find_entry_text = 'b'
2441  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2442  assert_equal(buffer.current_pos, 4)
2443  ui.find.find_entry_text = 'bb'
2444  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2445  assert_equal(buffer.current_pos, 1)
2446  events.emit(events.FIND, ui.find.find_entry_text, true) -- simulate Find Next
2447  assert_equal(buffer.current_pos, 1) -- cursor did not advance
2448  ui.find.find_entry_text = ''
2449  ui.find.incremental = false
2450  buffer:close(true)
2451end
2452
2453function test_ui_find_find_in_files()
2454  ui.find.find_entry_text = 'foo'
2455  ui.find.match_case = true
2456  ui.find.find_in_files(_HOME .. '/test')
2457  assert_equal(buffer._type, _L['[Files Found Buffer]'])
2458  if #_VIEWS > 1 then view:unsplit() end
2459  local count = 0
2460  for filename, line, text in buffer:get_text():gmatch('\n([^:]+):(%d+):([^\n]+)') do
2461    assert(filename:find('^' .. _HOME .. '/test'), 'invalid filename "%s"', filename)
2462    assert(text:find('foo'), '"foo" not found in "%s"', text)
2463    count = count + 1
2464  end
2465  assert(count > 0, 'no files found')
2466  local s = buffer:indicator_end(ui.find.INDIC_FIND, 0)
2467  while true do
2468    local e = buffer:indicator_end(ui.find.INDIC_FIND, s)
2469    if e == s then break end -- no more results
2470    assert_equal(buffer:text_range(s, e), 'foo')
2471    s = buffer:indicator_end(ui.find.INDIC_FIND, e + 1)
2472  end
2473  ui.find.goto_file_found(true) -- wraps around
2474  assert_equal(#_VIEWS, 2)
2475  assert(buffer.filename, 'not in file found result')
2476  ui.goto_view(1)
2477  assert_equal(view.buffer._type, _L['[Files Found Buffer]'])
2478  local filename, line_num = view.buffer:get_sel_text():match('^([^:]+):(%d+)')
2479  ui.goto_view(-1)
2480  assert_equal(buffer.filename, filename)
2481  assert_equal(buffer:line_from_position(buffer.current_pos), tonumber(line_num))
2482  assert_equal(buffer:get_sel_text(), 'foo')
2483  ui.goto_view(1) -- files found buffer
2484  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
2485  assert_equal(buffer.filename, filename)
2486  ui.goto_view(1) -- files found buffer
2487  events.emit(events.DOUBLE_CLICK, nil, buffer:line_from_position(buffer.current_pos))
2488  assert_equal(buffer.filename, filename)
2489  buffer:close()
2490  ui.goto_view(1) -- files found buffer
2491  ui.find.goto_file_found(nil, false) -- wraps around
2492  assert(buffer.filename and buffer.filename ~= filename, 'opened the same file')
2493  buffer:close()
2494  ui.goto_view(1) -- files found buffer
2495  ui.find.find_entry_text = ''
2496  view:unsplit()
2497  buffer:close()
2498  -- TODO: ui.find.find_in_files() -- no param
2499
2500  assert_raises(function() ui.find.find_in_files('', 1) end, 'string/table/nil expected, got number')
2501end
2502
2503function test_ui_find_find_in_files_interactive()
2504  local cwd = lfs.currentdir()
2505  lfs.chdir(_HOME)
2506  local filter = ui.find.find_in_files_filters[_HOME]
2507  ui.find.find_in_files_filters[_HOME] = nil -- ensure not set
2508  ui.find.find_entry_text = 'foo'
2509  ui.find.in_files = true
2510  ui.find.replace_entry_text = '/test'
2511  events.emit(events.FIND, ui.find.find_entry_text, true)
2512  local results = buffer:get_text()
2513  assert(results:find('Directory: '), 'directory not shown')
2514  assert(results:find('Filter: /test\n'), 'no filter defined')
2515  assert(results:find('src/foo.c'), 'foo.c not found')
2516  assert(results:find('include/foo.h'), 'foo.h not found')
2517  assert_equal(table.concat(ui.find.find_in_files_filters[_HOME], ','), '/test')
2518  buffer:clear_all()
2519  ui.find.replace_entry_text = '/test,.c'
2520  events.emit(events.FIND, ui.find.find_entry_text, true)
2521  results = buffer:get_text()
2522  assert(results:find('Filter: /test,.c\n'), 'no filter defined')
2523  assert(results:find('src/foo.c'), 'foo.c not found')
2524  assert(not results:find('include/foo.h'), 'foo.h found')
2525  assert_equal(table.concat(ui.find.find_in_files_filters[_HOME], ','), '/test,.c')
2526  if not CURSES then
2527    -- Verify save and restore of replacement text and directory filters.
2528    ui.find.focus{in_files = false}
2529    assert_equal(ui.find.in_files, false)
2530    ui.find.replace_entry_text = 'bar'
2531    ui.find.focus{in_files = true}
2532    assert_equal(ui.find.in_files, true)
2533    assert_equal(ui.find.replace_entry_text, '/test,.c')
2534    ui.find.focus{in_files = false}
2535    assert_equal(ui.find.replace_entry_text, 'bar')
2536  end
2537  ui.find.find_entry_text = ''
2538  ui.find.in_files = false
2539  buffer:close()
2540  ui.goto_view(1)
2541  view:unsplit()
2542  ui.find.find_in_files_filters[_HOME] = filter
2543  lfs.chdir(cwd)
2544end
2545
2546function test_ui_find_in_files_single_char()
2547  ui.find.find_entry_text = 'z'
2548  ui.find.find_in_files(_HOME .. '/test')
2549  ui.find.goto_file_found(true)
2550  assert_equal(buffer:get_sel_text(), 'z')
2551  ui.find.find_entry_text = ''
2552  buffer:close()
2553  ui.goto_view(1)
2554  view:unsplit()
2555  buffer:close()
2556end
2557
2558function test_ui_find_replace()
2559  buffer.new()
2560  buffer:set_text('foofoo')
2561  ui.find.find_entry_text = 'foo'
2562  ui.find.find_next()
2563  ui.find.replace_entry_text = 'bar'
2564  ui.find.replace()
2565  assert_equal(buffer.selection_start, 4)
2566  assert_equal(buffer.selection_end, buffer.selection_start + 3)
2567  assert_equal(buffer:get_sel_text(), 'foo')
2568  assert_equal(buffer:get_text(), 'barfoo')
2569  ui.find.regex = true
2570  ui.find.find_entry_text = 'f(.)\\1'
2571  ui.find.find_next()
2572  ui.find.replace_entry_text = 'b\\1\\1\\u1234'
2573  ui.find.replace()
2574  assert_equal(buffer:get_text(), 'barbooሴ')
2575  ui.find.regex = false
2576  ui.find.find_entry_text = 'quux'
2577  ui.find.find_next()
2578  ui.find.replace_entry_text = ''
2579  ui.find.replace()
2580  assert_equal(buffer:get_text(), 'barbooሴ')
2581  ui.find.find_entry_text, ui.find.replace_entry_text = '', ''
2582  buffer:close(true)
2583end
2584
2585function test_ui_find_replace_text_save_restore()
2586  if CURSES then return end -- there are focus issues in curses
2587  ui.find.focus()
2588  ui.find.find_entry_text = 'foo'
2589  ui.find.replace_entry_text = 'bar'
2590  ui.find.find_next()
2591  ui.find.focus() -- simulate activating "Find"
2592  assert_equal(ui.find.replace_entry_text, 'bar')
2593  ui.find.focus{in_files = true} -- simulate activating "Find in Files"
2594  assert(ui.find.replace_entry_text ~= 'bar', 'filter entry text not set')
2595  ui.find.focus{in_files = false} -- simulate activating "Find"
2596  assert_equal(ui.find.replace_entry_text, 'bar')
2597  ui.find.replace_entry_text = 'baz'
2598  ui.find.replace_all()
2599  ui.find.focus() -- simulate activating "Find"
2600  assert_equal(ui.find.replace_entry_text, 'baz')
2601end
2602
2603function test_ui_find_replace_all()
2604  buffer.new()
2605  local text = table.concat({
2606    'foo',
2607    'foobar',
2608    'foobaz',
2609    'foofoo'
2610  }, '\n')
2611  buffer:set_text(text)
2612  ui.find.find_entry_text, ui.find.replace_entry_text = 'foo', 'bar'
2613  ui.find.replace_all()
2614  assert_equal(buffer:get_text(), 'bar\nbarbar\nbarbaz\nbarbar')
2615  buffer:undo()
2616  assert_equal(buffer:get_text(), text) -- verify atomic undo
2617  ui.find.regex = true
2618  buffer:set_sel(buffer:position_from_line(2), buffer:position_from_line(4) + 3)
2619  ui.find.find_entry_text, ui.find.replace_entry_text = 'f(.)\\1', 'b\\1\\1'
2620  ui.find.replace_all() -- replace in selection
2621  assert_equal(buffer:get_text(), 'foo\nboobar\nboobaz\nboofoo')
2622  ui.find.regex = false
2623  buffer:undo()
2624  ui.find.find_entry_text, ui.find.replace_entry_text = 'foo', ''
2625  ui.find.replace_all()
2626  assert_equal(buffer:get_text(), '\nbar\nbaz\n')
2627  ui.find.find_entry_text, ui.find.replace_entry_text = 'quux', ''
2628  ui.find.replace_all()
2629  assert_equal(buffer:get_text(), '\nbar\nbaz\n')
2630  ui.find.find_entry_text, ui.find.replace_entry_text = '', ''
2631  buffer:close(true)
2632end
2633
2634function test_find_replace_regex_transforms()
2635  buffer.new()
2636  buffer:set_text('foObaRbaz')
2637  ui.find.find_entry_text = 'f([oO]+)ba(..)'
2638  ui.find.regex = true
2639  local replacements = {
2640    ['f\\1ba\\2'] = 'foObaRbaz',
2641    ['f\\u\\1ba\\l\\2'] = 'fOObarbaz',
2642    ['f\\U\\1ba\\2'] = 'fOOBARBaz',
2643    ['f\\U\\1ba\\l\\2'] = 'fOOBArBaz',
2644    ['f\\U\\1\\Eba\\2'] = 'fOObaRbaz',
2645    ['f\\L\\1ba\\2'] = 'foobarbaz',
2646    ['f\\L\\1ba\\u\\2'] = 'foobaRbaz',
2647    ['f\\L\\1ba\\U\\2'] = 'foobaRBaz',
2648    ['f\\L\\1\\Eba\\2'] = 'foobaRbaz',
2649    ['f\\L\\u\\1ba\\2'] = 'fOobarbaz',
2650    ['f\\L\\u\\1ba\\U\\l\\2'] = 'fOobarBaz',
2651    ['f\\L\\u\\1\\Eba\\2'] = 'fOobaRbaz',
2652    ['f\\1ba\\U\\2'] = 'foObaRBaz',
2653    ['f\\1ba\\L\\2'] = 'foObarbaz',
2654    ['f\\1ba\\U\\l\\2'] = 'foObarBaz',
2655    [''] = 'az',
2656    ['\\0'] = 'foObaRbaz'
2657  }
2658  for regex, replacement in pairs(replacements) do
2659    ui.find.replace_entry_text = regex
2660    ui.find.find_next()
2661    ui.find.replace()
2662    assert_equal(buffer:get_text(), replacement)
2663    buffer:undo()
2664    ui.find.replace_all()
2665    assert_equal(buffer:get_text(), replacement)
2666    buffer:undo()
2667  end
2668  ui.find.find_entry_text, ui.find.replace_entry_text = '', ''
2669  ui.find.regex = false
2670  buffer:close(true)
2671end
2672
2673function test_ui_find_focus()
2674  buffer:new()
2675  buffer:append_text(' foo\n\n foo')
2676  ui.find.focus{incremental = true}
2677  ui.find.find_entry_text = 'foo'
2678  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2679  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
2680  buffer:line_down()
2681  ui.find.focus() -- should turn off incremental find
2682  ui.find.find_entry_text = 'f'
2683  if CURSES then events.emit(events.FIND_TEXT_CHANGED) end -- simulate
2684  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
2685  buffer:close(true)
2686
2687  assert_raises(function() ui.find.focus(1) end, 'table/nil expected, got number')
2688end
2689
2690function test_history()
2691  local filename1 = _HOME .. '/test/modules/textadept/history/1'
2692  io.open_file(filename1)
2693  textadept.history.clear() -- clear initial buffer switch record
2694  buffer:goto_line(5)
2695  textadept.history.back() -- should not do anything
2696  assert_equal(buffer.filename, filename1)
2697  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
2698  buffer:add_text('foo')
2699  buffer:goto_line(5 + textadept.history.minimum_line_distance + 1)
2700  textadept.history.back()
2701  assert_equal(buffer.filename, filename1)
2702  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
2703  assert_equal(buffer.current_pos, buffer.line_end_position[5])
2704  textadept.history.forward() -- should stay put (no edits have been made since)
2705  assert_equal(buffer.filename, filename1)
2706  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
2707  buffer:new_line()
2708  buffer:add_text('bar') -- close changes should update current history
2709  local filename2 = _HOME .. '/test/modules/textadept/history/2'
2710  io.open_file(filename2)
2711  buffer:goto_line(10)
2712  buffer:add_text('baz')
2713  textadept.history.back() -- should ignore initial file load and go back to file 1
2714  assert_equal(buffer.filename, filename1)
2715  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
2716  textadept.history.back() -- should stay put (updated history from line 5 to line 6)
2717  assert_equal(buffer.filename, filename1)
2718  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
2719  textadept.history.forward()
2720  assert_equal(buffer.filename, filename2)
2721  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2722  textadept.history.back()
2723  buffer:goto_line(15)
2724  buffer:clear() -- erases forward history to file 2
2725  textadept.history.forward() -- should not do anything
2726  assert_equal(buffer.filename, filename1)
2727  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2728  textadept.history.back()
2729  assert_equal(buffer.filename, filename1)
2730  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
2731  textadept.history.forward()
2732  view:goto_buffer(1)
2733  assert_equal(buffer.filename, filename2)
2734  buffer:goto_line(20)
2735  buffer:add_text('quux')
2736  view:goto_buffer(-1)
2737  assert_equal(buffer.filename, filename1)
2738  buffer:undo() -- undo delete of '\n'
2739  buffer:undo() -- undo add of 'foo'
2740  buffer:redo() -- re-add 'foo'
2741  textadept.history.back() -- undo and redo should not affect history
2742  assert_equal(buffer.filename, filename2)
2743  assert_equal(buffer:line_from_position(buffer.current_pos), 20)
2744  textadept.history.back()
2745  assert_equal(buffer.filename, filename1)
2746  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2747  textadept.history.back()
2748  assert_equal(buffer.filename, filename1)
2749  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
2750  buffer:target_whole_document()
2751  buffer:replace_target(string.rep('\n', buffer.line_count)) -- whole buffer replacements should not affect history (e.g. clang-format)
2752  textadept.history.forward()
2753  assert_equal(buffer.filename, filename1)
2754  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2755  view:goto_buffer(1)
2756  assert_equal(buffer.filename, filename2)
2757  buffer:close(true)
2758  textadept.history.back() -- should re-open file 2
2759  assert_equal(buffer.filename, filename2)
2760  assert_equal(buffer:line_from_position(buffer.current_pos), 20)
2761  buffer:close(true)
2762  buffer:close(true)
2763
2764  assert_raises(function() textadept.history.record(1) end, 'string/nil expected, got number')
2765  assert_raises(function() textadept.history.record('', true) end, 'number/nil expected, got boolean')
2766  assert_raises(function() textadept.history.record('', 1, '') end, 'number/nil expected, got string')
2767end
2768
2769function test_history_soft_records()
2770  local filename1 = _HOME .. '/test/modules/textadept/history/1'
2771  io.open_file(filename1)
2772  textadept.history.clear() -- clear initial buffer switch record
2773  buffer:goto_line(5)
2774  local filename2 = _HOME .. '/test/modules/textadept/history/2'
2775  io.open_file(filename2)
2776  buffer:goto_line(10)
2777  textadept.history.back()
2778  assert_equal(buffer.filename, filename1)
2779  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
2780  buffer:goto_line(15)
2781  textadept.history.forward() -- should update soft record from line 5 to 15
2782  assert_equal(buffer.filename, filename2)
2783  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2784  buffer:goto_line(20)
2785  textadept.history.back() -- should update soft record from line 10 to 20
2786  assert_equal(buffer.filename, filename1)
2787  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2788  textadept.history.forward()
2789  assert_equal(buffer.filename, filename2)
2790  assert_equal(buffer:line_from_position(buffer.current_pos), 20)
2791  buffer:goto_line(10)
2792  buffer:add_text('foo') -- should update soft record from line 20 to 10 and make it hard
2793  textadept.history.back()
2794  assert_equal(buffer.filename, filename1)
2795  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2796  textadept.history.forward()
2797  assert_equal(buffer.filename, filename2)
2798  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2799  buffer:goto_line(20)
2800  buffer:add_text('bar') -- should create a new record
2801  textadept.history.back()
2802  assert_equal(buffer.filename, filename2)
2803  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2804  buffer:close(true)
2805  buffer:close(true)
2806end
2807
2808function test_history_per_view()
2809  local filename1 = _HOME .. '/test/modules/textadept/history/1'
2810  io.open_file(filename1)
2811  textadept.history.clear() -- clear initial buffer switch record
2812  buffer:goto_line(5)
2813  buffer:add_text('foo')
2814  buffer:goto_line(10)
2815  buffer:add_text('bar')
2816  view:split()
2817  textadept.history.back() -- no history for this view
2818  assert_equal(buffer.filename, filename1)
2819  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2820  local filename2 = _HOME .. '/test/modules/textadept/history/2'
2821  io.open_file(filename2)
2822  buffer:goto_line(15)
2823  buffer:add_text('baz')
2824  buffer:goto_line(20)
2825  textadept.history.back()
2826  assert_equal(buffer.filename, filename2)
2827  assert_equal(buffer:line_from_position(buffer.current_pos), 15)
2828  textadept.history.back()
2829  assert_equal(buffer.filename, filename1)
2830  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2831  textadept.history.back() -- no more history for this view
2832  assert_equal(buffer.filename, filename1)
2833  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2834  ui.goto_view(-1)
2835  textadept.history.back()
2836  assert_equal(buffer.filename, filename1)
2837  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
2838  textadept.history.forward()
2839  assert_equal(buffer.filename, filename1)
2840  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2841  textadept.history.forward() -- no more history for this view
2842  assert_equal(buffer.filename, filename1)
2843  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
2844  view:unsplit()
2845  view:goto_buffer(1)
2846  buffer:close(true)
2847  buffer:close(true)
2848end
2849
2850function test_history_print_buffer()
2851  local tabs = ui.tabs
2852  ui.tabs = true
2853  ui.print('foo')
2854  textadept.history.back()
2855  assert(buffer._type ~= _L['[Message Buffer]'])
2856  textadept.history.forward()
2857  assert_equal(buffer._type, _L['[Message Buffer]'])
2858  buffer:close()
2859  ui.tabs = tabs -- restore
2860end
2861
2862function test_macro_record_play_save_load()
2863  textadept.macros.save() -- should not do anything
2864  textadept.macros.play() -- should not do anything
2865  assert_equal(#_BUFFERS, 1)
2866  assert(not buffer.modify, 'a macro was played')
2867
2868  textadept.macros.record()
2869  events.emit(events.MENU_CLICKED, 1) -- File > New
2870  buffer:add_text('f')
2871  events.emit(events.CHAR_ADDED, string.byte('f'))
2872  events.emit(events.FIND, 'f', true)
2873  events.emit(events.REPLACE, 'b')
2874  buffer:replace_sel('a') -- typing would do this
2875  events.emit(events.CHAR_ADDED, string.byte('a'))
2876  buffer:add_text('r')
2877  events.emit(events.CHAR_ADDED, string.byte('r'))
2878  events.emit(events.KEYPRESS, string.byte('t'), false, true) -- transpose
2879  textadept.macros.play() -- should not do anything
2880  textadept.macros.save() -- should not do anything
2881  textadept.macros.load() -- should not do anything
2882  textadept.macros.record() -- stop
2883  assert_equal(#_BUFFERS, 2)
2884  assert_equal(buffer:get_text(), 'ra')
2885  buffer:close(true)
2886  textadept.macros.play()
2887  assert_equal(#_BUFFERS, 2)
2888  assert_equal(buffer:get_text(), 'ra')
2889  buffer:close(true)
2890  local filename = os.tmpname()
2891  textadept.macros.save(filename)
2892  textadept.macros.record()
2893  textadept.macros.record()
2894  textadept.macros.load(filename)
2895  textadept.macros.play()
2896  assert_equal(#_BUFFERS, 2)
2897  assert_equal(buffer:get_text(), 'ra')
2898  buffer:close(true)
2899  os.remove(filename)
2900
2901  assert_raises(function() textadept.macros.save(1) end, 'string/nil expected, got number')
2902  assert_raises(function() textadept.macros.load(1) end, 'string/nil expected, got number')
2903end
2904
2905function test_macro_record_play_with_keys_only()
2906  if keys.f9 ~= textadept.macros.record then
2907    print('Note: not running since F9 does not toggle macro recording')
2908    return
2909  end
2910  buffer.new()
2911  buffer:append_text('foo\nbar\nbaz\n')
2912  events.emit(events.KEYPRESS, 0xFFC6) -- f9; start recording
2913  events.emit(events.KEYPRESS, not CURSES and 0xFF57 or 305) -- end
2914  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 13) -- \n
2915  buffer:new_line()
2916  events.emit(events.KEYPRESS, not CURSES and 0xFF54 or 300) -- down
2917  events.emit(events.KEYPRESS, 0xFFC6) -- f9; stop recording
2918  assert_equal(buffer:get_text(), 'foo\n\nbar\nbaz\n')
2919  assert_equal(buffer.current_pos, buffer:position_from_line(3))
2920  if not CURSES then
2921    events.emit(events.KEYPRESS, 0xFFC6, true) -- sf9; play
2922  else
2923    events.emit(events.KEYPRESS, 0xFFC7) -- f10; play
2924  end
2925  assert_equal(buffer:get_text(), 'foo\n\nbar\n\nbaz\n')
2926  assert_equal(buffer.current_pos, buffer:position_from_line(5))
2927  if not CURSES then
2928    events.emit(events.KEYPRESS, 0xFFC6, true) -- sf9; play
2929  else
2930    events.emit(events.KEYPRESS, 0xFFC7) -- f10; play
2931  end
2932  assert_equal(buffer:get_text(), 'foo\n\nbar\n\nbaz\n\n')
2933  assert_equal(buffer.current_pos, buffer:position_from_line(7))
2934  buffer:close(true)
2935end
2936
2937function test_menu_menu_functions()
2938  buffer.new()
2939  textadept.menu.menubar[_L['Buffer']][_L['Indentation']][_L['Tab width: 8']][2]()
2940  assert_equal(buffer.tab_width, 8)
2941  textadept.menu.menubar[_L['Buffer']][_L['EOL Mode']][_L['CRLF']][2]()
2942  assert_equal(buffer.eol_mode, buffer.EOL_CRLF)
2943  textadept.menu.menubar[_L['Buffer']][_L['Encoding']][_L['CP-1252 Encoding']][2]()
2944  assert_equal(buffer.encoding, 'CP1252')
2945  buffer:set_text('foo')
2946  textadept.menu.menubar[_L['Edit']][_L['Delete Word']][2]()
2947  assert_equal(buffer:get_text(), '')
2948  buffer:set_text('(foo)')
2949  textadept.menu.menubar[_L['Edit']][_L['Match Brace']][2]()
2950  assert_equal(buffer.char_at[buffer.current_pos], string.byte(')'))
2951  buffer:set_text('foo f')
2952  buffer:line_end()
2953  textadept.menu.menubar[_L['Edit']][_L['Complete Word']][2]()
2954  assert_equal(buffer:get_text(), 'foo foo')
2955  buffer:set_text('2\n1\n3\n')
2956  textadept.menu.menubar[_L['Edit']][_L['Filter Through']][2]()
2957  ui.command_entry:set_text('sort')
2958  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
2959  assert_equal(buffer:get_text(), '1\n2\n3\n')
2960  buffer:set_text('foo')
2961  buffer:line_end()
2962  textadept.menu.menubar[_L['Edit']][_L['Selection']][_L['Enclose as XML Tags']][2]()
2963  assert_equal(buffer:get_text(), '<foo></foo>')
2964  assert_equal(buffer.current_pos, 6)
2965  buffer:undo()
2966  assert_equal(buffer:get_text(), 'foo') -- verify atomic undo
2967  textadept.menu.menubar[_L['Edit']][_L['Selection']][_L['Enclose as Single XML Tag']][2]()
2968  assert_equal(buffer:get_text(), '<foo />')
2969  assert_equal(buffer.current_pos, buffer.line_end_position[1])
2970  if not CURSES then -- there are focus issues in curses
2971    textadept.menu.menubar[_L['Search']][_L['Find in Files']][2]()
2972    assert(ui.find.in_files, 'not finding in files')
2973    textadept.menu.menubar[_L['Search']][_L['Find']][2]()
2974    assert(not ui.find.in_files, 'finding in files')
2975  end
2976  buffer:clear_all()
2977  buffer:set_lexer('lua')
2978  buffer:add_text('string.')
2979  textadept.menu.menubar[_L['Tools']][_L['Complete Symbol']][2]()
2980  assert(buffer:auto_c_active(), 'no autocompletions')
2981  assert_equal(buffer.auto_c_current_text, 'byte')
2982  buffer:auto_c_cancel()
2983  buffer:char_left()
2984  textadept.menu.menubar[_L['Tools']][_L['Show Style']][2]()
2985  assert(view:call_tip_active(), 'style not shown')
2986  view:call_tip_cancel()
2987  local use_tabs = buffer.use_tabs
2988  textadept.menu.menubar[_L['Buffer']][_L['Indentation']][_L['Toggle Use Tabs']][2]()
2989  assert(buffer.use_tabs ~= use_tabs, 'use tabs not toggled')
2990  local wrap_mode = view.wrap_mode
2991  textadept.menu.menubar[_L['Buffer']][_L['Toggle Wrap Mode']][2]()
2992  assert(view.wrap_mode ~= wrap_mode, 'wrap mode not toggled')
2993  local view_whitespace = view.view_ws
2994  textadept.menu.menubar[_L['Buffer']][_L['Toggle View Whitespace']][2]()
2995  assert(view.view_ws ~= view_whitespace, 'view whitespace not toggled')
2996  view:split()
2997  ui.update()
2998  local size = view.size
2999  textadept.menu.menubar[_L['View']][_L['Grow View']][2]()
3000  assert(view.size > size, 'view shrunk')
3001  textadept.menu.menubar[_L['View']][_L['Shrink View']][2]()
3002  assert_equal(view.size, size)
3003  view:unsplit()
3004  buffer:set_text('if foo then\n  bar\nend')
3005  buffer:colorize(1, -1)
3006  textadept.menu.menubar[_L['View']][_L['Toggle Current Fold']][2]()
3007  assert_equal(view.fold_expanded[buffer:line_from_position(buffer.current_pos)], false)
3008  local indentation_guides = view.indentation_guides
3009  textadept.menu.menubar[_L['View']][_L['Toggle Show Indent Guides']][2]()
3010  assert(view.indentation_guides ~= indentation_guides, 'indentation guides not toggled')
3011  local virtual_space = buffer.virtual_space_options
3012  textadept.menu.menubar[_L['View']][_L['Toggle Virtual Space']][2]()
3013  assert(buffer.virtual_space_options ~= virtual_space, 'virtual space not toggled')
3014  buffer:close(true)
3015end
3016
3017function test_menu_functions_interactive()
3018  buffer.new()
3019  textadept.menu.menubar[_L['Help']][_L['About']][2]()
3020  buffer:close(true)
3021end
3022
3023function test_menu_select_command_interactive()
3024  local num_buffers = #_BUFFERS
3025  textadept.menu.select_command()
3026  assert(#_BUFFERS > num_buffers, 'new buffer not created')
3027  buffer:close()
3028end
3029
3030function test_run_compile_run()
3031  textadept.run.compile() -- should not do anything
3032  textadept.run.run() -- should not do anything
3033  assert_equal(#_BUFFERS, 1)
3034  assert(not buffer.modify, 'a command was run')
3035
3036  local compile_file = _HOME .. '/test/modules/textadept/run/compile.lua'
3037  textadept.run.compile(compile_file)
3038  assert_equal(#_BUFFERS, 2)
3039  assert_equal(buffer._type, _L['[Message Buffer]'])
3040  ui.update() -- process output
3041  assert(buffer:get_text():find("'end' expected"), 'no compile error')
3042  assert(buffer:get_text():find('> exit status: 256'), 'no compile error')
3043  if #_VIEWS > 1 then view:unsplit() end
3044  textadept.run.goto_error(true) -- wraps
3045  assert_equal(#_VIEWS, 2)
3046  assert_equal(buffer.filename, compile_file)
3047  assert_equal(buffer:line_from_position(buffer.current_pos), 3)
3048  assert(buffer.annotation_text[3]:find("'end' expected"), 'annotation not visible')
3049  ui.goto_view(1) -- message buffer
3050  assert_equal(buffer._type, _L['[Message Buffer]'])
3051  assert(buffer:get_sel_text():find("'end' expected"), 'compile error not selected')
3052  assert(buffer:marker_get(buffer:line_from_position(buffer.current_pos)) & 1 << textadept.run.MARK_ERROR - 1 > 0)
3053  events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
3054  assert_equal(buffer.filename, compile_file)
3055  ui.goto_view(1) -- message buffer
3056  events.emit(events.DOUBLE_CLICK, nil, buffer:line_from_position(buffer.current_pos))
3057  assert_equal(buffer.filename, compile_file)
3058  local compile_command = textadept.run.compile_commands.lua
3059  textadept.run.compile() -- clears annotation
3060  ui.update() -- process output
3061  view:goto_buffer(1)
3062  assert(not buffer.annotation_text[3]:find("'end' expected"), 'annotation visible')
3063  buffer:close() -- compile_file
3064
3065  local run_file = _HOME .. '/test/modules/textadept/run/run.lua'
3066  textadept.run.run_commands[run_file] = function()
3067    return textadept.run.run_commands.lua, run_file:match('^(.+[/\\])') -- intentional trailing '/'
3068  end
3069  io.open_file(run_file)
3070  textadept.run.run()
3071  assert_equal(buffer._type, _L['[Message Buffer]'])
3072  ui.update() -- process output
3073  assert(buffer:get_text():find('attempt to call a nil value'), 'no run error')
3074  textadept.run.goto_error(false)
3075  assert_equal(buffer.filename, run_file)
3076  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
3077  textadept.run.goto_error(nil, false)
3078  assert_equal(buffer.filename, run_file)
3079  assert_equal(buffer:line_from_position(buffer.current_pos), 1)
3080  ui.goto_view(1)
3081  assert(buffer:marker_get(buffer:line_from_position(buffer.current_pos)) & 1 << textadept.run.MARK_WARNING - 1 > 0)
3082  ui.goto_view(-1)
3083  textadept.run.goto_error(false)
3084  assert_equal(buffer.filename, compile_file)
3085  if #_VIEWS > 1 then view:unsplit() end
3086  buffer:close() -- compile_file
3087  buffer:close() -- run_file
3088  buffer:close() -- message buffer
3089
3090  assert_raises(function() textadept.run.compile({}) end, 'string/nil expected, got table')
3091  assert_raises(function() textadept.run.run({}) end, 'string/nil expected, got table')
3092end
3093
3094function test_run_set_arguments()
3095  local lua_run_command = textadept.run.run_commands.lua
3096  local lua_compile_command = textadept.run.compile_commands.lua
3097
3098  buffer.new()
3099  buffer.filename = '/tmp/test.lua'
3100  textadept.run.set_arguments(nil, '-i', '-p')
3101  assert_equal(textadept.run.run_commands[buffer.filename], lua_run_command .. ' -i')
3102  assert_equal(textadept.run.compile_commands[buffer.filename], lua_compile_command .. ' -p')
3103  textadept.run.set_arguments(buffer.filename, '', '')
3104  assert_equal(textadept.run.run_commands[buffer.filename], lua_run_command .. ' ')
3105  assert_equal(textadept.run.compile_commands[buffer.filename], lua_compile_command .. ' ')
3106  buffer:close(true)
3107
3108  assert_raises(function() textadept.run.set_arguments(1) end, 'string/nil expected, got number')
3109  assert_raises(function() textadept.run.set_arguments('', true) end, 'string/nil expected, got boolean')
3110  assert_raises(function() textadept.run.set_arguments('', '', {}) end, 'string/nil expected, got table')
3111end
3112
3113function test_run_set_arguments_interactive()
3114  local lua_run_command = textadept.run.run_commands.lua
3115  local lua_compile_command = textadept.run.compile_commands.lua
3116  buffer.new()
3117  buffer.filename = '/tmp/test.lua'
3118  textadept.run.set_arguments(nil, '-i', '-p')
3119  textadept.run.set_arguments()
3120  assert_equal(textadept.run.run_commands[buffer.filename], lua_run_command .. ' -i')
3121  assert_equal(textadept.run.compile_commands[buffer.filename], lua_compile_command .. ' -p')
3122  buffer:close(true)
3123end
3124
3125function test_run_build()
3126  textadept.run.build_commands[_HOME] = function()
3127    return 'lua modules/textadept/run/build.lua', _HOME .. '/test/' -- intentional trailing '/'
3128  end
3129  textadept.run.stop() -- should not do anything
3130  textadept.run.build(_HOME)
3131  if #_VIEWS > 1 then view:unsplit() end
3132  assert_equal(buffer._type, _L['[Message Buffer]'])
3133  os.execute('sleep 0.1') -- ensure process is running
3134  buffer:add_text('foo')
3135  buffer:new_line() -- should send previous line as stdin
3136  os.execute('sleep 0.1') -- ensure process processed stdin
3137  textadept.run.stop()
3138  ui.update() -- process output
3139  assert(buffer:get_text():find('> cd '), 'did not change directory')
3140  assert(buffer:get_text():find('build%.lua'), 'did not run build command')
3141  assert(buffer:get_text():find('read "foo"'), 'did not send stdin')
3142  assert(buffer:get_text():find('> exit status: 9'), 'build not stopped')
3143  textadept.run.stop() -- should not do anything
3144  buffer:close()
3145  -- TODO: chdir(_HOME) and textadept.run.build() -- no param.
3146  -- TODO: project whose makefile is autodetected.
3147end
3148
3149function test_run_test()
3150  textadept.run.test_commands[_HOME] = function()
3151    return 'lua modules/textadept/run/test.lua', _HOME .. '/test/' -- intentional trailing '/'
3152  end
3153  textadept.run.test(_HOME)
3154  if #_VIEWS > 1 then view:unsplit() end
3155  ui.update() -- process output
3156  assert(buffer:get_text():find('test%.lua'), 'did not run test command')
3157  assert(buffer:get_text():find('assertion failed!'), 'assertion failure not detected')
3158  buffer:close()
3159end
3160
3161function test_run_goto_internal_lua_error()
3162  xpcall(error, function(message) events.emit(events.ERROR, debug.traceback(message)) end, 'internal error', 2)
3163  if #_VIEWS > 1 then view:unsplit() end
3164  textadept.run.goto_error(1)
3165  assert(buffer.filename:find('/test/test%.lua$'), 'did not detect internal Lua error')
3166  view:unsplit()
3167  buffer:close()
3168  buffer:close()
3169end
3170
3171function test_run_commands_function()
3172  local filename = os.tmpname()
3173  io.open_file(filename)
3174  textadept.run.run_commands.text = function()
3175    return [[lua -e 'print(os.getenv("FOO"))']], '/tmp', {FOO = 'bar'}
3176  end
3177  textadept.run.run()
3178  assert_equal(#_BUFFERS, 3) -- including [Test Output]
3179  assert_equal(buffer._type, _L['[Message Buffer]'])
3180  ui.update() -- process output
3181  assert(buffer:get_text():find('> cd /tmp'), 'cwd not set properly')
3182  assert(buffer:get_text():find('bar'), 'env not set properly')
3183  if #_VIEWS > 1 then view:unsplit() end
3184  buffer:close()
3185  buffer:close()
3186  textadept.run.run_commands.text = nil -- reset
3187  os.remove(filename)
3188end
3189
3190-- TODO: test textadept.run.run_in_background
3191
3192function test_session_save()
3193  local handler = function(session)
3194    session.baz = true
3195    session.quux = assert
3196    session.foobar = buffer.doc_pointer
3197    session.foobaz = coroutine.create(function() end)
3198  end
3199  events.connect(events.SESSION_SAVE, handler)
3200  buffer.new()
3201  buffer.filename = 'foo.lua'
3202  textadept.bookmarks.toggle()
3203  view:split()
3204  buffer.new()
3205  buffer.filename = 'bar.lua'
3206  local session_file = os.tmpname()
3207  textadept.session.save(session_file)
3208  local session = assert(loadfile(session_file, 't', {}))()
3209  assert_equal(session.buffers[#session.buffers - 1].filename, 'foo.lua')
3210  assert_equal(session.buffers[#session.buffers - 1].bookmarks, {1})
3211  assert_equal(session.buffers[#session.buffers].filename, 'bar.lua')
3212  assert_equal(session.ui.maximized, false)
3213  assert_equal(type(session.views[1]), 'table')
3214  assert_equal(session.views[1][1], #_BUFFERS - 1)
3215  assert_equal(session.views[1][2], #_BUFFERS)
3216  assert(not session.views[1].vertical, 'split vertical')
3217  assert(session.views[1].size > 1, 'split size not set properly')
3218  assert_equal(session.views.current, #_VIEWS)
3219  assert_equal(session.baz, true)
3220  assert(not session.quux, 'function serialized')
3221  assert(not session.foobar, 'userdata serialized')
3222  assert(not session.foobaz, 'thread serialized')
3223  view:unsplit()
3224  buffer:close()
3225  buffer:close()
3226  os.remove(session_file)
3227  events.disconnect(events.SESSION_SAVE, handler)
3228end
3229
3230function test_session_save_before_load()
3231  local test_output_text = buffer:get_text()
3232  local foo = os.tmpname()
3233  local bar = os.tmpname()
3234  local baz = os.tmpname()
3235  buffer.new()
3236  buffer.filename = foo
3237  local session1 = os.tmpname()
3238  textadept.session.save(session1)
3239  buffer:close()
3240  buffer.new()
3241  buffer.filename = bar
3242  local session2 = os.tmpname()
3243  textadept.session.save(session2)
3244  buffer.new()
3245  buffer.filename = baz
3246  textadept.session.load(session1) -- should save baz to session
3247  assert_equal(#_BUFFERS, 1 + 1) -- test output buffer is open
3248  assert_equal(buffer.filename, foo)
3249  for i = 1, 2 do
3250    textadept.session.load(session2) -- when i == 2, reload; should not re-save
3251    assert_equal(#_BUFFERS, 2 + 1) -- test output buffer is open
3252    assert_equal(_BUFFERS[#_BUFFERS - 1].filename, bar)
3253    assert_equal(_BUFFERS[#_BUFFERS].filename, baz)
3254    buffer:close()
3255    buffer:close()
3256  end
3257  os.remove(foo)
3258  os.remove(bar)
3259  os.remove(baz)
3260  os.remove(session1)
3261  os.remove(session2)
3262  buffer:add_text(test_output_text)
3263end
3264
3265function test_snippets_find_snippet()
3266  snippets.foo = 'bar'
3267  textadept.snippets.paths[1] = _HOME .. '/test/modules/textadept/snippets'
3268
3269  buffer.new()
3270  buffer:add_text('foo')
3271  assert(textadept.snippets.insert() == nil, 'snippet not inserted')
3272  assert_equal(buffer:get_text(), 'bar') -- from snippets
3273  textadept.snippets.insert()
3274  assert_equal(buffer:get_text(), 'baz\n') -- from bar file
3275  buffer:delete_back()
3276  textadept.snippets.insert()
3277  assert_equal(buffer:get_text(), 'quux\n') -- from baz.txt file
3278  buffer:delete_back()
3279  assert(not textadept.snippets.insert(), 'snippet inserted')
3280  assert_equal(buffer:get_text(), 'quux')
3281  buffer:clear_all()
3282  buffer:set_lexer('lua') -- prefer lexer-specific snippets
3283  snippets.lua = {foo = 'baz'} -- overwrite language module
3284  buffer:add_text('foo')
3285  textadept.snippets.insert()
3286  assert_equal(buffer:get_text(), 'baz') -- from snippets.lua
3287  textadept.snippets.insert()
3288  assert_equal(buffer:get_text(), 'bar\n') -- from lua.baz.lua file
3289  buffer:delete_back()
3290  textadept.snippets.insert()
3291  assert_equal(buffer:get_text(), 'quux\n') -- from lua.bar file
3292  buffer:close(true)
3293
3294  snippets.foo = nil
3295  table.remove(textadept.snippets.paths, 1)
3296end
3297
3298function test_snippets_match_indentation()
3299  local snippet = '\t    foo'
3300  local multiline_snippet = table.concat({
3301    'foo',
3302    '\tbar',
3303    '\t    baz',
3304    'quux'
3305  }, '\n')
3306  buffer.new()
3307
3308  buffer.use_tabs, buffer.tab_width, buffer.eol_mode = true, 4, buffer.EOL_CRLF
3309  textadept.snippets.insert(snippet)
3310  assert_equal(buffer:get_text(), '\t\tfoo')
3311  buffer:clear_all()
3312  buffer:add_text('\t')
3313  textadept.snippets.insert(snippet)
3314  assert_equal(buffer:get_text(), '\t\t\tfoo')
3315  buffer:clear_all()
3316  buffer:add_text('\t')
3317  textadept.snippets.insert(multiline_snippet)
3318  assert_equal(buffer:get_text(), table.concat({
3319    '\tfoo',
3320    '\t\tbar',
3321    '\t\t\tbaz',
3322    '\tquux'
3323  }, '\r\n'))
3324  buffer:clear_all()
3325
3326  buffer.use_tabs, buffer.tab_width, buffer.eol_mode = false, 2, buffer.EOL_LF
3327  textadept.snippets.insert(snippet)
3328  assert_equal(buffer:get_text(), '      foo')
3329  buffer:clear_all()
3330  buffer:add_text('  ')
3331  textadept.snippets.insert(snippet)
3332  assert_equal(buffer:get_text(), '        foo')
3333  buffer:clear_all()
3334  buffer:add_text('  ')
3335  textadept.snippets.insert(multiline_snippet)
3336  assert_equal(buffer:get_text(), table.concat({
3337    '  foo',
3338    '    bar',
3339    '        baz',
3340    '  quux'
3341  }, '\n'))
3342  buffer:close(true)
3343
3344  assert_raises(function() textadept.snippets.insert(true) end, 'string/nil expected, got boolean')
3345end
3346
3347function test_snippets_placeholders()
3348  buffer.new()
3349  local lua_date = os.date()
3350  local p = io.popen('date')
3351  local shell_date = p:read()
3352  p:close()
3353  textadept.snippets.insert(table.concat({
3354    '%0placeholder: %1(foo) %2(bar)',
3355    'choice: %3{baz,quux}',
3356    'mirror: %2%3',
3357    'Lua: %<os.date()> %1<text:upper()>',
3358    'Shell: %[date] %1[echo %]',
3359    'escape: %%1 %4%( %4%{',
3360  }, '\n'))
3361  assert_equal(buffer.selections, 1)
3362  assert_equal(buffer.selection_start, 1 + 14)
3363  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3364  assert_equal(buffer:get_sel_text(), 'foo')
3365  buffer:replace_sel('baz')
3366  events.emit(events.UPDATE_UI, buffer.UPDATE_CONTENT + buffer.UPDATE_SELECTION) -- simulate typing
3367  assert_equal(buffer:get_text(), string.format(table.concat({
3368    ' placeholder: baz bar', -- placeholders to visit have 1 empty space
3369    'choice:  ', -- placeholder choices are initially empty
3370    'mirror:   ', -- placeholder mirrors are initially empty
3371    'Lua: %s BAZ', -- verify real-time transforms
3372    'Shell: %s baz', -- verify real-time transforms
3373    'escape: %%1  (  { ' -- trailing space for snippet sentinel
3374  }, '\n'), lua_date, shell_date))
3375  textadept.snippets.insert()
3376  assert_equal(buffer.selections, 2)
3377  assert_equal(buffer.selection_start, 1 + 18)
3378  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3379  for i = 1, buffer.selections do
3380    assert_equal(buffer.selection_n_end[i], buffer.selection_n_start[i] + 3)
3381    assert_equal(buffer:text_range(buffer.selection_n_start[i], buffer.selection_n_end[i]), 'bar')
3382  end
3383  assert(buffer:get_text():find('mirror: bar'), 'mirror not updated')
3384  textadept.snippets.insert()
3385  assert_equal(buffer.selections, 2)
3386  assert(buffer:auto_c_active(), 'no choice')
3387  buffer:auto_c_select('quux')
3388  buffer:auto_c_complete()
3389  assert(buffer:get_text():find('\nmirror: barquux\n'), 'choice mirror not updated')
3390  textadept.snippets.insert()
3391  assert_equal(buffer.selection_start, buffer.selection_end) -- no default placeholder (escaped)
3392  textadept.snippets.insert()
3393  assert_equal(buffer:get_text(), string.format(table.concat({
3394    'placeholder: baz bar',
3395    'choice: quux',
3396    'mirror: barquux',
3397    'Lua: %s BAZ',
3398    'Shell: %s baz',
3399    'escape: %%1 ( {'
3400  }, '\n'), lua_date, shell_date))
3401  assert_equal(buffer.selection_start, 1)
3402  assert_equal(buffer.selection_start, 1)
3403  buffer:close(true)
3404end
3405
3406function test_snippets_irregular_placeholders()
3407  buffer.new()
3408  textadept.snippets.insert('%1(foo %2(bar))%5(quux)')
3409  assert_equal(buffer:get_sel_text(), 'foo bar')
3410  buffer:delete_back()
3411  textadept.snippets.insert()
3412  assert_equal(buffer:get_sel_text(), 'quux')
3413  textadept.snippets.insert()
3414  assert_equal(buffer:get_text(), 'quux')
3415  buffer:close(true)
3416end
3417
3418function test_snippets_previous_cancel()
3419  buffer.new()
3420  textadept.snippets.insert('%1(foo) %2(bar) %3(baz)')
3421  assert_equal(buffer:get_text(), 'foo bar baz ') -- trailing space for snippet sentinel
3422  buffer:delete_back()
3423  textadept.snippets.insert()
3424  assert_equal(buffer:get_text(), ' bar baz ')
3425  buffer:delete_back()
3426  textadept.snippets.insert()
3427  assert_equal(buffer:get_text(), '  baz ')
3428  textadept.snippets.previous()
3429  textadept.snippets.previous()
3430  assert_equal(buffer:get_text(), 'foo bar baz ')
3431  assert_equal(buffer:get_sel_text(), 'foo')
3432  textadept.snippets.insert()
3433  textadept.snippets.cancel_current()
3434  assert_equal(buffer.length, 0)
3435  buffer:close(true)
3436end
3437
3438function test_snippets_nested()
3439  snippets.foo = '%1(foo)%2(bar)%3(baz)'
3440  buffer.new()
3441
3442  buffer:add_text('foo')
3443  textadept.snippets.insert()
3444  buffer:char_right()
3445  textadept.snippets.insert()
3446  assert_equal(buffer:get_text(), 'foobarbaz barbaz ') -- trailing spaces for snippet sentinels
3447  assert_equal(buffer:get_sel_text(), 'foo')
3448  assert_equal(buffer.selection_start, 1)
3449  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3450  buffer:replace_sel('quux')
3451  textadept.snippets.insert()
3452  assert_equal(buffer:get_sel_text(), 'bar')
3453  assert_equal(buffer.selection_start, 1 + 4)
3454  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3455  textadept.snippets.insert()
3456  assert_equal(buffer:get_sel_text(), 'baz')
3457  assert_equal(buffer.selection_start, 1 + 7)
3458  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3459  textadept.snippets.insert()
3460  assert_equal(buffer.current_pos, 1 + 10)
3461  assert_equal(buffer.selection_start, buffer.selection_end)
3462  assert_equal(buffer:get_text(), 'quuxbarbazbarbaz ')
3463  textadept.snippets.insert()
3464  assert_equal(buffer:get_sel_text(), 'bar')
3465  assert_equal(buffer.selection_start, 1 + 10)
3466  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3467  textadept.snippets.insert()
3468  assert_equal(buffer:get_sel_text(), 'baz')
3469  assert_equal(buffer.selection_start, 1 + 13)
3470  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3471  textadept.snippets.insert()
3472  assert_equal(buffer:get_text(), 'quuxbarbazbarbaz')
3473  buffer:clear_all()
3474
3475  buffer:add_text('foo')
3476  textadept.snippets.insert()
3477  buffer:char_right()
3478  textadept.snippets.insert()
3479  textadept.snippets.cancel_current()
3480  assert_equal(buffer.current_pos, 1 + 3)
3481  assert_equal(buffer.selection_start, buffer.selection_end)
3482  assert_equal(buffer:get_text(), 'foobarbaz ')
3483  buffer:add_text('quux')
3484  assert_equal(buffer:get_text(), 'fooquuxbarbaz ')
3485  textadept.snippets.insert()
3486  assert_equal(buffer:get_sel_text(), 'bar')
3487  assert_equal(buffer.selection_start, 1 + 7)
3488  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3489  textadept.snippets.insert()
3490  assert_equal(buffer:get_sel_text(), 'baz')
3491  assert_equal(buffer.selection_start, 1 + 10)
3492  assert_equal(buffer.selection_end, buffer.selection_start + 3)
3493  textadept.snippets.insert()
3494  assert_equal(buffer.current_pos, buffer.line_end_position[1])
3495  assert_equal(buffer.selection_start, buffer.selection_end)
3496  assert_equal(buffer:get_text(), 'fooquuxbarbaz')
3497
3498  buffer:close(true)
3499  snippets.foo = nil
3500end
3501
3502function test_snippets_select_interactive()
3503  snippets.foo = 'bar'
3504  buffer.new()
3505  textadept.snippets.select()
3506  assert(buffer.length > 0, 'no snippet inserted')
3507  buffer:close(true)
3508  snippets.foo = nil
3509end
3510
3511function test_snippets_autocomplete()
3512  snippets.bar = 'baz'
3513  snippets.baz = 'quux'
3514  buffer.new()
3515  buffer:add_text('ba')
3516  textadept.editing.autocomplete('snippet')
3517  assert(buffer:auto_c_active(), 'snippet autocompletion list not shown')
3518  buffer:auto_c_complete()
3519  textadept.snippets.insert()
3520  assert_equal(buffer:get_text(), 'baz')
3521  buffer:close(true)
3522  snippets.bar = nil
3523  snippets.baz = nil
3524end
3525
3526function test_lua_autocomplete()
3527  buffer.new()
3528  buffer:set_lexer('lua')
3529
3530  buffer:add_text('raw')
3531  textadept.editing.autocomplete('lua')
3532  assert(buffer:auto_c_active(), 'no autocompletions')
3533  assert_equal(buffer.auto_c_current_text, 'rawequal')
3534  buffer:auto_c_cancel()
3535  buffer:clear_all()
3536
3537  buffer:add_text('string.')
3538  textadept.editing.autocomplete('lua')
3539  assert(buffer:auto_c_active(), 'no autocompletions')
3540  assert_equal(buffer.auto_c_current_text, 'byte')
3541  buffer:auto_c_cancel()
3542  buffer:clear_all()
3543
3544  buffer:add_text('s = "foo"\ns:')
3545  textadept.editing.autocomplete('lua')
3546  assert(buffer:auto_c_active(), 'no autocompletions')
3547  assert_equal(buffer.auto_c_current_text, 'byte')
3548  buffer:auto_c_cancel()
3549  buffer:clear_all()
3550
3551  buffer:add_text('f = io.open("path")\nf:')
3552  textadept.editing.autocomplete('lua')
3553  assert(buffer:auto_c_active(), 'no autocompletions')
3554  assert_equal(buffer.auto_c_current_text, 'close')
3555  buffer:auto_c_cancel()
3556  buffer:clear_all()
3557
3558  buffer:add_text('buffer:auto_c')
3559  textadept.editing.autocomplete('lua')
3560  assert(not buffer:auto_c_active(), 'autocompletions available')
3561  buffer.filename = _HOME .. '/test/autocomplete_lua.lua'
3562  textadept.editing.autocomplete('lua')
3563  assert(buffer:auto_c_active(), 'no autocompletions')
3564  assert_equal(buffer.auto_c_current_text, 'auto_c_active')
3565  buffer:auto_c_cancel()
3566  buffer:clear_all()
3567
3568  local autocomplete_snippets = _M.lua.autocomplete_snippets
3569  _M.lua.autocomplete_snippets = false
3570  buffer:add_text('for')
3571  textadept.editing.autocomplete('lua')
3572  assert(not buffer:auto_c_active(), 'autocompletions available')
3573  _M.lua.autocomplete_snippets = true
3574  textadept.editing.autocomplete('lua')
3575  assert(buffer:auto_c_active(), 'no autocompletions')
3576  buffer:auto_c_cancel()
3577  buffer:clear_all()
3578  _M.lua.autocomplete_snippets = autocomplete_snippets -- restore
3579
3580  buffer:close(true)
3581end
3582
3583function test_ansi_c_autocomplete()
3584  buffer.new()
3585  buffer:set_lexer('ansi_c')
3586
3587  buffer:add_text('str')
3588  textadept.editing.autocomplete('ansi_c')
3589  assert(buffer:auto_c_active(), 'no autocompletions')
3590  assert_equal(buffer.auto_c_current_text, 'strcat')
3591  buffer:auto_c_cancel()
3592  buffer:clear_all()
3593
3594  buffer:add_text('div_t d;\nd->')
3595  textadept.editing.autocomplete('ansi_c')
3596  assert(buffer:auto_c_active(), 'no autocompletions')
3597  assert_equal(buffer.auto_c_current_text, 'quot')
3598  buffer:auto_c_cancel()
3599  buffer:clear_all()
3600
3601  local autocomplete_snippets = _M.ansi_c.autocomplete_snippets
3602  _M.ansi_c.autocomplete_snippets = false
3603  buffer:add_text('for')
3604  textadept.editing.autocomplete('ansi_c')
3605  assert(not buffer:auto_c_active(), 'autocompletions available')
3606  _M.ansi_c.autocomplete_snippets = true
3607  textadept.editing.autocomplete('ansi_c')
3608  assert(buffer:auto_c_active(), 'no autocompletions')
3609  buffer:auto_c_cancel()
3610  buffer:clear_all()
3611  _M.ansi_c.autocomplete_snippets = autocomplete_snippets -- restore
3612
3613  -- TODO: typeref and rescan
3614
3615  buffer:close(true)
3616end
3617
3618function test_lexer_api()
3619  buffer.new()
3620  buffer.use_tabs, buffer.tab_width = true, 4
3621  buffer:set_text(table.concat({
3622    'if foo then',
3623    '\tbar',
3624    '',
3625    'end',
3626    'baz'
3627  }, '\n'))
3628  buffer:set_lexer('lua')
3629  buffer:colorize(1, -1)
3630  local lexer = require('lexer')
3631  assert(lexer.fold_level[1] & lexer.FOLD_HEADER > 0, 'not a fold header')
3632  assert_equal(lexer.fold_level[2], lexer.fold_level[3])
3633  assert(lexer.fold_level[4] > lexer.fold_level[5], 'incorrect fold levels')
3634  assert(lexer.indent_amount[1] < lexer.indent_amount[2], 'incorrect indent level')
3635  assert(lexer.indent_amount[2] > lexer.indent_amount[3], 'incorrect indent level')
3636  lexer.line_state[1] = 2
3637  assert_equal(lexer.line_state[1], 2)
3638  assert_equal(lexer.property['foo'], '')
3639  lexer.property['foo'] = 'bar'
3640  assert_equal(lexer.property['foo'], 'bar')
3641  lexer.property['bar'] = '$(foo),$(foo)'
3642  assert_equal(lexer.property_expanded['bar'], 'bar,bar')
3643  lexer.property['baz'] = '1'
3644  assert_equal(lexer.property_int['baz'], 1)
3645  lexer.property['baz'] = ''
3646  assert_equal(lexer.property_int['baz'], 0)
3647  assert_equal(lexer.property_int['quux'], 0)
3648  assert_equal(lexer.style_at[2], 'keyword')
3649  assert_equal(lexer.line_from_position(15), 2)
3650  buffer:close(true)
3651
3652  assert_raises(function() lexer.fold_level = nil end, 'read-only')
3653  assert_raises(function() lexer.fold_level[1] = 0 end, 'read-only')
3654  assert_raises(function() lexer.indent_amount = nil end, 'read-only')
3655  assert_raises(function() lexer.indent_amount[1] = 0 end, 'read-only')
3656  assert_raises(function() lexer.property = nil end, 'read-only')
3657  assert_raises(function() lexer.property_int = nil end, 'read-only')
3658  assert_raises(function() lexer.property_int['foo'] = 1 end, 'read-only')
3659  --TODO: assert_raises(function() lexer.property_expanded = nil end, 'read-only')
3660  assert_raises(function() lexer.property_expanded['foo'] = 'bar' end, 'read-only')
3661  assert_raises(function() lexer.style_at = nil end, 'read-only')
3662  assert_raises(function() lexer.style_at[1] = 0 end, 'read-only')
3663  assert_raises(function() lexer.line_state = nil end, 'read-only')
3664  assert_raises(function() lexer.line_from_position = nil end, 'read-only')
3665end
3666
3667function test_ui_size()
3668  local size = ui.size
3669  ui.size = {size[1] - 50, size[2] + 50}
3670  assert_equal(ui.size, size)
3671  ui.size = size
3672end
3673
3674function test_ui_maximized()
3675  local maximized = ui.maximized
3676  ui.maximized = not maximized
3677  local not_maximized = ui.maximized
3678  ui.maximized = maximized -- reset
3679  -- For some reason, the following fails, even though the window maximized
3680  -- status is toggled. `ui.update()` does not seem to help.
3681  assert_equal(not_maximized, not maximized)
3682end
3683expected_failure(test_ui_maximized)
3684
3685function test_ui_restore_view_state()
3686  buffer.new() -- 1
3687  view.view_ws = view.WS_VISIBLEALWAYS
3688  buffer.new() -- 2
3689  assert(view.view_ws ~= view.WS_VISIBLEALWAYS, 'view whitespace settings not reset')
3690  view:goto_buffer(-1) -- go back to 1
3691  assert_equal(view.view_ws, view.WS_VISIBLEALWAYS)
3692  view.view_ws = view.WS_INVISIBLE -- reset
3693  buffer.new() -- 3
3694  view.view_ws = view.WS_VISIBLEALWAYS
3695  buffer:close() -- switches back to 1 (after briefly switching to 2)
3696  assert_equal(view.view_ws, view.WS_INVISIBLE)
3697  view:goto_buffer(1) -- go back to 2
3698  assert_equal(view.view_ws, view.WS_INVISIBLE)
3699  buffer:close()
3700  buffer:close()
3701end
3702
3703function test_reset()
3704  local _persist
3705  _G.foo = 'bar'
3706  reset()
3707  assert(not _G.foo, 'Lua not reset')
3708  _G.foo = 'bar'
3709  events.connect(events.RESET_BEFORE, function(persist)
3710    persist.foo = _G.foo
3711    _persist = persist -- store
3712  end)
3713  reset()
3714  -- events.RESET_AFTER has already been run, but there was no opportunity to
3715  -- connect to it in this test, so connect and simulate the event again.
3716  events.connect(events.RESET_AFTER, function(persist) _G.foo = persist.foo end)
3717  events.emit(events.RESET_AFTER, _persist)
3718  assert_equal(_G.foo, 'bar')
3719end
3720
3721function test_timeout()
3722  if CURSES then
3723    assert_raises(function() timeout(1, function() end) end, 'not implemented')
3724    return
3725  end
3726
3727  local count = 0
3728  local function f()
3729    count = count + 1
3730    return count < 2
3731  end
3732  timeout(0.4, f)
3733  assert_equal(count, 0)
3734  os.execute('sleep 0.5')
3735  ui.update()
3736  assert_equal(count, 1)
3737  os.execute('sleep 0.5')
3738  ui.update()
3739  assert_equal(count, 2)
3740  os.execute('sleep 0.5')
3741  ui.update()
3742  assert_equal(count, 2)
3743end
3744
3745function test_view_split_resize_unsplit()
3746  view:split()
3747  local size = view.size
3748  view.size = view.size - 1
3749  assert_equal(view.size, size - 1)
3750  assert_equal(#_VIEWS, 2)
3751  view:split(true)
3752  size = view.size
3753  view.size = view.size + 1
3754  assert_equal(view.size, size + 1)
3755  assert_equal(#_VIEWS, 3)
3756  view:unsplit()
3757  assert_equal(#_VIEWS, 2)
3758  view:split(true)
3759  ui.goto_view(_VIEWS[1])
3760  view:unsplit() -- unsplits split view, leaving single view
3761  assert_equal(#_VIEWS, 1)
3762end
3763
3764function test_view_split_refresh_styles()
3765  io.open_file(_HOME .. '/init.lua')
3766  local style = buffer:style_of_name('library')
3767  assert(style > 1, 'cannot retrieve number of library style')
3768  local color = view.style_fore[style]
3769  assert(color ~= view.style_fore[view.STYLE_DEFAULT], 'library style not set')
3770  view:split()
3771  for _, view in ipairs(_VIEWS) do
3772    local view_style = buffer:style_of_name('library')
3773    assert_equal(view_style, style)
3774    local view_color = view.style_fore[view_style]
3775    assert_equal(view_color, color)
3776  end
3777  view:unsplit()
3778  buffer:close(true)
3779end
3780
3781function test_buffer_read_write_only_properties()
3782  assert_raises(function() view.all_lines_visible = false end, 'read-only property')
3783  assert_raises(function() return buffer.auto_c_fill_ups end, 'write-only property')
3784  assert_raises(function() buffer.annotation_text = {} end, 'read-only property')
3785  assert_raises(function() buffer.char_at[1] = string.byte(' ') end, 'read-only property')
3786  assert_raises(function() return view.marker_alpha[1] end, 'write-only property')
3787end
3788
3789function test_set_theme()
3790  local current_theme = view.style_fore[view.STYLE_DEFAULT]
3791  view:split()
3792  io.open_file(_HOME .. '/init.lua')
3793  view:split(true)
3794  io.open_file(_HOME .. '/src/textadept.c')
3795  _VIEWS[2]:set_theme('dark')
3796  _VIEWS[3]:set_theme('light')
3797  assert(_VIEWS[2].style_fore[view.STYLE_DEFAULT] ~= _VIEWS[3].style_fore[view.STYLE_DEFAULT], 'same default styles')
3798  buffer:close(true)
3799  buffer:close(true)
3800  ui.goto_view(_VIEWS[1])
3801  view:unsplit()
3802end
3803
3804function test_set_lexer_style()
3805  buffer.new()
3806  buffer:set_lexer('java')
3807  buffer:add_text('foo()')
3808  buffer:colorize(1, -1)
3809  local style = buffer:style_of_name('function')
3810  assert_equal(buffer.style_at[1], style)
3811  local default_fore = view.style_fore[view.STYLE_DEFAULT]
3812  assert(view.style_fore[style] ~= default_fore, 'function name style_fore same as default style_fore')
3813  view.style_fore[style] = view.style_fore[view.STYLE_DEFAULT]
3814  assert_equal(view.style_fore[style], default_fore)
3815  local color = lexer.colors[not CURSES and 'orange' or 'blue']
3816  assert(color > 0 and color ~= default_fore)
3817  lexer.styles['function'] = {fore = color}
3818  assert_equal(view.style_fore[style], color)
3819  buffer:close(true)
3820  -- Defined in Lua lexer, which is not currently loaded.
3821  assert(buffer:style_of_name('library'), view.STYLE_DEFAULT)
3822  -- Emulate a theme setting to trigger an LPeg lexer style refresh, but without
3823  -- a token defined.
3824  view.property['style.library'] = view.property['style.library']
3825end
3826
3827function test_lexer_fold_properties()
3828  lexer.property['fold.compact'] = '0'
3829  assert(not lexer.fold_compact, 'lexer.fold_compact not updated')
3830  lexer.fold_compact = true
3831  assert(lexer.fold_compact, 'lexer.fold_compact not updated')
3832  assert_equal(lexer.property['fold.compact'], '1')
3833  lexer.fold_compact = nil
3834  assert(not lexer.fold_compact)
3835  assert_equal(lexer.property['fold.compact'], '0')
3836  local truthy, falsy = {true, '1', 1}, {false, '0', 0}
3837  for i = 1, #truthy do
3838    lexer.fold_compact = truthy[i]
3839    assert(lexer.fold_compact, 'lexer.fold_compact not updated for "%s"', tostring(truthy[i]))
3840    lexer.fold_compact = falsy[i]
3841    assert(not lexer.fold_compact, 'lexer.fold_compact not updated for "%s"', tostring(falsy[i]))
3842  end
3843  -- Verify fold and folding properties are synchronized.
3844  lexer.property['fold'] = '0'
3845  assert(not lexer.folding)
3846  lexer.folding = true
3847  assert(lexer.property['fold'] == '1')
3848  -- Lexer fold properties and view fold properties do not mirror because
3849  -- Scintilla forwards view property settings to lexers, not vice-versa.
3850  view.property['fold'] = '0'
3851  assert(not lexer.folding)
3852  lexer.folding = true
3853  assert_equal(view.property['fold'], '0')
3854end
3855
3856function test_lexer_fold_line_groups()
3857  local fold_line_groups = lexer.fold_line_groups
3858  buffer.new()
3859  buffer:add_text[[
3860    package foo;
3861
3862    import bar;
3863    import baz;
3864    import quux;
3865    // comment
3866    // comment
3867    // comment
3868
3869    public class Foo {}
3870  ]]
3871  buffer:set_lexer('java')
3872  lexer.fold_line_groups = false
3873  buffer:colorize(1, -1)
3874  assert(buffer.fold_level[3] & lexer.FOLD_HEADER == 0, 'import is a fold point')
3875  assert(buffer.fold_level[6] & lexer.FOLD_HEADER == 0, 'line comment is a fold point')
3876  lexer.fold_line_groups = true
3877  buffer:colorize(1, -1)
3878  assert(buffer.fold_level[3] & lexer.FOLD_HEADER > 0, 'import is not a fold point')
3879  assert(buffer.fold_level[6] & lexer.FOLD_HEADER > 0, 'line comment is not a fold point')
3880  view:toggle_fold(3)
3881  for i = 4, 5 do assert(not view.line_visible[i], 'line %i is visible', i) end
3882  view:toggle_fold(6)
3883  for i = 7, 8 do assert(not view.line_visible[i], 'line %i is visible', i) end
3884  buffer:close(true)
3885  lexer.fold_line_groups = fold_line_groups -- restore
3886end
3887
3888-- TODO: test init.lua's buffer settings
3889
3890function test_ctags()
3891  local ctags = require('ctags')
3892
3893  -- Setup project.
3894  local dir = os.tmpname()
3895  os.remove(dir)
3896  lfs.mkdir(dir)
3897  os.execute(string.format('cp -r %s/test/modules/ctags/c/* %s', _HOME, dir))
3898  lfs.mkdir(dir .. '/.hg') -- simulate version control
3899  local foo_h, foo_c = dir .. '/include/foo.h', dir .. '/src/foo.c'
3900
3901  -- Generate tags and api.
3902  io.open_file(dir .. '/src/foo.c')
3903  textadept.menu.menubar[_L['Search']][_L['Ctags']][_L['Generate Project Tags and API']][2]()
3904  assert(lfs.attributes(dir .. '/tags'), 'tags file not generated')
3905  assert(lfs.attributes(dir .. '/api'), 'api file not generated')
3906  local f = io.open(dir .. '/api')
3907  local contents = f:read('a')
3908  f:close()
3909  assert(contents:find('main int main(int argc, char **argv) {', 1, true), 'did not properly generate api')
3910
3911  -- Test `ctags.goto_tag()`.
3912  ctags.goto_tag('main')
3913  assert_equal(buffer.filename, foo_c)
3914  assert(buffer:get_cur_line():find('^int main%('), 'not at "main" function')
3915  buffer:line_down()
3916  buffer:vc_home()
3917  ctags.goto_tag() -- foo(FOO)
3918  assert_equal(buffer.filename, foo_h)
3919  assert(buffer:get_cur_line():find('^void foo%('), 'not at "foo" function')
3920  view:goto_buffer(-1) -- back to src/foo.c
3921  assert_equal(buffer.filename, foo_c)
3922  buffer:word_right()
3923  buffer:word_right()
3924  ctags.goto_tag() -- FOO
3925  assert_equal(buffer.filename, foo_h)
3926  assert(buffer:get_cur_line():find('^#define FOO 1'), 'not at "FOO" definition')
3927
3928  -- Test tag autocompletion.
3929  buffer:line_end()
3930  buffer:new_line()
3931  buffer:add_text('m')
3932  textadept.editing.autocomplete('ctag')
3933  assert(buffer:get_cur_line():find('^main'), 'did not autocomplete "main" function')
3934
3935  -- Test `ctags.goto_tag()` with custom tags path.
3936  ctags.ctags_flags[dir] = '-R ' .. dir -- for writing absolute paths
3937  textadept.menu.menubar[_L['Search']][_L['Ctags']][_L['Generate Project Tags and API']][2]()
3938  os.execute(string.format('mv %s/tags %s/src', dir, dir))
3939  assert(not lfs.attributes(dir .. '/tags') and lfs.attributes(dir .. '/src/tags'), 'did not move tags file')
3940  ctags[dir] = dir .. '/src/tags'
3941  ctags.goto_tag('main')
3942  assert_equal(buffer.filename, foo_c)
3943  assert(buffer:get_cur_line():find('^int main%('), 'not at "main" function')
3944
3945  -- Test `ctags.goto_tag()` with no tags file and using current file contents.
3946  os.remove(dir .. '/src/tags')
3947  assert(not lfs.attributes(dir .. '/src/tags'), 'did not remove tags file')
3948  buffer:line_down()
3949  buffer:line_down()
3950  buffer:vc_home()
3951  ctags.goto_tag() -- bar()
3952  assert_equal(buffer.filename, foo_c)
3953  assert(buffer:get_cur_line():find('^void bar%('))
3954
3955  view:goto_buffer(1)
3956  buffer:close(true)
3957  buffer:close(true)
3958  os.execute('rm -r ' .. dir)
3959end
3960
3961function test_ctags_lua()
3962  local ctags = require('ctags')
3963
3964  -- Setup project.
3965  local dir = os.tmpname()
3966  os.remove(dir)
3967  lfs.mkdir(dir)
3968  os.execute(string.format('cp -r %s/test/modules/ctags/lua/* %s', _HOME, dir))
3969  lfs.mkdir(dir .. '/.hg') -- simulate version control
3970
3971  -- Generate tags and api.
3972  io.open_file(dir .. '/foo.lua')
3973  ctags.ctags_flags[dir] = '-R ' .. ctags.LUA_FLAGS
3974  textadept.menu.menubar[_L['Search']][_L['Ctags']][_L['Generate Project Tags and API']][2]()
3975  assert(lfs.attributes(dir .. '/tags'), 'tags file not generated')
3976  assert(lfs.attributes(dir .. '/api'), 'api file not generated')
3977
3978  if not CURSES then -- TODO: cannot properly spawn with ctags.LUA_FLAGS on curses
3979    ctags.goto_tag('foo')
3980    assert(buffer:get_cur_line():find('^function foo%('), 'not at "foo" function')
3981    ctags.goto_tag('bar')
3982    assert(buffer:get_cur_line():find('^local function bar%('), 'not at "bar" function')
3983    ctags.goto_tag('baz')
3984    assert(buffer:get_cur_line():find('^baz = %{'), 'not at "baz" table')
3985    ctags.goto_tag('quux')
3986    assert(buffer:get_cur_line():find('^function baz:quux%('), 'not at "baz.quux" function')
3987  end
3988
3989  -- Test using Textadept's tags and api generator.
3990  ctags.ctags_flags[dir] = ctags.LUA_GENERATOR
3991  ctags.api_commands[dir] = ctags.LUA_GENERATOR
3992  textadept.menu.menubar[_L['Search']][_L['Ctags']][_L['Generate Project Tags and API']][2]()
3993  ctags.goto_tag('new')
3994  assert(buffer:get_cur_line():find('^function M%.new%('), 'not at "M.new" function')
3995  local f = io.open(dir .. '/api')
3996  local contents = f:read('a')
3997  f:close()
3998  assert(contents:find('new foo%.new%(%)\\nFoo'), 'did not properly generate api')
3999
4000  buffer:close(true)
4001  os.execute('rm -r ' .. dir)
4002end
4003
4004function test_debugger_ansi_c()
4005  local debugger = require('debugger')
4006  require('debugger.ansi_c').logging = true
4007  local function wait()
4008    os.spawn('sleep 0.2'):wait()
4009    ui.update()
4010  end
4011  local tabs = ui.tabs
4012  ui.tabs = false
4013  local dir = os.tmpname()
4014  os.remove(dir)
4015  lfs.mkdir(dir)
4016  local filename = dir .. '/foo.c'
4017  os.execute(string.format('cp %s/test/modules/debugger/ansi_c/foo.c %s', _HOME, filename))
4018  io.open_file(filename)
4019  debugger.toggle_breakpoint(nil, 8)
4020  assert(buffer:marker_get(8) > 0, 'breakpoint marker not set')
4021  textadept.run.compile_commands[filename] = textadept.run.compile_commands.ansi_c .. ' -g'
4022  textadept.run.compile()
4023  wait()
4024  assert_equal(#_VIEWS, 2)
4025  local msg_buf = buffer
4026  assert(buffer:get_text():find('status: 0'), 'compile failed')
4027  ui.goto_view(-1)
4028  debugger.start(nil, dir .. '/foo')
4029  wait()
4030  debugger.continue()
4031  wait()
4032  assert_equal(buffer.filename, filename)
4033  assert_equal(buffer:line_from_position(buffer.current_pos), 8)
4034  assert(buffer:marker_number_from_line(8, 2) > 0, 'current line marker not set')
4035  assert(not msg_buf:get_text():find('^start\n'), 'not at breakpoint')
4036  debugger.restart()
4037  wait()
4038  assert_equal(buffer.filename, filename)
4039  assert_equal(buffer:line_from_position(buffer.current_pos), 8)
4040  assert(buffer:marker_number_from_line(8, 2) > 0, 'current line marker not set')
4041  assert(not msg_buf:get_text():find('^start\n'), 'not at breakpoint')
4042  debugger.stop()
4043  wait()
4044  assert_equal(buffer:marker_number_from_line(8, 2), -1, 'still debugging')
4045  debugger.start(nil, dir .. '/foo')
4046  wait()
4047  debugger.continue()
4048  wait()
4049  debugger.toggle_breakpoint() -- clear
4050  debugger.step_over()
4051  wait()
4052  assert_equal(buffer:line_from_position(buffer.current_pos), 9)
4053  assert(buffer:marker_get(9) > 0, 'current line marker not set')
4054  assert_equal(buffer:marker_get(8), 0) -- current line marker cleared
4055  -- TODO: gdb does not print program stdout to its stdout until the end when
4056  -- using the mi interface.
4057  --assert(msg_buf:get_text():find('^start\n'), 'process stdout not captured')
4058  debugger.step_over()
4059  wait()
4060  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
4061  debugger.evaluate('i')
4062  wait()
4063  assert_equal(buffer.filename, filename) -- still in file being debugged
4064  assert(msg_buf:get_text():find('\n0\n'), 'evaluation of i failed')
4065  debugger.step_into()
4066  wait()
4067  assert_equal(buffer:line_from_position(buffer.current_pos), 4)
4068  debugger.set_frame(2)
4069  wait()
4070  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
4071  debugger.set_frame(1)
4072  wait()
4073  assert_equal(buffer:line_from_position(buffer.current_pos), 4)
4074  buffer:search_anchor()
4075  local pos = buffer:search_next(buffer.FIND_MATCHCASE | buffer.FIND_WHOLEWORD, 'i')
4076  assert(pos > 0, "'i' not found")
4077  debugger.inspect(pos)
4078  wait()
4079  assert(buffer:call_tip_active(), 'no call tip active')
4080  debugger.step_out()
4081  wait()
4082  --assert(msg_buf:get_text():find('\nfoo 0\n'), 'process stdout not captured')
4083  assert_equal(buffer:line_from_position(buffer.current_pos), 9)
4084  debugger.set_watch('i')
4085  debugger.continue()
4086  wait()
4087  assert_equal(buffer:line_from_position(buffer.current_pos), 9)
4088  assert(not msg_buf:get_text():find('\nfoo 1\n'), 'watch point failed')
4089  debugger.remove_watch(1)
4090  debugger.step_over()
4091  wait()
4092  events.emit(events.MARGIN_CLICK, 2, buffer.current_pos, 0) -- simulate breakpoint margin click
4093  debugger.continue()
4094  wait()
4095  assert_equal(buffer:line_from_position(buffer.current_pos), 10)
4096  --assert(msg_buf:get_text():find('\nfoo 1\n'), 'set breakpoint failed')
4097  assert(not msg_buf:get_text():find('\nfoo 2\n'), 'set breakpoint failed')
4098  events.emit(events.MARGIN_CLICK, 2, buffer.current_pos, 0) -- simulate breakpoint margin click; clear
4099  debugger.continue()
4100  wait()
4101  --assert(msg_buf:get_text():find('\nfoo 2\n'), 'process stdout not captured')
4102  --assert(msg_buf:get_text():find('\nfoo 3\n'), 'process stdout not captured')
4103  --assert(msg_buf:get_text():find('\nend\n'), 'process stdout not captured')
4104  for i = 1, buffer.line_count do assert_equal(buffer:marker_get(i), 0) end
4105  ui.goto_view(1)
4106  buffer:close(true)
4107  view:unsplit()
4108  buffer:close(true)
4109  os.execute('rm -r ' .. dir)
4110  ui.tabs = tabs
4111end
4112
4113function test_debugger_lua()
4114  local debugger = require('debugger')
4115  local function wait()
4116    for i = 1, 10 do
4117      os.spawn('sleep 0.1'):wait()
4118      ui.update()
4119    end
4120  end
4121  local tabs = ui.tabs
4122  ui.tabs = false
4123  local filename = _HOME .. '/test/modules/debugger/lua/foo.lua'
4124  io.open_file(filename)
4125  debugger.toggle_breakpoint(nil, 5)
4126  assert(buffer:marker_get(5) > 0, 'breakpoint marker not set')
4127  debugger.continue() -- start
4128  wait()
4129  assert_equal(buffer.filename, filename)
4130  assert_equal(buffer:line_from_position(buffer.current_pos), 5)
4131  assert(buffer:marker_number_from_line(5, 2) > 0, 'current line marker not set')
4132  assert_equal(#_VIEWS, 1)
4133  debugger.restart()
4134  wait()
4135  assert_equal(buffer.filename, filename)
4136  assert_equal(buffer:line_from_position(buffer.current_pos), 3) -- for whatever reason
4137  assert(buffer:marker_get(3) > 0, 'current line marker not set')
4138  assert_equal(#_VIEWS, 1)
4139  debugger.stop()
4140  wait()
4141  assert_equal(buffer:marker_number_from_line(5, 2), -1, 'still debugging')
4142  debugger.continue() -- start
4143  wait()
4144  debugger.toggle_breakpoint() -- clear
4145  debugger.step_over()
4146  wait()
4147  assert_equal(#_VIEWS, 2)
4148  assert_equal(buffer.filename, filename)
4149  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
4150  assert(buffer:marker_get(6) > 0, 'current line marker not set')
4151  assert_equal(buffer:marker_get(5), 0) -- current line marker cleared
4152  local msg_buf = _VIEWS[#_VIEWS].buffer
4153  assert(msg_buf:get_text():find('^"start"\n'), 'process stdout not captured')
4154  debugger.step_over()
4155  wait()
4156  assert_equal(buffer:line_from_position(buffer.current_pos), 7)
4157  debugger.evaluate("print('i', i)")
4158  wait()
4159  assert_equal(buffer.filename, filename) -- still in file being debugged
4160  assert(msg_buf:get_text():find('\n"i"%s1\n'), 'evaluation of i failed')
4161  debugger.step_into()
4162  wait()
4163  assert_equal(buffer:line_from_position(buffer.current_pos), 2)
4164  -- TODO: set_frame is not implemented in the Lua debugger.
4165  --debugger.set_frame(2)
4166  --wait()
4167  --assert_equal(buffer:line_from_position(buffer.current_pos), 7)
4168  --debugger.set_frame(1)
4169  --wait()
4170  --assert_equal(buffer:line_from_position(buffer.current_pos), 2)
4171  buffer:search_anchor()
4172  local pos = buffer:search_next(buffer.FIND_MATCHCASE | buffer.FIND_WHOLEWORD, 'i')
4173  assert(pos > 0, "'i' not found")
4174  debugger.inspect(pos)
4175  wait()
4176  assert(buffer:call_tip_active(), 'no call tip active')
4177  debugger.step_out()
4178  wait()
4179  assert(msg_buf:get_text():find('\n"foo"%s1\n'), 'process stdout not captured')
4180  assert_equal(buffer:line_from_position(buffer.current_pos), 6)
4181  debugger.set_watch('i')
4182  debugger.continue()
4183  wait()
4184  assert_equal(buffer:line_from_position(buffer.current_pos), 7)
4185  assert(not msg_buf:get_text():find('\n"foo"%s2\n'), 'watch point failed')
4186  debugger.remove_watch(1)
4187  events.emit(events.MARGIN_CLICK, 2, buffer.current_pos, 0) -- simulate breakpoint margin click
4188  debugger.continue()
4189  wait()
4190  assert_equal(buffer:line_from_position(buffer.current_pos), 7)
4191  assert(msg_buf:get_text():find('\n"foo"%s2\n'), 'set breakpoint failed')
4192  assert(not msg_buf:get_text():find('\n"foo"%s3\n'), 'set breakpoint failed')
4193  events.emit(events.MARGIN_CLICK, 2, buffer.current_pos, 0) -- simulate breakpoint margin click; clear
4194  debugger.continue()
4195  wait()
4196  assert(msg_buf:get_text():find('\n"foo"%s3\n'), 'process stdout not captured')
4197  assert(msg_buf:get_text():find('\n"foo"%s4\n'), 'process stdout not captured')
4198  assert(msg_buf:get_text():find('\n"end"\n'), 'process stdout not captured')
4199  for i = 1, buffer.line_count do assert_equal(buffer:marker_get(i), 0) end
4200  ui.goto_view(1)
4201  buffer:close(true)
4202  view:unsplit()
4203  buffer:close(true)
4204  ui.tabs = tabs
4205end
4206
4207function test_export_interactive()
4208  local export = require('export')
4209  buffer.new()
4210  buffer:add_text("_G.foo=table.concat{1,'bar',true,print}\nbar=[[<>& ]]")
4211  buffer:set_lexer('lua')
4212  local filename = os.tmpname()
4213  export.to_html(nil, filename)
4214  _G.timeout(0.5, function() os.remove(filename) end)
4215  buffer:close(true)
4216end
4217
4218function test_file_diff()
4219  local diff = require('file_diff')
4220
4221  local filename1 = _HOME .. '/test/modules/file_diff/1'
4222  local filename2 = _HOME .. '/test/modules/file_diff/2'
4223  io.open_file(filename1)
4224  io.open_file(filename2)
4225  view:split()
4226  ui.goto_view(-1)
4227  view:goto_buffer(-1)
4228  diff.start('-', '-')
4229  assert_equal(#_VIEWS, 2)
4230  assert_equal(view, _VIEWS[1])
4231  local buffer1, buffer2 = _VIEWS[1].buffer, _VIEWS[2].buffer
4232  assert_equal(buffer1.filename, filename1)
4233  assert_equal(buffer2.filename, filename2)
4234
4235  local function verify(buffer, markers, indicators, annotations)
4236    for i = 1, buffer.line_count do
4237      if not markers[i] then
4238        assert(buffer:marker_get(i) == 0, 'unexpected marker on line %d', i)
4239      else
4240        assert(buffer:marker_get(i) & 1 << markers[i] - 1 > 0, 'incorrect marker on line %d', i)
4241      end
4242      if not annotations[i] then
4243        assert(buffer.annotation_text[i] == '', 'unexpected annotation on line %d', i)
4244      else
4245        assert(buffer.annotation_text[i] == annotations[i], 'incorrect annotation on line %d', i)
4246      end
4247    end
4248    for _, indic in ipairs{diff.INDIC_DELETION, diff.INDIC_ADDITION} do
4249      local s = buffer:indicator_end(indic, 1)
4250      local e = buffer:indicator_end(indic, s)
4251      while s < buffer.length and e > s do
4252        local text = buffer:text_range(s, e)
4253        assert(indicators[text] == indic, 'incorrect indicator for "%s"', text)
4254        s = buffer:indicator_end(indic, e)
4255        e = buffer:indicator_end(indic, s)
4256      end
4257    end
4258  end
4259
4260  -- Verify line markers.
4261  verify(buffer1, {
4262    [1] = diff.MARK_MODIFICATION,
4263    [2] = diff.MARK_MODIFICATION,
4264    [3] = diff.MARK_MODIFICATION,
4265    [4] = diff.MARK_MODIFICATION,
4266    [5] = diff.MARK_MODIFICATION,
4267    [6] = diff.MARK_MODIFICATION,
4268    [7] = diff.MARK_MODIFICATION,
4269    [12] = diff.MARK_MODIFICATION,
4270    [14] = diff.MARK_MODIFICATION,
4271    [15] = diff.MARK_MODIFICATION,
4272    [16] = diff.MARK_DELETION
4273  }, {
4274    ['is'] = diff.INDIC_DELETION,
4275    ['line\n'] = diff.INDIC_DELETION,
4276    ['    '] = diff.INDIC_DELETION,
4277    ['+'] = diff.INDIC_DELETION,
4278    ['pl'] = diff.INDIC_DELETION,
4279    ['one'] = diff.INDIC_DELETION,
4280    ['wo'] = diff.INDIC_DELETION,
4281    ['three'] = diff.INDIC_DELETION,
4282    ['will'] = diff.INDIC_DELETION
4283  }, {[11] = ' \n'})
4284  verify(buffer2, {
4285    [1] = diff.MARK_MODIFICATION,
4286    [2] = diff.MARK_MODIFICATION,
4287    [3] = diff.MARK_MODIFICATION,
4288    [4] = diff.MARK_MODIFICATION,
4289    [5] = diff.MARK_MODIFICATION,
4290    [6] = diff.MARK_MODIFICATION,
4291    [7] = diff.MARK_MODIFICATION,
4292    [12] = diff.MARK_ADDITION,
4293    [13] = diff.MARK_ADDITION,
4294    [14] = diff.MARK_MODIFICATION,
4295    [16] = diff.MARK_MODIFICATION,
4296    [17] = diff.MARK_MODIFICATION
4297  }, {
4298    ['at'] = diff.INDIC_ADDITION,
4299    ['paragraph\n    '] = diff.INDIC_ADDITION,
4300    ['-'] = diff.INDIC_ADDITION,
4301    ['min'] = diff.INDIC_ADDITION,
4302    ['two'] = diff.INDIC_ADDITION,
4303    ['\t'] = diff.INDIC_ADDITION,
4304    ['hree'] = diff.INDIC_ADDITION,
4305    ['there are '] = diff.INDIC_ADDITION,
4306    ['four'] = diff.INDIC_ADDITION,
4307    ['have'] = diff.INDIC_ADDITION,
4308    ['d'] = diff.INDIC_ADDITION
4309  }, {[17] = ' '})
4310
4311  -- Stop comparing, verify the buffers are restored to normal, and then start
4312  -- comparing again.
4313  textadept.menu.menubar[_L['Tools']][_L['Compare Files']][_L['Stop Comparing']][2]()
4314  verify(buffer1, {}, {}, {})
4315  verify(buffer2, {}, {}, {})
4316  textadept.menu.menubar[_L['Tools']][_L['Compare Files']][_L['Compare Buffers']][2]()
4317
4318  -- Test goto next/prev change.
4319  assert_equal(buffer1:line_from_position(buffer1.current_pos), 1)
4320  diff.goto_change(true)
4321  assert_equal(buffer1:line_from_position(buffer1.current_pos), 11)
4322  diff.goto_change(true)
4323  assert_equal(buffer1:line_from_position(buffer1.current_pos), 12)
4324  diff.goto_change(true)
4325  assert_equal(buffer1:line_from_position(buffer1.current_pos), 14)
4326  diff.goto_change(true)
4327  assert_equal(buffer1:line_from_position(buffer1.current_pos), 16)
4328  diff.goto_change(true)
4329  assert_equal(buffer1:line_from_position(buffer1.current_pos), 1)
4330  diff.goto_change()
4331  assert_equal(buffer1:line_from_position(buffer1.current_pos), 16)
4332  diff.goto_change()
4333  assert_equal(buffer1:line_from_position(buffer1.current_pos), 15)
4334  diff.goto_change()
4335  assert_equal(buffer1:line_from_position(buffer1.current_pos), 12)
4336  diff.goto_change()
4337  assert_equal(buffer1:line_from_position(buffer1.current_pos), 7)
4338  ui.goto_view(1)
4339  assert_equal(buffer2:line_from_position(buffer2.current_pos), 1)
4340  diff.goto_change(true)
4341  assert_equal(buffer2:line_from_position(buffer2.current_pos), 12)
4342  diff.goto_change(true)
4343  assert_equal(buffer2:line_from_position(buffer2.current_pos), 14)
4344  diff.goto_change(true)
4345  assert_equal(buffer2:line_from_position(buffer2.current_pos), 16)
4346  diff.goto_change(true)
4347  assert_equal(buffer2:line_from_position(buffer2.current_pos), 17)
4348  diff.goto_change(true)
4349  assert_equal(buffer2:line_from_position(buffer2.current_pos), 1)
4350  diff.goto_change()
4351  assert_equal(buffer2:line_from_position(buffer2.current_pos), 17)
4352  diff.goto_change()
4353  assert_equal(buffer2:line_from_position(buffer2.current_pos), 14)
4354  diff.goto_change()
4355  assert_equal(buffer2:line_from_position(buffer2.current_pos), 13)
4356  diff.goto_change()
4357  assert_equal(buffer2:line_from_position(buffer2.current_pos), 7)
4358  ui.goto_view(-1)
4359  buffer1:goto_line(1)
4360
4361  -- Merge first block right to left and verify.
4362  assert_equal(buffer1:line_from_position(buffer1.current_pos), 1)
4363  diff.merge(true)
4364  assert(buffer1:get_line(1):find('^that'), 'did not merge from right to left')
4365  local function verify_first_merge()
4366    for i = 1, 7 do assert_equal(buffer1:get_line(i), buffer2:get_line(i)) end
4367    verify(buffer1, {
4368      [12] = diff.MARK_MODIFICATION,
4369      [14] = diff.MARK_MODIFICATION,
4370      [15] = diff.MARK_MODIFICATION,
4371      [16] = diff.MARK_DELETION
4372    }, {['three'] = diff.INDIC_DELETION, ['will'] = diff.INDIC_DELETION}, {[11] = ' \n'})
4373    verify(buffer2, {
4374      [12] = diff.MARK_ADDITION,
4375      [13] = diff.MARK_ADDITION,
4376      [14] = diff.MARK_MODIFICATION,
4377      [16] = diff.MARK_MODIFICATION,
4378      [17] = diff.MARK_MODIFICATION
4379    }, {
4380      ['four'] = diff.INDIC_ADDITION,
4381      ['have'] = diff.INDIC_ADDITION,
4382      ['d'] = diff.INDIC_ADDITION
4383    }, {[17] = ' '})
4384  end
4385  verify_first_merge()
4386  -- Undo, merge left to right, and verify.
4387  buffer1:undo()
4388  buffer1:goto_line(1)
4389  assert_equal(buffer1:line_from_position(buffer1.current_pos), 1)
4390  diff.merge()
4391  assert(buffer2:get_line(1):find('^this'), 'did not merge from left to right')
4392  verify_first_merge()
4393
4394  if CURSES then goto curses_skip end do -- TODO: curses chokes trying to automate this
4395
4396  -- Go to next difference, merge second block right to left, and verify.
4397  diff.goto_change(true)
4398  assert_equal(buffer1:line_from_position(buffer1.current_pos), 11)
4399  ui.update()
4400  diff.merge(true)
4401  assert(buffer1:get_line(12):find('^%('), 'did not merge from right to left')
4402  for i = 12, 13 do assert_equal(buffer1:get_line(i), buffer2:get_line(i)) end
4403  verify(buffer1, {
4404    [14] = diff.MARK_MODIFICATION,
4405    [16] = diff.MARK_MODIFICATION,
4406    [17] = diff.MARK_MODIFICATION,
4407    [18] = diff.MARK_DELETION
4408  }, {['three'] = diff.INDIC_DELETION, ['will'] = diff.INDIC_DELETION}, {})
4409  verify(buffer2, {
4410    [14] = diff.MARK_MODIFICATION,
4411    [16] = diff.MARK_MODIFICATION,
4412    [17] = diff.MARK_MODIFICATION
4413  }, {
4414    ['four'] = diff.INDIC_ADDITION,
4415    ['have'] = diff.INDIC_ADDITION,
4416    ['d'] = diff.INDIC_ADDITION
4417  }, {[17] = ' '})
4418  -- Undo, merge left to right, and verify.
4419  buffer1:undo()
4420  buffer1:goto_line(11)
4421  assert_equal(buffer1:line_from_position(buffer1.current_pos), 11)
4422  diff.merge()
4423  assert(buffer2:get_line(12):find('^be changed'), 'did not merge from left to right')
4424  verify(buffer1, {
4425    [12] = diff.MARK_MODIFICATION,
4426    [14] = diff.MARK_MODIFICATION,
4427    [15] = diff.MARK_MODIFICATION,
4428    [16] = diff.MARK_DELETION
4429  }, {['three'] = diff.INDIC_DELETION, ['will'] = diff.INDIC_DELETION}, {})
4430  verify(buffer2, {
4431    [12] = diff.MARK_MODIFICATION,
4432    [14] = diff.MARK_MODIFICATION,
4433    [15] = diff.MARK_MODIFICATION
4434  }, {
4435    ['four'] = diff.INDIC_ADDITION,
4436    ['have'] = diff.INDIC_ADDITION,
4437    ['d'] = diff.INDIC_ADDITION
4438  }, {[15] = ' '})
4439
4440  -- Already on next difference; merge third block from right to left, and
4441  -- verify.
4442  assert_equal(buffer1:line_from_position(buffer1.current_pos), 12)
4443  diff.merge(true)
4444  assert(buffer1:get_line(12):find('into four'), 'did not merge from right to left')
4445  assert_equal(buffer1:get_line(12), buffer2:get_line(12))
4446  local function verify_third_merge()
4447    verify(buffer1, {
4448      [14] = diff.MARK_MODIFICATION,
4449      [15] = diff.MARK_MODIFICATION,
4450      [16] = diff.MARK_DELETION
4451    }, {['will'] = diff.INDIC_DELETION}, {})
4452    verify(buffer2, {
4453      [14] = diff.MARK_MODIFICATION,
4454      [15] = diff.MARK_MODIFICATION
4455    }, {['have'] = diff.INDIC_ADDITION, ['d'] = diff.INDIC_ADDITION}, {[15] = ' '})
4456  end
4457  verify_third_merge()
4458  -- Undo, merge left to right, and verify.
4459  buffer1:undo()
4460  buffer1:goto_line(12)
4461  assert_equal(buffer1:line_from_position(buffer1.current_pos), 12)
4462  diff.merge()
4463  assert(buffer2:get_line(12):find('into three'), 'did not merge from left to right')
4464  verify_third_merge()
4465
4466  -- Go to next difference, merge fourth block from right to left, and verify.
4467  diff.goto_change(true)
4468  assert_equal(buffer1:line_from_position(buffer1.current_pos), 14)
4469  diff.merge(true)
4470  assert(buffer1:get_line(14):find('have'), 'did not merge from right to left')
4471  local function verify_fourth_merge()
4472    for i = 14, 15 do assert_equal(buffer1:get_line(i), buffer2:get_line(i)) end
4473    verify(buffer1, {[16] = diff.MARK_DELETION}, {}, {})
4474    verify(buffer2, {}, {}, {[15] = ' '})
4475  end
4476  verify_fourth_merge()
4477  -- Undo, merge left to right, and verify.
4478  buffer1:undo()
4479  buffer1:goto_line(14)
4480  assert_equal(buffer1:line_from_position(buffer1.current_pos), 14)
4481  diff.merge()
4482  assert(buffer2:get_line(14):find('will'), 'did not merge from left to right')
4483  verify_fourth_merge()
4484
4485  -- Go to next difference, merge fifth block from right to left, and verify.
4486  diff.goto_change(true)
4487  assert_equal(buffer1:line_from_position(buffer1.current_pos), 16)
4488  diff.merge(true)
4489  assert(buffer1:get_line(16):find('^\n'), 'did not merge from right to left')
4490  local function verify_fifth_merge()
4491    assert_equal(buffer1.length, buffer2.length)
4492    for i = 1, buffer1.length do
4493      assert_equal(buffer1:get_line(i), buffer2:get_line(i))
4494    end
4495    verify(buffer1, {}, {}, {})
4496    verify(buffer2, {}, {}, {})
4497  end
4498  verify_fifth_merge()
4499  -- Undo, merge left to right, and verify.
4500  buffer1:undo()
4501  buffer1:goto_line(16)
4502  assert_equal(buffer1:line_from_position(buffer1.current_pos), 16)
4503  diff.merge()
4504  assert(buffer2:get_line(16):find('^%('), 'did not merge from left to right')
4505  verify_fifth_merge()
4506
4507  -- Test scroll synchronization.
4508  _VIEWS[1].x_offset = 50
4509  ui.update()
4510  assert_equal(_VIEWS[2].x_offset, _VIEWS[1].x_offset)
4511  _VIEWS[1].x_offset = 0
4512  -- TODO: test vertical synchronization
4513
4514  end ::curses_skip::
4515  textadept.menu.menubar[_L['Tools']][_L['Compare Files']][_L['Stop Comparing']][2]()
4516  ui.goto_view(_VIEWS[#_VIEWS])
4517  buffer:close(true)
4518  ui.goto_view(-1)
4519  view:unsplit()
4520  buffer:close(true)
4521  -- Make sure nothing bad happens.
4522  diff.goto_change()
4523  diff.merge()
4524end
4525
4526function test_file_diff_interactive()
4527  local diff = require('file_diff')
4528  diff.start(_HOME .. '/test/modules/file_diff/1')
4529  assert_equal(#_VIEWS, 2)
4530  textadept.menu.menubar[_L['Tools']][_L['Compare Files']][_L['Stop Comparing']][2]()
4531  local different_files = _VIEWS[1].buffer.filename ~= _VIEWS[2].buffer.filename
4532  ui.goto_view(1)
4533  buffer:close(true)
4534  view:unsplit()
4535  if different_files then buffer:close(true) end
4536end
4537
4538function test_spellcheck()
4539  local spellcheck = require('spellcheck')
4540  local SPELLING_ID = 1 -- not accessible
4541  buffer:new()
4542  buffer:add_text('-- foo bar\nbaz = "quux"')
4543
4544  -- Test background highlighting.
4545  spellcheck.check_spelling()
4546  local function get_misspellings()
4547    local misspellings = {}
4548    local s = buffer:indicator_end(spellcheck.INDIC_SPELLING, 1)
4549    local e = buffer:indicator_end(spellcheck.INDIC_SPELLING, s)
4550    while e > s do
4551      misspellings[#misspellings + 1] = buffer:text_range(s, e)
4552      s = buffer:indicator_end(spellcheck.INDIC_SPELLING, e)
4553      e = buffer:indicator_end(spellcheck.INDIC_SPELLING, s)
4554    end
4555    return misspellings
4556  end
4557  assert_equal(get_misspellings(), {'foo', 'baz', 'quux'})
4558  buffer:set_lexer('lua')
4559  spellcheck.check_spelling()
4560  assert_equal(get_misspellings(), {'foo', 'quux'})
4561
4562  -- Test interactive parts.
4563  spellcheck.check_spelling(true)
4564  assert(buffer:auto_c_active(), 'no misspellings')
4565  local s, e = buffer.current_pos, buffer:word_end_position(buffer.current_pos)
4566  assert_equal(buffer:text_range(s, e), 'foo')
4567  buffer:cancel()
4568  events.emit(events.USER_LIST_SELECTION, SPELLING_ID, 'goo', s)
4569  assert_equal(buffer:text_range(s, e), 'goo')
4570  ui.update()
4571  if CURSES then spellcheck.check_spelling() end -- not needed when interactive
4572  spellcheck.check_spelling(true)
4573  assert(buffer:auto_c_active(), 'spellchecker not active')
4574  s, e = buffer.current_pos, buffer:word_end_position(buffer.current_pos)
4575  assert_equal(buffer:text_range(s, e), 'quux')
4576  buffer:cancel()
4577  events.emit(events.INDICATOR_CLICK, s)
4578  assert(buffer:auto_c_active(), 'spellchecker not active')
4579  buffer:cancel()
4580  events.emit(events.USER_LIST_SELECTION, 1, '(Ignore)', s)
4581  assert_equal(get_misspellings(), {})
4582  spellcheck.check_spelling(true)
4583  assert(not buffer:auto_c_active(), 'misspellings')
4584
4585  -- TODO: test add.
4586
4587  buffer:close(true)
4588end
4589
4590function test_spellcheck_encodings()
4591  local spellcheck = require('spellcheck')
4592  local SPELLING_ID = 1 -- not accessible
4593  buffer:new()
4594
4595  -- Test UTF-8 dictionary and caret placement.
4596  buffer:set_text(' multiumesc')
4597  spellcheck.load('ro_RO')
4598  spellcheck.check_spelling()
4599  events.emit(events.INDICATOR_CLICK, 8)
4600  assert_equal(buffer.auto_c_current_text, 'mulțumesc')
4601  ui.update()
4602  events.emit(
4603    events.USER_LIST_SELECTION, SPELLING_ID, buffer.auto_c_current_text,
4604    buffer.current_pos)
4605  assert_equal(buffer:get_text(), ' mulțumesc')
4606  assert_equal(buffer.current_pos, 9)
4607
4608  -- Test ISO8859-1 dictionary with different buffer encodings.
4609  for _, encoding in pairs{'UTF-8', 'ISO8859-1', 'CP1252'} do
4610    buffer:clear_all()
4611    buffer:set_encoding(encoding)
4612    buffer:set_text('schoen')
4613    ui.update()
4614    spellcheck.load('de_DE')
4615    spellcheck.check_spelling(true)
4616    assert_equal(buffer.auto_c_current_text, 'schön')
4617    events.emit(
4618      events.USER_LIST_SELECTION, SPELLING_ID, buffer.auto_c_current_text,
4619      buffer.current_pos)
4620    assert_equal(buffer:get_text():iconv(encoding, 'UTF-8'), string.iconv('schön', encoding, 'UTF-8'))
4621    ui.update()
4622    spellcheck.check_spelling()
4623    assert_equal(buffer:indicator_end(spellcheck.INDIC_SPELLING, 1), 1)
4624  end
4625
4626  buffer:close(true)
4627end
4628
4629function test_spellcheck_load_interactive()
4630  require('spellcheck')
4631  textadept.menu.menubar[_L['Tools']][_L['Spelling']][_L['Load Dictionary...']][2]()
4632end
4633
4634-- Load buffer and view API from their respective LuaDoc files.
4635local function load_buffer_view_props()
4636  local buffer_props, view_props = {}, {}
4637  for name, props in pairs{buffer = buffer_props, view = view_props} do
4638    for line in io.lines(string.format('%s/core/.%s.luadoc', _HOME, name)) do
4639      if line:find('@field') then
4640        props[line:match('@field ([%w_]+)')] = true
4641      elseif line:find('^function') then
4642        props[line:match('^function ([%w_]+)')] = true
4643      end
4644    end
4645  end
4646  return buffer_props, view_props
4647end
4648
4649local function check_property_usage(filename, buffer_props, view_props)
4650  print(string.format('Processing file "%s"', filename:gsub(_HOME, '')))
4651  local line_num, count = 1, 0
4652  for line in io.lines(filename) do
4653    for pos, id, prop in line:gmatch('()([%w_]+)[.:]([%w_]+)') do
4654      if id == 'M' or id == 'f' or id == 'p' or id == 'lexer' or id == 'spawn_proc' then goto continue end
4655      if id == 'textadept' and prop == 'MARK_BOOKMARK' then goto continue end
4656      if (id == 'ui' or id == 'split') and prop == 'size' then goto continue end
4657      if id == 'keys' and prop == 'home' then goto continue end
4658      if id == 'Rout' and prop == 'save' then goto continue end
4659      if id == 'detail' and (prop == 'filename' or prop == 'column') then goto continue end
4660      if (id == 'placeholder' or id == 'ph') and prop == 'length' then goto continue end
4661      if id == 'client' and prop == 'close' then goto continue end
4662      if (id == 'Foo' or id == 'Array' or id == 'Server') and prop == 'new' then goto continue end
4663      if buffer_props[prop] then
4664        assert(
4665          id == 'buffer' or id == 'buf' or id == 'buffer1' or id == 'buffer2',
4666          'line %d:%d: "%s" should be a buffer property', line_num, pos, prop)
4667        count = count + 1
4668      elseif view_props[prop] then
4669        assert(
4670          id == 'view', 'line %d:%d: "%s" should be a view property', line_num,
4671          pos, prop)
4672        count = count + 1
4673      end
4674      ::continue::
4675    end
4676    line_num = line_num + 1
4677  end
4678  print(string.format('Checked %d buffer/view property usages.', count))
4679end
4680
4681function test_buffer_view_usage()
4682  local buffer_props, view_props = load_buffer_view_props()
4683  local filter = {
4684    '.lua', '.luadoc', '!/lexers', '!/modules/lsp/dkjson.lua',
4685    '!/modules/lua/lua.luadoc', '!/modules/debugger/lua/mobdebug.lua',
4686    '!/modules/yaml/lyaml.lua', '!/scripts', '!/src'
4687  }
4688  for filename in lfs.walk(_HOME, filter) do
4689    check_property_usage(filename, buffer_props, view_props)
4690  end
4691end
4692
4693--------------------------------------------------------------------------------
4694
4695assert(not WIN32 and not OSX, 'Test suite currently only runs on Linux')
4696
4697local TEST_OUTPUT_BUFFER = '[Test Output]'
4698function print(...) ui._print(TEST_OUTPUT_BUFFER, ...) end
4699-- Clean up after a previously failed test.
4700local function cleanup()
4701  while #_BUFFERS > 1 do
4702    if buffer._type == TEST_OUTPUT_BUFFER then view:goto_buffer(1) end
4703    buffer:close(true)
4704  end
4705  while view:unsplit() do end
4706end
4707
4708-- Determines whether or not to run the test whose name is string *name*.
4709-- If no arg patterns are provided, returns true.
4710-- If only inclusive arg patterns are provided, returns true if *name* matches
4711-- at least one of those patterns.
4712-- If only exclusive arg patterns are provided ('-' prefix), returns true if
4713-- *name* does not match any of them.
4714-- If both inclusive and exclusive arg patterns are provided, returns true if
4715-- *name* matches at least one of the inclusive ones, but not any of the
4716-- exclusive ones.
4717-- @param name Name of the test to check for inclusion.
4718-- @return true or false
4719local function include_test(name)
4720  if #arg == 0 then return true end
4721  local include, includes, excludes = false, false, false
4722  for _, patt in ipairs(arg) do
4723    if patt:find('^%-') then
4724      if name:find(patt:sub(2)) then return false end
4725      excludes = true
4726    else
4727      if name:find(patt) then include = true end
4728      includes = true
4729    end
4730  end
4731  return include or not includes and excludes
4732end
4733
4734local tests = {}
4735for k in pairs(_ENV) do
4736  if k:find('^test_') and include_test(k) then
4737    tests[#tests + 1] = k
4738  end
4739end
4740table.sort(tests)
4741
4742print('Starting test suite')
4743
4744local tests_run, tests_failed, tests_failed_expected = 0, 0, 0
4745
4746for i = 1, #tests do
4747  cleanup()
4748  assert_equal(#_BUFFERS, 1)
4749  assert_equal(#_VIEWS, 1)
4750
4751  _ENV = setmetatable({}, {__index = _ENV})
4752  local name, f, attempts = tests[i], _ENV[tests[i]], 1
4753  print(string.format('Running %s', name))
4754  ui.update()
4755  local ok, errmsg = xpcall(f, function(errmsg)
4756    local fail = not expected_failures[f] and 'Failed!' or 'Expected failure.'
4757    return string.format('%s %s', fail, debug.traceback(errmsg, 3))
4758  end)
4759  ui.update()
4760  if not errmsg then
4761    if #_BUFFERS > 1 then
4762      ok, errmsg = false, 'Failed! Test did not close the buffer(s) it created'
4763    elseif #_VIEWS > 1 then
4764      ok, errmsg = false, 'Failed! Test did not unsplit the view(s) it created'
4765    elseif expected_failures[f] then
4766      ok, errmsg = false, 'Failed! Test should have failed'
4767      expected_failures[f] = nil
4768    end
4769  end
4770  print(ok and 'Passed.' or errmsg)
4771
4772  tests_run = tests_run + 1
4773  if not ok then
4774    tests_failed = tests_failed + 1
4775    if expected_failures[f] then
4776      tests_failed_expected = tests_failed_expected + 1
4777    end
4778  end
4779end
4780
4781print(string.format('%d tests run, %d unexpected failures, %d expected failures', tests_run, tests_failed - tests_failed_expected, tests_failed_expected))
4782
4783-- Note: stock luacov crashes on hook.lua lines 51 and 58 every other run.
4784-- `file.max` and `file.max_hits` are both `nil`, so change comparisons to be
4785-- `(file.max or 0)` and `(file.max_hits or 0)`, respectively.
4786if package.loaded['luacov'] then
4787  require('luacov').save_stats()
4788  os.execute('luacov')
4789  local f = assert(io.open('luacov.report.out'))
4790  buffer:append_text(f:read('a'):match('\nSummary.+$'))
4791  f:close()
4792else
4793  buffer:new_line()
4794  buffer:append_text('No LuaCov coverage to report.')
4795end
4796buffer:set_save_point()
4797