285 lines
8.9 KiB
C
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;
|
|
}
|