import dlib; import vulkan; import std.format : sformat; import buffer; import ui : Nil; import ui; import parsing; import std.format; import std.stdio; import std.exception; import std.file; import std.string; import core.stdc.stdio; f32 g_delta = 0.0; debug bool g_frame_step = false; debug bool g_frame_continue = false; struct EditorCtx { Arena arena; PlatformWindow* window; UIPanel* base_panel; u64 panel_id; EditState state; u8[128] input_buf; u32 icount; Timer timer; CmdPalette cmd; u8[][] file_names; } struct CmdPalette { Arena arena; Arena cmd_arena; u8[] buffer; u32 icount; Command[] commands; u8[][] opt_strs; i64 selected; Command current; Parameter[] params; } struct Editor { Arena arena; FlatBuffer buf; Tokenizer tk; Vec2 cursor_pos; Vec2 select_start; Vec2 select_end; f32 text_size; } struct ChangeStacks { Arena arena; u64 current_pos; u8[] current_str; u64 current_len; EditorChange* undos; EditorChange* redos; } struct EditorChange { u8[] str; u64 pos; EditorChange* next; } struct Command { u8[] name; CmdType type; } struct Parameter { u8[] value; bool visible; } enum CmdType { None, OpenFile, SaveFile, CreateFile, VSplit, HSplit, } alias CT = CmdType; enum EditState { NormalMode, InputMode, CmdOpen, SetPanelFocus, // if moving left/right move up parent tree until one is found with a2d.x, same thing for up/down a2d.y } alias ES = EditState; bool g_input_mode = false; const Command NO_CMD = { name: [], type: CT.None, }; void Cycle(EditorCtx* ctx, Inputs* inputs) { ResetScratch(MB(4)); g_delta = DeltaTime(&ctx.timer); debug if(g_frame_step) { g_delta = 0.01; } assert(Nil(ctx.base_panel.next)); HandleInputs(ctx, inputs); debug if(g_frame_step && !g_frame_continue) return; debug g_frame_continue = false; g_input_mode = ctx.state == ES.InputMode; BeginUI(ctx, inputs); for(auto p = ctx.base_panel; !Nil(p); p = Recurse(p)) { Panel(p); } if(ctx.state == ES.CmdOpen) { if(ctx.cmd.commands.length == 0 && ctx.cmd.icount == 0) { GetCommands(&ctx.cmd); } CommandPalette(&ctx.cmd); } EndUI(); } EditorCtx InitEditorCtx(PlatformWindow* window) { InitUICtx(window); EditorCtx ctx = { window: window, arena: CreateArena(MB(2)), cmd: { arena: CreateArena(MB(1)), cmd_arena: CreateArena(MB(1)), buffer: Alloc!(u8)(1024), }, }; ctx.base_panel = CreatePanel(&ctx); ctx.base_panel.ed = CreateEditor(&ctx); ctx.timer = CreateTimer(); SetFocusedPanel(ctx.base_panel); if(getcwd() != "/") { // TODO: replace this with something nogc/nothrow try { u64 count = 0; foreach(DirEntry e; dirEntries(".", SpanMode.breadth)) { if(indexOf(e.name, ".git") != -1 || e.isDir) continue; u64 start = indexOf(e.name, "./") == 0 ? 2 : 0; count += 1; } ctx.file_names = Alloc!(u8[])(&ctx.arena, count); count = 0; foreach(DirEntry e; dirEntries(".", SpanMode.breadth)) { if(indexOf(e.name, ".git") != -1 || e.isDir) continue; u64 start = indexOf(e.name, "./") == 0 ? 2 : 0; ctx.file_names[count++] = Alloc!(u8)(&ctx.arena, CastStr!(u8)(e.name[start .. $])); } } catch(Exception e) { Logf("failed to open directory for filenames"); } } return ctx; } UIPanel* CreatePanel(EditorCtx* ctx, SizeType size_type = ST.Percentage) { UIPanel* p = Alloc!(UIPanel)(&ctx.arena); p.axis = A2D.Y; p.id = Alloc!(u8)(&ctx.arena, 10); p.pct = 1.0; p.scroll_offset = 0.0; p.scroll_target = 0.0; (cast(char[])p.id).sformat("##%08s", ctx.panel_id); p.parent = p.first = p.last = p.next = p.prev = g_UI_NIL_PANEL; ctx.panel_id += 1; return p; } bool EditModeActive() { return g_input_mode; } char[] ToAbsolutePath(u8[] file_name) { import core.stdc.string : strlen; char[1024] name_buf = '\0'; char[1024] wd_buf = '\0'; version(linux) { import core.sys.posix.unistd; getcwd(wd_buf.ptr, wd_buf.length); } version(Windows) { import core.sys.windows.direct; _getcwd(wd_buf.ptr, wd_buf.length); } char[] wd = wd_buf[0 .. strlen(wd_buf.ptr)]; version(linux) { if(file_name[0] != '/') { name_buf.sformat("%s/%s", wd, cast(char[])file_name); } else { name_buf.sformat("%s", cast(char[])file_name); } } version(Windows) { name_buf.sformat("%s/%s", wd, cast(char[])file_name); } char[] path_buf = ScratchAlloc!(char)(strlen(name_buf.ptr)+1); path_buf[0 .. $] = name_buf[0 .. path_buf.length]; return path_buf; } void SaveFile(Editor* ed, u8[] file_name) { import core.stdc.stdio; file_name = file_name.length == 0 ? ed.buf.file_name : file_name; if(file_name.length > 0) { char[] file_path = ToAbsolutePath(file_name); auto f = fopen(cast(char*)file_path.ptr, "wb"); if(f != null) { u64 tab_count; for(u64 i = 0; i < ed.buf.length; i += 1) { if(ed.buf.pbuf[i] == '\t') { tab_count += 1; } } u64 tab_width = GetCtx().tab_width; u64 buf_size = ed.buf.length + ((tab_width-1) * tab_count); u8[] temp_buf = ScratchAlloc!(u8)(buf_size); u64 buf_pos; for(u64 i = 0; i < ed.buf.length; i += 1) { if(ed.buf.pbuf[i] == '\t') { for(u64 j = 0; j < tab_width; j += 1) { temp_buf[buf_pos++] = ' '; } } else { temp_buf[buf_pos++] = ed.buf.pbuf[i]; } } fwrite(temp_buf.ptr, 1, buf_size, f); fflush(f); fclose(f); } } } void OpenFile(Editor* ed, u8[] file_name) { import core.stdc.stdio; import std.file; import std.conv; if(file_name.length > 0) { char[] file_path = ToAbsolutePath(file_name); auto f = fopen(file_path.ptr, "rb"); if(f != null) { fseek(f, 0, SEEK_END); i64 len = ftell(f); fseek(f, 0, SEEK_SET); if(len > 0) { u8[] buf = ScratchAlloc!(u8)(len); fread(buf.ptr, u8.sizeof, len, f); Change(&ed.buf, buf, file_name); ed.buf.file_name = file_name; } fclose(f); } else { perror("[Error] Unable to open file"); } } } // Load all files then move things into editor after being created when selected Editor* CreateEditor(EditorCtx* ctx) { Editor* ed = Alloc!(Editor)(&ctx.arena); ed.arena = CreateArena(MB(4)); ed.buf = CreateFlatBuffer([], []); return ed; } debug { __gshared u64 panel_count = 0; __gshared UIPanel*[1024] panels = null; } void AddEditor(EditorCtx* ctx, UIPanel* target, Axis2D axis) { if(Nil(target.parent) || target.parent.axis != axis) { UIPanel* first = CreatePanel(ctx); UIPanel* second = CreatePanel(ctx); first.ed = target.ed; second.ed = CreateEditor(ctx); first.pct = second.pct = 0.5; target.axis = axis; target.ed = null; PushPanel(target, first); PushPanel(target, second); SetFocusedPanel(second); debug { panels[panel_count+0] = first; panels[panel_count+1] = second; panel_count += 2; } } else if(target.parent.axis == axis) { UIPanel* panel = CreatePanel(ctx); panel.ed = CreateEditor(ctx); InsertPanel(target.parent, target, panel); u64 count = 0; for(UIPanel* p = target.parent.first; !Nil(p); p = p.next, count += 1) {} f32 pct = 1.0/count; for(UIPanel* p = target.parent.first; !Nil(p); p = p.next) { p.pct = pct; } SetFocusedPanel(panel); debug { panels[panel_count] = panel; panel_count += 1; } } debug for(u64 i = 0; i < panel_count; i += 1) { bool root_found = false; for(UIPanel* p = panels[i]; !Nil(p); p = p.parent) { if(p == ctx.base_panel) { root_found = true; break; } } if(!root_found) { Logf("[DEBUG] panel %s unable to reach root", cast(char[])panels[panel_count].id); assert(root_found); } } } pragma(inline) void InsertInputToBuf(EditorCtx* ctx) { if(ctx.icount > 0) { UIPanel* p = GetFocusedPanel(); if(!Nil(p)) { Insert(&p.ed.buf, ctx.input_buf, ctx.icount); ctx.icount = 0; } } } void ResetCtx(EditorCtx* ctx) { InsertInputToBuf(ctx); ctx.state = ES.NormalMode; ctx.cmd.icount = 0; ctx.cmd.commands = []; ctx.cmd.current = cast(Command)NO_CMD; ctx.cmd.selected = 0; } bool Shift(Modifier md) { return cast(bool)(md & (MD.LeftShift | MD.RightShift)); } bool MovePanelFocus(A2D axis, bool prev)(UIPanel* panel) { bool result = false; if(!Nil(panel)) { UIPanel* target = prev ? panel.prev : panel.next; if(panel.parent.axis == axis && !Nil(target) && target.ed != null) { SetFocusedPanel(target); result = true; } else for(UIPanel* p = panel.parent; !Nil(p); p = p.parent) { if(p.parent.axis == axis) { for(UIPanel* t = prev ? p.prev : p.next; !Nil(t); t = prev ? t.prev : t.next) { UIPanel* f = t.first; if(!Nil(f) || f.ed == null) { while(f.ed == null) { if(Nil(f.first)) { break; } f = f.first; } } if(!Nil(f) && f.ed != null) { SetFocusedPanel(f); result = true; break; } } } } } return result; } void HandleInputs(EditorCtx* ctx, Inputs* inputs) { u8[] cb_text; UIPanel* panel = GetFocusedPanel(); FlatBuffer* fb = !Nil(panel) ? &panel.ed.buf : null; for(auto node = inputs.list.first; node != null; node = node.next) { bool taken = false; Input key = node.value.key; Modifier md = node.value.md; bool pressed = node.value.pressed; if (pressed) { if(key == Input.Escape) { ResetCtx(ctx); taken = true; } else if(ctx.state == ES.InputMode) { taken = HandleInputMode(ctx, node.value); } else if(ctx.state == ES.CmdOpen) { taken = HandleCmdMode(ctx, node.value); } else if(ctx.state == ES.SetPanelFocus) { switch(key) with(Input) { case Up: { if(MovePanelFocus!(A2D.Y, true )(panel)) goto case Escape; } break; case Down: { if(MovePanelFocus!(A2D.Y, false)(panel)) goto case Escape; } break; case Left: { if(MovePanelFocus!(A2D.X, true )(panel)) goto case Escape; } break; case Right: { if(MovePanelFocus!(A2D.X, false)(panel)) goto case Escape; } break; case Escape: { ResetCtx(ctx); taken = true; } break; default: break; } } else { switch(key) with(Input) { case a, i: { if(key == a && Shift(md) && fb) { MoveToEOL(fb); } else if(key == a) { Move(fb, Right, MD.None); } else if(key == i && Shift(md)) { MoveToSOL(fb); } ctx.state = ES.InputMode; taken = true; } break; case Semicolon: { if(Shift(node.value.md)) { ctx.state = ES.CmdOpen; taken = true; } } break; case v: { if(Ctrl(md)) { cb_text = ClipboardText(ctx.window); } if(Shift(md)) { ToggleSelection(fb, SM.Line); } else { ToggleSelection(fb, SM.Normal); } } break; case c: { if(Ctrl(md)) { // Copy taken = true; } } break; case w: { if(Ctrl(md)) { ctx.state = ES.SetPanelFocus; } } break; debug case d: { static bool dbg = false; dbg = !dbg; SetDebug(dbg); } break; debug case g: { g_frame_step = !g_frame_step; } break; debug case s: { g_frame_continue = true; } break; default: taken = Move(fb, key, md); break; } } } if(taken) { DLLRemove(&inputs.list, node, null); } } InsertInputToBuf(ctx); if(cb_text.length > 0) { Insert(fb, cb_text, cb_text.length); } } void MoveCursor(InputEvent ev) { UIPanel* p = GetFocusedPanel(); if(!Nil(p)) { FlatBuffer* fb = &p.ed.buf; switch(ev.key) with(Input) { case Up, Down, Left, Right: Move(fb, ev.key, ev.md); break; default: break; } } } pragma(inline) void InsertChar(EditorCtx* ctx, Input input, bool modified) { ctx.input_buf[ctx.icount++] = InputToChar(input, modified); } static string CharCases() { import std.traits; string result = ""; foreach(input; EnumMembers!Input) { u8 ch = InputToChar(input); if(ch > 0) { result ~= format("case Input.%s: InsertChar(ctx, Input.%s, cast(bool)(ev.md & (MD.LeftShift | MD.RightShift))); taken = true; break;\n", input, input); } } return result; } bool HandleInputMode(EditorCtx* ctx, InputEvent ev) { bool taken = false; switch(ev.key) { mixin(CharCases()); case Input.Backspace: { UIPanel* p = GetFocusedPanel(); if(!Nil(p)) { Backspace(&p.ed.buf); } } break; case Input.Escape: { ctx.state = ES.NormalMode; } break; default: MoveCursor(ev); break; } return taken; } static string TextLineCharCases() { import std.traits; string result = ""; foreach(input; EnumMembers!Input) { u8 ch = InputToChar(input); if(ch > 0 && ch != '\n' && ch != '\t' && ch != ' ') { if(ch == '\'' || ch == '\\') { result ~= format("case %s: result = '\\%s'; taken = true; break;\n", input, cast(char)ch); } else { result ~= format("case %s: result = '%s'; taken = true; break;\n", input, cast(char)ch); } } } return result; } pragma(inline) u8 Lower(u8 ch) { if(ch >= 65 && ch <= 90) { ch += 32; } return ch; } bool StrContains(bool begins_with)(const u8[] str, u8[] match) { return StrContains!(begins_with)(cast(u8[])str, match); } bool StrContains(bool begins_with)(u8[] str, u8[] match) { u64 count; for(u64 i = 0; i < str.length; i += 1) { static if(begins_with) if(i >= match.length) { break; } if(Lower(str[i]) == Lower(match[count])) { count += 1; } else { count = 0; } static if(!begins_with) if(count == match.length) { break; } } return count == match.length; } void GetCommands(CmdPalette* cmd) { const Command[] cmd_list = [ { name: CastStr!(u8)("open"), type: CT.OpenFile, }, { name: CastStr!(u8)("save"), type: CT.SaveFile, }, { name: CastStr!(u8)("create"), type: CT.CreateFile, }, { name: CastStr!(u8)("vsplit"), type: CT.VSplit, }, { name: CastStr!(u8)("hsplit"), type: CT.HSplit, }, ]; Reset(&cmd.arena); cmd.commands = Alloc!(Command)(&cmd.arena, cmd_list.length); cmd.params = []; u8[] str = cmd.buffer[0 .. cmd.icount]; u64 count = 0; if(str.length > 0) { for(u64 i = 0; i < cmd_list.length; i += 1) { if(StrContains!(true)(cmd_list[i].name, str)) { cmd.commands[count] = cast(Command)cmd_list[i]; count += 1; } } } if(count == 0 && cmd.icount == 0) { cmd.commands[] = cast(Command[])cmd_list[]; } else { cmd.commands = cmd.commands[0 .. count]; } cmd.opt_strs = Alloc!(u8[])(&cmd.arena, cmd.commands.length); for(u64 i = 0; i < cmd.commands.length; i += 1) { cmd.opt_strs[i] = cmd.commands[i].name; } } bool HandleCmdMode(EditorCtx* ctx, InputEvent ev) { u8 result = 0; bool taken = false; CmdPalette* cmd = &ctx.cmd; u64 prev_count = cmd.icount; switch(ev.key) with(Input) { case Enter: { if(cmd.current.type == CT.None && cmd.commands.length > 0) { if(cmd.commands[cmd.selected].type == CT.OpenFile) { goto case Tab; } else { cmd.current = cmd.commands[cmd.selected]; } } UIPanel* p = GetFocusedPanel(); switch(cmd.current.type) { case CT.OpenFile: { if(!Nil(p) && cmd.selected >= 0 && cmd.selected < cmd.opt_strs.length) { OpenFile(p.ed, cmd.opt_strs[cmd.selected]); } } break; case CT.SaveFile: { if(!Nil(p)) { SaveFile(p.ed, GetParam(cmd)); } } break; case CT.VSplit, CT.HSplit: { AddEditor(ctx, p, cmd.current.type == CT.VSplit ? A2D.X : A2D.Y); } break; default: break; } ResetCtx(ctx); } goto CmdInputEnd; case Backspace: { if(CondIncr!(-1)(cmd.icount > 0, &cmd.icount) && cmd.buffer[cmd.icount] == ' ') { cmd.current = cast(Command)NO_CMD; } } break; case Space: { Check(cmd, 1); cmd.buffer[cmd.icount++] = ' '; } goto case Tab; case Tab: { if(cmd.commands.length > 0) { cmd.current = cmd.commands[cmd.selected]; cmd.buffer[0 .. cmd.current.name.length] = cmd.current.name[0 .. $]; cmd.icount = cast(u32)cmd.current.name.length; cmd.buffer[cmd.icount++] = ' '; } } break; case Up: { CondIncr!(-1)(cmd.selected > 0, &cmd.selected); } break; case Down: { CondIncr!(+1)(cmd.selected < cmd.opt_strs.length-1, &cmd.selected); } break; mixin(TextLineCharCases()); default: break; } if(result != 0) { Check(cmd, 1); cmd.buffer[cmd.icount++] = result; } if(cmd.current.type == CT.None && (cmd.commands.length == 0 || prev_count != cmd.icount)) { GetCommands(cmd); } else if(prev_count != cmd.icount) { switch(cmd.current.type) with(CT) { case OpenFile: { PopulateParams(cmd, ctx.file_names); } break; case SaveFile: { u8[] param = GetParam(cmd); } break; default: break; } } CmdInputEnd: return taken; } pragma(inline) void Check(CmdPalette* cmd, u64 length) { if(cmd.icount+length >= cmd.buffer.length) { cmd.buffer = Realloc!(u8)(cmd.buffer, cmd.buffer.length*2); } } u8[] GetParam(CmdPalette* cmd) { u8[] param = []; for(u64 i = cmd.current.name.length; i < cmd.icount; i += 1) { if(cmd.buffer[i] != ' ') { param = cmd.buffer[i .. cmd.icount]; break; } } return param; } void PopulateParams(CmdPalette* cmd, u8[][] strs) { u8[] param = GetParam(cmd); if(cmd.params.length == 0 || !param.length) { cmd.params = Alloc!(Parameter)(&cmd.cmd_arena, strs.length); cmd.opt_strs = Alloc!(u8[])(&cmd.cmd_arena, strs.length); for(u64 i = 0; i < cmd.params.length; i += 1) { cmd.params[i].value = strs[i]; cmd.params[i].visible = true; cmd.opt_strs[i] = cmd.params[i].value; } } if(param.length) { cmd.opt_strs = Alloc!(u8[])(&cmd.cmd_arena, strs.length); u64 matches; for(u64 i = 0; i < cmd.params.length; i += 1) { bool contains = StrContains!(false)(cmd.params[i].value, param); if(contains) { cmd.opt_strs[matches] = cmd.params[i].value; matches += 1; } cmd.params[i].visible = contains; } cmd.opt_strs = cmd.opt_strs[0 .. matches]; } if(cmd.selected >= cmd.opt_strs.length) { cmd.selected = Max(cmd.opt_strs.length, 0); } }