From 4a62774a5762515739dbcb65628862daffebd507 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sun, 5 Apr 2026 20:42:52 -0600 Subject: [PATCH] Adding basic search to the text editor --- src/cg/01-stdlib.cg | 9 +++ src/cg/textEditor.cg | 129 ++++++++++++++++++++++++++++++++++++++----- src/runtime-js.ts | 8 +++ 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index 2997bd7..d098cab 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -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 diff --git a/src/cg/textEditor.cg b/src/cg/textEditor.cg index 515bd5a..23651b7 100644 --- a/src/cg/textEditor.cg +++ b/src/cg/textEditor.cg @@ -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 @@ -352,8 +429,13 @@ textEditor = name \ | _ \ { state = state.{ pending = Some "g" }, emit = [] }) | Key { key = "G" } \ handleMotion LastLine state | Key { key = "." } \ (state.lastAction - | None \ { state = state, emit = [] } - | Some action \ applyOperator action.operator action.motion state) + | 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 ] } } diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 11802c3..94bd6c4 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -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),