From c5b14d7c41b3b7d9be65f3ddc9adfd370c9fd4e3 Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Mon, 13 Jul 2026 13:02:47 -0400 Subject: [PATCH] add c and jai --- c/base/base_arena.c | 53 ++ c/base/base_arena.h | 44 ++ c/base/base_core.h | 181 +++++ c/base/base_inc.c | 3 + c/base/base_inc.h | 8 + c/base/base_math.h | 110 +++ c/base/base_strings.c | 34 + c/base/base_strings.h | 49 ++ c/build.h | 707 ++++++++++++++++++ c/config/config.c | 235 ++++++ c/config/config.h | 51 ++ c/installer/installer.c | 604 +++++++++++++++ c/installer/installer.h | 16 + c/installer/installer.manifest | 17 + c/installer/installer.rc | 14 + c/lexer/lexer.c | 98 +++ c/lexer/lexer.h | 98 +++ c/lexer/lexer_c.c | 423 +++++++++++ c/lexer/lexer_go.c | 489 ++++++++++++ c/lexer/lexer_js.c | 651 ++++++++++++++++ c/lexer/lexer_lua.c | 422 +++++++++++ c/lexer/lexer_sql.c | 645 ++++++++++++++++ c/lexer/lexer_theme.c | 502 +++++++++++++ c/lexer/lexer_theme.h | 50 ++ c/platform/platform.h | 130 ++++ c/platform/platform_win32.c | 297 ++++++++ jai/modules/Console_Render/console.jai | 10 + jai/modules/Console_Render/draw.jai | 107 +++ .../Console_Render/file_operations.jai | 8 + jai/modules/Console_Render/macos/console.jai | 42 ++ jai/modules/Console_Render/math_extras.jai | 19 + jai/modules/Console_Render/module.jai | 122 +++ jai/modules/Console_Render/transform.jai | 67 ++ 33 files changed, 6306 insertions(+) create mode 100644 c/base/base_arena.c create mode 100644 c/base/base_arena.h create mode 100644 c/base/base_core.h create mode 100644 c/base/base_inc.c create mode 100644 c/base/base_inc.h create mode 100644 c/base/base_math.h create mode 100644 c/base/base_strings.c create mode 100644 c/base/base_strings.h create mode 100644 c/build.h create mode 100644 c/config/config.c create mode 100644 c/config/config.h create mode 100644 c/installer/installer.c create mode 100644 c/installer/installer.h create mode 100644 c/installer/installer.manifest create mode 100644 c/installer/installer.rc create mode 100644 c/lexer/lexer.c create mode 100644 c/lexer/lexer.h create mode 100644 c/lexer/lexer_c.c create mode 100644 c/lexer/lexer_go.c create mode 100644 c/lexer/lexer_js.c create mode 100644 c/lexer/lexer_lua.c create mode 100644 c/lexer/lexer_sql.c create mode 100644 c/lexer/lexer_theme.c create mode 100644 c/lexer/lexer_theme.h create mode 100644 c/platform/platform.h create mode 100644 c/platform/platform_win32.c create mode 100644 jai/modules/Console_Render/console.jai create mode 100644 jai/modules/Console_Render/draw.jai create mode 100644 jai/modules/Console_Render/file_operations.jai create mode 100644 jai/modules/Console_Render/macos/console.jai create mode 100644 jai/modules/Console_Render/math_extras.jai create mode 100644 jai/modules/Console_Render/module.jai create mode 100644 jai/modules/Console_Render/transform.jai diff --git a/c/base/base_arena.c b/c/base/base_arena.c new file mode 100644 index 00000000..905c3167 --- /dev/null +++ b/c/base/base_arena.c @@ -0,0 +1,53 @@ +#include "base/base_arena.h" +#include + +Arena *arena_alloc(U64 cap) { + U8 *mem = (U8 *)malloc(sizeof(Arena) + cap); + if (!mem) return NULL; + Arena *arena = (Arena *)mem; + arena->base = mem + sizeof(Arena); + arena->pos = 0; + arena->cap = cap; + return arena; +} + +void arena_release(Arena *arena) { + if (arena) free(arena); +} + +void *arena_push(Arena *arena, U64 size) { + U64 aligned = AlignPow2(size, 8); + if (arena->pos + aligned > arena->cap) { + Assert(!"Arena overflow"); + return NULL; + } + void *result = arena->base + arena->pos; + arena->pos += aligned; + MemoryZero(result, aligned); + return result; +} + +void *arena_push_no_zero(Arena *arena, U64 size) { + U64 aligned = AlignPow2(size, 8); + if (arena->pos + aligned > arena->cap) { + Assert(!"Arena overflow"); + return NULL; + } + void *result = arena->base + arena->pos; + arena->pos += aligned; + return result; +} + +U64 arena_pos(Arena *arena) { + return arena->pos; +} + +void arena_pop_to(Arena *arena, U64 pos) { + if (pos < arena->pos) { + arena->pos = pos; + } +} + +void arena_clear(Arena *arena) { + arena->pos = 0; +} diff --git a/c/base/base_arena.h b/c/base/base_arena.h new file mode 100644 index 00000000..3795d011 --- /dev/null +++ b/c/base/base_arena.h @@ -0,0 +1,44 @@ +#pragma once +// base_arena.h - Linear arena allocator +// Simplified from raddebugger's virtual-memory-backed arena to a malloc-based one. +// Suitable for per-frame scratch allocations and persistent state. + +#include "base/base_core.h" + +//////////////////////////////// +// Arena type + +typedef struct Arena { + U8 *base; + U64 pos; + U64 cap; +} Arena; + +// Temporary scope (save/restore position) +typedef struct Temp { + Arena *arena; + U64 pos; +} Temp; + +//////////////////////////////// +// Arena functions + +Arena *arena_alloc(U64 cap); +void arena_release(Arena *arena); +void *arena_push(Arena *arena, U64 size); +void *arena_push_no_zero(Arena *arena, U64 size); +U64 arena_pos(Arena *arena); +void arena_pop_to(Arena *arena, U64 pos); +void arena_clear(Arena *arena); + +//////////////////////////////// +// Temporary scope helpers + +static inline Temp temp_begin(Arena *arena) { Temp t = {arena, arena->pos}; return t; } +static inline void temp_end(Temp temp) { arena_pop_to(temp.arena, temp.pos); } + +//////////////////////////////// +// Push helper macros + +#define push_array(arena, T, count) ((T *)arena_push((arena), sizeof(T) * (count))) +#define push_array_no_zero(arena, T, count) ((T *)arena_push_no_zero((arena), sizeof(T) * (count))) diff --git a/c/base/base_core.h b/c/base/base_core.h new file mode 100644 index 00000000..6706435b --- /dev/null +++ b/c/base/base_core.h @@ -0,0 +1,181 @@ +#pragma once +// base_core.h - Fundamental types, macros, and linked list helpers +// Inspired by raddebugger's base_core.h + +#include +#include +#include +#include +#include +#include + +//////////////////////////////// +// Codebase keywords + +#ifndef __APPLE__ +#define internal static +#define global static +#endif +#define local_persist static + +#define trvke 1 + +//////////////////////////////// +// Base types + +typedef uint8_t U8; +typedef uint16_t U16; +typedef uint32_t U32; +typedef uint64_t U64; +typedef int8_t S8; +typedef int16_t S16; +typedef int32_t S32; +typedef int64_t S64; +typedef S32 B32; +typedef float F32; +typedef double F64; + +//////////////////////////////// +// Limits + +#define max_U8 0xFF +#define max_U16 0xFFFF +#define max_U32 0xFFFFFFFF +#define max_U64 0xFFFFFFFFFFFFFFFFull +#define max_S8 0x7F +#define max_S16 0x7FFF +#define max_S32 0x7FFFFFFF +#define max_S64 0x7FFFFFFFFFFFFFFFll + +//////////////////////////////// +// Units + +#define KB(n) (((U64)(n)) << 10) +#define MB(n) (((U64)(n)) << 20) +#define GB(n) (((U64)(n)) << 30) + +//////////////////////////////// +// Clamps, Mins, Maxes + +#define Min(A, B) (((A) < (B)) ? (A) : (B)) +#define Max(A, B) (((A) > (B)) ? (A) : (B)) +#define ClampTop(A, X) Min(A, X) +#define ClampBot(X, B) Max(X, B) +#define Clamp(A, X, B) (((X) < (A)) ? (A) : ((X) > (B)) ? (B) : (X)) + +//////////////////////////////// +// Alignment / Sizing + +#define AlignPow2(x, b) (((x) + (b) - 1) & (~((b) - 1))) +#define AlignDownPow2(x, b) ((x) & (~((b) - 1))) +#define ArrayCount(a) (sizeof(a) / sizeof((a)[0])) + +//////////////////////////////// +// Memory macros + +#define MemoryCopy(dst, src, size) memmove((dst), (src), (size)) +#define MemorySet(dst, byte, size) memset((dst), (byte), (size)) +#define MemoryCompare(a, b, size) memcmp((a), (b), (size)) +#define MemoryZero(s, z) memset((s), 0, (z)) +#define MemoryZeroStruct(s) MemoryZero((s), sizeof(*(s))) +#define MemoryZeroArray(a) MemoryZero((a), sizeof(a)) +#define MemoryMatch(a, b, z) (MemoryCompare((a), (b), (z)) == 0) + +//////////////////////////////// +// Pointer / integer casts + +#define IntFromPtr(ptr) ((U64)(ptr)) +#define PtrFromInt(i) (void *)(i) +#define OffsetOf(T, m) IntFromPtr(&(((T *)0)->m)) + +//////////////////////////////// +// Member access + +#define CastFromMember(T, m, ptr) (T *)(((U8 *)(ptr)) - OffsetOf(T, m)) + +//////////////////////////////// +// For-Loop construct macros + +#define DeferLoop(begin, end) for (int _i_ = ((begin), 0); !_i_; _i_ += 1, (end)) +#define DeferLoopChecked(begin, end) for (int _i_ = 2 * !(begin); (_i_ == 2 ? ((end), 0) : !_i_); _i_ += 1, (end)) + +#define EachIndex(it, count) (U64 it = 0; it < (count); it += 1) +#define EachElement(it, array) (U64 it = 0; it < ArrayCount(array); it += 1) + +//////////////////////////////// +// Glue / Stringify + +#define Stringify_(S) #S +#define Stringify(S) Stringify_(S) +#define Glue_(A, B) A##B +#define Glue(A, B) Glue_(A, B) + +#define Swap(T, a, b) do { T t__ = a; a = b; b = t__; } while (0) + +//////////////////////////////// +// Assert + +#if defined(_MSC_VER) +# define Trap() __debugbreak() +#elif defined(__clang__) || defined(__GNUC__) +# define Trap() __builtin_trap() +#else +# define Trap() (*(volatile int *)0 = 0) +#endif + +#define AssertAlways(x) do { if (!(x)) { Trap(); } } while (0) + +#ifdef _DEBUG +# define Assert(x) AssertAlways(x) +#else +# define Assert(x) (void)(x) +#endif + +#define InvalidPath Assert(!"Invalid Path!") +#define NotImplemented Assert(!"Not Implemented!") + +//////////////////////////////// +// Linked list macros +// Nil-aware doubly-linked-list operations + +#define CheckNil(nil, p) ((p) == 0 || (p) == nil) +#define SetNil(nil, p) ((p) = nil) + +// Doubly-linked-list (with nil support) +#define DLLInsert_NPZ(nil, f, l, p, n, next, prev) \ + (CheckNil(nil, f) ? \ + ((f) = (l) = (n), SetNil(nil, (n)->next), SetNil(nil, (n)->prev)) : \ + CheckNil(nil, p) ? \ + ((n)->next = (f), (f)->prev = (n), (f) = (n), SetNil(nil, (n)->prev)) : \ + ((p) == (l)) ? \ + ((l)->next = (n), (n)->prev = (l), (l) = (n), SetNil(nil, (n)->next)) : \ + (((!CheckNil(nil, p) && CheckNil(nil, (p)->next)) ? (0) : ((p)->next->prev = (n))), \ + ((n)->next = (p)->next), ((p)->next = (n)), ((n)->prev = (p)))) + +#define DLLPushBack_NPZ(nil, f, l, n, next, prev) DLLInsert_NPZ(nil, f, l, l, n, next, prev) +#define DLLPushFront_NPZ(nil, f, l, n, next, prev) DLLInsert_NPZ(nil, l, f, f, n, prev, next) + +#define DLLRemove_NPZ(nil, f, l, n, next, prev) \ + (((n) == (f) ? (f) = (n)->next : (0)), \ + ((n) == (l) ? (l) = (l)->prev : (0)), \ + (CheckNil(nil, (n)->prev) ? (0) : ((n)->prev->next = (n)->next)), \ + (CheckNil(nil, (n)->next) ? (0) : ((n)->next->prev = (n)->prev))) + +// Convenience wrappers using 0 as nil +#define DLLPushBack(f, l, n) DLLPushBack_NPZ(0, f, l, n, next, prev) +#define DLLPushFront(f, l, n) DLLPushFront_NPZ(0, f, l, n, next, prev) +#define DLLRemove(f, l, n) DLLRemove_NPZ(0, f, l, n, next, prev) + +// Singly-linked queue (doubly-headed) +#define SLLQueuePush_NZ(nil, f, l, n, next) \ + (CheckNil(nil, f) ? \ + ((f) = (l) = (n), SetNil(nil, (n)->next)) : \ + ((l)->next = (n), (l) = (n), SetNil(nil, (n)->next))) + +#define SLLQueuePush(f, l, n) SLLQueuePush_NZ(0, f, l, n, next) +#define SLLQueuePushFront(f, l, n) (((n)->next = (f)), ((f) = (n))) +#define SLLQueuePop(f, l) ((f) == (l) ? ((f) = 0, (l) = 0) : ((f) = (f)->next)) + +// Singly-linked stack +#define SLLStackPush(f, n) ((n)->next = (f), (f) = (n)) +#define SLLStackPop(f) ((f) = (f)->next) diff --git a/c/base/base_inc.c b/c/base/base_inc.c new file mode 100644 index 00000000..2b7f9673 --- /dev/null +++ b/c/base/base_inc.c @@ -0,0 +1,3 @@ +// base_inc.c - Unity build for the base layer +#include "base/base_arena.c" +#include "base/base_strings.c" diff --git a/c/base/base_inc.h b/c/base/base_inc.h new file mode 100644 index 00000000..8c0855da --- /dev/null +++ b/c/base/base_inc.h @@ -0,0 +1,8 @@ +#pragma once +// base_inc.h - Umbrella include for the base layer +// Include this one header to get all base types. + +#include "base/base_core.h" +#include "base/base_arena.h" +#include "base/base_math.h" +#include "base/base_strings.h" diff --git a/c/base/base_math.h b/c/base/base_math.h new file mode 100644 index 00000000..adb08ddf --- /dev/null +++ b/c/base/base_math.h @@ -0,0 +1,110 @@ +#pragma once +// base_math.h - Vector, range, and color types +// Inspired by raddebugger's base_math.h + +#include "base/base_core.h" + +//////////////////////////////// +// Axis enum + +typedef enum Axis2 { + Axis2_X = 0, + Axis2_Y = 1, + Axis2_COUNT, +} Axis2; + +typedef enum Side { + Side_Min = 0, + Side_Max = 1, + Side_COUNT, +} Side; + +typedef enum Corner { + Corner_00 = 0, // top-left + Corner_01 = 1, // top-right + Corner_10 = 2, // bottom-left + Corner_11 = 3, // bottom-right + Corner_COUNT, +} Corner; + +//////////////////////////////// +// Vector types + +typedef struct Vec2F32 { F32 x, y; } Vec2F32; +typedef struct Vec2S32 { S32 x, y; } Vec2S32; +typedef struct Vec3F32 { F32 x, y, z; } Vec3F32; +typedef struct Vec4F32 { F32 x, y, z, w; } Vec4F32; + +//////////////////////////////// +// Range types + +typedef struct Rng1F32 { F32 min, max; } Rng1F32; +typedef struct Rng1S64 { S64 min, max; } Rng1S64; +typedef struct Rng2F32 { Vec2F32 p0, p1; } Rng2F32; + +//////////////////////////////// +// Constructors + +static inline Vec2F32 v2f32(F32 x, F32 y) { Vec2F32 r = {x, y}; return r; } +static inline Vec2S32 v2s32(S32 x, S32 y) { Vec2S32 r = {x, y}; return r; } +static inline Vec3F32 v3f32(F32 x, F32 y, F32 z) { Vec3F32 r = {x, y, z}; return r; } +static inline Vec4F32 v4f32(F32 x, F32 y, F32 z, F32 w) { Vec4F32 r = {x, y, z, w}; return r; } +static inline Rng1F32 rng1f32(F32 min, F32 max) { Rng1F32 r = {min, max}; return r; } +static inline Rng1S64 rng1s64(S64 min, S64 max) { Rng1S64 r = {min, max}; return r; } +static inline Rng2F32 rng2f32(Vec2F32 p0, Vec2F32 p1) { Rng2F32 r = {p0, p1}; return r; } +static inline Rng2F32 rng2f32p(F32 x0, F32 y0, F32 x1, F32 y1) { Rng2F32 r = {{x0, y0}, {x1, y1}}; return r; } + +//////////////////////////////// +// Vec2F32 operations + +static inline Vec2F32 add_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x + b.x, a.y + b.y); } +static inline Vec2F32 sub_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x - b.x, a.y - b.y); } +static inline Vec2F32 mul_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x * b.x, a.y * b.y); } +static inline Vec2F32 scale_2f32(Vec2F32 v, F32 s) { return v2f32(v.x * s, v.y * s); } + +// Axis-indexed access +static inline F32 v2f32_axis(Vec2F32 v, Axis2 a) { return a == Axis2_X ? v.x : v.y; } +static inline void v2f32_set_axis(Vec2F32 *v, Axis2 a, F32 val) { + if (a == Axis2_X) v->x = val; else v->y = val; +} + +//////////////////////////////// +// Vec4F32 operations + +static inline Vec4F32 add_4f32(Vec4F32 a, Vec4F32 b) { return v4f32(a.x+b.x, a.y+b.y, a.z+b.z, a.w+b.w); } +static inline Vec4F32 scale_4f32(Vec4F32 v, F32 s) { return v4f32(v.x*s, v.y*s, v.z*s, v.w*s); } +static inline Vec4F32 lerp_4f32(Vec4F32 a, Vec4F32 b, F32 t) { + return v4f32(a.x + (b.x - a.x)*t, a.y + (b.y - a.y)*t, a.z + (b.z - a.z)*t, a.w + (b.w - a.w)*t); +} + +//////////////////////////////// +// Rng2F32 operations + +static inline F32 rng2f32_width(Rng2F32 r) { return r.p1.x - r.p0.x; } +static inline F32 rng2f32_height(Rng2F32 r) { return r.p1.y - r.p0.y; } +static inline Vec2F32 rng2f32_dim(Rng2F32 r) { return v2f32(r.p1.x - r.p0.x, r.p1.y - r.p0.y); } +static inline Vec2F32 rng2f32_center(Rng2F32 r) { return v2f32((r.p0.x + r.p1.x)*0.5f, (r.p0.y + r.p1.y)*0.5f); } +static inline B32 rng2f32_contains(Rng2F32 r, Vec2F32 p) { + return p.x >= r.p0.x && p.x <= r.p1.x && p.y >= r.p0.y && p.y <= r.p1.y; +} +static inline Rng2F32 rng2f32_pad(Rng2F32 r, F32 p) { + return rng2f32p(r.p0.x - p, r.p0.y - p, r.p1.x + p, r.p1.y + p); +} +static inline Rng2F32 rng2f32_shift(Rng2F32 r, Vec2F32 v) { + return rng2f32p(r.p0.x + v.x, r.p0.y + v.y, r.p1.x + v.x, r.p1.y + v.y); +} +static inline Rng2F32 rng2f32_intersect(Rng2F32 a, Rng2F32 b) { + return rng2f32p(Max(a.p0.x, b.p0.x), Max(a.p0.y, b.p0.y), + Min(a.p1.x, b.p1.x), Min(a.p1.y, b.p1.y)); +} + +// Axis-indexed range dimension +static inline F32 rng2f32_dim_axis(Rng2F32 r, Axis2 a) { + return a == Axis2_X ? (r.p1.x - r.p0.x) : (r.p1.y - r.p0.y); +} + +//////////////////////////////// +// F32 helpers + +static inline F32 lerp_1f32(F32 a, F32 b, F32 t) { return a + (b - a) * t; } +static inline F32 abs_f32(F32 x) { return x < 0 ? -x : x; } diff --git a/c/base/base_strings.c b/c/base/base_strings.c new file mode 100644 index 00000000..769fa991 --- /dev/null +++ b/c/base/base_strings.c @@ -0,0 +1,34 @@ +#include "base/base_strings.h" +#include "base/base_arena.h" + +Str8 str8_pushf(Arena *arena, const char *fmt, ...) { + va_list args, args2; + va_start(args, fmt); + va_copy(args2, args); + S32 len = vsnprintf(NULL, 0, fmt, args); + va_end(args); + + char *buf = push_array(arena, char, len + 1); + vsnprintf(buf, len + 1, fmt, args2); + va_end(args2); + + Str8 r = {buf, (U64)len}; + return r; +} + +Str8 str8_push_copy(Arena *arena, Str8 s) { + if (s.size == 0 || !s.str) { Str8 r = {NULL, 0}; return r; } + char *buf = push_array_no_zero(arena, char, s.size + 1); + MemoryCopy(buf, s.str, s.size); + buf[s.size] = 0; + Str8 r = {buf, s.size}; + return r; +} + +void str8_list_push(Arena *arena, Str8List *list, Str8 s) { + Str8Node *node = push_array(arena, Str8Node, 1); + node->string = s; + SLLQueuePush(list->first, list->last, node); + list->count++; + list->total_size += s.size; +} diff --git a/c/base/base_strings.h b/c/base/base_strings.h new file mode 100644 index 00000000..3bb78903 --- /dev/null +++ b/c/base/base_strings.h @@ -0,0 +1,49 @@ +#pragma once +// base_strings.h - Simple length-delimited string type +// Inspired by raddebugger's String8 + +#include "base/base_core.h" + +//////////////////////////////// +// String types + +typedef struct Str8 { + const char *str; + U64 size; +} Str8; + +typedef struct Str8Node { + struct Str8Node *next; + Str8 string; +} Str8Node; + +typedef struct Str8List { + Str8Node *first; + Str8Node *last; + U64 count; + U64 total_size; +} Str8List; + +//////////////////////////////// +// Forward declaration for Arena +typedef struct Arena Arena; + +//////////////////////////////// +// Constructors + +static inline Str8 str8(const char *s, U64 len) { Str8 r = {s, len}; return r; } +static inline Str8 str8_cstr(const char *s) { Str8 r = {s, s ? (U64)strlen(s) : 0}; return r; } +static inline Str8 str8_lit(const char *s) { Str8 r = {s, s ? (U64)strlen(s) : 0}; return r; } +static inline B32 str8_match(Str8 a, Str8 b) { + if (a.size != b.size) return 0; + return MemoryCompare(a.str, b.str, a.size) == 0; +} +static inline B32 str8_is_empty(Str8 s) { return s.size == 0 || s.str == NULL; } + +//////////////////////////////// +// String operations (require arena) + +Str8 str8_pushf(Arena *arena, const char *fmt, ...); +Str8 str8_push_copy(Arena *arena, Str8 s); + +void str8_list_push(Arena *arena, Str8List *list, Str8 s); diff --git a/c/build.h b/c/build.h new file mode 100644 index 00000000..728acba4 --- /dev/null +++ b/c/build.h @@ -0,0 +1,707 @@ +// build.h — Minimal C build system (stb-style single header) +// +// Define BUILD_IMPLEMENTATION in exactly one file before including this header. +// +// Bootstrap (one-time): +// Windows: cl /nologo build.c +// macOS: cc build.c -o build +// After that, just run ./build (or build.exe) — it rebuilds itself. + +#ifndef BUILD_H +#define BUILD_H + +#ifdef _MSC_VER +# define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +# include +#else +# include +# include +# include +#endif + +//////////////////////////////// +// Macros + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +// Self-rebuild: call at the top of main(). Recompiles the build script +// if the source file is newer than the running binary, then re-executes. +#define GO_REBUILD_URSELF(argc, argv) \ + go_rebuild_urself((argc), (argv), __FILE__) + +//////////////////////////////// +// Logging + +typedef enum { + LOG_INFO, + LOG_WARNING, + LOG_ERROR, +} Log_Level; + +void build_log(Log_Level level, const char *fmt, ...); + +//////////////////////////////// +// Temp allocator — ring buffer for short-lived sprintf results + +#ifndef TEMP_CAPACITY +#define TEMP_CAPACITY (8 * 1024 * 1024) +#endif + +char *temp_sprintf(const char *fmt, ...); +void temp_reset(void); + +//////////////////////////////// +// String builder — growable byte buffer + +typedef struct { + char *items; + size_t count; + size_t capacity; +} String_Builder; + +bool sb_read_file(String_Builder *sb, const char *path); +void sb_free(String_Builder *sb); + +//////////////////////////////// +// File I/O + +bool write_entire_file(const char *path, const void *data, size_t size); +bool delete_file(const char *path); +bool rename_file(const char *old_path, const char *new_path); +bool mkdir_if_not_exists(const char *path); + +//////////////////////////////// +// Rebuild checking — returns 1 if rebuild needed, 0 if up to date, -1 on error + +int needs_rebuild(const char *output, const char **inputs, size_t count); +int needs_rebuild1(const char *output, const char *input); + +//////////////////////////////// +// Command builder and runner + +typedef struct { + const char **items; + size_t count; + size_t capacity; +} Cmd; + +// Appends arguments to a command. Use via the macro which auto-counts args. +void cmd__append(Cmd *cmd, size_t n, ...); +void cmd__append_arr(Cmd *cmd, const char **args, size_t n); +#define cmd_append(cmd, ...) do { \ + const char *_cmd_args[] = {__VA_ARGS__}; \ + cmd__append_arr((cmd), _cmd_args, sizeof(_cmd_args) / sizeof(_cmd_args[0])); \ + } while(0) + +// Runs the command synchronously, resets cmd->count to 0, returns success. +bool cmd_run(Cmd *cmd); + +// Frees the command's allocated memory. +void cmd_free(Cmd *cmd); + +//////////////////////////////// +// Self-rebuild + +void go_rebuild_urself(int argc, char **argv, const char *source); + +//////////////////////////////// +// File embedding — convert a binary file to a C header with an array + size. + +bool embed_file(const char *input_path, const char *output_path, const char *var_name); + +//////////////////////////////// +// SPIR-V shader compilation — compile .glsl to .spv via glslc, then embed as C header. + +bool compile_shader(const char *glslc_path, const char *src, const char *spv_path, const char *stage); +bool embed_spirv(const char *spv_path, const char *header_path, const char *array_name); + +#endif // BUILD_H + +//////////////////////////////// +// Implementation +//////////////////////////////// + +#ifdef BUILD_IMPLEMENTATION + +//////////////////////////////// +// Temp allocator + +static size_t g_temp_size = 0; +static char g_temp[TEMP_CAPACITY]; + +void temp_reset(void) { + g_temp_size = 0; +} + +static char *temp_alloc(size_t size) { + if (g_temp_size + size > TEMP_CAPACITY) { + g_temp_size = 0; // wrap around + } + char *result = g_temp + g_temp_size; + g_temp_size += size; + return result; +} + +char *temp_sprintf(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + va_list args2; + va_copy(args2, args); + int n = vsnprintf(NULL, 0, fmt, args); + va_end(args); + + char *result = temp_alloc(n + 1); + vsnprintf(result, n + 1, fmt, args2); + va_end(args2); + return result; +} + +//////////////////////////////// +// Logging + +void build_log(Log_Level level, const char *fmt, ...) { + switch (level) { + case LOG_INFO: fprintf(stderr, "[INFO] "); break; + case LOG_WARNING: fprintf(stderr, "[WARNING] "); break; + case LOG_ERROR: fprintf(stderr, "[ERROR] "); break; + } + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); +} + +//////////////////////////////// +// String builder + +static void sb_ensure(String_Builder *sb, size_t needed) { + if (sb->count + needed <= sb->capacity) return; + size_t new_cap = sb->capacity ? sb->capacity * 2 : 256; + while (new_cap < sb->count + needed) new_cap *= 2; + sb->items = (char *)realloc(sb->items, new_cap); + sb->capacity = new_cap; +} + +bool sb_read_file(String_Builder *sb, const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) { + build_log(LOG_ERROR, "Could not open %s: %s", path, strerror(errno)); + return false; + } + + fseek(f, 0, SEEK_END); +#ifdef _WIN32 + long long m = _telli64(_fileno(f)); +#else + long long m = ftell(f); +#endif + if (m < 0) { + build_log(LOG_ERROR, "Could not get size of %s: %s", path, strerror(errno)); + fclose(f); + return false; + } + fseek(f, 0, SEEK_SET); + + sb_ensure(sb, (size_t)m); + fread(sb->items + sb->count, (size_t)m, 1, f); + if (ferror(f)) { + build_log(LOG_ERROR, "Could not read %s: %s", path, strerror(errno)); + fclose(f); + return false; + } + sb->count += (size_t)m; + + fclose(f); + return true; +} + +void sb_free(String_Builder *sb) { + free(sb->items); + sb->items = NULL; + sb->count = 0; + sb->capacity = 0; +} + +//////////////////////////////// +// File I/O + +bool write_entire_file(const char *path, const void *data, size_t size) { + FILE *f = fopen(path, "wb"); + if (!f) { + build_log(LOG_ERROR, "Could not open %s for writing: %s", path, strerror(errno)); + return false; + } + const char *buf = (const char *)data; + while (size > 0) { + size_t n = fwrite(buf, 1, size, f); + if (ferror(f)) { + build_log(LOG_ERROR, "Could not write to %s: %s", path, strerror(errno)); + fclose(f); + return false; + } + size -= n; + buf += n; + } + fclose(f); + return true; +} + +bool delete_file(const char *path) { + build_log(LOG_INFO, "Deleting %s", path); +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path); + if (attr == INVALID_FILE_ATTRIBUTES) return true; // doesn't exist + if (attr & FILE_ATTRIBUTE_DIRECTORY) { + if (!RemoveDirectoryA(path)) { + build_log(LOG_ERROR, "Could not delete directory %s", path); + return false; + } + } else { + if (!DeleteFileA(path)) { + build_log(LOG_ERROR, "Could not delete file %s", path); + return false; + } + } +#else + if (remove(path) < 0 && errno != ENOENT) { + build_log(LOG_ERROR, "Could not delete %s: %s", path, strerror(errno)); + return false; + } +#endif + return true; +} + +bool rename_file(const char *old_path, const char *new_path) { + build_log(LOG_INFO, "Renaming %s -> %s", old_path, new_path); +#ifdef _WIN32 + if (!MoveFileEx(old_path, new_path, MOVEFILE_REPLACE_EXISTING)) { + build_log(LOG_ERROR, "Could not rename %s to %s", old_path, new_path); + return false; + } +#else + if (rename(old_path, new_path) < 0) { + build_log(LOG_ERROR, "Could not rename %s to %s: %s", old_path, new_path, strerror(errno)); + return false; + } +#endif + return true; +} + +bool mkdir_if_not_exists(const char *path) { +#ifdef _WIN32 + int result = _mkdir(path); +#else + int result = mkdir(path, 0755); +#endif + if (result < 0) { + if (errno == EEXIST) return true; + build_log(LOG_ERROR, "Could not create directory %s: %s", path, strerror(errno)); + return false; + } + build_log(LOG_INFO, "Created directory %s", path); + return true; +} + +//////////////////////////////// +// Rebuild checking + +int needs_rebuild(const char *output, const char **inputs, size_t count) { +#ifdef _WIN32 + HANDLE out_h = CreateFile(output, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (out_h == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1; + build_log(LOG_ERROR, "Could not open %s", output); + return -1; + } + FILETIME out_time; + if (!GetFileTime(out_h, NULL, NULL, &out_time)) { + CloseHandle(out_h); + build_log(LOG_ERROR, "Could not get time of %s", output); + return -1; + } + CloseHandle(out_h); + + for (size_t i = 0; i < count; i++) { + HANDLE in_h = CreateFile(inputs[i], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (in_h == INVALID_HANDLE_VALUE) { + build_log(LOG_ERROR, "Could not open %s", inputs[i]); + return -1; + } + FILETIME in_time; + if (!GetFileTime(in_h, NULL, NULL, &in_time)) { + CloseHandle(in_h); + build_log(LOG_ERROR, "Could not get time of %s", inputs[i]); + return -1; + } + CloseHandle(in_h); + if (CompareFileTime(&in_time, &out_time) == 1) return 1; + } + return 0; +#else + struct stat sb; + memset(&sb, 0, sizeof(sb)); + if (stat(output, &sb) < 0) { + if (errno == ENOENT) return 1; + build_log(LOG_ERROR, "Could not stat %s: %s", output, strerror(errno)); + return -1; + } + time_t out_time = sb.st_mtime; + + for (size_t i = 0; i < count; i++) { + if (stat(inputs[i], &sb) < 0) { + build_log(LOG_ERROR, "Could not stat %s: %s", inputs[i], strerror(errno)); + return -1; + } + if (sb.st_mtime > out_time) return 1; + } + return 0; +#endif +} + +int needs_rebuild1(const char *output, const char *input) { + return needs_rebuild(output, &input, 1); +} + +//////////////////////////////// +// Command builder and runner + +static void cmd__grow(Cmd *cmd) { + size_t new_cap = cmd->capacity ? cmd->capacity * 2 : 32; + cmd->items = (const char **)realloc(cmd->items, new_cap * sizeof(const char *)); + cmd->capacity = new_cap; +} + +void cmd__append(Cmd *cmd, size_t n, ...) { + va_list args; + va_start(args, n); + for (size_t i = 0; i < n; i++) { + const char *arg = va_arg(args, const char *); + if (cmd->count >= cmd->capacity) cmd__grow(cmd); + cmd->items[cmd->count++] = arg; + } + va_end(args); +} + +void cmd__append_arr(Cmd *cmd, const char **args, size_t n) { + for (size_t i = 0; i < n; i++) { + if (cmd->count >= cmd->capacity) cmd__grow(cmd); + cmd->items[cmd->count++] = args[i]; + } +} + +static void cmd_render(Cmd *cmd, char *buf, size_t buf_size) { + size_t pos = 0; + for (size_t i = 0; i < cmd->count && pos < buf_size - 1; i++) { + if (i > 0 && pos < buf_size - 1) buf[pos++] = ' '; + const char *arg = cmd->items[i]; + size_t len = strlen(arg); + if (pos + len < buf_size - 1) { + memcpy(buf + pos, arg, len); + pos += len; + } + } + buf[pos] = '\0'; +} + +#ifdef _WIN32 +// Properly quote a command line for CreateProcess on Windows. +static void cmd_quote_win32(Cmd *cmd, String_Builder *quoted) { + for (size_t i = 0; i < cmd->count; i++) { + const char *arg = cmd->items[i]; + size_t len = strlen(arg); + if (i > 0) { + sb_ensure(quoted, 1); + quoted->items[quoted->count++] = ' '; + } + if (len != 0 && strpbrk(arg, " \t\n\v\"") == NULL) { + sb_ensure(quoted, len); + memcpy(quoted->items + quoted->count, arg, len); + quoted->count += len; + } else { + sb_ensure(quoted, len * 2 + 3); + quoted->items[quoted->count++] = '"'; + size_t backslashes = 0; + for (size_t j = 0; j < len; j++) { + char c = arg[j]; + if (c == '\\') { + backslashes++; + } else { + if (c == '"') { + for (size_t k = 0; k < backslashes + 1; k++) + quoted->items[quoted->count++] = '\\'; + } + backslashes = 0; + } + quoted->items[quoted->count++] = c; + } + for (size_t k = 0; k < backslashes; k++) + quoted->items[quoted->count++] = '\\'; + quoted->items[quoted->count++] = '"'; + } + } + sb_ensure(quoted, 1); + quoted->items[quoted->count] = '\0'; +} +#endif + +bool cmd_run(Cmd *cmd) { + if (cmd->count == 0) { + build_log(LOG_ERROR, "Cannot run empty command"); + return false; + } + + // Log the command + { + char render_buf[4096]; + cmd_render(cmd, render_buf, sizeof(render_buf)); + build_log(LOG_INFO, "CMD: %s", render_buf); + } + +#ifdef _WIN32 + STARTUPINFO si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.dwFlags |= STARTF_USESTDHANDLES; + + PROCESS_INFORMATION pi; + memset(&pi, 0, sizeof(pi)); + + String_Builder quoted = {0}; + cmd_quote_win32(cmd, "ed); + + BOOL ok = CreateProcessA(NULL, quoted.items, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + sb_free("ed); + + cmd->count = 0; + + if (!ok) { + build_log(LOG_ERROR, "Could not create process for %s", cmd->items[0]); + return false; + } + + CloseHandle(pi.hThread); + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit_code; + if (!GetExitCodeProcess(pi.hProcess, &exit_code)) { + build_log(LOG_ERROR, "Could not get exit code"); + CloseHandle(pi.hProcess); + return false; + } + CloseHandle(pi.hProcess); + + if (exit_code != 0) { + build_log(LOG_ERROR, "Command exited with code %lu", exit_code); + return false; + } + return true; +#else + pid_t pid = fork(); + if (pid < 0) { + build_log(LOG_ERROR, "Could not fork: %s", strerror(errno)); + cmd->count = 0; + return false; + } + + if (pid == 0) { + // Child: build null-terminated argv + const char **argv_null = (const char **)malloc((cmd->count + 1) * sizeof(const char *)); + memcpy(argv_null, cmd->items, cmd->count * sizeof(const char *)); + argv_null[cmd->count] = NULL; + execvp(argv_null[0], (char *const *)argv_null); + build_log(LOG_ERROR, "Could not exec %s: %s", argv_null[0], strerror(errno)); + exit(1); + } + + cmd->count = 0; + + int wstatus; + for (;;) { + if (waitpid(pid, &wstatus, 0) < 0) { + build_log(LOG_ERROR, "Could not wait on pid %d: %s", pid, strerror(errno)); + return false; + } + if (WIFEXITED(wstatus)) { + int code = WEXITSTATUS(wstatus); + if (code != 0) { + build_log(LOG_ERROR, "Command exited with code %d", code); + return false; + } + return true; + } + if (WIFSIGNALED(wstatus)) { + build_log(LOG_ERROR, "Command killed by signal %d", WTERMSIG(wstatus)); + return false; + } + } +#endif +} + +void cmd_free(Cmd *cmd) { + free(cmd->items); + cmd->items = NULL; + cmd->count = 0; + cmd->capacity = 0; +} + +//////////////////////////////// +// Self-rebuild + +#ifdef _WIN32 +# define REBUILD_CMD(binary, source) "cl.exe", "/nologo", temp_sprintf("/Fe:%s", binary), source +#else +# define REBUILD_CMD(binary, source) "cc", "-o", binary, source +#endif + +void go_rebuild_urself(int argc, char **argv, const char *source) { + const char *binary = argv[0]; + +#ifdef _WIN32 + // Ensure .exe extension for Windows + size_t len = strlen(binary); + if (len < 4 || strcmp(binary + len - 4, ".exe") != 0) { + binary = temp_sprintf("%s.exe", binary); + } +#endif + + int rebuild = needs_rebuild1(binary, source); + if (rebuild < 0) exit(1); + if (rebuild == 0) return; + + const char *old_binary = temp_sprintf("%s.old", binary); + if (!rename_file(binary, old_binary)) exit(1); + + Cmd cmd = {0}; + cmd_append(&cmd, REBUILD_CMD(binary, source)); + if (!cmd_run(&cmd)) { + rename_file(old_binary, binary); + exit(1); + } + + // Re-execute with the new binary + cmd_append(&cmd, binary); + { + int i; + for (i = 1; i < argc; i++) { + cmd__append(&cmd, 1, argv[i]); + } + } + if (!cmd_run(&cmd)) exit(1); + exit(0); +} + +//////////////////////////////// +// File embedding + +bool embed_file(const char *input_path, const char *output_path, const char *var_name) { + int rebuild = needs_rebuild1(output_path, input_path); + if (rebuild == 0) { + build_log(LOG_INFO, "Up to date: %s", output_path); + return true; + } + if (rebuild < 0) return false; + + build_log(LOG_INFO, "Embedding %s -> %s", input_path, output_path); + + String_Builder src = {0}; + if (!sb_read_file(&src, input_path)) return false; + + String_Builder out = {0}; + const char *header = temp_sprintf( + "// Auto-generated from %s — do not edit by hand.\n" + "#ifndef %s_GEN_H\n" + "#define %s_GEN_H\n\n" + "static const unsigned int %s_size = %u;\n\n" + "static const unsigned char %s_data[] = {\n", + input_path, var_name, var_name, var_name, (unsigned)src.count, var_name); + size_t header_len = strlen(header); + sb_ensure(&out, header_len); + memcpy(out.items + out.count, header, header_len); + out.count += header_len; + + for (size_t i = 0; i < src.count; i += 16) { + sb_ensure(&out, 4 + 16 * 6 + 2); + memcpy(out.items + out.count, " ", 4); + out.count += 4; + size_t end = i + 16; + if (end > src.count) end = src.count; + for (size_t j = i; j < end; j++) { + unsigned char b = (unsigned char)src.items[j]; + char hex[7]; + int n = snprintf(hex, sizeof(hex), "0x%02x, ", b); + memcpy(out.items + out.count, hex, (size_t)n); + out.count += (size_t)n; + } + out.items[out.count++] = '\n'; + } + + const char *footer = "};\n\n#endif\n"; + size_t footer_len = strlen(footer); + sb_ensure(&out, footer_len); + memcpy(out.items + out.count, footer, footer_len); + out.count += footer_len; + + bool ok = write_entire_file(output_path, out.items, out.count); + sb_free(&src); + sb_free(&out); + return ok; +} + +//////////////////////////////// +// SPIR-V shader compilation + +bool compile_shader(const char *glslc_path, const char *src, const char *spv_path, const char *stage) { + Cmd cmd = {0}; + cmd_append(&cmd, glslc_path, temp_sprintf("-fshader-stage=%s", stage), "-o", spv_path, src); + return cmd_run(&cmd); +} + +bool embed_spirv(const char *spv_path, const char *header_path, const char *array_name) { + String_Builder sb = {0}; + if (!sb_read_file(&sb, spv_path)) return false; + + FILE *out = fopen(header_path, "wb"); + if (!out) { + build_log(LOG_ERROR, "Could not open %s for writing", header_path); + sb_free(&sb); + return false; + } + + fprintf(out, "// Auto-generated from %s — do not edit\n", spv_path); + fprintf(out, "#pragma once\n\n"); + size_t word_count = sb.count / 4; + fprintf(out, "#include \n\n"); + fprintf(out, "static const uint32_t %s[] = {\n", array_name); + const uint32_t *words = (const uint32_t *)sb.items; + for (size_t i = 0; i < word_count; i++) { + if (i % 8 == 0) fprintf(out, " "); + fprintf(out, "0x%08x,", words[i]); + if (i % 8 == 7 || i == word_count - 1) fprintf(out, "\n"); + } + fprintf(out, "};\n"); + + fclose(out); + build_log(LOG_INFO, "Generated %s (%zu bytes)", header_path, sb.count); + sb_free(&sb); + return true; +} + +#endif // BUILD_IMPLEMENTATION diff --git a/c/config/config.c b/c/config/config.c new file mode 100644 index 00000000..ae84aa98 --- /dev/null +++ b/c/config/config.c @@ -0,0 +1,235 @@ +// config.c — Global program configuration (config.ini next to binary) + +#include +#include +#include + +#ifdef __APPLE__ +#include +#include +#include +#else +#include +#endif + +Config g_config = {0}; + +//////////////////////////////// +// Path resolution + +static void config_get_path(char *out, S32 out_size) { + char exe_path[CONFIG_PATH_MAX]; +#ifdef __APPLE__ + U32 sz = sizeof(exe_path); + _NSGetExecutablePath(exe_path, &sz); +#else + GetModuleFileNameA(NULL, exe_path, CONFIG_PATH_MAX); +#endif + // Strip binary name to get directory + char *sep = strrchr(exe_path, '/'); +#ifndef __APPLE__ + char *bsep = strrchr(exe_path, '\\'); + if (bsep && (!sep || bsep > sep)) sep = bsep; +#endif + if (sep) sep[1] = '\0'; + snprintf(out, out_size, "%sconfig.ini", exe_path); +} + +//////////////////////////////// +// Helpers + +static void config_trim(char *s) { + char *start = s; + while (*start == ' ' || *start == '\t') start++; + if (start != s) memmove(s, start, strlen(start) + 1); + S32 len = (S32)strlen(s); + while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t' || + s[len - 1] == '\r' || s[len - 1] == '\n')) + s[--len] = '\0'; +} + +//////////////////////////////// +// Defaults + +static void config_set_defaults(void) { + MemoryZeroStruct(&g_config); + snprintf(g_config.theme, sizeof(g_config.theme), "%s", "Gruvbox Dark"); + g_config.show_line_numbers = 1; + g_config.syntax_enabled = 1; + g_config.block_cursor = 1; + g_config.cursor_smear = 1; + g_config.smooth_scroll = 1; + g_config.ui_scale = 1.0f; + g_config.editor_font_size = 15.0f; + snprintf(g_config.editor_font, sizeof(g_config.editor_font), "%s", "FiraCode"); + snprintf(g_config.ui_font, sizeof(g_config.ui_font), "%s", "Inter"); + g_config.window_width = 1280; + g_config.window_height = 800; + g_config.active_project[0] = '\0'; + g_config.recent_dir_count = 0; +} + +//////////////////////////////// +// Load + +static void config_load(void) { + config_set_defaults(); + + char path[CONFIG_PATH_MAX]; + config_get_path(path, sizeof(path)); + + FILE *f = fopen(path, "rb"); + if (!f) { + // No config file — write defaults + config_save(); + return; + } + + char line_buf[CONFIG_PATH_MAX + 64]; + char current_section[64] = {0}; + + while (fgets(line_buf, sizeof(line_buf), f)) { + config_trim(line_buf); + if (line_buf[0] == '\0' || line_buf[0] == '#' || line_buf[0] == ';') + continue; + + // Section header + if (line_buf[0] == '[') { + char *close = strchr(line_buf, ']'); + if (close) { + *close = '\0'; + snprintf(current_section, sizeof(current_section), "%s", line_buf + 1); + } + continue; + } + + // Key = value + char *eq = strchr(line_buf, '='); + if (!eq) continue; + *eq = '\0'; + char *key = line_buf; + char *val = eq + 1; + config_trim(key); + config_trim(val); + + if (strcmp(current_section, "general") == 0) { + if (strcmp(key, "theme") == 0) + snprintf(g_config.theme, sizeof(g_config.theme), "%s", val); + else if (strcmp(key, "show_line_numbers") == 0) + g_config.show_line_numbers = (strcmp(val, "true") == 0); + else if (strcmp(key, "syntax_enabled") == 0) + g_config.syntax_enabled = (strcmp(val, "true") == 0); + else if (strcmp(key, "block_cursor") == 0) + g_config.block_cursor = (strcmp(val, "true") == 0); + else if (strcmp(key, "cursor_smear") == 0) + g_config.cursor_smear = (strcmp(val, "true") == 0); + else if (strcmp(key, "smooth_scroll") == 0) + g_config.smooth_scroll = (strcmp(val, "true") == 0); + else if (strcmp(key, "ui_scale") == 0) + g_config.ui_scale = (F32)atof(val); + else if (strcmp(key, "editor_font_size") == 0) + g_config.editor_font_size = (F32)atof(val); + else if (strcmp(key, "editor_font") == 0) + snprintf(g_config.editor_font, sizeof(g_config.editor_font), "%s", val); + else if (strcmp(key, "ui_font") == 0) + snprintf(g_config.ui_font, sizeof(g_config.ui_font), "%s", val); + else if (strcmp(key, "active_project") == 0) + snprintf(g_config.active_project, sizeof(g_config.active_project), "%s", val); + else if (strcmp(key, "window_width") == 0) + g_config.window_width = atoi(val); + else if (strcmp(key, "window_height") == 0) + g_config.window_height = atoi(val); + } else if (strcmp(current_section, "recent_dirs") == 0) { + // Keys are "0", "1", "2", etc. + S32 idx = atoi(key); + if (idx >= 0 && idx < CONFIG_MAX_RECENT_DIRS && val[0] != '\0') { + snprintf(g_config.recent_dirs[idx], CONFIG_PATH_MAX, "%s", val); + if (idx + 1 > g_config.recent_dir_count) + g_config.recent_dir_count = idx + 1; + } + } + } + + fclose(f); +} + +//////////////////////////////// +// Save + +static void config_save(void) { + char path[CONFIG_PATH_MAX]; + config_get_path(path, sizeof(path)); + + FILE *f = fopen(path, "wb"); + if (!f) return; + + fprintf(f, "# codeMAX configuration — managed by the program\r\n\r\n"); + + fprintf(f, "[general]\r\n"); + fprintf(f, "theme = %s\r\n", g_config.theme); + fprintf(f, "show_line_numbers = %s\r\n", g_config.show_line_numbers ? "true" : "false"); + fprintf(f, "syntax_enabled = %s\r\n", g_config.syntax_enabled ? "true" : "false"); + fprintf(f, "block_cursor = %s\r\n", g_config.block_cursor ? "true" : "false"); + fprintf(f, "cursor_smear = %s\r\n", g_config.cursor_smear ? "true" : "false"); + fprintf(f, "smooth_scroll = %s\r\n", g_config.smooth_scroll ? "true" : "false"); + fprintf(f, "ui_scale = %.2f\r\n", g_config.ui_scale); + fprintf(f, "editor_font_size = %.1f\r\n", g_config.editor_font_size); + fprintf(f, "editor_font = %s\r\n", g_config.editor_font); + fprintf(f, "ui_font = %s\r\n", g_config.ui_font); + fprintf(f, "window_width = %d\r\n", g_config.window_width); + fprintf(f, "window_height = %d\r\n", g_config.window_height); + if (g_config.active_project[0] != '\0') + fprintf(f, "active_project = %s\r\n", g_config.active_project); + + fprintf(f, "\r\n[recent_dirs]\r\n"); + for (S32 i = 0; i < g_config.recent_dir_count; i++) { + if (g_config.recent_dirs[i][0] != '\0') + fprintf(f, "%d = %s\r\n", i, g_config.recent_dirs[i]); + } + + fclose(f); +} + +//////////////////////////////// +// Recent directories + +static void config_push_recent_dir(const char *dir) { + if (!dir || dir[0] == '\0') return; + + // Normalize path for comparison + char norm[CONFIG_PATH_MAX]; + snprintf(norm, sizeof(norm), "%s", dir); + for (char *p = norm; *p; p++) { + if (*p == '/') *p = '\\'; + } + + // Check if already in list — if so, move to front + S32 existing = -1; + for (S32 i = 0; i < g_config.recent_dir_count; i++) { +#ifdef __APPLE__ + if (strcmp(g_config.recent_dirs[i], norm) == 0) { existing = i; break; } +#else + if (_stricmp(g_config.recent_dirs[i], norm) == 0) { existing = i; break; } +#endif + } + + if (existing == 0) return; // already at front + + // Save the entry if it exists, or use the new dir + char entry[CONFIG_PATH_MAX]; + snprintf(entry, sizeof(entry), "%s", norm); + + // Shift entries down + S32 start = (existing >= 0) ? existing : g_config.recent_dir_count; + if (start >= CONFIG_MAX_RECENT_DIRS) start = CONFIG_MAX_RECENT_DIRS - 1; + for (S32 i = start; i > 0; i--) + memcpy(g_config.recent_dirs[i], g_config.recent_dirs[i - 1], CONFIG_PATH_MAX); + + // Insert at front + snprintf(g_config.recent_dirs[0], CONFIG_PATH_MAX, "%s", entry); + + if (existing < 0 && g_config.recent_dir_count < CONFIG_MAX_RECENT_DIRS) + g_config.recent_dir_count++; + + config_save(); +} diff --git a/c/config/config.h b/c/config/config.h new file mode 100644 index 00000000..6cd42564 --- /dev/null +++ b/c/config/config.h @@ -0,0 +1,51 @@ +#pragma once +// config.h — Global program configuration (config.ini next to binary) +// +// Stores internal program state: active theme, recent project directories, +// and other preferences. This file is managed by the program, not the user. +// Per-project settings belong in .editorconfig. + +#include "base/base_inc.h" + +//////////////////////////////// +// Constants + +#define CONFIG_MAX_RECENT_DIRS 10 +#define CONFIG_PATH_MAX 1024 + +//////////////////////////////// +// Config state + +typedef struct Config { + char theme[64]; // active theme name + char recent_dirs[CONFIG_MAX_RECENT_DIRS][CONFIG_PATH_MAX]; // most recent first + S32 recent_dir_count; + B32 show_line_numbers; + B32 syntax_enabled; + B32 block_cursor; + B32 cursor_smear; + B32 smooth_scroll; + F32 ui_scale; + F32 editor_font_size; + char editor_font[64]; // "FiraCode", "JetBrains Mono", "Cascadia Mono" + char ui_font[64]; // "Inter", "FiraCode", etc. + S32 window_width; + S32 window_height; + char active_project[CONFIG_PATH_MAX]; +} Config; + +// Global config instance +extern Config g_config; + +// Resolve the path to config.ini (next to the running binary). +static void config_get_path(char *out, S32 out_size); + +// Load config from disk. Populates g_config. If the file doesn't exist, +// writes a default one and returns defaults. +static void config_load(void); + +// Save current g_config state to disk. +static void config_save(void); + +// Push a directory to the front of the recent list (deduplicates). +static void config_push_recent_dir(const char *dir); diff --git a/c/installer/installer.c b/c/installer/installer.c new file mode 100644 index 00000000..1b0508ce --- /dev/null +++ b/c/installer/installer.c @@ -0,0 +1,604 @@ +#define _CRT_SECURE_NO_WARNINGS +// installer.c — Self-contained Windows installer for codeMAX +// +// Uses Win32 PropertySheet wizard for the UI, embeds payload EXEs as +// resources, and handles PATH, Start Menu, and Add/Remove Programs. +// +// When invoked with /uninstall, runs the uninstaller instead. + +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "gdi32.lib") +#pragma comment(lib, "uuid.lib") +#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +#include "installer.h" + +// Use codebase types +typedef unsigned char U8; +typedef unsigned short U16; +typedef unsigned int U32; +typedef unsigned long long U64; +typedef int S32; +typedef int B32; + +#define ArrayCount(a) (sizeof(a) / sizeof((a)[0])) + +//////////////////////////////// +// Globals + +static wchar_t g_install_dir[MAX_PATH]; +static B32 g_add_to_path = 1; +static HINSTANCE g_hinst; + +//////////////////////////////// +// Helpers + +static B32 extract_resource(S32 resource_id, const wchar_t *dest_path) { + HRSRC res = FindResourceW(NULL, MAKEINTRESOURCEW(resource_id), (LPCWSTR)RT_RCDATA); + if (!res) return 0; + HGLOBAL h = LoadResource(NULL, res); + if (!h) return 0; + void *data = LockResource(h); + DWORD size = SizeofResource(NULL, res); + if (!data || size == 0) return 0; + + HANDLE f = CreateFileW(dest_path, GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (f == INVALID_HANDLE_VALUE) return 0; + DWORD written; + WriteFile(f, data, size, &written, NULL); + CloseHandle(f); + return written == size; +} + +static B32 create_directory_recursive(const wchar_t *path) { + wchar_t tmp[MAX_PATH]; + wcscpy_s(tmp, MAX_PATH, path); + for (wchar_t *p = tmp + 3; *p; p++) { // skip "C:\" + if (*p == L'\\' || *p == L'/') { + *p = 0; + CreateDirectoryW(tmp, NULL); + *p = L'\\'; + } + } + return CreateDirectoryW(tmp, NULL) || GetLastError() == ERROR_ALREADY_EXISTS; +} + +static B32 create_shortcut(const wchar_t *lnk_path, const wchar_t *target, + const wchar_t *work_dir, const wchar_t *description, + const wchar_t *icon_path) { + HRESULT hr; + IShellLinkW *sl = NULL; + hr = CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + &IID_IShellLinkW, (void **)&sl); + if (FAILED(hr)) return 0; + + sl->lpVtbl->SetPath(sl, target); + sl->lpVtbl->SetWorkingDirectory(sl, work_dir); + if (description) sl->lpVtbl->SetDescription(sl, description); + if (icon_path) sl->lpVtbl->SetIconLocation(sl, icon_path, 0); + + IPersistFile *pf = NULL; + hr = sl->lpVtbl->QueryInterface(sl, &IID_IPersistFile, (void **)&pf); + B32 ok = 0; + if (SUCCEEDED(hr)) { + ok = SUCCEEDED(pf->lpVtbl->Save(pf, lnk_path, TRUE)); + pf->lpVtbl->Release(pf); + } + sl->lpVtbl->Release(sl); + return ok; +} + +static B32 path_add(const wchar_t *dir) { + HKEY key; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", + 0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return 0; + + wchar_t buf[8192] = {0}; + DWORD buf_size = sizeof(buf) - sizeof(wchar_t); + DWORD type = REG_EXPAND_SZ; + RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size); + + // Check if already present + if (wcsstr(buf, dir)) { + RegCloseKey(key); + return 1; + } + + // Append + S32 len = (S32)wcslen(buf); + if (len > 0 && buf[len - 1] != L';') + wcscat_s(buf, ArrayCount(buf), L";"); + wcscat_s(buf, ArrayCount(buf), dir); + + RegSetValueExW(key, L"Path", 0, type, + (BYTE *)buf, (DWORD)((wcslen(buf) + 1) * sizeof(wchar_t))); + RegCloseKey(key); + return 1; +} + +static void path_remove(const wchar_t *dir) { + HKEY key; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", + 0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return; + + wchar_t buf[8192] = {0}; + DWORD buf_size = sizeof(buf) - sizeof(wchar_t); + DWORD type = REG_EXPAND_SZ; + RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size); + + wchar_t result[8192] = {0}; + wchar_t *ctx = NULL; + wchar_t copy[8192]; + wcscpy_s(copy, ArrayCount(copy), buf); + + wchar_t *tok = wcstok_s(copy, L";", &ctx); + B32 first = 1; + while (tok) { + if (_wcsicmp(tok, dir) != 0) { + if (!first) wcscat_s(result, ArrayCount(result), L";"); + wcscat_s(result, ArrayCount(result), tok); + first = 0; + } + tok = wcstok_s(NULL, L";", &ctx); + } + + RegSetValueExW(key, L"Path", 0, type, + (BYTE *)result, (DWORD)((wcslen(result) + 1) * sizeof(wchar_t))); + RegCloseKey(key); +} + +static void broadcast_env_change(void) { + SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, + (LPARAM)L"Environment", SMTO_ABORTIFHUNG, 5000, NULL); +} + +//////////////////////////////// +// In-memory dialog template builder + +typedef struct { + U8 data[4096]; + S32 len; +} DlgBuf; + +static void dlg_align(DlgBuf *b, S32 align) { + while (b->len % align) b->data[b->len++] = 0; +} + +static void dlg_write(DlgBuf *b, const void *src, S32 n) { + memcpy(b->data + b->len, src, n); + b->len += n; +} + +static void dlg_write16(DlgBuf *b, U16 v) { dlg_write(b, &v, 2); } +static void dlg_write32(DlgBuf *b, U32 v) { dlg_write(b, &v, 4); } + +static void dlg_write_wstr(DlgBuf *b, const wchar_t *s) { + S32 n = (S32)((wcslen(s) + 1) * sizeof(wchar_t)); + dlg_write(b, s, n); +} + +static DLGTEMPLATE *build_page_template(DlgBuf *b, S32 w, S32 h) { + b->len = 0; + // DLGTEMPLATE + DLGTEMPLATE *dt = (DLGTEMPLATE *)b->data; + dlg_write32(b, WS_CHILD | WS_VISIBLE | DS_SHELLFONT); // style + dlg_write32(b, 0); // dwExtendedStyle + dlg_write16(b, 0); // cdit (control count, filled later) + dlg_write16(b, 0); // x + dlg_write16(b, 0); // y + dlg_write16(b, (U16)w); // cx + dlg_write16(b, (U16)h); // cy + dlg_write16(b, 0); // menu (none) + dlg_write16(b, 0); // class (default) + dlg_write_wstr(b, L""); // title + // DS_SHELLFONT font + dlg_write16(b, 9); // point size + dlg_write_wstr(b, L"Segoe UI"); + return dt; +} + +static void add_control(DlgBuf *b, DLGTEMPLATE *dt, U32 style, S32 x, S32 y, + S32 w, S32 h, S32 id, const wchar_t *cls, const wchar_t *text) { + dlg_align(b, 4); + // DLGITEMTEMPLATE + dlg_write32(b, style | WS_CHILD | WS_VISIBLE); // style + dlg_write32(b, 0); // dwExtendedStyle + dlg_write16(b, (U16)x); + dlg_write16(b, (U16)y); + dlg_write16(b, (U16)w); + dlg_write16(b, (U16)h); + dlg_write16(b, (U16)id); + dlg_write_wstr(b, cls); + dlg_write_wstr(b, text); + dlg_write16(b, 0); // creation data + dt->cdit++; +} + +//////////////////////////////// +// Page IDs for controls + +#define IDC_DIR_EDIT 1001 +#define IDC_DIR_BROWSE 1002 +#define IDC_PATH_CHK 1003 +#define IDC_PROGRESS 1004 +#define IDC_STATUS 1005 +#define IDC_LAUNCH_CHK 1006 + +//////////////////////////////// +// Wizard pages + +static INT_PTR CALLBACK welcome_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_INITDIALOG: + return TRUE; + case WM_NOTIFY: { + NMHDR *nm = (NMHDR *)lp; + if (nm->code == PSN_SETACTIVE) { + PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT); + SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0); + } + return TRUE; + } + } + return FALSE; +} + +static INT_PTR CALLBACK dir_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_INITDIALOG: + SetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir); + CheckDlgButton(dlg, IDC_PATH_CHK, g_add_to_path ? BST_CHECKED : BST_UNCHECKED); + return TRUE; + case WM_COMMAND: + if (LOWORD(wp) == IDC_DIR_BROWSE) { + BROWSEINFOW bi = {0}; + bi.hwndOwner = dlg; + bi.lpszTitle = L"Select installation directory:"; + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + LPITEMIDLIST pidl = SHBrowseForFolderW(&bi); + if (pidl) { + wchar_t path[MAX_PATH]; + SHGetPathFromIDListW(pidl, path); + wcscat_s(path, MAX_PATH, L"\\codeMAX"); + SetDlgItemTextW(dlg, IDC_DIR_EDIT, path); + CoTaskMemFree(pidl); + } + } + return TRUE; + case WM_NOTIFY: { + NMHDR *nm = (NMHDR *)lp; + if (nm->code == PSN_SETACTIVE) + PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK | PSWIZB_NEXT); + if (nm->code == PSN_WIZNEXT) { + GetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir, MAX_PATH); + g_add_to_path = (IsDlgButtonChecked(dlg, IDC_PATH_CHK) == BST_CHECKED); + } + return TRUE; + } + } + return FALSE; +} + +static B32 do_install(HWND dlg) { + HWND prog = GetDlgItem(dlg, IDC_PROGRESS); + HWND status = GetDlgItem(dlg, IDC_STATUS); + SendMessageW(prog, PBM_SETRANGE, 0, MAKELPARAM(0, 7)); + SendMessageW(prog, PBM_SETSTEP, 1, 0); + + // 1. Create install directory + SetWindowTextW(status, L"Creating install directory..."); + create_directory_recursive(g_install_dir); + SendMessageW(prog, PBM_STEPIT, 0, 0); + + // 2. Extract terminal exe + SetWindowTextW(status, L"Installing codemax.exe..."); + wchar_t path[MAX_PATH]; + _snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir); + if (!extract_resource(IDR_EXE_GUI, path)) { + MessageBoxW(dlg, L"Failed to extract codemax.exe", L"Error", MB_ICONERROR); + return 0; + } + SendMessageW(prog, PBM_STEPIT, 0, 0); + + // 4. Copy self as uninstaller + SetWindowTextW(status, L"Creating uninstaller..."); + wchar_t self_path[MAX_PATH]; + GetModuleFileNameW(NULL, self_path, MAX_PATH); + _snwprintf(path, MAX_PATH, L"%s\\uninstall.exe", g_install_dir); + CopyFileW(self_path, path, FALSE); + SendMessageW(prog, PBM_STEPIT, 0, 0); + + // 5. Add to PATH (optional) + if (g_add_to_path) { + SetWindowTextW(status, L"Updating PATH..."); + path_add(g_install_dir); + } + SendMessageW(prog, PBM_STEPIT, 0, 0); + + // 6. Create Start Menu shortcuts + SetWindowTextW(status, L"Creating shortcuts..."); + CoInitialize(NULL); + { + wchar_t programs[MAX_PATH]; + SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs); + wchar_t menu_dir[MAX_PATH]; + _snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs); + CreateDirectoryW(menu_dir, NULL); + + wchar_t exe_path[MAX_PATH]; + _snwprintf(exe_path, MAX_PATH, L"%s\\codemax.exe", g_install_dir); + + wchar_t lnk[MAX_PATH]; + _snwprintf(lnk, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir); + create_shortcut(lnk, exe_path, g_install_dir, + L"codeMAX Text Editor", exe_path); + } + CoUninitialize(); + SendMessageW(prog, PBM_STEPIT, 0, 0); + + // 7. Register in Add/Remove Programs + SetWindowTextW(status, L"Registering application..."); + { + HKEY key; + RegCreateKeyExA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY, + 0, NULL, 0, KEY_WRITE, NULL, &key, NULL); + + char dir_a[MAX_PATH], uninst[MAX_PATH * 2]; + WideCharToMultiByte(CP_UTF8, 0, g_install_dir, -1, dir_a, MAX_PATH, NULL, NULL); + snprintf(uninst, sizeof(uninst), "\"%s\\uninstall.exe\" /uninstall", dir_a); + + RegSetValueExA(key, "DisplayName", 0, REG_SZ, + (BYTE *)CODEMAX_DISPLAY_NAME, (DWORD)strlen(CODEMAX_DISPLAY_NAME) + 1); + RegSetValueExA(key, "DisplayVersion", 0, REG_SZ, + (BYTE *)CODEMAX_VERSION, (DWORD)strlen(CODEMAX_VERSION) + 1); + RegSetValueExA(key, "Publisher", 0, REG_SZ, + (BYTE *)CODEMAX_PUBLISHER, (DWORD)strlen(CODEMAX_PUBLISHER) + 1); + RegSetValueExA(key, "InstallLocation", 0, REG_SZ, + (BYTE *)dir_a, (DWORD)strlen(dir_a) + 1); + RegSetValueExA(key, "UninstallString", 0, REG_SZ, + (BYTE *)uninst, (DWORD)strlen(uninst) + 1); + + char icon[MAX_PATH + 4]; + snprintf(icon, sizeof(icon), "%s\\codemax.exe,0", dir_a); + RegSetValueExA(key, "DisplayIcon", 0, REG_SZ, + (BYTE *)icon, (DWORD)strlen(icon) + 1); + + DWORD one = 1; + RegSetValueExA(key, "NoModify", 0, REG_DWORD, (BYTE *)&one, sizeof(one)); + RegSetValueExA(key, "NoRepair", 0, REG_DWORD, (BYTE *)&one, sizeof(one)); + + RegCloseKey(key); + } + SendMessageW(prog, PBM_STEPIT, 0, 0); + + broadcast_env_change(); + SetWindowTextW(status, L"Installation complete."); + return 1; +} + +static INT_PTR CALLBACK progress_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_INITDIALOG: + return TRUE; + case WM_NOTIFY: { + NMHDR *nm = (NMHDR *)lp; + if (nm->code == PSN_SETACTIVE) { + // Disable all buttons during install + PropSheet_SetWizButtons(GetParent(dlg), 0); + // Run install + if (do_install(dlg)) { + PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT); + } else { + PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK); + } + SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0); + } + return TRUE; + } + } + return FALSE; +} + +static INT_PTR CALLBACK finish_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_INITDIALOG: + CheckDlgButton(dlg, IDC_LAUNCH_CHK, BST_CHECKED); + return TRUE; + case WM_NOTIFY: { + NMHDR *nm = (NMHDR *)lp; + if (nm->code == PSN_SETACTIVE) { + PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_FINISH); + } + if (nm->code == PSN_WIZFINISH) { + if (IsDlgButtonChecked(dlg, IDC_LAUNCH_CHK) == BST_CHECKED) { + wchar_t exe[MAX_PATH]; + _snwprintf(exe, MAX_PATH, L"%s\\codemax.exe", g_install_dir); + ShellExecuteW(NULL, L"open", exe, NULL, g_install_dir, SW_SHOWNORMAL); + } + } + return TRUE; + } + } + return FALSE; +} + +//////////////////////////////// +// Wizard launcher + +static void run_installer(void) { + // Default install dir + wcscpy_s(g_install_dir, MAX_PATH, L"C:\\Program Files\\codeMAX"); + + InitCommonControls(); + + // Build page templates in memory + DlgBuf bufs[4] = {0}; + S32 page_w = 317, page_h = 143; + + // Page 0: Welcome + DLGTEMPLATE *dt0 = build_page_template(&bufs[0], page_w, page_h); + add_control(&bufs[0], dt0, SS_LEFT, 10, 10, 297, 40, -1, + L"Static", L"Welcome to the codeMAX installer.\n\n" + L"This will install codeMAX on your computer."); + add_control(&bufs[0], dt0, SS_LEFT, 10, 60, 297, 20, -1, + L"Static", L"Click Next to continue."); + + // Page 1: Directory + options + DLGTEMPLATE *dt1 = build_page_template(&bufs[1], page_w, page_h); + add_control(&bufs[1], dt1, SS_LEFT, 10, 10, 297, 10, -1, + L"Static", L"Choose the installation directory:"); + add_control(&bufs[1], dt1, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP, + 10, 28, 230, 14, IDC_DIR_EDIT, L"Edit", L""); + add_control(&bufs[1], dt1, BS_PUSHBUTTON | WS_TABSTOP, + 248, 27, 60, 14, IDC_DIR_BROWSE, L"Button", L"Browse..."); + add_control(&bufs[1], dt1, BS_AUTOCHECKBOX | WS_TABSTOP, + 10, 52, 250, 14, IDC_PATH_CHK, L"Button", L"Add to system PATH"); + + // Page 2: Progress + DLGTEMPLATE *dt2 = build_page_template(&bufs[2], page_w, page_h); + add_control(&bufs[2], dt2, SS_LEFT, 10, 10, 297, 10, IDC_STATUS, + L"Static", L"Installing..."); + add_control(&bufs[2], dt2, 0, 10, 30, 297, 14, IDC_PROGRESS, + PROGRESS_CLASSW, L""); + + // Page 3: Finish + DLGTEMPLATE *dt3 = build_page_template(&bufs[3], page_w, page_h); + add_control(&bufs[3], dt3, SS_LEFT, 10, 10, 297, 20, -1, + L"Static", L"codeMAX has been installed successfully."); + add_control(&bufs[3], dt3, BS_AUTOCHECKBOX | WS_TABSTOP, + 10, 45, 200, 14, IDC_LAUNCH_CHK, L"Button", L"Launch codeMAX GUI"); + + PROPSHEETPAGEW pages[4] = {0}; + for (S32 i = 0; i < 4; i++) { + pages[i].dwSize = sizeof(PROPSHEETPAGEW); + pages[i].dwFlags = PSP_DLGINDIRECT; + pages[i].hInstance = g_hinst; + } + pages[0].pResource = dt0; + pages[0].pfnDlgProc = welcome_proc; + pages[1].pResource = dt1; + pages[1].pfnDlgProc = dir_proc; + pages[2].pResource = dt2; + pages[2].pfnDlgProc = progress_proc; + pages[3].pResource = dt3; + pages[3].pfnDlgProc = finish_proc; + + PROPSHEETHEADERW psh = {0}; + psh.dwSize = sizeof(PROPSHEETHEADERW); + psh.dwFlags = PSH_WIZARD | PSH_PROPSHEETPAGE | PSH_USEICONID; + psh.hwndParent = NULL; + psh.hInstance = g_hinst; + psh.pszIcon = MAKEINTRESOURCEW(IDI_CODEMAX); + psh.pszCaption = L"codeMAX Setup"; + psh.nPages = 4; + psh.ppsp = pages; + + PropertySheetW(&psh); +} + +//////////////////////////////// +// Uninstaller + +static void run_uninstaller(void) { + // Get install dir from our own exe path + wchar_t self[MAX_PATH]; + GetModuleFileNameW(NULL, self, MAX_PATH); + wcscpy_s(g_install_dir, MAX_PATH, self); + wchar_t *last_sep = wcsrchr(g_install_dir, L'\\'); + if (last_sep) *last_sep = 0; + + S32 result = MessageBoxW(NULL, + L"Are you sure you want to uninstall codeMAX?", + L"codeMAX Uninstall", MB_YESNO | MB_ICONQUESTION); + if (result != IDYES) return; + + // Delete installed files + wchar_t path[MAX_PATH]; + _snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir); + DeleteFileW(path); + _snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir); + DeleteFileW(path); + + // Remove from PATH + path_remove(g_install_dir); + broadcast_env_change(); + + // Remove Start Menu shortcuts + { + wchar_t programs[MAX_PATH]; + SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs); + wchar_t menu_dir[MAX_PATH]; + _snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs); + + _snwprintf(path, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir); + DeleteFileW(path); + _snwprintf(path, MAX_PATH, L"%s\\codeMAX GUI.lnk", menu_dir); + DeleteFileW(path); + RemoveDirectoryW(menu_dir); + } + + // Remove registry entry + RegDeleteKeyA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY); + + // Schedule self-deletion and remove install dir + // Use cmd.exe to wait for us to exit, then delete + wchar_t cmd[MAX_PATH * 3]; + _snwprintf(cmd, ArrayCount(cmd), + L"cmd.exe /c timeout /t 2 /nobreak >nul & del \"%s\\uninstall.exe\" & rmdir \"%s\"", + g_install_dir, g_install_dir); + + STARTUPINFOW si = {0}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + PROCESS_INFORMATION pi = {0}; + CreateProcessW(NULL, cmd, NULL, NULL, FALSE, + CREATE_NO_WINDOW, NULL, NULL, &si, &pi); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + MessageBoxW(NULL, L"codeMAX has been uninstalled.", L"codeMAX Uninstall", + MB_OK | MB_ICONINFORMATION); +} + +//////////////////////////////// +// Entry point + +int main(int argc, char **argv) { + (void)argc; + g_hinst = GetModuleHandleW(NULL); + + // Check for uninstall flag + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "/uninstall") == 0) { + run_uninstaller(); + return 0; + } + } + + // Also check via GetCommandLineW for when launched without CRT argc/argv + wchar_t *cmdline = GetCommandLineW(); + if (wcsstr(cmdline, L"/uninstall")) { + run_uninstaller(); + return 0; + } + + run_installer(); + return 0; +} diff --git a/c/installer/installer.h b/c/installer/installer.h new file mode 100644 index 00000000..03ea5d2f --- /dev/null +++ b/c/installer/installer.h @@ -0,0 +1,16 @@ +#pragma once +// installer.h — Shared constants for installer and resource script + +// Resource IDs +#define IDI_CODEMAX 101 +#define IDR_EXE_TERMINAL 201 +#define IDR_EXE_GUI 202 + +// Product info +#define CODEMAX_APP_NAME "codemax" +#define CODEMAX_DISPLAY_NAME "codeMAX" +#define CODEMAX_VERSION "1.0.0" +#define CODEMAX_PUBLISHER "codeMAX" + +// Registry +#define CODEMAX_UNINSTALL_KEY "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\codeMAX" diff --git a/c/installer/installer.manifest b/c/installer/installer.manifest new file mode 100644 index 00000000..a34b60f1 --- /dev/null +++ b/c/installer/installer.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/c/installer/installer.rc b/c/installer/installer.rc new file mode 100644 index 00000000..156197ef --- /dev/null +++ b/c/installer/installer.rc @@ -0,0 +1,14 @@ +// installer.rc — Resource script for codemax installer +// Embeds icon, UAC manifest, and payload executables. + +#include "installer.h" +#include + +// Application icon +IDI_CODEMAX ICON "assets\\icons\\codemax.ico" + +// UAC elevation manifest +1 24 "src\\installer\\installer.manifest" + +// Payload executable (embedded as RCDATA) +IDR_EXE_GUI RCDATA "build_release\\codemax.exe" diff --git a/c/lexer/lexer.c b/c/lexer/lexer.c new file mode 100644 index 00000000..9dedeac8 --- /dev/null +++ b/c/lexer/lexer.c @@ -0,0 +1,98 @@ +// lexer.c -- Tokenizer infrastructure, color schemes, common helpers + +#include "lexer/lexer.h" +#include +#include + +const char *g_lang_names[LANG_COUNT] = { + "Plain Text", // LANG_PLAIN_TEXT + "C", // LANG_C + "Go", // LANG_GO + "JavaScript", // LANG_JS + "Lua", // LANG_LUA + "SQL", // LANG_SQL +}; + +//////////////////////////////// +// Tokenizer helpers + +static void tokenizer_init(Tokenizer *tok, const char *data, S32 length) { + tok->buf = data; + tok->t = data; + tok->max_t = data + length; + tok->start_t = data; +} + +static void tokenizer_eat_whitespace(Tokenizer *tok) { + while (tok->t < tok->max_t && (*tok->t == ' ' || *tok->t == '\t' || + *tok->t == '\n' || *tok->t == '\r')) + tok->t++; +} + +static void tokenizer_eat_until_newline(Tokenizer *tok) { + while (tok->t < tok->max_t && *tok->t != '\n') + tok->t++; +} + +//////////////////////////////// +// Paint helper -- fill out_tokens[start..start+len) with a token type + +static void paint_token(U8 *out_tokens, S32 start, S32 len, Token_Type type) { + memset(out_tokens + start, (U8)type, len); +} + +//////////////////////////////// +// Language detection + +static Lang lexer_detect_lang(const char *filename) { + if (!filename) return LANG_PLAIN_TEXT; + const char *dot = strrchr(filename, '.'); + if (!dot) return LANG_PLAIN_TEXT; + dot++; // skip the '.' + + if (strcmp(dot, "c") == 0 || strcmp(dot, "h") == 0 || + strcmp(dot, "C") == 0 || strcmp(dot, "H") == 0) + return LANG_C; + + if (strcmp(dot, "go") == 0) + return LANG_GO; + + if (strcmp(dot, "js") == 0 || strcmp(dot, "mjs") == 0 || + strcmp(dot, "cjs") == 0 || strcmp(dot, "jsx") == 0) + return LANG_JS; + + if (strcmp(dot, "lua") == 0) + return LANG_LUA; + + if (strcmp(dot, "sql") == 0 || strcmp(dot, "ddl") == 0 || + strcmp(dot, "dml") == 0 || strcmp(dot, "pgsql") == 0 || + strcmp(dot, "plsql") == 0 || strcmp(dot, "psql") == 0) + return LANG_SQL; + + return LANG_PLAIN_TEXT; +} + +//////////////////////////////// +// Forward-declare language tokenizers + +static void tokenize_c(const char *data, S32 length, U8 *out_tokens); +static void tokenize_go(const char *data, S32 length, U8 *out_tokens); +static void tokenize_js(const char *data, S32 length, U8 *out_tokens); +static void tokenize_lua(const char *data, S32 length, U8 *out_tokens); +static void tokenize_sql(const char *data, S32 length, U8 *out_tokens); + +static void tokenize_plain(const char *data, S32 length, U8 *out_tokens) { + (void)data; + memset(out_tokens, TOK_DEFAULT, length); +} + +static LexerTokenizeFn lexer_get_tokenize_fn(Lang lang) { + switch (lang) { + case LANG_C: return tokenize_c; + case LANG_GO: return tokenize_go; + case LANG_JS: return tokenize_js; + case LANG_LUA: return tokenize_lua; + case LANG_SQL: return tokenize_sql; + default: return tokenize_plain; + } +} diff --git a/c/lexer/lexer.h b/c/lexer/lexer.h new file mode 100644 index 00000000..8b6ba542 --- /dev/null +++ b/c/lexer/lexer.h @@ -0,0 +1,98 @@ +#pragma once +// lexer.h -- Token types, tokenizer interface, and color scheme +// +// Follows the Focus editor pattern: a universal Token_Type enum shared by all +// languages, a per-byte token-type array on each buffer, and language-specific +// tokenizer functions that fill that array. + +#include "base/base_core.h" + +//////////////////////////////// +// Token types (universal across all languages) +// +// The enum value doubles as an index into the color map, so CODE_DEFAULT +// must be 0. Keep this list ordered -- rendering indexes directly. + +typedef enum Token_Type { + TOK_DEFAULT = 0, + + TOK_COMMENT, + TOK_MULTILINE_COMMENT, + + TOK_STRING_LITERAL, + TOK_CHAR_LITERAL, + + TOK_NUMBER, + TOK_IDENTIFIER, + TOK_FUNCTION, + + TOK_KEYWORD, + TOK_TYPE, + TOK_VALUE, // true, false, NULL, nullptr + TOK_MODIFIER, // const, static, volatile, extern ... + + TOK_DIRECTIVE, // #include, #define, ... + TOK_PUNCTUATION, + TOK_OPERATION, + + TOK_INVALID, + + TOK_COUNT +} Token_Type; + +//////////////////////////////// +// Language enum + +typedef enum Lang { + LANG_PLAIN_TEXT = 0, + LANG_C, + LANG_GO, + LANG_JS, + LANG_LUA, + LANG_SQL, + LANG_COUNT +} Lang; + +// Human-readable language names (indexed by Lang) +extern const char *g_lang_names[LANG_COUNT]; + +//////////////////////////////// +// Tokenizer state (shared by all language tokenizers) + +typedef struct Tokenizer { + const char *buf; // start of buffer data + const char *t; // current scan cursor + const char *max_t; // one past end + const char *start_t; // cursor at start of current token +} Tokenizer; + +//////////////////////////////// +// Token (returned by get_next_token functions) + +typedef struct Token { + S32 start; // byte offset into buffer + S32 len; // byte length of token + Token_Type type; +} Token; + +//////////////////////////////// +// Language tokenizer function signature +// +// A tokenizer function takes a buffer's data + length, and writes token types +// into the `out_tokens` array (one byte per source byte, same length as data). +// This is the Focus "paint the token array" approach. + +typedef void (*LexerTokenizeFn)(const char *data, S32 length, U8 *out_tokens); + +// Get the tokenizer function for a language. +static LexerTokenizeFn lexer_get_tokenize_fn(Lang lang); + +// Detect language from a file name / extension. +static Lang lexer_detect_lang(const char *filename); + +//////////////////////////////// +// Common tokenizer helpers (usable by all language tokenizers) + +static void tokenizer_init(Tokenizer *tok, const char *data, S32 length); +static void tokenizer_eat_whitespace(Tokenizer *tok); +static void tokenizer_eat_until_newline(Tokenizer *tok); diff --git a/c/lexer/lexer_c.c b/c/lexer/lexer_c.c new file mode 100644 index 00000000..98ccde63 --- /dev/null +++ b/c/lexer/lexer_c.c @@ -0,0 +1,423 @@ +// lexer_c.c -- C language tokenizer +// +// Follows the Focus editor pattern: a get_next_token() loop that advances +// a cursor through the source, producing Token structs. After each token, +// we paint the per-byte token-type array with the token's type. +// A second pass retroactively marks identifiers followed by '(' as functions. + +#include "lexer/lexer.h" +#include +#include + +//////////////////////////////// +// C keyword tables + +typedef struct KeywordEntry { + const char *word; + Token_Type type; +} KeywordEntry; + +static const KeywordEntry c_keywords[] = { + // Control-flow & language keywords + {"auto", TOK_KEYWORD}, + {"break", TOK_KEYWORD}, + {"case", TOK_KEYWORD}, + {"continue", TOK_KEYWORD}, + {"default", TOK_KEYWORD}, + {"do", TOK_KEYWORD}, + {"else", TOK_KEYWORD}, + {"enum", TOK_KEYWORD}, + {"for", TOK_KEYWORD}, + {"goto", TOK_KEYWORD}, + {"if", TOK_KEYWORD}, + {"inline", TOK_KEYWORD}, + {"restrict", TOK_KEYWORD}, + {"return", TOK_KEYWORD}, + {"sizeof", TOK_KEYWORD}, + {"struct", TOK_KEYWORD}, + {"switch", TOK_KEYWORD}, + {"typedef", TOK_KEYWORD}, + {"union", TOK_KEYWORD}, + {"while", TOK_KEYWORD}, + {"_Alignas", TOK_KEYWORD}, + {"alignas", TOK_KEYWORD}, + {"_Alignof", TOK_KEYWORD}, + {"alignof", TOK_KEYWORD}, + {"_Atomic", TOK_KEYWORD}, + {"_Generic", TOK_KEYWORD}, + {"_Noreturn", TOK_KEYWORD}, + {"static_assert", TOK_KEYWORD}, + {"_Static_assert", TOK_KEYWORD}, + + // Types + {"char", TOK_TYPE}, + {"double", TOK_TYPE}, + {"float", TOK_TYPE}, + {"int", TOK_TYPE}, + {"long", TOK_TYPE}, + {"short", TOK_TYPE}, + {"void", TOK_TYPE}, + {"bool", TOK_TYPE}, + {"_Bool", TOK_TYPE}, + {"_Complex", TOK_TYPE}, + {"_Imaginary", TOK_TYPE}, + // stdint + {"int8_t", TOK_TYPE}, + {"int16_t", TOK_TYPE}, + {"int32_t", TOK_TYPE}, + {"int64_t", TOK_TYPE}, + {"uint8_t", TOK_TYPE}, + {"uint16_t", TOK_TYPE}, + {"uint32_t", TOK_TYPE}, + {"uint64_t", TOK_TYPE}, + {"size_t", TOK_TYPE}, + {"ssize_t", TOK_TYPE}, + {"ptrdiff_t", TOK_TYPE}, + {"intptr_t", TOK_TYPE}, + {"uintptr_t", TOK_TYPE}, + {"nullptr_t", TOK_TYPE}, + {"FILE", TOK_TYPE}, + // Project types + {"U8", TOK_TYPE}, + {"U16", TOK_TYPE}, + {"U32", TOK_TYPE}, + {"U64", TOK_TYPE}, + {"S8", TOK_TYPE}, + {"S16", TOK_TYPE}, + {"S32", TOK_TYPE}, + {"S64", TOK_TYPE}, + {"B32", TOK_TYPE}, + {"F32", TOK_TYPE}, + {"F64", TOK_TYPE}, + + // Values + {"false", TOK_VALUE}, + {"true", TOK_VALUE}, + {"NULL", TOK_VALUE}, + {"nullptr", TOK_VALUE}, + + // Modifiers + {"const", TOK_MODIFIER}, + {"constexpr", TOK_MODIFIER}, + {"extern", TOK_MODIFIER}, + {"register", TOK_MODIFIER}, + {"signed", TOK_MODIFIER}, + {"static", TOK_MODIFIER}, + {"unsigned", TOK_MODIFIER}, + {"volatile", TOK_MODIFIER}, + {"thread_local", TOK_MODIFIER}, + {"_Thread_local", TOK_MODIFIER}, +}; + +#define C_KEYWORD_COUNT (S32)(sizeof(c_keywords) / sizeof(c_keywords[0])) + +static Token_Type c_lookup_keyword(const char *word, S32 len) { + for (S32 i = 0; i < C_KEYWORD_COUNT; i++) { + const char *kw = c_keywords[i].word; + S32 kwlen = (S32)strlen(kw); + if (kwlen == len && memcmp(kw, word, len) == 0) + return c_keywords[i].type; + } + return TOK_IDENTIFIER; +} + +//////////////////////////////// +// Character helpers + +static B32 c_is_ident_start(char c) { + return isalpha((unsigned char)c) || c == '_'; +} + +static B32 c_is_ident_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +static B32 c_is_hex(char c) { + return isdigit((unsigned char)c) || + (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +//////////////////////////////// +// Individual token parsers + +static Token c_parse_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + const char *begin = tok->t; + while (tok->t < tok->max_t && c_is_ident_char(*tok->t)) + tok->t++; + S32 len = (S32)(tok->t - begin); + token.type = c_lookup_keyword(begin, len); + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token c_parse_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + char start_char = *tok->t; + tok->t++; + if (tok->t >= tok->max_t) goto done; + + if (start_char == '0' && tok->t < tok->max_t) { + if (*tok->t == 'x' || *tok->t == 'X') { + tok->t++; + while (tok->t < tok->max_t && c_is_hex(*tok->t)) tok->t++; + goto suffixes; + } + if (*tok->t == 'b' || *tok->t == 'B') { + tok->t++; + while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1')) tok->t++; + goto suffixes; + } + } + + // Decimal / float + { + B32 seen_dot = 0; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '.')) { + if (*tok->t == '.') { + if (seen_dot) break; + seen_dot = 1; + } + tok->t++; + } + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + // Float suffix + if (tok->t < tok->max_t && seen_dot) { + if (*tok->t == 'f' || *tok->t == 'F' || *tok->t == 'l' || *tok->t == 'L') + tok->t++; + goto done; + } + } + +suffixes: + // Integer suffixes: u, l, ll, ul, ull, lu, llu + if (tok->t < tok->max_t && (*tok->t == 'u' || *tok->t == 'U')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) tok->t++; + } + } else if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'u' || *tok->t == 'U')) tok->t++; + } + +done: + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token c_parse_string(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + B32 escape = 0; + tok->t++; // skip opening " + while (tok->t < tok->max_t && *tok->t != '\n') { + if (*tok->t == '"' && !escape) { tok->t++; break; } + escape = !escape && (*tok->t == '\\'); + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token c_parse_char_literal(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_CHAR_LITERAL; + + tok->t++; // skip opening ' + if (tok->t < tok->max_t && *tok->t == '\\') { + tok->t++; // escape char + if (tok->t < tok->max_t && *tok->t != '\n') tok->t++; // the escaped char + } else if (tok->t < tok->max_t && *tok->t != '\n' && *tok->t != '\'') { + tok->t++; // the char + } + if (tok->t < tok->max_t && *tok->t == '\'') tok->t++; // closing ' + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token c_parse_slash_or_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + + tok->t++; // skip '/' + if (tok->t >= tok->max_t) { + token.type = TOK_OPERATION; + token.len = 1; + return token; + } + + if (*tok->t == '/') { + // Line comment + token.type = TOK_COMMENT; + tok->t++; + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + } else if (*tok->t == '*') { + // Block comment + token.type = TOK_MULTILINE_COMMENT; + tok->t++; + while (tok->t < tok->max_t) { + if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') { + tok->t += 2; + break; + } + tok->t++; + } + } else if (*tok->t == '=') { + token.type = TOK_OPERATION; + tok->t++; + } else { + token.type = TOK_OPERATION; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token c_parse_directive(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_DIRECTIVE; + + tok->t++; // skip '#' + // Skip whitespace between # and directive name + while (tok->t < tok->max_t && (*tok->t == ' ' || *tok->t == '\t')) tok->t++; + // Read directive name + while (tok->t < tok->max_t && isalpha((unsigned char)*tok->t)) tok->t++; + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Two-char operator check +static Token c_parse_operator(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_OPERATION; + + char c = *tok->t; + tok->t++; + + if (tok->t < tok->max_t) { + char n = *tok->t; + // Two-character operators + if ((c == '=' && n == '=') || (c == '!' && n == '=') || + (c == '<' && n == '=') || (c == '>' && n == '=') || + (c == '+' && n == '=') || (c == '-' && n == '=') || + (c == '*' && n == '=') || (c == '%' && n == '=') || + (c == '&' && n == '=') || (c == '|' && n == '=') || + (c == '^' && n == '=') || (c == '+' && n == '+') || + (c == '-' && n == '-') || (c == '&' && n == '&') || + (c == '|' && n == '|') || (c == '<' && n == '<') || + (c == '>' && n == '>') || (c == '-' && n == '>')) { + tok->t++; + } + } + + // Handle negative number literal: operator '-' followed by digit + // (not done here -- handled by caller context if needed) + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +//////////////////////////////// +// Main get_next_token + +static Token c_get_next_token(Tokenizer *tok) { + tokenizer_eat_whitespace(tok); + + Token token = {0}; + token.start = (S32)(tok->t - tok->buf); + token.type = TOK_DEFAULT; + + if (tok->t >= tok->max_t) { + token.len = 0; + return token; // EOF + } + + tok->start_t = tok->t; + char c = *tok->t; + + if (c_is_ident_start(c)) { + return c_parse_identifier(tok); + } + if (isdigit((unsigned char)c)) { + return c_parse_number(tok); + } + + switch (c) { + case '"': return c_parse_string(tok); + case '\'': return c_parse_char_literal(tok); + case '/': return c_parse_slash_or_comment(tok); + case '#': return c_parse_directive(tok); + + // Punctuation + case ';': case ',': case '.': + case '{': case '}': case '(': case ')': case '[': case ']': + case '\\': + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Operators + case '=': case '!': case '<': case '>': + case '+': case '-': case '*': case '%': + case '&': case '|': case '^': case '~': + case '?': case ':': + return c_parse_operator(tok); + + default: + token.type = TOK_INVALID; + tok->t++; + token.len = 1; + return token; + } +} + +//////////////////////////////// +// tokenize_c -- main entry point +// +// Tokenizes the full buffer and paints out_tokens[] with token types. +// Second pass: retroactively marks identifiers before '(' as functions. + +static void tokenize_c(const char *data, S32 length, U8 *out_tokens) { + memset(out_tokens, TOK_DEFAULT, length); + + Tokenizer tok; + tokenizer_init(&tok, data, length); + + Token prev = {0}; + prev.type = TOK_DEFAULT; + + while (tok.t < tok.max_t) { + Token token = c_get_next_token(&tok); + if (token.len == 0) break; + + paint_token(out_tokens, token.start, token.len, token.type); + + // Retroactively mark identifier before '(' as a function + if (token.type == TOK_PUNCTUATION && token.len == 1 && + data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { + paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); + } + + prev = token; + } +} diff --git a/c/lexer/lexer_go.c b/c/lexer/lexer_go.c new file mode 100644 index 00000000..69373b53 --- /dev/null +++ b/c/lexer/lexer_go.c @@ -0,0 +1,489 @@ +// lexer_go.c -- Go language tokenizer +// +// Same pattern as lexer_c.c: a get_next_token() loop that advances a cursor +// through the source, producing Token structs. After each token we paint +// the per-byte token-type array. A second pass marks identifiers before '(' +// as functions. + +#include "lexer/lexer.h" +#include +#include + +//////////////////////////////// +// Go keyword tables + +typedef struct GoKeywordEntry { + const char *word; + Token_Type type; +} GoKeywordEntry; + +static const GoKeywordEntry go_keywords[] = { + // Control-flow & language keywords + {"break", TOK_KEYWORD}, + {"case", TOK_KEYWORD}, + {"chan", TOK_KEYWORD}, + {"const", TOK_KEYWORD}, + {"continue", TOK_KEYWORD}, + {"default", TOK_KEYWORD}, + {"defer", TOK_KEYWORD}, + {"else", TOK_KEYWORD}, + {"fallthrough", TOK_KEYWORD}, + {"for", TOK_KEYWORD}, + {"func", TOK_KEYWORD}, + {"go", TOK_KEYWORD}, + {"goto", TOK_KEYWORD}, + {"if", TOK_KEYWORD}, + {"import", TOK_KEYWORD}, + {"interface", TOK_KEYWORD}, + {"map", TOK_KEYWORD}, + {"package", TOK_KEYWORD}, + {"range", TOK_KEYWORD}, + {"return", TOK_KEYWORD}, + {"select", TOK_KEYWORD}, + {"struct", TOK_KEYWORD}, + {"switch", TOK_KEYWORD}, + {"type", TOK_KEYWORD}, + {"var", TOK_KEYWORD}, + + // Built-in types + {"bool", TOK_TYPE}, + {"byte", TOK_TYPE}, + {"complex64", TOK_TYPE}, + {"complex128", TOK_TYPE}, + {"error", TOK_TYPE}, + {"float32", TOK_TYPE}, + {"float64", TOK_TYPE}, + {"int", TOK_TYPE}, + {"int8", TOK_TYPE}, + {"int16", TOK_TYPE}, + {"int32", TOK_TYPE}, + {"int64", TOK_TYPE}, + {"rune", TOK_TYPE}, + {"string", TOK_TYPE}, + {"uint", TOK_TYPE}, + {"uint8", TOK_TYPE}, + {"uint16", TOK_TYPE}, + {"uint32", TOK_TYPE}, + {"uint64", TOK_TYPE}, + {"uintptr", TOK_TYPE}, + {"any", TOK_TYPE}, + {"comparable", TOK_TYPE}, + + // Built-in values + {"true", TOK_VALUE}, + {"false", TOK_VALUE}, + {"nil", TOK_VALUE}, + {"iota", TOK_VALUE}, + + // Built-in functions (treated as modifiers for distinct coloring) + {"append", TOK_MODIFIER}, + {"cap", TOK_MODIFIER}, + {"clear", TOK_MODIFIER}, + {"close", TOK_MODIFIER}, + {"complex", TOK_MODIFIER}, + {"copy", TOK_MODIFIER}, + {"delete", TOK_MODIFIER}, + {"imag", TOK_MODIFIER}, + {"len", TOK_MODIFIER}, + {"make", TOK_MODIFIER}, + {"max", TOK_MODIFIER}, + {"min", TOK_MODIFIER}, + {"new", TOK_MODIFIER}, + {"panic", TOK_MODIFIER}, + {"print", TOK_MODIFIER}, + {"println", TOK_MODIFIER}, + {"real", TOK_MODIFIER}, + {"recover", TOK_MODIFIER}, +}; + +#define GO_KEYWORD_COUNT (S32)(sizeof(go_keywords) / sizeof(go_keywords[0])) + +static Token_Type go_lookup_keyword(const char *word, S32 len) { + for (S32 i = 0; i < GO_KEYWORD_COUNT; i++) { + const char *kw = go_keywords[i].word; + S32 kwlen = (S32)strlen(kw); + if (kwlen == len && memcmp(kw, word, len) == 0) + return go_keywords[i].type; + } + return TOK_IDENTIFIER; +} + +//////////////////////////////// +// Character helpers + +static B32 go_is_ident_start(char c) { + return isalpha((unsigned char)c) || c == '_'; +} + +static B32 go_is_ident_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +static B32 go_is_hex(char c) { + return isdigit((unsigned char)c) || + (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static B32 go_is_octal(char c) { + return c >= '0' && c <= '7'; +} + +//////////////////////////////// +// Individual token parsers + +static Token go_parse_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + const char *begin = tok->t; + while (tok->t < tok->max_t && go_is_ident_char(*tok->t)) + tok->t++; + S32 len = (S32)(tok->t - begin); + token.type = go_lookup_keyword(begin, len); + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token go_parse_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + char start_char = *tok->t; + tok->t++; + if (tok->t >= tok->max_t) goto done; + + if (start_char == '0' && tok->t < tok->max_t) { + // Hex: 0x or 0X + if (*tok->t == 'x' || *tok->t == 'X') { + tok->t++; + while (tok->t < tok->max_t && (go_is_hex(*tok->t) || *tok->t == '_')) tok->t++; + // Hex float: optional .hex_digits, optional p exponent + if (tok->t < tok->max_t && *tok->t == '.') { + tok->t++; + while (tok->t < tok->max_t && (go_is_hex(*tok->t) || *tok->t == '_')) tok->t++; + } + if (tok->t < tok->max_t && (*tok->t == 'p' || *tok->t == 'P')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++; + } + goto imaginary; + } + // Octal: 0o or 0O + if (*tok->t == 'o' || *tok->t == 'O') { + tok->t++; + while (tok->t < tok->max_t && (go_is_octal(*tok->t) || *tok->t == '_')) tok->t++; + goto imaginary; + } + // Binary: 0b or 0B + if (*tok->t == 'b' || *tok->t == 'B') { + tok->t++; + while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1' || *tok->t == '_')) tok->t++; + goto imaginary; + } + } + + // Decimal or float (also handles legacy octal like 0755) + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + + // Decimal float: .digits + if (tok->t < tok->max_t && *tok->t == '.') { + // Check next char is digit to avoid consuming '..' or method calls + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++; + } else if ((tok->t + 1) >= tok->max_t || !go_is_ident_start(*(tok->t + 1))) { + // Trailing dot like "1." -- still a float in Go + tok->t++; + } + } + + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++; + } + +imaginary: + // Imaginary suffix + if (tok->t < tok->max_t && *tok->t == 'i') + tok->t++; + +done: + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Dot-starting float: .5, .123e4 +static Token go_parse_dot_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + tok->t++; // skip '.' + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++; + } + // Imaginary + if (tok->t < tok->max_t && *tok->t == 'i') + tok->t++; + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token go_parse_string(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + B32 escape = 0; + tok->t++; // skip opening " + while (tok->t < tok->max_t && *tok->t != '\n') { + if (*tok->t == '"' && !escape) { tok->t++; break; } + escape = !escape && (*tok->t == '\\'); + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Raw string literal: `...` +static Token go_parse_raw_string(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + tok->t++; // skip opening ` + while (tok->t < tok->max_t) { + if (*tok->t == '`') { tok->t++; break; } + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Rune literal: 'x', '\n', '\u0041', etc. +static Token go_parse_rune_literal(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_CHAR_LITERAL; + + tok->t++; // skip opening ' + if (tok->t < tok->max_t && *tok->t == '\\') { + tok->t++; // skip backslash + // Consume escape sequence characters + if (tok->t < tok->max_t) { + char esc = *tok->t; + tok->t++; + if (esc == 'x') { + // \xNN + for (int i = 0; i < 2 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++; + } else if (esc == 'u') { + // \uNNNN + for (int i = 0; i < 4 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++; + } else if (esc == 'U') { + // \UNNNNNNNN + for (int i = 0; i < 8 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++; + } else if (go_is_octal(esc)) { + // \NNN + for (int i = 0; i < 2 && tok->t < tok->max_t && go_is_octal(*tok->t); i++) tok->t++; + } + // else: simple escape like \n, \t, \\, \' -- already consumed + } + } else if (tok->t < tok->max_t && *tok->t != '\n' && *tok->t != '\'') { + tok->t++; // the rune character + } + if (tok->t < tok->max_t && *tok->t == '\'') tok->t++; // closing ' + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token go_parse_slash_or_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + + tok->t++; // skip '/' + if (tok->t >= tok->max_t) { + token.type = TOK_OPERATION; + token.len = 1; + return token; + } + + if (*tok->t == '/') { + // Line comment + token.type = TOK_COMMENT; + tok->t++; + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + } else if (*tok->t == '*') { + // Block comment + token.type = TOK_MULTILINE_COMMENT; + tok->t++; + while (tok->t < tok->max_t) { + if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') { + tok->t += 2; + break; + } + tok->t++; + } + } else if (*tok->t == '=') { + // /= + token.type = TOK_OPERATION; + tok->t++; + } else { + token.type = TOK_OPERATION; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token go_parse_operator(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_OPERATION; + + char c = *tok->t; + tok->t++; + + if (tok->t < tok->max_t) { + char n = *tok->t; + // Three-character operators + if (c == '<' && n == '<' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') { + tok->t += 2; goto done; + } + if (c == '>' && n == '>' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') { + tok->t += 2; goto done; + } + if (c == '&' && n == '^' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') { + tok->t += 2; goto done; + } + // := (short variable declaration) + if (c == ':' && n == '=') { tok->t++; goto done; } + // Two-character operators + if ((c == '=' && n == '=') || (c == '!' && n == '=') || + (c == '<' && n == '=') || (c == '>' && n == '=') || + (c == '+' && n == '=') || (c == '-' && n == '=') || + (c == '*' && n == '=') || (c == '%' && n == '=') || + (c == '&' && n == '=') || (c == '|' && n == '=') || + (c == '^' && n == '=') || (c == '+' && n == '+') || + (c == '-' && n == '-') || (c == '&' && n == '&') || + (c == '|' && n == '|') || (c == '<' && n == '<') || + (c == '>' && n == '>') || (c == '-' && n == '>') || + (c == '<' && n == '-') || (c == '&' && n == '^')) { + tok->t++; + } + } + +done: + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +//////////////////////////////// +// Main get_next_token + +static Token go_get_next_token(Tokenizer *tok) { + tokenizer_eat_whitespace(tok); + + Token token = {0}; + token.start = (S32)(tok->t - tok->buf); + token.type = TOK_DEFAULT; + + if (tok->t >= tok->max_t) { + token.len = 0; + return token; // EOF + } + + tok->start_t = tok->t; + char c = *tok->t; + + if (go_is_ident_start(c)) { + return go_parse_identifier(tok); + } + if (isdigit((unsigned char)c)) { + return go_parse_number(tok); + } + + switch (c) { + case '"': return go_parse_string(tok); + case '`': return go_parse_raw_string(tok); + case '\'': return go_parse_rune_literal(tok); + case '/': return go_parse_slash_or_comment(tok); + + // Dot: could start a float literal like .5 + case '.': + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + return go_parse_dot_number(tok); + } + // Ellipsis ... or just punctuation + token.type = TOK_PUNCTUATION; + tok->t++; + if (tok->t < tok->max_t && *tok->t == '.' && + (tok->t + 1) < tok->max_t && *(tok->t + 1) == '.') { + tok->t += 2; // consume ... + } + token.len = (S32)(tok->t - tok->start_t); + return token; + + // Punctuation + case ';': case ',': + case '{': case '}': case '(': case ')': case '[': case ']': + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Operators + case '=': case '!': case '<': case '>': + case '+': case '-': case '*': case '%': + case '&': case '|': case '^': case '~': + case ':': + return go_parse_operator(tok); + + default: + token.type = TOK_INVALID; + tok->t++; + token.len = 1; + return token; + } +} + +//////////////////////////////// +// tokenize_go -- main entry point +// +// Tokenizes the full buffer and paints out_tokens[] with token types. +// Second pass: retroactively marks identifiers before '(' as functions. + +static void tokenize_go(const char *data, S32 length, U8 *out_tokens) { + memset(out_tokens, TOK_DEFAULT, length); + + Tokenizer tok; + tokenizer_init(&tok, data, length); + + Token prev = {0}; + prev.type = TOK_DEFAULT; + + while (tok.t < tok.max_t) { + Token token = go_get_next_token(&tok); + if (token.len == 0) break; + + paint_token(out_tokens, token.start, token.len, token.type); + + // Retroactively mark identifier before '(' as a function + if (token.type == TOK_PUNCTUATION && token.len == 1 && + data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { + paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); + } + + prev = token; + } +} diff --git a/c/lexer/lexer_js.c b/c/lexer/lexer_js.c new file mode 100644 index 00000000..e3fdea15 --- /dev/null +++ b/c/lexer/lexer_js.c @@ -0,0 +1,651 @@ +// lexer_js.c -- JavaScript language tokenizer +// +// Supports nested tagged template literals with ${...} interpolation. +// Uses a depth stack to track template literal nesting so that expressions +// inside ${...} can themselves contain template literals at arbitrary depth. +// +// Example: html`outer ${css`inner ${x}`} rest` +// ^^^^ ^^^^^^ ^^^ ^^^^^^ ^ ^ ^^^^^^ +// func string func str id str string + +#include "lexer/lexer.h" +#include +#include + +//////////////////////////////// +// JavaScript keyword tables + +typedef struct JSKeywordEntry { + const char *word; + Token_Type type; +} JSKeywordEntry; + +static const JSKeywordEntry js_keywords[] = { + // Control-flow & language keywords + {"break", TOK_KEYWORD}, + {"case", TOK_KEYWORD}, + {"catch", TOK_KEYWORD}, + {"class", TOK_KEYWORD}, + {"const", TOK_KEYWORD}, + {"continue", TOK_KEYWORD}, + {"debugger", TOK_KEYWORD}, + {"default", TOK_KEYWORD}, + {"delete", TOK_KEYWORD}, + {"do", TOK_KEYWORD}, + {"else", TOK_KEYWORD}, + {"extends", TOK_KEYWORD}, + {"finally", TOK_KEYWORD}, + {"for", TOK_KEYWORD}, + {"function", TOK_KEYWORD}, + {"if", TOK_KEYWORD}, + {"in", TOK_KEYWORD}, + {"instanceof", TOK_KEYWORD}, + {"let", TOK_KEYWORD}, + {"new", TOK_KEYWORD}, + {"of", TOK_KEYWORD}, + {"return", TOK_KEYWORD}, + {"switch", TOK_KEYWORD}, + {"throw", TOK_KEYWORD}, + {"try", TOK_KEYWORD}, + {"typeof", TOK_KEYWORD}, + {"var", TOK_KEYWORD}, + {"void", TOK_KEYWORD}, + {"while", TOK_KEYWORD}, + {"with", TOK_KEYWORD}, + {"yield", TOK_KEYWORD}, + {"async", TOK_KEYWORD}, + {"await", TOK_KEYWORD}, + + // Module keywords (directive-style coloring) + {"import", TOK_DIRECTIVE}, + {"export", TOK_DIRECTIVE}, + {"from", TOK_DIRECTIVE}, + {"as", TOK_DIRECTIVE}, + + // Values + {"true", TOK_VALUE}, + {"false", TOK_VALUE}, + {"null", TOK_VALUE}, + {"undefined", TOK_VALUE}, + {"NaN", TOK_VALUE}, + {"Infinity", TOK_VALUE}, + {"this", TOK_VALUE}, + {"super", TOK_VALUE}, + + // Modifiers / contextual keywords + {"static", TOK_MODIFIER}, + {"get", TOK_MODIFIER}, + {"set", TOK_MODIFIER}, +}; + +#define JS_KEYWORD_COUNT (S32)(sizeof(js_keywords) / sizeof(js_keywords[0])) + +static Token_Type js_lookup_keyword(const char *word, S32 len) { + for (S32 i = 0; i < JS_KEYWORD_COUNT; i++) { + const char *kw = js_keywords[i].word; + S32 kwlen = (S32)strlen(kw); + if (kwlen == len && memcmp(kw, word, len) == 0) + return js_keywords[i].type; + } + return TOK_IDENTIFIER; +} + +//////////////////////////////// +// Character helpers + +static B32 js_is_ident_start(char c) { + return isalpha((unsigned char)c) || c == '_' || c == '$'; +} + +static B32 js_is_ident_char(char c) { + return isalnum((unsigned char)c) || c == '_' || c == '$'; +} + +static B32 js_is_hex(char c) { + return isdigit((unsigned char)c) || + (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +//////////////////////////////// +// Individual token parsers + +static Token js_parse_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + const char *begin = tok->t; + while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) + tok->t++; + S32 len = (S32)(tok->t - begin); + token.type = js_lookup_keyword(begin, len); + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token js_parse_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + char start_char = *tok->t; + tok->t++; + if (tok->t >= tok->max_t) goto done; + + if (start_char == '0' && tok->t < tok->max_t) { + // Hex: 0x / 0X + if (*tok->t == 'x' || *tok->t == 'X') { + tok->t++; + while (tok->t < tok->max_t && (js_is_hex(*tok->t) || *tok->t == '_')) tok->t++; + if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; // BigInt + goto done; + } + // Octal: 0o / 0O + if (*tok->t == 'o' || *tok->t == 'O') { + tok->t++; + while (tok->t < tok->max_t && ((*tok->t >= '0' && *tok->t <= '7') || *tok->t == '_')) tok->t++; + if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; + goto done; + } + // Binary: 0b / 0B + if (*tok->t == 'b' || *tok->t == 'B') { + tok->t++; + while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1' || *tok->t == '_')) tok->t++; + if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; + goto done; + } + } + + // Decimal digits + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + + // Float: .digits + if (tok->t < tok->max_t && *tok->t == '.') { + tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + } + + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + } + + // BigInt suffix + if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; + +done: + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Float starting with dot: .5, .123e4 +static Token js_parse_dot_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + tok->t++; // skip '.' + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) + tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// String literal with given quote character (" or ') +static Token js_parse_string(Tokenizer *tok, char quote) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + B32 escape = 0; + tok->t++; // skip opening quote + while (tok->t < tok->max_t && *tok->t != '\n') { + if (*tok->t == quote && !escape) { tok->t++; break; } + escape = !escape && (*tok->t == '\\'); + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// / followed by / or * (comments), or /= (divide-assign), or bare / (divide) +static Token js_parse_slash_or_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + + tok->t++; // skip '/' + if (tok->t >= tok->max_t) { + token.type = TOK_OPERATION; + token.len = 1; + return token; + } + + if (*tok->t == '/') { + // Line comment + token.type = TOK_COMMENT; + tok->t++; + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + } else if (*tok->t == '*') { + // Block comment + token.type = TOK_MULTILINE_COMMENT; + tok->t++; + while (tok->t < tok->max_t) { + if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') { + tok->t += 2; + break; + } + tok->t++; + } + } else if (*tok->t == '=') { + // /= + token.type = TOK_OPERATION; + tok->t++; + } else { + // bare / (division) + token.type = TOK_OPERATION; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Regex literal: /pattern/flags +static Token js_parse_regex(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; // color regex like strings + + B32 escape = 0; + B32 in_class = 0; // inside character class [...] + tok->t++; // skip opening / + + while (tok->t < tok->max_t && *tok->t != '\n') { + if (escape) { + escape = 0; + tok->t++; + continue; + } + if (*tok->t == '\\') { + escape = 1; + tok->t++; + continue; + } + if (*tok->t == '[') { in_class = 1; tok->t++; continue; } + if (*tok->t == ']') { in_class = 0; tok->t++; continue; } + if (*tok->t == '/' && !in_class) { + tok->t++; // closing / + // Consume flags: d, g, i, m, s, u, v, y + while (tok->t < tok->max_t && isalpha((unsigned char)*tok->t)) tok->t++; + break; + } + tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Operators (handles multi-character sequences) +static Token js_parse_operator(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_OPERATION; + + char c = *tok->t; + tok->t++; + + if (tok->t < tok->max_t) { + char n = *tok->t; + + // Three-character (and four-character) operators + if ((tok->t + 1) < tok->max_t) { + char nn = *(tok->t + 1); + // >>> and >>>= + if (c == '>' && n == '>' && nn == '>') { + tok->t += 2; + if (tok->t < tok->max_t && *tok->t == '=') tok->t++; + goto done; + } + // === + if (c == '=' && n == '=' && nn == '=') { tok->t += 2; goto done; } + // !== + if (c == '!' && n == '=' && nn == '=') { tok->t += 2; goto done; } + // **= + if (c == '*' && n == '*' && nn == '=') { tok->t += 2; goto done; } + // <<= + if (c == '<' && n == '<' && nn == '=') { tok->t += 2; goto done; } + // >>= + if (c == '>' && n == '>' && nn == '=') { tok->t += 2; goto done; } + // &&= + if (c == '&' && n == '&' && nn == '=') { tok->t += 2; goto done; } + // ||= + if (c == '|' && n == '|' && nn == '=') { tok->t += 2; goto done; } + // ??= + if (c == '?' && n == '?' && nn == '=') { tok->t += 2; goto done; } + } + + // Two-character operators + if ((c == '=' && n == '=') || (c == '!' && n == '=') || + (c == '<' && n == '=') || (c == '>' && n == '=') || + (c == '+' && n == '=') || (c == '-' && n == '=') || + (c == '*' && n == '=') || (c == '%' && n == '=') || + (c == '&' && n == '=') || (c == '|' && n == '=') || + (c == '^' && n == '=') || (c == '+' && n == '+') || + (c == '-' && n == '-') || (c == '&' && n == '&') || + (c == '|' && n == '|') || (c == '<' && n == '<') || + (c == '>' && n == '>') || (c == '=' && n == '>') || + (c == '*' && n == '*') || (c == '?' && n == '?') || + (c == '?' && n == '.')) { + tok->t++; + } + } + +done: + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +//////////////////////////////// +// Regex vs division heuristic +// +// After identifiers, numbers, values, strings, ) and ] -- slash is division. +// After keywords, operators, most punctuation, start of file -- slash is regex. + +static B32 js_slash_is_regex(Token prev, const char *data) { + switch (prev.type) { + case TOK_DEFAULT: return 1; // start of file / no previous token + case TOK_KEYWORD: return 1; // e.g. return /regex/ + case TOK_DIRECTIVE: return 1; // after import/export + case TOK_OPERATION: + // After ++ or --, it's division (x++ / y) + if (prev.len == 2) { + char c0 = data[prev.start]; + char c1 = data[prev.start + 1]; + if ((c0 == '+' && c1 == '+') || (c0 == '-' && c1 == '-')) + return 0; + } + return 1; + case TOK_PUNCTUATION: + // After ) or ], it's division + if (prev.len == 1) { + char c = data[prev.start]; + if (c == ')' || c == ']') return 0; + } + return 1; + case TOK_IDENTIFIER: + case TOK_FUNCTION: + case TOK_NUMBER: + case TOK_STRING_LITERAL: + case TOK_CHAR_LITERAL: + case TOK_VALUE: + return 0; // division + default: + return 1; + } +} + +//////////////////////////////// +// Template literal content scanner +// +// Scans the string content inside a template literal (the text between +// backticks, or between a closing } and the next ${ or closing backtick). +// Paints each byte as TOK_STRING_LITERAL. +// +// Returns 1 if we hit ${ (caller should enter expression mode), +// 0 if we hit closing ` or EOF (template is done). + +static int js_scan_template_content(Tokenizer *tok, U8 *out_tokens) { + while (tok->t < tok->max_t) { + if (*tok->t == '`') { + // Closing backtick -- template done + S32 pos = (S32)(tok->t - tok->buf); + out_tokens[pos] = TOK_STRING_LITERAL; + tok->t++; + return 0; + } + if (*tok->t == '$' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '{') { + // Interpolation start -- paint ${ as punctuation + S32 pos = (S32)(tok->t - tok->buf); + out_tokens[pos] = TOK_PUNCTUATION; + out_tokens[pos + 1] = TOK_PUNCTUATION; + tok->t += 2; + return 1; + } + if (*tok->t == '\\' && (tok->t + 1) < tok->max_t) { + // Escape sequence -- paint both chars as string + S32 pos = (S32)(tok->t - tok->buf); + out_tokens[pos] = TOK_STRING_LITERAL; + out_tokens[pos + 1] = TOK_STRING_LITERAL; + tok->t += 2; + continue; + } + // Regular string character + S32 pos = (S32)(tok->t - tok->buf); + out_tokens[pos] = TOK_STRING_LITERAL; + tok->t++; + } + return 0; // EOF -- unclosed template +} + +//////////////////////////////// +// Get next non-template token +// +// Called by the main loop for normal expression parsing. Template backticks +// and interpolation-closing braces are handled directly by the main loop +// before this function is reached. + +static Token js_get_next_token(Tokenizer *tok, Token prev, const char *data) { + tokenizer_eat_whitespace(tok); + + Token token = {0}; + token.start = (S32)(tok->t - tok->buf); + token.type = TOK_DEFAULT; + + if (tok->t >= tok->max_t) { + token.len = 0; + return token; + } + + tok->start_t = tok->t; + char c = *tok->t; + + if (js_is_ident_start(c)) { + return js_parse_identifier(tok); + } + if (isdigit((unsigned char)c)) { + return js_parse_number(tok); + } + + switch (c) { + case '"': return js_parse_string(tok, '"'); + case '\'': return js_parse_string(tok, '\''); + + case '/': + // Check for comment first (// or /*) + if ((tok->t + 1) < tok->max_t) { + char n = *(tok->t + 1); + if (n == '/' || n == '*') + return js_parse_slash_or_comment(tok); + } + // Regex or division based on context + if (js_slash_is_regex(prev, data)) + return js_parse_regex(tok); + return js_parse_slash_or_comment(tok); + + // Dot: float literal (.5), spread (...), or member access + case '.': + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + return js_parse_dot_number(tok); + } + if ((tok->t + 2) < tok->max_t && *(tok->t + 1) == '.' && *(tok->t + 2) == '.') { + token.type = TOK_OPERATION; + tok->t += 3; + token.len = 3; + return token; + } + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Punctuation + case ';': case ',': + case '(': case ')': case '[': case ']': + case '{': case '}': + case ':': + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Operators + case '=': case '!': case '<': case '>': + case '+': case '-': case '*': case '%': + case '&': case '|': case '^': case '~': + case '?': + return js_parse_operator(tok); + + // Private class fields: #name + case '#': + token.type = TOK_IDENTIFIER; + tok->t++; + while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++; + token.len = (S32)(tok->t - tok->start_t); + return token; + + // Decorators: @name + case '@': + token.type = TOK_DIRECTIVE; + tok->t++; + while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++; + token.len = (S32)(tok->t - tok->start_t); + return token; + + default: + token.type = TOK_INVALID; + tok->t++; + token.len = 1; + return token; + } +} + +//////////////////////////////// +// tokenize_js -- main entry point +// +// Tokenizes the full buffer and paints out_tokens[] with token types. +// +// Template literal nesting is tracked with a depth stack. Each level +// records how many unmatched { braces exist in the current interpolation +// expression. When a } is encountered and the brace count is zero, it +// closes the interpolation and we resume scanning template string content. +// +// html`text ${obj.x} more ${css`inner ${y}`} end` +// |str ||expr | str || |str || || str| +// ^punc ^punc ^punc ^ ^^ +// ${ } ${ } }` (nesting!) + +#define JS_MAX_TEMPLATE_DEPTH 32 + +static void tokenize_js(const char *data, S32 length, U8 *out_tokens) { + memset(out_tokens, TOK_DEFAULT, length); + + Tokenizer tok; + tokenizer_init(&tok, data, length); + + S32 tmpl_depth = 0; + S32 brace_count[JS_MAX_TEMPLATE_DEPTH]; + memset(brace_count, 0, sizeof(brace_count)); + + Token prev = {0}; + prev.type = TOK_DEFAULT; + + while (tok.t < tok.max_t) { + tokenizer_eat_whitespace(&tok); + if (tok.t >= tok.max_t) break; + + char c = *tok.t; + + // ---- Template literal: opening backtick ---- + // This handles both top-level template literals and nested ones + // (e.g. a tagged template inside a ${...} interpolation). + if (c == '`') { + S32 pos = (S32)(tok.t - tok.buf); + out_tokens[pos] = TOK_STRING_LITERAL; + tok.t++; + + int result = js_scan_template_content(&tok, out_tokens); + if (result == 1) { + // Hit ${ -- push template depth + if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) { + brace_count[tmpl_depth] = 0; + tmpl_depth++; + } + } + // result == 0: self-contained template (no interpolation, or + // all interpolations already resolved recursively) + + prev.type = TOK_STRING_LITERAL; + prev.start = pos; + prev.len = (S32)(tok.t - tok.buf) - pos; + continue; + } + + // ---- Closing brace that ends a template interpolation ---- + // When we're inside a template expression and the brace count is + // zero, this } closes the ${...} and we resume string scanning. + if (c == '}' && tmpl_depth > 0 && brace_count[tmpl_depth - 1] == 0) { + S32 pos = (S32)(tok.t - tok.buf); + out_tokens[pos] = TOK_PUNCTUATION; + tok.t++; + tmpl_depth--; + + // Resume template string content + int result = js_scan_template_content(&tok, out_tokens); + if (result == 1) { + // Hit another ${ -- re-enter expression mode + if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) { + brace_count[tmpl_depth] = 0; + tmpl_depth++; + } + } + // result == 0: template closed with backtick + + prev.type = TOK_STRING_LITERAL; + prev.start = pos; + prev.len = (S32)(tok.t - tok.buf) - pos; + continue; + } + + // ---- Normal token ---- + tok.start_t = tok.t; + Token token = js_get_next_token(&tok, prev, data); + if (token.len == 0) break; + + paint_token(out_tokens, token.start, token.len, token.type); + + // Track brace depth for template interpolation + if (tmpl_depth > 0 && token.type == TOK_PUNCTUATION && token.len == 1) { + char tc = data[token.start]; + if (tc == '{') brace_count[tmpl_depth - 1]++; + else if (tc == '}') brace_count[tmpl_depth - 1]--; + } + + // Retroactively mark identifier before '(' as function + if (token.type == TOK_PUNCTUATION && token.len == 1 && + data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { + paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); + } + + prev = token; + } +} diff --git a/c/lexer/lexer_lua.c b/c/lexer/lexer_lua.c new file mode 100644 index 00000000..0c9ac758 --- /dev/null +++ b/c/lexer/lexer_lua.c @@ -0,0 +1,422 @@ +// lexer_lua.c -- Lua language tokenizer +// +// Same pattern as lexer_go.c: a get_next_token() loop that advances a cursor +// through the source, producing Token structs. After each token we paint +// the per-byte token-type array. A second pass marks identifiers before '(' +// as functions. + +#include "lexer/lexer.h" +#include +#include + +//////////////////////////////// +// Lua keyword tables + +typedef struct LuaKeywordEntry { + const char *word; + Token_Type type; +} LuaKeywordEntry; + +static const LuaKeywordEntry lua_keywords[] = { + // Keywords + {"and", TOK_KEYWORD}, + {"break", TOK_KEYWORD}, + {"do", TOK_KEYWORD}, + {"else", TOK_KEYWORD}, + {"elseif", TOK_KEYWORD}, + {"end", TOK_KEYWORD}, + {"for", TOK_KEYWORD}, + {"function", TOK_KEYWORD}, + {"goto", TOK_KEYWORD}, + {"if", TOK_KEYWORD}, + {"in", TOK_KEYWORD}, + {"local", TOK_KEYWORD}, + {"not", TOK_KEYWORD}, + {"or", TOK_KEYWORD}, + {"repeat", TOK_KEYWORD}, + {"return", TOK_KEYWORD}, + {"then", TOK_KEYWORD}, + {"until", TOK_KEYWORD}, + {"while", TOK_KEYWORD}, + + // Values + {"true", TOK_VALUE}, + {"false", TOK_VALUE}, + {"nil", TOK_VALUE}, + + // Built-in globals / types (use TOK_TYPE for distinct coloring) + {"self", TOK_TYPE}, + + // Built-in functions (TOK_MODIFIER for distinct coloring) + {"assert", TOK_MODIFIER}, + {"collectgarbage", TOK_MODIFIER}, + {"dofile", TOK_MODIFIER}, + {"error", TOK_MODIFIER}, + {"getmetatable", TOK_MODIFIER}, + {"ipairs", TOK_MODIFIER}, + {"load", TOK_MODIFIER}, + {"loadfile", TOK_MODIFIER}, + {"next", TOK_MODIFIER}, + {"pairs", TOK_MODIFIER}, + {"pcall", TOK_MODIFIER}, + {"print", TOK_MODIFIER}, + {"rawequal", TOK_MODIFIER}, + {"rawget", TOK_MODIFIER}, + {"rawlen", TOK_MODIFIER}, + {"rawset", TOK_MODIFIER}, + {"require", TOK_MODIFIER}, + {"select", TOK_MODIFIER}, + {"setmetatable", TOK_MODIFIER}, + {"tonumber", TOK_MODIFIER}, + {"tostring", TOK_MODIFIER}, + {"type", TOK_MODIFIER}, + {"unpack", TOK_MODIFIER}, + {"xpcall", TOK_MODIFIER}, +}; + +#define LUA_KEYWORD_COUNT (S32)(sizeof(lua_keywords) / sizeof(lua_keywords[0])) + +static Token_Type lua_lookup_keyword(const char *word, S32 len) { + for (S32 i = 0; i < LUA_KEYWORD_COUNT; i++) { + const char *kw = lua_keywords[i].word; + S32 kwlen = (S32)strlen(kw); + if (kwlen == len && memcmp(kw, word, len) == 0) + return lua_keywords[i].type; + } + return TOK_IDENTIFIER; +} + +//////////////////////////////// +// Character helpers + +static B32 lua_is_ident_start(char c) { + return isalpha((unsigned char)c) || c == '_'; +} + +static B32 lua_is_ident_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +static B32 lua_is_hex(char c) { + return isdigit((unsigned char)c) || + (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +//////////////////////////////// +// Long bracket level detection +// Returns the level (number of '=' signs) if we're at a long bracket opening +// like [[ or [==[ etc. Returns -1 if not a long bracket. + +static S32 lua_long_bracket_level(const char *p, const char *max_p) { + if (p >= max_p || *p != '[') return -1; + p++; + S32 level = 0; + while (p < max_p && *p == '=') { level++; p++; } + if (p < max_p && *p == '[') return level; + return -1; +} + +// Scan past the closing long bracket of the given level. +// Cursor should be right after the opening [=*[. +static void lua_scan_long_bracket_close(Tokenizer *tok, S32 level) { + while (tok->t < tok->max_t) { + if (*tok->t == ']') { + const char *p = tok->t + 1; + S32 count = 0; + while (p < tok->max_t && *p == '=' && count < level) { count++; p++; } + if (count == level && p < tok->max_t && *p == ']') { + tok->t = p + 1; + return; + } + } + tok->t++; + } +} + +//////////////////////////////// +// Individual token parsers + +static Token lua_parse_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + const char *begin = tok->t; + while (tok->t < tok->max_t && lua_is_ident_char(*tok->t)) + tok->t++; + S32 len = (S32)(tok->t - begin); + token.type = lua_lookup_keyword(begin, len); + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + char c = *tok->t; + tok->t++; + + if (c == '0' && tok->t < tok->max_t && (*tok->t == 'x' || *tok->t == 'X')) { + // Hex literal + tok->t++; + while (tok->t < tok->max_t && lua_is_hex(*tok->t)) tok->t++; + // Hex float + if (tok->t < tok->max_t && *tok->t == '.') { + tok->t++; + while (tok->t < tok->max_t && lua_is_hex(*tok->t)) tok->t++; + } + // Hex exponent (p/P) + if (tok->t < tok->max_t && (*tok->t == 'p' || *tok->t == 'P')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + } else { + // Decimal + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + // Float + if (tok->t < tok->max_t && *tok->t == '.') { + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } else if ((tok->t + 1) >= tok->max_t || !lua_is_ident_start(*(tok->t + 1))) { + tok->t++; + } + } + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_dot_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + tok->t++; // skip '.' + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_string(Tokenizer *tok, char quote) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + B32 escape = 0; + tok->t++; // skip opening quote + while (tok->t < tok->max_t && *tok->t != '\n') { + if (*tok->t == quote && !escape) { tok->t++; break; } + escape = !escape && (*tok->t == '\\'); + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_long_string(Tokenizer *tok, S32 level) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + // Skip past opening [=*[ + tok->t++; // first [ + tok->t += level; // = signs + tok->t++; // second [ + + lua_scan_long_bracket_close(tok, level); + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + + tok->t += 2; // skip '--' + + // Check for long comment --[=*[ + S32 level = lua_long_bracket_level(tok->t, tok->max_t); + if (level >= 0) { + token.type = TOK_MULTILINE_COMMENT; + tok->t++; // first [ + tok->t += level; // = signs + tok->t++; // second [ + lua_scan_long_bracket_close(tok, level); + } else { + // Single-line comment + token.type = TOK_COMMENT; + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token lua_parse_operator(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_OPERATION; + + char c = *tok->t; + tok->t++; + + if (tok->t < tok->max_t) { + char n = *tok->t; + // Two-character operators + if ((c == '=' && n == '=') || (c == '~' && n == '=') || + (c == '<' && n == '=') || (c == '>' && n == '=') || + (c == '.' && n == '.') || (c == '<' && n == '<') || + (c == '>' && n == '>') || (c == '/' && n == '/')) { + tok->t++; + // Three-character: .. can be followed by . to make ... + if (c == '.' && n == '.' && tok->t < tok->max_t && *tok->t == '.') { + tok->t++; + } + } + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +//////////////////////////////// +// Main get_next_token + +static Token lua_get_next_token(Tokenizer *tok) { + tokenizer_eat_whitespace(tok); + + Token token = {0}; + token.start = (S32)(tok->t - tok->buf); + token.type = TOK_DEFAULT; + + if (tok->t >= tok->max_t) { + token.len = 0; + return token; + } + + tok->start_t = tok->t; + char c = *tok->t; + + if (lua_is_ident_start(c)) { + return lua_parse_identifier(tok); + } + if (isdigit((unsigned char)c)) { + return lua_parse_number(tok); + } + + switch (c) { + case '\'': + case '"': + return lua_parse_string(tok, c); + + case '[': { + // Check for long string [=*[ + S32 level = lua_long_bracket_level(tok->t, tok->max_t); + if (level >= 0) { + return lua_parse_long_string(tok, level); + } + // Plain bracket punctuation + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + } + + case '-': + // Check for comment -- + if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '-') { + return lua_parse_comment(tok); + } + // Minus operator + token.type = TOK_OPERATION; + tok->t++; + token.len = 1; + return token; + + case '.': + // .digits = float literal + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + return lua_parse_dot_number(tok); + } + // .. or ... or just . + return lua_parse_operator(tok); + + // Punctuation + case ';': case ',': case ':': + case '{': case '}': case '(': case ')': case ']': + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Directive: shebang or labels (::label::) + case '#': + // Shebang line or length operator + if (tok->t == tok->buf && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '!') { + token.type = TOK_DIRECTIVE; + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + token.len = (S32)(tok->t - tok->start_t); + return token; + } + token.type = TOK_OPERATION; + tok->t++; + token.len = 1; + return token; + + // Operators + case '=': case '~': case '<': case '>': + case '+': case '*': case '/': case '%': + case '^': case '&': case '|': + return lua_parse_operator(tok); + + default: + token.type = TOK_INVALID; + tok->t++; + token.len = 1; + return token; + } +} + +//////////////////////////////// +// tokenize_lua -- main entry point + +static void tokenize_lua(const char *data, S32 length, U8 *out_tokens) { + memset(out_tokens, TOK_DEFAULT, length); + + Tokenizer tok; + tokenizer_init(&tok, data, length); + + Token prev = {0}; + prev.type = TOK_DEFAULT; + + while (tok.t < tok.max_t) { + Token token = lua_get_next_token(&tok); + if (token.len == 0) break; + + paint_token(out_tokens, token.start, token.len, token.type); + + // Retroactively mark identifier before '(' as a function + if (token.type == TOK_PUNCTUATION && token.len == 1 && + data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { + paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); + } + + prev = token; + } +} diff --git a/c/lexer/lexer_sql.c b/c/lexer/lexer_sql.c new file mode 100644 index 00000000..4f9f7b32 --- /dev/null +++ b/c/lexer/lexer_sql.c @@ -0,0 +1,645 @@ +// lexer_sql.c -- SQL language tokenizer +// +// Same pattern as lexer_go.c: a get_next_token() loop that advances a cursor +// through the source, producing Token structs. After each token we paint +// the per-byte token-type array. A second pass marks identifiers before '(' +// as functions. +// +// SQL keywords are case-insensitive, so comparisons use a case-insensitive +// match. Covers standard SQL plus common extensions (MySQL, PostgreSQL, etc.). + +#include "lexer/lexer.h" +#include +#include + +//////////////////////////////// +// SQL keyword tables + +typedef struct SQLKeywordEntry { + const char *word; + Token_Type type; +} SQLKeywordEntry; + +static const SQLKeywordEntry sql_keywords[] = { + // DML / DQL keywords + {"SELECT", TOK_KEYWORD}, + {"FROM", TOK_KEYWORD}, + {"WHERE", TOK_KEYWORD}, + {"INSERT", TOK_KEYWORD}, + {"INTO", TOK_KEYWORD}, + {"UPDATE", TOK_KEYWORD}, + {"DELETE", TOK_KEYWORD}, + {"SET", TOK_KEYWORD}, + {"VALUES", TOK_KEYWORD}, + {"AS", TOK_KEYWORD}, + {"ON", TOK_KEYWORD}, + {"JOIN", TOK_KEYWORD}, + {"INNER", TOK_KEYWORD}, + {"LEFT", TOK_KEYWORD}, + {"RIGHT", TOK_KEYWORD}, + {"OUTER", TOK_KEYWORD}, + {"FULL", TOK_KEYWORD}, + {"CROSS", TOK_KEYWORD}, + {"NATURAL", TOK_KEYWORD}, + {"USING", TOK_KEYWORD}, + {"ORDER", TOK_KEYWORD}, + {"BY", TOK_KEYWORD}, + {"GROUP", TOK_KEYWORD}, + {"HAVING", TOK_KEYWORD}, + {"LIMIT", TOK_KEYWORD}, + {"OFFSET", TOK_KEYWORD}, + {"UNION", TOK_KEYWORD}, + {"ALL", TOK_KEYWORD}, + {"DISTINCT", TOK_KEYWORD}, + {"TOP", TOK_KEYWORD}, + {"FETCH", TOK_KEYWORD}, + {"NEXT", TOK_KEYWORD}, + {"ROWS", TOK_KEYWORD}, + {"ONLY", TOK_KEYWORD}, + {"FIRST", TOK_KEYWORD}, + {"LAST", TOK_KEYWORD}, + + // DDL keywords + {"CREATE", TOK_KEYWORD}, + {"ALTER", TOK_KEYWORD}, + {"DROP", TOK_KEYWORD}, + {"TABLE", TOK_KEYWORD}, + {"VIEW", TOK_KEYWORD}, + {"INDEX", TOK_KEYWORD}, + {"DATABASE", TOK_KEYWORD}, + {"SCHEMA", TOK_KEYWORD}, + {"COLUMN", TOK_KEYWORD}, + {"ADD", TOK_KEYWORD}, + {"RENAME", TOK_KEYWORD}, + {"TRUNCATE", TOK_KEYWORD}, + {"REPLACE", TOK_KEYWORD}, + {"TEMPORARY", TOK_KEYWORD}, + {"TEMP", TOK_KEYWORD}, + {"IF", TOK_KEYWORD}, + {"EXISTS", TOK_KEYWORD}, + {"CASCADE", TOK_KEYWORD}, + {"RESTRICT", TOK_KEYWORD}, + + // Constraints & keys + {"PRIMARY", TOK_KEYWORD}, + {"KEY", TOK_KEYWORD}, + {"FOREIGN", TOK_KEYWORD}, + {"REFERENCES", TOK_KEYWORD}, + {"UNIQUE", TOK_KEYWORD}, + {"CHECK", TOK_KEYWORD}, + {"CONSTRAINT", TOK_KEYWORD}, + {"DEFAULT", TOK_KEYWORD}, + {"AUTOINCREMENT", TOK_KEYWORD}, + {"AUTO_INCREMENT", TOK_KEYWORD}, + {"IDENTITY", TOK_KEYWORD}, + + // Control flow / procedural + {"BEGIN", TOK_KEYWORD}, + {"END", TOK_KEYWORD}, + {"COMMIT", TOK_KEYWORD}, + {"ROLLBACK", TOK_KEYWORD}, + {"SAVEPOINT", TOK_KEYWORD}, + {"TRANSACTION", TOK_KEYWORD}, + {"RETURN", TOK_KEYWORD}, + {"RETURNS", TOK_KEYWORD}, + {"DECLARE", TOK_KEYWORD}, + {"CURSOR", TOK_KEYWORD}, + {"OPEN", TOK_KEYWORD}, + {"CLOSE", TOK_KEYWORD}, + {"DEALLOCATE", TOK_KEYWORD}, + {"EXEC", TOK_KEYWORD}, + {"EXECUTE", TOK_KEYWORD}, + {"CALL", TOK_KEYWORD}, + {"PROCEDURE", TOK_KEYWORD}, + {"FUNCTION", TOK_KEYWORD}, + {"TRIGGER", TOK_KEYWORD}, + {"CASE", TOK_KEYWORD}, + {"WHEN", TOK_KEYWORD}, + {"THEN", TOK_KEYWORD}, + {"ELSE", TOK_KEYWORD}, + {"WHILE", TOK_KEYWORD}, + {"LOOP", TOK_KEYWORD}, + {"FOR", TOK_KEYWORD}, + {"EACH", TOK_KEYWORD}, + {"ROW", TOK_KEYWORD}, + {"AFTER", TOK_KEYWORD}, + {"BEFORE", TOK_KEYWORD}, + {"INSTEAD", TOK_KEYWORD}, + {"OF", TOK_KEYWORD}, + + // Logical operators (keywords) + {"AND", TOK_KEYWORD}, + {"OR", TOK_KEYWORD}, + {"NOT", TOK_KEYWORD}, + {"IN", TOK_KEYWORD}, + {"BETWEEN", TOK_KEYWORD}, + {"LIKE", TOK_KEYWORD}, + {"ILIKE", TOK_KEYWORD}, + {"IS", TOK_KEYWORD}, + {"ANY", TOK_KEYWORD}, + {"SOME", TOK_KEYWORD}, + {"EXCEPT", TOK_KEYWORD}, + {"INTERSECT", TOK_KEYWORD}, + {"WITH", TOK_KEYWORD}, + {"RECURSIVE", TOK_KEYWORD}, + {"OVER", TOK_KEYWORD}, + {"PARTITION", TOK_KEYWORD}, + {"WINDOW", TOK_KEYWORD}, + {"LATERAL", TOK_KEYWORD}, + + // Misc + {"GRANT", TOK_KEYWORD}, + {"REVOKE", TOK_KEYWORD}, + {"EXPLAIN", TOK_KEYWORD}, + {"ANALYZE", TOK_KEYWORD}, + {"VACUUM", TOK_KEYWORD}, + {"PRAGMA", TOK_KEYWORD}, + {"DESCRIBE", TOK_KEYWORD}, + {"SHOW", TOK_KEYWORD}, + {"USE", TOK_KEYWORD}, + {"COPY", TOK_KEYWORD}, + {"PERFORM", TOK_KEYWORD}, + {"RAISE", TOK_KEYWORD}, + + // Modifiers + {"ASC", TOK_MODIFIER}, + {"DESC", TOK_MODIFIER}, + {"NULLS", TOK_MODIFIER}, + {"NOT", TOK_MODIFIER}, + + // Data types + {"INT", TOK_TYPE}, + {"INTEGER", TOK_TYPE}, + {"SMALLINT", TOK_TYPE}, + {"BIGINT", TOK_TYPE}, + {"TINYINT", TOK_TYPE}, + {"MEDIUMINT", TOK_TYPE}, + {"SERIAL", TOK_TYPE}, + {"BIGSERIAL", TOK_TYPE}, + {"FLOAT", TOK_TYPE}, + {"REAL", TOK_TYPE}, + {"DOUBLE", TOK_TYPE}, + {"DECIMAL", TOK_TYPE}, + {"NUMERIC", TOK_TYPE}, + {"PRECISION", TOK_TYPE}, + {"CHAR", TOK_TYPE}, + {"VARCHAR", TOK_TYPE}, + {"TEXT", TOK_TYPE}, + {"NCHAR", TOK_TYPE}, + {"NVARCHAR", TOK_TYPE}, + {"NTEXT", TOK_TYPE}, + {"BLOB", TOK_TYPE}, + {"CLOB", TOK_TYPE}, + {"BYTEA", TOK_TYPE}, + {"BOOLEAN", TOK_TYPE}, + {"BOOL", TOK_TYPE}, + {"DATE", TOK_TYPE}, + {"TIME", TOK_TYPE}, + {"DATETIME", TOK_TYPE}, + {"TIMESTAMP", TOK_TYPE}, + {"TIMESTAMPTZ", TOK_TYPE}, + {"INTERVAL", TOK_TYPE}, + {"UUID", TOK_TYPE}, + {"JSON", TOK_TYPE}, + {"JSONB", TOK_TYPE}, + {"XML", TOK_TYPE}, + {"ARRAY", TOK_TYPE}, + {"ENUM", TOK_TYPE}, + {"MONEY", TOK_TYPE}, + {"BIT", TOK_TYPE}, + {"VARBIT", TOK_TYPE}, + {"INET", TOK_TYPE}, + {"CIDR", TOK_TYPE}, + {"MACADDR", TOK_TYPE}, + {"POINT", TOK_TYPE}, + {"LINE", TOK_TYPE}, + {"POLYGON", TOK_TYPE}, + {"GEOMETRY", TOK_TYPE}, + {"GEOGRAPHY", TOK_TYPE}, + + // Values + {"NULL", TOK_VALUE}, + {"TRUE", TOK_VALUE}, + {"FALSE", TOK_VALUE}, + {"CURRENT_DATE", TOK_VALUE}, + {"CURRENT_TIME", TOK_VALUE}, + {"CURRENT_TIMESTAMP", TOK_VALUE}, + {"CURRENT_USER", TOK_VALUE}, + + // Built-in aggregate / window / common functions (TOK_MODIFIER) + {"COUNT", TOK_MODIFIER}, + {"SUM", TOK_MODIFIER}, + {"AVG", TOK_MODIFIER}, + {"MIN", TOK_MODIFIER}, + {"MAX", TOK_MODIFIER}, + {"COALESCE", TOK_MODIFIER}, + {"NULLIF", TOK_MODIFIER}, + {"CAST", TOK_MODIFIER}, + {"CONVERT", TOK_MODIFIER}, + {"IFNULL", TOK_MODIFIER}, + {"ISNULL", TOK_MODIFIER}, + {"NVL", TOK_MODIFIER}, + {"ROW_NUMBER", TOK_MODIFIER}, + {"RANK", TOK_MODIFIER}, + {"DENSE_RANK", TOK_MODIFIER}, + {"NTILE", TOK_MODIFIER}, + {"LAG", TOK_MODIFIER}, + {"LEAD", TOK_MODIFIER}, + {"FIRST_VALUE", TOK_MODIFIER}, + {"LAST_VALUE", TOK_MODIFIER}, + {"SUBSTR", TOK_MODIFIER}, + {"SUBSTRING", TOK_MODIFIER}, + {"TRIM", TOK_MODIFIER}, + {"UPPER", TOK_MODIFIER}, + {"LOWER", TOK_MODIFIER}, + {"LENGTH", TOK_MODIFIER}, + {"CONCAT", TOK_MODIFIER}, + {"REPLACE", TOK_MODIFIER}, + {"ABS", TOK_MODIFIER}, + {"ROUND", TOK_MODIFIER}, + {"CEIL", TOK_MODIFIER}, + {"FLOOR", TOK_MODIFIER}, + {"NOW", TOK_MODIFIER}, + {"EXTRACT", TOK_MODIFIER}, + {"STRING_AGG", TOK_MODIFIER}, + {"GROUP_CONCAT", TOK_MODIFIER}, + {"ARRAY_AGG", TOK_MODIFIER}, + {"GREATEST", TOK_MODIFIER}, + {"LEAST", TOK_MODIFIER}, +}; + +#define SQL_KEYWORD_COUNT (S32)(sizeof(sql_keywords) / sizeof(sql_keywords[0])) + +// Case-insensitive keyword lookup +static Token_Type sql_lookup_keyword(const char *word, S32 len) { + for (S32 i = 0; i < SQL_KEYWORD_COUNT; i++) { + const char *kw = sql_keywords[i].word; + S32 kwlen = (S32)strlen(kw); + if (kwlen != len) continue; + B32 match = 1; + for (S32 j = 0; j < len; j++) { + if (toupper((unsigned char)word[j]) != (unsigned char)kw[j]) { + match = 0; + break; + } + } + if (match) return sql_keywords[i].type; + } + return TOK_IDENTIFIER; +} + +//////////////////////////////// +// Character helpers + +static B32 sql_is_ident_start(char c) { + return isalpha((unsigned char)c) || c == '_'; +} + +static B32 sql_is_ident_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +//////////////////////////////// +// Individual token parsers + +static Token sql_parse_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + const char *begin = tok->t; + while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) + tok->t++; + S32 len = (S32)(tok->t - begin); + token.type = sql_lookup_keyword(begin, len); + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token sql_parse_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) + tok->t++; + + // Decimal part + if (tok->t < tok->max_t && *tok->t == '.') { + tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) + tok->t++; + } + + // Exponent + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token sql_parse_dot_number(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_NUMBER; + + tok->t++; // skip '.' + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { + tok->t++; + if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Single-quoted string: 'text', with '' as escape for embedded quote +static Token sql_parse_string(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_STRING_LITERAL; + + tok->t++; // skip opening ' + while (tok->t < tok->max_t) { + if (*tok->t == '\'') { + tok->t++; + // '' is an escaped quote inside a string + if (tok->t < tok->max_t && *tok->t == '\'') { + tok->t++; + continue; + } + break; + } + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Double-quoted identifier: "column_name" +static Token sql_parse_quoted_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_IDENTIFIER; + + tok->t++; // skip opening " + while (tok->t < tok->max_t) { + if (*tok->t == '"') { + tok->t++; + // "" is an escaped quote inside a quoted identifier + if (tok->t < tok->max_t && *tok->t == '"') { + tok->t++; + continue; + } + break; + } + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Backtick-quoted identifier (MySQL): `column_name` +static Token sql_parse_backtick_identifier(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_IDENTIFIER; + + tok->t++; // skip opening ` + while (tok->t < tok->max_t) { + if (*tok->t == '`') { tok->t++; break; } + tok->t++; + } + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Line comment: -- ... +static Token sql_parse_line_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_COMMENT; + + tok->t += 2; // skip '--' + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Block comment: /* ... */ +static Token sql_parse_block_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_MULTILINE_COMMENT; + + tok->t += 2; // skip '/*' + while (tok->t < tok->max_t) { + if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') { + tok->t += 2; + break; + } + tok->t++; + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// MySQL # comment +static Token sql_parse_hash_comment(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_COMMENT; + + tok->t++; // skip '#' + while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +// Variable / parameter: @var, @@var, :param, $1 +static Token sql_parse_variable(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_DIRECTIVE; + + char c = *tok->t; + tok->t++; + + if (c == '@') { + // @@ for system variables + if (tok->t < tok->max_t && *tok->t == '@') tok->t++; + while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++; + } else if (c == ':') { + while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++; + } else if (c == '$') { + // $1, $2, ... (PostgreSQL positional params) + while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; + // Also handle $tag$ dollar-quoted strings if no digits follow + if (tok->t == tok->start_t + 1) { + // Just a lone $, treat as punctuation + token.type = TOK_PUNCTUATION; + } + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +static Token sql_parse_operator(Tokenizer *tok) { + Token token; + token.start = (S32)(tok->start_t - tok->buf); + token.type = TOK_OPERATION; + + char c = *tok->t; + tok->t++; + + if (tok->t < tok->max_t) { + char n = *tok->t; + // Two-character operators + if ((c == '<' && n == '=') || (c == '>' && n == '=') || + (c == '<' && n == '>') || (c == '!' && n == '=') || + (c == '|' && n == '|') || (c == ':' && n == ':')) { + tok->t++; + } + } + + token.len = (S32)(tok->t - tok->start_t); + return token; +} + +//////////////////////////////// +// Main get_next_token + +static Token sql_get_next_token(Tokenizer *tok) { + tokenizer_eat_whitespace(tok); + + Token token = {0}; + token.start = (S32)(tok->t - tok->buf); + token.type = TOK_DEFAULT; + + if (tok->t >= tok->max_t) { + token.len = 0; + return token; + } + + tok->start_t = tok->t; + char c = *tok->t; + + if (sql_is_ident_start(c)) { + return sql_parse_identifier(tok); + } + if (isdigit((unsigned char)c)) { + return sql_parse_number(tok); + } + + switch (c) { + case '\'': + return sql_parse_string(tok); + + case '"': + return sql_parse_quoted_identifier(tok); + + case '`': + return sql_parse_backtick_identifier(tok); + + case '-': + if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '-') { + return sql_parse_line_comment(tok); + } + token.type = TOK_OPERATION; + tok->t++; + token.len = 1; + return token; + + case '/': + if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '*') { + return sql_parse_block_comment(tok); + } + token.type = TOK_OPERATION; + tok->t++; + token.len = 1; + return token; + + case '#': + return sql_parse_hash_comment(tok); + + case '.': + if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { + return sql_parse_dot_number(tok); + } + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + case '@': case '$': + return sql_parse_variable(tok); + + case ':': + // :param or :: cast operator + if ((tok->t + 1) < tok->max_t) { + char n = *(tok->t + 1); + if (n == ':') return sql_parse_operator(tok); + if (sql_is_ident_start(n)) return sql_parse_variable(tok); + } + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Punctuation + case ';': case ',': + case '{': case '}': case '(': case ')': case '[': case ']': + token.type = TOK_PUNCTUATION; + tok->t++; + token.len = 1; + return token; + + // Operators + case '=': case '<': case '>': case '!': + case '+': case '*': case '%': case '&': + case '|': case '^': case '~': + return sql_parse_operator(tok); + + default: + token.type = TOK_INVALID; + tok->t++; + token.len = 1; + return token; + } +} + +//////////////////////////////// +// tokenize_sql -- main entry point + +static void tokenize_sql(const char *data, S32 length, U8 *out_tokens) { + memset(out_tokens, TOK_DEFAULT, length); + + Tokenizer tok; + tokenizer_init(&tok, data, length); + + Token prev = {0}; + prev.type = TOK_DEFAULT; + + while (tok.t < tok.max_t) { + Token token = sql_get_next_token(&tok); + if (token.len == 0) break; + + paint_token(out_tokens, token.start, token.len, token.type); + + // Retroactively mark identifier before '(' as a function + if (token.type == TOK_PUNCTUATION && token.len == 1 && + data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { + paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); + } + + prev = token; + } +} diff --git a/c/lexer/lexer_theme.c b/c/lexer/lexer_theme.c new file mode 100644 index 00000000..dcdacc4e --- /dev/null +++ b/c/lexer/lexer_theme.c @@ -0,0 +1,502 @@ +// lexer_theme.c -- Built-in color themes +// +// Colors ported from the Focus editor theme files. +// Hex values are RGB (alpha channel dropped, always opaque in terminal). + +#include "lexer/lexer_theme.h" + +S32 g_active_theme_idx = 0; + +Theme g_themes[THEME_COUNT] = { + + //////////////////////////////// + // Default + // Uses the terminal's own 256-color palette. No background override. + { + .name = "Default", + .use_truecolor = 0, + .colors = { + /* DEFAULT */ RGB_HEX(0xBDBDBD), + /* COMMENT */ RGB_HEX(0x6A6A6A), + /* MULTILINE_COMMENT */ RGB_HEX(0x6A6A6A), + /* STRING_LITERAL */ RGB_HEX(0x6AAF50), + /* CHAR_LITERAL */ RGB_HEX(0x6AAF50), + /* NUMBER */ RGB_HEX(0xC678DD), + /* IDENTIFIER */ RGB_HEX(0xBDBDBD), + /* FUNCTION */ RGB_HEX(0x56B6C2), + /* KEYWORD */ RGB_HEX(0xD19A66), + /* TYPE */ RGB_HEX(0x61AFEF), + /* VALUE */ RGB_HEX(0xC678DD), + /* MODIFIER */ RGB_HEX(0xE06C75), + /* DIRECTIVE */ RGB_HEX(0xE06C75), + /* PUNCTUATION */ RGB_HEX(0xBDBDBD), + /* OPERATION */ RGB_HEX(0xBDBDBD), + /* INVALID */ RGB_HEX(0xFF5555), + }, + .ansi_colors = { + /* DEFAULT */ 7, + /* COMMENT */ 242, + /* MULTILINE_COMMENT */ 242, + /* STRING_LITERAL */ 2, + /* CHAR_LITERAL */ 2, + /* NUMBER */ 5, + /* IDENTIFIER */ 7, + /* FUNCTION */ 6, + /* KEYWORD */ 3, + /* TYPE */ 4, + /* VALUE */ 5, + /* MODIFIER */ 1, + /* DIRECTIVE */ 1, + /* PUNCTUATION */ 7, + /* OPERATION */ 7, + /* INVALID */ 9, + }, + .background = RGB_HEX(0x1E1E1E), + .set_background = 0, + .status_bg = RGB_HEX(0x252525), + .status_fg = RGB_HEX(0xDDDDDD), + .status_fg_dim = RGB_HEX(0x888888), + .mb_bg = RGB_HEX(0x3D5A80), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0x888888), + }, + + //////////////////////////////// + // Default Light + // Uses the terminal's own 256-color palette with a light background. + { + .name = "Default Light", + .use_truecolor = 0, + .colors = { + /* DEFAULT */ RGB_HEX(0x383A42), + /* COMMENT */ RGB_HEX(0xA0A1A7), + /* MULTILINE_COMMENT */ RGB_HEX(0xA0A1A7), + /* STRING_LITERAL */ RGB_HEX(0xA62C21), + /* CHAR_LITERAL */ RGB_HEX(0xA62C21), + /* NUMBER */ RGB_HEX(0x986801), + /* IDENTIFIER */ RGB_HEX(0x383A42), + /* FUNCTION */ RGB_HEX(0x0184BC), + /* KEYWORD */ RGB_HEX(0x4078F2), + /* TYPE */ RGB_HEX(0x50A14F), + /* VALUE */ RGB_HEX(0x986801), + /* MODIFIER */ RGB_HEX(0x4078F2), + /* DIRECTIVE */ RGB_HEX(0xA62C21), + /* PUNCTUATION */ RGB_HEX(0x383A42), + /* OPERATION */ RGB_HEX(0x383A42), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = { + /* DEFAULT */ 0, // black + /* COMMENT */ 243, // dark gray + /* MULTILINE_COMMENT */ 243, + /* STRING_LITERAL */ 1, // red + /* CHAR_LITERAL */ 1, + /* NUMBER */ 5, // magenta + /* IDENTIFIER */ 0, // black + /* FUNCTION */ 6, // cyan + /* KEYWORD */ 4, // blue + /* TYPE */ 2, // green + /* VALUE */ 5, // magenta + /* MODIFIER */ 4, // blue + /* DIRECTIVE */ 1, // red + /* PUNCTUATION */ 0, // black + /* OPERATION */ 0, // black + /* INVALID */ 9, // bright red + }, + .background = RGB_HEX(0xFAFAFA), + .set_background = 0, + .status_bg = RGB_HEX(0xD0D0D0), + .status_fg = RGB_HEX(0x000000), + .status_fg_dim = RGB_HEX(0x606060), + .mb_bg = RGB_HEX(0x0060C0), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0x808080), + }, + + //////////////////////////////// + // Focus + // The default Focus editor theme. + { + .name = "Focus", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xBFC9DB), + /* COMMENT */ RGB_HEX(0x87919D), + /* MULTILINE_COMMENT */ RGB_HEX(0x87919D), + /* STRING_LITERAL */ RGB_HEX(0xD4BC7D), + /* CHAR_LITERAL */ RGB_HEX(0xD4BC7D), + /* NUMBER */ RGB_HEX(0xD699B5), + /* IDENTIFIER */ RGB_HEX(0xBFC9DB), + /* FUNCTION */ RGB_HEX(0xD0C5A9), + /* KEYWORD */ RGB_HEX(0xE67D74), + /* TYPE */ RGB_HEX(0x82AAA3), + /* VALUE */ RGB_HEX(0xD699B5), + /* MODIFIER */ RGB_HEX(0xE67D74), + /* DIRECTIVE */ RGB_HEX(0xE67D74), + /* PUNCTUATION */ RGB_HEX(0xBFC9DB), + /* OPERATION */ RGB_HEX(0xE0AD82), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x15212A), + .set_background = 1, + .status_bg = RGB_HEX(0x0C1620), + .status_fg = RGB_HEX(0xBFC9DB), + .status_fg_dim = RGB_HEX(0x607080), + .mb_bg = RGB_HEX(0x1C3040), + .mb_fg = RGB_HEX(0xBFC9DB), + .mb_detail = RGB_HEX(0x607080), + }, + + //////////////////////////////// + // Handmade Hero + // Based on Casey Muratori's emacs theme from the Handmade Hero series. + { + .name = "Handmade Hero", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xCDAA7D), + /* COMMENT */ RGB_HEX(0x7F7F7F), + /* MULTILINE_COMMENT */ RGB_HEX(0x87919D), + /* STRING_LITERAL */ RGB_HEX(0x6B8E23), + /* CHAR_LITERAL */ RGB_HEX(0x6B8E23), + /* NUMBER */ RGB_HEX(0xD699B5), + /* IDENTIFIER */ RGB_HEX(0xBFC9DB), + /* FUNCTION */ RGB_HEX(0xCDAA7D), + /* KEYWORD */ RGB_HEX(0xB8860B), + /* TYPE */ RGB_HEX(0xB8860B), + /* VALUE */ RGB_HEX(0x6B8E23), + /* MODIFIER */ RGB_HEX(0xE67D74), + /* DIRECTIVE */ RGB_HEX(0xE67D74), + /* PUNCTUATION */ RGB_HEX(0xCDAA7D), + /* OPERATION */ RGB_HEX(0xCDAA7D), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x161616), + .set_background = 1, + .status_bg = RGB_HEX(0x0C0C0C), + .status_fg = RGB_HEX(0xCDAA7D), + .status_fg_dim = RGB_HEX(0x6B5A3F), + .mb_bg = RGB_HEX(0x403020), + .mb_fg = RGB_HEX(0xCDAA7D), + .mb_detail = RGB_HEX(0x6B5A3F), + }, + + //////////////////////////////// + // Witness Classic + // Jonathan Blow's classic color scheme. + { + .name = "Witness Classic", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xD3B58D), + /* COMMENT */ RGB_HEX(0xFFFF00), + /* MULTILINE_COMMENT */ RGB_HEX(0x87919D), + /* STRING_LITERAL */ RGB_HEX(0xBEBEBE), + /* CHAR_LITERAL */ RGB_HEX(0xBEBEBE), + /* NUMBER */ RGB_HEX(0xD699B5), + /* IDENTIFIER */ RGB_HEX(0xBFC9DB), + /* FUNCTION */ RGB_HEX(0xD3B58D), + /* KEYWORD */ RGB_HEX(0xFFFFFF), + /* TYPE */ RGB_HEX(0x98FB98), + /* VALUE */ RGB_HEX(0x7FFFD4), + /* MODIFIER */ RGB_HEX(0xE67D74), + /* DIRECTIVE */ RGB_HEX(0xE67D74), + /* PUNCTUATION */ RGB_HEX(0xD3B58D), + /* OPERATION */ RGB_HEX(0xD3B58D), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x292929), + .set_background = 1, + .status_bg = RGB_HEX(0x222222), + .status_fg = RGB_HEX(0xDDDDDD), + .status_fg_dim = RGB_HEX(0x888888), + .mb_bg = RGB_HEX(0x3A3A50), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0x888888), + }, + + //////////////////////////////// + // Witness + // Jonathan Blow's alternative dark-teal theme. + { + .name = "Witness", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xD3B58D), + /* COMMENT */ RGB_HEX(0x3DDF23), + /* MULTILINE_COMMENT */ RGB_HEX(0x87919D), + /* STRING_LITERAL */ RGB_HEX(0x0FDFAF), + /* CHAR_LITERAL */ RGB_HEX(0x0FDFAF), + /* NUMBER */ RGB_HEX(0xD699B5), + /* IDENTIFIER */ RGB_HEX(0xBFC9DB), + /* FUNCTION */ RGB_HEX(0xD3B58D), + /* KEYWORD */ RGB_HEX(0xFFFFFF), + /* TYPE */ RGB_HEX(0x98FB98), + /* VALUE */ RGB_HEX(0x7FFFD4), + /* MODIFIER */ RGB_HEX(0xE67D74), + /* DIRECTIVE */ RGB_HEX(0xE67D74), + /* PUNCTUATION */ RGB_HEX(0xD3B58D), + /* OPERATION */ RGB_HEX(0xE0AD82), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x072626), + .set_background = 1, + .status_bg = RGB_HEX(0x041818), + .status_fg = RGB_HEX(0xD3B58D), + .status_fg_dim = RGB_HEX(0x6B7A6B), + .mb_bg = RGB_HEX(0x0E3A3A), + .mb_fg = RGB_HEX(0xD3B58D), + .mb_detail = RGB_HEX(0x6B7A6B), + }, + + //////////////////////////////// + // Visual Studio Classic + // Light theme matching Visual Studio 6.0 (1998). + { + .name = "VS Classic", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0x000000), + /* COMMENT */ RGB_HEX(0x008000), + /* MULTILINE_COMMENT */ RGB_HEX(0x008000), + /* STRING_LITERAL */ RGB_HEX(0x800000), + /* CHAR_LITERAL */ RGB_HEX(0x800000), + /* NUMBER */ RGB_HEX(0x000000), + /* IDENTIFIER */ RGB_HEX(0x000000), + /* FUNCTION */ RGB_HEX(0x000000), + /* KEYWORD */ RGB_HEX(0x0000FF), + /* TYPE */ RGB_HEX(0x0000FF), + /* VALUE */ RGB_HEX(0x0000FF), + /* MODIFIER */ RGB_HEX(0x0000FF), + /* DIRECTIVE */ RGB_HEX(0x0000FF), + /* PUNCTUATION */ RGB_HEX(0x000000), + /* OPERATION */ RGB_HEX(0x000000), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0xFFFFFF), + .set_background = 1, + .status_bg = RGB_HEX(0xD4D0C8), + .status_fg = RGB_HEX(0x000000), + .status_fg_dim = RGB_HEX(0x606060), + .mb_bg = RGB_HEX(0x0A246A), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0xA0A0A0), + .fb_bg = RGB_HEX(0xFFFFFF), + .set_fb_bg = 1, + }, + + //////////////////////////////// + // RAD Debugger + // Default color scheme from the RAD Debugger project. + { + .name = "RAD Debugger", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xDADADA), + /* COMMENT */ RGB_HEX(0x5D7856), + /* MULTILINE_COMMENT */ RGB_HEX(0x5D7856), + /* STRING_LITERAL */ RGB_HEX(0xFFA070), + /* CHAR_LITERAL */ RGB_HEX(0xFFA070), + /* NUMBER */ RGB_HEX(0xD0D0A0), + /* IDENTIFIER */ RGB_HEX(0xDADADA), + /* FUNCTION */ RGB_HEX(0xDADADA), + /* KEYWORD */ RGB_HEX(0xF0C674), + /* TYPE */ RGB_HEX(0x70C0B0), + /* VALUE */ RGB_HEX(0xD0D0A0), + /* MODIFIER */ RGB_HEX(0xF0C674), + /* DIRECTIVE */ RGB_HEX(0xCC7070), + /* PUNCTUATION */ RGB_HEX(0xDADADA), + /* OPERATION */ RGB_HEX(0xDADADA), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x1C1C1C), + .set_background = 1, + .status_bg = RGB_HEX(0x121212), + .status_fg = RGB_HEX(0xDADADA), + .status_fg_dim = RGB_HEX(0x707070), + .mb_bg = RGB_HEX(0x3A2A1A), + .mb_fg = RGB_HEX(0xDADADA), + .mb_detail = RGB_HEX(0x707070), + }, + + //////////////////////////////// + // 4coder + // Default theme from Allen Webster's 4coder editor. + { + .name = "4coder", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0x90B080), + /* COMMENT */ RGB_HEX(0x2090F0), + /* MULTILINE_COMMENT */ RGB_HEX(0x2090F0), + /* STRING_LITERAL */ RGB_HEX(0x50FF30), + /* CHAR_LITERAL */ RGB_HEX(0x50FF30), + /* NUMBER */ RGB_HEX(0x50FF30), + /* IDENTIFIER */ RGB_HEX(0x90B080), + /* FUNCTION */ RGB_HEX(0x90B080), + /* KEYWORD */ RGB_HEX(0xD08F20), + /* TYPE */ RGB_HEX(0xD08F20), + /* VALUE */ RGB_HEX(0x50FF30), + /* MODIFIER */ RGB_HEX(0xD08F20), + /* DIRECTIVE */ RGB_HEX(0xFF5F5F), + /* PUNCTUATION */ RGB_HEX(0x90B080), + /* OPERATION */ RGB_HEX(0x90B080), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x0C0C0C), + .set_background = 1, + .status_bg = RGB_HEX(0x1A1A1A), + .status_fg = RGB_HEX(0x90B080), + .status_fg_dim = RGB_HEX(0x506050), + .mb_bg = RGB_HEX(0x2A3A20), + .mb_fg = RGB_HEX(0x90B080), + .mb_detail = RGB_HEX(0x606060), + }, + + //////////////////////////////// + // Ryan Fleury + // Ryan Fleury's personal theme, ported from the fleury-theme.el emacs package. + { + .name = "Ryan Fleury", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xB99468), + /* COMMENT */ RGB_HEX(0x666666), + /* MULTILINE_COMMENT */ RGB_HEX(0x666666), + /* STRING_LITERAL */ RGB_HEX(0xFFAA00), + /* CHAR_LITERAL */ RGB_HEX(0xFFAA00), + /* NUMBER */ RGB_HEX(0xFFAA00), + /* IDENTIFIER */ RGB_HEX(0xB99468), + /* FUNCTION */ RGB_HEX(0xDE451F), + /* KEYWORD */ RGB_HEX(0xF0C674), + /* TYPE */ RGB_HEX(0xEDB211), + /* VALUE */ RGB_HEX(0xFFAA00), + /* MODIFIER */ RGB_HEX(0xF0C674), + /* DIRECTIVE */ RGB_HEX(0xDC7575), + /* PUNCTUATION */ RGB_HEX(0xB99468), + /* OPERATION */ RGB_HEX(0xB99468), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x020202), + .set_background = 1, + .status_bg = RGB_HEX(0x1A120B), + .status_fg = RGB_HEX(0xE7AA4D), + .status_fg_dim = RGB_HEX(0x63523D), + .mb_bg = RGB_HEX(0x3A2510), + .mb_fg = RGB_HEX(0xE7AA4D), + .mb_detail = RGB_HEX(0x63523D), + }, + + //////////////////////////////// + // Gruber Darker + // Gruber Darker theme by Alexey Kutepov (rexim). + { + .name = "Gruber Darker", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xE4E4EF), + /* COMMENT */ RGB_HEX(0x565F73), + /* MULTILINE_COMMENT */ RGB_HEX(0x565F73), + /* STRING_LITERAL */ RGB_HEX(0x73C936), + /* CHAR_LITERAL */ RGB_HEX(0x73C936), + /* NUMBER */ RGB_HEX(0xFFDD33), + /* IDENTIFIER */ RGB_HEX(0xE4E4EF), + /* FUNCTION */ RGB_HEX(0x96A6C8), + /* KEYWORD */ RGB_HEX(0xFFDD33), + /* TYPE */ RGB_HEX(0x96A6C8), + /* VALUE */ RGB_HEX(0xFFDD33), + /* MODIFIER */ RGB_HEX(0x9E95C7), + /* DIRECTIVE */ RGB_HEX(0x9E95C7), + /* PUNCTUATION */ RGB_HEX(0xE4E4EF), + /* OPERATION */ RGB_HEX(0xE4E4EF), + /* INVALID */ RGB_HEX(0xF43841), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x181818), + .set_background = 1, + .status_bg = RGB_HEX(0x101010), + .status_fg = RGB_HEX(0xE4E4EF), + .status_fg_dim = RGB_HEX(0x565F73), + .mb_bg = RGB_HEX(0x282828), + .mb_fg = RGB_HEX(0xE4E4EF), + .mb_detail = RGB_HEX(0x95A99F), + }, + + //////////////////////////////// + // VS Dark + // Visual Studio Dark theme. + { + .name = "VS Dark", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xDCDCAA), + /* COMMENT */ RGB_HEX(0x6A9955), + /* MULTILINE_COMMENT */ RGB_HEX(0x6A9955), + /* STRING_LITERAL */ RGB_HEX(0xCE9178), + /* CHAR_LITERAL */ RGB_HEX(0xCE9178), + /* NUMBER */ RGB_HEX(0xB5CEA8), + /* IDENTIFIER */ RGB_HEX(0x9CDCFE), + /* FUNCTION */ RGB_HEX(0xDCDCAA), + /* KEYWORD */ RGB_HEX(0x569CD6), + /* TYPE */ RGB_HEX(0x4EC9B0), + /* VALUE */ RGB_HEX(0x569CD6), + /* MODIFIER */ RGB_HEX(0x569CD6), + /* DIRECTIVE */ RGB_HEX(0xC586C0), + /* PUNCTUATION */ RGB_HEX(0xD4D4D4), + /* OPERATION */ RGB_HEX(0xD4D4D4), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x1E1E1E), + .set_background = 1, + .status_bg = RGB_HEX(0x252526), + .status_fg = RGB_HEX(0xCCCCCC), + .status_fg_dim = RGB_HEX(0x808080), + .mb_bg = RGB_HEX(0x094771), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0x808080), + }, + + //////////////////////////////// + // Freshcut Contrast + // High-contrast dark theme by Dayle Rees. + { + .name = "Freshcut Contrast", + .use_truecolor = 1, + .colors = { + /* DEFAULT */ RGB_HEX(0xF8F8F2), + /* COMMENT */ RGB_HEX(0x737B84), + /* MULTILINE_COMMENT */ RGB_HEX(0x737B84), + /* STRING_LITERAL */ RGB_HEX(0xE9EE00), + /* CHAR_LITERAL */ RGB_HEX(0xE9EE00), + /* NUMBER */ RGB_HEX(0x8FBE00), + /* IDENTIFIER */ RGB_HEX(0xF8F8F2), + /* FUNCTION */ RGB_HEX(0xAEE239), + /* KEYWORD */ RGB_HEX(0xC8D7E8), + /* TYPE */ RGB_HEX(0x4ECDC4), + /* VALUE */ RGB_HEX(0x8FBE00), + /* MODIFIER */ RGB_HEX(0x00A8C6), + /* DIRECTIVE */ RGB_HEX(0x00A8C6), + /* PUNCTUATION */ RGB_HEX(0xF8F8F2), + /* OPERATION */ RGB_HEX(0xF8F8F2), + /* INVALID */ RGB_HEX(0xFF0000), + }, + .ansi_colors = {}, + .background = RGB_HEX(0x000000), + .set_background = 1, + .status_bg = RGB_HEX(0x3C3C3C), + .status_fg = RGB_HEX(0xE0E0E0), + .status_fg_dim = RGB_HEX(0x808080), + .mb_bg = RGB_HEX(0x505050), + .mb_fg = RGB_HEX(0xFFFFFF), + .mb_detail = RGB_HEX(0x909090), + }, +}; diff --git a/c/lexer/lexer_theme.h b/c/lexer/lexer_theme.h new file mode 100644 index 00000000..7f179f54 --- /dev/null +++ b/c/lexer/lexer_theme.h @@ -0,0 +1,50 @@ +#pragma once +// lexer_theme.h -- Color themes for syntax highlighting +// +// Each theme maps Token_Type values to 24-bit RGB colors, plus a background. +// Themes use true-color ANSI escapes (supported by Windows Terminal, most +// modern terminals). + +#include "base/base_core.h" +#include "lexer/lexer.h" + +//////////////////////////////// +// RGB color + +typedef struct RGB { U8 r, g, b; } RGB; + +// Use for static initializers (no compound literal — MSVC C11 compat) +#define RGB_HEX(hex) { ((hex) >> 16) & 0xFF, ((hex) >> 8) & 0xFF, (hex) & 0xFF } + +//////////////////////////////// +// Theme + +typedef struct Theme { + const char *name; + B32 use_truecolor; // if false, use ansi_colors[] with term_fg() instead + RGB colors[TOK_COUNT]; // foreground color per token type (true color) + U8 ansi_colors[TOK_COUNT]; // foreground per token type (256-color fallback) + RGB background; // terminal background color + B32 set_background; // whether to override terminal background + + // UI chrome colors + RGB status_bg; // status bar background + RGB status_fg; // status bar foreground (focused) + RGB status_fg_dim; // status bar foreground (unfocused) + RGB mb_bg; // minibuffer item background (highlighted) + RGB mb_fg; // minibuffer item foreground (highlighted) + RGB mb_detail; // minibuffer detail text color + RGB fb_bg; // file browser background (0,0,0 = use WindowBg) + B32 set_fb_bg; // whether to override file browser background +} Theme; + +//////////////////////////////// +// Built-in themes + +#define THEME_COUNT 13 + +extern Theme g_themes[THEME_COUNT]; +extern S32 g_active_theme_idx; + +// Convenience: pointer to the active theme +static inline Theme *theme_active(void) { return &g_themes[g_active_theme_idx]; } diff --git a/c/platform/platform.h b/c/platform/platform.h new file mode 100644 index 00000000..1a8121c2 --- /dev/null +++ b/c/platform/platform.h @@ -0,0 +1,130 @@ +#pragma once +// platform.h — GUI windowing, input, and system services +// +// Provides window creation, keyboard/mouse input, clipboard access, +// and process spawning. Implementation: platform_win32.c. + +#include "base/base_core.h" +#include "base/base_math.h" + +//////////////////////////////// +// Key codes (matching Windows VK codes) + +enum { + PKEY_BACKSPACE = 0x08, + PKEY_TAB = 0x09, + PKEY_RETURN = 0x0D, + PKEY_ESCAPE = 0x1B, + PKEY_PAGEUP = 0x21, + PKEY_PAGEDOWN = 0x22, + PKEY_END = 0x23, + PKEY_HOME = 0x24, + PKEY_LEFT = 0x25, + PKEY_UP = 0x26, + PKEY_RIGHT = 0x27, + PKEY_DOWN = 0x28, + PKEY_DELETE = 0x2E, + PKEY_0 = 0x30, + PKEY_1 = 0x31, + PKEY_2 = 0x32, + PKEY_3 = 0x33, + PKEY_A = 0x41, + PKEY_B = 0x42, + PKEY_C = 0x43, + PKEY_E = 0x45, + PKEY_F = 0x46, + PKEY_G = 0x47, + PKEY_H = 0x48, + PKEY_J = 0x4A, + PKEY_K = 0x4B, + PKEY_L = 0x4C, + PKEY_N = 0x4E, + PKEY_O = 0x4F, + PKEY_P = 0x50, + PKEY_Q = 0x51, + PKEY_S = 0x53, + PKEY_V = 0x56, + PKEY_W = 0x57, + PKEY_X = 0x58, + PKEY_Y = 0x59, + PKEY_Z = 0x5A, + PKEY_F4 = 0x73, + PKEY_F8 = 0x77, + PKEY_EQUAL = 0xBB, + PKEY_MINUS = 0xBD, + PKEY_BACKSLASH = 0xDC, +}; + +//////////////////////////////// +// Input accumulated per frame + +#define PLATFORM_MAX_CHARS_PER_FRAME 64 +#define PLATFORM_MAX_KEYS_PER_FRAME 32 + +// Editor-facing input (ASCII chars + key codes + modifiers) +typedef struct PlatformInput { + char chars[PLATFORM_MAX_CHARS_PER_FRAME]; + S32 char_count; + + U8 keys[PLATFORM_MAX_KEYS_PER_FRAME]; + S32 key_count; + + B32 ctrl_held; + B32 shift_held; + B32 alt_held; +} PlatformInput; + +// Raw windowing input (UTF-16 chars + VK codes + mouse) +typedef struct PlatformRawInput { + U16 chars[PLATFORM_MAX_CHARS_PER_FRAME]; + S32 char_count; + U8 keys[PLATFORM_MAX_KEYS_PER_FRAME]; + S32 key_count; + B32 ctrl_held; + B32 shift_held; + Vec2F32 mouse_pos; + Vec2F32 scroll_delta; + B32 mouse_down; + B32 was_mouse_down; +} PlatformRawInput; + +//////////////////////////////// +// Window + +typedef struct PlatformWindow PlatformWindow; + +typedef struct PlatformWindowDesc { + const char *title; + S32 width; + S32 height; +} PlatformWindowDesc; + +typedef void (*PlatformFrameCallback)(void *user_data); + +//////////////////////////////// +// Window API + +PlatformWindow *platform_create_window(PlatformWindowDesc *desc); +void platform_destroy_window(PlatformWindow *window); +B32 platform_poll_events(PlatformWindow *window); +void platform_get_size(PlatformWindow *window, S32 *w, S32 *h); +void *platform_get_native_handle(PlatformWindow *window); +PlatformRawInput platform_get_input(PlatformWindow *window); +void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data); +F32 platform_get_dpi_scale(PlatformWindow *window); + +//////////////////////////////// +// Input adapter — convert raw windowing input to editor input + +PlatformInput platform_adapt_input(PlatformRawInput *raw); + +//////////////////////////////// +// Clipboard + +void platform_clipboard_set(const char *text); +const char *platform_clipboard_get(void); + +//////////////////////////////// +// Process spawning + +void platform_spawn_terminal(const char *command, const char *working_dir); diff --git a/c/platform/platform_win32.c b/c/platform/platform_win32.c new file mode 100644 index 00000000..8051ea9d --- /dev/null +++ b/c/platform/platform_win32.c @@ -0,0 +1,297 @@ +// platform_win32.c — Win32 windowing, input, clipboard, and process spawning + +#include "platform/platform.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include + +//////////////////////////////// +// Window internals + +struct PlatformWindow { + HWND hwnd; + B32 should_close; + S32 width; + S32 height; + PlatformFrameCallback frame_callback; + void *frame_callback_user_data; + PlatformRawInput input; + B32 prev_mouse_down; +}; + +static PlatformWindow *g_main_window = NULL; +static B32 g_wndclass_registered = false; + +//////////////////////////////// +// Window procedure + +// Forward declare ImGui Win32 handler (defined in imgui_impl_win32.cpp) +extern LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +static LRESULT CALLBACK platform_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + // Let ImGui process input first + if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam)) + return true; + + PlatformWindow *pw = (PlatformWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + + switch (msg) { + case WM_SIZE: + if (pw && wparam != SIZE_MINIMIZED) { + pw->width = (S32)LOWORD(lparam); + pw->height = (S32)HIWORD(lparam); + if (pw->frame_callback) + pw->frame_callback(pw->frame_callback_user_data); + } + return 0; + case WM_CHAR: + if (pw && wparam >= 32 && wparam < 0xFFFF) { + PlatformRawInput *ev = &pw->input; + if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) + ev->chars[ev->char_count++] = (U16)wparam; + } + return 0; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (pw) { + PlatformRawInput *ev = &pw->input; + if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME) + ev->keys[ev->key_count++] = (U8)wparam; + ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + ev->shift_held = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + } + break; + case WM_MOUSEWHEEL: + if (pw) { + S16 wheel_delta = (S16)HIWORD(wparam); + pw->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f; + } + return 0; + case WM_SETCURSOR: + if (LOWORD(lparam) == HTCLIENT) { + SetCursor(LoadCursor(NULL, IDC_IBEAM)); + return TRUE; + } + break; + case WM_DPICHANGED: + if (pw) { + RECT *suggested = (RECT *)lparam; + SetWindowPos(hwnd, NULL, suggested->left, suggested->top, + suggested->right - suggested->left, suggested->bottom - suggested->top, + SWP_NOZORDER | SWP_NOACTIVATE); + } + return 0; + case WM_CLOSE: + if (pw) pw->should_close = true; + return 0; + case WM_DESTROY: + if (pw == g_main_window) + PostQuitMessage(0); + return 0; + case WM_SYSCOMMAND: + if ((wparam & 0xfff0) == SC_KEYMENU) + return 0; + break; + } + return DefWindowProcW(hwnd, msg, wparam, lparam); +} + +//////////////////////////////// +// Window lifecycle + +PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + if (!g_wndclass_registered) { + WNDCLASSEXW wc = {0}; + wc.cbSize = sizeof(wc); + wc.style = CS_CLASSDC; + wc.lpfnWndProc = platform_wndproc; + wc.hInstance = GetModuleHandleW(NULL); + wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(101)); + wc.hIconSm = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(101)); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszClassName = L"codemax_wc"; + RegisterClassExW(&wc); + g_wndclass_registered = true; + } + + UINT dpi = GetDpiForSystem(); + int screen_w = GetSystemMetrics(SM_CXSCREEN); + int screen_h = GetSystemMetrics(SM_CYSCREEN); + int x = (screen_w - desc->width) / 2; + int y = (screen_h - desc->height) / 2; + + DWORD style = WS_OVERLAPPEDWINDOW; + RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height }; + AdjustWindowRectExForDpi(&rect, style, FALSE, 0, dpi); + + int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, NULL, 0); + wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t)); + MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, wtitle, wchar_count); + + HWND hwnd = CreateWindowExW( + 0, L"codemax_wc", wtitle, + style, + x, y, + rect.right - rect.left, + rect.bottom - rect.top, + NULL, NULL, GetModuleHandleW(NULL), NULL + ); + _freea(wtitle); + + if (!hwnd) return NULL; + + PlatformWindow *window = (PlatformWindow *)calloc(1, sizeof(PlatformWindow)); + window->hwnd = hwnd; + window->should_close = false; + window->width = desc->width; + window->height = desc->height; + + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)window); + g_main_window = window; + + ShowWindow(hwnd, SW_SHOWDEFAULT); + UpdateWindow(hwnd); + return window; +} + +void platform_destroy_window(PlatformWindow *window) { + if (!window) return; + if (window->hwnd) DestroyWindow(window->hwnd); + if (g_main_window == window) g_main_window = NULL; + free(window); +} + +B32 platform_poll_events(PlatformWindow *window) { + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + if (msg.message == WM_QUIT) + window->should_close = true; + } + return !window->should_close; +} + +void platform_get_size(PlatformWindow *window, S32 *w, S32 *h) { + if (w) *w = window->width; + if (h) *h = window->height; +} + +void *platform_get_native_handle(PlatformWindow *window) { + return (void *)window->hwnd; +} + +PlatformRawInput platform_get_input(PlatformWindow *window) { + PlatformRawInput result = window->input; + + POINT cursor; + GetCursorPos(&cursor); + ScreenToClient(window->hwnd, &cursor); + result.mouse_pos = v2f32((F32)cursor.x, (F32)cursor.y); + + result.was_mouse_down = window->prev_mouse_down; + result.mouse_down = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + window->prev_mouse_down = result.mouse_down; + + memset(&window->input, 0, sizeof(window->input)); + return result; +} + +void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data) { + window->frame_callback = cb; + window->frame_callback_user_data = user_data; +} + +F32 platform_get_dpi_scale(PlatformWindow *window) { + if (!window || !window->hwnd) return 1.0f; + return (F32)GetDpiForWindow(window->hwnd) / 96.0f; +} + +//////////////////////////////// +// Input adapter + +PlatformInput platform_adapt_input(PlatformRawInput *raw) { + PlatformInput pi = {0}; + pi.ctrl_held = raw->ctrl_held; + pi.shift_held = raw->shift_held; + pi.alt_held = (GetKeyState(VK_MENU) & 0x8000) != 0; + + // Convert UTF-16 chars to ASCII (printable range only) + for (S32 i = 0; i < raw->char_count && pi.char_count < PLATFORM_MAX_CHARS_PER_FRAME; i++) { + U16 ch = raw->chars[i]; + if (ch >= 32 && ch < 127) + pi.chars[pi.char_count++] = (char)ch; + } + + // Key codes are identical (both use Windows VK codes) + for (S32 i = 0; i < raw->key_count && pi.key_count < PLATFORM_MAX_KEYS_PER_FRAME; i++) + pi.keys[pi.key_count++] = raw->keys[i]; + + return pi; +} + +//////////////////////////////// +// Clipboard + +void platform_clipboard_set(const char *text) { + if (!text) return; + int len = (int)strlen(text); + if (len == 0) return; + int wlen = MultiByteToWideChar(CP_UTF8, 0, text, len, NULL, 0); + if (wlen == 0) return; + HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t)); + if (!hmem) return; + wchar_t *wbuf = (wchar_t *)GlobalLock(hmem); + MultiByteToWideChar(CP_UTF8, 0, text, len, wbuf, wlen); + wbuf[wlen] = L'\0'; + GlobalUnlock(hmem); + HWND hwnd = g_main_window ? g_main_window->hwnd : NULL; + if (OpenClipboard(hwnd)) { + EmptyClipboard(); + SetClipboardData(CF_UNICODETEXT, hmem); + CloseClipboard(); + } else { + GlobalFree(hmem); + } +} + +const char *platform_clipboard_get(void) { + static char buf[64 * 1024]; + buf[0] = '\0'; + HWND hwnd = g_main_window ? g_main_window->hwnd : NULL; + if (!OpenClipboard(hwnd)) return NULL; + HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT); + if (hmem) { + wchar_t *wbuf = (wchar_t *)GlobalLock(hmem); + if (wbuf) { + int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, sizeof(buf) - 1, NULL, NULL); + buf[len > 0 ? len - 1 : 0] = '\0'; + GlobalUnlock(hmem); + } + } + CloseClipboard(); + return buf[0] ? buf : NULL; +} + +//////////////////////////////// +// Process spawning + +void platform_spawn_terminal(const char *command, const char *working_dir) { + char cmd_line[2048]; + snprintf(cmd_line, sizeof(cmd_line), "cmd.exe /k %s", command); + + STARTUPINFOA si = {0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {0}; + CreateProcessA(NULL, cmd_line, NULL, NULL, FALSE, + CREATE_NEW_CONSOLE, NULL, working_dir, &si, &pi); + if (pi.hProcess) CloseHandle(pi.hProcess); + if (pi.hThread) CloseHandle(pi.hThread); +} diff --git a/jai/modules/Console_Render/console.jai b/jai/modules/Console_Render/console.jai new file mode 100644 index 00000000..ac9a7f4e --- /dev/null +++ b/jai/modules/Console_Render/console.jai @@ -0,0 +1,10 @@ +swap_buffer :: (screen: *Screen) { + using screen; + write_string(cast(string)buffer); +} + +clear_screen :: (screen: *Screen) { + for 0..screen.height * screen.width { + print_color(" ", color = .BLACK); + } +} \ No newline at end of file diff --git a/jai/modules/Console_Render/draw.jai b/jai/modules/Console_Render/draw.jai new file mode 100644 index 00000000..6e0d15ea --- /dev/null +++ b/jai/modules/Console_Render/draw.jai @@ -0,0 +1,107 @@ +// "Squashes" two dimensional coordinates onto the one dimensional screen buffer +draw :: (using screen: *Screen, p: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height { + buffer[p.y * width + p.x] = char; + } +} + +// Implementation of the Bresenham line algorithm +// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm +draw_line :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + p1_c := p1; + p2_c := p2; + + dx := abs(p2_c.x - p1_c.x); + dy := -abs(p2_c.y - p1_c.y); + sx := ifx p1_c.x < p2_c.x then 1 else -1; + sy := ifx p1_c.y < p2_c.y then 1 else -1; + + err := dx + dy; + e2 : s64; + + while true { + draw(screen, Vec2s64.{p1_c.x, p1_c.y}, char); + + if p1_c.x == p2_c.x && p1_c.y == p2_c.y then break; + + e2 = 2 * err; + + if e2 >= dy { + err += dy; + p1_c.x += sx; + } + + if e2 <= dx { + err += dx; + p1_c.y += sy; + } + } +} + +draw_triangle :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + draw_line(screen, p1, p2, char); + draw_line(screen, p2, p3, char); + draw_line(screen, p3, p1, char); +} + +draw_triangle :: (screen: *Screen, t: Triangle, char: u8 = DEFAULT_PIXEL_CHAR) { + draw_triangle(screen, t.p1, t.p2, t.p3, char); +} + +draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + // (x1, y1) => top left + // (x2, y2) => bottom right + + draw_line(screen, .{p1.x, p1.y}, .{p1.x, p2.y}, char); + draw_line(screen, .{p1.x, p2.y}, .{p2.x, p2.y}, char); + draw_line(screen, .{p2.x, p2.y}, .{p2.x, p1.y}, char); + draw_line(screen, .{p2.x, p1.y}, .{p1.x, p1.y}, char); +} + +draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, p4: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + // (x1, y1) => top left + // (x2, y2) => bottom left + // (x3, y3) => bottom right + // (x4, y4) => top right + + draw_line(screen, .{p1.x, p1.y}, .{p2.x, p2.y}, char); + draw_line(screen, .{p2.x, p2.y}, .{p3.x, p3.y}, char); + draw_line(screen, .{p3.x, p3.y}, .{p4.x, p4.y}, char); + draw_line(screen, .{p4.x, p4.y}, .{p1.x, p1.y}, char); +} + +draw_quad :: (screen: *Screen, q: Quad, char: u8 = DEFAULT_PIXEL_CHAR) { + draw_quad(screen, q.p1, q.p2, q.p3, q.p4, char); +} + +draw_text :: (using screen: *Screen, p: Vec2s64, s: string) { + if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height { + for cast([]u8)s { + buffer[(p.y * width + p.x) + it_index] = it; + } + } +} + +// When attempting to draw outside of the visible screen, "clip" the pixels +// by setting their position to the maximum width / height available +clip :: (using screen: *Screen, p: *Vec2s64) { + if p.x < 0 then p.x = 0; + if p.x >= width then p.x = width; + if p.y < 0 then p.y = 0; + if p.y >= height then p.y = height; +} + +fill :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) { + clip(screen, *p1); + clip(screen, *p2); + + for x: p1.x..p2.x { + for y: p1.y..p2.y { + draw(screen, .{x, y}, char); + } + } +} + +fill_entire_screen :: (screen: *Screen, char: u8 = DEFAULT_PIXEL_CHAR) { + fill(screen, .{0,0}, .{screen.width, screen.height}, char); +} \ No newline at end of file diff --git a/jai/modules/Console_Render/file_operations.jai b/jai/modules/Console_Render/file_operations.jai new file mode 100644 index 00000000..aefb141c --- /dev/null +++ b/jai/modules/Console_Render/file_operations.jai @@ -0,0 +1,8 @@ +// Render current screen state to a file +render_to_file :: (screen: *Screen, preserve_color: bool, filepath: string) { + assert(false, "Not implemented"); +} + +#scope_file + +#import "File"; \ No newline at end of file diff --git a/jai/modules/Console_Render/macos/console.jai b/jai/modules/Console_Render/macos/console.jai new file mode 100644 index 00000000..aaa110e7 --- /dev/null +++ b/jai/modules/Console_Render/macos/console.jai @@ -0,0 +1,42 @@ +get_console_size :: () -> s64, s64 { + //@Hack - This is terrible, but this is typically called only once, so maybe it doesn't matter... + width := string_to_int(get_stdout_from_cmd("tput cols")); + height := string_to_int(get_stdout_from_cmd("tput lines")); + + return cast(s64)width, cast(s64)height; +} + +#scope_file + +get_stdout_from_cmd :: (cmd: string) -> string { + buffer: [256]u8; + stream : *FILE; + data : string; + + // m_cmd := tprint("% %", cmd, " 2>&1"); + + stream = popen(to_c_string(cmd), to_c_string("r")); + + if stream { + while !feof(stream) { + if fgets(buffer.data, buffer.count, stream) { + data = tprint("%1%2", data, cast(string)(buffer)); + } + } + + pclose(stream); + } + + return data; +} + +to_c_string :: (s: string) -> *u8 { + result := cast(*u8) alloc(s.count + 1); + memcpy(result, s.data, s.count); + result[s.count] = 0; + + return result; +} + + +#import "POSIX"; \ No newline at end of file diff --git a/jai/modules/Console_Render/math_extras.jai b/jai/modules/Console_Render/math_extras.jai new file mode 100644 index 00000000..351105fa --- /dev/null +++ b/jai/modules/Console_Render/math_extras.jai @@ -0,0 +1,19 @@ +get_triangle_centroid :: (t: Triangle) -> Vec2s64 { + return get_triangle_centroid(t.p1, t.p2, t.p3); +} + +get_triangle_centroid :: (p1: Vec2s64, p2: Vec2s64, p3: Vec2s64) -> Vec2s64 { + x := (p1.x + p2.x + p3.x) / 3; + y := (p1.y + p2.y + p3.y) / 3; + + return .{x, y}; +} + +make_quad_from_rect :: (p1: Vec2s64, p2: Vec2s64) -> Quad { + return .{ + .{ p1.x, p1.y }, + .{ p2.x, p1.y }, + .{ p2.x, p2.y }, + .{ p1.x, p2.y }, + }; +} \ No newline at end of file diff --git a/jai/modules/Console_Render/module.jai b/jai/modules/Console_Render/module.jai new file mode 100644 index 00000000..3d07385f --- /dev/null +++ b/jai/modules/Console_Render/module.jai @@ -0,0 +1,122 @@ +/****************************************************** + * Copyright 2024 Max Amundsen - All Rights Reserved + ****************************************************** + + Console Screen Rendering: + + Terminal emulators, or consoles, are programs that display text from a sequence of characters. + These characters are usually fixed-width, meaning each character takes up the same amount of space on screen. + The dimensions of your console window determines how the console will display the text in rows and columns. + + This module is designed to draw simple 2d, or 3d shapes, inside a standard terminal emulator program. + + -------------------------------------------------------------- + Suppose the following sequence of characters is sent to the console: + + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U'] + + For a console displaying 7 characters horizontally, and 3 characters vertically, the result would be the following: + + 0123456 + _________ + 0 |ABCDEFG| + 1 |HIJKLMN| + 2 |OPQRSTU| + --------- + + As displayed in the above figure, the 0th element, 'A', in the sequence represents (0,0) in 2D space, while the 9th element, 'J' represents (2,1). + + => Calculating the two dimensional area of the console output yields the number of elements in the initial 1D sequence: + + (7 * 3) = 21 + + This may appear obvious, however this is an important connection to make, when considering how the console represents its output. + + The terminal automatically transforms our 1D sequence, to a 2D sequence displayed on your monitor, based on the size of the window. + + Since the console is responsible for transforming our 1D array into a 2D output, we must provide the console with a 1D array. + In order to draw 2D and 3D elements on the screen, we must construct procedures in the code to represent the transformations from + higher dimensions, down to the first dimension. + + This simple 2d -> 1d transformation is implemented as the `draw` procedure found in the `draw.jai` file in this module. + +*/ + +BLANK_PIXEL : u8 : 0x20; +DEFAULT_PIXEL_CHAR : u8 : #char "#"; + +Vec2s64 :: struct { + x: s64; + y: s64; +} + +Triangle :: struct { + p1: Vec2s64; + p3: Vec2s64; + p2: Vec2s64; +} + +Quad :: struct { + p1: Vec2s64; + p2: Vec2s64; + p3: Vec2s64; + p4: Vec2s64; +} + +Screen :: struct { + width : s64; + height: s64; + buffer: [..] u8; +} + +init_screen :: (screen: *Screen, width: s64, height: s64) { + buffer : [..] u8; + + array_resize(*buffer, width * height); + + screen.width = width; + screen.height = height; + screen.buffer = buffer; +} + +resize_screen :: (screen: *Screen, width: s64, height: s64) { + screen.width = width; + screen.height = height; + + array_resize(*screen.buffer, width * height); +} + +maybe_resize_screen :: (screen: *Screen) { + w, h := get_console_size(); + + should_update := false; + + if w != screen.width { + should_update = true; + } + + if h != screen.height { + should_update = true; + } + + if should_update { + resize_screen(screen, w, h); + } +} + +#load "draw.jai"; +#load "transform.jai"; +#load "file_operations.jai"; +#load "console.jai"; +#load "math_extras.jai"; + +#if OS == .MACOS { + #load "macos/console.jai"; +} else { + #assert false "Platform unsupported. Sorry."; +} + +#scope_module + +#import "Basic"; +#import "Math"; diff --git a/jai/modules/Console_Render/transform.jai b/jai/modules/Console_Render/transform.jai new file mode 100644 index 00000000..935de2a9 --- /dev/null +++ b/jai/modules/Console_Render/transform.jai @@ -0,0 +1,67 @@ +// for n points +translate :: (amount: Vec2s64, pn: ..*Vec2s64) { + for pn { + it.x += amount.x; + it.y += amount.y; + } +} + +// for a triangle +translate :: (amount: Vec2s64, t: *Triangle) { + translate(amount, *t.p1, *t.p2, *t.p3); +} + + +// for a quad +translate :: (amount: Vec2s64, q: *Quad) { + translate(amount, *q.p1, *q.p2, *q.p3, *q.p4); +} + +// single point rotation +rotate :: (angle: s64, center: Vec2s64, p: *Vec2s64) { + rad := cast(float64)angle * (PI / 180.0); + + // translate to center + tx := p.x - center.x; + ty := p.y - center.y; + + // apply rotation + rx := cast(s64)(tx * cos(rad) - ty * sin(rad)); + ry := cast(s64)(tx * sin(rad) + ty * cos(rad)); + + // translate back + p.x = rx + center.x; + p.y = ry + center.y; +} + +// rotate n points +rotate :: (angle: s64, center: Vec2s64, pn: ..*Vec2s64) { + for pn { + rotate(angle, center, it); + } +} + +// rotate 3 points via triangle struct +rotate :: (angle: s64, center: Vec2s64, t: *Triangle) { + rotate(angle, center, *t.p1, *t.p2, *t.p3); +} + +// rotate 4 points via quad struct +rotate :: (angle: s64, center: Vec2s64, q: *Quad) { + rotate(angle, center, *q.p1, *q.p2, *q.p3, *q.p4); +} + +scale :: (factor: Vector2, pn: ..*Vec2s64) { + for pn { + it.x = cast(s64)(it.x * factor.x); + it.y = cast(s64)(it.y * factor.y); + } +} + +scale :: (factor: Vector2, t: *Triangle) { + scale(factor, *t.p1, *t.p2, *t.p3); +} + +scale :: (factor: Vector2, q: *Quad) { + scale(factor, *q.p1, *q.p2, *q.p3, *q.p4); +} \ No newline at end of file