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__ || __DragonFly__)
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__ || __DragonFly__)
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