Skip to content

Commit 8bf61d6

Browse files
authored
Implement auto-pairing features in TextBox
Added properties for auto-pairing functionality in TextBox.
1 parent 602cf48 commit 8bf61d6

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

src/elements/TextBox.lua

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ TextBox.defineProperty(TextBox, "editable", {default = true, type = "boolean"})
2525
TextBox.defineProperty(TextBox, "syntaxPatterns", {default = {}, type = "table"})
2626
---@property cursorColor number nil Color of the cursor
2727
TextBox.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
2939
TextBox.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()
768786
end
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+
770794
local function newLine(self)
771795
local lines = self.get("lines")
772796
local cursorX = self.get("cursorX")
@@ -836,6 +860,48 @@ end
836860
--- @protected
837861
function 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

Comments
 (0)