/** * @file builtins.c * @brief Built-in Lisp functions for editor operations * @details Provides Lisp bindings for core editor functionality including * cursor movement, file operations, keybinding management, and text * manipulation */ #include "../include/builtins.h" #include "../include/buffer.h" #include "../include/data.h" #include "../include/define.h" #include "../include/editor_op.h" #include "../include/file_io.h" #include "../include/input.h" #include "../include/terminal.h" #include "../include/split_screen.h" #include "../include/completion.h" #include #include #include /** * @brief Finds a prefix configuration by name * @details Searches the prefix array for a prefix matching the given name. * Returns the first prefix (default) if no match is found. * @param prefix_name Name of the prefix to search for (max 64 chars) * @return Matching prefix_t structure, or E.prefix[0] if not found * @note Updates global editor state E */ struct prefix_t find_prefix(const char prefix_name[64]) { int i = E.number_of_prefix + 1; while (i--) { if (!strcmp(prefix_name, E.prefix[i].prefix_name)) { return E.prefix[i]; } } return E.prefix[0]; } /** * @brief Lisp function to bind a command to a key sequence * @details Registers a keybinding with an associated Lisp command and optional * prefix modifier. Dynamically extends the keybind array. * @param args Lisp list of 3 arguments: (key-sequence command prefix-name) * - key_sequence (string): The key or key combination to bind * - command (Lisp): The Lisp command/function to execute * - prefix_name (string): Prefix modifier name * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E (key_binds array) */ Lisp mapKey(Lisp args, LispError *e, LispContext ctx) { /* * 3 arguments keybind command prefix */ const char *key_sequence = lisp_string(lisp_car(args)); args = lisp_cdr(args); // second argument const Lisp func = lisp_car(args); E.key_binds = (struct keyBind_t *)realloc( E.key_binds, ++E.number_of_keybinds * sizeof(struct keyBind_t)); E.key_binds[E.number_of_keybinds - 1].key_sequence = (char *)malloc(50 * sizeof(char)); strncpy(E.key_binds[E.number_of_keybinds - 1].key_sequence, key_sequence, 50); E.key_binds[E.number_of_keybinds - 1].command = func; // Third argument args = lisp_cdr(args); const char *prefix_name = lisp_string(lisp_car(args)); struct prefix_t prefix = find_prefix(prefix_name); E.key_binds[E.number_of_keybinds - 1].prefix_id = prefix.prefix_id; return lisp_null(); } /** * @brief Lisp function to move cursor in a specified direction * @details Moves the editor cursor up, down, left, or right based on direction * string. * @param args Lisp list with one argument: direction string * - "u": up, "d": down, "r": right, "l": left * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return Lisp boolean indicating whether movement was valid * @note Updates global editor state E * @see editorMoveCursor() */ Lisp moveCursor(Lisp args, LispError *e, LispContext ctx) { const char *direction = lisp_string(lisp_car(args)); int is_in = 0; switch (direction[0]) { case 'u': is_in = editorMoveCursor(ARROW_UP); break; case 'd': is_in = editorMoveCursor(ARROW_DOWN); break; case 'r': is_in = editorMoveCursor(ARROW_RIGHT); break; case 'l': is_in = editorMoveCursor(ARROW_LEFT); break; } appDebug("move lisp %d\n", is_in); return lisp_make_bool(is_in); } /** * @brief Frees all dynamically allocated editor structures * @details Releases memory for prefix table, keybinds, filename, and all rows. * Called during shutdown to prevent memory leaks. * @note Updates global editor state E */ void free_structs(void) { int i, j; free(E.prefix); for (i = 0; i < E.number_of_keybinds; ++i) { free(E.key_binds[i].key_sequence); } free(E.key_binds); // free layout free(E.layout.panes); // Free buffers for (i = 0; i < E.number_of_buffer; ++i) { free(E.buffers[i].filename); for (j = 0; j < E.buffers[i].numrows; ++j) { free(E.buffers[i].row[j].chars); } free(E.buffers[i].row); } free(E.init_file_path); fclose(E.fd_init_file); } /** * @brief Lisp function to quit the editor * @details Closes editor with unsaved changes protection. Prompts user to * confirm quit after multiple attempts if file is dirty. Cleans up resources * and restores terminal. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() (never returns on successful exit) * @note Calls exit(0) to terminate program * @note Updates quit_times_buffer counter * @see free_structs() */ Lisp editorQuit(Lisp args, LispError *e, LispContext ctx) { if (E.dirty && E.quit_times_buffer > 0) { editorSetStatusMessage("WARNING! Changes hasn't been saved. Press Ctrl-Q " "another time to quit."); --E.quit_times_buffer; return lisp_null(); } free_structs(); write(STDOUT_FILENO, "\x1b[2J", 4); write(STDOUT_FILENO, CURSOR_TOP_LEFT, 3); disableRawMode(); lisp_shutdown(E.ctx); exit(0); return lisp_null(); } /** * @brief Lisp function to save the current file * @details Wrapper around editorSave() for use in Lisp keybindings. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @see editorSave() */ Lisp l_editorSplitScreenVertical(Lisp args, LispError *e, LispContext ctx) { if (E.number_of_buffer >= 2) { splitScreenVertical(E.buffers[0].buffer_id, E.buffers[1].buffer_id); } else { editorSetStatusMessage("Need at least 2 buffers open"); } return lisp_null(); } Lisp l_editorSave(Lisp args, LispError *e, LispContext ctx) { editorSave(); return lisp_null(); } /** * @brief Lisp function to insert a new line at cursor * @details Wrapper around editorInsertNewLine() for use in Lisp keybindings. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @see editorInsertNewLine() */ Lisp l_editorInsertNewLine(Lisp args, LispError *e, LispContext ctx) { bufferInsertNewLine(); return lisp_null(); } /** * @brief Lisp function to insert a tab (spaces) at cursor * @details Inserts TAB_LENGTH spaces at the current cursor position. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Uses E.constantes.TAB_LENGTH for indentation width */ Lisp l_editorInserTab(Lisp args, LispError *e, LispContext ctx) { for (int i = 0; i < E.constantes.TAB_LENGTH; ++i) { bufferInsertBytes(" ", 1); } return lisp_null(); } /** * @brief Lisp function to move cursor to beginning of line * @details Moves cursor to column 0 of the current line. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E */ Lisp moveCursorBeginLine(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buf = bufferFindById(active->buffer_id); buf->x = 0; return lisp_null(); } /** * @brief Lisp function to move cursor to end of line * @details Moves cursor to the end of the current line content. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E */ Lisp moveCursorEndLine(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buf = bufferFindById(active->buffer_id); buf->x = buf->row[buf->y].size; return lisp_null(); } /** * @brief Lisp function to delete character before cursor * @details Wrapper around editorDelChar() for use in Lisp keybindings. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @see editorDelChar() */ Lisp deletePreviousChar(Lisp args, LispError *e, LispContext ctx) { bufferDelBytes(); return lisp_null(); } /** * @brief Lisp function to move cursor up by one screen * @details Scrolls up one full screen height, moving cursor to top of visible * area. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E * @see editorMoveCursor() */ Lisp editorMoveCursorPageUp(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); active->cursor_y = active->y_offset; int times = E.screenrows; while (--times) { editorMoveCursor(ARROW_UP); } return lisp_null(); } /** * @brief Lisp function to move cursor down by one screen * @details Scrolls down one full screen height, moving cursor to bottom of * visible area. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E * @see editorMoveCursor() */ Lisp editorMoveCursorPageDown(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); active->cursor_y = active->y_offset + E.screenrows - 1; if (active->cursor_y > buffer->numrows) { active->cursor_y = buffer->numrows; } int times = E.screenrows; while (--times) { editorMoveCursor(ARROW_DOWN); } return lisp_null(); } /** * @brief Lisp function to open a file * @details Prompts user for filename and opens the file for editing. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E * @see editorOpen() * @see editorPrompt() */ Lisp editorOpenFile(Lisp args, LispError *e, LispContext ctx) { char *filename = editorPrompt("Open : %s", getenv("PWD"), 1); if (filename) { // editorOpen(filename); EditorPane *active = splitScreenGetActivePane(); active->buffer_id = bufferCreate(filename, READ_AND_WRITE); } free(filename); return lisp_null(); } /** * @brief Lisp function to insert a character * @details Extracts a character from Lisp string argument and inserts it at * cursor. * @param args Lisp list with one string argument * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Uses first character of the string argument */ Lisp editorPrintC(Lisp args, LispError *e, LispContext ctx) { const char *src = lisp_string(lisp_car(args)); bufferInsertBytes(src, strlen(src)); return lisp_null(); } /** * @brief Lisp function to load and execute a package * @details Loads a Lisp package from the user's packages directory * (~/.beluga/packages//init.lisp) and evaluates it. * @param args Lisp list with one argument: package name (string) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Package files must be valid Lisp code * @note Package directory defaults to ~/.beluga/packages/ */ Lisp addPackage(Lisp args, LispError *e, LispContext ctx) { const char *package_name = lisp_string(lisp_car(args)); appDebug("%s\n", package_name); char *package_dir = (char *)calloc(256, sizeof(char)); FILE *fd_package = NULL; strcat(package_dir, getenv("HOME")); strcat(package_dir, "/.beluga/packages/"); strcat(package_dir, package_name); strcat(package_dir, "/init.lisp"); appDebug("%s\n", package_dir); fd_package = fopen(package_dir, "r"); lisp_eval(lisp_read_file(fd_package, &E.ctx_error, E.ctx), &E.ctx_error, E.ctx); fclose(fd_package); free(package_dir); return lisp_null(); } Lisp editorSwitchNextBuffer(Lisp args, LispError *e, LispContext ctx) { appDebug("switch buffer\n"); if (E.number_of_buffer > 0) { EditorPane *active = splitScreenGetActivePane(); int next_idx = (active->buffer_id + 1) % E.number_of_buffer; active->buffer_id = next_idx; } return lisp_null(); } Lisp editorSwitchNextPane(Lisp args, LispError *e, LispContext ctx) { splitScreenSwitchPane(); return lisp_null(); } Lisp editorUnifiedPanes(Lisp args, LispError *e, LispContext ctx) { if (E.layout.num_panes - 1) { splitScreenUnify(); } return lisp_null(); } /** * @brief Lisp function to search for text * @details Wrapper around editorFind() for use in Lisp keybindings. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @see editorFind() */ Lisp bufferFind_L(Lisp args, LispError *e, LispContext ctx) { appDebug("LispFind\n"); EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); bufferFind(buffer); return lisp_null(); } /** * @brief Lisp function to search for text * @details Wrapper around editorFind() for use in Lisp keybindings. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @see editorFind() */ Lisp bufferFindReverse_L(Lisp args, LispError *e, LispContext ctx) { appDebug("LispFind\n"); EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); bufferFindReverse(buffer); return lisp_null(); } /** * @brief Lisp function to read character at cursor * @details Returns the character at the current cursor position as a Lisp * character. Returns 'a' if at end of line. * @param args Lisp arguments (unused) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return Lisp character object representing the character at cursor */ Lisp editorReadChar_L(Lisp args, LispError *e, LispContext ctx) { Lisp returned_char; EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); if (buffer->row[buffer->y].chars[buffer->x] == 0) { returned_char = lisp_make_char('a'); } else { returned_char = lisp_make_char(buffer->row[buffer->y].chars[buffer->x]); } return returned_char; } /** * @brief Lisp function to set the current prefix mode * @details Changes the editor's prefix state to a named prefix, affecting which * keybindings are active. Updates status message to show active prefix. * @param args Lisp list with one argument: prefix name (string) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E (prefix_state) * @see find_prefix() */ Lisp editorSetPrefix(Lisp args, LispError *e, LispContext ctx) { /* * Set the prefix state of editor to the prefix in argument */ const char *prefix_name = lisp_string(lisp_car(args)); struct prefix_t prefix = find_prefix(prefix_name); E.prefix_state = prefix.prefix_id; editorSetStatusMessage("prefix %s", prefix.prefix_name); appDebug("%s set\n", prefix_name); return lisp_null(); } /** * @brief Lisp function to define a new prefix modifier * @details Registers a named prefix modifier that can be used with keybindings * to create context-aware command sequences (e.g., Ctrl-X as a prefix). * @param args Lisp list with one argument: prefix name (string, max 64 chars) * @param e Error pointer for Lisp error handling * @param ctx Lisp context * @return lisp_null() * @note Updates global editor state E (prefix array) */ Lisp editorPrefix(Lisp args, LispError *e, LispContext ctx) { E.prefix = (struct prefix_t *)realloc(E.prefix, (++(E.number_of_prefix) + 1) * sizeof(struct prefix_t)); E.prefix[E.number_of_prefix].prefix_id = E.number_of_prefix; strncpy(E.prefix[E.number_of_prefix].prefix_name, lisp_string(lisp_car(args)), 64); return lisp_null(); } Lisp editorPaste(Lisp args, LispError *e, LispContext ctx) { const char *char_to_paste = editorGetClipboard(); appDebug("editor-paste, %s\n", char_to_paste); bufferInsertBytes(char_to_paste, (int) strlen(char_to_paste)); return lisp_null(); } Lisp editorCutEndLine(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); int bytes_to_delete = buffer->row[buffer->y].size - buffer->x; editorSetClipboard(&buffer->row[buffer->y].chars[buffer->x], bytes_to_delete); buffer->x = buffer->row[buffer->y].size; while (bytes_to_delete--) { bufferDelBytes(); } return lisp_null(); } Lisp editorMoveBegBuffer(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); buffer->x = 0; buffer->y = 0; return lisp_null(); } Lisp editorMoveEndBuffer(Lisp args, LispError *e, LispContext ctx) { EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buffer = bufferFindById(active->buffer_id); buffer->x = buffer->row[buffer->numrows-1].size; buffer->y = buffer->numrows-1; return lisp_null(); } Lisp editorAutoComplete(Lisp args, LispError *e, LispContext ctx) { createContextBuffer(E.cursor_x - 2, E.cursor_y + 1, "hello"); return lisp_null(); }