diff --git a/Makefile b/Makefile index ebec0c3..6098ec7 100644 --- a/Makefile +++ b/Makefile @@ -18,13 +18,13 @@ DOCDIR = $(PREFIX)/share/doc/emsys # Source files OBJECTS = main.o wcwidth.o unicode.o buffer.o region.o undo.o transform.o \ - find.o pipe.o tab.o register.o fileio.o terminal.o display.o \ - keymap.o edit.o prompt.o util.o + find.o pipe.o register.o fileio.o terminal.o display.o \ + keymap.o edit.o prompt.o util.o completion.o history.o # Default target with git version detection all: @VERSION="`git describe --tags --always --dirty 2>/dev/null || echo $(VERSION)`"; \ - make VERSION="$$VERSION" $(PROGNAME) + $(MAKE) VERSION="$$VERSION" $(PROGNAME) # Link the executable $(PROGNAME): $(OBJECTS) @@ -73,13 +73,15 @@ check: test # Sorry Dave hal: - make clean - make CFLAGS="$(CFLAGS) -D_POSIX_C_SOURCE=200112L -Werror" $(PROGNAME) - make test + $(MAKE) format + $(MAKE) clean + for f in *.c; do clang-tidy $$f -- -I. ; done + $(MAKE) CFLAGS="$(CFLAGS) -D_POSIX_C_SOURCE=200112L -Werror" $(PROGNAME) + $(MAKE) test # Development targets debug: - make CFLAGS="$(CFLAGS) -g -O0" $(PROGNAME) + $(MAKE) CFLAGS="$(CFLAGS) -g -O0" $(PROGNAME) format: @@ -87,21 +89,21 @@ format: # Platform-specific variants android: - make CC=clang CFLAGS="$(CFLAGS) -fPIC -fPIE -DEMSYS_DISABLE_PIPE" LDFLAGS="-pie" $(PROGNAME) + $(MAKE) CC=clang CFLAGS="$(CFLAGS) -fPIC -fPIE -DEMSYS_DISABLE_PIPE" LDFLAGS="-pie" $(PROGNAME) msys2: - make CFLAGS="$(CFLAGS) -D_GNU_SOURCE" $(PROGNAME) + $(MAKE) CFLAGS="$(CFLAGS) -D_GNU_SOURCE" $(PROGNAME) minimal: - make CFLAGS="$(CFLAGS) -DEMSYS_DISABLE_PIPE -Os" $(PROGNAME) + $(MAKE) CFLAGS="$(CFLAGS) -DEMSYS_DISABLE_PIPE -Os" $(PROGNAME) solaris: - VERSION="$(VERSION)" make CC=cc CFLAGS="-xc99 -D__EXTENSIONS__ -O2 -errtags=yes -erroff=E_ARG_INCOMPATIBLE_WITH_ARG_L" $(PROGNAME) + VERSION="$(VERSION)" $(MAKE) CC=cc CFLAGS="-xc99 -D__EXTENSIONS__ -O2 -errtags=yes -erroff=E_ARG_INCOMPATIBLE_WITH_ARG_L" $(PROGNAME) darwin: - make CC=clang CFLAGS="$(CFLAGS) -D_DARWIN_C_SOURCE" $(PROGNAME) + $(MAKE) CC=clang CFLAGS="$(CFLAGS) -D_DARWIN_C_SOURCE" $(PROGNAME) diff --git a/buffer.c b/buffer.c index ea5b023..4cecbb2 100644 --- a/buffer.c +++ b/buffer.c @@ -326,6 +326,11 @@ struct editorBuffer *newBuffer(void) { ret->special_buffer = 0; ret->undo = newUndo(); ret->redo = NULL; + ret->completion_state.last_completed_text = NULL; + ret->completion_state.completion_start_pos = 0; + ret->completion_state.successive_tabs = 0; + ret->completion_state.last_completion_count = 0; + ret->completion_state.preserve_message = 0; ret->next = NULL; ret->truncate_lines = 0; ret->rectangle_mode = 0; @@ -340,7 +345,9 @@ struct editorBuffer *newBuffer(void) { void destroyBuffer(struct editorBuffer *buf) { clearUndosAndRedos(buf); free(buf->filename); + free(buf->query); free(buf->screen_line_start); + free(buf->completion_state.last_completed_text); for (int i = 0; i < buf->numrows; i++) { freeRow(&buf->row[i]); } diff --git a/completion.c b/completion.c new file mode 100644 index 0000000..4a322b5 --- /dev/null +++ b/completion.c @@ -0,0 +1,805 @@ +#include +#include +#include +#include +#include +#include +#include "emsys.h" +#include "completion.h" +#include "buffer.h" +#include "util.h" +#include "display.h" +#include "terminal.h" +#include "fileio.h" +#include "prompt.h" +#include "edit.h" +#include "unicode.h" +#include "undo.h" +#include + +extern struct editorConfig E; + +void resetCompletionState(struct completion_state *state) { + free(state->last_completed_text); + state->last_completed_text = NULL; + state->completion_start_pos = 0; + state->successive_tabs = 0; + state->last_completion_count = 0; + state->preserve_message = 0; +} + +void freeCompletionResult(struct completion_result *result) { + if (result->matches) { + for (int i = 0; i < result->n_matches; i++) { + free(result->matches[i]); + } + free(result->matches); + } + free(result->common_prefix); + result->matches = NULL; + result->common_prefix = NULL; + result->n_matches = 0; + result->prefix_len = 0; +} + +char *findCommonPrefix(char **strings, int count) { + if (count == 0) + return NULL; + if (count == 1) + return xstrdup(strings[0]); + + int prefix_len = 0; + while (1) { + char ch = strings[0][prefix_len]; + if (ch == '\0') + break; + + int all_match = 1; + for (int i = 1; i < count; i++) { + if (strings[i][prefix_len] != ch) { + all_match = 0; + break; + } + } + + if (!all_match) + break; + prefix_len++; + } + + char *prefix = xmalloc(prefix_len + 1); + emsys_strlcpy(prefix, strings[0], prefix_len + 1); + return prefix; +} + +void getFileCompletions(const char *prefix, struct completion_result *result) { + glob_t globlist; + result->matches = NULL; + result->n_matches = 0; + result->common_prefix = NULL; + result->prefix_len = strlen(prefix); + + char *glob_pattern = NULL; + const char *pattern_to_use = prefix; + + /* Manual tilde expansion */ + if (*prefix == '~') { + char *home_dir = getenv("HOME"); + if (!home_dir) { + return; + } + + size_t home_len = strlen(home_dir); + size_t prefix_len = strlen(prefix); + char *expanded = xmalloc(home_len + prefix_len); + emsys_strlcpy(expanded, home_dir, home_len + prefix_len); + emsys_strlcat(expanded, prefix + 1, home_len + prefix_len); + pattern_to_use = expanded; + } + +#ifndef EMSYS_NO_SIMPLE_GLOB + /* Add * for globbing */ + int len = strlen(pattern_to_use); + glob_pattern = xmalloc(len + 2); + emsys_strlcpy(glob_pattern, pattern_to_use, len + 2); + glob_pattern[len] = '*'; + glob_pattern[len + 1] = '\0'; + + if (pattern_to_use != prefix) { + free((void *)pattern_to_use); + } + pattern_to_use = glob_pattern; +#endif + + int glob_result = glob(pattern_to_use, GLOB_MARK, NULL, &globlist); + if (glob_result == 0) { + if (globlist.gl_pathc > 0) { + result->matches = + xmalloc(globlist.gl_pathc * sizeof(char *)); + result->n_matches = globlist.gl_pathc; + + for (size_t i = 0; i < globlist.gl_pathc; i++) { + result->matches[i] = + xstrdup(globlist.gl_pathv[i]); + } + + result->common_prefix = findCommonPrefix( + result->matches, result->n_matches); + } + globfree(&globlist); + } else if (glob_result == GLOB_NOMATCH) { + /* No matches found */ + result->n_matches = 0; + } + + if (glob_pattern) { + free(glob_pattern); + } + if (pattern_to_use != prefix && pattern_to_use != glob_pattern) { + free((void *)pattern_to_use); + } +} + +void getBufferCompletions(struct editorConfig *ed, const char *prefix, + struct editorBuffer *currentBuffer, + struct completion_result *result) { + result->matches = NULL; + result->n_matches = 0; + result->common_prefix = NULL; + result->prefix_len = strlen(prefix); + + int capacity = 8; + result->matches = xmalloc(capacity * sizeof(char *)); + + for (struct editorBuffer *b = ed->headbuf; b != NULL; b = b->next) { + if (b == currentBuffer) + continue; + + /* Skip the *Completions* buffer */ + if (b->filename && strcmp(b->filename, "*Completions*") == 0) + continue; + + char *name = b->filename ? b->filename : "*scratch*"; + if (strncmp(name, prefix, strlen(prefix)) == 0) { + if (result->n_matches >= capacity) { + if (capacity > INT_MAX / 2 || + (size_t)capacity > + SIZE_MAX / (2 * sizeof(char *))) { + die("buffer size overflow"); + } + capacity *= 2; + result->matches = + xrealloc(result->matches, + capacity * sizeof(char *)); + } + result->matches[result->n_matches++] = xstrdup(name); + } + } + + if (result->n_matches > 0) { + result->common_prefix = + findCommonPrefix(result->matches, result->n_matches); + } else { + free(result->matches); + result->matches = NULL; + } +} + +void getCommandCompletions(struct editorConfig *ed, const char *prefix, + struct completion_result *result) { + result->matches = NULL; + result->n_matches = 0; + result->common_prefix = NULL; + result->prefix_len = strlen(prefix); + + int capacity = 8; + result->matches = xmalloc(capacity * sizeof(char *)); + + /* Convert prefix to lowercase for case-insensitive matching */ + int prefix_len = strlen(prefix); + char *lower_prefix = xmalloc(prefix_len + 1); + for (int i = 0; i <= prefix_len; i++) { + char c = prefix[i]; + if ('A' <= c && c <= 'Z') { + c |= 0x60; + } + lower_prefix[i] = c; + } + + for (int i = 0; i < ed->cmd_count; i++) { + if (strncmp(ed->cmd[i].key, lower_prefix, prefix_len) == 0) { + if (result->n_matches >= capacity) { + if (capacity > INT_MAX / 2 || + (size_t)capacity > + SIZE_MAX / (2 * sizeof(char *))) { + die("buffer size overflow"); + } + capacity *= 2; + result->matches = + xrealloc(result->matches, + capacity * sizeof(char *)); + } + result->matches[result->n_matches++] = + xstrdup(ed->cmd[i].key); + } + } + + free(lower_prefix); + + if (result->n_matches > 0) { + result->common_prefix = + findCommonPrefix(result->matches, result->n_matches); + } else { + free(result->matches); + result->matches = NULL; + } +} + +static void replaceMinibufferText(struct editorBuffer *minibuf, + const char *text) { + /* Clear current content */ + while (minibuf->numrows > 0) { + editorDelRow(minibuf, 0); + } + + /* Insert new text */ + editorInsertRow(minibuf, 0, (char *)text, strlen(text)); + minibuf->cx = strlen(text); + minibuf->cy = 0; +} + +static struct editorBuffer *findOrCreateBuffer(const char *name) { + /* Search for existing buffer */ + for (struct editorBuffer *b = E.headbuf; b != NULL; b = b->next) { + if (b->filename && strcmp(b->filename, name) == 0) { + return b; + } + } + + /* Create new buffer */ + struct editorBuffer *new_buf = newBuffer(); + new_buf->filename = xstrdup(name); + new_buf->special_buffer = 1; + new_buf->next = E.headbuf; + E.headbuf = new_buf; + return new_buf; +} + +static void clearBuffer(struct editorBuffer *buf) { + while (buf->numrows > 0) { + editorDelRow(buf, 0); + } +} + +static void showCompletionsBuffer(char **matches, int n_matches) { + /* Find or create completions buffer */ + struct editorBuffer *comp_buf = findOrCreateBuffer("*Completions*"); + clearBuffer(comp_buf); + comp_buf->read_only = 1; + + /* Add header */ + char header[100]; + snprintf(header, sizeof(header), + "Possible completions (%d):", n_matches); + editorInsertRow(comp_buf, 0, header, strlen(header)); + editorInsertRow(comp_buf, 1, "", 0); + + /* Find max width */ + int max_width = 0; + for (int i = 0; i < n_matches; i++) { + int width = stringWidth((uint8_t *)matches[i]); + if (width > max_width) { + max_width = width; + } + } + + /* Calculate columns */ + int term_width = E.screencols; + int col_width = max_width + 2; + int columns = term_width / col_width; + if (columns < 1) + columns = 1; + + /* Format matches in columns */ + int rows = (n_matches + columns - 1) / columns; + for (int row = 0; row < rows; row++) { + char line[1024] = { 0 }; + int line_pos = 0; + + for (int col = 0; col < columns; col++) { + int idx = row + col * rows; + if (idx >= n_matches) + break; + + int written = snprintf(line + line_pos, + sizeof(line) - line_pos, "%-*s", + col_width, matches[idx]); + if (written > 0) { + line_pos += written; + } + } + + /* Trim trailing spaces */ + while (line_pos > 0 && line[line_pos - 1] == ' ') { + line_pos--; + } + line[line_pos] = '\0'; + + editorInsertRow(comp_buf, comp_buf->numrows, line, line_pos); + } + + /* Display in window if not already visible */ + int comp_window = findBufferWindow(comp_buf); + if (comp_window == -1) { + /* Not visible - create new window at bottom */ + if (E.nwindows >= 1) { + /* Create new window for completions */ + int new_window_idx = E.nwindows; + editorCreateWindow(); + + /* Set the new window to show completions buffer */ + E.windows[new_window_idx]->buf = comp_buf; + E.windows[new_window_idx]->focused = 0; + + /* Keep focus on the first window */ + for (int i = 0; i < E.nwindows; i++) { + E.windows[i]->focused = (i == 0); + } + + comp_window = new_window_idx; + } + } else { + } + + /* Adjust window sizes for completions display */ + if (E.nwindows >= 2 && comp_window >= 0) { + /* Calculate desired height for completions window */ + int comp_height = comp_buf->numrows + 2; /* +2 for padding */ + + /* Calculate total available height */ + int total_height = E.screenrows - minibuffer_height - + (statusbar_height * E.nwindows); + + /* Calculate minimum space needed for non-completion windows */ + int non_comp_windows = E.nwindows - 1; + int min_space_for_others = + non_comp_windows * 3; /* 3 lines minimum each */ + + /* Maximum height for completions is what's left after ensuring minimums */ + int max_comp_height = total_height - min_space_for_others; + if (comp_height > max_comp_height) { + comp_height = max_comp_height; + } + + /* Ensure completions window itself gets at least 3 lines */ + if (comp_height < 3) { + comp_height = 3; + } + + /* Distribute remaining space to other windows */ + int remaining_height = total_height - comp_height; + int height_per_window = remaining_height / non_comp_windows; + + /* Set window heights */ + for (int i = 0; i < E.nwindows; i++) { + if (i == comp_window) { + E.windows[i]->height = comp_height; + } else { + E.windows[i]->height = height_per_window; + } + } + } + + refreshScreen(); +} + +void closeCompletionsBuffer(void) { + struct editorBuffer *comp_buf = NULL; + struct editorBuffer *prev_buf = NULL; + + /* Find the completions buffer and its predecessor */ + for (struct editorBuffer *b = E.headbuf; b != NULL; + prev_buf = b, b = b->next) { + if (b->filename && strcmp(b->filename, "*Completions*") == 0) { + comp_buf = b; + break; + } + } + + if (comp_buf) { + int comp_window = findBufferWindow(comp_buf); + if (comp_window >= 0 && E.nwindows > 1) { + editorDestroyWindow(comp_window); + } + + /* Remove the buffer from the buffer list */ + if (prev_buf) { + prev_buf->next = comp_buf->next; + } else { + E.headbuf = comp_buf->next; + } + + /* Update E.buf if it pointed to the completions buffer */ + if (E.buf == comp_buf) { + E.buf = comp_buf->next ? comp_buf->next : E.headbuf; + } + + /* Update lastVisitedBuffer if it pointed to completions buffer */ + if (E.lastVisitedBuffer == comp_buf) { + E.lastVisitedBuffer = NULL; + } + + /* Destroy the buffer */ + destroyBuffer(comp_buf); + } +} + +/* Removed - using showCompletionsBuffer instead */ + +static int alnum(uint8_t c) { + return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || (c == '_'); +} + +static int sortstring(const void *str1, const void *str2) { + return strcasecmp(*(char **)str1, *(char **)str2); +} + +void editorCompleteWord(struct editorConfig *ed, struct editorBuffer *bufr) { + /* TODO Finish implementing this sometime */ + if (bufr->cy >= bufr->numrows || bufr->cx == 0) { + editorSetStatusMessage("Nothing to complete here."); + return; + } + + /* Don't attempt word completion within special buffers */ + if (bufr->special_buffer) { + return; + } + + /* Check whether there's a word here to complete */ + struct erow *row = &bufr->row[bufr->cy]; + int wordStart = bufr->cx; + while (wordStart > 0 && alnum(row->chars[wordStart - 1])) { + wordStart--; + } + int wordLen = bufr->cx - wordStart; + if (wordLen == 0) { + editorSetStatusMessage("Nothing to complete here."); + return; + } + + /* Copy the word and escape regex characters*/ + char word[wordLen + 1]; + word[wordLen] = 0; + char regexWord[2 * wordLen + 2]; /* worst case, we escape everything and + add .* */ + for (int i = 0; i < wordLen; i++) { + word[i] = row->chars[wordStart + i]; + } + int regexPos = 0; + for (int i = 0; i < wordLen; i++) { + switch (word[i]) { + case '.': + case '[': + case '{': + case '}': + case '(': + case ')': + case '\\': + case '*': + case '+': + case '?': + case '|': + case '^': + case '$': + regexWord[regexPos++] = '\\'; + /* fall through */ + default: + regexWord[regexPos++] = word[i]; + } + } + regexWord[regexPos++] = '.'; + regexWord[regexPos++] = '*'; + regexWord[regexPos] = 0; + + /* Compile a regex out of it. This is slow, but Russ Cox said + * regexes are fast, so he's probably got a C regex library + * just lying around that we could use. */ + regex_t regex; + int reti = regcomp(®ex, regexWord, REG_EXTENDED | REG_NEWLINE); + if (reti) { + editorSetStatusMessage("Could not compile regex: %s", + regexWord); + return; + } + + /* Ok. Now, search for matches. Note that we're searching for + * matches that look like a C identifier, then taking everything + * up to the end of the identifier. So a search for "ed" will + * match "editor", not "ed". */ + char **candidates = NULL; + int ncand = 0; + int scand = 0; + + for (struct editorBuffer *scanbuf = ed->headbuf; scanbuf != NULL; + scanbuf = scanbuf->next) { + /* Don't scan special buffers */ + if (scanbuf->special_buffer) { + continue; + } + for (int rownum = 0; rownum < scanbuf->numrows; rownum++) { + struct erow *scanrow = &scanbuf->row[rownum]; + regmatch_t pmatch; + char *line = (char *)scanrow->chars; + char *cursor = line; + + while (regexec(®ex, cursor, 1, &pmatch, 0) == 0) { + /* Did we match at the beginning of the + * string or is the previous character not + * alnum? If not, then we didn't match the + * beginning of a word. */ + if (!((cursor == line || + !alnum(*(cursor + pmatch.rm_so - 1))))) { + cursor += pmatch.rm_eo; + continue; + } + + /* Copy the whole word */ + int candidateLen = pmatch.rm_eo - pmatch.rm_so; + while (candidateLen + pmatch.rm_so < + scanrow->size && + alnum(scanrow->chars[cursor - line + + pmatch.rm_so + + candidateLen])) { + candidateLen++; + } + /* Make the copy */ + if (ncand >= scand) { + /* Out of space, add more. */ + if (scand == 0) { + scand = 32; + candidates = xmalloc( + sizeof(char *) * scand); + } else { + if (scand > INT_MAX / 2 || + (size_t)scand > + SIZE_MAX / + (2 * + sizeof(char *))) { + die("buffer size overflow"); + } + scand *= 2; + candidates = xrealloc( + candidates, + sizeof(char *) * scand); + } + } + candidates[ncand] = xmalloc(candidateLen + 1); + emsys_strlcpy( + candidates[ncand], + (char *)&scanrow->chars[cursor - line + + pmatch.rm_so], + candidateLen + 1); + ncand++; + + /* Onward! */ + cursor += pmatch.rm_so + candidateLen; + } + + /* Also add keywords if they match. */ + const char *keywords[] = { "auto", + "break", + "case", + "char", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extern", + "float", + "for", + "goto", + "if", + "inline", + "int", + "long", + "register", + "restrict", + "return", + "short", + "signed", + "sizeof", + "static", + "struct", + "switch", + "typedef", + "union", + "unsigned", + "void", + "volatile", + "while", + "_Alignas", + "_Alignof", + "_Atomic", + "_Bool", + "_Complex", + "_Generic", + "_Imaginary", + "_Noreturn", + "_Static_assert", + "_Thread_local", + NULL }; + + /* This is pretty dumb. We should be able to + * reduce the number of regex invocations to one, + * by searching for the regex only at the + * beginning of the string. */ + regmatch_t pmatch2; + for (int i = 0; keywords[i] != NULL; i++) { + if (regexec(®ex, keywords[i], 1, &pmatch2, + 0) == 0 && + pmatch2.rm_so == 0) { + /* Copy it. */ + if (ncand >= scand) { + /* Out of space, add more. */ + if (scand == 0) { + scand = 32; + candidates = xmalloc( + sizeof(char *) * + scand); + } else { + if (scand > INT_MAX / + 2 || + (size_t)scand > + SIZE_MAX / + (2 * + sizeof(char *))) { + die("buffer size overflow"); + } + scand *= 2; + candidates = xrealloc( + candidates, + sizeof(char *) * + scand); + } + } + candidates[ncand] = + xstrdup(keywords[i]); + ncand++; + } + } + } + } + /* Dunmatchin'. Restore word to non-regex contents. */ + word[bufr->cx - wordStart] = 0; + + /* No matches? Cleanup. */ + if (ncand == 0) { + editorSetStatusMessage("No match for %s", word); + goto COMPLETE_WORD_CLEANUP; + } + + int sel = 0; + /* Only one candidate? Take it. */ + if (ncand == 1) { + goto COMPLETE_WORD_DONE; + } + + /* Next, sort the list */ + qsort(candidates, ncand, sizeof(char *), sortstring); + + /* Finally, uniq' it. We now have our candidate list. */ + int newlen = 1; + char *prev = NULL; + for (int i = 0; i < ncand; i++) { + if (prev == NULL) { + prev = candidates[i]; + continue; + } + if (strcmp(prev, candidates[i])) { + /* Nonduplicate, copy it over. */ + prev = candidates[i]; + candidates[newlen++] = prev; + } else { + /* We don't need the memory for duplicates any more. */ + free(candidates[i]); + } + } + ncand = newlen; + +COMPLETE_WORD_DONE:; + /* Replace the stem with the new word. */ + int newWordLen = strlen(candidates[sel]); + if (wordLen < newWordLen) { + /* Insert the additional characters */ + for (int i = wordLen; i < newWordLen; i++) { + editorInsertChar(bufr, candidates[sel][i], 1); + } + } else if (wordLen > newWordLen) { + for (int i = wordLen; i > newWordLen; i--) { + editorDelChar(bufr, 1); + } + } + /* No else clause, they're equal, nothing to do. */ + +COMPLETE_WORD_CLEANUP: + regfree(®ex); + for (int i = 0; i < ncand; i++) { + free(candidates[i]); + } + free(candidates); +} + +void handleMinibufferCompletion(struct editorBuffer *minibuf, + enum promptType type) { + /* Get current buffer text */ + char *current_text = + minibuf->numrows > 0 ? (char *)minibuf->row[0].chars : ""; + + /* Check if text changed since last completion */ + if (minibuf->completion_state.last_completed_text == NULL || + strcmp(current_text, + minibuf->completion_state.last_completed_text) != 0) { + /* Text changed - reset completion state */ + resetCompletionState(&minibuf->completion_state); + } + + /* Get matches based on type */ + struct completion_result result; + switch (type) { + case PROMPT_FILES: + getFileCompletions(current_text, &result); + break; + case PROMPT_BASIC: + getBufferCompletions(&E, current_text, E.edbuf, &result); + break; + case PROMPT_COMMAND: + getCommandCompletions(&E, current_text, &result); + break; + case PROMPT_SEARCH: + /* For search, we can provide buffer completions */ + getBufferCompletions(&E, current_text, E.edbuf, &result); + break; + } + + /* Handle based on number of matches */ + if (result.n_matches == 0) { + editorSetStatusMessage("[No match]"); + minibuf->completion_state.preserve_message = 1; + } else if (result.n_matches == 1) { + /* Complete fully */ + replaceMinibufferText(minibuf, result.matches[0]); + closeCompletionsBuffer(); + } else { + /* Multiple matches */ + if (result.common_prefix && + strlen(result.common_prefix) > strlen(current_text)) { + /* Can extend to common prefix */ + replaceMinibufferText(minibuf, result.common_prefix); + closeCompletionsBuffer(); + } else { + /* Already at common prefix (or no common prefix found) */ + if (minibuf->completion_state.successive_tabs > 0) { + showCompletionsBuffer(result.matches, + result.n_matches); + } else { + editorSetStatusMessage( + "[Complete, but not unique]"); + minibuf->completion_state.preserve_message = 1; + } + } + } + + /* Update state BEFORE cleanup */ + minibuf->completion_state.successive_tabs++; + free(minibuf->completion_state.last_completed_text); + minibuf->completion_state.last_completed_text = xstrdup( + minibuf->numrows > 0 ? (char *)minibuf->row[0].chars : ""); + + /* Cleanup */ + freeCompletionResult(&result); +} diff --git a/completion.h b/completion.h new file mode 100644 index 0000000..fd3aeb2 --- /dev/null +++ b/completion.h @@ -0,0 +1,20 @@ +#ifndef EMSYS_COMPLETION_H +#define EMSYS_COMPLETION_H + +#include "emsys.h" + +void resetCompletionState(struct completion_state *state); +void freeCompletionResult(struct completion_result *result); +void getFileCompletions(const char *prefix, struct completion_result *result); +void getBufferCompletions(struct editorConfig *ed, const char *prefix, + struct editorBuffer *currentBuffer, + struct completion_result *result); +void getCommandCompletions(struct editorConfig *ed, const char *prefix, + struct completion_result *result); +void handleMinibufferCompletion(struct editorBuffer *minibuf, + enum promptType type); +char *findCommonPrefix(char **strings, int count); +void closeCompletionsBuffer(void); +void editorCompleteWord(struct editorConfig *ed, struct editorBuffer *bufr); + +#endif diff --git a/display.c b/display.c index 4b10896..cdf99e2 100644 --- a/display.c +++ b/display.c @@ -239,6 +239,15 @@ int windowFocusedIdx(void) { return 0; } +int findBufferWindow(struct editorBuffer *buf) { + for (int i = 0; i < E.nwindows; i++) { + if (E.windows[i]->buf == buf) { + return i; + } + } + return -1; +} + void synchronizeBufferCursor(struct editorBuffer *buf, struct editorWindow *win) { // Ensure the cursor is within the buffer's bounds @@ -808,21 +817,37 @@ void drawMinibuffer(struct abuf *ab) { void refreshScreen(void) { struct abuf ab = ABUF_INIT; abAppend(&ab, "\x1b[?25l", 6); // Hide cursor - abAppend(&ab, "\x1b[H", 3); // Move cursor to top-left corner + abAppend(&ab, "\x1b[H", 3); // Move cursor to top-left corner int focusedIdx = windowFocusedIdx(); int cumulative_height = 0; int total_height = E.screenrows - minibuffer_height - (statusbar_height * E.nwindows); - int window_height = total_height / E.nwindows; - int remaining_height = total_height % E.nwindows; + + /* skip if heights already set */ + int heights_set = 1; + for (int i = 0; i < E.nwindows; i++) { + if (E.windows[i]->height <= 0) { + heights_set = 0; + break; + } + } + + if (!heights_set) { + int window_height = total_height / E.nwindows; + int remaining_height = total_height % E.nwindows; + + for (int i = 0; i < E.nwindows; i++) { + struct editorWindow *win = E.windows[i]; + win->height = window_height; + if (i == E.nwindows - 1) + win->height += remaining_height; + } + } for (int i = 0; i < E.nwindows; i++) { struct editorWindow *win = E.windows[i]; - win->height = window_height; - if (i == E.nwindows - 1) - win->height += remaining_height; if (win->focused) scroll(); @@ -860,9 +885,9 @@ void refreshScreen(void) { abAppend(&ab, buf, strlen(buf)); abAppend(&ab, "\x1b[?25h", 6); // Show cursor - + write(STDOUT_FILENO, ab.b, ab.len); - + abFree(&ab); } @@ -907,28 +932,36 @@ void editorCreateWindow(void) { statusbar_height; } -void editorDestroyWindow(void) { +void editorDestroyWindow(int window_idx) { if (E.nwindows == 1) { editorSetStatusMessage("Can't kill last window"); return; } - int idx = windowFocusedIdx(); - editorSwitchWindow(); - free(E.windows[idx]); + + int focused_idx = windowFocusedIdx(); + + /* switch focus before destroying current window */ + if (window_idx == focused_idx) { + editorSwitchWindow(); + } + + free(E.windows[window_idx]); struct editorWindow **windows = xmalloc(sizeof(struct editorWindow *) * (--E.nwindows)); int j = 0; - for (int i = 0; i <= E.nwindows; i++) { - if (i != idx) { + for (int i = 0; i < E.nwindows + 1; i++) { + if (i != window_idx) { windows[j] = E.windows[i]; - if (windows[j]->focused) { - E.buf = windows[j]->buf; - } j++; } } free(E.windows); E.windows = windows; + + /* reset heights */ + for (int i = 0; i < E.nwindows; i++) { + E.windows[i]->height = 0; + } } void editorDestroyOtherWindows(void) { @@ -953,23 +986,22 @@ void editorDestroyOtherWindows(void) { } void editorWhatCursor(void) { - struct editorBuffer *bufr = E.buf; int c = 0; - if (bufr->cy >= bufr->numrows) { + if (E.buf->cy >= E.buf->numrows) { editorSetStatusMessage("End of buffer"); return; - } else if (bufr->row[bufr->cy].size <= bufr->cx) { + } else if (E.buf->row[E.buf->cy].size <= E.buf->cx) { c = (uint8_t)'\n'; } else { - c = (uint8_t)bufr->row[bufr->cy].chars[bufr->cx]; + c = (uint8_t)E.buf->row[E.buf->cy].chars[E.buf->cx]; } int npoint = 0, point = 0; - for (int y = 0; y < bufr->numrows; y++) { - for (int x = 0; x <= bufr->row[y].size; x++) { + for (int y = 0; y < E.buf->numrows; y++) { + for (int x = 0; x <= E.buf->row[y].size; x++) { npoint++; - if (x == bufr->cx && y == bufr->cy) { + if (x == E.buf->cx && y == E.buf->cy) { point = npoint; } } diff --git a/display.h b/display.h index e00a48e..675ddfa 100644 --- a/display.h +++ b/display.h @@ -49,11 +49,12 @@ void editorToggleTruncateLinesWrapper(struct editorConfig *ed, /* Window management functions */ int windowFocusedIdx(void); +int findBufferWindow(struct editorBuffer *buf); void editorSwitchWindow(void); void synchronizeBufferCursor(struct editorBuffer *buf, struct editorWindow *win); void editorCreateWindow(void); -void editorDestroyWindow(void); +void editorDestroyWindow(int window_idx); void editorDestroyOtherWindows(void); void editorWhatCursor(void); diff --git a/edit.c b/edit.c index 1182251..07b69f6 100644 --- a/edit.c +++ b/edit.c @@ -13,14 +13,26 @@ #include "region.h" #include "prompt.h" #include "terminal.h" +#include "history.h" #include "util.h" extern struct editorConfig E; +static void addToKillRing(const char *text) { + if (!text || strlen(text) == 0) + return; + + addHistory(&E.kill_history, text); + E.kill_ring_pos = -1; /* Reset position for M-y */ + + /* Update E.kill to point to the new kill */ + free(E.kill); + E.kill = xstrdup((uint8_t *)text); +} + /* Character insertion */ void editorInsertChar(struct editorBuffer *bufr, int c, int count) { - if (count <= 0) count = 1; @@ -613,10 +625,11 @@ void editorKillLine(int count) { } else { // Copy to kill ring int kill_len = row->size - E.buf->cx; - free(E.kill); - E.kill = xmalloc(kill_len + 1); - memcpy(E.kill, &row->chars[E.buf->cx], kill_len); - E.kill[kill_len] = '\0'; + char *killed_text = xmalloc(kill_len + 1); + memcpy(killed_text, &row->chars[E.buf->cx], kill_len); + killed_text[kill_len] = '\0'; + addToKillRing(killed_text); + free(killed_text); clearRedos(E.buf); struct editorUndo *new = newUndo(); @@ -655,10 +668,11 @@ void editorKillLineBackwards(void) { erow *row = &E.buf->row[E.buf->cy]; // Copy to kill ring - free(E.kill); - E.kill = xmalloc(E.buf->cx + 1); - memcpy(E.kill, row->chars, E.buf->cx); - E.kill[E.buf->cx] = '\0'; + char *killed_text = xmalloc(E.buf->cx + 1); + memcpy(killed_text, row->chars, E.buf->cx); + killed_text[E.buf->cx] = '\0'; + addToKillRing(killed_text); + free(killed_text); clearRedos(E.buf); struct editorUndo *new = newUndo(); @@ -691,118 +705,140 @@ void editorKillLineBackwards(void) { /* Navigation */ void editorPageUp(int count) { - struct editorBuffer *bufr = E.buf; struct editorWindow *win = E.windows[windowFocusedIdx()]; int times = count ? count : 1; for (int n = 0; n < times; n++) { int scroll_lines = win->height - page_overlap; - if (scroll_lines < 1) scroll_lines = 1; - - if (bufr->truncate_lines) { + if (scroll_lines < 1) + scroll_lines = 1; + + if (E.buf->truncate_lines) { /* Move view up by scroll_lines */ win->rowoff -= scroll_lines; if (win->rowoff < 0) { win->rowoff = 0; } - + /* Ensure cursor is within visible window */ - if (bufr->cy >= win->rowoff + win->height) { + if (E.buf->cy >= win->rowoff + win->height) { /* Cursor is below window - move it to bottom of window */ - bufr->cy = win->rowoff + win->height - 1; + E.buf->cy = win->rowoff + win->height - 1; } /* If cursor is above window, it's already visible */ } else { /* In wrapped mode, need to handle variable line heights */ int lines_scrolled = 0; int new_rowoff = win->rowoff; - + /* Scroll up by the desired number of screen lines */ - while (lines_scrolled < scroll_lines && new_rowoff > 0) { + while (lines_scrolled < scroll_lines && + new_rowoff > 0) { new_rowoff--; - int line_height = (calculateLineWidth(&bufr->row[new_rowoff]) / E.screencols) + 1; + int line_height = + (calculateLineWidth( + &E.buf->row[new_rowoff]) / + E.screencols) + + 1; lines_scrolled += line_height; } win->rowoff = new_rowoff; - + /* Ensure cursor is visible - calculate screen position */ - int cursor_screen_line = getScreenLineForRow(bufr, bufr->cy); - int window_start_screen_line = getScreenLineForRow(bufr, win->rowoff); - - if (cursor_screen_line >= window_start_screen_line + win->height) { + int cursor_screen_line = + getScreenLineForRow(E.buf, E.buf->cy); + int window_start_screen_line = + getScreenLineForRow(E.buf, win->rowoff); + + if (cursor_screen_line >= + window_start_screen_line + win->height) { /* Cursor is below window - move it up to be within window */ - while (bufr->cy > 0) { - cursor_screen_line = getScreenLineForRow(bufr, bufr->cy); - if (cursor_screen_line < window_start_screen_line + win->height) + while (E.buf->cy > 0) { + cursor_screen_line = + getScreenLineForRow(E.buf, + E.buf->cy); + if (cursor_screen_line < + window_start_screen_line + + win->height) break; - bufr->cy--; + E.buf->cy--; } } } - + /* Ensure cursor column is valid for new row */ - if (bufr->cy < bufr->numrows && bufr->cx > bufr->row[bufr->cy].size) { - bufr->cx = bufr->row[bufr->cy].size; + if (E.buf->cy < E.buf->numrows && + E.buf->cx > E.buf->row[E.buf->cy].size) { + E.buf->cx = E.buf->row[E.buf->cy].size; } } } void editorPageDown(int count) { - struct editorBuffer *bufr = E.buf; struct editorWindow *win = E.windows[windowFocusedIdx()]; int times = count ? count : 1; for (int n = 0; n < times; n++) { int scroll_lines = win->height - page_overlap; - if (scroll_lines < 1) scroll_lines = 1; - - if (bufr->truncate_lines) { + if (scroll_lines < 1) + scroll_lines = 1; + + if (E.buf->truncate_lines) { /* Move view down by scroll_lines */ win->rowoff += scroll_lines; - + /* Don't scroll past end of file */ - if (win->rowoff + win->height > bufr->numrows) { - win->rowoff = bufr->numrows - win->height; - if (win->rowoff < 0) win->rowoff = 0; + if (win->rowoff + win->height > E.buf->numrows) { + win->rowoff = E.buf->numrows - win->height; + if (win->rowoff < 0) + win->rowoff = 0; } - + /* Ensure cursor is within visible window */ - if (bufr->cy < win->rowoff) { + if (E.buf->cy < win->rowoff) { /* Cursor is above window - move it to top of window */ - bufr->cy = win->rowoff; + E.buf->cy = win->rowoff; } /* If cursor is below window, it's already visible */ } else { /* In wrapped mode, need to handle variable line heights */ int lines_scrolled = 0; int new_rowoff = win->rowoff; - + /* Scroll down by the desired number of screen lines */ - while (lines_scrolled < scroll_lines && new_rowoff < bufr->numrows) { - int line_height = (calculateLineWidth(&bufr->row[new_rowoff]) / E.screencols) + 1; + while (lines_scrolled < scroll_lines && + new_rowoff < E.buf->numrows) { + int line_height = + (calculateLineWidth( + &E.buf->row[new_rowoff]) / + E.screencols) + + 1; lines_scrolled += line_height; new_rowoff++; } - + /* Don't scroll too far */ - if (new_rowoff > bufr->numrows) { - new_rowoff = bufr->numrows; + if (new_rowoff > E.buf->numrows) { + new_rowoff = E.buf->numrows; } win->rowoff = new_rowoff; - + /* Ensure cursor is visible - calculate screen position */ - int cursor_screen_line = getScreenLineForRow(bufr, bufr->cy); - int window_start_screen_line = getScreenLineForRow(bufr, win->rowoff); - + int cursor_screen_line = + getScreenLineForRow(E.buf, E.buf->cy); + int window_start_screen_line = + getScreenLineForRow(E.buf, win->rowoff); + if (cursor_screen_line < window_start_screen_line) { /* Cursor is above window - move it down to be within window */ - bufr->cy = win->rowoff; + E.buf->cy = win->rowoff; } } - + /* Ensure cursor column is valid for new row */ - if (bufr->cy < bufr->numrows && bufr->cx > bufr->row[bufr->cy].size) { - bufr->cx = bufr->row[bufr->cy].size; + if (E.buf->cy < E.buf->numrows && + E.buf->cx > E.buf->row[E.buf->cy].size) { + E.buf->cx = E.buf->row[E.buf->cy].size; } } } diff --git a/emsys.h b/emsys.h index 57267a8..2fb37ae 100644 --- a/emsys.h +++ b/emsys.h @@ -15,7 +15,6 @@ #define EMSYS_VERSION "unknown" #endif - #define ESC "\033" #define CSI ESC "[" #define CRLF "\r\n" @@ -25,6 +24,7 @@ enum promptType { PROMPT_BASIC, PROMPT_FILES, PROMPT_COMMAND, + PROMPT_SEARCH, }; /*** data ***/ @@ -53,6 +53,21 @@ struct editorUndo { uint8_t *data; }; +struct completion_state { + char *last_completed_text; + int completion_start_pos; + int successive_tabs; + int last_completion_count; + int preserve_message; +}; + +struct completion_result { + char **matches; + int n_matches; + char *common_prefix; + int prefix_len; +}; + struct editorBuffer { int indent; int cx, cy; @@ -77,6 +92,7 @@ struct editorBuffer { int *screen_line_start; int screen_line_cache_size; int screen_line_cache_valid; + struct completion_state completion_state; }; struct editorWindow { @@ -136,6 +152,20 @@ struct editorRegister { union registerData rdata; }; +#define HISTORY_MAX_ENTRIES 100 + +struct historyEntry { + char *str; + struct historyEntry *prev; + struct historyEntry *next; +}; + +struct editorHistory { + struct historyEntry *head; + struct historyEntry *tail; + int count; +}; + struct editorConfig { uint8_t *kill; uint8_t *rectKill; @@ -168,6 +198,13 @@ struct editorConfig { struct editorBuffer *lastVisitedBuffer; int uarg; /* Universal argument: 0 = off, non-zero = active with that value */ int macro_depth; /* Current macro execution depth to prevent infinite recursion */ + + struct editorHistory file_history; + struct editorHistory command_history; + struct editorHistory shell_history; + struct editorHistory search_history; + struct editorHistory kill_history; + int kill_ring_pos; /* Current position in kill ring for M-y */ }; /*** prototypes ***/ diff --git a/fileio.c b/fileio.c index a0dacb7..0ce26c9 100644 --- a/fileio.c +++ b/fileio.c @@ -3,7 +3,6 @@ #include "emsys.h" #include "fileio.h" #include "buffer.h" -#include "tab.h" #include #include #include @@ -72,7 +71,7 @@ void editorOpen(struct editorBuffer *bufr, char *filename) { linelen--; editorInsertRow(bufr, bufr->numrows, line, linelen); } - + free(line); fclose(fp); bufr->dirty = 0; diff --git a/find.c b/find.c index 1432bc3..500e51c 100644 --- a/find.c +++ b/find.c @@ -16,6 +16,8 @@ #include "prompt.h" #include "unused.h" #include "util.h" +#include "history.h" +#include "buffer.h" extern struct editorConfig E; static int regex_mode = 0; @@ -127,7 +129,11 @@ char *str_replace(char *orig, char *rep, char *with) { void editorFindCallback(struct editorBuffer *bufr, uint8_t *query, int key) { static int last_match = -1; static int direction = 1; - bufr->query = query; + + if (bufr->query != query) { + free(bufr->query); + bufr->query = query ? xstrdup((char *)query) : NULL; + } bufr->match = 0; if (key == CTRL('g') || key == CTRL('c') || key == '\r') { @@ -144,6 +150,10 @@ void editorFindCallback(struct editorBuffer *bufr, uint8_t *query, int key) { direction = 1; } + if (!query || strlen((char *)query) == 0) { + return; + } + if (last_match == -1) direction = 1; int current = last_match; @@ -157,8 +167,9 @@ void editorFindCallback(struct editorBuffer *bufr, uint8_t *query, int key) { match = regexSearch(&(row->chars[bufr->cx + 1]), query); } else { - match = strstr((char *)&(row->chars[bufr->cx + 1]), - (char *)query); + match = strstr( + (char *)&(row->chars[bufr->cx + 1]), + (char *)query); } } if (match) { @@ -212,8 +223,9 @@ void editorFind(struct editorBuffer *bufr) { // int saved_rowoff = bufr->rowoff; uint8_t *query = editorPrompt(bufr, "Search (C-g to cancel): %s", - PROMPT_BASIC, editorFindCallback); + PROMPT_SEARCH, editorFindCallback); + free(bufr->query); bufr->query = NULL; if (query) { free(query); @@ -230,8 +242,9 @@ void editorRegexFind(struct editorBuffer *bufr) { int saved_cy = bufr->cy; uint8_t *query = editorPrompt(bufr, "Regex search (C-g to cancel): %s", - PROMPT_BASIC, editorFindCallback); + PROMPT_SEARCH, editorFindCallback); + free(bufr->query); bufr->query = NULL; regex_mode = 0; /* Reset after search */ if (query) { @@ -280,7 +293,8 @@ static int nextOccur(struct editorBuffer *buf, uint8_t *needle, int ocheck) { } while (buf->cy < buf->numrows) { erow *row = &buf->row[buf->cy]; - uint8_t *match = strstr((char *)&(row->chars[buf->cx]), (char *)needle); + uint8_t *match = + strstr((char *)&(row->chars[buf->cx]), (char *)needle); if (match) { if (!(buf->cx == ox && buf->cy == oy)) { buf->cx = match - row->chars; diff --git a/history.c b/history.c new file mode 100644 index 0000000..ae98433 --- /dev/null +++ b/history.c @@ -0,0 +1,82 @@ +#include +#include +#include "emsys.h" +#include "history.h" +#include "util.h" + +extern struct editorConfig E; + +void initHistory(struct editorHistory *hist) { + hist->head = NULL; + hist->tail = NULL; + hist->count = 0; +} + +void addHistory(struct editorHistory *hist, const char *str) { + if (!str || strlen(str) == 0) { + return; + } + + /* Don't add duplicates of the most recent entry */ + if (hist->tail && strcmp(hist->tail->str, str) == 0) { + return; + } + + /* Create new entry */ + struct historyEntry *entry = xmalloc(sizeof(struct historyEntry)); + entry->str = xstrdup(str); + entry->next = NULL; + entry->prev = hist->tail; + + /* Add to list */ + if (hist->tail) { + hist->tail->next = entry; + } else { + hist->head = entry; + } + hist->tail = entry; + hist->count++; + + /* Remove oldest entries if we exceed the limit */ + while (hist->count > HISTORY_MAX_ENTRIES) { + struct historyEntry *old = hist->head; + hist->head = old->next; + if (hist->head) { + hist->head->prev = NULL; + } + free(old->str); + free(old); + hist->count--; + } +} + +char *getHistoryAt(struct editorHistory *hist, int index) { + if (index < 0 || index >= hist->count) { + return NULL; + } + + struct historyEntry *entry = hist->tail; + for (int i = hist->count - 1; i > index && entry; i--) { + entry = entry->prev; + } + + return entry ? entry->str : NULL; +} + +void freeHistory(struct editorHistory *hist) { + struct historyEntry *entry = hist->head; + while (entry) { + struct historyEntry *next = entry->next; + free(entry->str); + free(entry); + entry = next; + } + initHistory(hist); +} + +char *getLastHistory(struct editorHistory *hist) { + if (hist->tail) { + return hist->tail->str; + } + return NULL; +} diff --git a/history.h b/history.h new file mode 100644 index 0000000..cd568f5 --- /dev/null +++ b/history.h @@ -0,0 +1,12 @@ +#ifndef EMSYS_HISTORY_H +#define EMSYS_HISTORY_H + +#include "emsys.h" + +void initHistory(struct editorHistory *hist); +void addHistory(struct editorHistory *hist, const char *str); +char *getHistoryAt(struct editorHistory *hist, int index); +void freeHistory(struct editorHistory *hist); +char *getLastHistory(struct editorHistory *hist); + +#endif diff --git a/keymap.c b/keymap.c index b581b7b..e89531b 100644 --- a/keymap.c +++ b/keymap.c @@ -21,7 +21,7 @@ #include "region.h" #include "register.h" #include "buffer.h" -#include "tab.h" +#include "completion.h" #include "transform.h" #include "undo.h" #include "unicode.h" @@ -143,14 +143,12 @@ void editorRecordKey(int c) { /* Command execution with prefix state machine */ void executeCommand(int key) { static enum PrefixState prefix = PREFIX_NONE; - struct editorBuffer *UNUSED(bufr) = E.buf; - /* Handle prefix state transitions and commands */ switch (key) { case CTRL('x'): #ifdef EMSYS_CUA /* CUA mode: if region marked, cut instead of prefix */ - if (bufr->markx != -1 && bufr->marky != -1) { + if (E.buf->markx != -1 && E.buf->marky != -1) { /* Let the regular processing handle the cut */ prefix = PREFIX_NONE; editorProcessKeypress(CUT); @@ -417,7 +415,14 @@ void editorProcessKeypress(int c) { editorRecordKey(c); } - struct editorBuffer *bufr = E.buf; + if (c != CTRL('y') && c != YANK_POP +#ifdef EMSYS_CUA + && c != CTRL('v') +#endif + ) { + E.kill_ring_pos = -1; + } + int windowIdx = windowFocusedIdx(); struct editorWindow *win = E.windows[windowIdx]; @@ -427,7 +432,7 @@ void editorProcessKeypress(int c) { #else if (E.micro == REDO && c == CTRL('_')) { #endif //EMSYS_CUA - editorDoRedo(bufr, 1); + editorDoRedo(E.buf, 1); return; } else { E.micro = 0; @@ -472,7 +477,7 @@ void editorProcessKeypress(int c) { // Handle PIPE_CMD if (c == PIPE_CMD) { - editorPipeCmd(&E, bufr); + editorPipeCmd(&E, E.buf); E.uarg = 0; return; } @@ -482,15 +487,15 @@ void editorProcessKeypress(int c) { switch (c) { case '\r': - editorInsertNewline(bufr, uarg); + editorInsertNewline(E.buf, uarg); break; case BACKSPACE: case CTRL('h'): - editorBackSpace(bufr, uarg); + editorBackSpace(E.buf, uarg); break; case DEL_KEY: case CTRL('d'): - editorDelChar(bufr, uarg); + editorDelChar(E.buf, uarg); break; case CTRL('l'): recenter(win); @@ -531,8 +536,8 @@ void editorProcessKeypress(int c) { editorPageDown(uarg); break; case BEG_OF_FILE: - bufr->cy = 0; - bufr->cx = 0; + E.buf->cy = 0; + E.buf->cx = 0; break; case CUSTOM_INFO_MESSAGE: { int winIdx = windowFocusedIdx(); @@ -545,8 +550,8 @@ void editorProcessKeypress(int c) { E.screenrows, win->rowoff); } break; case END_OF_FILE: - bufr->cy = bufr->numrows; - bufr->cx = 0; + E.buf->cy = E.buf->numrows; + E.buf->cx = 0; break; case HOME_KEY: case CTRL('a'): @@ -557,36 +562,36 @@ void editorProcessKeypress(int c) { editorEndOfLine(uarg); break; case CTRL('s'): - editorFind(bufr); + editorFind(E.buf); break; case REGEX_SEARCH_FORWARD: - editorRegexFind(bufr); + editorRegexFind(E.buf); break; case REGEX_SEARCH_BACKWARD: - editorBackwardRegexFind(bufr); + editorBackwardRegexFind(E.buf); break; case UNICODE_ERROR: editorSetStatusMessage("Bad UTF-8 sequence"); break; case UNICODE: - editorInsertUnicode(bufr, uarg); + editorInsertUnicode(E.buf, uarg); break; #ifdef EMSYS_CUA case CUT: - editorKillRegion(&E, bufr); + editorKillRegion(&E, E.buf); editorClearMark(); break; #endif //EMSYS_CUA case SAVE: - editorSave(bufr); + editorSave(E.buf); break; case COPY: - editorCopyRegion(&E, bufr); + editorCopyRegion(&E, E.buf); editorClearMark(); break; #ifdef EMSYS_CUA case CTRL('C'): - editorCopyRegion(&E, bufr); + editorCopyRegion(&E, E.buf); editorClearMark(); break; #endif //EMSYS_CUA @@ -597,17 +602,20 @@ void editorProcessKeypress(int c) { #ifdef EMSYS_CUA case CTRL('v'): #endif //EMSYS_CUA - editorYank(&E, bufr, uarg ? uarg : 1); + editorYank(&E, E.buf, uarg ? uarg : 1); + break; + case YANK_POP: + editorYankPop(&E, E.buf); break; case CTRL('w'): - editorKillRegion(&E, bufr); + editorKillRegion(&E, E.buf); editorClearMark(); break; case CTRL('_'): #ifdef EMSYS_CUA case CTRL('z'): #endif //EMSYS_CUA - editorDoUndo(bufr, uarg); + editorDoUndo(E.buf, uarg); break; case CTRL('k'): editorKillLine(uarg); @@ -618,10 +626,10 @@ void editorProcessKeypress(int c) { break; #endif case CTRL('j'): - editorInsertNewlineAndIndent(bufr, uarg ? uarg : 1); + editorInsertNewlineAndIndent(E.buf, uarg ? uarg : 1); break; case CTRL('o'): - editorOpenLine(bufr, uarg ? uarg : 1); + editorOpenLine(E.buf, uarg ? uarg : 1); break; case CTRL('q'):; int nread; @@ -631,9 +639,9 @@ void editorProcessKeypress(int c) { } int count = uarg ? uarg : 1; for (int i = 0; i < count; i++) { - editorUndoAppendChar(bufr, c); + editorUndoAppendChar(E.buf, c); } - editorInsertChar(bufr, c, count); + editorInsertChar(E.buf, c, count); break; case FORWARD_WORD: editorForwardWord(uarg); @@ -648,8 +656,8 @@ void editorProcessKeypress(int c) { editorBackPara(uarg); break; case REDO: - editorDoRedo(bufr, uarg); - if (bufr->redo != NULL) { + editorDoRedo(E.buf, uarg); + if (E.buf->redo != NULL) { editorSetStatusMessage( "Press C-_ or C-/ to redo again"); E.micro = REDO; @@ -669,19 +677,39 @@ void editorProcessKeypress(int c) { break; case OTHER_WINDOW: - editorSwitchWindow(); + if (E.buf == E.minibuf) { + editorSetStatusMessage( + "Command attempted to use minibuffer while in minibuffer"); + } else { + editorSwitchWindow(); + } break; case CREATE_WINDOW: - editorCreateWindow(); + if (E.buf == E.minibuf) { + editorSetStatusMessage( + "Command attempted to use minibuffer while in minibuffer"); + } else { + editorCreateWindow(); + } break; case DESTROY_WINDOW: - editorDestroyWindow(); + if (E.buf == E.minibuf) { + editorSetStatusMessage( + "Command attempted to use minibuffer while in minibuffer"); + } else { + editorDestroyWindow(windowFocusedIdx()); + } break; case DESTROY_OTHER_WINDOWS: - editorDestroyOtherWindows(); + if (E.buf == E.minibuf) { + editorSetStatusMessage( + "Command attempted to use minibuffer while in minibuffer"); + } else { + editorDestroyOtherWindows(); + } break; case KILL_BUFFER: editorKillBuffer(); @@ -692,30 +720,30 @@ void editorProcessKeypress(int c) { break; case DELETE_WORD: - editorDeleteWord(bufr, uarg); + editorDeleteWord(E.buf, uarg); break; case BACKSPACE_WORD: - editorBackspaceWord(bufr, uarg); + editorBackspaceWord(E.buf, uarg); break; case UPCASE_WORD: - editorUpcaseWord(bufr, uarg ? uarg : 1); + editorUpcaseWord(E.buf, uarg ? uarg : 1); break; case DOWNCASE_WORD: - editorDowncaseWord(bufr, uarg ? uarg : 1); + editorDowncaseWord(E.buf, uarg ? uarg : 1); break; case CAPCASE_WORD: - editorCapitalCaseWord(bufr, uarg ? uarg : 1); + editorCapitalCaseWord(E.buf, uarg ? uarg : 1); break; case UPCASE_REGION: - editorTransformRegion(&E, bufr, transformerUpcase); + editorTransformRegion(&E, E.buf, transformerUpcase); break; case DOWNCASE_REGION: - editorTransformRegion(&E, bufr, transformerDowncase); + editorTransformRegion(&E, E.buf, transformerDowncase); break; case TOGGLE_TRUNCATE_LINES: editorToggleTruncateLines(); @@ -726,23 +754,23 @@ void editorProcessKeypress(int c) { break; case TRANSPOSE_WORDS: - editorTransposeWords(bufr); + editorTransposeWords(E.buf); break; case CTRL('t'): - editorTransposeChars(bufr); + editorTransposeChars(E.buf); break; case EXEC_CMD:; uint8_t *cmd = - editorPrompt(bufr, "cmd: %s", PROMPT_COMMAND, NULL); + editorPrompt(E.buf, "cmd: %s", PROMPT_COMMAND, NULL); if (cmd != NULL) { - runCommand(cmd, &E, bufr); + runCommand(cmd, &E, E.buf); free(cmd); } break; case QUERY_REPLACE: - editorQueryReplace(&E, bufr); + editorQueryReplace(&E, E.buf); break; case GOTO_LINE: @@ -750,7 +778,7 @@ void editorProcessKeypress(int c) { break; case INSERT_FILE: - editorInsertFile(&E, bufr); + editorInsertFile(&E, E.buf); break; case CTRL('x'): @@ -768,18 +796,18 @@ void editorProcessKeypress(int c) { break; case BACKTAB: - editorUnindent(bufr, uarg); + editorUnindent(E.buf, uarg); break; case SWAP_MARK: - if (0 <= bufr->markx && - (0 <= bufr->marky && bufr->marky < bufr->numrows)) { - int swapx = bufr->cx; - int swapy = bufr->cy; - bufr->cx = bufr->markx; - bufr->cy = bufr->marky; - bufr->markx = swapx; - bufr->marky = swapy; + if (0 <= E.buf->markx && + (0 <= E.buf->marky && E.buf->marky < E.buf->numrows)) { + int swapx = E.buf->cx; + int swapy = E.buf->cy; + E.buf->cx = E.buf->markx; + E.buf->cy = E.buf->marky; + E.buf->markx = swapx; + E.buf->marky = swapy; } break; @@ -796,42 +824,42 @@ void editorProcessKeypress(int c) { editorNumberToRegister(&E, uarg); break; case REGION_REGISTER: - editorRegionToRegister(&E, bufr); + editorRegionToRegister(&E, E.buf); break; case INC_REGISTER: - editorIncrementRegister(&E, bufr); + editorIncrementRegister(&E, E.buf); break; case INSERT_REGISTER: - editorInsertRegister(&E, bufr); + editorInsertRegister(&E, E.buf); break; case VIEW_REGISTER: - editorViewRegister(&E, bufr); + editorViewRegister(&E, E.buf); break; case STRING_RECT: - editorStringRectangle(&E, bufr); + editorStringRectangle(&E, E.buf); break; case COPY_RECT: - editorCopyRectangle(&E, bufr); + editorCopyRectangle(&E, E.buf); editorClearMark(); break; case KILL_RECT: - editorKillRectangle(&E, bufr); + editorKillRectangle(&E, E.buf); editorClearMark(); break; case YANK_RECT: - editorYankRectangle(&E, bufr); + editorYankRectangle(&E, E.buf); break; case RECT_REGISTER: - editorRectRegister(&E, bufr); + editorRectRegister(&E, E.buf); break; case EXPAND: - editorCompleteWord(&E, bufr); + editorCompleteWord(&E, E.buf); break; case MACRO_RECORD: @@ -874,24 +902,24 @@ void editorProcessKeypress(int c) { // Handle TAB character // In minibuffer, tab should NOT be processed here // It's handled by the prompt function for completion - if (bufr == E.minibuf) { + if (E.buf == E.minibuf) { // Do nothing - let prompt handle it break; } int count = uarg ? uarg : 1; for (int i = 0; i < count; i++) { - editorUndoAppendChar(bufr, '\t'); + editorUndoAppendChar(E.buf, '\t'); } - editorInsertChar(bufr, '\t', count); + editorInsertChar(E.buf, '\t', count); } else if (ISCTRL(c)) { editorSetStatusMessage("Unknown command C-%c", c | 0x60); } else { int count = uarg ? uarg : 1; for (int i = 0; i < count; i++) { - editorUndoAppendChar(bufr, c); + editorUndoAppendChar(E.buf, c); } - editorInsertChar(bufr, c, count); + editorInsertChar(E.buf, c, count); } break; } diff --git a/keymap.h b/keymap.h index 2d740ea..8630bee 100644 --- a/keymap.h +++ b/keymap.h @@ -92,6 +92,9 @@ enum editorKey { REGEX_SEARCH_FORWARD, REGEX_SEARCH_BACKWARD, INSERT_FILE, + HISTORY_PREV, + HISTORY_NEXT, + YANK_POP, }; struct editorBuffer; diff --git a/main.c b/main.c index ca823d6..b3eb666 100644 --- a/main.c +++ b/main.c @@ -18,8 +18,9 @@ #include "pipe.h" #include "region.h" #include "register.h" +#include "history.h" #include "buffer.h" -#include "tab.h" +#include "completion.h" #include "transform.h" #include "undo.h" #include "unicode.h" @@ -83,6 +84,13 @@ void initEditor(void) { E.lastVisitedBuffer = NULL; E.macro_depth = 0; + initHistory(&E.file_history); + initHistory(&E.command_history); + initHistory(&E.shell_history); + initHistory(&E.search_history); + initHistory(&E.kill_history); + E.kill_ring_pos = -1; + if (getWindowSize(&E.screenrows, &E.screencols) == -1) die("getWindowSize"); } @@ -96,7 +104,7 @@ int main(int argc, char *argv[]) { enableRawMode(); initEditor(); - + E.headbuf = newBuffer(); E.buf = E.headbuf; if (argc >= 2) { @@ -139,7 +147,7 @@ int main(int argc, char *argv[]) { for (;;) { refreshScreen(); - + int c = editorReadKey(); if (c == MACRO_RECORD) { if (E.recording) { @@ -178,5 +186,9 @@ int main(int argc, char *argv[]) { executeCommand(c); } } + + /* cleanup */ + free(E.kill); + return 0; } diff --git a/pipe.c b/pipe.c index d0e5fde..31e2f8f 100644 --- a/pipe.c +++ b/pipe.c @@ -190,8 +190,8 @@ void editorPipeCmd(struct editorConfig *ed, struct editorBuffer *bufr) { #include "display.h" void editorPipeCmd(struct editorConfig *ed, struct editorBuffer *bufr) { - (void)ed; /* unused parameter */ - (void)bufr; /* unused parameter */ + (void)ed; /* unused parameter */ + (void)bufr; /* unused parameter */ editorSetStatusMessage("Pipe command not available on this platform"); } diff --git a/prompt.c b/prompt.c index d7efdd9..77d49f1 100644 --- a/prompt.c +++ b/prompt.c @@ -9,23 +9,20 @@ #include "display.h" #include "keymap.h" #include "unicode.h" -#include "tab.h" #include "edit.h" #include "buffer.h" #include "util.h" +#include "completion.h" +#include "history.h" extern struct editorConfig E; uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, enum promptType t, void (*callback)(struct editorBuffer *, uint8_t *, int)) { - uint8_t *result = NULL; + int history_pos = -1; - /* Save current buffer context */ - struct editorBuffer *saved_edbuf = E.edbuf; - - /* Clear minibuffer */ while (E.minibuf->numrows > 0) { editorDelRow(E.minibuf, 0); } @@ -33,7 +30,7 @@ uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, E.minibuf->cx = 0; E.minibuf->cy = 0; - /* Switch to minibuffer */ + /* Save editor buffer and switch to minibuffer */ E.edbuf = E.buf; E.buf = E.minibuf; @@ -42,7 +39,10 @@ uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, char *content = E.minibuf->numrows > 0 ? (char *)E.minibuf->row[0].chars : ""; - editorSetStatusMessage((char *)prompt, content); + if (!E.minibuf->completion_state.preserve_message) { + editorSetStatusMessage((char *)prompt, content); + } + E.minibuf->completion_state.preserve_message = 0; refreshScreen(); /* Position cursor on bottom line */ @@ -53,6 +53,8 @@ uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, int c = editorReadKey(); editorRecordKey(c); + int callback_key = c; + /* Handle special minibuffer keys */ switch (c) { case '\r': @@ -70,71 +72,103 @@ uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, result = NULL; goto done; - case CTRL('i'): /* Tab completion */ - if (t == PROMPT_FILES) { - char *old_text = - E.minibuf->numrows > 0 ? - xstrdup((char *)E.minibuf - ->row[0] - .chars) : - xstrdup(""); - uint8_t *tc = - tabCompleteFiles((uint8_t *)old_text); - if (tc && tc != (uint8_t *)old_text) { - /* Replace minibuffer content */ - editorDelRow(E.minibuf, 0); + case '\t': + handleMinibufferCompletion(E.minibuf, t); + break; + + case CTRL('s'): + /* C-s C-s: populate empty search with last search */ + if (t == PROMPT_SEARCH && E.minibuf->numrows > 0 && + E.minibuf->row[0].size == 0) { + char *last_search = + getLastHistory(&E.search_history); + if (last_search) { + while (E.minibuf->numrows > 0) { + editorDelRow(E.minibuf, 0); + } editorInsertRow(E.minibuf, 0, - (char *)tc, - strlen((char *)tc)); - E.minibuf->cx = strlen((char *)tc); + last_search, + strlen(last_search)); + E.minibuf->cx = strlen(last_search); E.minibuf->cy = 0; - free(tc); + } else { + editorSetStatusMessage( + "[No previous search]"); } - free(old_text); - } else if (t == PROMPT_BASIC) { - char *old_text = - E.minibuf->numrows > 0 ? - xstrdup((char *)E.minibuf - ->row[0] - .chars) : - xstrdup(""); - uint8_t *tc = tabCompleteBufferNames( - &E, (uint8_t *)old_text, bufr); - if (tc && tc != (uint8_t *)old_text) { - /* Replace minibuffer content */ - editorDelRow(E.minibuf, 0); - editorInsertRow(E.minibuf, 0, - (char *)tc, - strlen((char *)tc)); - E.minibuf->cx = strlen((char *)tc); - E.minibuf->cy = 0; - free(tc); + } + break; + + case HISTORY_PREV: + case HISTORY_NEXT: { + struct editorHistory *hist = NULL; + char *history_str = NULL; + + switch (t) { + case PROMPT_FILES: + hist = &E.file_history; + break; + case PROMPT_COMMAND: + hist = &E.command_history; + break; + case PROMPT_BASIC: + hist = &E.shell_history; + break; + case PROMPT_SEARCH: + hist = &E.search_history; + break; + } + + if (hist && hist->count > 0) { + if (c == HISTORY_PREV) { + if (history_pos == -1) { + history_pos = hist->count - 1; + } else if (history_pos > 0) { + history_pos--; + } + } else { + if (history_pos >= 0 && + history_pos < hist->count - 1) { + history_pos++; + } else { + history_pos = -1; + } } - free(old_text); - } else if (t == PROMPT_COMMAND) { - char *old_text = - E.minibuf->numrows > 0 ? - xstrdup((char *)E.minibuf - ->row[0] - .chars) : - xstrdup(""); - uint8_t *tc = tabCompleteCommands( - &E, (uint8_t *)old_text); - if (tc && tc != (uint8_t *)old_text) { - /* Replace minibuffer content */ - editorDelRow(E.minibuf, 0); - editorInsertRow(E.minibuf, 0, - (char *)tc, - strlen((char *)tc)); - E.minibuf->cx = strlen((char *)tc); + + if (history_pos >= 0) { + history_str = + getHistoryAt(hist, history_pos); + if (history_str) { + while (E.minibuf->numrows > 0) { + editorDelRow(E.minibuf, + 0); + } + editorInsertRow( + E.minibuf, 0, + history_str, + strlen(history_str)); + E.minibuf->cx = + strlen(history_str); + E.minibuf->cy = 0; + } + } else { + while (E.minibuf->numrows > 0) { + editorDelRow(E.minibuf, 0); + } + editorInsertRow(E.minibuf, 0, "", 0); + E.minibuf->cx = 0; E.minibuf->cy = 0; - free(tc); } - free(old_text); } break; + } default: + if (E.minibuf->completion_state.last_completed_text != + NULL) { + resetCompletionState( + &E.minibuf->completion_state); + } + editorProcessKeypress(c); /* Ensure single line */ @@ -172,16 +206,49 @@ uint8_t *editorPrompt(struct editorBuffer *bufr, uint8_t *prompt, char *text = E.minibuf->numrows > 0 ? (char *)E.minibuf->row[0].chars : ""; - callback(bufr, (uint8_t *)text, c); + callback(bufr, (uint8_t *)text, callback_key); } } done: - /* Restore previous buffer */ - E.buf = saved_edbuf; - E.edbuf = saved_edbuf; + if (result && strlen((char *)result) > 0) { + struct editorHistory *hist = NULL; + switch (t) { + case PROMPT_FILES: + hist = &E.file_history; + break; + case PROMPT_COMMAND: + hist = &E.command_history; + break; + case PROMPT_BASIC: + hist = &E.shell_history; + break; + case PROMPT_SEARCH: + hist = &E.search_history; + break; + } + if (hist) { + addHistory(hist, (char *)result); + } + } + + closeCompletionsBuffer(); + + /* Destroy the completions buffer entirely */ + struct editorBuffer *comp_buf = NULL; + for (struct editorBuffer *b = E.headbuf; b != NULL; b = b->next) { + if (b->filename && strcmp(b->filename, "*Completions*") == 0) { + comp_buf = b; + break; + } + } + if (comp_buf) { + destroyBuffer(comp_buf); + } + + E.buf = E.edbuf; editorSetStatusMessage(""); - + return result; } diff --git a/region.c b/region.c index f5cbf29..b36b7f9 100644 --- a/region.c +++ b/region.c @@ -8,11 +8,23 @@ #include "buffer.h" #include "undo.h" #include "display.h" +#include "history.h" #include "prompt.h" #include "util.h" extern struct editorConfig E; +static void addToKillRing(const char *text) { + if (!text || strlen(text) == 0) + return; + + addHistory(&E.kill_history, text); + E.kill_ring_pos = -1; + + free(E.kill); + E.kill = xstrdup((uint8_t *)text); +} + void editorSetMark(void) { E.buf->markx = E.buf->cx; E.buf->marky = E.buf->cy; @@ -165,6 +177,8 @@ void editorCopyRegion(struct editorConfig *ed, struct editorBuffer *buf) { } ed->kill[killpos] = 0; + addToKillRing((char *)ed->kill); + buf->cx = origCx; buf->cy = origCy; buf->markx = origMarkx; @@ -220,6 +234,45 @@ void editorYank(struct editorConfig *ed, struct editorBuffer *buf, int count) { buf->dirty = 1; editorUpdateBuffer(buf); + + /* Set kill ring position to most recent */ + ed->kill_ring_pos = + ed->kill_history.count > 0 ? ed->kill_history.count - 1 : 0; +} + +void editorYankPop(struct editorConfig *ed, struct editorBuffer *buf) { + if (ed->kill_history.count == 0) { + editorSetStatusMessage("Kill ring is empty"); + return; + } + + if (ed->kill_ring_pos < 0) { + editorSetStatusMessage("Previous command was not a yank"); + return; + } + + if (buf->undo == NULL || buf->undo->delete != 0) { + editorSetStatusMessage("Previous command was not a yank"); + return; + } + + editorDoUndo(buf, 1); + + ed->kill_ring_pos--; + if (ed->kill_ring_pos < 0) { + ed->kill_ring_pos = ed->kill_history.count - 1; + } + + char *kill_text = getHistoryAt(&ed->kill_history, ed->kill_ring_pos); + if (kill_text) { + free(ed->kill); + ed->kill = xstrdup((uint8_t *)kill_text); + int saved_pos = ed->kill_ring_pos; + editorYank(ed, buf, 1); + ed->kill_ring_pos = saved_pos; + } else { + editorSetStatusMessage("No more kill ring entries to yank!"); + } } void editorTransformRegion(struct editorConfig *ed, struct editorBuffer *buf, @@ -236,15 +289,13 @@ void editorTransformRegion(struct editorConfig *ed, struct editorBuffer *buf, editorKillRegion(ed, buf); uint8_t *input = ed->kill; - ed->kill = transformer(input); + uint8_t *transformed = transformer(input); + free(ed->kill); + ed->kill = transformed; editorYank(ed, buf, 1); buf->undo->paired = 1; - if (input == ed->kill) { - editorSetStatusMessage("Shouldn't free input here"); - } else { - free(ed->kill); - } + free(ed->kill); ed->kill = okill; } @@ -350,16 +401,21 @@ void editorReplaceRegex(struct editorConfig *ed, struct editorBuffer *buf) { buf->markx - buf->cx); } else if (i == buf->cy) { emsys_strlcat((char *)new->data, - (char *)&row->chars[buf->cx], new->datasize); + (char *)&row->chars[buf->cx], + new->datasize); } else if (i == buf->marky) { strncat((char *)new->data, (char *)row->chars, buf->markx); } else { - emsys_strlcat((char *)new->data, (char *)row->chars, new->datasize); + emsys_strlcat((char *)new->data, + (char *)row->chars, + new->datasize); } continue; } else if (i == buf->cy && match_idx < buf->cx) { - emsys_strlcat((char *)new->data, (char *)&row->chars[buf->cx], new->datasize); + emsys_strlcat((char *)new->data, + (char *)&row->chars[buf->cx], + new->datasize); continue; } else if (i == buf->marky && match_idx + match_length > buf->markx) { @@ -388,13 +444,16 @@ void editorReplaceRegex(struct editorConfig *ed, struct editorBuffer *buf) { strncat((char *)new->data, (char *)&row->chars[buf->cx], buf->markx - buf->cx); } else if (i == buf->cy) { - emsys_strlcat((char *)new->data, (char *)&row->chars[buf->cx], new->datasize); + emsys_strlcat((char *)new->data, + (char *)&row->chars[buf->cx], + new->datasize); } else if (i == buf->marky) { buf->markx += extra; strncat((char *)new->data, (char *)row->chars, buf->markx); } else { - emsys_strlcat((char *)new->data, (char *)row->chars, new->datasize); + emsys_strlcat((char *)new->data, (char *)row->chars, + new->datasize); } } /* Now take care of insert undo */ @@ -536,7 +595,8 @@ void editorStringRectangle(struct editorConfig *ed, struct editorBuffer *buf) { if (boty == topy) { emsys_strlcat((char *)new->data, (char *)string, new->datasize); } else { - emsys_strlcat((char *)new->data, (char *)&row->chars[topx], new->datasize); + emsys_strlcat((char *)new->data, (char *)&row->chars[topx], + new->datasize); } for (int i = topy + 1; i < boty; i++) { @@ -559,7 +619,8 @@ void editorStringRectangle(struct editorConfig *ed, struct editorBuffer *buf) { memcpy(&row->chars[topx], string, slen); row->size += extra; row->chars[row->size] = 0; - emsys_strlcat((char *)new->data, (char *)row->chars, new->datasize); + emsys_strlcat((char *)new->data, (char *)row->chars, + new->datasize); } /* Finally, end line */ @@ -759,8 +820,10 @@ void editorKillRectangle(struct editorConfig *ed, struct editorBuffer *buf) { row->size -= (row->size - (botx - ed->rx)); row->chars[row->size] = 0; if (boty != topy) { - emsys_strlcat((char *)new->data, - (char *)&row->chars[botx - ed->rx], new->datasize); + emsys_strlcat( + (char *)new->data, + (char *)&row->chars[botx - ed->rx], + new->datasize); } } } else { @@ -770,7 +833,8 @@ void editorKillRectangle(struct editorConfig *ed, struct editorBuffer *buf) { row->size -= ed->rx; row->chars[row->size] = 0; if (boty != topy) { - emsys_strlcat((char *)new->data, (char *)&row->chars[topx], new->datasize); + emsys_strlcat((char *)new->data, + (char *)&row->chars[topx], new->datasize); } } idx++; @@ -798,7 +862,8 @@ void editorKillRectangle(struct editorConfig *ed, struct editorBuffer *buf) { row->chars[row->size] = 0; } - emsys_strlcat((char *)new->data, (char *)row->chars, new->datasize); + emsys_strlcat((char *)new->data, (char *)row->chars, + new->datasize); idx++; } @@ -951,7 +1016,8 @@ void editorYankRectangle(struct editorConfig *ed, struct editorBuffer *buf) { if (boty == topy) { emsys_strlcat((char *)new->data, string, new->datasize); } else { - emsys_strlcat((char *)new->data, (char *)&row->chars[topx], new->datasize); + emsys_strlcat((char *)new->data, (char *)&row->chars[topx], + new->datasize); } idx++; @@ -976,7 +1042,8 @@ void editorYankRectangle(struct editorConfig *ed, struct editorBuffer *buf) { memcpy(&row->chars[topx], string, ed->rx); row->size += ed->rx; row->chars[row->size] = 0; - emsys_strlcat((char *)new->data, (char *)row->chars, new->datasize); + emsys_strlcat((char *)new->data, (char *)row->chars, + new->datasize); idx++; } diff --git a/region.h b/region.h index e64f7a2..c76595f 100644 --- a/region.h +++ b/region.h @@ -21,6 +21,8 @@ void editorCopyRegion(struct editorConfig *ed, struct editorBuffer *buf); void editorYank(struct editorConfig *ed, struct editorBuffer *buf, int count); +void editorYankPop(struct editorConfig *ed, struct editorBuffer *buf); + void editorTransformRegion(struct editorConfig *ed, struct editorBuffer *buf, uint8_t *(*transformer)(uint8_t *)); diff --git a/register.c b/register.c index b8fcc55..4c13469 100644 --- a/register.c +++ b/register.c @@ -290,9 +290,8 @@ void editorViewRegister(struct editorConfig *ed, ed->registers[reg].rdata.region); break; case REGISTER_NUMBER: - editorSetStatusMessage( - "%s (number): %" PRId64, str, - ed->registers[reg].rdata.number); + editorSetStatusMessage("%s (number): %" PRId64, str, + ed->registers[reg].rdata.number); break; case REGISTER_POINT:; struct editorPoint *pt = ed->registers[reg].rdata.point; diff --git a/tab.c b/tab.c deleted file mode 100644 index 56d4150..0000000 --- a/tab.c +++ /dev/null @@ -1,494 +0,0 @@ -#include "util.h" -#include -#include -#include -#include -#include -#include "emsys.h" -#include -#include "buffer.h" -#include "tab.h" -#include "util.h" -#include "undo.h" -#include "unicode.h" -#include "display.h" -#include "terminal.h" - -uint8_t *tabCompleteBufferNames(struct editorConfig *ed, uint8_t *input, - struct editorBuffer *currentBuffer) { - char **completions = NULL; - int count = 0; - int capacity = 8; // Initial capacity - uint8_t *ret = input; - - // Allocate initial memory - completions = xmalloc(capacity * sizeof(char *)); - if (completions == NULL) { - // Handle allocation failure - return ret; - } - - // Collect matching buffer names - for (struct editorBuffer *b = ed->headbuf; b != NULL; b = b->next) { - if (b == currentBuffer) - continue; - - char *name = b->filename ? b->filename : "*scratch*"; - if (strncmp(name, (char *)input, strlen((char *)input)) == 0) { - if (count + 1 >= capacity) { - // Double capacity and reallocate - if (capacity > INT_MAX / 2 || - (size_t)capacity > - SIZE_MAX / (2 * sizeof(char *))) { - die("buffer size overflow"); - } - capacity *= 2; - completions = xrealloc( - completions, capacity * sizeof(char *)); - } - completions[count++] = xstrdup(name); - } - } - - if (count < 1) { - goto cleanup; - } - - if (count == 1) { - ret = (uint8_t *)xstrdup(completions[0]); - goto cleanup; - } - - // Multiple matches, allow cycling through them - int cur = 0; - for (;;) { - editorSetStatusMessage("Multiple options: %s", - completions[cur]); - refreshScreen(); - cursorBottomLine(strlen(completions[cur]) + 19); - - int c = editorReadKey(); - switch (c) { - case '\r': - ret = (uint8_t *)xstrdup(completions[cur]); - goto cleanup; - case CTRL('i'): - cur = (cur + 1) % count; - break; - case BACKTAB: - cur = (cur == 0) ? count - 1 : cur - 1; - break; - case CTRL('g'): - goto cleanup; - } - } - -cleanup: - for (int i = 0; i < count; i++) { - free(completions[i]); - } - free(completions); - - return ret; -} - -uint8_t *tabCompleteFiles(uint8_t *prompt) { - glob_t globlist; - uint8_t *ret = prompt; - uint8_t *allocated_prompt = NULL; - uint8_t *tilde_expanded = NULL; - - /* Manual tilde expansion - works on all systems */ - if (*prompt == '~') { - char *home_dir = getenv("HOME"); - if (!home_dir) { - // Handle error: HOME environment variable not found - return prompt; - } - - size_t home_len = strlen(home_dir); - size_t prompt_len = strlen((char *)prompt); - char *new_prompt = xmalloc( - home_len + prompt_len - 1 + - 1); // -1 for removed '~', +1 for null terminator - if (!new_prompt) { - // Handle memory allocation failure - return prompt; - } - - emsys_strlcpy(new_prompt, home_dir, home_len + prompt_len); - emsys_strlcpy(new_prompt + home_len, (char *)(prompt + 1), prompt_len); // Skip the '~' - tilde_expanded = (uint8_t *)new_prompt; - prompt = tilde_expanded; - } - - /* - * Define this to do manual globbing. It does mean you'll have - * to add the *s yourself. However, it will let you tab - * complete for more interesting scenarios, like - * *dir/other*dir/file.*.gz -> mydir/otherFOOdir/file.tar.gz - */ -#ifndef EMSYS_NO_SIMPLE_GLOB - int end = strlen((char *)prompt); - /* Need to allocate a new string with room for the '*' */ - char *glob_pattern = xmalloc(end + 2); - if (!glob_pattern) { - if (tilde_expanded) - free(tilde_expanded); - return ret; /* Return original prompt, not modified one */ - } - emsys_strlcpy(glob_pattern, (char *)prompt, end + 2); - glob_pattern[end] = '*'; - glob_pattern[end + 1] = 0; - allocated_prompt = (uint8_t *)glob_pattern; - prompt = allocated_prompt; -#endif - - if (glob((char *)prompt, GLOB_MARK, NULL, &globlist)) - goto TC_FILES_CLEANUP; - - size_t cur = 0; - - if (globlist.gl_pathc < 1) - goto TC_FILES_CLEANUP; - - if (globlist.gl_pathc == 1) - goto TC_FILES_ACCEPT; - - int curw = stringWidth((uint8_t *)globlist.gl_pathv[cur]); - - for (;;) { - editorSetStatusMessage("Multiple options: %s", - globlist.gl_pathv[cur]); - refreshScreen(); - cursorBottomLine(curw + 19); - - int c = editorReadKey(); - switch (c) { - case '\r':; -TC_FILES_ACCEPT:; - ret = xcalloc(strlen(globlist.gl_pathv[cur]) + 1, 1); - emsys_strlcpy((char *)ret, globlist.gl_pathv[cur], strlen(globlist.gl_pathv[cur]) + 1); - goto TC_FILES_CLEANUP; - break; - case CTRL('i'): - cur++; - if (cur >= globlist.gl_pathc) { - cur = 0; - } - curw = stringWidth((uint8_t *)globlist.gl_pathv[cur]); - break; - case BACKTAB: - if (cur == 0) { - cur = globlist.gl_pathc - 1; - } else { - cur--; - } - curw = stringWidth((uint8_t *)globlist.gl_pathv[cur]); - break; - case CTRL('g'): - goto TC_FILES_CLEANUP; - break; - } - } - -TC_FILES_CLEANUP: - globfree(&globlist); -#ifndef EMSYS_NO_SIMPLE_GLOB - if (allocated_prompt) { - free(allocated_prompt); - } -#endif - if (tilde_expanded) { - free(tilde_expanded); - } - return ret; -} - -uint8_t *tabCompleteCommands(struct editorConfig *ed, uint8_t *input) { - char **completions = NULL; - int count = 0; - int capacity = 8; // Initial capacity - uint8_t *ret = input; - - // Allocate initial memory - completions = xmalloc(capacity * sizeof(char *)); - if (completions == NULL) { - return ret; - } - - // Convert input to lowercase for case-insensitive matching - int input_len = strlen((char *)input); - char *lower_input = xmalloc(input_len + 1); - if (lower_input == NULL) { - free(completions); - return ret; - } - for (int i = 0; i <= input_len; i++) { - uint8_t c = input[i]; - if ('A' <= c && c <= 'Z') { - c |= 0x60; - } - lower_input[i] = c; - } - - // Collect matching command names - for (int i = 0; i < ed->cmd_count; i++) { - if (strncmp(ed->cmd[i].key, lower_input, input_len) == 0) { - if (count >= capacity) { - if (capacity > INT_MAX / 2 || - (size_t)capacity > - SIZE_MAX / (2 * sizeof(char *))) { - die("buffer size overflow"); - } - capacity *= 2; - completions = xrealloc( - completions, capacity * sizeof(char *)); - } - completions[count++] = xstrdup(ed->cmd[i].key); - } - } - - free(lower_input); - - if (count < 1) { - goto cleanup; - } - - if (count == 1) { - ret = (uint8_t *)xstrdup(completions[0]); - goto cleanup; - } - - // Multiple matches, allow cycling through them - int cur = 0; - for (;;) { - editorSetStatusMessage("cmd: %s", completions[cur]); - refreshScreen(); - - // Position cursor after the command text - int prompt_width = 5; // "cmd: " - cursorBottomLine(prompt_width + strlen(completions[cur])); - - int c = editorReadKey(); - switch (c) { - case '\r': - ret = (uint8_t *)xstrdup(completions[cur]); - goto cleanup; - case CTRL('i'): - cur = (cur + 1) % count; - break; - case BACKTAB: - cur = (cur == 0) ? count - 1 : cur - 1; - break; - case CTRL('g'): - goto cleanup; - } - } - -cleanup: - for (int i = 0; i < count; i++) { - free(completions[i]); - } - free(completions); - - return ret; -} - -static int alnum(uint8_t c) { - return c > 127 || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || - ('0' <= c && c <= '9') || c == '_'; -} - -static int sortstring(const void *str1, const void *str2) { - char *const *pp1 = str1; - char *const *pp2 = str2; - return strcmp(*pp1, *pp2); -} - -void editorCompleteWord(struct editorConfig *ed, struct editorBuffer *bufr) { - if (bufr->cy >= bufr->numrows || bufr->cx == 0) { - editorSetStatusMessage("Nothing to complete here."); - return; - } - struct erow *row = &bufr->row[bufr->cy]; - if (!row->chars) { - editorSetStatusMessage("Nothing to complete here."); - return; - } - int wordStart = bufr->cx; - for (int i = bufr->cx - 1; i >= 0; i--) { - if (!alnum(row->chars[i])) - break; - wordStart = i; - } - if (wordStart == bufr->cx) { - editorSetStatusMessage("Nothing to complete here."); - return; - } - - char rpattern[] = "[A-Za-z0-9\200-\377_]+"; - char *word = xcalloc(bufr->cx - wordStart + 1 + sizeof(rpattern), 1); - strncpy(word, (char *)&row->chars[wordStart], bufr->cx - wordStart); - emsys_strlcat(word, rpattern, bufr->cx - wordStart + 1 + sizeof(rpattern)); - int ncand = 0; - int scand = 32; - char **candidates = xmalloc(sizeof(uint8_t *) * scand); - - regex_t pattern; - regmatch_t matches[1]; - if (regcomp(&pattern, word, REG_EXTENDED) != 0) { - free(word); - free(candidates); - editorSetStatusMessage("Invalid regex pattern"); - return; - } - - /* This is a deeply naive algorithm. */ - /* First, find every word that starts with the word to complete */ - for (struct editorBuffer *buf = ed->headbuf; buf; buf = buf->next) { - for (int i = 0; i < buf->numrows; i++) { - if (buf == bufr && buf->cy == i) - continue; - struct erow *row = &buf->row[i]; - if (!row->chars) - continue; - if (regexec(&pattern, (char *)row->chars, 1, matches, - 0) == 0) { - int match_idx = matches[0].rm_so; - int match_length = - matches[0].rm_eo - matches[0].rm_so; - candidates[ncand] = xcalloc(match_length + 1, 1); - strncpy(candidates[ncand], - (char *)&row->chars[match_idx], - match_length); - ncand++; - if (ncand >= scand) { - scand <<= 1; - candidates = xrealloc(candidates, - sizeof(char *) * - scand); - } - } - } - } - /* Dunmatchin'. Restore word to non-regex contents. */ - word[bufr->cx - wordStart] = 0; - - /* No matches? Cleanup. */ - if (ncand == 0) { - editorSetStatusMessage("No match for %s", word); - goto COMPLETE_WORD_CLEANUP; - } - - int sel = 0; - /* Only one candidate? Take it. */ - if (ncand == 1) { - goto COMPLETE_WORD_DONE; - } - - /* Next, sort the list */ - qsort(candidates, ncand, sizeof(char *), sortstring); - - /* Finally, uniq' it. We now have our candidate list. */ - int newlen = 1; - char *prev = NULL; - for (int i = 0; i < ncand; i++) { - if (prev == NULL) { - prev = candidates[i]; - continue; - } - if (strcmp(prev, candidates[i])) { - /* Nonduplicate, copy it over. */ - prev = candidates[i]; - candidates[newlen++] = prev; - } else { - /* We don't need the memory for duplicates any more. */ - free(candidates[i]); - } - } - ncand = newlen; - - /* If after all that mess we only have one candidate, use it. */ - if (ncand == 1) { - goto COMPLETE_WORD_DONE; - } - - /* Otherwise, standard tab complete interface. */ - int selw = stringWidth((uint8_t *)candidates[sel]); - for (;;) { - editorSetStatusMessage("Multiple options: %s", candidates[sel]); - refreshScreen(); - cursorBottomLine(selw + 19); - - int c = editorReadKey(); - switch (c) { - case '\r': - goto COMPLETE_WORD_DONE; - break; - case EXPAND: - case CTRL('i'): - sel++; - if (sel >= ncand) { - sel = 0; - } - selw = stringWidth((uint8_t *)candidates[sel]); - break; - case BACKTAB: - if (sel == 0) { - sel = ncand - 1; - } else { - sel--; - } - selw = stringWidth((uint8_t *)candidates[sel]); - break; - case CTRL('g'): - editorSetStatusMessage("Canceled"); - goto COMPLETE_WORD_CLEANUP; - break; - } - } - - /* Finally, make the modification to the row, setup undos, and - * cleanup. */ -COMPLETE_WORD_DONE:; - /* Length of the rest of the completed word */ - int completelen = strlen(candidates[sel]) - (bufr->cx - wordStart); - struct editorUndo *new = newUndo(); - new->prev = bufr->undo; - new->startx = bufr->cx; - new->starty = bufr->cy; - new->endx = bufr->cx + completelen; - new->endy = bufr->cy; - new->datalen = completelen; - if (new->datasize < completelen + 1) { - new->data = xrealloc(new->data, new->datalen + 1); - new->datasize = new->datalen + 1; - } - new->append = 0; - new->delete = 0; - new->data[0] = 0; - emsys_strlcat((char *)new->data, &candidates[sel][bufr->cx - wordStart], new->datasize); - bufr->undo = new; - - row->chars = xrealloc(row->chars, row->size + 1 + completelen); - memcpy(&row->chars[bufr->cx + completelen], &row->chars[bufr->cx], - row->size - bufr->cx); - memcpy(&row->chars[bufr->cx], &candidates[sel][bufr->cx - wordStart], - completelen); - row->size += completelen; - row->chars[row->size] = 0; - row->render_valid = 0; - - editorSetStatusMessage("Expanded %.30s to %.30s", word, - candidates[sel]); - bufr->cx += completelen; - -COMPLETE_WORD_CLEANUP: - regfree(&pattern); - for (int i = 0; i < ncand; i++) { - free(candidates[i]); - } - free(candidates); - free(word); -} diff --git a/tab.h b/tab.h deleted file mode 100644 index 906c74d..0000000 --- a/tab.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef TAB_H -#define TAB_H 1 -#include - -uint8_t *tabCompleteBufferNames(struct editorConfig *ed, uint8_t *input, - struct editorBuffer *currentBuffer); -uint8_t *tabCompleteFiles(uint8_t *); -uint8_t *tabCompleteCommands(struct editorConfig *ed, uint8_t *input); -void editorCompleteWord(struct editorConfig *, struct editorBuffer *); -#endif diff --git a/terminal.c b/terminal.c index c571541..50476e4 100644 --- a/terminal.c +++ b/terminal.c @@ -6,7 +6,7 @@ #include #include #ifdef __sun -#include /* This might be needed first */ +#include /* This might be needed first */ #include #endif #include @@ -200,6 +200,10 @@ int editorReadKey(void) { return REGEX_SEARCH_FORWARD; } else if (seq[0] == CTRL('r')) { return REGEX_SEARCH_BACKWARD; + } else if (seq[0] == 'p') { + return HISTORY_PREV; + } else if (seq[0] == 'n') { + return HISTORY_NEXT; } else { switch ((seq[0] & 0x1f) | 0x40) { case 'B': @@ -230,6 +234,8 @@ int editorReadKey(void) { return COPY; case 'X': return EXEC_CMD; + case 'Y': + return YANK_POP; } } @@ -239,7 +245,8 @@ ESC_UNKNOWN:; char buf[8]; for (int i = 0; seq[i]; i++) { if (seq[i] < ' ') { - snprintf(buf, sizeof(buf), "C-%c ", seq[i] + '`'); + snprintf(buf, sizeof(buf), "C-%c ", + seq[i] + '`'); } else { snprintf(buf, sizeof(buf), "%c ", seq[i]); } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d334511..5a09e13 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -2,11 +2,17 @@ # Test suite for emsys set -e -echo "Building emsys..." -make clean > /dev/null 2>&1 -make > /dev/null 2>&1 - echo "Running tests..." + +# Test 0: Check for missing newlines (must be first, before any builds) +if ./tests/check-newlines.sh > /dev/null 2>&1; then + echo "✓ Source file newlines" +else + echo "✗ Missing newlines in source files" + ./tests/check-newlines.sh + exit 1 +fi + # Test 1: Version flag works ./emsys --version > /dev/null || exit 1 echo "✓ Version check" @@ -29,7 +35,13 @@ test -x ./emsys || exit 1 echo "✓ Binary executable" # Test 4: Compile and run core tests -cc -std=c99 -o test_core tests/test_core.c unicode.o wcwidth.o util.o || exit 1 +# Check if object files were built with sanitizers by looking for ASAN symbols +if nm unicode.o 2>/dev/null | grep -q "__asan_"; then + echo "✓ Detected sanitizer build, using sanitizer flags for test" + cc -std=c99 -fsanitize=address,undefined -o test_core tests/test_core.c unicode.o wcwidth.o util.o || exit 1 +else + cc -std=c99 -o test_core tests/test_core.c unicode.o wcwidth.o util.o || exit 1 +fi if ./test_core | grep -q "FAIL"; then echo "✗ Core tests failed" ./test_core @@ -39,14 +51,6 @@ else fi rm -f test_core -# Test 5: Check for missing newlines -if ./tests/check-newlines.sh > /dev/null 2>&1; then - echo "✓ Source file newlines" -else - echo "✗ Missing newlines in source files" - ./tests/check-newlines.sh - exit 1 -fi echo "" echo "All tests passed" \ No newline at end of file diff --git a/transform.c b/transform.c index b286917..6b67b4f 100644 --- a/transform.c +++ b/transform.c @@ -12,7 +12,7 @@ #include "region.h" #include "util.h" -#define MKOUTPUT(in, l, o) \ +#define MKOUTPUT(in, l, o) \ int l = strlen((char *)in); \ uint8_t *o = xmalloc(l + 1) diff --git a/util.c b/util.c index b4a2c43..a63c094 100644 --- a/util.c +++ b/util.c @@ -113,12 +113,12 @@ size_t emsys_strlcpy(char *dst, const char *src, size_t dsize) { /* Not enough room in dst, add NUL and traverse rest of src. */ if (nleft == 0) { if (dsize != 0) - *dst = '\0'; /* NUL-terminate dst */ + *dst = '\0'; /* NUL-terminate dst */ while (*src++) ; } - return(src - osrc - 1); /* count does not include NUL */ + return (src - osrc - 1); /* count does not include NUL */ } size_t emsys_strlcat(char *dst, const char *src, size_t dsize) { @@ -134,7 +134,7 @@ size_t emsys_strlcat(char *dst, const char *src, size_t dsize) { n = dsize - dlen; if (n-- == 0) - return(dlen + strlen(src)); + return (dlen + strlen(src)); while (*src != '\0') { if (n != 0) { @@ -145,5 +145,5 @@ size_t emsys_strlcat(char *dst, const char *src, size_t dsize) { } *dst = '\0'; - return(dlen + (src - osrc)); /* count does not include NUL */ + return (dlen + (src - osrc)); /* count does not include NUL */ }