Adding ved
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Make a hello world in C
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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]");
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Binary file not shown.
+246
@@ -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);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Binary file not shown.
@@ -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.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user