Adding basic search to the text editor

This commit is contained in:
Dustin Swan 2026-04-05 20:42:52 -06:00
parent 8e05690ef3
commit 4a62774a57
No known key found for this signature in database
GPG key ID: 30D46587E2100467
3 changed files with 131 additions and 15 deletions

View file

@ -160,6 +160,15 @@ find : (a \ Bool) \ List a \ Maybe a = f list \ list
| True \ Some x
| False \ find f xs);
findAll = query lines \
mapWithIndex (line row \
findInLine = col \
strIndexOf query line col
| None \ []
| Some i \ [{ row = row, col = i }, ...findInLine (i + 1)];
findInLine 0
) lines ~ flatten;
flatten : List (List a) \ List a = lists \ fold cat [] lists;
and : Bool \ Bool \ Bool = x y \ x

View file

@ -1,7 +1,5 @@
@textEditor
textEditorBuffers = [];
textEditor = name \
# defaults = {};
# c = { ...defaults, ...config };
@ -12,13 +10,6 @@ textEditor = name \
charGap = 2;
lineH = charH * scale + lineGap;
buffersKey = "textEditorBuffers";
# load from staging buffers if it exists there. if not, load from source
# source = getAt [buffersKey, name]
# | None \ getSource name
# | Some v \ v;
source = getModuleSource name
| Some s \ s
| None \ getSource name;
@ -251,6 +242,25 @@ textEditor = name \
newState = clampCursor state.{ cursorCol = state.cursorCol + 1 };
{ state = newState, emit = [] };
findNext = query lines row col \
rest = strIndexOf query (nth row lines ~ unwrapOr "") (col + 1);
rest
| Some c \ { row = row, col = c }
| None \ (
findInLines = r \
r >= len lines
| True \ findInLines 0 # wrap to top
| False \ (r == row
| True \ (strIndexOf query (nth row lines ~ unwrapOr "") 0
| Some c \ { row = row, col = c }
| None \ { row = row, col = col }) # no match
| False \ (strIndexOf query (nth r lines ~ unwrapOr "") 0
| Some c \ { row = r, col = c }
| None \ findInLines (r + 1)));
findInLines (row + 1));
# TODO: findPrev
{
view = ctx \
@ -273,6 +283,62 @@ textEditor = name \
withScroll = result \
{ state = autoScroll result.state, emit = result.emit };
# search stuff
initSearch = state \
newState = state.{
mode = Search,
searchQuery = "",
savedCursorCol = state.cursorCol,
savedCursorRow = state.cursorRow,
};
{ state = newState, emit = [] };
cancelSearch = state \
newState = state.{
mode = Normal,
searchQuery = "",
cursorCol = state.savedCursorCol,
cursorRow = state.savedCursorRow,
};
{ state = newState, emit = [] };
updateSearchQuery = query state \
next = findNext query state.lines state.cursorRow state.cursorCol;
newState = autoScroll state.{
searchQuery = query,
cursorCol = next.col,
cursorRow = next.row,
};
{ state = newState, emit = [] };
backspaceSearch = state \
updateSearchQuery (slice state.searchQuery 0 (len state.searchQuery - 1)) state;
addCharToSearchQuery = char state \
updateSearchQuery (state.searchQuery & char) state;
endSearch = state \
# next = findNext state.searchQuery state.lines state.cursorRow state.cursorCol;
newState = autoScroll state.{
mode = Normal,
# cursorCol = next.col,
# cursorRow = next.row,
};
{ state = newState, emit = [] };
nextSearch = state \
next = findNext state.searchQuery state.lines state.cursorRow state.cursorCol;
newState = autoScroll state.{
mode = Normal,
cursorCol = next.col,
cursorRow = next.row,
};
{ state = newState, emit = [] };
scrollHalfUp = state ctx \ (
diff = floor ((ctx.h / 2) / lineH);
newRow = state.cursorRow - diff;
@ -295,19 +361,30 @@ textEditor = name \
key = "textEditor-" & name,
init = {
mode = Normal, # Normal | Insert | Visual | Search
lines = lines,
cursorRow = 0,
cursorCol = 0,
scrollX = 0,
scrollY = 0,
pending = None, # Some "d" | Some "g" | etc.
lastAction = None, # Some { operator, motion },
undoStack = [],
redoStack = [],
mode = Normal, # Normal | Insert | Visual
pending = None, # Some "d" | Some "g" | etc.
lastAction = None, # Some { operator, motion }
searchQuery = "",
searchIndex = 0,
},
update = state event \ state.mode
| Search \ (event
| Key { key = "Escape" } \ cancelSearch state
| Key { key = "Enter" } \ endSearch state
| Key { key = "Backspace" } \ backspaceSearch state
| Key { key = k, printable = True } \ addCharToSearchQuery k state
| _ \ { state = state , emit = [] })
| Insert \ (event
| Key { key = "Escape" } \ escape state
| Key { key = "Control" } \ escape state
@ -354,6 +431,11 @@ textEditor = name \
| Key { key = "." } \ (state.lastAction
| None \ { state = state, emit = [] }
| Some action \ applyOperator action.operator action.motion state)
# Search
| Key { key = "/" } \ initSearch state
| Key { key = "n" } \ nextSearch state
| Key { key = "N" } \ prevSearch state
| Key { key = "W", ctrl = True, shift = True } \ write state
| Key { key = "A", ctrl = True, shift = True } \ apply state
# any other key or event
@ -374,6 +456,22 @@ textEditor = name \
totalWidth = maxLineLen * charW * scale + maxLineLen * charGap;
totalHeight = len state.lines * lineH;
highlights = state.searchQuery == ""
| True \ []
| False \ findAll state.searchQuery state.lines;
highlightsUi = map (c \
ui.positioned {
x = c.col * charW * scale + charGap * c.col,
y = c.row * lineH,
child = ui.rect {
w = (len state.searchQuery) * charW * scale + charGap * (len state.searchQuery - 1),
h = charH * scale,
color = "rgba(255,200,0,0.5)"
}
}
) highlights;
# buffer = map (l \ text l) state.lines;
# only render visible lines
startLine = max 0 (floor (state.scrollY / lineH));
@ -399,7 +497,8 @@ textEditor = name \
child = ui.stack {
children = [
...buffer,
cursor
cursor,
...highlightsUi
]
}
}

View file

@ -79,6 +79,14 @@ export const _rt = {
return String(value);
},
chars: (s: string) => s.split(''),
strIndexOf: (needle: string) => (haystack: string) => (start: number) => {
const i = haystack.indexOf(needle, start);
return i === -1 ? { _tag: 'None' } : { _tag: 'Some', _0: i };
},
strLastIndexOf: (needle: string) => (haystack: string) => {
const i = haystack.lastIndexOf(needle);
return i === -1 ? { _tag: 'None' } : { _tag: 'Some', _0: i };
},
isWordChar: (s: string) => ({ _tag: /^\w$/.test(s) ? 'True' : 'False' }),
// join: (delim: string) => (xs: string[]) => xs.join(delim),
split: (delim: string) => (xs: string) => xs.split(delim),