Merge pull request #9 from Shlomo1412/patch-3
Implement auto-pairing features in TextBox
This commit is contained in:
@@ -25,6 +25,16 @@ TextBox.defineProperty(TextBox, "editable", {default = true, type = "boolean"})
|
|||||||
TextBox.defineProperty(TextBox, "syntaxPatterns", {default = {}, type = "table"})
|
TextBox.defineProperty(TextBox, "syntaxPatterns", {default = {}, type = "table"})
|
||||||
---@property cursorColor number nil Color of the cursor
|
---@property cursorColor number nil Color of the cursor
|
||||||
TextBox.defineProperty(TextBox, "cursorColor", {default = nil, type = "color"})
|
TextBox.defineProperty(TextBox, "cursorColor", {default = nil, type = "color"})
|
||||||
|
---@property autoPairEnabled boolean true Whether automatic bracket/quote pairing is enabled
|
||||||
|
TextBox.defineProperty(TextBox, "autoPairEnabled", {default = true, type = "boolean"})
|
||||||
|
---@property autoPairCharacters table { ["("]=")", ["["]="]", ["{"]="}", ['"']='"', ['\'']='\'', ['`']='`'} Mapping of opening to closing characters for auto pairing
|
||||||
|
TextBox.defineProperty(TextBox, "autoPairCharacters", {default = { ["("]=")", ["["]="]", ["{"]="}", ['"']='"', ['\'']='\'', ['`']='`' }, type = "table"})
|
||||||
|
---@property autoPairSkipClosing boolean true Skip inserting a closing char if the same one is already at cursor
|
||||||
|
TextBox.defineProperty(TextBox, "autoPairSkipClosing", {default = true, type = "boolean"})
|
||||||
|
---@property autoPairOverType boolean true When pressing a closing char that matches the next char, move over it instead of inserting
|
||||||
|
TextBox.defineProperty(TextBox, "autoPairOverType", {default = true, type = "boolean"})
|
||||||
|
---@property autoPairNewlineIndent boolean true On Enter between matching braces, create blank line and keep closing aligned
|
||||||
|
TextBox.defineProperty(TextBox, "autoPairNewlineIndent", {default = true, type = "boolean"})
|
||||||
---@property autoCompleteEnabled boolean false Whether autocomplete suggestions are enabled
|
---@property autoCompleteEnabled boolean false Whether autocomplete suggestions are enabled
|
||||||
TextBox.defineProperty(TextBox, "autoCompleteEnabled", {default = false, type = "boolean"})
|
TextBox.defineProperty(TextBox, "autoCompleteEnabled", {default = false, type = "boolean"})
|
||||||
---@property autoCompleteItems table {} List of suggestions used when no provider is supplied
|
---@property autoCompleteItems table {} List of suggestions used when no provider is supplied
|
||||||
@@ -487,7 +497,12 @@ local function placeAutoCompleteFrame(self, visibleCount, width)
|
|||||||
|
|
||||||
if baseHeight and baseHeight > 0 then
|
if baseHeight and baseHeight > 0 then
|
||||||
if y + frameHeight - 1 > baseHeight then
|
if y + frameHeight - 1 > baseHeight then
|
||||||
|
-- Place above
|
||||||
y = aboveY
|
y = aboveY
|
||||||
|
if border > 0 then
|
||||||
|
-- Shift further up so lower border does not overlap the text line
|
||||||
|
y = y - border
|
||||||
|
end
|
||||||
if y < 1 then
|
if y < 1 then
|
||||||
y = math.max(1, baseHeight - frameHeight + 1)
|
y = math.max(1, baseHeight - frameHeight + 1)
|
||||||
end
|
end
|
||||||
@@ -497,6 +512,9 @@ local function placeAutoCompleteFrame(self, visibleCount, width)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
if y < 1 then y = 1 end
|
if y < 1 then y = 1 end
|
||||||
|
if y == aboveY and border > 0 then
|
||||||
|
y = math.max(1, y - border)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
frame:setPosition(x, y)
|
frame:setPosition(x, y)
|
||||||
@@ -767,6 +785,12 @@ local function insertChar(self, char)
|
|||||||
self:updateRender()
|
self:updateRender()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function insertText(self, text)
|
||||||
|
for i = 1, #text do
|
||||||
|
insertChar(self, text:sub(i,i))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function newLine(self)
|
local function newLine(self)
|
||||||
local lines = self.get("lines")
|
local lines = self.get("lines")
|
||||||
local cursorX = self.get("cursorX")
|
local cursorX = self.get("cursorX")
|
||||||
@@ -836,6 +860,48 @@ end
|
|||||||
--- @protected
|
--- @protected
|
||||||
function TextBox:char(char)
|
function TextBox:char(char)
|
||||||
if not self.get("editable") or not self.get("focused") then return false end
|
if not self.get("editable") or not self.get("focused") then return false end
|
||||||
|
-- Auto-pair logic only triggers for single characters
|
||||||
|
local autoPair = self.get("autoPairEnabled")
|
||||||
|
if autoPair and #char == 1 then
|
||||||
|
local map = self.get("autoPairCharacters") or {}
|
||||||
|
local lines = self.get("lines")
|
||||||
|
local cursorX = self.get("cursorX")
|
||||||
|
local cursorY = self.get("cursorY")
|
||||||
|
local line = lines[cursorY] or ""
|
||||||
|
local afterChar = line:sub(cursorX, cursorX)
|
||||||
|
|
||||||
|
-- If typed char is an opening pair and we should skip duplicating closing when already there
|
||||||
|
local closing = map[char]
|
||||||
|
if closing then
|
||||||
|
-- If skip closing and same closing already directly after, just insert opening?
|
||||||
|
insertChar(self, char)
|
||||||
|
if self.get("autoPairSkipClosing") then
|
||||||
|
if afterChar ~= closing then
|
||||||
|
insertChar(self, closing)
|
||||||
|
-- Move cursor back inside pair
|
||||||
|
self.set("cursorX", self.get("cursorX") - 1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
insertChar(self, closing)
|
||||||
|
self.set("cursorX", self.get("cursorX") - 1)
|
||||||
|
end
|
||||||
|
refreshAutoComplete(self)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If typed char is a closing we might want to overtype
|
||||||
|
if self.get("autoPairOverType") then
|
||||||
|
for open, close in pairs(map) do
|
||||||
|
if char == close and afterChar == close then
|
||||||
|
-- move over instead of inserting
|
||||||
|
self.set("cursorX", cursorX + 1)
|
||||||
|
refreshAutoComplete(self)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
insertChar(self, char)
|
insertChar(self, char)
|
||||||
refreshAutoComplete(self)
|
refreshAutoComplete(self)
|
||||||
return true
|
return true
|
||||||
@@ -855,6 +921,32 @@ function TextBox:key(key)
|
|||||||
local cursorY = self.get("cursorY")
|
local cursorY = self.get("cursorY")
|
||||||
|
|
||||||
if key == keys.enter then
|
if key == keys.enter then
|
||||||
|
-- Smart newline between matching braces/brackets if enabled
|
||||||
|
if self.get("autoPairEnabled") and self.get("autoPairNewlineIndent") then
|
||||||
|
local lines = self.get("lines")
|
||||||
|
local cursorX = self.get("cursorX")
|
||||||
|
local cursorY = self.get("cursorY")
|
||||||
|
local line = lines[cursorY] or ""
|
||||||
|
local before = line:sub(1, cursorX - 1)
|
||||||
|
local after = line:sub(cursorX)
|
||||||
|
local pairMap = self.get("autoPairCharacters") or {}
|
||||||
|
local inverse = {}
|
||||||
|
for o,c in pairs(pairMap) do inverse[c]=o end
|
||||||
|
local prevChar = before:sub(-1)
|
||||||
|
local nextChar = after:sub(1,1)
|
||||||
|
if prevChar ~= "" and nextChar ~= "" and pairMap[prevChar] == nextChar then
|
||||||
|
-- Split line into two with an empty line between, caret positioned on inner line
|
||||||
|
lines[cursorY] = before
|
||||||
|
table.insert(lines, cursorY + 1, "")
|
||||||
|
table.insert(lines, cursorY + 2, after)
|
||||||
|
self.set("cursorY", cursorY + 1)
|
||||||
|
self.set("cursorX", 1)
|
||||||
|
self:updateViewport()
|
||||||
|
self:updateRender()
|
||||||
|
refreshAutoComplete(self)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
newLine(self)
|
newLine(self)
|
||||||
elseif key == keys.backspace then
|
elseif key == keys.backspace then
|
||||||
backspace(self)
|
backspace(self)
|
||||||
|
|||||||
Reference in New Issue
Block a user