@@ -25,6 +25,16 @@ TextBox.defineProperty(TextBox, "editable", {default = true, type = "boolean"})
2525TextBox .defineProperty (TextBox , " syntaxPatterns" , {default = {}, type = " table" })
2626--- @property cursorColor number nil Color of the cursor
2727TextBox .defineProperty (TextBox , " cursorColor" , {default = nil , type = " color" })
28+ --- @property autoPairEnabled boolean true Whether automatic bracket/quote pairing is enabled
29+ TextBox .defineProperty (TextBox , " autoPairEnabled" , {default = true , type = " boolean" })
30+ --- @property autoPairCharacters table { ["("]=")", ["["]="]", ["{"]="}", ['"']='"', ['\'']='\'', ['`']='`'} Mapping of opening to closing characters for auto pairing
31+ TextBox .defineProperty (TextBox , " autoPairCharacters" , {default = { [" (" ]= " )" , [" [" ]= " ]" , [" {" ]= " }" , [' "' ]= ' "' , [' \' ' ]= ' \' ' , [' `' ]= ' `' }, type = " table" })
32+ --- @property autoPairSkipClosing boolean true Skip inserting a closing char if the same one is already at cursor
33+ TextBox .defineProperty (TextBox , " autoPairSkipClosing" , {default = true , type = " boolean" })
34+ --- @property autoPairOverType boolean true When pressing a closing char that matches the next char, move over it instead of inserting
35+ TextBox .defineProperty (TextBox , " autoPairOverType" , {default = true , type = " boolean" })
36+ --- @property autoPairNewlineIndent boolean true On Enter between matching braces, create blank line and keep closing aligned
37+ TextBox .defineProperty (TextBox , " autoPairNewlineIndent" , {default = true , type = " boolean" })
2838--- @property autoCompleteEnabled boolean false Whether autocomplete suggestions are enabled
2939TextBox .defineProperty (TextBox , " autoCompleteEnabled" , {default = false , type = " boolean" })
3040--- @property autoCompleteItems table {} List of suggestions used when no provider is supplied
@@ -487,7 +497,12 @@ local function placeAutoCompleteFrame(self, visibleCount, width)
487497
488498 if baseHeight and baseHeight > 0 then
489499 if y + frameHeight - 1 > baseHeight then
500+ -- Place above
490501 y = aboveY
502+ if border > 0 then
503+ -- Shift further up so lower border does not overlap the text line
504+ y = y - border
505+ end
491506 if y < 1 then
492507 y = math.max (1 , baseHeight - frameHeight + 1 )
493508 end
@@ -497,6 +512,9 @@ local function placeAutoCompleteFrame(self, visibleCount, width)
497512 end
498513 else
499514 if y < 1 then y = 1 end
515+ if y == aboveY and border > 0 then
516+ y = math.max (1 , y - border )
517+ end
500518 end
501519
502520 frame :setPosition (x , y )
@@ -767,6 +785,12 @@ local function insertChar(self, char)
767785 self :updateRender ()
768786end
769787
788+ local function insertText (self , text )
789+ for i = 1 , # text do
790+ insertChar (self , text :sub (i ,i ))
791+ end
792+ end
793+
770794local function newLine (self )
771795 local lines = self .get (" lines" )
772796 local cursorX = self .get (" cursorX" )
836860--- @protected
837861function TextBox :char (char )
838862 if not self .get (" editable" ) or not self .get (" focused" ) then return false end
863+ -- Auto-pair logic only triggers for single characters
864+ local autoPair = self .get (" autoPairEnabled" )
865+ if autoPair and # char == 1 then
866+ local map = self .get (" autoPairCharacters" ) or {}
867+ local lines = self .get (" lines" )
868+ local cursorX = self .get (" cursorX" )
869+ local cursorY = self .get (" cursorY" )
870+ local line = lines [cursorY ] or " "
871+ local afterChar = line :sub (cursorX , cursorX )
872+
873+ -- If typed char is an opening pair and we should skip duplicating closing when already there
874+ local closing = map [char ]
875+ if closing then
876+ -- If skip closing and same closing already directly after, just insert opening?
877+ insertChar (self , char )
878+ if self .get (" autoPairSkipClosing" ) then
879+ if afterChar ~= closing then
880+ insertChar (self , closing )
881+ -- Move cursor back inside pair
882+ self .set (" cursorX" , self .get (" cursorX" ) - 1 )
883+ end
884+ else
885+ insertChar (self , closing )
886+ self .set (" cursorX" , self .get (" cursorX" ) - 1 )
887+ end
888+ refreshAutoComplete (self )
889+ return true
890+ end
891+
892+ -- If typed char is a closing we might want to overtype
893+ if self .get (" autoPairOverType" ) then
894+ for open , close in pairs (map ) do
895+ if char == close and afterChar == close then
896+ -- move over instead of inserting
897+ self .set (" cursorX" , cursorX + 1 )
898+ refreshAutoComplete (self )
899+ return true
900+ end
901+ end
902+ end
903+ end
904+
839905 insertChar (self , char )
840906 refreshAutoComplete (self )
841907 return true
@@ -855,6 +921,32 @@ function TextBox:key(key)
855921 local cursorY = self .get (" cursorY" )
856922
857923 if key == keys .enter then
924+ -- Smart newline between matching braces/brackets if enabled
925+ if self .get (" autoPairEnabled" ) and self .get (" autoPairNewlineIndent" ) then
926+ local lines = self .get (" lines" )
927+ local cursorX = self .get (" cursorX" )
928+ local cursorY = self .get (" cursorY" )
929+ local line = lines [cursorY ] or " "
930+ local before = line :sub (1 , cursorX - 1 )
931+ local after = line :sub (cursorX )
932+ local pairMap = self .get (" autoPairCharacters" ) or {}
933+ local inverse = {}
934+ for o ,c in pairs (pairMap ) do inverse [c ]= o end
935+ local prevChar = before :sub (- 1 )
936+ local nextChar = after :sub (1 ,1 )
937+ if prevChar ~= " " and nextChar ~= " " and pairMap [prevChar ] == nextChar then
938+ -- Split line into two with an empty line between, caret positioned on inner line
939+ lines [cursorY ] = before
940+ table.insert (lines , cursorY + 1 , " " )
941+ table.insert (lines , cursorY + 2 , after )
942+ self .set (" cursorY" , cursorY + 1 )
943+ self .set (" cursorX" , 1 )
944+ self :updateViewport ()
945+ self :updateRender ()
946+ refreshAutoComplete (self )
947+ return true
948+ end
949+ end
858950 newLine (self )
859951 elseif key == keys .backspace then
860952 backspace (self )
0 commit comments