Cleaned up and simplified tab-completion logic a little.

Added shift-tab to inverse cycle suggestions.
Starting completion from cursor location now (sh just eats everything after the cursor for now).
This commit is contained in:
Florian Nücke 2014-08-08 18:37:37 +02:00
parent e3a6a3a9b4
commit d7e7a7edf2
2 changed files with 105 additions and 101 deletions

View File

@ -6,6 +6,7 @@ local process = require("process")
local shell = require("shell") local shell = require("shell")
local term = require("term") local term = require("term")
local text = require("text") local text = require("text")
local unicode = require("unicode")
local function expand(value) local function expand(value)
local result = value:gsub("%$(%w+)", os.getenv):gsub("%$%b{}", local result = value:gsub("%$(%w+)", os.getenv):gsub("%$%b{}",
@ -148,74 +149,66 @@ end
local args, options = shell.parse(...) local args, options = shell.parse(...)
local history = {} local history = {}
local lastSearch local function getMatchingPrograms(baseName)
local result = {}
local function drawPrompt() -- TODO only matching files with .lua extension for now, might want to
local foreground = component.gpu.setForeground(0xFF0000) -- extend this to other extensions at some point? env var? file attrs?
term.write(expand(os.getenv("PS1") or "$ ")) if not baseName or #baseName == 0 then
component.gpu.setForeground(foreground) baseName = "^(.*)%.lua$"
end
local function getMatchingPrograms(pattern)
local res = {}
for dir in string.gmatch(os.getenv("PATH"), "[a-zA-Z0-9/.]+") do
for file in fs.list(dir) do
if string.match(file, "^" .. pattern .. "(.+)[.]lua") then
res[#res+1] = file:match("(.+).lua")
end
end
end
return res
end
local function getMatchingFiles(pattern)
local res = {}
local dir = fs.isDirectory(pattern) and pattern or fs.path(pattern) or "/"
local name = (dir == pattern) and "" or fs.name(pattern) or ""
for file in fs.list(dir) do
if string.match(file, "^" .. name) then
res[#res+1] = file
end
end
return res
end
local function hintHandler(line)
local base, space, after = string.match(line, "(.+)(%s)(.+)")
local searchProgram = not base
if not base then
base = ""
after = line or ""
end
if searchProgram then
local matches
if after:find("[/.]") == 1 then
matches = getMatchingFiles(after)
if #matches == 1 then
lastSearch = ""
local ret = base .. (space or "") .. after .. matches[1]:gsub(after:match("[/]*(%w+)$"),"",1)
return ret:gsub("[^/]$","%1 ")
end
else
matches = getMatchingPrograms(after)
if #matches == 1 then
lastSearch = ""
return matches[1] .. " "
end
end
if lastSearch == line then return matches end
else else
local matches = getMatchingFiles(after) baseName = "^(" .. baseName .. ".*)%.lua$"
for k in ipairs(matches)do
local ret = base .. space .. after .. (matches[k]):gsub(after:match("[/]*(%w+)$") or "","",1)
matches[k] = ret:gsub("[^/]$","%1 "):gsub("/$","")
end
return matches
end end
lastSearch = line for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do
for file in fs.list(basePath) do
local match = file:match(baseName)
if match then
table.insert(result, match)
end
end
end
return result
end
local function getMatchingFiles(baseName)
local result, basePath = {}
-- note: we strip the trailing / to make it easier to navigate through
-- directories using tab completion (since entering the / will then serve
-- as the intention to go into the currently hinted one).
if fs.isDirectory(baseName) then
basePath = baseName
baseName = "^(.-)/?$"
else
basePath = fs.path(baseName) or "/"
baseName = "^(" .. fs.name(baseName) .. ".-)/?$"
end
for file in fs.list(basePath) do
local match = file:match(baseName)
if match then
table.insert(result, fs.concat(basePath, match))
end
end
return result
end
local function hintHandler(line, cursor)
local line = unicode.sub(line, 1, cursor - 1)
if not line or #line < 1 then
return nil
end
local result
local prefix, partial = string.match(line, "^(.+%s)(.+)$")
local searchInPath = not prefix and not line:find("/")
if searchInPath then
-- first part and no path, look for programs in the $PATH
result = getMatchingPrograms(line)
else -- just look normal files
result = getMatchingFiles(partial or line)
end
for i = 1, #result do
result[i] = (prefix or "") .. result[i] .. (searchInPath and " " or "")
end
table.sort(result)
return result
end end
if #args == 0 and (io.input() == io.stdin or options.i) and not options.c then if #args == 0 and (io.input() == io.stdin or options.i) and not options.c then
@ -228,7 +221,9 @@ if #args == 0 and (io.input() == io.stdin or options.i) and not options.c then
term.clear() term.clear()
end end
while term.isAvailable() do while term.isAvailable() do
drawPrompt() local foreground = component.gpu.setForeground(0xFF0000)
term.write(expand(os.getenv("PS1") or "$ "))
component.gpu.setForeground(foreground)
local command = term.read(history, nil, hintHandler) local command = term.read(history, nil, hintHandler)
if not command then if not command then
term.write("exit\n") term.write("exit\n")

View File

@ -101,16 +101,22 @@ function term.isAvailable()
return component.isAvailable("gpu") and component.isAvailable("screen") return component.isAvailable("gpu") and component.isAvailable("screen")
end end
function term.read(history, dobreak, hint, prompt) function term.read(history, dobreak, hint)
checkArg(1, history, "table", "nil") checkArg(1, history, "table", "nil")
checkArg(3, hint, "function", "table", "nil")
history = history or {} history = history or {}
table.insert(history, "") table.insert(history, "")
local offset = term.getCursor() - 1 local offset = term.getCursor() - 1
local scrollX, scrollY = 0, #history - 1 local scrollX, scrollY = 0, #history - 1
local cursorX = 1 local cursorX = 1
local hintCache = (type(hint)=="table" and #hint > 1)and hint if type(hint) == "table" then
local selectedHint = 0 local hintTable = hint
hint = function()
return hintTable
end
end
local hintCache, hintIndex
local function getCursor() local function getCursor()
return cursorX, 1 + scrollY return cursorX, 1 + scrollY
@ -121,6 +127,10 @@ function term.read(history, dobreak, hint, prompt)
return history[cby] return history[cby]
end end
local function clearHint()
hintCache = nil
end
local function setCursor(nbx, nby) local function setCursor(nbx, nby)
local w, h = component.gpu.getResolution() local w, h = component.gpu.getResolution()
local cx, cy = term.getCursor() local cx, cy = term.getCursor()
@ -149,6 +159,7 @@ function term.read(history, dobreak, hint, prompt)
cursorX = nbx cursorX = nbx
term.setCursor(nbx - scrollX + offset, cy) term.setCursor(nbx - scrollX + offset, cy)
clearHint()
end end
local function copyIfNecessary() local function copyIfNecessary()
@ -216,6 +227,7 @@ function term.read(history, dobreak, hint, prompt)
local function delete() local function delete()
copyIfNecessary() copyIfNecessary()
clearHint()
local cbx, cby = getCursor() local cbx, cby = getCursor()
if cbx <= unicode.len(line()) then if cbx <= unicode.len(line()) then
local cw = unicode.charWidth(unicode.sub(line(), cbx)) local cw = unicode.charWidth(unicode.sub(line(), cbx))
@ -235,6 +247,7 @@ function term.read(history, dobreak, hint, prompt)
local function insert(value) local function insert(value)
copyIfNecessary() copyIfNecessary()
clearHint()
local cx, cy = term.getCursor() local cx, cy = term.getCursor()
local cbx, cby = getCursor() local cbx, cby = getCursor()
local w, h = component.gpu.getResolution() local w, h = component.gpu.getResolution()
@ -250,61 +263,58 @@ function term.read(history, dobreak, hint, prompt)
right(unicode.len(value)) right(unicode.len(value))
end end
local function tab() local function tab(direction)
if not hintCache then local cbx, cby = getCursor()
if type(hint) == "function" then if not hintCache then -- hint is never nil, see onKeyDown
local h = hint(line()) hintCache = hint(line(), cbx)
if type(h) == "string" then hintIndex = 0
local _, cby = getCursor() if type(hintCache) == "string" then
history[cby] = after hintCache = {hintCache}
elseif type(h) == "table" and #h > 0 then end
hintCache = h if type(hintCache) ~= "table" or #hintCache < 1 then
selectedHint = 1 hintCache = nil -- invalid hint
local _, cby = getCursor()
history[cby] = hintCache[selectedHint] or ""
end
end end
else
selectedHint = (selectedHint+1)<=#hintCache and (selectedHint+1) or 1
local _, cby = getCursor()
history[cby] = hintCache[selectedHint] or ""
end end
redraw() if hintCache then
ende() hintIndex = (hintIndex + direction + #hintCache - 1) % #hintCache + 1
end history[cby] = tostring(hintCache[hintIndex])
-- because all other cases of the cursor being moved will result
local function cleanHint() -- in the hint cache getting invalidated we do that in setCursor,
if type(hint) ~= "table" then -- so we have to back it up here to restore it after moving.
hintCache = nil local savedCache = hintCache
redraw()
ende()
if #savedCache > 1 then -- stop if only one hint exists.
hintCache = savedCache
end
end end
end end
local function onKeyDown(char, code) local function onKeyDown(char, code)
term.setCursorBlink(false) term.setCursorBlink(false)
if code == keyboard.keys.back then if code == keyboard.keys.back then
if left() then delete() end cleanHint() if left() then delete() end
elseif code == keyboard.keys.delete then elseif code == keyboard.keys.delete then
delete()cleanHint() delete()
elseif code == keyboard.keys.left then elseif code == keyboard.keys.left then
left() left()
elseif code == keyboard.keys.right then elseif code == keyboard.keys.right then
right() right()
elseif code == keyboard.keys.home then elseif code == keyboard.keys.home then
home()cleanHint() home()
elseif code == keyboard.keys["end"] then elseif code == keyboard.keys["end"] then
ende()cleanHint() ende()
elseif code == keyboard.keys.up then elseif code == keyboard.keys.up then
up() up()
elseif code == keyboard.keys.down then elseif code == keyboard.keys.down then
down() down()
elseif code == keyboard.keys.tab and hint then elseif code == keyboard.keys.tab and hint then
tab() tab(keyboard.isShiftDown() and -1 or 1)
elseif code == keyboard.keys.enter then elseif code == keyboard.keys.enter then
local cbx, cby = getCursor() local cbx, cby = getCursor()
if cby ~= #history then -- bring entry to front if cby ~= #history then -- bring entry to front
history[#history] = line() history[#history] = line()
table.remove(history, cby) table.remove(history, cby)
cleanHint()
end end
return true, history[#history] .. "\n" return true, history[#history] .. "\n"
elseif keyboard.isControlDown() and code == keyboard.keys.d then elseif keyboard.isControlDown() and code == keyboard.keys.d then
@ -317,7 +327,6 @@ function term.read(history, dobreak, hint, prompt)
return true, nil return true, nil
elseif not keyboard.isControl(char) then elseif not keyboard.isControl(char) then
insert(unicode.char(char)) insert(unicode.char(char))
cleanHint()
end end
term.setCursorBlink(true) term.setCursorBlink(true)
term.setCursorBlink(true) -- force toggle to caret term.setCursorBlink(true) -- force toggle to caret