editor/src/editor/editor.d

1073 lines
18 KiB
D

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);
}
}