diff --git a/ved/Makefile b/ved/Makefile new file mode 100644 index 0000000..85a1ade --- /dev/null +++ b/ved/Makefile @@ -0,0 +1,17 @@ +CC = gcc +CFLAGS = -Wall -Wextra -std=c99 -pedantic +LDFLAGS = -lncurses -lcurl -lpthread +SRC = $(wildcard src/*.c) +OBJ = $(SRC:.c=.o) +BIN = ved + +$(BIN): $(OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +src/%.o: src/%.c src/editor.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f src/*.o $(BIN) + +.PHONY: clean diff --git a/ved/WORKPLAN.md b/ved/WORKPLAN.md new file mode 100644 index 0000000..6816900 --- /dev/null +++ b/ved/WORKPLAN.md @@ -0,0 +1,80 @@ +# Vi-style Editor with Ollama Code Prediction + +## Overview +Terminal-based code editor written in C with nvi-style modal editing and local LLM-powered code completion via Ollama. For Linux CLI. Target users: personal + colleagues. + +## Editor Scope (nvi-style) + +### Modal Editing +- Normal, Insert, Command-line (`:`) modes + +### Navigation +- `h/j/k/l` — basic movement +- `w/b/e` — word movement +- `0/$` — line start/end +- `^f/^b` — page up/down +- `G/gg` — file start/end +- `f/t/F/T` — find char on line, with counts + +### Operators + Motions +- `d`, `y`, `c` combined with any motion (e.g. `c3td`, `d$`, `y2w`) +- Single unnamed buffer for yank/delete +- `p/P` — paste after/before + +### Other +- `.` — repeat last command +- `u` — undo +- `:w`, `:q`, `:wq`, `:e` — file commands +- `:/regex` — search +- `:%s/pat/rep/g` — substitution + +### Out of Scope +- Macros, named registers, splits, tabs + +## Language Support +- C +- Assembly + +## Ollama Integration + +### Models (user-configurable) +- gemma2:2b +- deepseek-r1:1.5b +- qwen2.5-coder:3b +- gemma3:4b + +### Completion Behavior +- Idle timer in insert mode (~300ms) triggers completion request +- Tab to accept ghost text (rendered dim) +- Context sent: current function/block (enclosing `{}`) + current line +- Endpoint: `POST http://localhost:11434/api/generate` (streaming JSON) + +## Architecture + +``` +src/ + main.c — entry point, arg parsing + terminal.c — raw mode, ncurses screen management + buffer.c — gap buffer or piece table, line indexing + editor.c — editor state, viewport, cursor + input.c — keypress reading, modal dispatch + normal.c — normal mode commands, operator-motion parsing + insert.c — insert mode, char input, trigger completion + command.c — : command line parsing and execution + search.c — regex search (POSIX regex.h) + undo.c — undo list + ollama.c — HTTP client (libcurl), prompt building, response parsing + syntax.c — minimal scope detection for C/asm (brace matching) + config.c — runtime config (~/.editorrc or similar) +``` + +## Dependencies +- ncurses — terminal UI +- libcurl — Ollama HTTP communication +- POSIX regex.h — search/replace (libc) +- cJSON (or hand-rolled) — parse Ollama JSON responses + +## Design Decisions to Finalize +- [ ] Text buffer: gap buffer (simpler) vs piece table (better for large files) +- [ ] Visual mode: include char/line visual mode? +- [ ] Config file format and location diff --git a/ved/desc.txt b/ved/desc.txt new file mode 100644 index 0000000..f7e4b84 --- /dev/null +++ b/ved/desc.txt @@ -0,0 +1 @@ +Make a hello world in C diff --git a/ved/src/buffer.c b/ved/src/buffer.c new file mode 100644 index 0000000..c0c911a --- /dev/null +++ b/ved/src/buffer.c @@ -0,0 +1,157 @@ +#define _GNU_SOURCE +#include "editor.h" +#include +#include +#include + +void buf_init(Buffer *b) +{ + b->lines = NULL; + b->numlines = 0; + b->cap = 0; + b->filename = NULL; + b->dirty = 0; +} + +void buf_free(Buffer *b) +{ + for (int i = 0; i < b->numlines; i++) + free(b->lines[i].chars); + free(b->lines); + free(b->filename); +} + +static void line_ensure(Line *l, int need) +{ + if (need <= l->cap) return; + l->cap = need * 2; + l->chars = realloc(l->chars, l->cap); +} + +static void buf_ensure_lines(Buffer *b, int need) +{ + if (need <= b->cap) return; + b->cap = need * 2; + b->lines = realloc(b->lines, b->cap * sizeof(Line)); +} + +int buf_load(Buffer *b, const char *filename) +{ + FILE *f = fopen(filename, "r"); + if (!f) { + /* new file: start with one empty line */ + buf_ensure_lines(b, 1); + b->lines[0] = (Line){NULL, 0, 0}; + b->numlines = 1; + b->filename = strdup(filename); + return 0; + } + b->filename = strdup(filename); + + char *line = NULL; + size_t linecap = 0; + ssize_t len; + while ((len = getline(&line, &linecap, f)) != -1) { + /* strip trailing newline */ + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) + len--; + buf_ensure_lines(b, b->numlines + 1); + Line *l = &b->lines[b->numlines]; + l->len = (int)len; + l->cap = (int)len + 1; + l->chars = malloc(l->cap); + memcpy(l->chars, line, len); + l->chars[len] = '\0'; + b->numlines++; + } + free(line); + fclose(f); + + if (b->numlines == 0) { + buf_ensure_lines(b, 1); + b->lines[0] = (Line){NULL, 0, 0}; + b->numlines = 1; + } + return 0; +} + +int buf_save(Buffer *b) +{ + if (!b->filename) return -1; + FILE *f = fopen(b->filename, "w"); + if (!f) return -1; + for (int i = 0; i < b->numlines; i++) { + if (b->lines[i].len > 0) + fwrite(b->lines[i].chars, 1, b->lines[i].len, f); + fputc('\n', f); + } + fclose(f); + b->dirty = 0; + return 0; +} + +int buf_line_len(Buffer *b, int row) +{ + if (row < 0 || row >= b->numlines) return 0; + return b->lines[row].len; +} + +void buf_insert_char(Buffer *b, int row, int col, int c) +{ + Line *l = &b->lines[row]; + line_ensure(l, l->len + 2); + memmove(l->chars + col + 1, l->chars + col, l->len - col); + l->chars[col] = (char)c; + l->len++; + l->chars[l->len] = '\0'; + b->dirty = 1; +} + +void buf_delete_char(Buffer *b, int row, int col) +{ + Line *l = &b->lines[row]; + if (col >= l->len) return; + memmove(l->chars + col, l->chars + col + 1, l->len - col - 1); + l->len--; + if (l->chars) l->chars[l->len] = '\0'; + b->dirty = 1; +} + +void buf_insert_line(Buffer *b, int at) +{ + buf_ensure_lines(b, b->numlines + 1); + memmove(b->lines + at + 1, b->lines + at, + (b->numlines - at) * sizeof(Line)); + b->lines[at] = (Line){NULL, 0, 0}; + b->numlines++; + b->dirty = 1; +} + +void buf_break_line(Buffer *b, int row, int col) +{ + Line *cur = &b->lines[row]; + int tail = cur->len - col; + + buf_insert_line(b, row + 1); + Line *newl = &b->lines[row + 1]; + if (tail > 0) { + line_ensure(newl, tail + 1); + memcpy(newl->chars, cur->chars + col, tail); + newl->len = tail; + newl->chars[tail] = '\0'; + } + /* cur pointer may be invalidated by insert_line, re-fetch */ + cur = &b->lines[row]; + cur->len = col; + if (cur->chars) cur->chars[col] = '\0'; + b->dirty = 1; +} + +void buf_delete_line(Buffer *b, int at) +{ + free(b->lines[at].chars); + memmove(b->lines + at, b->lines + at + 1, + (b->numlines - at - 1) * sizeof(Line)); + b->numlines--; + b->dirty = 1; +} diff --git a/ved/src/buffer.o b/ved/src/buffer.o new file mode 100644 index 0000000..2586bf7 Binary files /dev/null and b/ved/src/buffer.o differ diff --git a/ved/src/command.c b/ved/src/command.c new file mode 100644 index 0000000..cb7fbbc --- /dev/null +++ b/ved/src/command.c @@ -0,0 +1,61 @@ +#include "editor.h" +#include +#include +#include + +static char cmdbuf[256]; +static int cmdlen = 0; + +void command_process(Editor *e, int c) +{ + switch (c) { + case 27: /* ESC */ + cmdlen = 0; + e->mode = MODE_NORMAL; + editor_set_status(e, ""); + break; + case '\r': + case '\n': + case KEY_ENTER: + cmdbuf[cmdlen] = '\0'; + if (strcmp(cmdbuf, "q") == 0 || strcmp(cmdbuf, "q!") == 0) { + buf_free(&e->buf); + term_end(); + _exit(0); + } else if (strcmp(cmdbuf, "w") == 0) { + if (buf_save(&e->buf) == 0) + editor_set_status(e, "\"%s\" written", e->buf.filename); + else + editor_set_status(e, "Error writing file"); + } else if (strcmp(cmdbuf, "wq") == 0 || strcmp(cmdbuf, "wq!") == 0 || + strcmp(cmdbuf, "x") == 0) { + buf_save(&e->buf); + buf_free(&e->buf); + term_end(); + _exit(0); + } else { + editor_set_status(e, "Unknown command: %s", cmdbuf); + } + cmdlen = 0; + e->mode = MODE_NORMAL; + break; + case KEY_BACKSPACE: + case 127: + if (cmdlen > 0) { + cmdlen--; + cmdbuf[cmdlen] = '\0'; + editor_set_status(e, ":%s", cmdbuf); + } else { + e->mode = MODE_NORMAL; + editor_set_status(e, ""); + } + break; + default: + if (c >= 32 && c < 127 && cmdlen < (int)sizeof(cmdbuf) - 1) { + cmdbuf[cmdlen++] = (char)c; + cmdbuf[cmdlen] = '\0'; + editor_set_status(e, ":%s", cmdbuf); + } + break; + } +} diff --git a/ved/src/command.o b/ved/src/command.o new file mode 100644 index 0000000..3b4f9cc Binary files /dev/null and b/ved/src/command.o differ diff --git a/ved/src/editor.c b/ved/src/editor.c new file mode 100644 index 0000000..5713bc0 --- /dev/null +++ b/ved/src/editor.c @@ -0,0 +1,230 @@ +#include "editor.h" +#include +#include +#include +#include + +static char statusmsg[256]; + +static void load_desc(Editor *e) +{ + e->desc[0] = '\0'; + if (!e->buf.filename) return; + char tmp[512]; + strncpy(tmp, e->buf.filename, sizeof(tmp) - 1); + tmp[sizeof(tmp) - 1] = '\0'; + char *dir = dirname(tmp); + char path[512]; + snprintf(path, sizeof(path), "%s/desc.txt", dir); + FILE *f = fopen(path, "r"); + if (!f) return; + int pos = 0; + int c; + while ((c = fgetc(f)) != EOF && pos < (int)sizeof(e->desc) - 1) + e->desc[pos++] = (char)c; + e->desc[pos] = '\0'; + fclose(f); +} + +void editor_init(Editor *e) +{ + buf_init(&e->buf); + e->cx = e->cy = 0; + e->rowoff = e->coloff = 0; + e->mode = MODE_NORMAL; + e->ft = FT_NONE; + e->ghost[0] = '\0'; + e->idle_count = 0; + e->nano_mode = 0; + e->cutbuf = (Line){NULL, 0, 0}; + e->desc[0] = '\0'; + e->autocomplete_row = -1; + e->autocomplete_col = 0; + getmaxyx(stdscr, e->screenrows, e->screencols); + e->screenrows -= 2; /* reserve status + command line */ + statusmsg[0] = '\0'; +} + +void editor_open(Editor *e, const char *filename) +{ + buf_load(&e->buf, filename); + e->ft = syntax_detect(filename); + load_desc(e); +} + +void editor_scroll(Editor *e) +{ + if (e->cy < e->rowoff) + e->rowoff = e->cy; + if (e->cy >= e->rowoff + e->screenrows) + e->rowoff = e->cy - e->screenrows + 1; + if (e->cx < e->coloff) + e->coloff = e->cx; + if (e->cx >= e->coloff + e->screencols) + e->coloff = e->cx - e->screencols + 1; +} + +void editor_set_status(Editor *e, const char *fmt, ...) +{ + (void)e; + va_list ap; + va_start(ap, fmt); + vsnprintf(statusmsg, sizeof(statusmsg), fmt, ap); + va_end(ap); +} + +void editor_draw(Editor *e) +{ + editor_scroll(e); + erase(); + + if (e->nano_mode) { + /* nano layout: title bar row 0, content rows 1..screenrows, + status at screenrows+1, shortcut bars at screenrows+2..+3 */ + int content_start = 1; + for (int y = 0; y < e->screenrows; y++) { + int filerow = y + e->rowoff; + int sy = y + content_start; + if (filerow < e->buf.numlines) { + Line *l = &e->buf.lines[filerow]; + int len = l->len - e->coloff; + if (len < 0) len = 0; + if (len > e->screencols) len = e->screencols; + if (len > 0 && l->chars) { + if (e->ft != FT_NONE) { + int bc = syntax_line_in_block_comment( + &e->buf, filerow, e->ft); + syntax_draw_line(l->chars + e->coloff, len, + sy, 0, e->screencols, e->ft, bc); + } else { + mvaddnstr(sy, 0, l->chars + e->coloff, len); + } + } + } + } + + /* ghost text */ + if (e->ghost[0] != '\0') { + int gy = e->cy - e->rowoff + content_start; + int gx = e->cx - e->coloff; + if (gy > 0 && gy <= e->screenrows && gx >= 0) { + attron(A_DIM); + int glen = (int)strlen(e->ghost); + if (gx + glen > e->screencols) glen = e->screencols - gx; + if (glen > 0) mvaddnstr(gy, gx, e->ghost, glen); + attroff(A_DIM); + } + } + + /* status message line */ + if (statusmsg[0]) + mvaddnstr(e->screenrows + 1, 0, statusmsg, e->screencols); + + /* nano title bar + shortcut bars */ + nano_draw_bars(e); + + /* cursor */ + move(e->cy - e->rowoff + content_start, e->cx - e->coloff); + } else { + /* vi layout */ + for (int y = 0; y < e->screenrows; y++) { + int filerow = y + e->rowoff; + if (filerow < e->buf.numlines) { + Line *l = &e->buf.lines[filerow]; + int len = l->len - e->coloff; + if (len < 0) len = 0; + if (len > e->screencols) len = e->screencols; + if (len > 0 && l->chars) { + if (e->ft != FT_NONE) { + int bc = syntax_line_in_block_comment( + &e->buf, filerow, e->ft); + syntax_draw_line(l->chars + e->coloff, len, + y, 0, e->screencols, e->ft, bc); + } else { + mvaddnstr(y, 0, l->chars + e->coloff, len); + } + } + } else { + mvaddch(y, 0, '~'); + } + } + + /* ghost text */ + if (e->mode == MODE_INSERT && e->ghost[0] != '\0') { + int gy = e->cy - e->rowoff; + int gx = e->cx - e->coloff; + if (gy >= 0 && gy < e->screenrows && gx >= 0) { + attron(A_DIM); + int glen = (int)strlen(e->ghost); + if (gx + glen > e->screencols) glen = e->screencols - gx; + if (glen > 0) mvaddnstr(gy, gx, e->ghost, glen); + attroff(A_DIM); + } + } + + /* status bar */ + attron(A_REVERSE); + char status[256]; + const char *modestr = e->mode == MODE_INSERT ? "-- INSERT --" : + e->mode == MODE_COMMAND ? ":" : ""; + const char *fname = e->buf.filename ? e->buf.filename : "[No Name]"; + int slen = snprintf(status, sizeof(status), " %s %s%s", + modestr, fname, e->buf.dirty ? " [+]" : ""); + char rinfo[64]; + int rlen = snprintf(rinfo, sizeof(rinfo), "%d/%d ", + e->cy + 1, e->buf.numlines); + for (int i = slen; i < e->screencols - rlen; i++) + status[i] = ' '; + memcpy(status + e->screencols - rlen, rinfo, rlen); + status[e->screencols] = '\0'; + mvaddnstr(e->screenrows, 0, status, e->screencols); + attroff(A_REVERSE); + + /* message/command line */ + mvaddnstr(e->screenrows + 1, 0, statusmsg, e->screencols); + + /* cursor */ + move(e->cy - e->rowoff, e->cx - e->coloff); + } + + refresh(); +} + +void editor_prompt_desc(Editor *e) +{ + int row = e->nano_mode ? e->screenrows + 1 : e->screenrows + 1; + char buf[sizeof(e->desc)]; + int len = 0; + + /* pre-fill with current desc */ + if (e->desc[0]) { + len = (int)strlen(e->desc); + memcpy(buf, e->desc, len); + } + buf[len] = '\0'; + + editor_set_status(e, "Desc: %s", buf); + editor_draw(e); + move(row, 6 + len); + refresh(); + + for (;;) { + int c = getch(); + if (c == '\r' || c == '\n' || c == KEY_ENTER) break; + if (c == 27) { return; } /* ESC cancels */ + if ((c == KEY_BACKSPACE || c == 127) && len > 0) { + buf[--len] = '\0'; + } else if (c >= 32 && c < 127 && len < (int)sizeof(buf) - 1) { + buf[len++] = (char)c; + buf[len] = '\0'; + } + editor_set_status(e, "Desc: %s", buf); + editor_draw(e); + move(row, 6 + len); + refresh(); + } + + memcpy(e->desc, buf, len); + e->desc[len] = '\0'; + editor_set_status(e, len > 0 ? "[Description set]" : "[Description cleared]"); +} diff --git a/ved/src/editor.h b/ved/src/editor.h new file mode 100644 index 0000000..3dacd08 --- /dev/null +++ b/ved/src/editor.h @@ -0,0 +1,99 @@ +#ifndef EDITOR_H +#define EDITOR_H + +#include + +/* Modes */ +enum mode { MODE_NORMAL, MODE_INSERT, MODE_COMMAND, MODE_NANO }; + +/* File types for syntax highlighting */ +enum filetype { FT_NONE, FT_C, FT_ASM, FT_PYTHON, FT_BASH }; + +/* Line in the buffer */ +typedef struct { + char *chars; + int len; + int cap; +} Line; + +/* Text buffer */ +typedef struct { + Line *lines; + int numlines; + int cap; + char *filename; + int dirty; +} Buffer; + +/* Editor state */ +typedef struct { + Buffer buf; + int cx, cy; /* cursor position in buffer */ + int rowoff, coloff; /* viewport scroll offsets */ + int screenrows, screencols; + enum mode mode; + enum filetype ft; + char ghost[512]; /* ghost text suggestion from LLM */ + int idle_count; /* idle ticks in insert mode */ + int nano_mode; /* launched with -nano */ + Line cutbuf; /* nano ^K cut buffer */ + char desc[1024]; /* project description for AI context */ + int autocomplete_row; /* row to auto-insert completion into, -1 if none */ + int autocomplete_col; /* column where completion starts */ +} Editor; + +/* buffer.c */ +void buf_init(Buffer *b); +void buf_free(Buffer *b); +int buf_load(Buffer *b, const char *filename); +int buf_save(Buffer *b); +void buf_insert_char(Buffer *b, int row, int col, int c); +void buf_delete_char(Buffer *b, int row, int col); +void buf_insert_line(Buffer *b, int at); +void buf_break_line(Buffer *b, int row, int col); +void buf_delete_line(Buffer *b, int at); +int buf_line_len(Buffer *b, int row); + +/* terminal.c */ +void term_init(void); +void term_end(void); + +/* editor.c */ +void editor_init(Editor *e); +void editor_open(Editor *e, const char *filename); +void editor_draw(Editor *e); +void editor_scroll(Editor *e); +void editor_set_status(Editor *e, const char *fmt, ...); + +/* input.c */ +void input_process(Editor *e); + +/* normal.c */ +void normal_process(Editor *e, int c); + +/* insert.c */ +void insert_process(Editor *e, int c); + +/* command.c */ +void command_process(Editor *e, int c); + +/* syntax.c */ +void syntax_init(void); +enum filetype syntax_detect(const char *filename); +void syntax_draw_line(const char *s, int len, int y, int x, + int maxcols, enum filetype ft, int in_block_comment); +int syntax_line_in_block_comment(Buffer *b, int row, enum filetype ft); + +/* ollama.c */ +int ollama_request(Editor *e); +int ollama_request_line(Editor *e, int row); +int ollama_poll(Editor *e); + +/* nano.c */ +void nano_process(Editor *e, int c); +void nano_draw_bars(Editor *e); + +/* editor.c */ +void editor_prompt_desc(Editor *e); + +#endif diff --git a/ved/src/editor.o b/ved/src/editor.o new file mode 100644 index 0000000..8d07152 Binary files /dev/null and b/ved/src/editor.o differ diff --git a/ved/src/input.c b/ved/src/input.c new file mode 100644 index 0000000..3e22508 --- /dev/null +++ b/ved/src/input.c @@ -0,0 +1,25 @@ +#include "editor.h" + +void input_process(Editor *e) +{ + timeout(100); + + int c = getch(); + + if (c == ERR) { + /* poll for async ollama result */ + if (e->mode == MODE_INSERT || e->mode == MODE_NANO) { + int r = ollama_poll(e); + if (r == 1) + editor_set_status(e, "[Tab to accept, any key to dismiss]"); + } + return; + } + + switch (e->mode) { + case MODE_NORMAL: normal_process(e, c); break; + case MODE_INSERT: insert_process(e, c); break; + case MODE_COMMAND: command_process(e, c); break; + case MODE_NANO: nano_process(e, c); break; + } +} diff --git a/ved/src/input.o b/ved/src/input.o new file mode 100644 index 0000000..fe22673 Binary files /dev/null and b/ved/src/input.o differ diff --git a/ved/src/insert.c b/ved/src/insert.c new file mode 100644 index 0000000..5476546 --- /dev/null +++ b/ved/src/insert.c @@ -0,0 +1,134 @@ +#include "editor.h" +#include + +#define TAB_WIDTH 4 + +static int get_indent(Buffer *b, int row) +{ + if (row < 0 || row >= b->numlines) return 0; + char *s = b->lines[row].chars; + int len = b->lines[row].len; + int indent = 0; + while (indent < len && (s[indent] == ' ' || s[indent] == '\t')) + indent++; + return indent; +} + +static int line_ends_with(Buffer *b, int row, char c) +{ + int len = b->lines[row].len; + char *s = b->lines[row].chars; + if (!s || len == 0) return 0; + int i = len - 1; + while (i >= 0 && (s[i] == ' ' || s[i] == '\t')) i--; + return (i >= 0 && s[i] == c); +} + +void insert_process(Editor *e, int c) +{ + /* if there's a ghost suggestion and user presses Tab, accept it */ + if ((c == '\t' || c == KEY_STAB) && e->ghost[0] != '\0') { + for (int i = 0; e->ghost[i]; i++) + buf_insert_char(&e->buf, e->cy, e->cx++, e->ghost[i]); + e->ghost[0] = '\0'; + return; + } + + /* any other key clears ghost text */ + e->ghost[0] = '\0'; + + switch (c) { + case 27: /* ESC */ + e->mode = MODE_NORMAL; + if (e->cx > 0) e->cx--; + editor_set_status(e, ""); + break; + case 20: /* ^T — set description */ + editor_prompt_desc(e); + break; + case 16: /* ^P — request AI prediction */ + editor_set_status(e, "[predicting...]"); + ollama_request(e); + break; + case KEY_BACKSPACE: + case 127: + if (e->cx > 0) { + buf_delete_char(&e->buf, e->cy, e->cx - 1); + e->cx--; + } else if (e->cy > 0) { + e->cx = buf_line_len(&e->buf, e->cy - 1); + Line *prev = &e->buf.lines[e->cy - 1]; + Line *cur = &e->buf.lines[e->cy]; + if (cur->len > 0) { + for (int i = 0; i < cur->len; i++) + buf_insert_char(&e->buf, e->cy - 1, + prev->len, cur->chars[i]); + } + buf_delete_line(&e->buf, e->cy); + e->cy--; + } + break; + case '\t': + case KEY_STAB: + /* insert TAB_WIDTH spaces */ + for (int i = 0; i < TAB_WIDTH; i++) + buf_insert_char(&e->buf, e->cy, e->cx++, ' '); + break; + case KEY_BTAB: + /* shift-tab: remove up to TAB_WIDTH leading spaces */ + for (int i = 0; i < TAB_WIDTH && e->cx > 0; i++) { + if (e->buf.lines[e->cy].chars[e->cx - 1] == ' ') { + buf_delete_char(&e->buf, e->cy, e->cx - 1); + e->cx--; + } else break; + } + break; + case '\r': + case '\n': + case KEY_ENTER: { + int prev_row = e->cy; + int prev_len = buf_line_len(&e->buf, e->cy); + int indent = get_indent(&e->buf, e->cy); + if (indent > e->cx) indent = e->cx; + int extra = 0; + if (e->ft == FT_C && line_ends_with(&e->buf, e->cy, '{')) + extra = TAB_WIDTH; + if ((e->ft == FT_PYTHON || e->ft == FT_BASH) && + line_ends_with(&e->buf, e->cy, ':')) + extra = TAB_WIDTH; + + buf_break_line(&e->buf, e->cy, e->cx); + e->cy++; + e->cx = 0; + for (int i = 0; i < indent + extra; i++) + buf_insert_char(&e->buf, e->cy, e->cx++, ' '); + + /* fire autocomplete for previous line if it looks partial */ + int content_len = prev_len - indent; + if (content_len > 0 && content_len < 60 && e->ft != FT_NONE) + ollama_request_line(e, prev_row); + break; + } + case '}': + if (e->ft == FT_C) { + char *s = e->buf.lines[e->cy].chars; + int all_space = 1; + for (int i = 0; i < e->cx && s; i++) + if (s[i] != ' ' && s[i] != '\t') { all_space = 0; break; } + if (all_space && e->cx >= TAB_WIDTH) { + for (int i = 0; i < TAB_WIDTH; i++) + buf_delete_char(&e->buf, e->cy, e->cx - TAB_WIDTH); + e->cx -= TAB_WIDTH; + } + } + buf_insert_char(&e->buf, e->cy, e->cx, c); + e->cx++; + break; + default: + if (c >= 32 && c < 127) { + buf_insert_char(&e->buf, e->cy, e->cx, c); + e->cx++; + } + break; + } +} diff --git a/ved/src/insert.o b/ved/src/insert.o new file mode 100644 index 0000000..6b57d50 Binary files /dev/null and b/ved/src/insert.o differ diff --git a/ved/src/main.c b/ved/src/main.c new file mode 100644 index 0000000..d8e6bde --- /dev/null +++ b/ved/src/main.c @@ -0,0 +1,73 @@ +#include "editor.h" +#include +#include +#include + +static void usage(void) +{ + printf("ved - vi editor with AI prediction\n\n" + "Usage: ved [options] [file]\n\n" + "Options:\n" + " -nano Launch in nano-style mode (no vi keybindings)\n" + " --help Show this help message\n\n" + "Vi mode keys:\n" + " hjkl Navigation\n" + " i/a/o/A Enter insert mode\n" + " dd Delete line\n" + " ZZ Save and quit\n" + " :w :q :wq Command mode\n" + " ^T Set AI project description\n" + " ^P Request AI prediction\n" + " Tab Accept AI suggestion (insert mode)\n\n" + "Nano mode keys:\n" + " ^X Exit ^O Save ^K Cut line\n" + " ^U Paste ^W Search ^T Set AI description\n" + " ^P Request AI prediction\n" + " Tab Accept AI suggestion\n\n" + "AI prediction:\n" + " Connects to Ollama (qwen2.5-coder:3b).\n" + " Host configured via ollama_host= in ~/.ahvibe.conf\n" + " Place desc.txt in the file's directory for project context.\n\n" + "Supported syntax: C (.c/.h), Assembly (.s/.S/.asm), Python (.py), Bash (.sh)\n"); +} + +int main(int argc, char *argv[]) +{ + int nano = 0; + const char *filename = NULL; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + usage(); + return 0; + } else if (strcmp(argv[i], "-nano") == 0) { + nano = 1; + } else { + filename = argv[i]; + } + } + + term_init(); + + Editor e; + editor_init(&e); + + if (nano) { + e.nano_mode = 1; + e.mode = MODE_NANO; + e.screenrows -= 2; + } + + if (filename) + editor_open(&e, filename); + else + editor_open(&e, "[No Name]"); + + for (;;) { + editor_draw(&e); + input_process(&e); + } + + term_end(); + return 0; +} diff --git a/ved/src/main.o b/ved/src/main.o new file mode 100644 index 0000000..68bf12f Binary files /dev/null and b/ved/src/main.o differ diff --git a/ved/src/nano.c b/ved/src/nano.c new file mode 100644 index 0000000..accba59 --- /dev/null +++ b/ved/src/nano.c @@ -0,0 +1,246 @@ +#include "editor.h" +#include +#include +#include + +void nano_process(Editor *e, int c) +{ + /* Tab accepts ghost text */ + if ((c == '\t' || c == KEY_STAB) && e->ghost[0] != '\0') { + for (int i = 0; e->ghost[i]; i++) + buf_insert_char(&e->buf, e->cy, e->cx++, e->ghost[i]); + e->ghost[0] = '\0'; + return; + } + + /* any other key clears ghost text */ + e->ghost[0] = '\0'; + + switch (c) { + /* ^T set description */ + case 20: + editor_prompt_desc(e); + break; + /* ^P request AI prediction */ + case 16: + editor_set_status(e, "[predicting...]"); + ollama_request(e); + break; + + /* ^X exit */ + case 24: + if (e->buf.dirty) { + editor_set_status(e, "Save modified buffer? (Y/N)"); + editor_draw(e); + timeout(-1); + int ans = getch(); + if (ans == 'y' || ans == 'Y') { + buf_save(&e->buf); + } else if (ans != 'n' && ans != 'N') { + editor_set_status(e, "Cancelled"); + return; + } + } + buf_free(&e->buf); + term_end(); + _exit(0); + break; + + /* ^O write out */ + case 15: + if (buf_save(&e->buf) == 0) + editor_set_status(e, "[ Wrote %d lines ]", e->buf.numlines); + else + editor_set_status(e, "[ Error writing ]"); + break; + + /* ^K cut line */ + case 11: + free(e->cutbuf.chars); + e->cutbuf.chars = NULL; + e->cutbuf.len = 0; + if (e->cy < e->buf.numlines) { + Line *l = &e->buf.lines[e->cy]; + if (l->len > 0 && l->chars) { + e->cutbuf.chars = malloc(l->len + 1); + memcpy(e->cutbuf.chars, l->chars, l->len); + e->cutbuf.len = l->len; + e->cutbuf.chars[l->len] = '\0'; + } + if (e->buf.numlines > 1) { + buf_delete_line(&e->buf, e->cy); + if (e->cy >= e->buf.numlines) + e->cy = e->buf.numlines - 1; + } else { + l->len = 0; + if (l->chars) l->chars[0] = '\0'; + e->buf.dirty = 1; + } + e->cx = 0; + } + break; + + /* ^U paste */ + case 21: + if (e->cutbuf.chars && e->cutbuf.len > 0) { + buf_insert_line(&e->buf, e->cy); + Line *l = &e->buf.lines[e->cy]; + l->chars = malloc(e->cutbuf.len + 1); + memcpy(l->chars, e->cutbuf.chars, e->cutbuf.len); + l->len = e->cutbuf.len; + l->cap = e->cutbuf.len + 1; + l->chars[l->len] = '\0'; + e->buf.dirty = 1; + e->cx = 0; + } + break; + + /* ^W search (simple forward) */ + case 23: { + static char search[128]; + editor_set_status(e, "Search: "); + editor_draw(e); + int si = 0; + for (;;) { + int sc = getch(); + if (sc == '\r' || sc == '\n' || sc == KEY_ENTER) break; + if (sc == 27) { si = 0; break; } + if ((sc == KEY_BACKSPACE || sc == 127) && si > 0) si--; + else if (sc >= 32 && sc < 127 && si < (int)sizeof(search) - 1) + search[si++] = (char)sc; + search[si] = '\0'; + editor_set_status(e, "Search: %s", search); + editor_draw(e); + } + if (si > 0) { + for (int r = e->cy; r < e->buf.numlines; r++) { + char *s = e->buf.lines[r].chars; + if (!s) continue; + int start = (r == e->cy) ? e->cx + 1 : 0; + char *found = strstr(s + start, search); + if (found) { + e->cy = r; + e->cx = (int)(found - s); + editor_set_status(e, ""); + return; + } + } + editor_set_status(e, "[ Not found ]"); + } else { + editor_set_status(e, ""); + } + break; + } + + /* arrow keys */ + case KEY_UP: e->cy--; break; + case KEY_DOWN: e->cy++; break; + case KEY_LEFT: e->cx--; break; + case KEY_RIGHT: e->cx++; break; + case KEY_HOME: e->cx = 0; break; + case KEY_END: e->cx = buf_line_len(&e->buf, e->cy); break; + case KEY_PPAGE: e->cy -= e->screenrows; break; + case KEY_NPAGE: e->cy += e->screenrows; break; + + /* backspace */ + case KEY_BACKSPACE: + case 127: + if (e->cx > 0) { + buf_delete_char(&e->buf, e->cy, e->cx - 1); + e->cx--; + } else if (e->cy > 0) { + e->cx = buf_line_len(&e->buf, e->cy - 1); + Line *prev = &e->buf.lines[e->cy - 1]; + Line *cur = &e->buf.lines[e->cy]; + if (cur->len > 0) + for (int i = 0; i < cur->len; i++) + buf_insert_char(&e->buf, e->cy - 1, prev->len, cur->chars[i]); + buf_delete_line(&e->buf, e->cy); + e->cy--; + } + break; + + /* delete key */ + case KEY_DC: + if (e->cx < buf_line_len(&e->buf, e->cy)) + buf_delete_char(&e->buf, e->cy, e->cx); + else if (e->cy < e->buf.numlines - 1) { + /* join next line */ + Line *cur = &e->buf.lines[e->cy]; + Line *next = &e->buf.lines[e->cy + 1]; + if (next->len > 0) + for (int i = 0; i < next->len; i++) + buf_insert_char(&e->buf, e->cy, cur->len, next->chars[i]); + buf_delete_line(&e->buf, e->cy + 1); + } + break; + + /* enter */ + case '\r': + case '\n': + case KEY_ENTER: + buf_break_line(&e->buf, e->cy, e->cx); + e->cy++; + e->cx = 0; + break; + + /* tab */ + case '\t': + for (int i = 0; i < 4; i++) + buf_insert_char(&e->buf, e->cy, e->cx++, ' '); + break; + + /* printable chars */ + default: + if (c >= 32 && c < 127) { + buf_insert_char(&e->buf, e->cy, e->cx, c); + e->cx++; + } + break; + } + + /* clamp cursor */ + if (e->cy < 0) e->cy = 0; + if (e->cy >= e->buf.numlines) e->cy = e->buf.numlines - 1; + int len = buf_line_len(&e->buf, e->cy); + if (e->cx > len) e->cx = len; + if (e->cx < 0) e->cx = 0; +} + +void nano_draw_bars(Editor *e) +{ + /* title bar */ + attron(A_REVERSE); + char title[256]; + const char *fname = e->buf.filename ? e->buf.filename : "New Buffer"; + int tlen = snprintf(title, sizeof(title), " VED 1.0%*s%s%s", + (int)(e->screencols/2 - 10), "", fname, + e->buf.dirty ? " (modified)" : ""); + for (int i = tlen; i < e->screencols; i++) title[i] = ' '; + title[e->screencols] = '\0'; + mvaddnstr(0, 0, title, e->screencols); + attroff(A_REVERSE); + + /* shortcut bar — 2 rows at bottom */ + int r1 = e->screenrows + 2; + int r2 = e->screenrows + 3; + int col = e->screencols / 6; + + const char *keys1[] = {"^X", "^O", "^K", "^U", "^W", "^T"}; + const char *labs1[] = {"Exit", "Write", "Cut", "Paste", "Search", "Desc"}; + const char *keys2[] = {"^P", "^J", "^R", "^\\", "^_", "^G"}; + const char *labs2[] = {"Predict", "Justify", "Read", "Replace", "Go To", "Help"}; + + for (int i = 0; i < 6; i++) { + int x = i * col; + attron(A_REVERSE); + mvaddstr(r1, x, keys1[i]); + attroff(A_REVERSE); + mvaddnstr(r1, x + 2, labs1[i], col - 2); + + attron(A_REVERSE); + mvaddstr(r2, x, keys2[i]); + attroff(A_REVERSE); + mvaddnstr(r2, x + 2, labs2[i], col - 2); + } +} diff --git a/ved/src/nano.o b/ved/src/nano.o new file mode 100644 index 0000000..aa75fa6 Binary files /dev/null and b/ved/src/nano.o differ diff --git a/ved/src/normal.c b/ved/src/normal.c new file mode 100644 index 0000000..0c7aae3 --- /dev/null +++ b/ved/src/normal.c @@ -0,0 +1,238 @@ +#include "editor.h" +#include +#include +#include + +static void clamp_cursor(Editor *e) +{ + if (e->cy >= e->buf.numlines) + e->cy = e->buf.numlines - 1; + if (e->cy < 0) e->cy = 0; + int len = buf_line_len(&e->buf, e->cy); + /* in normal mode cursor can't go past last char */ + if (e->mode == MODE_NORMAL && len > 0) len--; + if (e->cx > len) e->cx = len; + if (e->cx < 0) e->cx = 0; +} + +void normal_process(Editor *e, int c) +{ + static int pending_op = 0; + static int count = 0; + + /* accumulate count prefix */ + if (c >= '1' && c <= '9' && !pending_op) { + count = count * 10 + (c - '0'); + return; + } + if (c == '0' && count > 0) { + count = count * 10; + return; + } + int n = count > 0 ? count : 1; + count = 0; + + (void)pending_op; + pending_op = 0; + + switch (c) { + /* navigation */ + case 'h': e->cx -= n; break; + case 'l': e->cx += n; break; + case 'j': e->cy += n; break; + case 'k': e->cy -= n; break; + case '0': e->cx = 0; break; + case '$': e->cx = buf_line_len(&e->buf, e->cy); break; + case 'G': e->cy = e->buf.numlines - 1; break; + case 'g': /* gg handled simply: wait for next g */ + /* for now treat single g as gg */ + e->cy = 0; e->cx = 0; break; + case 6: /* ^F */ + e->cy += e->screenrows; break; + case 2: /* ^B */ + e->cy -= e->screenrows; break; + + /* word forward */ + case 'w': { + for (int i = 0; i < n; i++) { + int len = buf_line_len(&e->buf, e->cy); + char *s = e->buf.lines[e->cy].chars; + if (e->cx < len && s) { + /* skip current word chars */ + while (e->cx < len && s[e->cx] != ' ') e->cx++; + /* skip spaces */ + while (e->cx < len && s[e->cx] == ' ') e->cx++; + } + if (e->cx >= len && e->cy < e->buf.numlines - 1) { + e->cy++; e->cx = 0; + } + } + break; + } + case 'b': { + for (int i = 0; i < n; i++) { + char *s = e->buf.lines[e->cy].chars; + if (e->cx == 0 && e->cy > 0) { + e->cy--; + e->cx = buf_line_len(&e->buf, e->cy); + s = e->buf.lines[e->cy].chars; + } + if (s) { + if (e->cx > 0) e->cx--; + while (e->cx > 0 && s[e->cx] == ' ') e->cx--; + while (e->cx > 0 && s[e->cx-1] != ' ') e->cx--; + } + } + break; + } + + /* enter insert mode */ + case 'i': + e->mode = MODE_INSERT; + editor_set_status(e, "-- INSERT --"); + break; + case 'a': + e->cx++; + e->mode = MODE_INSERT; + editor_set_status(e, "-- INSERT --"); + break; + case 'o': + buf_insert_line(&e->buf, e->cy + 1); + e->cy++; + e->cx = 0; + e->mode = MODE_INSERT; + editor_set_status(e, "-- INSERT --"); + break; + case 'O': + buf_insert_line(&e->buf, e->cy); + e->cx = 0; + e->mode = MODE_INSERT; + editor_set_status(e, "-- INSERT --"); + break; + case 'A': + e->cx = buf_line_len(&e->buf, e->cy); + e->mode = MODE_INSERT; + editor_set_status(e, "-- INSERT --"); + break; + + /* delete char under cursor */ + case 'x': + for (int i = 0; i < n; i++) { + if (buf_line_len(&e->buf, e->cy) > 0) + buf_delete_char(&e->buf, e->cy, e->cx); + } + break; + + /* delete with motion */ + case 'd': { + timeout(-1); + /* read count prefix for motion */ + int mc = 0; + int c2 = getch(); + while (c2 >= '0' && c2 <= '9') { + mc = mc * 10 + (c2 - '0'); + c2 = getch(); + } + if (mc == 0) mc = 1; + + int from = e->cy, to = e->cy; + + if (c2 == 'd') { + /* dd: delete n lines */ + to = from + n - 1; + } else if (c2 == 'G') { + /* dG or d3G: delete from here to line mc (or end) */ + if (mc > 0 && !(c2 == 'G' && mc == 1 && e->cy == 0)) + to = mc - 1; /* d3G = delete to line 3 */ + else + to = e->buf.numlines - 1; + } else if (c2 == 'g') { + int c3 = getch(); + if (c3 == 'g') { + /* dgg: delete from here to top */ + to = 0; + } else break; + } else if (c2 == 'j') { + to = from + mc; + } else if (c2 == 'k') { + to = from; from = from - mc; + } else if (c2 == '$' || c2 == '0' || c2 == 'w' || c2 == 'b') { + /* single-line delete: just delete chars, not whole lines */ + /* for now treat as delete to end/start of line */ + if (c2 == '$') { + Line *l = &e->buf.lines[e->cy]; + l->len = e->cx; + if (l->chars) l->chars[e->cx] = '\0'; + e->buf.dirty = 1; + } else if (c2 == '0') { + Line *l = &e->buf.lines[e->cy]; + if (e->cx > 0 && l->chars) { + memmove(l->chars, l->chars + e->cx, l->len - e->cx); + l->len -= e->cx; + l->chars[l->len] = '\0'; + e->cx = 0; + e->buf.dirty = 1; + } + } else if (c2 == 'w') { + /* delete word forward */ + Line *l = &e->buf.lines[e->cy]; + int end = e->cx; + for (int i = 0; i < mc && end < l->len; i++) { + while (end < l->len && l->chars[end] != ' ') end++; + while (end < l->len && l->chars[end] == ' ') end++; + } + if (end > e->cx && l->chars) { + memmove(l->chars + e->cx, l->chars + end, l->len - end); + l->len -= (end - e->cx); + l->chars[l->len] = '\0'; + e->buf.dirty = 1; + } + } + break; /* don't fall through to line deletion */ + } else { + break; /* unknown motion */ + } + + /* normalize range */ + if (from > to) { int tmp = from; from = to; to = tmp; } + if (from < 0) from = 0; + if (to >= e->buf.numlines) to = e->buf.numlines - 1; + + /* delete line range */ + for (int i = to; i >= from; i--) { + if (e->buf.numlines > 1) { + buf_delete_line(&e->buf, i); + } else { + Line *l = &e->buf.lines[0]; + l->len = 0; + if (l->chars) l->chars[0] = '\0'; + e->buf.dirty = 1; + } + } + e->cy = from; + e->cx = 0; + break; + } + + /* command mode */ + case ':': + e->mode = MODE_COMMAND; + editor_set_status(e, ":"); + break; + + /* ZZ — save and quit */ + case 'Z': { + timeout(-1); /* block for next key */ + int c2 = getch(); + if (c2 == 'Z') { + buf_save(&e->buf); + buf_free(&e->buf); + term_end(); + _exit(0); + } + break; + } + } + + clamp_cursor(e); +} diff --git a/ved/src/normal.o b/ved/src/normal.o new file mode 100644 index 0000000..3d58542 Binary files /dev/null and b/ved/src/normal.o differ diff --git a/ved/src/ollama.c b/ved/src/ollama.c new file mode 100644 index 0000000..c9cd32f --- /dev/null +++ b/ved/src/ollama.c @@ -0,0 +1,404 @@ +#define _GNU_SOURCE +#include "editor.h" +#include +#include +#include +#include +#include + +#define OLLAMA_DEFAULT_HOST "http://localhost:11434" +#define OLLAMA_MODEL "qwen2.5-coder:3b" +#define CONTEXT_LINES 20 +#define LOGFILE "/tmp/ved_ollama.log" + +static char ollama_url[512]; +static FILE *logfp; + +/* async state */ +static pthread_t worker_tid; +static volatile int worker_running; +static char worker_result[512]; +static volatile int worker_done; + +static void dbglog(const char *fmt, ...) +{ + if (!logfp) logfp = fopen(LOGFILE, "a"); + if (!logfp) return; + va_list ap; + va_start(ap, fmt); + vfprintf(logfp, fmt, ap); + va_end(ap); + fflush(logfp); +} + +static void load_config(void) +{ + char path[256]; + snprintf(path, sizeof(path), "%s/.ahvibe.conf", getenv("HOME")); + FILE *f = fopen(path, "r"); + char host[256] = ""; + if (f) { + char line[512]; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "ollama_host=", 12) == 0) { + char *v = line + 12; + int len = (int)strlen(v); + while (len > 0 && (v[len-1] == '\n' || v[len-1] == '\r')) + len--; + if (len > 0) { memcpy(host, v, len); host[len] = '\0'; } + } + } + fclose(f); + } + if (host[0]) + snprintf(ollama_url, sizeof(ollama_url), "%s/api/generate", host); + else + snprintf(ollama_url, sizeof(ollama_url), "%s/api/generate", OLLAMA_DEFAULT_HOST); + dbglog("ollama url: %s\n", ollama_url); +} + +typedef struct { char *data; size_t len; } RespBuf; + +static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *ud) +{ + RespBuf *rb = ud; + size_t total = size * nmemb; + char *tmp = realloc(rb->data, rb->len + total + 1); + if (!tmp) return 0; + rb->data = tmp; + memcpy(rb->data + rb->len, ptr, total); + rb->len += total; + rb->data[rb->len] = '\0'; + return total; +} + +static int hex2int(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static int extract_response(const char *json, char *out, int outsize) +{ + int pos = 0; + const char *p = json; + while ((p = strstr(p, "\"response\":\"")) != NULL) { + p += 12; + while (*p && *p != '"' && pos < outsize - 1) { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case 'n': out[pos++] = '\n'; break; + case 't': out[pos++] = '\t'; break; + case '\\': out[pos++] = '\\'; break; + case '"': out[pos++] = '"'; break; + case '/': out[pos++] = '/'; break; + case 'u': + if (p[1] && p[2] && p[3] && p[4] && + hex2int(p[1]) >= 0 && hex2int(p[2]) >= 0 && + hex2int(p[3]) >= 0 && hex2int(p[4]) >= 0) { + int cp = (hex2int(p[1]) << 12) | (hex2int(p[2]) << 8) | + (hex2int(p[3]) << 4) | hex2int(p[4]); + p += 4; + if (cp < 0x80) { + out[pos++] = (char)cp; + } else if (cp < 0x800 && pos + 1 < outsize) { + out[pos++] = (char)(0xC0 | (cp >> 6)); + out[pos++] = (char)(0x80 | (cp & 0x3F)); + } else if (pos + 2 < outsize) { + out[pos++] = (char)(0xE0 | (cp >> 12)); + out[pos++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[pos++] = (char)(0x80 | (cp & 0x3F)); + } + } else { + out[pos++] = 'u'; + } + break; + default: out[pos++] = *p; break; + } + } else { + out[pos++] = *p; + } + p++; + } + if (*p == '"') p++; + } + out[pos] = '\0'; + return pos; +} + +static char *json_escape(const char *s) +{ + size_t len = strlen(s); + char *out = malloc(len * 6 + 1); + if (!out) return NULL; + int j = 0; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': out[j++] = '\\'; out[j++] = '"'; break; + case '\\': out[j++] = '\\'; out[j++] = '\\'; break; + case '\n': out[j++] = '\\'; out[j++] = 'n'; break; + case '\r': out[j++] = '\\'; out[j++] = 'r'; break; + case '\t': out[j++] = '\\'; out[j++] = 't'; break; + default: + if (c < 0x20) { + j += sprintf(out + j, "\\u%04x", c); + } else { + out[j++] = c; + } + break; + } + } + out[j] = '\0'; + return out; +} + +static void clean_response(char *full, char *ghost, int ghostsize) +{ + char *p = full; + dbglog("raw response: [%s]\n", full); + + /* skip markdown fences */ + if (strncmp(p, "```", 3) == 0) { + p += 3; + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + } + /* remove trailing ``` */ + char *bt = strstr(p, "```"); + if (bt) *bt = '\0'; + /* remove FIM/special tokens */ + for (char *t = p; *t; t++) { + if (*t == '<' && (strncmp(t, "<|fim", 5) == 0 || + strncmp(t, "<|end", 5) == 0 || + strncmp(t, "<|im", 4) == 0)) { + *t = '\0'; break; + } + } + /* first line only */ + char *nl = strchr(p, '\n'); + if (nl) *nl = '\0'; + + /* trim trailing whitespace */ + int plen = (int)strlen(p); + while (plen > 0 && (p[plen-1] == ' ' || p[plen-1] == '\t')) + p[--plen] = '\0'; + + dbglog("cleaned: [%s]\n", p); + strncpy(ghost, p, ghostsize - 1); + ghost[ghostsize - 1] = '\0'; +} + +/* thread argument: the POST body */ +typedef struct { char *body; } WorkerArg; + +static void *worker_fn(void *arg) +{ + WorkerArg *wa = arg; + worker_result[0] = '\0'; + + CURL *curl = curl_easy_init(); + if (!curl) { free(wa->body); free(wa); worker_done = 1; return NULL; } + + RespBuf rb = {NULL, 0}; + struct curl_slist *hdrs = curl_slist_append(NULL, "Content-Type: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, ollama_url); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, wa->body); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); + + dbglog("POST %s\nbody: %.200s...\n", ollama_url, wa->body); + CURLcode res = curl_easy_perform(curl); + dbglog("curl result: %d\n", res); + + curl_slist_free_all(hdrs); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && rb.data) { + dbglog("response: %.300s\n", rb.data); + char full[512]; + extract_response(rb.data, full, sizeof(full)); + clean_response(full, worker_result, sizeof(worker_result)); + } else { + dbglog("curl error: %s\n", curl_easy_strerror(res)); + } + + free(rb.data); + free(wa->body); + free(wa); + worker_done = 1; + return NULL; +} + +int ollama_request(Editor *e) +{ + static int loaded = 0; + if (!loaded) { load_config(); loaded = 1; } + + /* don't start if already running */ + if (worker_running) return -1; + + e->autocomplete_row = -1; /* ghost text mode */ + + /* build prompt */ + char *prompt = NULL; + size_t plen = 0; + FILE *f = open_memstream(&prompt, &plen); + if (!f) return -1; + + fprintf(f, "<|fim_prefix|>"); + + /* inject project description as a comment for context */ + if (e->desc[0]) { + const char *cs = (e->ft == FT_ASM) ? "; " : + (e->ft == FT_PYTHON || e->ft == FT_BASH) ? "# " : "// "; + fprintf(f, "%sProject: %s\n", cs, e->desc); + } + + int start = e->cy - CONTEXT_LINES; + if (start < 0) start = 0; + for (int i = start; i < e->cy; i++) { + Line *l = &e->buf.lines[i]; + if (l->chars && l->len > 0) fwrite(l->chars, 1, l->len, f); + fputc('\n', f); + } + Line *cur = &e->buf.lines[e->cy]; + if (cur->chars && e->cx > 0) fwrite(cur->chars, 1, e->cx, f); + fprintf(f, "<|fim_suffix|>"); + if (cur->chars && e->cx < cur->len) + fwrite(cur->chars + e->cx, 1, cur->len - e->cx, f); + fputc('\n', f); + int end = e->cy + 1 + CONTEXT_LINES; + if (end > e->buf.numlines) end = e->buf.numlines; + for (int i = e->cy + 1; i < end; i++) { + Line *l = &e->buf.lines[i]; + if (l->chars && l->len > 0) fwrite(l->chars, 1, l->len, f); + fputc('\n', f); + } + fprintf(f, "<|fim_middle|>"); + fclose(f); + + char *escaped = json_escape(prompt); + free(prompt); + if (!escaped) return -1; + + char *body = NULL; + if (asprintf(&body, + "{\"model\":\"%s\",\"prompt\":\"%s\"," + "\"raw\":true,\"stream\":false," + "\"options\":{\"num_predict\":64,\"temperature\":0.2," + "\"stop\":[\"\\n\\n\",\"<|fim\",\"<|end\",\"<|im_end\"]}}", + OLLAMA_MODEL, escaped) < 0) { + free(escaped); + return -1; + } + free(escaped); + + WorkerArg *wa = malloc(sizeof(*wa)); + wa->body = body; + + worker_done = 0; + worker_running = 1; + pthread_create(&worker_tid, NULL, worker_fn, wa); + pthread_detach(worker_tid); + return 0; +} + +int ollama_poll(Editor *e) +{ + if (!worker_running) return -1; + if (!worker_done) return 0; /* still working */ + + worker_running = 0; + if (worker_result[0] == '\0') return -1; + + /* autocomplete mode: insert directly into the target line */ + if (e->autocomplete_row >= 0 && e->autocomplete_row < e->buf.numlines) { + int row = e->autocomplete_row; + int col = e->autocomplete_col; + for (int i = 0; worker_result[i]; i++) + buf_insert_char(&e->buf, row, col++, worker_result[i]); + e->autocomplete_row = -1; + return 1; + } + + /* ghost text mode */ + strncpy(e->ghost, worker_result, sizeof(e->ghost) - 1); + e->ghost[sizeof(e->ghost) - 1] = '\0'; + return 1; +} + +int ollama_request_line(Editor *e, int row) +{ + static int loaded = 0; + if (!loaded) { load_config(); loaded = 1; } + if (worker_running) return -1; + + e->autocomplete_row = row; + e->autocomplete_col = buf_line_len(&e->buf, row); + + /* build prompt with cursor at end of target line */ + char *prompt = NULL; + size_t plen = 0; + FILE *f = open_memstream(&prompt, &plen); + if (!f) return -1; + + fprintf(f, "<|fim_prefix|>"); + if (e->desc[0]) { + const char *cs = (e->ft == FT_ASM) ? "; " : + (e->ft == FT_PYTHON || e->ft == FT_BASH) ? "# " : "// "; + fprintf(f, "%sProject: %s\n", cs, e->desc); + } + + int start = row - CONTEXT_LINES; + if (start < 0) start = 0; + for (int i = start; i <= row; i++) { + Line *l = &e->buf.lines[i]; + if (l->chars && l->len > 0) fwrite(l->chars, 1, l->len, f); + if (i < row) fputc('\n', f); + } + fprintf(f, "<|fim_suffix|>\n"); + + /* lines after for context */ + int end = row + 1 + CONTEXT_LINES; + if (end > e->buf.numlines) end = e->buf.numlines; + for (int i = row + 1; i < end; i++) { + Line *l = &e->buf.lines[i]; + if (l->chars && l->len > 0) fwrite(l->chars, 1, l->len, f); + fputc('\n', f); + } + fprintf(f, "<|fim_middle|>"); + fclose(f); + + char *escaped = json_escape(prompt); + free(prompt); + if (!escaped) return -1; + + char *body = NULL; + if (asprintf(&body, + "{\"model\":\"%s\",\"prompt\":\"%s\"," + "\"raw\":true,\"stream\":false," + "\"options\":{\"num_predict\":64,\"temperature\":0.2," + "\"stop\":[\"\\n\",\"<|fim\",\"<|end\",\"<|im_end\"]}}", + OLLAMA_MODEL, escaped) < 0) { + free(escaped); + return -1; + } + free(escaped); + + WorkerArg *wa = malloc(sizeof(*wa)); + wa->body = body; + + worker_done = 0; + worker_running = 1; + pthread_create(&worker_tid, NULL, worker_fn, wa); + pthread_detach(worker_tid); + return 0; +} diff --git a/ved/src/ollama.o b/ved/src/ollama.o new file mode 100644 index 0000000..e6bb695 Binary files /dev/null and b/ved/src/ollama.o differ diff --git a/ved/src/syntax.c b/ved/src/syntax.c new file mode 100644 index 0000000..dcb4b78 --- /dev/null +++ b/ved/src/syntax.c @@ -0,0 +1,326 @@ +#include "editor.h" +#include +#include + +#define CP_KEYWORD 1 +#define CP_TYPE 2 +#define CP_STRING 3 +#define CP_COMMENT 4 +#define CP_NUMBER 5 +#define CP_PREPROC 6 + +static const char *c_keywords[] = { + "auto","break","case","continue","default","do","else","enum", + "extern","for","goto","if","inline","register","restrict", + "return","sizeof","static","struct","switch","typedef","union", + "volatile","while","_Alignas","_Alignof","_Atomic","_Bool", + "_Complex","_Generic","_Imaginary","_Noreturn","_Static_assert", + "_Thread_local",NULL +}; + +static const char *c_types[] = { + "char","const","double","float","int","long","short","signed", + "unsigned","void","int8_t","int16_t","int32_t","int64_t", + "uint8_t","uint16_t","uint32_t","uint64_t","size_t","ssize_t", + "bool","true","false","NULL","FILE",NULL +}; + +static const char *asm_keywords[] = { + "section","global","extern","bits","org","align","equ","db","dw", + "dd","dq","resb","resw","resd","resq","times", + "mov","push","pop","call","ret","jmp","je","jne","jz","jnz", + "jg","jge","jl","jle","ja","jae","jb","jbe","cmp","test", + "add","sub","mul","imul","div","idiv","inc","dec","neg","not", + "and","or","xor","shl","shr","sal","sar","rol","ror", + "lea","nop","int","syscall","sysenter","hlt","rep","movsb", + "stosb","lodsb","cmpsb","scasb",NULL +}; + +static const char *py_keywords[] = { + "and","as","assert","async","await","break","class","continue", + "def","del","elif","else","except","finally","for","from", + "global","if","import","in","is","lambda","nonlocal","not", + "or","pass","raise","return","try","while","with","yield",NULL +}; + +static const char *py_types[] = { + "True","False","None","self","cls", + "int","float","str","bool","list","dict","tuple","set", + "bytes","bytearray","complex","range","type","object", + "print","len","open","super","isinstance","hasattr", + "getattr","setattr","enumerate","zip","map","filter",NULL +}; + +static const char *bash_keywords[] = { + "if","then","else","elif","fi","for","while","until","do","done", + "case","esac","in","function","select","time","coproc", + "return","exit","break","continue","shift","export","unset", + "local","readonly","declare","typeset","source","eval","exec", + "trap","set","shopt",NULL +}; + +static const char *bash_builtins[] = { + "echo","printf","read","cd","pwd","pushd","popd","dirs", + "test","true","false","let","expr", + "grep","sed","awk","cut","sort","uniq","wc","tr","find", + "cat","head","tail","tee","xargs","mkdir","rm","cp","mv", + "chmod","chown","ln","touch","basename","dirname",NULL +}; + +static int is_word_char(int c) +{ + return isalnum(c) || c == '_'; +} + +static int word_match(const char *s, int len, const char **list) +{ + for (int i = 0; list[i]; i++) + if ((int)strlen(list[i]) == len && memcmp(s, list[i], len) == 0) + return 1; + return 0; +} + +void syntax_init(void) +{ + if (!has_colors()) return; + start_color(); + use_default_colors(); + init_pair(CP_KEYWORD, COLOR_YELLOW, -1); + init_pair(CP_TYPE, COLOR_GREEN, -1); + init_pair(CP_STRING, COLOR_MAGENTA,-1); + init_pair(CP_COMMENT, COLOR_CYAN, -1); + init_pair(CP_NUMBER, COLOR_RED, -1); + init_pair(CP_PREPROC, COLOR_BLUE, -1); +} + +enum filetype syntax_detect(const char *filename) +{ + if (!filename) return FT_NONE; + const char *dot = strrchr(filename, '.'); + if (!dot) return FT_NONE; + if (strcmp(dot, ".c") == 0 || strcmp(dot, ".h") == 0) + return FT_C; + if (strcmp(dot, ".s") == 0 || strcmp(dot, ".S") == 0 || + strcmp(dot, ".asm") == 0 || strcmp(dot, ".nasm") == 0) + return FT_ASM; + if (strcmp(dot, ".py") == 0) + return FT_PYTHON; + if (strcmp(dot, ".sh") == 0 || strcmp(dot, ".bash") == 0) + return FT_BASH; + return FT_NONE; +} + +void syntax_draw_line(const char *s, int len, int y, int x, + int maxcols, enum filetype ft, int in_block_comment) +{ + int i = 0; + while (i < len && (i + x) < maxcols) { + /* C block comment continuation */ + if (in_block_comment) { + int start = i; + while (i < len) { + if (i + 1 < len && s[i] == '*' && s[i+1] == '/') { + i += 2; break; + } + i++; + } + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_COMMENT)); + in_block_comment = (i >= len || !(i >= 2 && s[i-2] == '*' && s[i-1] == '/')); + continue; + } + + /* line comments */ + if (ft == FT_C && i + 1 < len && s[i] == '/' && s[i+1] == '/') { + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + i, s + i, len - i); + attroff(COLOR_PAIR(CP_COMMENT)); + return; + } + if (ft == FT_ASM && s[i] == ';') { + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + i, s + i, len - i); + attroff(COLOR_PAIR(CP_COMMENT)); + return; + } + if (ft == FT_PYTHON && s[i] == '#') { + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + i, s + i, len - i); + attroff(COLOR_PAIR(CP_COMMENT)); + return; + } + if (ft == FT_BASH && s[i] == '#') { + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + i, s + i, len - i); + attroff(COLOR_PAIR(CP_COMMENT)); + return; + } + + /* C block comment start */ + if (ft == FT_C && i + 1 < len && s[i] == '/' && s[i+1] == '*') { + int start = i; + i += 2; + while (i + 1 < len && !(s[i] == '*' && s[i+1] == '/')) i++; + if (i + 1 < len) i += 2; + attron(COLOR_PAIR(CP_COMMENT)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_COMMENT)); + continue; + } + + /* C preprocessor */ + if (ft == FT_C && s[i] == '#') { + attron(COLOR_PAIR(CP_PREPROC)); + mvaddnstr(y, x + i, s + i, len - i); + attroff(COLOR_PAIR(CP_PREPROC)); + return; + } + + /* Python decorator */ + if (ft == FT_PYTHON && s[i] == '@') { + int start = i++; + while (i < len && is_word_char((unsigned char)s[i])) i++; + attron(COLOR_PAIR(CP_PREPROC)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_PREPROC)); + continue; + } + + /* triple-quoted strings (Python) */ + if (ft == FT_PYTHON && i + 2 < len && + ((s[i] == '"' && s[i+1] == '"' && s[i+2] == '"') || + (s[i] == '\'' && s[i+1] == '\'' && s[i+2] == '\''))) { + char q = s[i]; + int start = i; + i += 3; + while (i + 2 < len && !(s[i] == q && s[i+1] == q && s[i+2] == q)) + i++; + if (i + 2 < len) i += 3; + attron(COLOR_PAIR(CP_STRING)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_STRING)); + continue; + } + + /* f-string prefix (Python) */ + if (ft == FT_PYTHON && (s[i] == 'f' || s[i] == 'r' || s[i] == 'b') && + i + 1 < len && (s[i+1] == '"' || s[i+1] == '\'')) { + int start = i++; + char q = s[i++]; + while (i < len && s[i] != q) { + if (s[i] == '\\' && i + 1 < len) i++; + i++; + } + if (i < len) i++; + attron(COLOR_PAIR(CP_STRING)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_STRING)); + continue; + } + + /* string / char literal */ + if (s[i] == '"' || s[i] == '\'') { + char q = s[i]; + int start = i++; + while (i < len && s[i] != q) { + if (s[i] == '\\' && i + 1 < len) i++; + i++; + } + if (i < len) i++; + attron(COLOR_PAIR(CP_STRING)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_STRING)); + continue; + } + + /* number */ + if (isdigit((unsigned char)s[i]) || + (s[i] == '0' && i + 1 < len && (s[i+1] == 'x' || s[i+1] == 'b' || s[i+1] == 'o'))) { + int start = i; + if (s[i] == '0' && i + 1 < len && (s[i+1] == 'x' || s[i+1] == 'b' || s[i+1] == 'o')) + i += 2; + while (i < len && (isxdigit((unsigned char)s[i]) || s[i] == '.' || s[i] == '_')) + i++; + attron(COLOR_PAIR(CP_NUMBER)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_NUMBER)); + continue; + } + + /* word: keyword or type */ + if (is_word_char((unsigned char)s[i])) { + int start = i; + while (i < len && is_word_char((unsigned char)s[i])) i++; + int wlen = i - start; + + const char **kw = NULL, **tp = NULL; + if (ft == FT_C) { kw = c_keywords; tp = c_types; } + else if (ft == FT_ASM) { kw = asm_keywords; tp = NULL; } + else if (ft == FT_PYTHON) { kw = py_keywords; tp = py_types; } + else if (ft == FT_BASH) { kw = bash_keywords; tp = bash_builtins; } + + if (kw && word_match(s + start, wlen, kw)) { + attron(COLOR_PAIR(CP_KEYWORD) | A_BOLD); + mvaddnstr(y, x + start, s + start, wlen); + attroff(COLOR_PAIR(CP_KEYWORD) | A_BOLD); + } else if (tp && word_match(s + start, wlen, tp)) { + attron(COLOR_PAIR(CP_TYPE)); + mvaddnstr(y, x + start, s + start, wlen); + attroff(COLOR_PAIR(CP_TYPE)); + } else { + mvaddnstr(y, x + start, s + start, wlen); + } + continue; + } + + /* ASM register */ + if (ft == FT_ASM && s[i] == '%') { + int start = i++; + while (i < len && is_word_char((unsigned char)s[i])) i++; + attron(COLOR_PAIR(CP_TYPE)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_TYPE)); + continue; + } + + /* bash $variable, ${var}, $(...) */ + if (ft == FT_BASH && s[i] == '$') { + int start = i++; + if (i < len && (s[i] == '{' || s[i] == '(')) { + char close = (s[i] == '{') ? '}' : ')'; + i++; + while (i < len && s[i] != close) i++; + if (i < len) i++; + } else { + while (i < len && is_word_char((unsigned char)s[i])) i++; + } + attron(COLOR_PAIR(CP_TYPE)); + mvaddnstr(y, x + start, s + start, i - start); + attroff(COLOR_PAIR(CP_TYPE)); + continue; + } + + mvaddnstr(y, x + i, s + i, 1); + i++; + } +} + +int syntax_line_in_block_comment(Buffer *b, int row, enum filetype ft) +{ + if (ft != FT_C) return 0; + int in = 0; + for (int r = 0; r < row; r++) { + char *s = b->lines[r].chars; + int len = b->lines[r].len; + if (!s) continue; + for (int i = 0; i < len; i++) { + if (!in && i + 1 < len && s[i] == '/' && s[i+1] == '*') { + in = 1; i++; + } else if (in && i + 1 < len && s[i] == '*' && s[i+1] == '/') { + in = 0; i++; + } + } + } + return in; +} diff --git a/ved/src/syntax.o b/ved/src/syntax.o new file mode 100644 index 0000000..277d4d2 Binary files /dev/null and b/ved/src/syntax.o differ diff --git a/ved/src/terminal.c b/ved/src/terminal.c new file mode 100644 index 0000000..7ae09bf --- /dev/null +++ b/ved/src/terminal.c @@ -0,0 +1,16 @@ +#include "editor.h" + +void term_init(void) +{ + initscr(); + raw(); + noecho(); + keypad(stdscr, TRUE); + set_escdelay(25); + syntax_init(); +} + +void term_end(void) +{ + endwin(); +} diff --git a/ved/src/terminal.o b/ved/src/terminal.o new file mode 100644 index 0000000..4f8084d Binary files /dev/null and b/ved/src/terminal.o differ diff --git a/vedit/vedit.py b/vedit/vedit.py index 474b4ae..e21b63b 100755 --- a/vedit/vedit.py +++ b/vedit/vedit.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 + +# vedit, a test client to vibe code. - anders@holck.se March 2026 + """Vibe - A simple vibe coding tool bridging Ollama LLM and bash shell.""" import curses