1 // Copyright 2007-2021 Mitchell. See LICENSE.
2 
3 #if __linux__
4 #define _XOPEN_SOURCE 500 // for readlink from unistd.h
5 #endif
6 
7 // Library includes.
8 #include <errno.h>
9 #include <limits.h> // for MB_LEN_MAX
10 #include <locale.h>
11 #include <iconv.h>
12 #include <math.h> // for fmax
13 //#include <stdarg.h>
14 #include <stdbool.h>
15 //#include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #if __linux__
19 //#include <unistd.h>
20 #elif _WIN32
21 #include <windows.h>
22 #include <fcntl.h> // for _open_osfhandle, _O_RDONLY
23 #define main main_
24 #elif __APPLE__
25 #include <mach-o/dyld.h> // for _NSGetExecutablePath
26 #elif (__FreeBSD__ || __NetBSD__ || __OpenBSD__)
27 #include <sys/types.h>
28 #include <sys/sysctl.h>
29 #endif
30 #if GTK
31 #include <gdk/gdkkeysyms.h>
32 #include <gtk/gtk.h>
33 #if __APPLE__
34 #include <gtkmacintegration/gtkosxapplication.h>
35 #endif
36 #elif CURSES
37 #if !_WIN32
38 #include <signal.h>
39 #include <sys/ioctl.h>
40 //#include <sys/select.h>
41 #include <sys/time.h>
42 //#include <termios.h>
43 #else
44 #undef main
45 #endif
46 #include <curses.h>
47 #endif
48 
49 // External dependency includes.
50 #include "gtdialog.h"
51 //#include "lua.h"
52 #include "lualib.h"
53 #include "lauxlib.h"
54 #include "Scintilla.h"
55 #include "LexLPeg.h"
56 #if GTK
57 #include "ScintillaWidget.h"
58 #elif CURSES
59 #include "ScintillaCurses.h"
60 #include "cdk_int.h"
61 #include "termkey.h"
62 #endif
63 
64 #if GTK
65 typedef GtkWidget Scintilla;
66 // Translate GTK 2.x API to GTK 3.0 for compatibility.
67 #if GTK_CHECK_VERSION(3,0,0)
68 #define gtk_combo_box_entry_new_with_model(m,_) \
69   gtk_combo_box_new_with_model_and_entry(m)
70 #define gtk_combo_box_entry_set_text_column gtk_combo_box_set_entry_text_column
71 #define GTK_COMBO_BOX_ENTRY GTK_COMBO_BOX
72 #endif
73 #if !_WIN32
74 #define ID "textadept.editor"
75 #else
76 #define ID "\\\\.\\pipe\\textadept.editor"
77 // Win32 single-instance functionality.
78 #define g_application_command_line_get_arguments(_,__) \
79   g_strsplit(buf, "\n", 0); argc = g_strv_length(argv)
80 #define g_application_command_line_get_cwd(_) argv[0]
81 #define g_application_register(_,__,___) true
82 #define g_application_get_is_remote(_) \
83   (WaitNamedPipe(ID, NMPWAIT_WAIT_FOREVER) != 0)
84 #define gtk_main() \
85   HANDLE pipe = NULL, thread = NULL; \
86   if (!g_application_get_is_remote(app)) \
87     pipe = CreateNamedPipe( \
88       ID, PIPE_ACCESS_INBOUND, PIPE_WAIT, 1, 0, 0, INFINITE, NULL), \
89       thread = CreateThread(NULL, 0, &pipe_listener, pipe, 0, NULL); \
90   gtk_main(); \
91   if (pipe && thread) \
92     TerminateThread(thread, 0), CloseHandle(thread), CloseHandle(pipe);
93 #endif
94 #elif CURSES
95 typedef void Scintilla;
96 #endif
97 #define set_metatable(l, n, name, __index, __newindex) \
98   if (luaL_newmetatable(l, name)) \
99     lua_pushcfunction(l, __index), lua_setfield(l, -2, "__index"), \
100       lua_pushcfunction(l, __newindex), lua_setfield(l, -2, "__newindex"); \
101   lua_setmetatable(l, n > 0 ? n : n - 1);
102 
103 static char *textadept_home, *platform;
104 
105 // User interface objects and related macros for GTK and curses
106 // interoperability.
107 static Scintilla *focused_view, *dummy_view, *command_entry;
108 #if GTK
109 // GTK window.
110 static GtkWidget *window, *menubar, *tabbar, *statusbar[2];
111 static GtkAccelGroup *accel;
112 #if __APPLE__
113 static GtkosxApplication *osxapp;
114 #endif
115 typedef GtkWidget Pane;
116 #define SS(view, msg, w, l) scintilla_send_message(SCINTILLA(view), msg, w, l)
117 #define focus_view(view) gtk_widget_grab_focus(view)
118 #define scintilla_delete(view) gtk_widget_destroy(view)
119 // GTK find & replace pane.
120 static GtkWidget *findbox, *find_entry, *repl_entry, *find_label, *repl_label;
121 #define find_text gtk_entry_get_text(GTK_ENTRY(find_entry))
122 #define repl_text gtk_entry_get_text(GTK_ENTRY(repl_entry))
123 #define set_entry_text(entry, text) gtk_entry_set_text( \
124   GTK_ENTRY(entry == find_text ? find_entry : repl_entry), text)
125 typedef GtkWidget *FindButton;
126 static FindButton find_next, find_prev, replace, replace_all;
127 static GtkWidget *match_case, *whole_word, *regex, *in_files;
128 typedef GtkListStore ListStore;
129 static ListStore *find_history, *repl_history;
130 #define checked(w) gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w))
131 #define toggle(w, on) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), on)
132 #define set_label_text(l, t) gtk_label_set_text_with_mnemonic(GTK_LABEL(l), t)
133 #define set_button_label(b, l) gtk_button_set_label(GTK_BUTTON(b), l)
134 #define set_option_label(o, _, l) gtk_button_set_label(GTK_BUTTON(o), l)
135 #define find_active(w) gtk_widget_get_visible(w)
136 // GTK command entry.
137 #define command_entry_active gtk_widget_has_focus(command_entry)
138 #elif CURSES
139 // curses window.
140 typedef struct Pane {
141   int y, x, rows, cols, split_size; // dimensions
142   enum {SINGLE, VSPLIT, HSPLIT} type; // pane type
143   WINDOW *win; // either the Scintilla curses window or the split bar's window
144   Scintilla *view; // Scintilla view for a non-split view
145   struct Pane *child1, *child2; // each pane in a split view
146 } Pane; // Pane implementation based on code by Chris Emerson.
147 static Pane *pane;
148 TermKey *ta_tk; // global for CDK use
149 #define SS(view, msg, w, l) scintilla_send_message(view, msg, w, l)
150 #define focus_view(view) ( \
151   focused_view ? SS(focused_view, SCI_SETFOCUS, 0, 0) : 0, \
152   SS(view, SCI_SETFOCUS, 1, 0))
153 // curses find & replace pane.
154 static CDKSCREEN *findbox;
155 static CDKENTRY *find_entry, *repl_entry, *focused_entry;
156 static char *find_text, *repl_text, *find_label, *repl_label;
157 #define set_entry_text(entry, text) copyfree(&entry, text)
158 typedef enum {find_next, replace, find_prev, replace_all} FindButton;
159 static bool find_options[4], *match_case = &find_options[0],
160   *whole_word = &find_options[1], *regex = &find_options[2],
161   *in_files = &find_options[3];
162 static char *button_labels[4], *option_labels[4];
163 typedef char *ListStore;
164 static ListStore find_history[10], repl_history[10];
165 #define checked(find_option) *find_option
166 // Use pointer arithmetic to highlight/unhighlight options as necessary.
167 #define toggle(o, on) do { \
168   if (*o != on) *o = on, option_labels[o - match_case] += *o ? -4 : 4; \
169 } while (false)
170 #define set_label_text(label, text) copyfree(&label, text)
171 #define set_button_label(button, label) copyfree(&button_labels[button], label)
172 // Prepend "</R>" to each option label because pointer arithmetic will be used
173 // to make the "</R>" visible or invisible (thus highlighting or unhighlighting
174 // the label) depending on whether or not the option is enabled by the user.
175 #define set_option_label(option, i, label) do { \
176   lua_pushstring(L, "</R>"), lua_pushstring(L, label), lua_concat(L, 2); \
177   if (option_labels[i] && !*option) option_labels[i] -= 4; \
178   copyfree(&option_labels[i], lua_tostring(L, -1)); \
179   if (!*option) option_labels[i] += 4; \
180 } while (false)
181 #define find_active(w) (w != NULL)
182 // Curses command entry and statusbar.
183 static bool command_entry_active;
184 int statusbar_length[2];
185 #endif
186 
187 // Lua objects.
188 static lua_State *lua;
189 #if CURSES
190 static bool quitting;
191 #endif
192 static bool initing, closing, show_tabs = true, tab_sync, dialog_active;
193 enum {SVOID, SINT, SLEN, SINDEX, SCOLOR, SBOOL, SKEYMOD, SSTRING, SSTRINGRET};
194 
195 // Forward declarations.
196 static void new_buffer(sptr_t);
197 static Scintilla *new_view(sptr_t);
198 static bool init_lua(lua_State *, int, char **, bool);
199 LUALIB_API int luaopen_lpeg(lua_State *), luaopen_lfs(lua_State *);
200 LUALIB_API int os_spawn_pushfds(lua_State *), os_spawn_readfds(lua_State *);
201 
202 /**
203  * Emits an event.
204  * @param L The Lua state.
205  * @param name The event name.
206  * @param ... Arguments to pass with the event. Each pair of arguments should be
207  *   a Lua type followed by the data value itself. For LUA_TLIGHTUSERDATA and
208  *   LUA_TTABLE types, push the data values to the stack and give the value
209  *   returned by luaL_ref(); luaL_unref() will be called appropriately. The list
210  *   must be terminated with a -1.
211  * @return true or false depending on the boolean value returned by the event
212  *   handler, if any.
213  */
emit(lua_State * L,const char * name,...)214 static bool emit(lua_State *L, const char *name, ...) {
215   bool ret = false;
216   if (lua_getglobal(L, "events") != LUA_TTABLE) return (lua_pop(L, 1), ret);
217   if (lua_getfield(L, -1, "emit") != LUA_TFUNCTION) return (lua_pop(L, 2), ret);
218   lua_pushstring(L, name);
219   int n = 1;
220   va_list ap;
221   va_start(ap, name);
222   for (int type = va_arg(ap, int); type != -1; type = va_arg(ap, int), n++)
223     switch (type) {
224       case LUA_TBOOLEAN: lua_pushboolean(L, va_arg(ap, int)); break;
225       case LUA_TNUMBER: lua_pushinteger(L, va_arg(ap, int)); break;
226       case LUA_TSTRING: lua_pushstring(L, va_arg(ap, char *)); break;
227       case LUA_TLIGHTUSERDATA: case LUA_TTABLE: {
228         sptr_t arg = va_arg(ap, sptr_t);
229         lua_rawgeti(L, LUA_REGISTRYINDEX, arg);
230         luaL_unref(L, LUA_REGISTRYINDEX, arg);
231         break;
232       } default: lua_pushnil(L);
233     }
234   va_end(ap);
235   if (lua_pcall(L, n, 1, 0) != LUA_OK) {
236     // An error occurred within `events.emit()` itself, not an event handler.
237     const char *argv[] = {"--title", "Error", "--text", lua_tostring(L, -1)};
238     free(gtdialog(GTDIALOG_TEXTBOX, 4, argv));
239     return (lua_pop(L, 2), ret); // result, events
240   } else ret = lua_toboolean(L, -1);
241   return (lua_pop(L, 2), ret); // result, events
242 }
243 
244 #if GTK
245 /** Processes a remote Textadept's command line arguments. */
process(GApplication * _,GApplicationCommandLine * line,void * buf)246 static int process(GApplication *_, GApplicationCommandLine *line, void *buf) {
247   if (!lua) return 0; // only process argv for secondary/remote instances
248   int argc = 0;
249   char **argv = g_application_command_line_get_arguments(line, &argc);
250   if (argc > 1) {
251     lua_newtable(lua);
252     lua_pushstring(lua, g_application_command_line_get_cwd(line)),
253       lua_rawseti(lua, -2, -1);
254     while (--argc) lua_pushstring(lua, argv[argc]), lua_rawseti(lua, -2, argc);
255     emit(lua, "command_line", LUA_TTABLE, luaL_ref(lua, LUA_REGISTRYINDEX), -1);
256   }
257   g_strfreev(argv);
258   return (gtk_window_present(GTK_WINDOW(window)), 0);
259 }
260 #endif
261 
262 #if CURSES
263 /**
264  * Copies the given value to the given string after freeing that string's
265  * existing value (if any).
266  * The given string must be freed when finished.
267  * @param s The address of the string to copy value to.
268  * @param value String value to copy. It may be freed immediately.
269  */
copyfree(char ** s,const char * value)270 static void copyfree(char **s, const char *value) {
271   if (*s) free(*s);
272   *s = strcpy(malloc(strlen(value) + 1), value);
273 }
274 #endif
275 
276 /**
277  * Adds the given text to the find/replace history list if it is not at the top.
278  * @param store The ListStore to add the text to.
279  * @param text The text to add.
280  */
add_to_history(ListStore * store,const char * text)281 static void add_to_history(ListStore* store, const char *text) {
282 #if GTK
283   // Note: GtkComboBoxEntry key navigation behaves contrary to command line
284   // history navigation. Down cycles from newer to older, and up cycles from
285   // older to newer. In order to mimic traditional command line history
286   // navigation, append to the list instead of prepending to it.
287   int n = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), NULL);
288   GtkTreeIter iter;
289   if (n > 9)
290     gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter),
291       gtk_list_store_remove(store, &iter), n--; // keep 10 items
292   char *last_text = NULL;
293   if (n > 0)
294     gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(store), &iter, NULL, n - 1),
295       gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, 0, &last_text, -1);
296   if (!last_text || strcmp(text, last_text) != 0)
297     gtk_list_store_append(store, &iter),
298       gtk_list_store_set(store, &iter, 0, text, -1);
299   g_free(last_text);
300 #elif CURSES
301   if (!text || (store[0] && strcmp(text, store[0]) == 0)) return;
302   if (store[9]) free(store[9]);
303   for (int i = 9; i > 0; i--) store[i] = store[i - 1];
304   store[0] = NULL, copyfree(&store[0], text);
305 #endif
306 }
307 
308 /** Signal for a find box button click. */
find_clicked(FindButton button,void * L)309 static void find_clicked(FindButton button, void *L) {
310   if (find_text && !*find_text) return;
311   if (button == find_next || button == find_prev)
312     add_to_history(find_history, find_text);
313   else
314     add_to_history(repl_history, repl_text);
315   if (button == find_next || button == find_prev)
316     emit(
317       L, "find", LUA_TSTRING, find_text, LUA_TBOOLEAN, button == find_next, -1);
318   else if (button == replace) {
319     emit(L, "replace", LUA_TSTRING, repl_text, -1);
320     emit(L, "find", LUA_TSTRING, find_text, LUA_TBOOLEAN, true, -1);
321   } else if (button == replace_all)
322     emit(L, "replace_all", LUA_TSTRING, find_text, LUA_TSTRING, repl_text, -1);
323 }
324 
325 /** `find.find_next()` Lua function. */
click_find_next(lua_State * L)326 static int click_find_next(lua_State *L) {
327   return (find_clicked(find_next, L), 0);
328 }
329 
330 /** `find.find_prev()` Lua function. */
click_find_prev(lua_State * L)331 static int click_find_prev(lua_State *L) {
332   return (find_clicked(find_prev, L), 0);
333 }
334 
335 /** `find.replace()` Lua function. */
click_replace(lua_State * L)336 static int click_replace(lua_State *L) {
337   return (find_clicked(replace, L), 0);
338 }
339 
340 /** `find.replace_all()` Lua function. */
click_replace_all(lua_State * L)341 static int click_replace_all(lua_State *L) {
342   return (find_clicked(replace_all, L), 0);
343 }
344 
345 #if CURSES
346 /**
347  * Redraws an entire pane and its children.
348  * @param pane The pane to redraw.
349  */
refresh_pane(Pane * pane)350 static void refresh_pane(Pane *pane) {
351   if (pane->type == VSPLIT) {
352     mvwvline(pane->win, 0, 0, 0, pane->rows), wrefresh(pane->win);
353     refresh_pane(pane->child1), refresh_pane(pane->child2);
354   } else if (pane->type == HSPLIT) {
355     mvwhline(pane->win, 0, 0, 0, pane->cols), wrefresh(pane->win);
356     refresh_pane(pane->child1), refresh_pane(pane->child2);
357   } else scintilla_noutrefresh(pane->view);
358 }
359 
360 /** Refreshes the entire screen. */
refresh_all()361 static void refresh_all() {
362   refresh_pane(pane);
363   if (command_entry_active) scintilla_noutrefresh(command_entry);
364   refresh();
365 }
366 
367 /**
368  * Signal for Find/Replace entry keypress.
369  * For tab keys, toggle through find/replace buttons.
370  * For ^N and ^P keys, cycle through find/replace history.
371  * For F1-F4 keys, toggle the respective search option.
372  * For up and down keys, toggle entry focus.
373  * Otherwise, emit events for entry text changes.
374  */
find_keypress(EObjectType _,void * object,void * data,chtype key)375 static int find_keypress(EObjectType _, void *object, void *data, chtype key) {
376   CDKENTRY *entry = (CDKENTRY *)object;
377   char *text = getCDKEntryValue(entry);
378   if (key == KEY_TAB) {
379     CDKBUTTONBOX *box = (CDKBUTTONBOX *)data;
380     FindButton button = entry == find_entry ?
381       (getCDKButtonboxCurrentButton(box) == find_next ? find_prev : find_next) :
382       (getCDKButtonboxCurrentButton(box) == replace ? replace_all : replace);
383     setCDKButtonboxCurrentButton(box, button);
384     drawCDKButtonbox(box, false), drawCDKEntry(entry, false);
385   } else if (key == CDK_PREV || key == CDK_NEXT) {
386     ListStore *store = entry == find_entry ? find_history : repl_history;
387     int i;
388     for (i = 9; i >= 0; i--) if (store[i] && strcmp(store[i], text) == 0) break;
389     key == CDK_PREV ? i++ : i--;
390     if (i >= 0 && i <= 9 && store[i])
391       setCDKEntryValue(entry, store[i]), drawCDKEntry(entry, false);
392   } else if (key >= KEY_F(1) && key <= KEY_F(4)) {
393     toggle(&find_options[key - KEY_F(1)], !find_options[key - KEY_F(1)]);
394     // Redraw the optionbox.
395     CDKBUTTONBOX **optionbox = (CDKBUTTONBOX **)data;
396     int width = (*optionbox)->boxWidth - 1;
397     destroyCDKButtonbox(*optionbox);
398     *optionbox = newCDKButtonbox(
399       findbox, RIGHT, TOP, 2, width, NULL, 2, 2, option_labels, 4, A_NORMAL,
400       false, false);
401     drawCDKButtonbox(*optionbox, false);
402   } else if (key == KEY_UP || key == KEY_DOWN) {
403     focused_entry = entry == find_entry ? repl_entry : find_entry;
404     setCDKButtonboxCurrentButton(
405       (CDKBUTTONBOX *)data, focused_entry == find_entry ? find_next : replace);
406     injectCDKEntry(entry, KEY_ENTER); // exit this entry
407   } else if ((!find_text || strcmp(find_text, text) != 0)) {
408     copyfree(&find_text, text);
409     if (emit(lua, "find_text_changed", -1)) refresh_all();
410   }
411   return true;
412 }
413 #endif
414 
415 /** `find.focus()` Lua function. */
focus_find(lua_State * L)416 static int focus_find(lua_State *L) {
417 #if GTK
418   if (!gtk_widget_has_focus(find_entry) && !gtk_widget_has_focus(repl_entry))
419     gtk_widget_show(findbox), gtk_widget_grab_focus(find_entry);
420   else
421     gtk_widget_hide(findbox), gtk_widget_grab_focus(focused_view);
422 #elif CURSES
423   if (findbox) return 0; // already active
424   wresize(scintilla_get_window(focused_view), LINES - 4, COLS);
425   findbox = initCDKScreen(newwin(2, 0, LINES - 3, 0)), eraseCDKScreen(findbox);
426   int b_width = fmax(strlen(button_labels[0]), strlen(button_labels[1])) +
427     fmax(strlen(button_labels[2]), strlen(button_labels[3])) + 3;
428   int o_width = fmax(strlen(option_labels[0]), strlen(option_labels[1])) +
429     fmax(strlen(option_labels[2]), strlen(option_labels[3])) + 3;
430   int l_width = fmax(strlen(find_label), strlen(repl_label));
431   int e_width = COLS - o_width - b_width - l_width - 1;
432   find_entry = newCDKEntry(
433     findbox, l_width - strlen(find_label), TOP, NULL, find_label, A_NORMAL, '_',
434     vMIXED, e_width, 0, 1024, false, false);
435   repl_entry = newCDKEntry(
436     findbox, l_width - strlen(repl_label), BOTTOM, NULL, repl_label, A_NORMAL,
437     '_', vMIXED, e_width, 0, 1024, false, false);
438   CDKBUTTONBOX *buttonbox, *optionbox;
439   buttonbox = newCDKButtonbox(
440     findbox, COLS - o_width - b_width, TOP, 2, b_width, NULL, 2, 2,
441     button_labels, 4, A_REVERSE, false, false);
442   optionbox = newCDKButtonbox(
443     findbox, RIGHT, TOP, 2, o_width, NULL, 2, 2, option_labels, 4, A_NORMAL,
444     false, false);
445   // TODO: ideally no #define here.
446   #define bind(k, d) (bindCDKObject(vENTRY, find_entry, k, find_keypress, d), \
447     bindCDKObject(vENTRY, repl_entry, k, find_keypress, d))
448   bind(KEY_TAB, buttonbox), bind(CDK_NEXT, NULL), bind(CDK_PREV, NULL);
449   for (int i = 1; i <= 4; i++) bind(KEY_F(i), &optionbox);
450   bind(KEY_DOWN, buttonbox), bind(KEY_UP, buttonbox);
451   setCDKEntryValue(find_entry, find_text);
452   setCDKEntryValue(repl_entry, repl_text);
453   setCDKEntryPostProcess(find_entry, find_keypress, NULL);
454   char *clipboard = scintilla_get_clipboard(focused_view, NULL);
455   GPasteBuffer = copyChar(clipboard); // set the CDK paste buffer
456   curs_set(1);
457   refreshCDKScreen(findbox), activateCDKEntry(focused_entry = find_entry, NULL);
458   while (focused_entry->exitType == vNORMAL ||
459          focused_entry->exitType == vNEVER_ACTIVATED) {
460     copyfree(&find_text, getCDKEntryValue(find_entry));
461     copyfree(&repl_text, getCDKEntryValue(repl_entry));
462     if (focused_entry->exitType == vNORMAL)
463       find_clicked(getCDKButtonboxCurrentButton(buttonbox), L), refresh_all();
464     find_entry->exitType = repl_entry->exitType = vNEVER_ACTIVATED;
465     refreshCDKScreen(findbox), activateCDKEntry(focused_entry, NULL);
466   }
467   curs_set(0);
468   // Set Scintilla clipboard with new CDK paste buffer if necessary.
469   if (strcmp(clipboard, GPasteBuffer) != 0)
470     SS(focused_view, SCI_COPYTEXT, strlen(GPasteBuffer), (sptr_t)GPasteBuffer);
471   free(clipboard), free(GPasteBuffer), GPasteBuffer = NULL;
472   destroyCDKEntry(find_entry), destroyCDKEntry(repl_entry);
473   destroyCDKButtonbox(buttonbox), destroyCDKButtonbox(optionbox);
474   delwin(findbox->window), destroyCDKScreen(findbox), findbox = NULL;
475   timeout(0), getch(), timeout(-1); // flush potential extra Escape
476   wresize(scintilla_get_window(focused_view), LINES - 2, COLS);
477 #endif
478   return 0;
479 }
480 
481 /** `find.__index` Lua metamethod. */
find_index(lua_State * L)482 static int find_index(lua_State *L) {
483   const char *key = lua_tostring(L, 2);
484   if (strcmp(key, "find_entry_text") == 0)
485     lua_pushstring(L, find_text ? find_text : "");
486   else if (strcmp(key, "replace_entry_text") == 0)
487     lua_pushstring(L, repl_text ? repl_text : "");
488   else if (strcmp(key, "match_case") == 0)
489     lua_pushboolean(L, checked(match_case));
490   else if (strcmp(key, "whole_word") == 0)
491     lua_pushboolean(L, checked(whole_word));
492   else if (strcmp(key, "regex") == 0)
493     lua_pushboolean(L, checked(regex));
494   else if (strcmp(key, "in_files") == 0)
495     lua_pushboolean(L, checked(in_files));
496   else if (strcmp(key, "active") == 0)
497     lua_pushboolean(L, find_active(findbox));
498   else
499     lua_rawget(L, 1);
500   return 1;
501 }
502 
503 /** `find.__newindex` Lua metamethod. */
find_newindex(lua_State * L)504 static int find_newindex(lua_State *L) {
505   const char *key = lua_tostring(L, 2);
506   if (strcmp(key, "find_entry_text") == 0)
507     set_entry_text(find_text, lua_tostring(L, 3));
508   else if (strcmp(key, "replace_entry_text") == 0)
509     set_entry_text(repl_text, lua_tostring(L, 3));
510   else if (strcmp(key, "match_case") == 0)
511     toggle(match_case, lua_toboolean(L, -1));
512   else if (strcmp(key, "whole_word") == 0)
513     toggle(whole_word, lua_toboolean(L, -1));
514   else if (strcmp(key, "regex") == 0)
515     toggle(regex, lua_toboolean(L, -1));
516   else if (strcmp(key, "in_files") == 0)
517     toggle(in_files, lua_toboolean(L, -1));
518   else if (strcmp(key, "find_label_text") == 0)
519     set_label_text(find_label, lua_tostring(L, 3));
520   else if (strcmp(key, "replace_label_text") == 0)
521     set_label_text(repl_label, lua_tostring(L, 3));
522   else if (strcmp(key, "find_next_button_text") == 0)
523     set_button_label(find_next, lua_tostring(L, 3));
524   else if (strcmp(key, "find_prev_button_text") == 0)
525     set_button_label(find_prev, lua_tostring(L, 3));
526   else if (strcmp(key, "replace_button_text") == 0)
527     set_button_label(replace, lua_tostring(L, 3));
528   else if (strcmp(key, "replace_all_button_text") == 0)
529     set_button_label(replace_all, lua_tostring(L, 3));
530   else if (strcmp(key, "match_case_label_text") == 0)
531     set_option_label(match_case, 0, lua_tostring(L, 3));
532   else if (strcmp(key, "whole_word_label_text") == 0)
533     set_option_label(whole_word, 1, lua_tostring(L, 3));
534   else if (strcmp(key, "regex_label_text") == 0)
535     set_option_label(regex, 2, lua_tostring(L, 3));
536   else if (strcmp(key, "in_files_label_text") == 0)
537     set_option_label(in_files, 3, lua_tostring(L, 3));
538   else
539     lua_rawset(L, 1);
540   return 0;
541 }
542 
543 /** `command_entry.focus()` Lua function. */
focus_command_entry(lua_State * L)544 static int focus_command_entry(lua_State *L) {
545 #if GTK
546   if (!gtk_widget_get_visible(command_entry))
547     gtk_widget_show(command_entry), gtk_widget_grab_focus(command_entry);
548   else
549     gtk_widget_hide(command_entry), gtk_widget_grab_focus(focused_view);
550 #elif CURSES
551   command_entry_active = !command_entry_active;
552   if (!command_entry_active) SS(command_entry, SCI_SETFOCUS, 0, 0);
553   focus_view(command_entry_active ? command_entry : focused_view);
554 #endif
555   return 0;
556 }
557 
558 /** Runs the work function passed to `ui.dialogs.progressbar()`. */
work(void * L)559 static char *work(void *L) {
560   lua_getfield(L, LUA_REGISTRYINDEX, "ta_workf");
561   if (lua_pcall(L, 0, 2, 0) == LUA_OK) {
562     if (lua_isnil(L, -2)) return (lua_pop(L, 2), NULL); // done
563     if (lua_isnil(L, -1)) lua_pushliteral(L, ""), lua_replace(L, -2);
564     if (lua_isnumber(L, -2) && lua_isstring(L, -1)) {
565       lua_pushliteral(L, " "), lua_insert(L, -2), lua_pushliteral(L, "\n"),
566         lua_concat(L, 4); // "num str\n"
567       char *input = strcpy(malloc(lua_rawlen(L, -1) + 1), lua_tostring(L, -1));
568       return (lua_pop(L, 1), input); // will be freed by gtdialog
569     } else lua_pop(L, 2), lua_pushliteral(L, "invalid return values");
570   }
571   emit(L, "error", LUA_TSTRING, lua_tostring(L, -1), -1);
572   return (lua_pop(L, 1), NULL);
573 }
574 
575 /** `ui.dialog()` Lua function. */
dialog(lua_State * L)576 static int dialog(lua_State *L) {
577   GTDialogType type = gtdialog_type(luaL_checkstring(L, 1));
578   int n = lua_gettop(L) - 1, argc = n;
579   for (int i = 2; i < n + 2; i++)
580     if (lua_istable(L, i)) argc += lua_rawlen(L, i) - 1;
581   if (type == GTDIALOG_PROGRESSBAR)
582     lua_pushnil(L), lua_setfield(L, LUA_REGISTRYINDEX, "ta_workf"), argc--;
583   const char *argv[argc + 1]; // not malloc since luaL_checkstring throws
584   for (int i = 0, j = 2; j < n + 2; j++)
585     if (lua_istable(L, j))
586       for (int k = 1, len = lua_rawlen(L, j); k <= len; lua_pop(L, 1), k++)
587         argv[i++] = (lua_rawgeti(L, j, k), luaL_checkstring(L, -1)); // ^popped
588     else if (lua_isfunction(L, j) && type == GTDIALOG_PROGRESSBAR) {
589       lua_pushvalue(L, j), lua_setfield(L, LUA_REGISTRYINDEX, "ta_workf");
590       gtdialog_set_progressbar_callback(work, L);
591     } else argv[i++] = luaL_checkstring(L, j);
592   argv[argc] = NULL;
593   char *out;
594   dialog_active = true, out = gtdialog(type, argc, argv), dialog_active = false;
595   return (lua_pushstring(L, out), free(out), 1);
596 }
597 
598 /**
599  * Pushes the Scintilla view onto the stack.
600  * The view must have previously been added with add_view.
601  * @param L The Lua state.
602  * @param view The Scintilla view to push.
603  * @see add_view
604  */
lua_pushview(lua_State * L,Scintilla * view)605 static void lua_pushview(lua_State *L, Scintilla *view) {
606   lua_getfield(L, LUA_REGISTRYINDEX, "ta_views"),
607     lua_pushlightuserdata(L, view), lua_gettable(L, -2), lua_replace(L, -2);
608 }
609 
610 /**
611  * Pushes onto the stack a split view.
612  * @param L The Lua state.
613  * @param pane The pane of the split to push.
614  */
lua_pushsplit(lua_State * L,Pane * pane)615 static void lua_pushsplit(lua_State *L, Pane *pane) {
616 #if GTK
617   if (GTK_IS_PANED(pane)) {
618     GtkPaned *p = GTK_PANED(pane);
619     lua_newtable(L);
620     lua_pushsplit(L, gtk_paned_get_child1(p)), lua_rawseti(L, -2, 1);
621     lua_pushsplit(L, gtk_paned_get_child2(p)), lua_rawseti(L, -2, 2);
622     lua_pushboolean(
623       L, gtk_orientable_get_orientation(GTK_ORIENTABLE(pane)) ==
624       GTK_ORIENTATION_HORIZONTAL), lua_setfield(L, -2, "vertical");
625     lua_pushinteger(L, gtk_paned_get_position(p)), lua_setfield(L, -2, "size");
626   } else lua_pushview(L, pane);
627 #elif CURSES
628   if (pane->type != SINGLE) {
629     lua_newtable(L);
630     lua_pushsplit(L, pane->child1), lua_rawseti(L, -2, 1);
631     lua_pushsplit(L, pane->child2), lua_rawseti(L, -2, 2);
632     lua_pushboolean(L, pane->type == VSPLIT), lua_setfield(L, -2, "vertical");
633     lua_pushinteger(L, pane->split_size), lua_setfield(L, -2, "size");
634   } else lua_pushview(L, pane->view);
635 #endif
636 }
637 
638 /** `ui.get_split_table()` Lua function. */
get_split_table(lua_State * L)639 static int get_split_table(lua_State *L) {
640 #if GTK
641   GtkWidget *pane = focused_view;
642   while (GTK_IS_PANED(gtk_widget_get_parent(pane)))
643     pane = gtk_widget_get_parent(pane);
644 #endif
645   return (lua_pushsplit(L, pane), 1);
646 }
647 
648 /**
649  * Returns the view at the given acceptable index as a Scintilla view.
650  * @param L The Lua state.
651  * @param index Stack index of the view.
652  * @return Scintilla view
653  */
lua_toview(lua_State * L,int index)654 static Scintilla *lua_toview(lua_State *L, int index) {
655   Scintilla *view = (lua_getfield(L, index, "widget_pointer"),
656     lua_touserdata(L, -1));
657   return (lua_pop(L, 1), view); // widget pointer
658 }
659 
660 /**
661  * Pushes the Scintilla document onto the stack.
662  * The document must have previously been added with add_doc.
663  * @param L The Lua state.
664  * @param doc The document to push.
665  * @see add_doc
666  */
lua_pushdoc(lua_State * L,sptr_t doc)667 static void lua_pushdoc(lua_State *L, sptr_t doc) {
668   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"),
669     lua_pushlightuserdata(L, (sptr_t *)doc), lua_gettable(L, -2),
670     lua_replace(L, -2);
671 }
672 
673 /**
674  * Synchronizes the tabbar after switching between Scintilla views or documents.
675  */
sync_tabbar()676 static void sync_tabbar() {
677 #if GTK
678   int i = (lua_getfield(lua, LUA_REGISTRYINDEX, "ta_buffers"),
679     lua_pushdoc(lua, SS(focused_view, SCI_GETDOCPOINTER, 0, 0)),
680     lua_gettable(lua, -2), lua_tointeger(lua, -1) - 1);
681   lua_pop(lua, 2); // index and buffers
682   GtkNotebook *tabs = GTK_NOTEBOOK(tabbar);
683   tab_sync = true, gtk_notebook_set_current_page(tabs, i), tab_sync = false;
684 //#elif CURSES
685   // TODO: tabs
686 #endif
687 }
688 
689 /**
690  * Returns whether or not the value at the given index has a given metatable.
691  * @param L The Lua state.
692  * @param index The stack index of the value to test.
693  * @param tname The name of the expected metatable.
694  */
is_type(lua_State * L,int index,const char * tname)695 static bool is_type(lua_State *L, int index, const char *tname) {
696   if (!lua_getmetatable(L, index)) return false;
697   luaL_getmetatable(L, tname);
698   bool has_metatable = lua_rawequal(L, -1, -2);
699   return (lua_pop(L, 2), has_metatable); // metatable, metatable
700 }
701 
702 /**
703  * Checks whether the function argument arg is a Scintilla view and returns
704  * this view cast to a Scintilla.
705  * @param L The Lua state.
706  * @param arg The stack index of the Scintilla view.
707  * @return Scintilla view
708  */
luaL_checkview(lua_State * L,int arg)709 static Scintilla *luaL_checkview(lua_State *L, int arg) {
710   luaL_argcheck(L, is_type(L, arg, "ta_view"), arg, "View expected");
711   return lua_toview(L, arg);
712 }
713 
714 /**
715  * Change focus to the given Scintilla view.
716  * Generates 'view_before_switch' and 'view_after_switch' events.
717  * @param view The Scintilla view to focus.
718  * @param L The Lua state.
719  */
view_focused(Scintilla * view,lua_State * L)720 static void view_focused(Scintilla *view, lua_State *L) {
721   if (!initing && !closing) emit(L, "view_before_switch", -1);
722   lua_pushview(L, focused_view = view), lua_setglobal(L, "view"), sync_tabbar();
723   lua_pushdoc(L, SS(view, SCI_GETDOCPOINTER, 0, 0)), lua_setglobal(L, "buffer");
724   if (!initing && !closing) emit(L, "view_after_switch", -1);
725 }
726 
727 /** `ui.goto_view()` Lua function. */
goto_view(lua_State * L)728 static int goto_view(lua_State *L) {
729   if (lua_isnumber(L, 1)) {
730     int n = (lua_getfield(L, LUA_REGISTRYINDEX, "ta_views"),
731       lua_pushview(L, focused_view), lua_gettable(L, -2),
732       lua_tointeger(L, -1)) + lua_tointeger(L, 1);
733     if (n > (int)lua_rawlen(L, -2))
734       n = 1;
735     else if (n < 1)
736       n = lua_rawlen(L, -2);
737     lua_rawgeti(L, -2, n), lua_replace(L, 1); // index
738   }
739   Scintilla *view = luaL_checkview(L, 1);
740   focus_view(view);
741 #if GTK
742   // ui.dialog() interferes with focus so gtk_widget_grab_focus() does not
743   // always work. If this is the case, ensure view_focused() is called.
744   if (!gtk_widget_has_focus(view)) view_focused(view, L);
745 #endif
746   return 0;
747 }
748 
749 /**
750  * Returns the value t[n] as an integer where t is the value at the given valid
751  * index.
752  * The access is raw; that is, it does not invoke metamethods.
753  * @param L The Lua state.
754  * @param index The stack index of the table.
755  * @param n The index in the table to get.
756  * @return integer
757  */
get_int_field(lua_State * L,int index,int n)758 static int get_int_field(lua_State *L, int index, int n) {
759   int i = (lua_rawgeti(L, index, n), lua_tointeger(L, -1));
760   return (lua_pop(L, 1), i); // integer
761 }
762 
763 #if GTK
764 /**
765  * Pushes a menu created from the table at the given valid index onto the stack.
766  * Consult the LuaDoc for the table format.
767  * @param L The Lua state.
768  * @param index The stack index of the table to create the menu from.
769  * @param f An optional GTK callback function associated with each menu item.
770  * @param submenu Flag indicating whether or not this menu is a submenu.
771  */
lua_pushmenu(lua_State * L,int index,GCallback f,bool submenu)772 static void lua_pushmenu(lua_State *L, int index, GCallback f, bool submenu) {
773   GtkWidget *menu = gtk_menu_new(), *submenu_root = NULL;
774   if (lua_getfield(L, index, "title") != LUA_TNIL || submenu) { // submenu title
775     const char *label = !lua_isnil(L, -1) ? lua_tostring(L, -1) : "no title";
776     submenu_root = gtk_menu_item_new_with_mnemonic(label);
777     gtk_menu_item_set_submenu(GTK_MENU_ITEM(submenu_root), menu);
778   }
779   lua_pop(L, 1); // title
780   for (size_t i = 1; i <= lua_rawlen(L, index); lua_pop(L, 1), i++) {
781     if (lua_rawgeti(L, -1, i) != LUA_TTABLE) continue; // popped on loop
782     bool is_submenu = lua_getfield(L, -1, "title") != LUA_TNIL;
783     if (lua_pop(L, 1), is_submenu) {
784       lua_pushmenu(L, -1, f, true);
785       gtk_menu_shell_append(GTK_MENU_SHELL(menu), lua_touserdata(L, -1));
786       lua_pop(L, 1); // menu
787       continue;
788     }
789     const char *label = (lua_rawgeti(L, -1, 1), lua_tostring(L, -1));
790     if (lua_pop(L, 1), !label) continue;
791     // Menu item table is of the form {label, id, key, modifiers}.
792     GtkWidget *menu_item = *label ?
793       gtk_menu_item_new_with_mnemonic(label) : gtk_separator_menu_item_new();
794     if (*label && get_int_field(L, -1, 3) > 0)
795       gtk_widget_add_accelerator(
796         menu_item, "activate", accel, get_int_field(L, -1, 3),
797         get_int_field(L, -1, 4), GTK_ACCEL_VISIBLE);
798     g_signal_connect(
799       menu_item, "activate", f, (void*)(long)get_int_field(L, -1, 2));
800     gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
801   }
802   lua_pushlightuserdata(L, !submenu_root ? menu : submenu_root);
803 }
804 
805 /** Signal for a menu item click. */
menu_clicked(GtkWidget * _,void * id)806 static void menu_clicked(GtkWidget *_, void *id) {
807   emit(lua, "menu_clicked", LUA_TNUMBER, (int)(long)id, -1);
808 }
809 #endif
810 
811 /** `ui.menu()` Lua function. */
menu(lua_State * L)812 static int menu(lua_State *L) {
813   luaL_checktype(L, 1, LUA_TTABLE);
814 #if GTK
815   return (lua_pushmenu(L, -1, G_CALLBACK(menu_clicked), false), 1);
816 #elif CURSES
817   return (lua_pushnil(L), 1);
818 #endif
819 }
820 
821 /** `ui.update()` Lua function. */
update_ui(lua_State * L)822 static int update_ui(lua_State *L) {
823 #if GTK
824   while (gtk_events_pending()) gtk_main_iteration();
825 #elif (CURSES && !_WIN32)
826   struct timeval timeout = {0, 1e5}; // 0.1s
827   int nfds = os_spawn_pushfds(L);
828   while (select(nfds, lua_touserdata(L, -1), NULL, NULL, &timeout) > 0)
829     if (os_spawn_readfds(L) >= 0) refresh_all();
830   lua_pop(L, 1); // fd_set
831 #endif
832   return 0;
833 }
834 
835 /** `ui.__index` Lua metamethod. */
ui_index(lua_State * L)836 static int ui_index(lua_State *L) {
837   const char *key = lua_tostring(L, 2);
838   if (strcmp(key, "clipboard_text") == 0) {
839 #if GTK
840     char *text = gtk_clipboard_wait_for_text(
841       gtk_clipboard_get(GDK_SELECTION_CLIPBOARD));
842     text ? lua_pushstring(L, text) : lua_pushliteral(L, "");
843     g_free(text);
844 #elif CURSES
845     int len;
846     char *text = scintilla_get_clipboard(focused_view, &len);
847     lua_pushlstring(L, text, len), free(text);
848 #endif
849   } else if (strcmp(key, "maximized") == 0) {
850 #if GTK
851     GdkWindowState state = gdk_window_get_state(gtk_widget_get_window(window));
852     lua_pushboolean(L, state & GDK_WINDOW_STATE_MAXIMIZED);
853 #elif CURSES
854     lua_pushboolean(L, false);
855 #endif
856   } else if (strcmp(key, "size") == 0) {
857     int width, height;
858 #if GTK
859     gtk_window_get_size(GTK_WINDOW(window), &width, &height);
860 #elif CURSES
861     width = COLS, height = LINES;
862 #endif
863     lua_newtable(L);
864     lua_pushinteger(L, width), lua_rawseti(L, -2, 1);
865     lua_pushinteger(L, height), lua_rawseti(L, -2, 2);
866   } else if (strcmp(key, "tabs") == 0)
867     lua_pushboolean(L, show_tabs);
868   else
869     lua_rawget(L, 1);
870   return 1;
871 }
872 
set_statusbar_text(const char * text,int bar)873 static void set_statusbar_text(const char *text, int bar) {
874 #if GTK
875   if (statusbar[bar]) gtk_label_set_text(GTK_LABEL(statusbar[bar]), text);
876 #elif CURSES
877   int start = bar == 0 ? 0 : statusbar_length[0];
878   int end = bar == 0 ? COLS - statusbar_length[1] : COLS;
879   for (int i = start; i < end; i++) mvaddch(LINES - 1, i, ' '); // clear
880   int len = utf8strlen(text);
881   mvaddstr(LINES - 1, bar == 0 ? 0 : COLS - len, text), refresh();
882   statusbar_length[bar] = len;
883 #endif
884 }
885 
886 /** `ui.__newindex` Lua metatable. */
ui_newindex(lua_State * L)887 static int ui_newindex(lua_State *L) {
888   const char *key = lua_tostring(L, 2);
889   if (strcmp(key, "title") == 0) {
890 #if GTK
891     gtk_window_set_title(GTK_WINDOW(window), lua_tostring(L, 3));
892 #elif CURSES
893     for (int i = 0; i < COLS; i++) mvaddch(0, i, ' '); // clear titlebar
894     mvaddstr(0, 0, lua_tostring(L, 3)), refresh();
895 #endif
896   } else if (strcmp(key, "clipboard_text") == 0) {
897     const char *text = luaL_checkstring(L, 3);
898     SS(focused_view, SCI_COPYTEXT, lua_rawlen(L, 3), (sptr_t)text);
899   } else if (strcmp(key, "statusbar_text") == 0 ||
900              strcmp(key, "buffer_statusbar_text") == 0)
901     set_statusbar_text(lua_tostring(L, 3), *key == 's' ? 0 : 1);
902   else if (strcmp(key, "menubar") == 0) {
903 #if GTK
904     luaL_argcheck(L, lua_istable(L, 3), 3, "table of menus expected");
905     for (size_t i = 1; i <= lua_rawlen(L, 3); lua_pop(L, 1), i++)
906       luaL_argcheck(
907         L, lua_rawgeti(L, 3, i) == LUA_TLIGHTUSERDATA, 3,
908         "table of menus expected"); // popped on loop
909     GtkWidget *new_menubar = gtk_menu_bar_new();
910     for (size_t i = 1; i <= lua_rawlen(L, 3); lua_pop(L, 1), i++)
911       gtk_menu_shell_append(
912         GTK_MENU_SHELL(new_menubar),
913         (lua_rawgeti(L, 3, i), lua_touserdata(L, -1))); // popped on loop
914     GtkWidget *vbox = gtk_widget_get_parent(menubar);
915     gtk_container_remove(GTK_CONTAINER(vbox), menubar);
916     gtk_box_pack_start(GTK_BOX(vbox), menubar = new_menubar, false, false, 0);
917     gtk_box_reorder_child(GTK_BOX(vbox), new_menubar, 0);
918     if (lua_rawlen(L, 3) > 0) gtk_widget_show_all(new_menubar);
919 #if __APPLE__
920     gtkosx_application_set_menu_bar(osxapp, GTK_MENU_SHELL(new_menubar));
921     gtk_widget_hide(new_menubar); // hide in window
922 #endif
923 //#elif CURSES
924     // TODO: menus
925 #endif
926   } else if (strcmp(key, "maximized") == 0) {
927 #if GTK
928     lua_toboolean(L, 3) ? gtk_window_maximize(
929       GTK_WINDOW(window)) : gtk_window_unmaximize(GTK_WINDOW(window));
930 #endif
931   } else if (strcmp(key, "size") == 0) {
932 #if GTK
933     luaL_argcheck(
934       L, lua_istable(L, 3) && lua_rawlen(L, 3) == 2, 3,
935       "{width, height} table expected");
936     int w = get_int_field(L, 3, 1), h = get_int_field(L, 3, 2);
937     if (w > 0 && h > 0) gtk_window_resize(GTK_WINDOW(window), w, h);
938 #endif
939   } else if (strcmp(key, "tabs") == 0) {
940     show_tabs = lua_toboolean(L, 3);
941 #if GTK
942     gtk_widget_set_visible(
943       tabbar, show_tabs && gtk_notebook_get_n_pages(GTK_NOTEBOOK(tabbar)) > 1);
944 //#elif CURSES
945     // TODO: tabs
946 #endif
947   } else lua_rawset(L, 1);
948   return 0;
949 }
950 
951 /**
952  * Returns the buffer at the given acceptable index as a Scintilla document.
953  * @param L The Lua state.
954  * @param index Stack index of the buffer.
955  * @return Scintilla document
956  */
lua_todoc(lua_State * L,int index)957 static sptr_t lua_todoc(lua_State *L, int index) {
958   sptr_t doc = (lua_getfield(L, index, "doc_pointer"),
959     (sptr_t)lua_touserdata(L, -1));
960   return (lua_pop(L, 1), doc); // doc_pointer
961 }
962 
963 /**
964  * Returns for the Scintilla document at the given index a suitable Scintilla
965  * view that can operate on it.
966  * For non-global, non-command entry documents, loads that document in
967  * `dummy_view` for non-global document use (unless it is already loaded).
968  * Raises and error if the value is not a Scintilla document or if the document
969  * no longer exists.
970  * @param L The Lua state.
971  * @param index The stack index of the Scintilla document.
972  */
view_for_doc(lua_State * L,int index)973 static Scintilla *view_for_doc(lua_State *L, int index) {
974   luaL_argcheck(
975     L, is_type(L, index, "ta_buffer"), index, "Buffer expected");
976   sptr_t doc = lua_todoc(L, index);
977   if (doc == SS(focused_view, SCI_GETDOCPOINTER, 0, 0)) return focused_view;
978   luaL_argcheck(
979     L, (lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"), lua_pushdoc(L, doc),
980       lua_gettable(L, -2) != LUA_TNIL), index, "this Buffer does not exist");
981   lua_pop(L, 2); // buffer, ta_buffers
982   if (doc == SS(command_entry, SCI_GETDOCPOINTER, 0, 0)) return command_entry;
983   if (doc == SS(dummy_view, SCI_GETDOCPOINTER, 0, 0)) return dummy_view;
984   return (SS(dummy_view, SCI_SETDOCPOINTER, 0, doc), dummy_view);
985 }
986 
987 /**
988  * Switches to a document in the given view.
989  * @param L The Lua state.
990  * @param view The Scintilla view.
991  * @param n Relative or absolute index of the document to switch to. An absolute
992  *   n of -1 represents the last document.
993  * @param relative Flag indicating whether or not n is relative.
994  */
goto_doc(lua_State * L,Scintilla * view,int n,bool relative)995 static void goto_doc(lua_State *L, Scintilla *view, int n, bool relative) {
996   if (relative && n == 0) return;
997   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers");
998   if (relative) {
999     n = (lua_pushdoc(L, SS(view, SCI_GETDOCPOINTER, 0, 0)), lua_gettable(L, -2),
1000       lua_tointeger(L, -1)) + n;
1001     if (n > (int)lua_rawlen(L, -2))
1002       n = 1;
1003     else if (n < 1)
1004       n = lua_rawlen(L, -2);
1005     lua_rawgeti(L, -2, n), lua_replace(L, -2); // index
1006   } else lua_rawgeti(L, -1, n > 0 ? n : (int)lua_rawlen(L, -1));
1007   luaL_argcheck(L, !lua_isnil(L, -1), 2, "no Buffer exists at that index");
1008   sptr_t doc = lua_todoc(L, -1);
1009   SS(view, SCI_SETDOCPOINTER, 0, doc), sync_tabbar();
1010   lua_setglobal(L, "buffer");
1011   lua_pop(L, 1); // buffers
1012 }
1013 
1014 /**
1015  * Adds the command entry's buffer to the 'buffers' registry table at a constant
1016  * index (0).
1017  */
register_command_entry_doc()1018 static void register_command_entry_doc() {
1019   sptr_t doc = SS(command_entry, SCI_GETDOCPOINTER, 0, 0);
1020   lua_getfield(lua, LUA_REGISTRYINDEX, "ta_buffers");
1021   lua_getglobal(lua, "ui"), lua_getfield(lua, -1, "command_entry"),
1022     lua_replace(lua, -2);
1023   lua_pushstring(lua, "doc_pointer"), lua_pushlightuserdata(lua, (sptr_t *)doc),
1024     lua_rawset(lua, -3);
1025   // t[doc_pointer] = command_entry, t[0] = command_entry, t[command_entry] = 0
1026   lua_pushlightuserdata(lua, (sptr_t *)doc), lua_pushvalue(lua, -2),
1027     lua_settable(lua, -4);
1028   lua_pushvalue(lua, -1), lua_rawseti(lua, -3, 0);
1029   lua_pushinteger(lua, 0), lua_settable(lua, -3);
1030   lua_pop(lua, 1); // buffers
1031 }
1032 
1033 /**
1034  * Removes the Scintilla document from the 'buffers' registry table.
1035  * The document must have been previously added with add_doc.
1036  * It is removed from any other views showing it first. Therefore, ensure the
1037  * length of 'buffers' is more than one unless quitting the application.
1038  * @param L The Lua state.
1039  * @param doc The Scintilla document to remove.
1040  * @see add_doc
1041  */
remove_doc(lua_State * L,sptr_t doc)1042 static void remove_doc(lua_State *L, sptr_t doc) {
1043   lua_getfield(L, LUA_REGISTRYINDEX, "ta_views");
1044   for (size_t i = 1; i <= lua_rawlen(L, -1); lua_pop(L, 1), i++) {
1045     Scintilla *view = (lua_rawgeti(L, -1, i), lua_toview(L, -1)); // ^popped
1046     if (doc == SS(view, SCI_GETDOCPOINTER, 0, 0)) goto_doc(L, view, -1, true);
1047   }
1048   lua_pop(L, 1); // views
1049   lua_newtable(L);
1050   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers");
1051   for (size_t i = 1; i <= lua_rawlen(L, -1); i++)
1052     if (doc != (lua_rawgeti(L, -1, i), lua_todoc(L, -1))) {
1053       // t[doc_pointer] = buffer, t[#t + 1] = buffer, t[buffer] = #t
1054       lua_getfield(L, -1, "doc_pointer"), lua_pushvalue(L, -2),
1055         lua_settable(L, -5);
1056       lua_pushvalue(L, -1), lua_rawseti(L, -4, lua_rawlen(L, -4) + 1);
1057       lua_pushinteger(L, lua_rawlen(L, -3)), lua_settable(L, -4);
1058     } else {
1059 #if GTK
1060       // Remove the tab from the tabbar.
1061       gtk_notebook_remove_page(GTK_NOTEBOOK(tabbar), i - 1);
1062       gtk_widget_set_visible(tabbar, show_tabs && lua_rawlen(L, -2) > 2);
1063 //#elif CURSES
1064       // TODO: tabs
1065 #endif
1066       lua_pop(L, 1); // buffer
1067     }
1068   lua_pop(L, 1); // buffers
1069   lua_pushvalue(L, -1), lua_setfield(L, LUA_REGISTRYINDEX, "ta_buffers");
1070   register_command_entry_doc();
1071   lua_setglobal(L, "_BUFFERS");
1072 }
1073 
1074 /**
1075  * Removes the Scintilla buffer from the current Scintilla view.
1076  * @param doc The Scintilla document.
1077  * @see remove_doc
1078  */
delete_buffer(sptr_t doc)1079 static void delete_buffer(sptr_t doc) {
1080   remove_doc(lua, doc), SS(dummy_view, SCI_SETDOCPOINTER, 0, 0);
1081   SS(focused_view, SCI_RELEASEDOCUMENT, 0, doc);
1082 }
1083 
1084 /** `buffer.delete()` Lua function. */
delete_buffer_lua(lua_State * L)1085 static int delete_buffer_lua(lua_State *L) {
1086   Scintilla *view = view_for_doc(L, 1);
1087   luaL_argcheck(L, view != command_entry, 1, "cannot delete command entry");
1088   sptr_t doc = SS(view, SCI_GETDOCPOINTER, 0, 0);
1089   if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"), lua_rawlen(L, -1) == 1)
1090     new_buffer(0);
1091   if (view == focused_view) goto_doc(L, focused_view, -1, true);
1092   delete_buffer(doc), emit(L, "buffer_deleted", -1);
1093   if (view == focused_view) emit(L, "buffer_after_switch", -1);
1094   return 0;
1095 }
1096 
1097 /** `_G.buffer_new()` Lua function. */
new_buffer_lua(lua_State * L)1098 static int new_buffer_lua(lua_State *L) {
1099   if (initing) luaL_error(L, "cannot create buffers during initialization");
1100   new_buffer(0);
1101   return (lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"),
1102     lua_rawgeti(L, -1, lua_rawlen(L, -1)), 1);
1103 }
1104 
1105 /**
1106  * Checks whether the function argument arg is the given Scintilla parameter
1107  * type and returns it cast to the proper type.
1108  * @param L The Lua state.
1109  * @param arg The stack index of the Scintilla parameter.
1110  * @param type The Scintilla type to convert to.
1111  * @return Scintilla param
1112  */
luaL_checkscintilla(lua_State * L,int * arg,int type)1113 static sptr_t luaL_checkscintilla(lua_State *L, int *arg, int type) {
1114   if (type == SSTRING) return (sptr_t)luaL_checkstring(L, (*arg)++);
1115   if (type == SBOOL) return lua_toboolean(L, (*arg)++);
1116   if (type == SINDEX) {
1117     int i = luaL_checkinteger(L, (*arg)++);
1118     return i >= 0 ? i - 1 : i; // do not adjust significant values like -1
1119   }
1120   return type >= SINT && type <= SKEYMOD ? luaL_checkinteger(L, (*arg)++) : 0;
1121 }
1122 
1123 /**
1124  * Calls a function as a Scintilla function.
1125  * Does not remove any arguments from the stack, but does push results.
1126  * @param L The Lua state.
1127  * @param view The Scintilla view to call.
1128  * @param msg The Scintilla message.
1129  * @param wtype The type of Scintilla wParam.
1130  * @param ltype The type of Scintilla lParam.
1131  * @param rtype The type of the Scintilla return.
1132  * @param arg The stack index of the first Scintilla parameter. Subsequent
1133  *   elements will also be passed to Scintilla as needed.
1134  * @return number of results pushed onto the stack.
1135  * @see luaL_checkscintilla
1136  */
call_scintilla(lua_State * L,Scintilla * view,int msg,int wtype,int ltype,int rtype,int arg)1137 static int call_scintilla(
1138   lua_State *L, Scintilla *view, int msg, int wtype, int ltype, int rtype,
1139   int arg)
1140 {
1141   uptr_t wparam = 0;
1142   sptr_t lparam = 0, len = 0;
1143   int params_needed = 2, nresults = 0;
1144   bool string_return = false;
1145   char *text = NULL;
1146 
1147   // Even though the SCI_PRIVATELEXERCALL interface has ltype int, the LPeg
1148   // lexer API uses different types depending on wparam. Modify ltype
1149   // appropriately. See the LPeg lexer API for more information.
1150   if (msg == SCI_PRIVATELEXERCALL)
1151     switch (luaL_checkinteger(L, arg)) {
1152       case SCI_GETDIRECTFUNCTION: case SCI_SETDOCPOINTER:
1153       case SCI_CHANGELEXERSTATE:
1154         ltype = SINT; break;
1155       case SCI_SETLEXERLANGUAGE: case SCI_LOADLEXERLIBRARY:
1156         ltype = SSTRING; break;
1157       case SCI_GETNAMEDSTYLES: ltype = SSTRING, rtype = SINDEX; break;
1158       default: ltype = SSTRINGRET;
1159     }
1160 
1161   // Set wParam and lParam appropriately for Scintilla based on wtype and ltype.
1162   if (wtype == SLEN && ltype == SSTRING) {
1163     wparam = (uptr_t)lua_rawlen(L, arg);
1164     lparam = (sptr_t)luaL_checkstring(L, arg);
1165     params_needed = 0;
1166   } else if (ltype == SSTRINGRET || rtype == SSTRINGRET)
1167     string_return = true, params_needed = wtype == SLEN ? 0 : 1;
1168   if (params_needed > 0) wparam = luaL_checkscintilla(L, &arg, wtype);
1169   if (params_needed > 1) lparam = luaL_checkscintilla(L, &arg, ltype);
1170   if (string_return) { // create a buffer for the return string
1171     len = SS(view, msg, wparam, 0);
1172     if (wtype == SLEN) wparam = len;
1173     text = malloc(len + 1), text[len] = '\0';
1174     if (msg == SCI_GETTEXT || msg == SCI_GETSELTEXT || msg == SCI_GETCURLINE)
1175       len--; // Scintilla appends '\0' for these messages; compensate
1176     lparam = (sptr_t)text;
1177   }
1178 
1179   // Send the message to Scintilla and return the appropriate values.
1180   sptr_t result = SS(view, msg, wparam, lparam);
1181   if (string_return) lua_pushlstring(L, text, len), nresults++, free(text);
1182   if (rtype == SINDEX && result >= 0) result++;
1183   if (rtype > SVOID && rtype < SBOOL)
1184     lua_pushinteger(L, result), nresults++;
1185   else if (rtype == SBOOL)
1186     lua_pushboolean(L, result), nresults++;
1187   return nresults;
1188 }
1189 
1190 /** `buffer:method()` Lua function. */
call_scintilla_lua(lua_State * L)1191 static int call_scintilla_lua(lua_State *L) {
1192   Scintilla *view = focused_view;
1193   // If optional buffer/view argument is given, check it.
1194   if (is_type(L, 1, "ta_buffer"))
1195     view = view_for_doc(L, 1);
1196   else if (is_type(L, 1, "ta_view"))
1197     view = lua_toview(L, 1);
1198   // Interface table is of the form {msg, rtype, wtype, ltype}.
1199   return call_scintilla(
1200     L, view, get_int_field(L, lua_upvalueindex(1), 1),
1201     get_int_field(L, lua_upvalueindex(1), 3),
1202     get_int_field(L, lua_upvalueindex(1), 4),
1203     get_int_field(L, lua_upvalueindex(1), 2), lua_istable(L, 1) ? 2 : 1);
1204 }
1205 
1206 #if GTK
1207 /**
1208  * Shows the context menu for a widget based on a mouse event.
1209  * @param L The Lua state.
1210  * @param event An optional GTK mouse button event.
1211  * @param k The ui table field that contains the context menu.
1212  */
show_context_menu(lua_State * L,GdkEventButton * event,char * k)1213 static void show_context_menu(lua_State *L, GdkEventButton *event, char *k) {
1214   if (lua_getglobal(L, "ui") == LUA_TTABLE) {
1215     if (lua_getfield(L, -1, k) == LUA_TLIGHTUSERDATA) {
1216       GtkWidget *menu = lua_touserdata(L, -1);
1217       gtk_widget_show_all(menu);
1218       gtk_menu_popup(
1219         GTK_MENU(menu), NULL, NULL, NULL, NULL, event ? event->button : 0,
1220         gdk_event_get_time((GdkEvent *)event));
1221     }
1222     lua_pop(L, 2); // ui context menu field, ui
1223   } else lua_pop(L, 1); // non-table
1224 }
1225 
1226 /** Signal for a tab label mouse click. */
tab_clicked(GtkWidget * label,GdkEventButton * event,void * L)1227 static bool tab_clicked(GtkWidget *label, GdkEventButton *event, void *L) {
1228   GtkNotebook *tabs = GTK_NOTEBOOK(tabbar);
1229   for (int i = 0; i < gtk_notebook_get_n_pages(tabs); i++) {
1230     GtkWidget *page = gtk_notebook_get_nth_page(tabs, i);
1231     if (label != gtk_notebook_get_tab_label(tabs, page)) continue;
1232     emit(
1233       L, "tab_clicked", LUA_TNUMBER, i + 1, LUA_TNUMBER, event->button,
1234       LUA_TBOOLEAN, event->state & GDK_SHIFT_MASK,
1235       LUA_TBOOLEAN, event->state & GDK_CONTROL_MASK,
1236       LUA_TBOOLEAN, event->state & GDK_MOD1_MASK,
1237       LUA_TBOOLEAN, event->state & GDK_META_MASK, -1);
1238     if (event->button == 3) show_context_menu(L, event, "tab_context_menu");
1239     break;
1240   }
1241   return true;
1242 }
1243 #endif
1244 
1245 /** `buffer[k].__index` metamethod. */
property_index(lua_State * L)1246 static int property_index(lua_State *L) {
1247   Scintilla *view = (lua_getfield(L, 1, "_self"), !is_type(L, -1, "ta_view")) ?
1248     view_for_doc(L, -1) : lua_toview(L, -1);
1249   lua_getfield(L, 1, "_iface"); // {get_id, set_id, rtype, wtype}.
1250   int msg = get_int_field(L, -1, 1), wtype = get_int_field(L, -1, 4),
1251     ltype = SVOID, rtype = get_int_field(L, -1, 3);
1252   luaL_argcheck(L, msg, 2, "write-only property");
1253   return (call_scintilla(L, view, msg, wtype, ltype, rtype, 2), 1);
1254 }
1255 
1256 /** `buffer[k].__newindex` metamethod. */
property_newindex(lua_State * L)1257 static int property_newindex(lua_State *L) {
1258   Scintilla *view = (lua_getfield(L, 1, "_self"), !is_type(L, -1, "ta_view")) ?
1259     view_for_doc(L, -1) : lua_toview(L, -1);
1260   lua_getfield(L, 1, "_iface"); // {get_id, set_id, rtype, wtype}.
1261   int msg = get_int_field(L, -1, 2), wtype = get_int_field(L, -1, 4),
1262     ltype = get_int_field(L, -1, 3), rtype = SVOID;
1263   luaL_argcheck(L, msg, 3, "read-only property");
1264   if (ltype == SSTRINGRET) ltype = SSTRING;
1265   return (call_scintilla(L, view, msg, wtype, ltype, rtype, 2), 0);
1266 }
1267 
1268 // Helper function for `buffer_index()` and `view_index()` that gets Scintilla
1269 // properties.
get_property(lua_State * L)1270 static void get_property(lua_State *L) {
1271   Scintilla *view = is_type(L, 1, "ta_buffer") ? view_for_doc(L, 1) :
1272     lua_toview(L, 1);
1273   // Interface table is of the form {get_id, set_id, rtype, wtype}.
1274   int msg = get_int_field(L, -1, 1), wtype = get_int_field(L, -1, 4),
1275     ltype = SVOID, rtype = get_int_field(L, -1, 3);
1276   luaL_argcheck(L, msg || wtype != SVOID, 2, "write-only property");
1277   if (wtype != SVOID) { // indexible property
1278     lua_createtable(L, 2, 0);
1279     lua_pushvalue(L, 1), lua_setfield(L, -2, "_self");
1280     lua_pushvalue(L, -2), lua_setfield(L, -2, "_iface");
1281     set_metatable(L, -1, "ta_property", property_index, property_newindex);
1282   } else call_scintilla(L, view, msg, wtype, ltype, rtype, 2);
1283 }
1284 
1285 // Helper function for `buffer_newindex()` and `view_newindex()` that sets
1286 // Scintilla properties.
set_property(lua_State * L)1287 static void set_property(lua_State *L) {
1288   Scintilla *view = is_type(L, 1, "ta_buffer") ? view_for_doc(L, 1) :
1289     lua_toview(L, 1);
1290   // Interface table is of the form {get_id, set_id, rtype, wtype}.
1291   int msg = get_int_field(L, -1, 2), wtype = get_int_field(L, -1, 3),
1292     ltype = get_int_field(L, -1, 4), rtype = SVOID, temp;
1293   luaL_argcheck(L, msg && ltype == SVOID, 3, "read-only property");
1294   if (wtype == SSTRING || wtype == SSTRINGRET ||
1295       msg == SCI_SETMARGINLEFT || msg == SCI_SETMARGINRIGHT)
1296     temp = wtype != SSTRINGRET ? wtype : SSTRING, wtype = ltype, ltype = temp;
1297   call_scintilla(L, view, msg, wtype, ltype, rtype, 3);
1298 }
1299 
1300 /** `buffer.__index` metamethod. */
buffer_index(lua_State * L)1301 static int buffer_index(lua_State *L) {
1302   if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_functions"), lua_pushvalue(L, 2),
1303       lua_rawget(L, -2) == LUA_TTABLE)
1304     // If the key is a Scintilla function, return a callable closure.
1305     lua_pushcclosure(L, call_scintilla_lua, 1);
1306   else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_properties"),
1307            lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TTABLE)
1308     // If the key is a Scintilla property, determine if it is an indexible one
1309     // or not. If so, return a table with the appropriate metatable; otherwise
1310     // call Scintilla to get the property's value.
1311     get_property(L);
1312   else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_constants"),
1313            lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TNUMBER); // pushed
1314     // If the key is a Scintilla constant, return its value.
1315   else if (strcmp(lua_tostring(L, 2), "tab_label") == 0 &&
1316            lua_todoc(L, 1) != SS(command_entry, SCI_GETDOCPOINTER, 0, 0)) {
1317     // Return the buffer's tab label.
1318     lua_getfield(L, 1, "tab_pointer");
1319 #if GTK
1320     lua_pushstring(
1321       L, gtk_notebook_get_tab_label_text(GTK_NOTEBOOK(tabbar),
1322       lua_touserdata(L, -1)));
1323 //#elif CURSES
1324     // TODO: tabs
1325 #endif
1326   } else if (strcmp(lua_tostring(L, 2), "active") == 0 &&
1327              lua_todoc(L, 1) == SS(command_entry, SCI_GETDOCPOINTER, 0, 0))
1328     lua_pushboolean(L, command_entry_active);
1329   else if (strcmp(lua_tostring(L, 2), "height") == 0 &&
1330              lua_todoc(L, 1) == SS(command_entry, SCI_GETDOCPOINTER, 0, 0)) {
1331     // Return the command entry's pixel height.
1332 #if GTK
1333     GtkAllocation allocation;
1334     gtk_widget_get_allocation(command_entry, &allocation);
1335     lua_pushinteger(L, allocation.height);
1336 #elif CURSES
1337     lua_pushinteger(L, getmaxy(scintilla_get_window(command_entry)));
1338 #endif
1339   } else lua_settop(L, 2), lua_rawget(L, 1);
1340   return 1;
1341 }
1342 
1343 /** `buffer.__newindex` metamethod. */
buffer_newindex(lua_State * L)1344 static int buffer_newindex(lua_State *L) {
1345   if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_properties"), lua_pushvalue(L, 2),
1346       lua_rawget(L, -2) == LUA_TTABLE)
1347     // If the key is a Scintilla property, call Scintilla to set its value.
1348     // Interface table is of the form {get_id, set_id, rtype, wtype}.
1349     set_property(L);
1350   else if (strcmp(lua_tostring(L, 2), "tab_label") == 0 &&
1351            lua_todoc(L, 1) != SS(command_entry, SCI_GETDOCPOINTER, 0, 0)) {
1352     // Update the buffer's tab label.
1353     lua_getfield(L, 1, "tab_pointer");
1354 #if GTK
1355     GtkWidget *box = gtk_event_box_new();
1356     gtk_event_box_set_visible_window(GTK_EVENT_BOX(box), false);
1357     GtkWidget *label = gtk_label_new(luaL_checkstring(L, 3));
1358     gtk_container_add(GTK_CONTAINER(box), label), gtk_widget_show(label);
1359     gtk_notebook_set_tab_label(
1360       GTK_NOTEBOOK(tabbar), lua_touserdata(L, -1), box);
1361     g_signal_connect(box, "button-press-event", G_CALLBACK(tab_clicked), L);
1362 //#elif CURSES
1363     // TODO: tabs
1364 #endif
1365   } else if (strcmp(lua_tostring(L, 2), "height") == 0 &&
1366              lua_todoc(L, 1) == SS(command_entry, SCI_GETDOCPOINTER, 0, 0)) {
1367     // Set the command entry's pixel height.
1368     int height = fmax(
1369       luaL_checkinteger(L, 3), SS(command_entry, SCI_TEXTHEIGHT, 0, 0));
1370 #if GTK
1371     GtkWidget *paned = gtk_widget_get_parent(command_entry);
1372     GtkAllocation allocation;
1373     gtk_widget_get_allocation(paned, &allocation);
1374     gtk_widget_set_size_request(command_entry, -1, height);
1375     gtk_paned_set_position(GTK_PANED(paned), allocation.height - height);
1376 #elif CURSES
1377     WINDOW *win = scintilla_get_window(command_entry);
1378     wresize(win, height, COLS), mvwin(win, LINES - 1 - height, 0);
1379 #endif
1380   } else lua_settop(L, 3), lua_rawset(L, 1);
1381   return 0;
1382 }
1383 
1384 /**
1385  * Adds a Scintilla document with a metatable to the 'buffers' registry table.
1386  * @param L The Lua state.
1387  * @param doc The Scintilla document to add.
1388  */
add_doc(lua_State * L,sptr_t doc)1389 static void add_doc(lua_State *L, sptr_t doc) {
1390   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers");
1391   lua_newtable(L);
1392   lua_pushlightuserdata(L, (sptr_t *)doc), lua_setfield(L, -2, "doc_pointer");
1393 #if GTK
1394   GtkWidget *tab = gtk_vbox_new(false, 0); // placeholder in GtkNotebook
1395   lua_pushlightuserdata(L, tab), lua_setfield(L, -2, "tab_pointer");
1396 //#elif CURSES
1397   // TODO: tabs
1398 #endif
1399   lua_pushcfunction(L, delete_buffer_lua), lua_setfield(L, -2, "delete");
1400   lua_pushcfunction(L, new_buffer_lua) , lua_setfield(L, -2, "new");
1401   set_metatable(L, -1, "ta_buffer", buffer_index, buffer_newindex);
1402   // t[doc_pointer] = buffer, t[#t + 1] = buffer, t[buffer] = #t
1403   lua_getfield(L, -1, "doc_pointer"), lua_pushvalue(L, -2), lua_settable(L, -4);
1404   lua_pushvalue(L, -1), lua_rawseti(L, -3, lua_rawlen(L, -3) + 1);
1405   lua_pushinteger(L, lua_rawlen(L, -2)), lua_settable(L, -3);
1406   lua_pop(L, 1); // buffers
1407 }
1408 
1409 /**
1410  * Creates a new Scintilla document and adds it to the Lua state.
1411  * Generates 'buffer_before_switch' and 'buffer_new' events.
1412  * @param doc Almost always zero, except for the first Scintilla view created,
1413  *   in which its doc pointer would be given here since it already has a
1414  *   pre-created buffer.
1415  * @see add_doc
1416  */
new_buffer(sptr_t doc)1417 static void new_buffer(sptr_t doc) {
1418   if (!doc) {
1419     emit(lua, "buffer_before_switch", -1);
1420     add_doc(lua, doc = SS(focused_view, SCI_CREATEDOCUMENT, 0, 0));
1421     goto_doc(lua, focused_view, -1, false);
1422   } else add_doc(lua, doc), SS(focused_view, SCI_ADDREFDOCUMENT, 0, doc);
1423 #if GTK
1424   // Add a tab to the tabbar.
1425   GtkWidget *tab = (lua_pushdoc(lua, SS(focused_view, SCI_GETDOCPOINTER, 0, 0)),
1426     lua_getfield(lua, -1, "tab_pointer"), lua_touserdata(lua, -1));
1427   tab_sync = true;
1428   int i = gtk_notebook_append_page(GTK_NOTEBOOK(tabbar), tab, NULL);
1429   gtk_widget_show(tab), gtk_widget_set_visible(tabbar, show_tabs && i > 0);
1430   gtk_notebook_set_current_page(GTK_NOTEBOOK(tabbar), i);
1431   tab_sync = false;
1432   lua_pop(lua, 2); // tab_pointer and buffer
1433 //#elif CURSES
1434   // TODO: tabs
1435 #endif
1436   SS(focused_view, SCI_SETILEXER, 0, (sptr_t)CreateLexer(NULL));
1437   lua_pushdoc(lua, doc), lua_setglobal(lua, "buffer");
1438   if (!initing) emit(lua, "buffer_new", -1);
1439 }
1440 
1441 /** `_G.quit()` Lua function. */
quit(lua_State * L)1442 static int quit(lua_State *L) {
1443 #if GTK
1444   GdkEventAny event = {GDK_DELETE, gtk_widget_get_window(window), true};
1445   gdk_event_put((GdkEvent *)&event);
1446 #elif CURSES
1447   quitting = !emit(L, "quit", -1);
1448 #endif
1449   return 0;
1450 }
1451 
1452 /**
1453  * Loads and runs the given file.
1454  * @param L The Lua state.
1455  * @param filename The file name relative to textadept_home.
1456  * @return true if there are no errors or false in case of errors.
1457  */
run_file(lua_State * L,const char * filename)1458 static bool run_file(lua_State *L, const char *filename) {
1459   char *file = malloc(strlen(textadept_home) + 1 + strlen(filename) + 1);
1460   sprintf(file, "%s/%s", textadept_home, filename);
1461   bool ok = luaL_dofile(L, file) == LUA_OK;
1462   if (!ok) {
1463     const char *argv[] = {
1464       "--title", "Initialization Error", "--text", lua_tostring(L, -1)
1465     };
1466     free(gtdialog(GTDIALOG_TEXTBOX, 4, argv));
1467     lua_settop(L, 0);
1468   }
1469   return (free(file), ok);
1470 }
1471 
1472 /** `_G.reset()` Lua function. */
reset(lua_State * L)1473 static int reset(lua_State *L) {
1474   int persist_ref = (lua_newtable(L), luaL_ref(L, LUA_REGISTRYINDEX));
1475   lua_rawgeti(L, LUA_REGISTRYINDEX, persist_ref); // emit will unref
1476   emit(L, "reset_before", LUA_TTABLE, luaL_ref(L, LUA_REGISTRYINDEX), -1);
1477   init_lua(L, 0, NULL, true);
1478   lua_pushview(L, focused_view), lua_setglobal(L, "view");
1479   lua_pushdoc(L, SS(focused_view, SCI_GETDOCPOINTER, 0, 0)),
1480     lua_setglobal(L, "buffer");
1481   lua_pushnil(L), lua_setglobal(L, "arg");
1482   run_file(L, "init.lua"), emit(L, "initialized", -1);
1483   lua_getfield(L, LUA_REGISTRYINDEX, "ta_arg"), lua_setglobal(L, "arg");
1484   return (emit(L, "reset_after", LUA_TTABLE, persist_ref, -1), 0);
1485 }
1486 
1487 /** Runs the timeout function passed to `_G.timeout()`. */
timed_out(void * args)1488 static int timed_out(void *args) {
1489   int *refs = args, nargs = 0;
1490   lua_rawgeti(lua, LUA_REGISTRYINDEX, refs[0]); // function
1491   while (refs[++nargs]) lua_rawgeti(lua, LUA_REGISTRYINDEX, refs[nargs]);
1492   bool ok = lua_pcall(lua, nargs - 1, 1, 0) == LUA_OK, repeat = true;
1493   if (!ok || !lua_toboolean(lua, -1)) {
1494     while (--nargs >= 0) luaL_unref(lua, LUA_REGISTRYINDEX, refs[nargs]);
1495     repeat = false;
1496     if (!ok) emit(lua, "error", LUA_TSTRING, lua_tostring(lua, -1), -1);
1497   }
1498   return (lua_pop(lua, 1), repeat); // result
1499 }
1500 
1501 /** `_G.timeout()` Lua function. */
add_timeout(lua_State * L)1502 static int add_timeout(lua_State *L) {
1503 #if GTK
1504   double interval = luaL_checknumber(L, 1);
1505   luaL_argcheck(L, interval > 0, 1, "interval must be > 0");
1506   luaL_argcheck(L, lua_isfunction(L, 2), 2, "function expected");
1507   int n = lua_gettop(L), *refs = (int *)calloc(n, sizeof(int));
1508   for (int i = 2; i <= n; i++)
1509     lua_pushvalue(L, i), refs[i - 2] = luaL_ref(L, LUA_REGISTRYINDEX);
1510   return (g_timeout_add(interval * 1000, timed_out, refs), 0);
1511 #elif CURSES
1512   return luaL_error(L, "not implemented in this environment");
1513 #endif
1514 }
1515 
1516 /** `string.iconv()` Lua function. */
iconv_lua(lua_State * L)1517 static int iconv_lua(lua_State *L) {
1518   size_t inbytesleft = 0;
1519   char *inbuf = (char *)luaL_checklstring(L, 1, &inbytesleft);
1520   const char *to = luaL_checkstring(L, 2), *from = luaL_checkstring(L, 3);
1521   iconv_t cd = iconv_open(to, from);
1522   if (cd == (iconv_t)-1) luaL_error(L, "invalid encoding(s)");
1523   // Ensure the minimum buffer size can hold a potential output BOM and one
1524   // multibyte character.
1525   size_t bufsiz = 4 + (inbytesleft > MB_LEN_MAX ? inbytesleft : MB_LEN_MAX);
1526   char *outbuf = malloc(bufsiz + 1), *p = outbuf;
1527   size_t outbytesleft = bufsiz;
1528   int n = 1; // concat this many converted strings
1529   while (iconv(cd, &inbuf, &inbytesleft, &p, &outbytesleft) == (size_t)-1)
1530     if (errno == E2BIG && p - outbuf > 0) {
1531       // Buffer was too small to store converted string. Push the partially
1532       // converted string for later concatenation.
1533       lua_checkstack(L, 2), lua_pushlstring(L, outbuf, p - outbuf), n++;
1534       p = outbuf, outbytesleft = bufsiz;
1535     } else free(outbuf), iconv_close(cd), luaL_error(L, "conversion failed");
1536   lua_pushlstring(L, outbuf, p - outbuf);
1537   free(outbuf), iconv_close(cd);
1538   return (lua_concat(L, n), 1);
1539 }
1540 
1541 /**
1542  * Initializes or re-initializes the Lua state.
1543  * Populates the state with global variables and functions, then runs the
1544  * 'core/init.lua' script.
1545  * @param L The Lua state.
1546  * @param argc The number of command line parameters.
1547  * @param argv The array of command line parameters.
1548  * @param reinit Flag indicating whether or not to reinitialize the Lua state.
1549  * @return true on success, false otherwise.
1550  */
init_lua(lua_State * L,int argc,char ** argv,bool reinit)1551 static bool init_lua(lua_State *L, int argc, char **argv, bool reinit) {
1552   if (!reinit) {
1553     lua_newtable(L);
1554     for (int i = 0; i < argc; i++)
1555       lua_pushstring(L, argv[i]), lua_rawseti(L, -2, i);
1556     lua_setfield(L, LUA_REGISTRYINDEX, "ta_arg");
1557     lua_newtable(L), lua_setfield(L, LUA_REGISTRYINDEX, "ta_buffers");
1558     lua_newtable(L), lua_setfield(L, LUA_REGISTRYINDEX, "ta_views");
1559   } else { // clear package.loaded and _G
1560     lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
1561     while (lua_pushnil(L), lua_next(L, -2))
1562       lua_pushnil(L), lua_replace(L, -2), lua_rawset(L, -3); // clear
1563     lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
1564     while (lua_pushnil(L), lua_next(L, -2))
1565       lua_pushnil(L), lua_replace(L, -2), lua_rawset(L, -3); // clear
1566     lua_pop(L, 2); // package.loaded, _G
1567     lua_gc(L, LUA_GCCOLLECT, 0);
1568   }
1569   lua_pushinteger(L, (intptr_t)L), lua_setglobal(L, "_LUA"); // for LPeg lexer
1570   luaL_openlibs(L);
1571   luaL_requiref(L, "lpeg", luaopen_lpeg, 1), lua_pop(L, 1);
1572   luaL_requiref(L, "lfs", luaopen_lfs, 1), lua_pop(L, 1);
1573 
1574   lua_newtable(L);
1575   lua_newtable(L);
1576   lua_pushcfunction(L, click_find_next), lua_setfield(L, -2, "find_next");
1577   lua_pushcfunction(L, click_find_prev), lua_setfield(L, -2, "find_prev");
1578   lua_pushcfunction(L, click_replace), lua_setfield(L, -2, "replace");
1579   lua_pushcfunction(L, click_replace_all), lua_setfield(L, -2, "replace_all");
1580   lua_pushcfunction(L, focus_find), lua_setfield(L, -2, "focus");
1581   set_metatable(L, -1, "ta_find", find_index, find_newindex);
1582   lua_setfield(L, -2, "find");
1583   if (!reinit) {
1584     lua_newtable(L);
1585     lua_pushcfunction(L, focus_command_entry), lua_setfield(L, -2, "focus");
1586     set_metatable(L, -1, "ta_buffer", buffer_index, buffer_newindex);
1587   } else
1588     lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"), lua_rawgeti(L, -1, 0),
1589       lua_replace(L, -2); // _BUFFERS[0] == command_entry
1590   lua_setfield(L, -2, "command_entry");
1591   lua_pushcfunction(L, dialog), lua_setfield(L, -2, "dialog");
1592   lua_pushcfunction(L, get_split_table), lua_setfield(L, -2, "get_split_table");
1593   lua_pushcfunction(L, goto_view), lua_setfield(L, -2, "goto_view");
1594   lua_pushcfunction(L, menu), lua_setfield(L, -2, "menu");
1595   lua_pushcfunction(L, update_ui), lua_setfield(L, -2, "update");
1596   set_metatable(L, -1, "ta_ui", ui_index, ui_newindex);
1597   lua_setglobal(L, "ui");
1598 
1599   lua_pushcfunction(L, quit), lua_setglobal(L, "quit");
1600   lua_pushcfunction(L, reset), lua_setglobal(L, "reset");
1601   lua_pushcfunction(L, add_timeout), lua_setglobal(L, "timeout");
1602 
1603   lua_getglobal(L, "string"), lua_pushcfunction(L, iconv_lua),
1604     lua_setfield(L, -2, "iconv"), lua_pop(L, 1);
1605 
1606   lua_getfield(L, LUA_REGISTRYINDEX, "ta_arg"), lua_setglobal(L, "arg");
1607   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"),
1608     lua_setglobal(L, "_BUFFERS");
1609   lua_getfield(L, LUA_REGISTRYINDEX, "ta_views"), lua_setglobal(L, "_VIEWS");
1610   lua_pushstring(L, textadept_home), lua_setglobal(L, "_HOME");
1611   if (platform) lua_pushboolean(L, true), lua_setglobal(L, platform);
1612 #if CURSES
1613   lua_pushboolean(L, true), lua_setglobal(L, "CURSES");
1614   show_tabs = false; // TODO: tabs
1615 #endif
1616   const char *charset = NULL;
1617 #if GTK
1618   g_get_charset(&charset);
1619 #elif (CURSES && !_WIN32)
1620   charset = getenv("CHARSET");
1621   if (!charset || !*charset) {
1622     char *locale = getenv("LC_ALL");
1623     if (!locale || !*locale) locale = getenv("LANG");
1624     if (locale && (charset = strchr(locale, '.'))) charset++;
1625   }
1626 #elif (CURSES && _WIN32)
1627   char codepage[8];
1628   sprintf(codepage, "CP%d", GetACP()), charset = codepage;
1629 #endif
1630   lua_pushstring(L, charset), lua_setglobal(L, "_CHARSET");
1631 
1632   if (!run_file(L, "core/init.lua")) return (lua_close(L), false);
1633   lua_getglobal(L, "_SCINTILLA");
1634   lua_getfield(L, -1, "constants"),
1635     lua_setfield(L, LUA_REGISTRYINDEX, "ta_constants");
1636   lua_getfield(L, -1, "functions"),
1637     lua_setfield(L, LUA_REGISTRYINDEX, "ta_functions");
1638   lua_getfield(L, -1, "properties"),
1639     lua_setfield(L, LUA_REGISTRYINDEX, "ta_properties");
1640   lua_pop(L, 1); // _SCINTILLA
1641   return true;
1642 }
1643 
1644 #if GTK
1645 /** Signal for a Textadept window focus change. */
window_focused(GtkWidget * _,GdkEventFocus * __,void * L)1646 static bool window_focused(GtkWidget *_, GdkEventFocus *__, void *L) {
1647   if (!command_entry_active) emit(L, "focus", -1);
1648   return false;
1649 }
1650 
1651 /** Signal for a Textadept keypress. */
window_keypress(GtkWidget * _,GdkEventKey * event,void * __)1652 static bool window_keypress(GtkWidget *_, GdkEventKey *event, void *__) {
1653   if (event->keyval != GDK_KEY_Escape || !gtk_widget_get_visible(findbox) ||
1654       gtk_widget_has_focus(command_entry))
1655     return false;
1656   return (gtk_widget_hide(findbox), gtk_widget_grab_focus(focused_view), true);
1657 }
1658 #endif
1659 
1660 /**
1661  * Removes the Scintilla view from the 'views' registry table.
1662  * The view must have been previously added with add_view.
1663  * @param L The Lua state.
1664  * @param view The Scintilla view to remove.
1665  * @see add_view
1666  */
remove_view(lua_State * L,Scintilla * view)1667 static void remove_view(lua_State *L, Scintilla *view) {
1668   lua_newtable(L);
1669   lua_getfield(L, LUA_REGISTRYINDEX, "ta_views");
1670   for (size_t i = 1; i <= lua_rawlen(L, -1); i++) {
1671     if (view != (lua_rawgeti(L, -1, i), lua_toview(L, -1))) {
1672       // t[widget_pointer] = view, t[#t + 1] = view, t[view] = #t
1673       lua_getfield(L, -1, "widget_pointer"), lua_pushvalue(L, -2),
1674         lua_settable(L, -5);
1675       lua_pushvalue(L, -1), lua_rawseti(L, -4, lua_rawlen(L, -4) + 1);
1676       lua_pushinteger(L, lua_rawlen(L, -3)), lua_settable(L, -4);
1677     } else lua_pop(L, 1); // view
1678   }
1679   lua_pop(L, 1); // views
1680   lua_pushvalue(L, -1), lua_setfield(L, LUA_REGISTRYINDEX, "ta_views");
1681   lua_setglobal(L, "_VIEWS");
1682 }
1683 
1684 /**
1685  * Removes a Scintilla view.
1686  * @param view The Scintilla view to remove.
1687  * @see remove_view
1688  */
delete_view(Scintilla * view)1689 static void delete_view(Scintilla *view) {
1690   remove_view(lua, view), scintilla_delete(view);
1691 }
1692 
1693 /**
1694  * Removes all Scintilla views from the given pane and deletes them along with
1695  * the child panes themselves.
1696  * @param pane The pane to remove Scintilla views from.
1697  * @see delete_view
1698  */
remove_views_from_pane(Pane * pane)1699 static void remove_views_from_pane(Pane *pane) {
1700 #if GTK
1701   GtkWidget *child1 = gtk_paned_get_child1(GTK_PANED(pane)),
1702     *child2 = gtk_paned_get_child2(GTK_PANED(pane));
1703   GTK_IS_PANED(child1) ? remove_views_from_pane(child1) : delete_view(child1);
1704   GTK_IS_PANED(child2) ? remove_views_from_pane(child2) : delete_view(child2);
1705 #elif CURSES
1706   if (pane->type == VSPLIT || pane->type == HSPLIT) {
1707     remove_views_from_pane(pane->child1), remove_views_from_pane(pane->child2);
1708     delwin(pane->win), pane->win = NULL; // delete split bar
1709   } else delete_view(pane->view);
1710   free(pane);
1711 #endif
1712 }
1713 
1714 #if CURSES
1715 /**
1716  * Resizes and repositions a pane.
1717  * @param pane the pane to resize and move.
1718  * @param rows The number of rows the pane should show.
1719  * @param cols The number of columns the pane should show.
1720  * @param y The y-coordinate to place the pane at.
1721  * @param x The x-coordinate to place the pane at.
1722  */
resize_pane(Pane * pane,int rows,int cols,int y,int x)1723 static void resize_pane(Pane *pane, int rows, int cols, int y, int x) {
1724   if (pane->type == VSPLIT) {
1725     int ssize = pane->split_size * cols / fmax(pane->cols, 1);
1726     if (ssize < 1 || ssize >= cols - 1) ssize = ssize < 1 ? 1 : cols - 2;
1727     pane->split_size = ssize;
1728     resize_pane(pane->child1, rows, ssize, y, x);
1729     resize_pane(pane->child2, rows, cols - ssize - 1, y, x + ssize + 1);
1730     wresize(pane->win, rows, 1), mvwin(pane->win, y, x + ssize); // split bar
1731   } else if (pane->type == HSPLIT) {
1732     int ssize = pane->split_size * rows / fmax(pane->rows, 1);
1733     if (ssize < 1 || ssize >= rows - 1) ssize = ssize < 1 ? 1 : rows - 2;
1734     pane->split_size = ssize;
1735     resize_pane(pane->child1, ssize, cols, y, x);
1736     resize_pane(pane->child2, rows - ssize - 1, cols, y + ssize + 1, x);
1737     wresize(pane->win, 1, cols), mvwin(pane->win, y + ssize, x); // split bar
1738   } else wresize(pane->win, rows, cols), mvwin(pane->win, y, x);
1739   pane->rows = rows, pane->cols = cols, pane->y = y, pane->x = x;
1740 }
1741 
1742 /**
1743  * Helper for unsplitting a view.
1744  * @param pane The pane that contains the view to unsplit.
1745  * @param view The view to unsplit.
1746  * @param parent The parent of pane. Used recursively.
1747  * @return true if the view can be split and was; false otherwise
1748  * @see unsplit_view
1749  */
unsplit_pane(Pane * pane,Scintilla * view,Pane * parent)1750 static bool unsplit_pane(Pane *pane, Scintilla *view, Pane *parent) {
1751   if (pane->type != SINGLE)
1752     return unsplit_pane(pane->child1, view, pane) ||
1753       unsplit_pane(pane->child2, view, pane);
1754   if (pane->view != view) return false;
1755   remove_views_from_pane(
1756     pane == parent->child1 ? parent->child2 : parent->child1);
1757   delwin(parent->win); // delete split bar
1758   // Inherit child's properties.
1759   parent->type = pane->type, parent->split_size = pane->split_size;
1760   parent->win = pane->win, parent->view = pane->view;
1761   parent->child1 = pane->child1, parent->child2 = pane->child2;
1762   free(pane);
1763   // Update.
1764   resize_pane(parent, parent->rows, parent->cols, parent->y, parent->x);
1765   return true;
1766 }
1767 #endif
1768 
1769 /**
1770  * Unsplits the pane the given Scintilla view is in and keeps the view.
1771  * All views in the other pane are deleted.
1772  * @param view The Scintilla view to keep when unsplitting.
1773  * @return true if the view was split; false otherwise
1774  * @see remove_views_from_pane
1775  * @see delete_view
1776  */
unsplit_view(Scintilla * view)1777 static bool unsplit_view(Scintilla *view) {
1778 #if GTK
1779   GtkWidget *pane = gtk_widget_get_parent(view);
1780   if (!GTK_IS_PANED(pane)) return false;
1781   GtkWidget *other = gtk_paned_get_child1(GTK_PANED(pane)) != view ?
1782     gtk_paned_get_child1(GTK_PANED(pane)) :
1783     gtk_paned_get_child2(GTK_PANED(pane));
1784   g_object_ref(view), g_object_ref(other);
1785   gtk_container_remove(GTK_CONTAINER(pane), view);
1786   gtk_container_remove(GTK_CONTAINER(pane), other);
1787   GTK_IS_PANED(other) ? remove_views_from_pane(other) : delete_view(other);
1788   GtkWidget *parent = gtk_widget_get_parent(pane);
1789   gtk_container_remove(GTK_CONTAINER(parent), pane);
1790   if (GTK_IS_PANED(parent))
1791     !gtk_paned_get_child1(GTK_PANED(parent)) ?
1792       gtk_paned_add1(GTK_PANED(parent), view) :
1793       gtk_paned_add2(GTK_PANED(parent), view);
1794   else
1795     gtk_container_add(GTK_CONTAINER(parent), view);
1796   //gtk_widget_show_all(parent);
1797   gtk_widget_grab_focus(GTK_WIDGET(view));
1798   g_object_unref(view), g_object_unref(other);
1799 #elif CURSES
1800   if (pane->type == SINGLE) return false;
1801   unsplit_pane(pane, view, NULL), scintilla_noutrefresh(view);
1802 #endif
1803   return true;
1804 }
1805 
1806 /**
1807  * Closes the Lua state.
1808  * Unsplits and destroys all Scintilla views and removes all Scintilla
1809  * documents, before closing the state.
1810  * @param L The Lua state.
1811  */
close_lua(lua_State * L)1812 static void close_lua(lua_State *L) {
1813   closing = true;
1814   while (unsplit_view(focused_view)) ; // need space to fix compiler warning
1815   lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers");
1816   for (size_t i = 1; i <= lua_rawlen(L, -1); i++)
1817     lua_rawgeti(L, -1, i), delete_buffer(lua_todoc(L, -1)), lua_pop(L, 1);
1818   lua_pop(L, 1); // buffers
1819   scintilla_delete(focused_view), scintilla_delete(dummy_view);
1820   scintilla_delete(command_entry);
1821   lua_close(L);
1822 }
1823 
1824 #if GTK
1825 /**
1826  * Signal for exiting Textadept.
1827  * Generates a 'quit' event.
1828  * Closes the Lua state and releases resources.
1829  * @see close_lua
1830  */
exiting(GtkWidget * _,GdkEventAny * __,void * L)1831 static bool exiting(GtkWidget *_, GdkEventAny *__, void *L) {
1832   if (emit(L, "quit", -1)) return true; // halt
1833   close_lua(L);
1834   scintilla_release_resources();
1835   return (gtk_main_quit(), false);
1836 }
1837 
1838 #if (__APPLE__ && !CURSES)
1839 /**
1840  * Signal for opening files from macOS.
1841  * Generates an 'appleevent_odoc' event for each document sent.
1842  */
open_file(GtkosxApplication * _,char * path,void * L)1843 static bool open_file(GtkosxApplication*_, char *path, void *L) {
1844   return (emit(L, "appleevent_odoc", LUA_TSTRING, path, -1), true);
1845 }
1846 
1847 /**
1848  * Signal for block terminating Textadept from macOS.
1849  * Generates a 'quit' event.
1850  */
terminating(GtkosxApplication * _,void * L)1851 static bool terminating(GtkosxApplication *_, void *L) {
1852   return emit(L, "quit", -1);
1853 }
1854 
1855 /**
1856  * Signal for terminating Textadept from macOS.
1857  * Closes the Lua state and releases resources.
1858  * @see close_lua
1859  */
terminate(GtkosxApplication * _,void * L)1860 static void terminate(GtkosxApplication *_, void *L) {
1861   close_lua(L);
1862   scintilla_release_resources();
1863   g_object_unref(osxapp);
1864   gtk_main_quit();
1865 }
1866 #endif
1867 
1868 /**
1869  * Signal for switching buffer tabs.
1870  * When triggered by the user (i.e. not synchronizing the tabbar), switches to
1871  * the specified buffer.
1872  * Generates 'buffer_before_switch' and 'buffer_after_switch' events.
1873  */
tab_changed(GtkNotebook * _,GtkWidget * __,int tab_num,void * L)1874 static void tab_changed(GtkNotebook *_, GtkWidget *__, int tab_num, void *L) {
1875   if (!tab_sync)
1876     emit(L, "tab_clicked", LUA_TNUMBER, tab_num + 1, LUA_TNUMBER, 1, -1);
1877 }
1878 #endif // if GTK
1879 
1880 /**
1881  * Emits a Scintilla notification event.
1882  * @param L The Lua state.
1883  * @param n The Scintilla notification struct.
1884  * @see emit
1885  */
emit_notification(lua_State * L,SCNotification * n)1886 static void emit_notification(lua_State *L, SCNotification *n) {
1887   lua_newtable(L);
1888   lua_pushinteger(L, n->nmhdr.code), lua_setfield(L, -2, "code");
1889   lua_pushinteger(L, n->position + 1), lua_setfield(L, -2, "position");
1890   lua_pushinteger(L, n->ch), lua_setfield(L, -2, "ch");
1891   lua_pushinteger(L, n->modifiers), lua_setfield(L, -2, "modifiers");
1892   lua_pushinteger(L, n->modificationType),
1893     lua_setfield(L, -2, "modification_type");
1894   if (n->text)
1895     lua_pushlstring(L, n->text, n->length ? n->length : strlen(n->text)),
1896       lua_setfield(L, -2, "text");
1897   lua_pushinteger(L, n->length), lua_setfield(L, -2, "length"); // SCN_MODIFIED
1898   //lua_pushinteger(L, n->linesAdded), lua_setfield(L, -2, "lines_added");
1899   //lua_pushinteger(L, n->message), lua_setfield(L, -2, "message");
1900   lua_pushinteger(L, n->listType), lua_setfield(L, -2, "list_type");
1901   //lua_pushinteger(L, n->lParam), lua_setfield(L, -2, "lParam");
1902   lua_pushinteger(L, n->line + 1), lua_setfield(L, -2, "line");
1903   //lua_pushinteger(L, n->foldLevelNow), lua_setfield(L, -2, "fold_level_now");
1904   //lua_pushinteger(L, n->foldLevelPrev),
1905   //  lua_setfield(L, -2, "fold_level_prev");
1906   lua_pushinteger(L, n->margin + 1), lua_setfield(L, -2, "margin");
1907   lua_pushinteger(L, n->x), lua_setfield(L, -2, "x");
1908   lua_pushinteger(L, n->y), lua_setfield(L, -2, "y");
1909   //lua_pushinteger(L, n->token), lua_setfield(L, -2, "token");
1910   //lua_pushinteger(L, n->annotationLinesAdded),
1911   //  lua_setfield(L, -2, "annotation_lines_added");
1912   lua_pushinteger(L, n->updated), lua_setfield(L, -2, "updated");
1913   //lua_pushinteger(L, n->listCompletionMethod),
1914   //  lua_setfield(L, -2, "list_completion_method");
1915   emit(L, "SCN", LUA_TTABLE, luaL_ref(L, LUA_REGISTRYINDEX), -1);
1916 }
1917 
1918 /** Signal for a Scintilla notification. */
notified(Scintilla * view,int _,SCNotification * n,void * L)1919 static void notified(Scintilla *view, int _, SCNotification *n, void *L) {
1920   if (view == command_entry) {
1921     if (n->nmhdr.code == SCN_MODIFIED &&
1922         (n->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)))
1923       emit(L, "command_text_changed", -1);
1924   } else if (view == focused_view || n->nmhdr.code == SCN_URIDROPPED) {
1925     if (view != focused_view) view_focused(view, L);
1926     emit_notification(L, n);
1927   } else if (n->nmhdr.code == SCN_FOCUSIN)
1928     view_focused(view, L);
1929 }
1930 
1931 #if GTK
1932 /**
1933  * Signal for a Scintilla keypress.
1934  * Note: cannot use bool return value due to modern i686-w64-mingw32-gcc issue.
1935  */
keypress(GtkWidget * _,GdkEventKey * event,void * L)1936 static int keypress(GtkWidget *_, GdkEventKey *event, void *L) {
1937   return emit(
1938     L, "keypress", LUA_TNUMBER, event->keyval,
1939     LUA_TBOOLEAN, event->state & GDK_SHIFT_MASK,
1940     LUA_TBOOLEAN, event->state & GDK_CONTROL_MASK,
1941     LUA_TBOOLEAN, event->state & GDK_MOD1_MASK,
1942     LUA_TBOOLEAN, event->state & GDK_META_MASK,
1943     LUA_TBOOLEAN, event->state & GDK_LOCK_MASK, -1);
1944 }
1945 
1946 /** Signal for a Scintilla mouse click. */
mouse_clicked(GtkWidget * _,GdkEventButton * event,void * L)1947 static bool mouse_clicked(GtkWidget *_, GdkEventButton *event, void *L) {
1948   if (event->type != GDK_BUTTON_PRESS || event->button != 3) return false;
1949   return (show_context_menu(L, event, "context_menu"), true);
1950 }
1951 #endif
1952 
1953 /** `view.goto_buffer()` Lua function. */
goto_doc_lua(lua_State * L)1954 static int goto_doc_lua(lua_State *L) {
1955   Scintilla *view = luaL_checkview(L, 1), *prev_view = focused_view;
1956   bool relative = lua_isnumber(L, 2);
1957   if (!relative) {
1958     lua_getfield(L, LUA_REGISTRYINDEX, "ta_buffers"), lua_pushvalue(L, 2),
1959       lua_gettable(L, -2), lua_replace(L, 2);
1960     luaL_argcheck(
1961       L, lua_isnumber(L, 2), 2, "Buffer or relative index expected");
1962   }
1963   // If the indexed view is not currently focused, temporarily focus it so
1964   // `_G.buffer` in handlers is accurate.
1965   if (view != focused_view) focus_view(view);
1966   if (!initing) emit(L, "buffer_before_switch", -1);
1967   goto_doc(L, view, lua_tointeger(L, 2), relative);
1968   if (!initing) emit(L, "buffer_after_switch", -1);
1969   if (focused_view != prev_view) focus_view(prev_view);
1970   return 0;
1971 }
1972 
1973 #if CURSES
1974 /**
1975  * Creates a new pane that contains a Scintilla view.
1976  * @param view The Scintilla view to place in the pane.
1977  */
new_pane(Scintilla * view)1978 static Pane *new_pane(Scintilla *view) {
1979   Pane *p = calloc(1, sizeof(Pane));
1980   p->type = SINGLE, p->win = scintilla_get_window(view), p->view = view;
1981   return p;
1982 }
1983 
1984 /**
1985  * Helper for splitting a view.
1986  * Recursively propagates a split to child panes.
1987  * @param pane The pane to split.
1988  * @param vertical Whether to split the pane vertically or horizontally.
1989  * @param view The first Scintilla view to place in the split view.
1990  * @param view2 The second Scintilla view to place in the split view.
1991  * @see split_view
1992  */
split_pane(Pane * pane,bool vertical,Scintilla * view,Scintilla * view2)1993 static bool split_pane(
1994   Pane *pane, bool vertical, Scintilla *view, Scintilla *view2)
1995 {
1996   if (pane->type != SINGLE)
1997     return split_pane(pane->child1, vertical, view, view2) ||
1998       split_pane(pane->child2, vertical, view, view2);
1999   if (view != pane->view) return false;
2000   Pane *child1 = new_pane(view), *child2 = new_pane(view2);
2001   pane->type = vertical ? VSPLIT : HSPLIT;
2002   pane->child1 = child1, pane->child2 = child2, pane->view = NULL;
2003   // Resize children and create a split bar.
2004   if (vertical) {
2005     pane->split_size = pane->cols / 2;
2006     resize_pane(child1, pane->rows, pane->split_size, pane->y, pane->x);
2007     resize_pane(
2008       child2, pane->rows, pane->cols - pane->split_size - 1, pane->y,
2009       pane->x + pane->split_size + 1);
2010     pane->win = newwin(pane->rows, 1, pane->y, pane->x + pane->split_size);
2011   } else {
2012     pane->split_size = pane->rows / 2;
2013     resize_pane(child1, pane->split_size, pane->cols, pane->y, pane->x);
2014     resize_pane(
2015       child2, pane->rows - pane->split_size - 1, pane->cols,
2016       pane->y + pane->split_size + 1, pane->x);
2017     pane->win = newwin(1, pane->cols, pane->y + pane->split_size, pane->x);
2018   }
2019   return (refresh_pane(pane), true);
2020 }
2021 #endif
2022 
2023 /**
2024  * Splits the given Scintilla view into two views.
2025  * The new view shows the same document as the original one.
2026  * @param view The Scintilla view to split.
2027  * @param vertical Flag indicating whether to split the view vertically or
2028  *   horozontally.
2029  */
split_view(Scintilla * view,bool vertical)2030 static void split_view(Scintilla *view, bool vertical) {
2031   sptr_t curdoc = SS(view, SCI_GETDOCPOINTER, 0, 0);
2032   int first_line = SS(view, SCI_GETFIRSTVISIBLELINE, 0, 0),
2033     x_offset = SS(view, SCI_GETXOFFSET, 0, 0),
2034     current_pos = SS(view, SCI_GETCURRENTPOS, 0, 0),
2035     anchor = SS(view, SCI_GETANCHOR, 0, 0);
2036 
2037 #if GTK
2038   GtkAllocation allocation;
2039   gtk_widget_get_allocation(view, &allocation);
2040   int middle = (vertical ? allocation.width : allocation.height) / 2;
2041 
2042   GtkWidget *parent = gtk_widget_get_parent(view);
2043   if (!parent) return; // error on startup (e.g. loading theme or settings)
2044   GtkWidget *view2 = new_view(curdoc);
2045   g_object_ref(view);
2046   gtk_container_remove(GTK_CONTAINER(parent), view);
2047   GtkWidget *pane = vertical ? gtk_hpaned_new() : gtk_vpaned_new();
2048   gtk_paned_add1(GTK_PANED(pane), view), gtk_paned_add2(GTK_PANED(pane), view2);
2049   gtk_container_add(GTK_CONTAINER(parent), pane);
2050   gtk_paned_set_position(GTK_PANED(pane), middle);
2051   gtk_widget_show_all(pane);
2052   g_object_unref(view);
2053 
2054   while (gtk_events_pending()) gtk_main_iteration(); // ensure view2 is painted
2055 #elif CURSES
2056   Scintilla *view2 = new_view(curdoc);
2057   split_pane(pane, vertical, view, view2);
2058 #endif
2059   focus_view(view2);
2060 
2061   SS(view2, SCI_SETSEL, anchor, current_pos);
2062   SS(view2, SCI_LINESCROLL, first_line - SS(
2063     view2, SCI_GETFIRSTVISIBLELINE, 0, 0), 0);
2064   SS(view2, SCI_SETXOFFSET, x_offset, 0);
2065 }
2066 
2067 /** `view.split()` Lua function. */
split_view_lua(lua_State * L)2068 static int split_view_lua(lua_State *L) {
2069   split_view(luaL_checkview(L, 1), lua_toboolean(L, 2));
2070   return (lua_pushvalue(L, 1), lua_getglobal(L, "view"), 2); // old, new view
2071 }
2072 
2073 /** `view.unsplit()` Lua function. */
unsplit_view_lua(lua_State * L)2074 static int unsplit_view_lua(lua_State *L) {
2075   return (lua_pushboolean(L, unsplit_view(luaL_checkview(L, 1))), 1);
2076 }
2077 
2078 #if CURSES
2079 /**
2080  * Searches for the given view and returns its parent pane.
2081  * @param pane The pane that contains the desired view.
2082  * @param view The view to get the parent pane of.
2083  */
get_parent_pane(Pane * pane,Scintilla * view)2084 static Pane *get_parent_pane(Pane *pane, Scintilla *view) {
2085   if (pane->type == SINGLE) return NULL;
2086   if (pane->child1->view == view || pane->child2->view == view) return pane;
2087   Pane *parent = get_parent_pane(pane->child1, view);
2088   return parent ? parent : get_parent_pane(pane->child2, view);
2089 }
2090 #endif
2091 
2092 /** `view.__index` metamethod. */
view_index(lua_State * L)2093 static int view_index(lua_State *L) {
2094   if (strcmp(lua_tostring(L, 2), "buffer") == 0)
2095     lua_pushdoc(L, SS(lua_toview(L, 1), SCI_GETDOCPOINTER, 0, 0));
2096   else if (strcmp(lua_tostring(L, 2), "size") == 0) {
2097     lua_pushnil(L); // default
2098     Pane *p;
2099 #if GTK
2100     if (GTK_IS_PANED(p = gtk_widget_get_parent(lua_toview(L, 1))))
2101       lua_pushinteger(L, gtk_paned_get_position(GTK_PANED(p)));
2102 #elif CURSES
2103     if ((p = get_parent_pane(pane, lua_toview(L, 1))))
2104       lua_pushinteger(L, p->split_size);
2105 #endif
2106   } else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_functions"),
2107              lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TTABLE)
2108     // If the key is a Scintilla function, return a callable closure.
2109     lua_pushcclosure(L, call_scintilla_lua, 1);
2110   else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_properties"),
2111            lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TTABLE)
2112     // If the key is a Scintilla property, determine if it is an indexible one
2113     // or not. If so, return a table with the appropriate metatable; otherwise
2114     // call Scintilla to get the property's value.
2115     get_property(L);
2116   else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_constants"),
2117            lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TNUMBER); // pushed
2118     // If the key is a Scintilla constant, return its value.
2119   else
2120     lua_settop(L, 2), lua_rawget(L, 1);
2121   return 1;
2122 }
2123 
2124 /** `view.__newindex` metamethod. */
view_newindex(lua_State * L)2125 static int view_newindex(lua_State *L) {
2126   if (strcmp(lua_tostring(L, 2), "buffer") == 0)
2127     luaL_argerror(L, 2, "read-only property");
2128   else if (strcmp(lua_tostring(L, 2), "size") == 0) {
2129     Pane *p;
2130 #if GTK
2131     if (GTK_IS_PANED(p = gtk_widget_get_parent(lua_toview(L, 1))))
2132       gtk_paned_set_position(GTK_PANED(p), fmax(luaL_checkinteger(L, 3), 0));
2133 #elif CURSES
2134     if ((p = get_parent_pane(pane, lua_toview(L, 1))))
2135       p->split_size = fmax(luaL_checkinteger(L, 3), 0),
2136         resize_pane(p, p->rows, p->cols, p->y, p->x);
2137 #endif
2138   } else if (lua_getfield(L, LUA_REGISTRYINDEX, "ta_properties"),
2139              lua_pushvalue(L, 2), lua_rawget(L, -2) == LUA_TTABLE)
2140     set_property(L);
2141   else
2142     lua_settop(L, 3), lua_rawset(L, 1);
2143   return 0;
2144 }
2145 
2146 /**
2147  * Adds the Scintilla view with a metatable to the 'views' registry table.
2148  * @param L The Lua state.
2149  * @param view The Scintilla view to add.
2150  */
add_view(lua_State * L,Scintilla * view)2151 static void add_view(lua_State *L, Scintilla *view) {
2152   lua_getfield(L, LUA_REGISTRYINDEX, "ta_views");
2153   lua_newtable(L);
2154   lua_pushlightuserdata(L, view), lua_setfield(L, -2, "widget_pointer");
2155   lua_pushcfunction(L, goto_doc_lua), lua_setfield(L, -2, "goto_buffer");
2156   lua_pushcfunction(L, split_view_lua), lua_setfield(L, -2, "split");
2157   lua_pushcfunction(L, unsplit_view_lua), lua_setfield(L, -2, "unsplit");
2158   set_metatable(L, -1, "ta_view", view_index, view_newindex);
2159   // t[widget_pointer] = view, t[#t + 1] = view, t[view] = #t
2160   lua_getfield(L, -1, "widget_pointer"), lua_pushvalue(L, -2),
2161     lua_settable(L, -4);
2162   lua_pushvalue(L, -1), lua_rawseti(L, -3, lua_rawlen(L, -3) + 1);
2163   lua_pushinteger(L, lua_rawlen(L, -2)), lua_settable(L, -3);
2164   lua_pop(L, 1); // views
2165 }
2166 
2167 /**
2168  * Creates a new Scintilla view.
2169  * Generates a 'view_new' event.
2170  * @param doc The document to load in the new view. Almost never zero, except
2171  *   for the first Scintilla view created, in which there is no doc pointer.
2172  * @return Scintilla view
2173  * @see add_view
2174  */
new_view(sptr_t doc)2175 static Scintilla *new_view(sptr_t doc) {
2176 #if GTK
2177   Scintilla *view = scintilla_new();
2178   gtk_widget_set_size_request(view, 1, 1); // minimum size
2179   g_signal_connect(view, SCINTILLA_NOTIFY, G_CALLBACK(notified), lua);
2180   g_signal_connect(view, "key-press-event", G_CALLBACK(keypress), lua);
2181   g_signal_connect(view, "button-press-event", G_CALLBACK(mouse_clicked), lua);
2182 #elif CURSES
2183   Scintilla *view = scintilla_new(notified, lua);
2184 #endif
2185   SS(view, SCI_USEPOPUP, SC_POPUP_NEVER, 0);
2186   add_view(lua, view);
2187   lua_pushview(lua, view), lua_setglobal(lua, "view");
2188   if (doc) SS(view, SCI_SETDOCPOINTER, 0, doc);
2189   focus_view(view), focused_view = view;
2190   if (!doc) new_buffer(SS(view, SCI_GETDOCPOINTER, 0, 0));
2191   if (!initing) emit(lua, "view_new", -1);
2192   return view;
2193 }
2194 
2195 #if GTK
2196 /** Signal for a Find/Replace entry keypress. */
find_keypress(GtkWidget * widget,GdkEventKey * event,void * L)2197 static bool find_keypress(GtkWidget *widget, GdkEventKey *event, void *L) {
2198   if (event->keyval != GDK_KEY_Return) return false;
2199   FindButton button = (event->state & GDK_SHIFT_MASK) == 0 ?
2200     (widget == find_entry ? find_next : replace) :
2201     (widget == find_entry ? find_prev : replace_all);
2202   return (find_clicked(button, L), true);
2203 }
2204 
2205 /**
2206  * Creates and returns for the findbox a new GtkComboBoxEntry, storing its
2207  * GtkLabel, GtkEntry, and GtkListStore in the given pointers.
2208  */
new_combo(GtkWidget ** label,GtkWidget ** entry,ListStore ** history)2209 static GtkWidget *new_combo(
2210   GtkWidget **label, GtkWidget **entry, ListStore **history)
2211 {
2212   *label = gtk_label_new(""); // localized label text set later via Lua
2213   *history = gtk_list_store_new(1, G_TYPE_STRING);
2214   GtkWidget *combo = gtk_combo_box_entry_new_with_model(
2215     GTK_TREE_MODEL(*history), 0);
2216   g_object_unref(*history);
2217   gtk_combo_box_entry_set_text_column(GTK_COMBO_BOX_ENTRY(combo), 0);
2218   gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(combo), false);
2219   *entry = gtk_bin_get_child(GTK_BIN(combo));
2220   gtk_entry_set_text(GTK_ENTRY(*entry), " "),
2221     gtk_entry_set_text(GTK_ENTRY(*entry), ""); // initialize with non-NULL
2222   PangoFontDescription *font = pango_font_description_new();
2223   pango_font_description_set_family_static(font, "monospace");
2224   gtk_widget_modify_font(*entry, font);
2225   pango_font_description_free(font);
2226   gtk_label_set_mnemonic_widget(GTK_LABEL(*label), *entry);
2227   g_signal_connect(*entry, "key-press-event", G_CALLBACK(find_keypress), lua);
2228   return combo;
2229 }
2230 
2231 /** Signal for a Find entry keypress. */
find_changed(GtkEditable * _,void * L)2232 static void find_changed(GtkEditable *_, void *L) {
2233   emit(L, "find_text_changed", -1);
2234 }
2235 
2236 /** Creates and returns a new button for the findbox. */
new_button()2237 static GtkWidget *new_button() {
2238   GtkWidget *button = gtk_button_new_with_mnemonic(""); // localized via Lua
2239   g_signal_connect(button, "clicked", G_CALLBACK(find_clicked), lua);
2240   gtk_widget_set_can_focus(button, false);
2241   return button;
2242 }
2243 
2244 /** Creates and returns a new checkbox option for the findbox. */
new_option()2245 static GtkWidget *new_option() {
2246   GtkWidget *option = gtk_check_button_new_with_mnemonic(""); // localized later
2247   gtk_widget_set_can_focus(option, false);
2248   return option;
2249 }
2250 
2251 /** Creates the Find box. */
new_findbox()2252 static GtkWidget *new_findbox() {
2253   findbox = gtk_table_new(2, 6, false);
2254 
2255   GtkWidget *find_combo = new_combo(&find_label, &find_entry, &find_history),
2256     *replace_combo = new_combo(&repl_label, &repl_entry, &repl_history);
2257   g_signal_connect(
2258     GTK_EDITABLE(find_entry), "changed", G_CALLBACK(find_changed), lua);
2259   find_next = new_button(), find_prev = new_button(), replace = new_button(),
2260     replace_all = new_button(), match_case = new_option(),
2261     whole_word = new_option(), regex = new_option(), in_files = new_option();
2262 
2263   GtkTable *table = GTK_TABLE(findbox);
2264   int expand = GTK_FILL | GTK_EXPAND, shrink = GTK_FILL | GTK_SHRINK;
2265   gtk_table_attach(table, find_label, 0, 1, 0, 1, shrink, shrink, 5, 0);
2266   gtk_table_attach(table, repl_label, 0, 1, 1, 2, shrink, shrink, 5, 0);
2267   gtk_table_attach(table, find_combo, 1, 2, 0, 1, expand, shrink, 5, 0);
2268   gtk_table_attach(table, replace_combo, 1, 2, 1, 2, expand, shrink, 5, 0);
2269   gtk_table_attach(table, find_next, 2, 3, 0, 1, shrink, shrink, 0, 0);
2270   gtk_table_attach(table, find_prev, 3, 4, 0, 1, shrink, shrink, 0, 0);
2271   gtk_table_attach(table, replace, 2, 3, 1, 2, shrink, shrink, 0, 0);
2272   gtk_table_attach(table, replace_all, 3, 4, 1, 2, shrink, shrink, 0, 0);
2273   gtk_table_attach(table, match_case, 4, 5, 0, 1, shrink, shrink, 5, 0);
2274   gtk_table_attach(table, whole_word, 4, 5, 1, 2, shrink, shrink, 5, 0);
2275   gtk_table_attach(table, regex, 5, 6, 0, 1, shrink, shrink, 5, 0);
2276   gtk_table_attach(table, in_files, 5, 6, 1, 2, shrink, shrink, 5, 0);
2277 
2278   return findbox;
2279 }
2280 
2281 /**
2282  * Signal for window or command entry focus loss.
2283  * Emit "Escape" key for the command entry on focus lost unless the window is
2284  * losing focus or the application is quitting.
2285  */
focus_lost(GtkWidget * widget,GdkEvent * _,void * L)2286 static bool focus_lost(GtkWidget *widget, GdkEvent *_, void *L) {
2287   if (widget == window) {
2288     if (!dialog_active) emit(L, "unfocus", -1);
2289     if (command_entry_active) return true; // keep focus if window losing focus
2290   } else if (!closing)
2291     emit(L, "keypress", LUA_TNUMBER, GDK_KEY_Escape, -1);
2292   return false;
2293 }
2294 #endif // if GTK
2295 
2296 /**
2297  * Creates the Textadept window.
2298  * The window contains a menubar, frame for Scintilla views, hidden find box,
2299  * hidden command entry, and two status bars: one for notifications and the
2300  * other for buffer status.
2301  */
new_window()2302 static void new_window() {
2303 #if GTK
2304   GList *icon_list = NULL;
2305   const char *icons[] = {"16x16", "32x32", "48x48", "64x64", "128x128"};
2306   for (int i = 0; i < 5; i++) {
2307     char icon_file[strlen(textadept_home) + 30];
2308     sprintf(icon_file, "%s/core/images/ta_%s.png", textadept_home, icons[i]);
2309     GdkPixbuf *icon = gdk_pixbuf_new_from_file(icon_file, NULL);
2310     if (icon) icon_list = g_list_prepend(icon_list, icon);
2311   }
2312   gtk_window_set_default_icon_list(icon_list);
2313   g_list_foreach(icon_list, (GFunc)g_object_unref, NULL);
2314   g_list_free(icon_list);
2315 
2316   window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2317   gtk_widget_set_name(window, "textadept");
2318   gtk_window_set_default_size(GTK_WINDOW(window), 1000, 600);
2319   g_signal_connect(window, "delete-event", G_CALLBACK(exiting), lua);
2320   g_signal_connect(window, "focus-in-event", G_CALLBACK(window_focused), lua);
2321   g_signal_connect(window, "focus-out-event", G_CALLBACK(focus_lost), lua);
2322   g_signal_connect(
2323     window, "key-press-event", G_CALLBACK(window_keypress), NULL);
2324   gtdialog_set_parent(GTK_WINDOW(window));
2325   accel = gtk_accel_group_new();
2326 
2327 #if (__APPLE__ && !CURSES)
2328   gtkosx_application_set_use_quartz_accelerators(osxapp, false);
2329   g_signal_connect(osxapp, "NSApplicationOpenFile", G_CALLBACK(open_file), lua);
2330   g_signal_connect(
2331     osxapp, "NSApplicationBlockTermination", G_CALLBACK(terminating), lua);
2332   g_signal_connect(
2333     osxapp, "NSApplicationWillTerminate", G_CALLBACK(terminate), lua);
2334 #endif
2335 
2336   GtkWidget *vbox = gtk_vbox_new(false, 0);
2337   gtk_container_add(GTK_CONTAINER(window), vbox);
2338 
2339   menubar = gtk_menu_bar_new();
2340   gtk_box_pack_start(GTK_BOX(vbox), menubar, false, false, 0);
2341 
2342   tabbar = gtk_notebook_new();
2343   g_signal_connect(tabbar, "switch-page", G_CALLBACK(tab_changed), lua);
2344   gtk_notebook_set_scrollable(GTK_NOTEBOOK(tabbar), true);
2345   gtk_widget_set_can_focus(tabbar, false);
2346   gtk_box_pack_start(GTK_BOX(vbox), tabbar, false, false, 0);
2347 
2348   GtkWidget *paned = gtk_vpaned_new();
2349   gtk_box_pack_start(GTK_BOX(vbox), paned, true, true, 0);
2350 
2351   GtkWidget *vboxp = gtk_vbox_new(false, 0);
2352   gtk_paned_add1(GTK_PANED(paned), vboxp);
2353 
2354   GtkWidget *hbox = gtk_hbox_new(false, 0);
2355   gtk_box_pack_start(GTK_BOX(vboxp), hbox, true, true, 0);
2356 
2357   gtk_box_pack_start(GTK_BOX(hbox), new_view(0), true, true, 0);
2358   gtk_widget_grab_focus(focused_view);
2359 
2360   gtk_box_pack_start(GTK_BOX(vboxp), new_findbox(), false, false, 5);
2361 
2362   command_entry = scintilla_new();
2363   gtk_widget_set_size_request(command_entry, 1, 1);
2364   g_signal_connect(command_entry, SCINTILLA_NOTIFY, G_CALLBACK(notified), lua);
2365   g_signal_connect(command_entry, "key-press-event", G_CALLBACK(keypress), lua);
2366   g_signal_connect(
2367     command_entry, "focus-out-event", G_CALLBACK(focus_lost), lua);
2368   gtk_paned_add2(GTK_PANED(paned), command_entry);
2369   gtk_container_child_set(
2370     GTK_CONTAINER(paned), command_entry, "shrink", false, NULL);
2371 
2372   GtkWidget *hboxs = gtk_hbox_new(false, 0);
2373   gtk_box_pack_start(GTK_BOX(vbox), hboxs, false, false, 1);
2374 
2375   statusbar[0] = gtk_label_new(NULL), statusbar[1] = gtk_label_new(NULL);
2376   gtk_box_pack_start(GTK_BOX(hboxs), statusbar[0], true, true, 5);
2377   gtk_misc_set_alignment(GTK_MISC(statusbar[0]), 0, 0);
2378   gtk_box_pack_start(GTK_BOX(hboxs), statusbar[1], true, true, 5);
2379   gtk_misc_set_alignment(GTK_MISC(statusbar[1]), 1, 0);
2380 
2381   gtk_widget_show_all(window);
2382   gtk_widget_hide(menubar), gtk_widget_hide(tabbar),
2383     gtk_widget_hide(findbox), gtk_widget_hide(command_entry); // hide initially
2384 
2385   dummy_view = scintilla_new();
2386 #elif CURSES
2387   pane = new_pane(new_view(0)), resize_pane(pane, LINES - 2, COLS, 1, 0);
2388   command_entry = scintilla_new(notified, lua);
2389   wresize(scintilla_get_window(command_entry), 1, COLS);
2390   mvwin(scintilla_get_window(command_entry), LINES - 2, 0);
2391   dummy_view = scintilla_new(NULL, NULL);
2392 #endif
2393   SS(command_entry, SCI_SETILEXER, 0, (sptr_t)CreateLexer(NULL));
2394   register_command_entry_doc();
2395 }
2396 
2397 #if GTK && _WIN32
2398 /** Reads and processes a remote Textadept's command line arguments. */
read_pipe(GIOChannel * source,GIOCondition _,HANDLE pipe)2399 static bool read_pipe(GIOChannel *source, GIOCondition _, HANDLE pipe) {
2400   char *buf;
2401   size_t len;
2402   g_io_channel_read_to_end(source, &buf, &len, NULL);
2403   for (char *p = buf; p < buf + len - 2; p++) if (!*p) *p = '\n'; // '\0\0' end
2404   process(NULL, NULL, buf);
2405   return (g_free(buf), DisconnectNamedPipe(pipe), false);
2406 }
2407 
2408 /** Listens for remote Textadept communications. */
pipe_listener(HANDLE pipe)2409 static DWORD WINAPI pipe_listener(HANDLE pipe) {
2410   while (true)
2411     if (pipe != INVALID_HANDLE_VALUE && ConnectNamedPipe(pipe, NULL)) {
2412       GIOChannel *channel = g_io_channel_win32_new_fd(
2413         _open_osfhandle((intptr_t)pipe, _O_RDONLY));
2414       g_io_add_watch(channel, G_IO_IN, read_pipe, pipe),
2415         g_io_channel_unref(channel);
2416     }
2417   return 0;
2418 }
2419 
2420 /** Replacement for `g_application_run()` that handles multiple instances. */
g_application_run(GApplication * _,int __,char ** ___)2421 int g_application_run(GApplication *_, int __, char **___) {
2422   HANDLE pipe = CreateFile(ID, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
2423   char cwd[FILENAME_MAX + 1]; // TODO: is this big enough?
2424   GetCurrentDirectory(FILENAME_MAX + 1, cwd);
2425   DWORD len_written;
2426   WriteFile(pipe, cwd, strlen(cwd) + 1, &len_written, NULL);
2427   for (int i = 1; i < __argc; i++)
2428     WriteFile(pipe, __argv[i], strlen(__argv[i]) + 1, &len_written, NULL);
2429   return (CloseHandle(pipe), 0);
2430 }
2431 #endif
2432 
2433 #if CURSES
2434 #if !_WIN32
2435 /**
2436  * Signal for a terminal suspend, continue, and resize.
2437  * libtermkey has been patched to enable suspend as well as enable/disable mouse
2438  * mode (1002).
2439  */
signalled(int signal)2440 static void signalled(int signal) {
2441   if (signal != SIGTSTP) {
2442     if (signal == SIGCONT) termkey_start(ta_tk);
2443     struct winsize w;
2444     ioctl(0, TIOCGWINSZ, &w);
2445     resizeterm(w.ws_row, w.ws_col), resize_pane(pane, LINES - 2, COLS, 1, 0);
2446     WINDOW *win = scintilla_get_window(command_entry);
2447     wresize(win, 1, COLS), mvwin(win, LINES - 1 - getmaxy(win), 0);
2448     if (signal == SIGCONT) emit(lua, "resume", -1);
2449     emit(lua, "update_ui", LUA_TNUMBER, 0, -1);
2450   } else if (!emit(lua, "suspend", -1))
2451     endwin(), termkey_stop(ta_tk), kill(0, SIGSTOP);
2452   refresh_all();
2453 }
2454 #endif
2455 
2456 /** Replacement for `termkey_waitkey()` that handles asynchronous I/O. */
textadept_waitkey(TermKey * tk,TermKeyKey * key)2457 static TermKeyResult textadept_waitkey(TermKey *tk, TermKeyKey *key) {
2458 #if !_WIN32
2459   bool force = false;
2460   struct timeval timeout = {0, termkey_get_waittime(tk)};
2461   while (true) {
2462     TermKeyResult res = !force ?
2463       termkey_getkey(tk, key) : termkey_getkey_force(tk, key);
2464     if (res != TERMKEY_RES_AGAIN && res != TERMKEY_RES_NONE) return res;
2465     if (res == TERMKEY_RES_AGAIN) force = true;
2466     // Wait for input.
2467     int nfds = os_spawn_pushfds(lua);
2468     fd_set *fds = lua_touserdata(lua, -1);
2469     FD_SET(0, fds); // monitor stdin
2470     if (select(nfds, fds, NULL, NULL, force ? &timeout : NULL) > 0) {
2471       if (FD_ISSET(0, fds)) termkey_advisereadable(tk);
2472       if (os_spawn_readfds(lua) > 0) refresh_all();
2473     }
2474     lua_pop(lua, 1); // fd_set
2475   }
2476 #else
2477   // TODO: ideally computation of view would not be done twice.
2478   Scintilla *view = !command_entry_active ? focused_view : command_entry;
2479   termkey_set_fd(ta_tk, scintilla_get_window(view));
2480   mouse_set(ALL_MOUSE_EVENTS); // _popen() and system() change console mode
2481   return termkey_getkey(tk, key);
2482 #endif
2483 }
2484 #endif
2485 
2486 /**
2487  * Runs Textadept.
2488  * Initializes the Lua state, creates the user interface, and then runs
2489  * `core/init.lua` followed by `init.lua`.
2490  * On Windows, creates a pipe and thread for communication with remote
2491  * instances.
2492  * @param argc The number of command line params.
2493  * @param argv The array of command line params.
2494  */
main(int argc,char ** argv)2495 int main(int argc, char **argv) {
2496 #if GTK
2497   gtk_init(&argc, &argv);
2498 #elif CURSES
2499   ta_tk = termkey_new(0, 0);
2500   setlocale(LC_CTYPE, ""); // for displaying UTF-8 characters properly
2501   initscr(); // raw()/cbreak() and noecho() are taken care of in libtermkey
2502   curs_set(0); // disable cursor when Scintilla has focus
2503 #if NCURSES_REENTRANT
2504   ESCDELAY = getenv("ESCDELAY") ? atoi(getenv("ESCDELAY")) : 100;
2505 #endif
2506 #endif
2507 
2508   char *last_slash = NULL;
2509 #if __linux__
2510   textadept_home = malloc(FILENAME_MAX + 1);
2511   int len = readlink("/proc/self/exe", textadept_home, FILENAME_MAX + 1);
2512   textadept_home[len] = '\0';
2513   if ((last_slash = strrchr(textadept_home, '/'))) *last_slash = '\0';
2514   platform = "LINUX";
2515 #elif _WIN32
2516   textadept_home = malloc(FILENAME_MAX + 1);
2517   GetModuleFileName(NULL, textadept_home, FILENAME_MAX + 1);
2518   if ((last_slash = strrchr(textadept_home, '\\'))) *last_slash = '\0';
2519   platform = "WIN32";
2520 #elif __APPLE__
2521   char *path = malloc(FILENAME_MAX + 1), *p = NULL;
2522   uint32_t size = FILENAME_MAX + 1;
2523   _NSGetExecutablePath(path, &size);
2524   textadept_home = realpath(path, NULL);
2525   p = strstr(textadept_home, "MacOS"), strcpy(p, "Resources\0");
2526   free(path);
2527 #if !CURSES
2528   osxapp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
2529   platform = "OSX"; // OSX is only set for GUI version
2530 #endif
2531 #elif (__FreeBSD__ || __NetBSD__ || __OpenBSD__)
2532   textadept_home = malloc(FILENAME_MAX + 1);
2533   int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
2534   size_t cb = FILENAME_MAX + 1;
2535   sysctl(mib, 4, textadept_home, &cb, NULL, 0);
2536   if ((last_slash = strrchr(textadept_home, '/'))) *last_slash = '\0';
2537   platform = "BSD";
2538 #endif
2539 
2540 #if GTK
2541   bool force = false;
2542   for (int i = 0; i < argc; i++)
2543     if (strcmp("-f", argv[i]) == 0 || strcmp("--force", argv[i]) == 0) {
2544       force = true;
2545       break;
2546     }
2547   GApplication *app = g_application_new(ID, G_APPLICATION_HANDLES_COMMAND_LINE);
2548   g_signal_connect(app, "command-line", G_CALLBACK(process), NULL);
2549   bool registered = g_application_register(app, NULL, NULL);
2550   if (!registered || !g_application_get_is_remote(app) || force) {
2551 #endif
2552 
2553   setlocale(LC_COLLATE, "C"), setlocale(LC_NUMERIC, "C"); // for Lua
2554   if (lua = luaL_newstate(), !init_lua(lua, argc, argv, false)) return 1;
2555   initing = true, new_window(), run_file(lua, "init.lua"), initing = false;
2556   emit(lua, "buffer_new", -1), emit(lua, "view_new", -1); // first ones
2557   lua_pushdoc(lua, SS(command_entry, SCI_GETDOCPOINTER, 0, 0)),
2558     lua_setglobal(lua, "buffer");
2559   emit(lua, "buffer_new", -1), emit(lua, "view_new", -1); // command entry
2560   lua_pushdoc(lua, SS(focused_view, SCI_GETDOCPOINTER, 0, 0)),
2561     lua_setglobal(lua, "buffer");
2562   emit(lua, "initialized", -1); // ready
2563 #if (__APPLE__ && !CURSES)
2564   gtkosx_application_ready(osxapp);
2565 #endif
2566 
2567 #if GTK
2568     gtk_main();
2569   } else g_application_run(app, argc, argv);
2570   g_object_unref(app);
2571 #elif CURSES
2572   refresh_all();
2573 
2574 #if !_WIN32
2575   freopen("/dev/null", "w", stderr); // redirect stderr
2576   // Set terminal suspend, resume, and resize handlers, preventing any signals
2577   // in them from causing interrupts.
2578   struct sigaction act;
2579   memset(&act, 0, sizeof(struct sigaction));
2580   act.sa_handler = signalled, sigfillset(&act.sa_mask);
2581   sigaction(SIGTSTP, &act, NULL), sigaction(SIGCONT, &act, NULL),
2582     sigaction(SIGWINCH, &act, NULL);
2583 #else
2584   freopen("NUL", "w", stdout), freopen("NUL", "w", stderr); // redirect
2585 #endif
2586 
2587   Scintilla *view = focused_view;
2588   int ch = 0, event = 0, button = 0, y = 0, x = 0, millis = 0;
2589   TermKeyResult res;
2590   TermKeyKey key;
2591   int keysyms[] = {0,SCK_BACK,SCK_TAB,SCK_RETURN,SCK_ESCAPE,0,SCK_BACK,SCK_UP,SCK_DOWN,SCK_LEFT,SCK_RIGHT,0,0,SCK_INSERT,SCK_DELETE,0,SCK_PRIOR,SCK_NEXT,SCK_HOME,SCK_END};
2592   while ((ch = 0, res = textadept_waitkey(ta_tk, &key)) != TERMKEY_RES_EOF) {
2593     if (res == TERMKEY_RES_ERROR) continue;
2594     if (key.type == TERMKEY_TYPE_UNICODE)
2595       ch = key.code.codepoint;
2596     else if (key.type == TERMKEY_TYPE_FUNCTION)
2597       ch = 0xFFBD + key.code.number; // use GDK keysym values for now
2598     else if (key.type == TERMKEY_TYPE_KEYSYM &&
2599              key.code.sym >= 0 && key.code.sym <= TERMKEY_SYM_END)
2600       ch = keysyms[key.code.sym];
2601     else if (key.type == TERMKEY_TYPE_UNKNOWN_CSI) {
2602       long args[16];
2603       size_t nargs = 16;
2604       unsigned long cmd;
2605       termkey_interpret_csi(ta_tk, &key, args, &nargs, &cmd);
2606       lua_newtable(lua);
2607       for (size_t i = 0; i < nargs; i++)
2608         lua_pushinteger(lua, args[i]), lua_rawseti(lua, -2, i + 1);
2609       emit(lua, "csi", LUA_TNUMBER, cmd, LUA_TTABLE, luaL_ref(
2610         lua, LUA_REGISTRYINDEX), -1);
2611     } else if (key.type == TERMKEY_TYPE_MOUSE) {
2612       termkey_interpret_mouse(
2613         ta_tk, &key, (TermKeyMouseEvent*)&event, &button, &y, &x), y--, x--;
2614 #if !_WIN32
2615       struct timeval time = {0, 0};
2616       gettimeofday(&time, NULL);
2617       millis = time.tv_sec * 1000 + time.tv_usec / 1000;
2618 #else
2619       FILETIME time;
2620       GetSystemTimeAsFileTime(&time);
2621       ULARGE_INTEGER ticks;
2622       ticks.LowPart = time.dwLowDateTime, ticks.HighPart = time.dwHighDateTime;
2623       millis = ticks.QuadPart / 10000; // each tick is a 100-nanosecond interval
2624 #endif
2625     } else continue; // skip unknown types
2626     bool shift = key.modifiers & TERMKEY_KEYMOD_SHIFT;
2627     bool ctrl = key.modifiers & TERMKEY_KEYMOD_CTRL;
2628     bool alt = key.modifiers & TERMKEY_KEYMOD_ALT;
2629     if (ch && !emit(
2630           lua, "keypress", LUA_TNUMBER, ch, LUA_TBOOLEAN, shift,
2631           LUA_TBOOLEAN, ctrl, LUA_TBOOLEAN, alt, -1))
2632       scintilla_send_key(view, ch, shift, ctrl, alt);
2633     else if (!ch && !scintilla_send_mouse(
2634                view, event, millis, button, y, x, shift, ctrl, alt) &&
2635              !emit(
2636                lua, "mouse", LUA_TNUMBER, event, LUA_TNUMBER, button,
2637                LUA_TNUMBER, y, LUA_TNUMBER, x, LUA_TBOOLEAN, shift,
2638                LUA_TBOOLEAN, ctrl, LUA_TBOOLEAN, alt, -1))
2639       // Try again with possibly another view.
2640       scintilla_send_mouse(
2641         focused_view, event, millis, button, y, x, shift, ctrl, alt);
2642     if (quitting) {
2643       close_lua(lua);
2644       // Free some memory.
2645       free(pane), free(find_label), free(repl_label);
2646       if (find_text) free(find_text);
2647       if (repl_text) free(repl_text);
2648       for (int i = 0; i < 10; i++) {
2649         if (find_history[i]) free(find_history[i]);
2650         if (repl_history[i]) free(repl_history[i]);
2651         if (i > 3) continue;
2652         free(find_options[i] ? option_labels[i] : option_labels[i] - 4);
2653         free(button_labels[i]);
2654       }
2655       break;
2656     }
2657     refresh_all();
2658     view = !command_entry_active ? focused_view : command_entry;
2659   }
2660   endwin();
2661   termkey_destroy(ta_tk);
2662 #endif
2663 
2664   return (free(textadept_home), 0);
2665 }
2666 
2667 #if (_WIN32 && !CURSES)
2668 /**
2669  * Runs Textadept in Windows.
2670  * @see main
2671  */
WinMain(HINSTANCE _,HINSTANCE __,LPSTR ___,int ____)2672 int WINAPI WinMain(HINSTANCE _, HINSTANCE __, LPSTR ___, int ____) {
2673   return main(__argc, __argv); // MSVC extensions
2674 }
2675 #endif
2676