#include "../include/input.h" #include "../include/define.h" #include "../include/editor_op.h" #include "../include/output.h" #include "include/buffer.h" #include "include/builtins.h" #include "../include/completion.h" #include "include/data.h" #include "include/split_screen.h" #include #include #include #include #include #include #include "include/terminal.h" #include "include/utf8.h" /** * @file input.c * @brief Input handling module for the Beluga text editor * @details Manages user input processing, key bindings, cursor movement, and * file path completion */ /** * @brief Returns the first file completion match for the given path * @details Searches the directory containing the given path prefix and returns * the first file or directory entry that matches the filename prefix. * Appends a trailing slash for directory entries. * @param path The file path to complete (can be relative or absolute) * @return Pointer to the completed file path (dynamically allocated), or NULL * if: * - path ends with '/' (already a directory) * - no matching entries found * - directory cannot be opened * @note Caller is responsible for bFreeing the returned string * @note Uses static buffer internally; may return stale pointers across calls */ const char *fileCompletion(const char *path) { DIR *dir; struct dirent *entry; char directory[256]; char predict[128]; const char *last_slash; int predict_len = 0; size_t dir_len; // path is a directory if (path[strlen(path) - 1] == '/') { appDebug("[FILE COMP] is dir\n"); strncpy(directory, path, 256); } // Find dir name last_slash = strrchr(path, '/'); if (last_slash) { dir_len = last_slash - path + 1; // length of dir_path strncpy(directory, path, dir_len); predict_len = (int)(strlen(path) - dir_len); strncpy(predict, last_slash + 1, predict_len); directory[dir_len] = '\0'; predict[predict_len] = '\0'; appDebug("%s %s\n", directory, predict); } else { appDebug("[FILE COMP] dir not found\n"); return strdup(path); } dir = opendir(directory); if (!dir) return strdup(path); while ((entry = readdir(dir)) != NULL) { if (strncmp(entry->d_name, predict, predict_len) == 0) { static char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s%s", directory, entry->d_name); struct stat st; if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) { strcat(full_path, "/"); // add slash for directories } closedir(dir); return strdup(full_path); } } // Cleanup when no more entries closedir(dir); dir = NULL; free(entry); appDebug("[FILE COMP] no entries\n"); return strdup(path); } /** * @brief Displays an interactive prompt and returns user input * @details Allows the user to enter text in a prompt with optional path * completion via Tab key. Supports backspace, delete, and escape key handling. * Dynamically allocates memory for the input buffer. * @param prompt The prompt message format string (printf-style) * @param placeHolder Initial text to display in the input buffer * @param bPathMode If non-zero, enables Tab key file path completion * @return Pointer to the user-entered text (dynamically allocated), or NULL if: * - User pressed ESC to cancel * - Input buffer is empty when Enter is pressed * @note Caller is responsible for bFreeing the returned string * @note Uses editorReadKey() for input and editorRefreshScreen() for display */ char *editorPrompt(char *prompt, char *placeHolder, char bPathMode) { size_t buf_size = 256; appDebug("[FILE COMP] %s %d\n", placeHolder, strlen(placeHolder)); char *buf = malloc(buf_size); size_t buf_len = 0; int c = 0; buf[0] = '\0'; strcpy(buf, placeHolder); buf_len = strlen(placeHolder); while (1) { editorSetStatusMessage(prompt, buf); editorRefreshScreen(); c = editorReadKey(); if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) { if (buf_len != 0) { buf[--buf_len] = '\0'; } } else if (c == ESCAPE) { editorSetStatusMessage(""); free(buf); return NULL; } else if (c == '\r') { if (buf_len != 0) { editorSetStatusMessage(""); return buf; } } else if (bPathMode && c == '\t') { char path[256]; char *pwd; if (buf[0] != '/') { pwd = getenv("PWD"); appDebug("%s\n", pwd); memcpy(path, pwd, strlen(pwd)); path[strlen(pwd)] = '/'; strncat(path, buf, buf_len); } else { strcpy(path, buf); } memset(buf, 0, 256); char *buf_complete = (char *)fileCompletion(path); strcpy(buf, buf_complete); free(buf_complete); buf_len = strlen(buf); buf[buf_len] = '\0'; } else if (!iscntrl(c) && c < 256) { if (buf_len == buf_size - 1) { buf_size *= 2; char *new_buf = realloc(buf, buf_size); if (!new_buf) { free(buf); return NULL; } buf = new_buf; } } } } /** * @brief Executes the command bound to a key sequence * @details Searches the keybinding table for a matching key sequence and * prefix state, then evaluates the associated Lisp command. * @param key_sequence The string representation of the key sequence * @return 1 if a matching keybinding was found and executed, 0 otherwise * @note Updates global editor state E (prefix_state) * @note Uses Lisp interpreter to evaluate bound commands */ int executeKeyBind(char *key_sequence) { int i; int previous_state = 0; appDebug("pressed %s\n", key_sequence); for (i = 0; i < E.number_of_keybinds; ++i) { if (!strcmp(key_sequence, E.key_binds[i].key_sequence)) { if (E.prefix_state != E.key_binds[i].prefix_id) { continue; } previous_state = E.prefix_state; // It's a symbol, create a function call lisp_eval(lisp_cons(E.key_binds[i].command, lisp_null(), E.ctx), &E.ctx_error, E.ctx); if (E.prefix_state == previous_state) E.prefix_state = 0; return 1; } } return 0; } /** * @brief Processes a single keypress from the user * @details Reads a key, checks if it matches any registered keybinding, * and either executes the bound command or inserts the character. Resets * the quit buffer counter on successful key processing. * @note Updates global editor state E * @note Calls editorReadKey() to get input and editorInsertChar() for unbound * keys */ void editorProcessKeypress() { int c = editorReadKey(); char key_sequence[8] = {0}; EditorPane *active = splitScreenGetActivePane(); struct buffer_t *buf = bufferFindById(active->buffer_id); if (E.constantes.LSP && buf->b_lsp_open) { if (c == LSP_WAKE_KEY) return; if (E.lsp_client && E.lsp_completion.visible) { if (c == ARROW_UP || c == CTRL_KEY('p')) { if (E.lsp_completion.selected > 0) E.lsp_completion.selected--; return; // consumed, redraw on next loop } if (c == ARROW_DOWN || c == CTRL_KEY('n')) { if (E.lsp_completion.selected < E.lsp_completion.count - 1) E.lsp_completion.selected++; return; } if (c == '\r') { CompletionItem *item = &E.lsp_completion.items[E.lsp_completion.selected]; // Find how many chars the user already typed by looking at the // current word (chars before cursor on the same line) int file_col = active->cursor_x + active->x_offset; row_t *row = &buf->row[active->cursor_y + active->y_offset]; // Walk backwards from cursor to find start of current word int word_start = file_col; while (word_start > 0 && (isalnum((unsigned char)row->chars[word_start - 1]) || row->chars[word_start - 1] == '_')) word_start--; int already_typed = file_col - word_start; // chars user already typed // Insert only the suffix — what comes after what's already typed const char *suffix = item->label + already_typed; for (int i = 0; suffix[i]; i++) bufferInsertBytes(&suffix[i], 1); E.lsp_completion.visible = 0; return; } if (c == ESCAPE) { E.lsp_completion.visible = 0; return; } // Any other key: dismiss popup and fall through to normal handling E.lsp_completion.visible = 0; } } if (executeKeyBind(keyToString(c))) { return; } int seq_len = utf8Encode(c, key_sequence); appDebug("key seq : %s\n", key_sequence); bufferInsertBytes(key_sequence, seq_len); if (buf->b_lsp_open && is_word_char(key_sequence)) { if (E.lsp_client && E.lsp_client->state == LSP_READY) { lspDidChange(E.lsp_client, buf); E.lsp_client->completion_just_arrived = 0; // consume the flag } buf->b_has_changed = 0; editorAutoComplete(lisp_null(), &E.ctx_error, E.ctx); } E.quit_times_buffer = E.constantes.QUIT_TIMES; }