Adding ved

This commit is contained in:
2026-04-08 16:37:32 +02:00
parent f1d1deab0f
commit e6596f3e27
27 changed files with 2110 additions and 0 deletions
+17
View File
@@ -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
+80
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
Make a hello world in C
+157
View File
@@ -0,0 +1,157 @@
#define _GNU_SOURCE
#include "editor.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
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;
}
BIN
View File
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
#include "editor.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
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;
}
}
BIN
View File
Binary file not shown.
+230
View File
@@ -0,0 +1,230 @@
#include "editor.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <libgen.h>
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]");
}
+99
View File
@@ -0,0 +1,99 @@
#ifndef EDITOR_H
#define EDITOR_H
#include <ncurses.h>
/* 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
BIN
View File
Binary file not shown.
+25
View File
@@ -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;
}
}
BIN
View File
Binary file not shown.
+134
View File
@@ -0,0 +1,134 @@
#include "editor.h"
#include <string.h>
#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;
}
}
BIN
View File
Binary file not shown.
+73
View File
@@ -0,0 +1,73 @@
#include "editor.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
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;
}
BIN
View File
Binary file not shown.
+246
View File
@@ -0,0 +1,246 @@
#include "editor.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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);
}
}
BIN
View File
Binary file not shown.
+238
View File
@@ -0,0 +1,238 @@
#include "editor.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
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);
}
BIN
View File
Binary file not shown.
+404
View File
@@ -0,0 +1,404 @@
#define _GNU_SOURCE
#include "editor.h"
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#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;
}
BIN
View File
Binary file not shown.
+326
View File
@@ -0,0 +1,326 @@
#include "editor.h"
#include <string.h>
#include <ctype.h>
#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;
}
BIN
View File
Binary file not shown.
+16
View File
@@ -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();
}
Binary file not shown.