Files
beluga/src/input.c
T

285 lines
8.9 KiB
C

#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 <ctype.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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;
}