Auto format textEditor

This commit is contained in:
Dustin Swan 2026-04-06 19:53:58 -06:00
parent 88098a9ce0
commit b4372b69fa
No known key found for this signature in database
GPG key ID: 30D46587E2100467

View file

@ -1,98 +1,77 @@
@textEditor @textEditor
_ = "TODO _TODO = [
# R to replace instead of insert "# R to replace instead of insert",
# s to seek "# s to seek",
# w / b bug, can't select `textEditor` at start of line "# w / b bug, can't select `textEditor` at start of line",
# smart case searching "# smart case searching"
"; ];
textEditor = name \ textEditor = name \
# defaults = {};
# c = { ...defaults, ...config };
scale = 2; scale = 2;
charH = 12; charH = 12;
charW = 5; charW = 5;
lineGap = 1; lineGap = 1;
charGap = 2; charGap = 2;
lineH = charH * scale + lineGap; lineH = (charH * scale) + lineGap;
source = getModuleSource name source = getModuleSource name
| Some s \ s | (Some s) \ s
| None \ getSource name; | None \ getSource name;
lines = split "\n" source; lines = split "\n" source;
clampCursor = state \ clampCursor = state \
line = nth state.cursorRow state.lines ~ unwrapOr ""; line = unwrapOr "" (nth state.cursorRow state.lines);
newRow = max 0 state.cursorRow; newRow = max 0 state.cursorRow;
newRow2 = min (len state.lines - 1) newRow; newRow2 = min ((len state.lines) - 1) newRow;
maxCol = state.mode maxCol = state.mode
| Insert \ len line | Insert \ len line
| Normal \ max 0 (len line - 1); | Normal \ max 0 ((len line) - 1);
newCol2 = min maxCol (max 0 state.cursorCol); newCol2 = min maxCol (max 0 state.cursorCol);
state.{ cursorRow = newRow2, cursorCol = newCol2 }; state.{ cursorRow = newRow2, cursorCol = newCol2 };
charKind = ch \ charKind = ch \
ch == " " | True \ Space ch == " "
| True \ Space
| False \ (isWordChar ch | False \ (isWordChar ch
| True \ Word | True \ Word
| False \ Punct); | False \ Punct);
skipWhile = pred lines row col \ skipWhile = pred lines row col \
line = nth row lines ~ unwrapOr ""; line = unwrapOr "" (nth row lines);
col >= len line col >= (len line)
| True \ (row + 1 < len lines | True \ ((row + 1) < (len lines)
| True \ skipWhile pred lines (row + 1) 0 | True \ skipWhile pred lines (row + 1) 0
| False \ { row = row, col = col }) | False \ { row = row, col = col })
| False \ (pred (nth col line ~ unwrapOr "") | False \ (pred (unwrapOr "" (nth col line))
| True \ skipWhile pred lines row (col + 1) | True \ skipWhile pred lines row (col + 1)
| False \ { row = row, col = col }); | False \ { row = row, col = col });
nextWordStart = lines row col \ nextWordStart = lines row col \
line = nth row lines ~ unwrapOr ""; line = unwrapOr "" (nth row lines);
kind = charKind (nth col line ~ unwrapOr " "); kind = charKind (unwrapOr " " (nth col line));
pos = skipWhile (ch \ charKind ch == kind) lines row col; pos = skipWhile (ch \ (charKind ch) == kind) lines row col;
skipWhile (ch \ ch == " ") lines pos.row pos.col; skipWhile (ch \ ch == " ") lines pos.row pos.col;
skipBack = pred line row col lines \ skipBack = pred line row col lines \
col < 0 col < 0
| True \ (row > 0 | True \ (row > 0
| True \ ( | True \ (prevLine = unwrapOr "" (nth (row - 1) lines);
prevLine = nth (row - 1) lines ~ unwrapOr ""; skipBack pred prevLine (row - 1) ((len prevLine) - 1) lines)
skipBack pred prevLine (row - 1) (len prevLine - 1) lines)
| False \ { row = 0, col = 0 - 1 }) | False \ { row = 0, col = 0 - 1 })
| False \ (pred (nth col line ~ unwrapOr "") | False \ (pred (unwrapOr "" (nth col line))
| True \ skipBack pred line row (col - 1) lines | True \ skipBack pred line row (col - 1) lines
| False \ { row = row, col = col }); | False \ { row = row, col = col });
prevWordStart = lines row col \ prevWordStart = lines row col \
# step back 1
c = col - 1; c = col - 1;
r = row; r = row;
r2 = c < 0 r2 = c < 0
| True \ r - 1 | True \ r - 1
| False \ r; | False \ r;
c2 = c < 0 c2 = c < 0
| True \ (max 0 (len (nth (r - 1) lines ~ unwrapOr "") - 1)) | True \ max 0 ((len (unwrapOr "" (nth (r - 1) lines))) - 1)
| False \ c; | False \ c;
# no prev line
r2 < 0 r2 < 0
| True \ { row = 0, col = 0 } | True \ { row = 0, col = 0 }
| False \ ( | False \ (line = unwrapOr "" (nth r2 lines);
line = nth r2 lines ~ unwrapOr "";
# skip spaces backward
pos = skipBack (ch \ ch == " ") line r2 c2 lines; pos = skipBack (ch \ ch == " ") line r2 c2 lines;
# skip same kind backward kind = charKind (unwrapOr " " (nth pos.col (unwrapOr "" (nth pos.row lines))));
kind = charKind (nth pos.col (nth pos.row lines ~ unwrapOr "") ~ unwrapOr " "); pos2 = skipBack (ch \ (charKind ch) == kind) (unwrapOr "" (nth pos.row lines)) pos.row (pos.col - 1) lines;
pos2 = skipBack (ch \ charKind ch == kind) (nth pos.row lines ~ unwrapOr "") pos.row (pos.col - 1) lines; { row = pos2.row, col = pos2.col + 1 });
# go 1 ahead
{ row = pos2.row, col = pos2.col + 1 }
);
resolveMotion = motion state \ resolveMotion = motion state \
from = { row = state.cursorRow, col = state.cursorCol }; from = { row = state.cursorRow, col = state.cursorCol };
to = motion to = motion
@ -101,443 +80,387 @@ textEditor = name \
| WholeLine \ { row = from.row, col = 0, linewise = True } | WholeLine \ { row = from.row, col = 0, linewise = True }
| StartOfLine \ { row = from.row, col = 0 } | StartOfLine \ { row = from.row, col = 0 }
| StartOfLineChars \ nextWordStart state.lines from.row 0 | StartOfLineChars \ nextWordStart state.lines from.row 0
| EndOfLine \ ( | EndOfLine \ (line = unwrapOr "" (nth state.cursorRow state.lines);
line = nth state.cursorRow state.lines ~ unwrapOr ""; { row = state.cursorRow, col = len line })
{ row = state.cursorRow, col = (len line) }) | FirstLine \ (line = unwrapOr "" (nth 0 state.lines);
| FirstLine \ ( { row = 0, col = (len line) - 1 })
line = nth 0 state.lines ~ unwrapOr ""; | LastLine \ (lastRow = (len state.lines) - 1;
{ row = 0, col = len line - 1 }) line = unwrapOr "" (nth lastRow state.lines);
| LastLine \ ( { row = lastRow, col = (len line) - 1 })
lastRow = len state.lines - 1;
line = nth lastRow state.lines ~ unwrapOr "";
{ row = lastRow, col = len line - 1 })
| Cursor \ { row = state.cursorRow, col = state.cursorCol + 1 }; | Cursor \ { row = state.cursorRow, col = state.cursorCol + 1 };
{ from = from, to = to }; { from = from, to = to };
deleteLine = state \ deleteLine = state \
newLines = [...(take (state.cursorRow) state.lines), ...(drop (state.cursorRow + 1) state.lines)]; newLines = [
...take state.cursorRow state.lines,
...drop (state.cursorRow + 1) state.lines
];
{ state = state.{ lines = newLines }, emit = [] }; { state = state.{ lines = newLines }, emit = [] };
deleteRange = state from to \ deleteRange = state from to \
line = nth from.row state.lines ~ unwrapOr ""; line = unwrapOr "" (nth from.row state.lines);
startCol = min from.col to.col; startCol = min from.col to.col;
endCol = max from.col to.col; endCol = max from.col to.col;
newLine = (slice line 0 startCol) & (slice line endCol (len line)); newLine = (slice line 0 startCol) & (slice line endCol (len line));
state.{ lines = updateAt from.row (_ \ newLine) state.lines, cursorCol = startCol }; state.{ lines = updateAt from.row (_ \ newLine) state.lines, cursorCol = startCol };
applyOperator = operator motion state \ applyOperator = operator motion state \
target = resolveMotion motion state; target = resolveMotion motion state;
action = { operator = operator, motion = motion }; action = { operator = operator, motion = motion };
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; stateSnapshot = {
lines = state.lines,
cursorRow = state.cursorRow,
cursorCol = state.cursorCol
};
newState = operator newState = operator
| Delete \ (hasField "linewise" target.to | Delete \ (hasField "linewise" target.to
| True \ deleteLine state | True \ deleteLine state
| False \ { state = deleteRange state target.from target.to, emit = [] }); | False \ {
{ state = clampCursor newState.state.{ undoStack = [stateSnapshot, ...state.undoStack], pending = None, lastAction = Some action }, emit = newState.emit }; state = deleteRange state target.from target.to,
emit = []
moveCursor = motion state \ });
state.{ cursorCol = motion.to.col, cursorRow = motion.to.row }; {
state = clampCursor newState.state.{ undoStack = [stateSnapshot, ...state.undoStack], pending = None, lastAction = Some action },
emit = newState.emit
};
moveCursor = motion state \ state.{ cursorCol = motion.to.col, cursorRow = motion.to.row };
write = state \ write = state \
content = join "\n" state.lines; content = join "\n" state.lines;
{ state = state, emit = [rebindAt [buffersKey, name] content] }; {
state = state,
emit = [rebindAt [buffersKey, name] content]
};
apply = state \ apply = state \
content = (join "\n" state.lines); content = join "\n" state.lines;
result = eval! content; isModule = getModuleSource name
_ = debug! "apply" [content, result]; | (Some _) \ True
| None \ False;
result = isModule
| True \ applyModule! name content
| False \ eval! content;
_ = debug! "apply" result;
result result
| Defined _ \ { state = state, emit = [] } | (Defined _) \ { state = state, emit = [] }
| Err msg \ ( | (Err msg) \ (_ = debug! "error applying" [];
_ = debug! "error applying" []; { state = state, emit = [] })
{ state = state, emit = [] }
)
| _ \ { state = state, emit = [] }; | _ \ { state = state, emit = [] };
insertChar = char state \ insertChar = char state \
newLines = updateAt state.cursorRow (line \ newLines = updateAt state.cursorRow (line \ insertCharAt line state.cursorCol char) state.lines;
insertCharAt line state.cursorCol char {
) state.lines; state = state.{ lines = newLines, cursorCol = state.cursorCol + 1 },
emit = []
{ state = state.{ lines = newLines, cursorCol = state.cursorCol + 1 }, emit = [] }; };
replaceChar = char state \ replaceChar = char state \
newLines = updateAt state.cursorRow (line \ newLines = updateAt state.cursorRow (line \
newLine = deleteCharAt line state.cursorCol; newLine = deleteCharAt line state.cursorCol;
insertCharAt newLine state.cursorCol char insertCharAt newLine state.cursorCol char) state.lines;
) state.lines; stateSnapshot = {
lines = state.lines,
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; cursorRow = state.cursorRow,
newUndoStack = [stateSnapshot, ...state.undoStack]; cursorCol = state.cursorCol
};
{ state = state.{ lines = newLines, mode = Normal, undoStack = newUndoStack }, emit = [] };
insertMode = state \
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol };
newUndoStack = [stateSnapshot, ...state.undoStack]; newUndoStack = [stateSnapshot, ...state.undoStack];
{ {
state = state.{ state = state.{ lines = newLines, mode = Normal, undoStack = newUndoStack },
undoStack = newUndoStack, emit = []
mode = Insert };
}, insertMode = state \
stateSnapshot = {
lines = state.lines,
cursorRow = state.cursorRow,
cursorCol = state.cursorCol
};
newUndoStack = [stateSnapshot, ...state.undoStack];
{
state = state.{ undoStack = newUndoStack, mode = Insert },
emit = [] emit = []
}; };
append = state \ insertMode state.{ cursorCol = state.cursorCol + 1 }; append = state \ insertMode state.{ cursorCol = state.cursorCol + 1 };
appendEndLine = state \ appendEndLine = state \
line = nth state.cursorRow state.lines ~ unwrapOr ""; line = unwrapOr "" (nth state.cursorRow state.lines);
insertMode state.{ cursorCol = len line }; insertMode state.{ cursorCol = len line };
openLine = state \ openLine = state \
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; stateSnapshot = {
lines = state.lines,
cursorRow = state.cursorRow,
cursorCol = state.cursorCol
};
newUndoStack = [stateSnapshot, ...state.undoStack]; newUndoStack = [stateSnapshot, ...state.undoStack];
cursorRow = state.cursorRow + 1; cursorRow = state.cursorRow + 1;
newLines = insertAt cursorRow "" state.lines; newLines = insertAt cursorRow "" state.lines;
{ {
state = state.{ state = state.{ lines = newLines, cursorRow = cursorRow, cursorCol = 0, undoStack = newUndoStack, mode = Insert },
lines = newLines,
cursorRow = cursorRow,
cursorCol = 0,
undoStack = newUndoStack,
mode = Insert
},
emit = [] emit = []
}; };
openLineAbove = state \ openLine state.{ cursorRow = state.cursorRow - 1 }; openLineAbove = state \ openLine state.{ cursorRow = state.cursorRow - 1 };
escape = state \ {
escape = state \ { state = state.{ mode = Normal, pending = None }, emit = [] }; state = state.{ mode = Normal, pending = None },
emit = []
undo = state \ state.undoStack };
undo = state \
state.undoStack
| [] \ { state = state, emit = [] } | [] \ { state = state, emit = [] }
| [prev, ...rest] \ ( | [prev, ...rest] \ (stateSnapshot = {
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; lines = state.lines,
cursorRow = state.cursorRow,
{ state = state.{ lines = prev.lines, cursorCol = prev.cursorCol, cursorRow = prev.cursorRow, undoStack = rest, redoStack = [stateSnapshot, ...state.redoStack] }, emit = [] }); cursorCol = state.cursorCol
};
redo = state \ state.redoStack {
state = state.{ lines = prev.lines, cursorCol = prev.cursorCol, cursorRow = prev.cursorRow, undoStack = rest, redoStack = [stateSnapshot, ...state.redoStack] },
emit = []
});
redo = state \
state.redoStack
| [] \ { state = state, emit = [] } | [] \ { state = state, emit = [] }
| [prev, ...rest] \ ( | [prev, ...rest] \ (stateSnapshot = {
stateSnapshot = { lines = state.lines, cursorRow = state.cursorRow, cursorCol = state.cursorCol }; lines = state.lines,
cursorRow = state.cursorRow,
{ state = state.{ lines = prev.lines, cursorCol = prev.cursorCol, cursorRow = prev.cursorRow, undoStack = [stateSnapshot, ...state.undoStack], redoStack = rest }, emit = [] }); cursorCol = state.cursorCol
};
{
state = state.{ lines = prev.lines, cursorCol = prev.cursorCol, cursorRow = prev.cursorRow, undoStack = [stateSnapshot, ...state.undoStack], redoStack = rest },
emit = []
});
backspace = state \ backspace = state \
state.cursorCol == 0 state.cursorCol == 0
| True \ ( | True \ (state.cursorRow == 0
state.cursorRow == 0
| True \ { state = state, emit = [] } | True \ { state = state, emit = [] }
| False \ ( | False \ { state = state, emit = [] })
{ state = state, emit = [] } # todo, join with previous line | False \ (newLines = updateAt state.cursorRow (line \
)
)
| False \ (
newLines = updateAt state.cursorRow (line \
before = slice line 0 (state.cursorCol - 1); before = slice line 0 (state.cursorCol - 1);
after = slice line state.cursorCol (len line); after = slice line state.cursorCol (len line);
before & after) state.lines; before & after) state.lines;
{
{ state = state.{ lines = newLines, cursorCol = state.cursorCol - 1 }, emit = [] }); state = state.{ lines = newLines, cursorCol = state.cursorCol - 1 },
emit = []
});
enter = state \ enter = state \
line = nth state.cursorRow state.lines ~ unwrapOr ""; line = unwrapOr "" (nth state.cursorRow state.lines);
before = slice line 0 state.cursorCol; before = slice line 0 state.cursorCol;
after = slice line state.cursorCol (len line); after = slice line state.cursorCol (len line);
newLines = updateAt state.cursorRow (_ \ before) state.lines; newLines = updateAt state.cursorRow (_ \ before) state.lines;
newLines2 = insertAt (state.cursorRow + 1) after newLines; newLines2 = insertAt (state.cursorRow + 1) after newLines;
{
{ state = state.{ lines = newLines2, cursorCol = 0, cursorRow = state.cursorRow + 1 }, emit = [] }; state = state.{ lines = newLines2, cursorCol = 0, cursorRow = state.cursorRow + 1 },
emit = []
};
upArrow = state \ upArrow = state \
newState = clampCursor state.{ cursorRow = state.cursorRow - 1 }; newState = clampCursor state.{ cursorRow = state.cursorRow - 1 };
{ state = newState, emit = [] }; { state = newState, emit = [] };
downArrow = state \ downArrow = state \
newState = clampCursor state.{ cursorRow = state.cursorRow + 1 }; newState = clampCursor state.{ cursorRow = state.cursorRow + 1 };
{ state = newState, emit = [] }; { state = newState, emit = [] };
leftArrow = state \ leftArrow = state \
newState = clampCursor state.{ cursorCol = state.cursorCol - 1 }; newState = clampCursor state.{ cursorCol = state.cursorCol - 1 };
{ state = newState, emit = [] }; { state = newState, emit = [] };
rightArrow = state \ rightArrow = state \
newState = clampCursor state.{ cursorCol = state.cursorCol + 1 }; newState = clampCursor state.{ cursorCol = state.cursorCol + 1 };
{ state = newState, emit = [] }; { state = newState, emit = [] };
findNext = query lines row col \ findNext = query lines row col \
rest = strIndexOf query (nth row lines ~ unwrapOr "") col; rest = strIndexOf query (unwrapOr "" (nth row lines)) col;
rest rest
| Some c \ { row = row, col = c } | (Some c) \ { row = row, col = c }
| None \ ( | None \ (findInLines = r \
findInLines = r \ r >= (len lines)
r >= len lines | True \ findInLines 0
| True \ findInLines 0 # wrap to top
| False \ (r == row | False \ (r == row
| True \ (strIndexOf query (nth row lines ~ unwrapOr "") 0 | True \ (strIndexOf query (unwrapOr "" (nth row lines)) 0
| Some c \ { row = row, col = c } | (Some c) \ { row = row, col = c }
| None \ { row = row, col = col }) # no match | None \ { row = row, col = col })
| False \ (strIndexOf query (nth r lines ~ unwrapOr "") 0 | False \ (strIndexOf query (unwrapOr "" (nth r lines)) 0
| Some c \ { row = r, col = c } | (Some c) \ { row = r, col = c }
| None \ findInLines (r + 1))); | None \ findInLines (r + 1)));
findInLines (row + 1)); findInLines (row + 1));
findPrev = query lines row col \ findPrev = query lines row col \
line = slice (nth row lines ~ unwrapOr "") 0 col; line = slice (unwrapOr "" (nth row lines)) 0 col;
rest = strLastIndexOf query line; rest = strLastIndexOf query line;
rest rest
| Some c \ { row = row, col = c } | (Some c) \ { row = row, col = c }
| None \ ( | None \ (findInLines = r \
findInLines = r \
r < 0 r < 0
| True \ findInLines (len lines - 1) # wrap to bottom | True \ findInLines ((len lines) - 1)
| False \ (r == row | False \ (r == row
| True \ (strLastIndexOf query (nth row lines ~ unwrapOr "") | True \ (strLastIndexOf query (unwrapOr "" (nth row lines))
| Some c \ { row = row, col = c } | (Some c) \ { row = row, col = c }
| None \ { row = row, col = col }) # no match | None \ { row = row, col = col })
| False \ (strLastIndexOf query (nth r lines ~ unwrapOr "") | False \ (strLastIndexOf query (unwrapOr "" (nth r lines))
| Some c \ { row = r, col = c } | (Some c) \ { row = r, col = c }
| None \ findInLines (r - 1))); | None \ findInLines (r - 1)));
findInLines (row - 1)); findInLines (row - 1));
{ view = ctx \
{
view = ctx \
autoScroll = state \ autoScroll = state \
cursorY = state.cursorRow * lineH; cursorY = state.cursorRow * lineH;
newScrollY = (cursorY < state.scrollY newScrollY = cursorY < state.scrollY
| True \ cursorY | True \ cursorY
| False \ (cursorY + lineH > state.scrollY + ctx.h | False \ ((cursorY + lineH) > (state.scrollY + ctx.h)
| True \ cursorY + lineH - ctx.h | True \ (cursorY + lineH) - ctx.h
| False \ state.scrollY)); | False \ state.scrollY);
cursorX = ((state.cursorCol * charW) * scale) + (charGap * state.cursorCol);
cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; newScrollX = cursorX < state.scrollX
newScrollX = (cursorX < state.scrollX
| True \ cursorX | True \ cursorX
| False \ (cursorX + charW * scale > state.scrollX + ctx.w | False \ ((cursorX + (charW * scale)) > (state.scrollX + ctx.w)
| True \ cursorX + charW * scale - ctx.w | True \ (cursorX + (charW * scale)) - ctx.w
| False \ state.scrollX)); | False \ state.scrollX);
state.{ scrollY = newScrollY, scrollX = newScrollX }; state.{ scrollY = newScrollY, scrollX = newScrollX };
withScroll = result \ { state = autoScroll result.state, emit = result.emit };
withScroll = result \
{ state = autoScroll result.state, emit = result.emit };
# search stuff
initSearch = state \ initSearch = state \
newState = state.{ newState = state.{ mode = Search, searchQuery = "", savedCursorCol = state.cursorCol, savedCursorRow = state.cursorRow };
mode = Search,
searchQuery = "",
savedCursorCol = state.cursorCol,
savedCursorRow = state.cursorRow,
};
{ state = newState, emit = [] }; { state = newState, emit = [] };
cancelSearch = state \ cancelSearch = state \
newState = state.{ newState = state.{ mode = Normal, searchQuery = "", cursorCol = state.savedCursorCol, cursorRow = state.savedCursorRow };
mode = Normal,
searchQuery = "",
cursorCol = state.savedCursorCol,
cursorRow = state.savedCursorRow,
};
{ state = newState, emit = [] }; { state = newState, emit = [] };
updateSearchQuery = query state \ updateSearchQuery = query state \
next = findNext query state.lines state.cursorRow state.cursorCol; next = findNext query state.lines state.cursorRow state.cursorCol;
newState = autoScroll state.{ newState = autoScroll state.{ searchQuery = query, cursorCol = next.col, cursorRow = next.row };
searchQuery = query,
cursorCol = next.col,
cursorRow = next.row,
};
{ state = newState, emit = [] }; { state = newState, emit = [] };
backspaceSearch = state \ updateSearchQuery (slice state.searchQuery 0 ((len state.searchQuery) - 1)) state;
backspaceSearch = state \ addCharToSearchQuery = char state \ updateSearchQuery (state.searchQuery & char) state;
updateSearchQuery (slice state.searchQuery 0 (len state.searchQuery - 1)) state; endSearch = state \ { state = autoScroll state.{ mode = Normal }, emit = [] };
addCharToSearchQuery = char state \
updateSearchQuery (state.searchQuery & char) state;
endSearch = state \
{ state = autoScroll state.{ mode = Normal }, emit = [] };
nextSearch = state \ nextSearch = state \
next = findNext state.searchQuery state.lines state.cursorRow (state.cursorCol + 1); next = findNext state.searchQuery state.lines state.cursorRow (state.cursorCol + 1);
newState = autoScroll state.{ newState = autoScroll state.{ mode = Normal, cursorCol = next.col, cursorRow = next.row };
mode = Normal,
cursorCol = next.col,
cursorRow = next.row,
};
{ state = newState, emit = [] }; { state = newState, emit = [] };
prevSearch = state \ prevSearch = state \
prev = findPrev state.searchQuery state.lines state.cursorRow state.cursorCol; prev = findPrev state.searchQuery state.lines state.cursorRow state.cursorCol;
newState = autoScroll state.{ newState = autoScroll state.{ mode = Normal, cursorCol = prev.col, cursorRow = prev.row };
mode = Normal,
cursorCol = prev.col,
cursorRow = prev.row,
};
{ state = newState, emit = [] }; { state = newState, emit = [] };
scrollHalfUp = state ctx \
scrollHalfUp = state ctx \ (
diff = floor ((ctx.h / 2) / lineH); diff = floor ((ctx.h / 2) / lineH);
newRow = state.cursorRow - diff; newRow = state.cursorRow - diff;
withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); withScroll {
state = clampCursor state.{ cursorRow = newRow },
scrollHalfDown = state ctx \ ( emit = []
};
scrollHalfDown = state ctx \
diff = floor ((ctx.h / 2) / lineH); diff = floor ((ctx.h / 2) / lineH);
newRow = state.cursorRow + diff; newRow = state.cursorRow + diff;
withScroll { state = clampCursor state.{ cursorRow = newRow }, emit = [] }); withScroll {
state = clampCursor state.{ cursorRow = newRow },
emit = []
};
handleMotion = motion state \ handleMotion = motion state \
state.pending state.pending
| None \ withScroll { state = moveCursor (resolveMotion motion state) state, emit = [] } | None \ withScroll {
| Some Delete \ withScroll (applyOperator Delete motion state); state = moveCursor (resolveMotion motion state) state,
emit = []
}
| (Some Delete) \ withScroll (applyOperator Delete motion state);
ui.stateful { ui.stateful {
focusable = True, focusable = True,
autoFocus = True, autoFocus = True,
key = "textEditor-" & name, key = "textEditor-" & name,
init = { init = {
mode = Normal, # Normal | Insert | Visual | Search mode = Normal,
lines = lines, lines = lines,
cursorRow = 0, cursorRow = 0,
cursorCol = 0, cursorCol = 0,
scrollX = 0, scrollX = 0,
scrollY = 0, scrollY = 0,
pending = None,
pending = None, # Some "d" | Some "g" | etc. lastAction = None,
lastAction = None, # Some { operator, motion },
undoStack = [], undoStack = [],
redoStack = [], redoStack = [],
searchQuery = "", searchQuery = "",
searchIndex = 0, searchIndex = 0
}, },
update = state event \
update = state event \ state.mode state.mode
| Search \ (event | Search \ (event
| Key { key = "Escape" } \ cancelSearch state | (Key {key = "Escape"}) \ cancelSearch state
| Key { key = "Enter" } \ endSearch state | (Key {key = "Enter"}) \ endSearch state
| Key { key = "Backspace" } \ backspaceSearch state | (Key {key = "Backspace"}) \ backspaceSearch state
| Key { key = k, printable = True } \ addCharToSearchQuery k state | (Key {key = k, printable = True}) \ addCharToSearchQuery k state
| _ \ { state = state , emit = [] }) | _ \ { state = state, emit = [] })
| Insert \ (event | Insert \ (event
| Key { key = "Escape" } \ escape state | (Key {key = "Escape"}) \ escape state
| Key { key = "Control" } \ escape state | (Key {key = "Control"}) \ escape state
| Key { key = "Backspace" } \ backspace state | (Key {key = "Backspace"}) \ backspace state
| Key { key = "Enter" } \ enter state | (Key {key = "Enter"}) \ enter state
| Key { key = k, printable = True } \ insertChar k state | (Key {key = k, printable = True}) \ insertChar k state
| _ \ { state = state , emit = [] }) | _ \ { state = state, emit = [] })
| ReplaceChar \ (event | ReplaceChar \ (event
| Key { key = k, printable = True } \ replaceChar k state | (Key {key = k, printable = True}) \ replaceChar k state
| _ \ { state = state.{ mode = Normal }, emit = [] }) | _ \ { state = state.{ mode = Normal }, emit = [] })
| Normal \ (event | Normal \ (event
| Scrolled delta \ ( | (Scrolled delta) \ (maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines;
maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; totalW = ((maxLineLen * charW) * scale) + (maxLineLen * charGap);
totalW = maxLineLen * charW * scale + maxLineLen * charGap; totalH = (len state.lines) * lineH;
totalH = len state.lines * lineH;
newX = max 0 (min (totalW - ctx.w) (state.scrollX + delta.deltaX)); newX = max 0 (min (totalW - ctx.w) (state.scrollX + delta.deltaX));
newY = max 0 (min (totalH - ctx.h) (state.scrollY + delta.deltaY)); newY = max 0 (min (totalH - ctx.h) (state.scrollY + delta.deltaY));
{ state = state.{ scrollY = newY, scrollX = newX }, emit = [] }) {
state = state.{ scrollY = newY, scrollX = newX },
| Key { key = "ArrowDown" } \ withScroll (downArrow state) emit = []
| Key { key = "j" } \ withScroll (downArrow state) })
| Key { key = "ArrowUp" } \ withScroll (upArrow state) | (Key {key = "W", ctrl = True, shift = True}) \ write state
| Key { key = "k" } \ withScroll(upArrow state) | (Key {key = "A", ctrl = True, shift = True}) \ apply state
| Key { key = "ArrowLeft" } \ withScroll (leftArrow state) | (Key {key = "ArrowDown"}) \ withScroll (downArrow state)
| Key { key = "h" } \ withScroll(leftArrow state) | (Key {key = "j"}) \ withScroll (downArrow state)
| Key { key = "ArrowRight" } \ withScroll (rightArrow state) | (Key {key = "ArrowUp"}) \ withScroll (upArrow state)
| Key { key = "l" } \ withScroll(rightArrow state) | (Key {key = "k"}) \ withScroll (upArrow state)
| Key { key = "a" } \ append state | (Key {key = "ArrowLeft"}) \ withScroll (leftArrow state)
| Key { key = "A" } \ appendEndLine state | (Key {key = "h"}) \ withScroll (leftArrow state)
| Key { key = "i" } \ insertMode state | (Key {key = "ArrowRight"}) \ withScroll (rightArrow state)
| Key { key = "o" } \ openLine state | (Key {key = "l"}) \ withScroll (rightArrow state)
| Key { key = "O" } \ openLineAbove state | (Key {key = "a"}) \ append state
| Key { key = "d", ctrl = True } \ scrollHalfDown state ctx | (Key {key = "A"}) \ appendEndLine state
| Key { key = "u", ctrl = True } \ scrollHalfUp state ctx | (Key {key = "i"}) \ insertMode state
| Key { key = "u" } \ undo state | (Key {key = "o"}) \ openLine state
| Key { key = "r", ctrl = True } \ redo state | (Key {key = "O"}) \ openLineAbove state
| Key { key = "r" } \ { state = state.{ mode = ReplaceChar }, emit = [] } | (Key {key = "d", ctrl = True}) \ scrollHalfDown state ctx
| Key { key = "x" } \ applyOperator Delete Cursor state | (Key {key = "u", ctrl = True}) \ scrollHalfUp state ctx
| Key { key = "d" } \ (state.pending | (Key {key = "u"}) \ undo state
| Some Delete \ applyOperator Delete WholeLine state | (Key {key = "r", ctrl = True}) \ redo state
| (Key {key = "r"}) \ { state = state.{ mode = ReplaceChar }, emit = [] }
| (Key {key = "x"}) \ applyOperator Delete Cursor state
| (Key {key = "d"}) \ (state.pending
| (Some Delete) \ applyOperator Delete WholeLine state
| _ \ { state = state.{ pending = Some Delete }, emit = [] }) | _ \ { state = state.{ pending = Some Delete }, emit = [] })
| Key { key = "b" } \ handleMotion BackWord state | (Key {key = "b"}) \ handleMotion BackWord state
| Key { key = "w" } \ handleMotion Word state | (Key {key = "w"}) \ handleMotion Word state
| Key { key = "0" } \ handleMotion StartOfLine state | (Key {key = "0"}) \ handleMotion StartOfLine state
| Key { key = "^" } \ handleMotion StartOfLineChars state | (Key {key = "^"}) \ handleMotion StartOfLineChars state
| Key { key = "$" } \ handleMotion EndOfLine state | (Key {key = "$"}) \ handleMotion EndOfLine state
| Key { key = "g" } \ (state.pending | (Key {key = "g"}) \ (state.pending
| Some "g" \ handleMotion FirstLine state.{ pending = None } | (Some "g") \ handleMotion FirstLine state.{ pending = None }
| _ \ { state = state.{ pending = Some "g" }, emit = [] }) | _ \ { state = state.{ pending = Some "g" }, emit = [] })
| Key { key = "G" } \ handleMotion LastLine state | (Key {key = "G"}) \ handleMotion LastLine state
| Key { key = "." } \ (state.lastAction | (Key {key = "."}) \ (state.lastAction
| None \ { state = state, emit = [] } | None \ { state = state, emit = [] }
| Some action \ applyOperator action.operator action.motion state) | (Some action) \ applyOperator action.operator action.motion state)
# Search | (Key {key = "/"}) \ initSearch state
| Key { key = "/" } \ initSearch state | (Key {key = "n"}) \ nextSearch state
| Key { key = "n" } \ nextSearch state | (Key {key = "N"}) \ prevSearch state
| Key { key = "N" } \ prevSearch state | (Key {key = "Escape"}) \ escape state
| _ \ { state = state, emit = [] }),
| Key { key = "W", ctrl = True, shift = True } \ write state
| Key { key = "A", ctrl = True, shift = True } \ apply state
| Key { key = "Escape" } \ escape state
# any other key or event
| _ \ { state = state, emit = [] }
),
view = state emit \ view = state emit \
cursorX = state.cursorCol * charW * scale + charGap * state.cursorCol; cursorX = ((state.cursorCol * charW) * scale) + (charGap * state.cursorCol);
cursorY = state.cursorRow * lineH; cursorY = state.cursorRow * lineH;
cursor = ui.positioned { cursor = ui.positioned {
x = cursorX, x = cursorX,
y = cursorY, y = cursorY,
child = ui.rect { w = charW * scale, h = charH * scale, color = "rgba(255,255,255,0.5)" } child = ui.rect {
w = charW * scale,
h = charH * scale,
color = "rgba(255,255,255,0.5)"
}
}; };
maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines; maxLineLen = fold (acc line \ max acc (len line)) 0 state.lines;
totalWidth = maxLineLen * charW * scale + maxLineLen * charGap; totalWidth = ((maxLineLen * charW) * scale) + (maxLineLen * charGap);
totalHeight = len state.lines * lineH; totalHeight = (len state.lines) * lineH;
highlights = state.searchQuery == "" highlights = state.searchQuery == ""
| True \ [] | True \ []
| False \ findAll state.searchQuery state.lines; | False \ findAll state.searchQuery state.lines;
highlightsUi = map (c \ ui.positioned {
highlightsUi = map (c \ x = ((c.col * charW) * scale) + (charGap * c.col),
ui.positioned {
x = c.col * charW * scale + charGap * c.col,
y = c.row * lineH, y = c.row * lineH,
child = ui.rect { child = ui.rect {
w = (len state.searchQuery) * charW * scale + charGap * (len state.searchQuery - 1), w = (((len state.searchQuery) * charW) * scale) + (charGap * ((len state.searchQuery) - 1)),
h = charH * scale, h = charH * scale,
color = "rgba(255,200,0,0.5)" color = "rgba(255,200,0,0.5)"
} }
} }) highlights;
) highlights;
# buffer = map (l \ text l) state.lines;
# only render visible lines
startLine = max 0 (floor (state.scrollY / lineH)); startLine = max 0 (floor (state.scrollY / lineH));
visibleCount = floor (ctx.h / lineH) + 2; visibleCount = (floor (ctx.h / lineH)) + 2;
endLine = min (len state.lines) (startLine + visibleCount); endLine = min (len state.lines) (startLine + visibleCount);
visibleLines = slice state.lines startLine endLine; visibleLines = slice state.lines startLine endLine;
buffer = mapWithIndex (l i \ buffer = mapWithIndex (l i \ ui.positioned { x = 0, y = (startLine + i) * lineH, child = text l }) visibleLines;
ui.positioned {
x = 0,
y = (startLine + i) * lineH,
child = text l
}
) visibleLines;
scrollable { scrollable {
w = ctx.w, w = ctx.w,
h = ctx.h, h = ctx.h,
@ -546,14 +469,8 @@ textEditor = name \
scrollX = state.scrollX, scrollX = state.scrollX,
scrollY = state.scrollY, scrollY = state.scrollY,
onScroll = delta \ emit (Scrolled delta), onScroll = delta \ emit (Scrolled delta),
child = ui.stack { child = ui.stack { children = [...buffer, cursor, ...highlightsUi] }
children = [
...buffer,
cursor,
...highlightsUi
]
} }
} } };
}
};
@ @