add many tools

This commit is contained in:
rabix 2022-02-27 15:17:46 +08:00 committed by you
parent faf4a2cb11
commit d58efd789f
63 changed files with 5639 additions and 9 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.cache

2
bin/7zip.cmd Normal file
View File

@ -0,0 +1,2 @@
@echo off
call %VID_HOME%\tools\7z.bat %*

2
bin/cmake.cmd Normal file
View File

@ -0,0 +1,2 @@
@echo off
call %VID_HOME%\tools\cmakew.bat %*

5
bin/edit.cmd Normal file
View File

@ -0,0 +1,5 @@
@echo off
setlocal
for /D %%A in (. %1) do set open=%%~dpnA
echo %open%
start "" %VID_HOME%\tools\lite\lite.exe %open%

2
bin/g++.cmd Normal file
View File

@ -0,0 +1,2 @@
@echo off
call %VID_HOME%\tools\6.0g-mingw64.bat g++ %*

2
bin/gcc.cmd Normal file
View File

@ -0,0 +1,2 @@
@echo off
call %VID_HOME%\tools\6.0g-mingw64.bat gcc %*

2
bin/make.cmd Normal file
View File

@ -0,0 +1,2 @@
@echo off
call %VID_HOME%\tools\6.0g-mingw64.bat mingw32-make %*

7
bin/vid-init.cmd Normal file
View File

@ -0,0 +1,7 @@
@echo off
set fn=%VID_HOME%\templates\%1.7z
if exist %fn% (
call %VID_HOME%\tools\7z.bat x -o. -y %fn%
) else (
echo Template %1 not found
)

4
start.cmd Normal file
View File

@ -0,0 +1,4 @@
@echo off
set VID_HOME=%~dp0
set PATH=%~dp0\bin;%PATH%
cmd /k

BIN
templates/C-cmake.7z Normal file

Binary file not shown.

View File

@ -6,7 +6,7 @@ setlocal enableDelayedExpansion
if "%OS%"=="Windows_NT" setlocal
@rem get script directory name
set DIRNAME=%~dp0
set DIRNAME=%~dp0\..\.cache
if "%DIRNAME%" == "" set DIRNAME=.
@rem strip trailing backslash from DIRNAME path to make it easier to work with

66
tools/7z.bat Normal file
View File

@ -0,0 +1,66 @@
@echo off
setlocal ENABLEEXTENSIONS
setlocal enableDelayedExpansion
@rem set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem get script directory name
set DIRNAME=%~dp0\..\.cache
if "%DIRNAME%" == "" set DIRNAME=.
@rem strip trailing backslash from DIRNAME path to make it easier to work with
IF %DIRNAME:~-1%==\ SET DIRNAME=%DIRNAME:~0,-1%
@rem get OS bitness
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OS_BITNESS=32BIT || set OS_BITNESS=64BIT
set UNZIP_EXE=%DIRNAME%\7zip\7z.exe
@rem require 7zip to unzip the cmake dist because windows sucks
if not exist %UNZIP_EXE% (
echo Downloading 7zip...
if %OS_BITNESS%==64BIT (
call :downloadFile https://www.7-zip.org/a/7z1801-x64.exe %TEMP%\7zInstall.exe
) else (
call :downloadFile https://www.7-zip.org/a/7z1801.exe %TEMP%\7zInstall.exe
)
echo Installing 7zip
@rem don't forget the trailing backslash here because 7zip is too stupid to deal with paths and just removes the last char
%TEMP%\7zInstall.exe /S /D=%DIRNAME%\7zip\
del %TEMP%\7zInstall.exe
)
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto runCmake
set CMD_LINE_ARGS=%*
goto runCmake
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:runCmake
%UNZIP_EXE% %CMD_LINE_ARGS%
goto end
@rem args: fileUrl, filePath
:downloadFile
powershell -Command "(New-Object Net.WebClient).DownloadFile('%~1', '%~2')"
EXIT /B 0
:end
if "%OS%"=="Windows_NT" endlocal

View File

@ -11,17 +11,16 @@ set "CMAKE_VERSION=3.13.0"
for /f "tokens=1,2,3 delims=." %%a in ("%CMAKE_VERSION%") do set CMAKE_VERSION_MAJOR=%%a&set CMAKE_VERSION_MINOR=%%b&set CMAKE_VERSION_PATCH=%%c
@rem get script directory name
set DIRNAME=%~dp0
set DIRNAME=%~dp0\..\.cache
if "%DIRNAME%" == "" set DIRNAME=.
@rem strip trailing backslash from DIRNAME path to make it easier to work with
IF %DIRNAME:~-1%==\ SET DIRNAME=%DIRNAME:~0,-1%
@rem get OS bitness
echo (ignore reg error, I don't know where it comes from)
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OS_BITNESS=32BIT || set OS_BITNESS=64BIT
set UNZIP_EXE=%DIRNAME%\.cmakew\7zip\7z.exe
set UNZIP_EXE=%DIRNAME%\7zip\7z.exe
@rem require 7zip to unzip the cmake dist because windows sucks
if not exist %UNZIP_EXE% (
@ -36,11 +35,11 @@ if not exist %UNZIP_EXE% (
echo Installing 7zip
@rem don't forget the trailing backslash here because 7zip is too stupid to deal with paths and just removes the last char
%TEMP%\7zInstall.exe /S /D=%DIRNAME%\.cmakew\7zip\
%TEMP%\7zInstall.exe /S /D=%DIRNAME%\7zip\
del %TEMP%\7zInstall.exe
)
set CMAKE_DIR=%DIRNAME%\.cmakew\cmake-%CMAKE_VERSION%
set CMAKE_DIR=%DIRNAME%\cmake-%CMAKE_VERSION%
set CMAKE_ZIP=%TEMP%\cmake-%CMAKE_VERSION%.zip
set CMAKE_EXE=%CMAKE_DIR%\bin\cmake.exe
@ -56,13 +55,13 @@ if not exist %CMAKE_EXE% (
echo Installing cmake
%UNZIP_EXE% x %CMAKE_ZIP% -o%DIRNAME%\.cmakew -y
%UNZIP_EXE% x %CMAKE_ZIP% -o%DIRNAME% -y
del %CMAKE_ZIP%
if %OS_BITNESS%==64BIT (
ren %DIRNAME%\.cmakew\cmake-%CMAKE_VERSION%-win64-x64 cmake-%CMAKE_VERSION%
ren %DIRNAME%\cmake-%CMAKE_VERSION%-win64-x64 cmake-%CMAKE_VERSION%
) else (
ren %DIRNAME%\.cmakew\cmake-%CMAKE_VERSION%-win32-x86 cmake-%CMAKE_VERSION%
ren %DIRNAME%\cmake-%CMAKE_VERSION%-win32-x86 cmake-%CMAKE_VERSION%
)
)

BIN
tools/lite/SDL2.dll Normal file

Binary file not shown.

View File

@ -0,0 +1,69 @@
local core = require "core"
local command = {}
command.map = {}
local always_true = function() return true end
function command.add(predicate, map)
predicate = predicate or always_true
if type(predicate) == "string" then
predicate = require(predicate)
end
if type(predicate) == "table" then
local class = predicate
predicate = function() return core.active_view:is(class) end
end
for name, fn in pairs(map) do
assert(not command.map[name], "command already exists: " .. name)
command.map[name] = { predicate = predicate, perform = fn }
end
end
local function capitalize_first(str)
return str:sub(1, 1):upper() .. str:sub(2)
end
function command.prettify_name(name)
return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first)
end
function command.get_all_valid()
local res = {}
for name, cmd in pairs(command.map) do
if cmd.predicate() then
table.insert(res, name)
end
end
return res
end
local function perform(name)
local cmd = command.map[name]
if cmd and cmd.predicate() then
cmd.perform()
return true
end
return false
end
function command.perform(...)
local ok, res = core.try(perform, ...)
return not ok or res
end
function command.add_defaults()
local reg = { "core", "root", "command", "doc", "findreplace" }
for _, name in ipairs(reg) do
require("core.commands." .. name)
end
end
return command

View File

@ -0,0 +1,30 @@
local core = require "core"
local command = require "core.command"
local CommandView = require "core.commandview"
local function has_commandview()
return core.active_view:is(CommandView)
end
command.add(has_commandview, {
["command:submit"] = function()
core.active_view:submit()
end,
["command:complete"] = function()
core.active_view:complete()
end,
["command:escape"] = function()
core.active_view:exit()
end,
["command:select-previous"] = function()
core.active_view:move_suggestion_idx(1)
end,
["command:select-next"] = function()
core.active_view:move_suggestion_idx(-1)
end,
})

View File

@ -0,0 +1,101 @@
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local keymap = require "core.keymap"
local LogView = require "core.logview"
local fullscreen = false
command.add(nil, {
["core:quit"] = function()
core.quit()
end,
["core:force-quit"] = function()
core.quit(true)
end,
["core:toggle-fullscreen"] = function()
fullscreen = not fullscreen
system.set_window_mode(fullscreen and "fullscreen" or "normal")
end,
["core:reload-module"] = function()
core.command_view:enter("Reload Module", function(text, item)
local text = item and item.text or text
core.reload_module(text)
core.log("Reloaded module %q", text)
end, function(text)
local items = {}
for name in pairs(package.loaded) do
table.insert(items, name)
end
return common.fuzzy_match(items, text)
end)
end,
["core:find-command"] = function()
local commands = command.get_all_valid()
core.command_view:enter("Do Command", function(text, item)
if item then
command.perform(item.command)
end
end, function(text)
local res = common.fuzzy_match(commands, text)
for i, name in ipairs(res) do
res[i] = {
text = command.prettify_name(name),
info = keymap.get_binding(name),
command = name,
}
end
return res
end)
end,
["core:find-file"] = function()
core.command_view:enter("Open File From Project", function(text, item)
text = item and item.text or text
core.root_view:open_doc(core.open_doc(text))
end, function(text)
local files = {}
for _, item in pairs(core.project_files) do
if item.type == "file" then
table.insert(files, item.filename)
end
end
return common.fuzzy_match(files, text)
end)
end,
["core:new-doc"] = function()
core.root_view:open_doc(core.open_doc())
end,
["core:open-file"] = function()
core.command_view:enter("Open File", function(text)
core.root_view:open_doc(core.open_doc(text))
end, common.path_suggest)
end,
["core:open-log"] = function()
local node = core.root_view:get_active_node()
node:add_view(LogView())
end,
["core:open-user-module"] = function()
core.root_view:open_doc(core.open_doc(EXEDIR .. "/data/user/init.lua"))
end,
["core:open-project-module"] = function()
local filename = ".lite_project.lua"
if system.get_file_info(filename) then
core.root_view:open_doc(core.open_doc(filename))
else
local doc = core.open_doc()
core.root_view:open_doc(doc)
doc:save(filename)
end
end,
})

View File

@ -0,0 +1,363 @@
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local translate = require "core.doc.translate"
local DocView = require "core.docview"
local function dv()
return core.active_view
end
local function doc()
return core.active_view.doc
end
local function get_indent_string()
if config.tab_type == "hard" then
return "\t"
end
return string.rep(" ", config.indent_size)
end
local function insert_at_start_of_selected_lines(text, skip_empty)
local line1, col1, line2, col2, swap = doc():get_selection(true)
for line = line1, line2 do
local line_text = doc().lines[line]
if (not skip_empty or line_text:find("%S")) then
doc():insert(line, 1, text)
end
end
doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap)
end
local function remove_from_start_of_selected_lines(text, skip_empty)
local line1, col1, line2, col2, swap = doc():get_selection(true)
for line = line1, line2 do
local line_text = doc().lines[line]
if line_text:sub(1, #text) == text
and (not skip_empty or line_text:find("%S"))
then
doc():remove(line, 1, line, #text + 1)
end
end
doc():set_selection(line1, col1 - #text, line2, col2 - #text, swap)
end
local function append_line_if_last_line(line)
if line >= #doc().lines then
doc():insert(line, math.huge, "\n")
end
end
local function save(filename)
doc():save(filename)
core.log("Saved \"%s\"", doc().filename)
end
local commands = {
["doc:undo"] = function()
doc():undo()
end,
["doc:redo"] = function()
doc():redo()
end,
["doc:cut"] = function()
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
system.set_clipboard(text)
doc():delete_to(0)
end
end,
["doc:copy"] = function()
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
system.set_clipboard(text)
end
end,
["doc:paste"] = function()
doc():text_input(system.get_clipboard():gsub("\r", ""))
end,
["doc:newline"] = function()
local line, col = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
if col <= #indent then
indent = indent:sub(#indent + 2 - col)
end
doc():text_input("\n" .. indent)
end,
["doc:newline-below"] = function()
local line = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, math.huge, "\n" .. indent)
doc():set_selection(line + 1, math.huge)
end,
["doc:newline-above"] = function()
local line = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, 1, indent .. "\n")
doc():set_selection(line, math.huge)
end,
["doc:delete"] = function()
local line, col = doc():get_selection()
if not doc():has_selection() and doc().lines[line]:find("^%s*$", col) then
doc():remove(line, col, line, math.huge)
end
doc():delete_to(translate.next_char)
end,
["doc:backspace"] = function()
local line, col = doc():get_selection()
if not doc():has_selection() then
local text = doc():get_text(line, 1, line, col)
if #text >= config.indent_size and text:find("^ *$") then
doc():delete_to(0, -config.indent_size)
return
end
end
doc():delete_to(translate.previous_char)
end,
["doc:select-all"] = function()
doc():set_selection(1, 1, math.huge, math.huge)
end,
["doc:select-none"] = function()
local line, col = doc():get_selection()
doc():set_selection(line, col)
end,
["doc:select-lines"] = function()
local line1, _, line2, _, swap = doc():get_selection(true)
append_line_if_last_line(line2)
doc():set_selection(line1, 1, line2 + 1, 1, swap)
end,
["doc:select-word"] = function()
local line1, col1 = doc():get_selection(true)
local line1, col1 = translate.start_of_word(doc(), line1, col1)
local line2, col2 = translate.end_of_word(doc(), line1, col1)
doc():set_selection(line2, col2, line1, col1)
end,
["doc:join-lines"] = function()
local line1, _, line2 = doc():get_selection(true)
if line1 == line2 then line2 = line2 + 1 end
local text = doc():get_text(line1, 1, line2, math.huge)
text = text:gsub("\n[\t ]*", " ")
doc():insert(line1, 1, text)
doc():remove(line1, #text + 1, line2, math.huge)
if doc():has_selection() then
doc():set_selection(line1, math.huge)
end
end,
["doc:indent"] = function()
local text = get_indent_string()
if doc():has_selection() then
insert_at_start_of_selected_lines(text)
else
doc():text_input(text)
end
end,
["doc:unindent"] = function()
local text = get_indent_string()
remove_from_start_of_selected_lines(text)
end,
["doc:duplicate-lines"] = function()
local line1, col1, line2, col2, swap = doc():get_selection(true)
append_line_if_last_line(line2)
local text = doc():get_text(line1, 1, line2 + 1, 1)
doc():insert(line2 + 1, 1, text)
local n = line2 - line1 + 1
doc():set_selection(line1 + n, col1, line2 + n, col2, swap)
end,
["doc:delete-lines"] = function()
local line1, col1, line2 = doc():get_selection(true)
append_line_if_last_line(line2)
doc():remove(line1, 1, line2 + 1, 1)
doc():set_selection(line1, col1)
end,
["doc:move-lines-up"] = function()
local line1, col1, line2, col2, swap = doc():get_selection(true)
append_line_if_last_line(line2)
if line1 > 1 then
local text = doc().lines[line1 - 1]
doc():insert(line2 + 1, 1, text)
doc():remove(line1 - 1, 1, line1, 1)
doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap)
end
end,
["doc:move-lines-down"] = function()
local line1, col1, line2, col2, swap = doc():get_selection(true)
append_line_if_last_line(line2 + 1)
if line2 < #doc().lines then
local text = doc().lines[line2 + 1]
doc():remove(line2 + 1, 1, line2 + 2, 1)
doc():insert(line1, 1, text)
doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap)
end
end,
["doc:toggle-line-comments"] = function()
local comment = doc().syntax.comment
if not comment then return end
local comment_text = comment .. " "
local line1, _, line2 = doc():get_selection(true)
local uncomment = true
for line = line1, line2 do
local text = doc().lines[line]
if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then
uncomment = false
end
end
if uncomment then
remove_from_start_of_selected_lines(comment_text, true)
else
insert_at_start_of_selected_lines(comment_text, true)
end
end,
["doc:upper-case"] = function()
doc():replace(string.upper)
end,
["doc:lower-case"] = function()
doc():replace(string.lower)
end,
["doc:go-to-line"] = function()
local dv = dv()
local items
local function init_items()
if items then return end
items = {}
local mt = { __tostring = function(x) return x.text end }
for i, line in ipairs(dv.doc.lines) do
local item = { text = line:sub(1, -2), line = i, info = "line: " .. i }
table.insert(items, setmetatable(item, mt))
end
end
core.command_view:enter("Go To Line", function(text, item)
local line = item and item.line or tonumber(text)
if not line then
core.error("Invalid line number or unmatched string")
return
end
dv.doc:set_selection(line, 1 )
dv:scroll_to_line(line, true)
end, function(text)
if not text:find("^%d*$") then
init_items()
return common.fuzzy_match(items, text)
end
end)
end,
["doc:toggle-line-ending"] = function()
doc().crlf = not doc().crlf
end,
["doc:save-as"] = function()
if doc().filename then
core.command_view:set_text(doc().filename)
end
core.command_view:enter("Save As", function(filename)
save(filename)
end, common.path_suggest)
end,
["doc:save"] = function()
if doc().filename then
save()
else
command.perform("doc:save-as")
end
end,
["doc:rename"] = function()
local old_filename = doc().filename
if not old_filename then
core.error("Cannot rename unsaved doc")
return
end
core.command_view:set_text(old_filename)
core.command_view:enter("Rename", function(filename)
doc():save(filename)
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
if filename ~= old_filename then
os.remove(old_filename)
end
end, common.path_suggest)
end,
}
local translations = {
["previous-char"] = translate.previous_char,
["next-char"] = translate.next_char,
["previous-word-start"] = translate.previous_word_start,
["next-word-end"] = translate.next_word_end,
["previous-block-start"] = translate.previous_block_start,
["next-block-end"] = translate.next_block_end,
["start-of-doc"] = translate.start_of_doc,
["end-of-doc"] = translate.end_of_doc,
["start-of-line"] = translate.start_of_line,
["end-of-line"] = translate.end_of_line,
["start-of-word"] = translate.start_of_word,
["end-of-word"] = translate.end_of_word,
["previous-line"] = DocView.translate.previous_line,
["next-line"] = DocView.translate.next_line,
["previous-page"] = DocView.translate.previous_page,
["next-page"] = DocView.translate.next_page,
}
for name, fn in pairs(translations) do
commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end
commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end
commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end
end
commands["doc:move-to-previous-char"] = function()
if doc():has_selection() then
local line, col = doc():get_selection(true)
doc():set_selection(line, col)
else
doc():move_to(translate.previous_char)
end
end
commands["doc:move-to-next-char"] = function()
if doc():has_selection() then
local _, _, line, col = doc():get_selection(true)
doc():set_selection(line, col)
else
doc():move_to(translate.next_char)
end
end
command.add("core.docview", commands)

View File

@ -0,0 +1,170 @@
local core = require "core"
local command = require "core.command"
local config = require "core.config"
local search = require "core.doc.search"
local DocView = require "core.docview"
local max_previous_finds = 50
local function doc()
return core.active_view.doc
end
local previous_finds
local last_doc
local last_fn, last_text
local function push_previous_find(doc, sel)
if last_doc ~= doc then
last_doc = doc
previous_finds = {}
end
if #previous_finds >= max_previous_finds then
table.remove(previous_finds, 1)
end
table.insert(previous_finds, sel or { doc:get_selection() })
end
local function find(label, search_fn)
local dv = core.active_view
local sel = { dv.doc:get_selection() }
local text = dv.doc:get_text(table.unpack(sel))
local found = false
core.command_view:set_text(text, true)
core.command_view:enter(label, function(text)
if found then
last_fn, last_text = search_fn, text
previous_finds = {}
push_previous_find(dv.doc, sel)
else
core.error("Couldn't find %q", text)
dv.doc:set_selection(table.unpack(sel))
dv:scroll_to_make_visible(sel[1], sel[2])
end
end, function(text)
local ok, line1, col1, line2, col2 = pcall(search_fn, dv.doc, sel[1], sel[2], text)
if ok and line1 and text ~= "" then
dv.doc:set_selection(line2, col2, line1, col1)
dv:scroll_to_line(line2, true)
found = true
else
dv.doc:set_selection(table.unpack(sel))
found = false
end
end, function(explicit)
if explicit then
dv.doc:set_selection(table.unpack(sel))
dv:scroll_to_make_visible(sel[1], sel[2])
end
end)
end
local function replace(kind, default, fn)
core.command_view:set_text(default, true)
core.command_view:enter("Find To Replace " .. kind, function(old)
core.command_view:set_text(old, true)
local s = string.format("Replace %s %q With", kind, old)
core.command_view:enter(s, function(new)
local n = doc():replace(function(text)
return fn(text, old, new)
end)
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end)
end)
end
local function has_selection()
return core.active_view:is(DocView)
and core.active_view.doc:has_selection()
end
command.add(has_selection, {
["find-replace:select-next"] = function()
local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2)
local l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l2 then doc():set_selection(l2, c2, l1, c1) end
end
})
command.add("core.docview", {
["find-replace:find"] = function()
find("Find Text", function(doc, line, col, text)
local opt = { wrap = true, no_case = true }
return search.find(doc, line, col, text, opt)
end)
end,
["find-replace:find-pattern"] = function()
find("Find Text Pattern", function(doc, line, col, text)
local opt = { wrap = true, no_case = true, pattern = true }
return search.find(doc, line, col, text, opt)
end)
end,
["find-replace:repeat-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local line, col = doc():get_selection()
local line1, col1, line2, col2 = last_fn(doc(), line, col, last_text)
if line1 then
push_previous_find(doc())
doc():set_selection(line2, col2, line1, col1)
core.active_view:scroll_to_line(line2, true)
end
end
end,
["find-replace:previous-find"] = function()
local sel = table.remove(previous_finds)
if not sel or doc() ~= last_doc then
core.error("No previous finds")
return
end
doc():set_selection(table.unpack(sel))
core.active_view:scroll_to_line(sel[3], true)
end,
["find-replace:replace"] = function()
replace("Text", "", function(text, old, new)
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
end)
end,
["find-replace:replace-pattern"] = function()
replace("Pattern", "", function(text, old, new)
return text:gsub(old, new)
end)
end,
["find-replace:replace-symbol"] = function()
local first = ""
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
first = text:match(config.symbol_pattern) or ""
end
replace("Symbol", first, function(text, old, new)
local n = 0
local res = text:gsub(config.symbol_pattern, function(sym)
if old == sym then
n = n + 1
return new
end
end)
return res, n
end)
end,
})

View File

@ -0,0 +1,105 @@
local core = require "core"
local style = require "core.style"
local DocView = require "core.docview"
local command = require "core.command"
local common = require "core.common"
local t = {
["root:close"] = function()
local node = core.root_view:get_active_node()
node:close_active_view(core.root_view.root_node)
end,
["root:switch-to-previous-tab"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
idx = idx - 1
if idx < 1 then idx = #node.views end
node:set_active_view(node.views[idx])
end,
["root:switch-to-next-tab"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
idx = idx + 1
if idx > #node.views then idx = 1 end
node:set_active_view(node.views[idx])
end,
["root:move-tab-left"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
if idx > 1 then
table.remove(node.views, idx)
table.insert(node.views, idx - 1, core.active_view)
end
end,
["root:move-tab-right"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
if idx < #node.views then
table.remove(node.views, idx)
table.insert(node.views, idx + 1, core.active_view)
end
end,
["root:shrink"] = function()
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and -0.1 or 0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end,
["root:grow"] = function()
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and 0.1 or -0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end,
}
for i = 1, 9 do
t["root:switch-to-tab-" .. i] = function()
local node = core.root_view:get_active_node()
local view = node.views[i]
if view then
node:set_active_view(view)
end
end
end
for _, dir in ipairs { "left", "right", "up", "down" } do
t["root:split-" .. dir] = function()
local node = core.root_view:get_active_node()
local av = node.active_view
node:split(dir)
if av:is(DocView) then
core.root_view:open_doc(av.doc)
end
end
t["root:switch-to-" .. dir] = function()
local node = core.root_view:get_active_node()
local x, y
if dir == "left" or dir == "right" then
y = node.position.y + node.size.y / 2
x = node.position.x + (dir == "left" and -1 or node.size.x + style.divider_size)
else
x = node.position.x + node.size.x / 2
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end
local node = core.root_view.root_node:get_child_overlapping_point(x, y)
if not node:get_locked_size() then
core.set_active_view(node.active_view)
end
end
end
command.add(function()
local node = core.root_view:get_active_node()
return not node:get_locked_size()
end, t)

View File

@ -0,0 +1,256 @@
local core = require "core"
local common = require "core.common"
local style = require "core.style"
local Doc = require "core.doc"
local DocView = require "core.docview"
local View = require "core.view"
local SingleLineDoc = Doc:extend()
function SingleLineDoc:insert(line, col, text)
SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
end
local CommandView = DocView:extend()
local max_suggestions = 10
local noop = function() end
local default_state = {
submit = noop,
suggest = noop,
cancel = noop,
}
function CommandView:new()
CommandView.super.new(self, SingleLineDoc())
self.suggestion_idx = 1
self.suggestions = {}
self.suggestions_height = 0
self.last_change_id = 0
self.gutter_width = 0
self.gutter_text_brightness = 0
self.selection_offset = 0
self.state = default_state
self.font = "font"
self.size.y = 0
self.label = ""
end
function CommandView:get_name()
return View.get_name(self)
end
function CommandView:get_line_screen_position()
local x = CommandView.super.get_line_screen_position(self, 1)
local _, y = self:get_content_offset()
local lh = self:get_line_height()
return x, y + (self.size.y - lh) / 2
end
function CommandView:get_scrollable_size()
return 0
end
function CommandView:scroll_to_make_visible()
-- no-op function to disable this functionality
end
function CommandView:get_text()
return self.doc:get_text(1, 1, 1, math.huge)
end
function CommandView:set_text(text, select)
self.doc:remove(1, 1, math.huge, math.huge)
self.doc:text_input(text)
if select then
self.doc:set_selection(math.huge, math.huge, 1, 1)
end
end
function CommandView:move_suggestion_idx(dir)
local n = self.suggestion_idx + dir
self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
self:complete()
self.last_change_id = self.doc:get_change_id()
end
function CommandView:complete()
if #self.suggestions > 0 then
self:set_text(self.suggestions[self.suggestion_idx].text)
end
end
function CommandView:submit()
local suggestion = self.suggestions[self.suggestion_idx]
local text = self:get_text()
local submit = self.state.submit
self:exit(true)
submit(text, suggestion)
end
function CommandView:enter(text, submit, suggest, cancel)
if self.state ~= default_state then
return
end
self.state = {
submit = submit or noop,
suggest = suggest or noop,
cancel = cancel or noop,
}
core.set_active_view(self)
self:update_suggestions()
self.gutter_text_brightness = 100
self.label = text .. ": "
end
function CommandView:exit(submitted, inexplicit)
if core.active_view == self then
core.set_active_view(core.last_active_view)
end
local cancel = self.state.cancel
self.state = default_state
self.doc:reset()
self.suggestions = {}
if not submitted then cancel(not inexplicit) end
end
function CommandView:get_gutter_width()
return self.gutter_width
end
function CommandView:get_suggestion_line_height()
return self:get_font():get_height() + style.padding.y
end
function CommandView:update_suggestions()
local t = self.state.suggest(self:get_text()) or {}
local res = {}
for i, item in ipairs(t) do
if i == max_suggestions then
break
end
if type(item) == "string" then
item = { text = item }
end
res[i] = item
end
self.suggestions = res
self.suggestion_idx = 1
end
function CommandView:update()
CommandView.super.update(self)
if core.active_view ~= self and self.state ~= default_state then
self:exit(false, true)
end
-- update suggestions if text has changed
if self.last_change_id ~= self.doc:get_change_id() then
self:update_suggestions()
self.last_change_id = self.doc:get_change_id()
end
-- update gutter text color brightness
self:move_towards("gutter_text_brightness", 0, 0.1)
-- update gutter width
local dest = self:get_font():get_width(self.label) + style.padding.x
if self.size.y <= 0 then
self.gutter_width = dest
else
self:move_towards("gutter_width", dest)
end
-- update suggestions box height
local lh = self:get_suggestion_line_height()
local dest = #self.suggestions * lh
self:move_towards("suggestions_height", dest)
-- update suggestion cursor offset
local dest = self.suggestion_idx * self:get_suggestion_line_height()
self:move_towards("selection_offset", dest)
-- update size based on whether this is the active_view
local dest = 0
if self == core.active_view then
dest = style.font:get_height() + style.padding.y * 2
end
self:move_towards(self.size, "y", dest)
end
function CommandView:draw_line_highlight()
-- no-op function to disable this functionality
end
function CommandView:draw_line_gutter(idx, x, y)
local yoffset = self:get_line_text_y_offset()
local pos = self.position
local color = common.lerp(style.text, style.accent, self.gutter_text_brightness / 100)
core.push_clip_rect(pos.x, pos.y, self:get_gutter_width(), self.size.y)
x = x + style.padding.x
renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color)
core.pop_clip_rect()
end
local function draw_suggestions_box(self)
local lh = self:get_suggestion_line_height()
local dh = style.divider_size
local x, _ = self:get_line_screen_position()
local h = math.ceil(self.suggestions_height)
local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h
-- draw suggestions background
if #self.suggestions > 0 then
renderer.draw_rect(rx, ry, rw, rh, style.background3)
renderer.draw_rect(rx, ry - dh, rw, dh, style.divider)
local y = self.position.y - self.selection_offset - dh
renderer.draw_rect(rx, y, rw, lh, style.line_highlight)
end
-- draw suggestion text
core.push_clip_rect(rx, ry, rw, rh)
for i, item in ipairs(self.suggestions) do
local color = (i == self.suggestion_idx) and style.accent or style.text
local y = self.position.y - i * lh - dh
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh)
if item.info then
local w = self.size.x - x - style.padding.x
common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh)
end
end
core.pop_clip_rect()
end
function CommandView:draw()
CommandView.super.draw(self)
core.root_view:defer_draw(draw_suggestions_box, self)
end
return CommandView

View File

@ -0,0 +1,140 @@
local common = {}
function common.is_utf8_cont(char)
local byte = char:byte()
return byte >= 0x80 and byte < 0xc0
end
function common.utf8_chars(text)
return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*")
end
function common.clamp(n, lo, hi)
return math.max(math.min(n, hi), lo)
end
function common.round(n)
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
end
function common.lerp(a, b, t)
if type(a) ~= "table" then
return a + (b - a) * t
end
local res = {}
for k, v in pairs(b) do
res[k] = common.lerp(a[k], v, t)
end
return res
end
function common.color(str)
local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)")
if r then
r = tonumber(r, 16)
g = tonumber(g, 16)
b = tonumber(b, 16)
a = 1
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
local f = str:gmatch("[%d.]+")
r = (f() or 0)
g = (f() or 0)
b = (f() or 0)
a = f() or 1
else
error(string.format("bad color string '%s'", str))
end
return r, g, b, a * 0xff
end
local function compare_score(a, b)
return a.score > b.score
end
local function fuzzy_match_items(items, needle)
local res = {}
for _, item in ipairs(items) do
local score = system.fuzzy_match(tostring(item), needle)
if score then
table.insert(res, { text = item, score = score })
end
end
table.sort(res, compare_score)
for i, item in ipairs(res) do
res[i] = item.text
end
return res
end
function common.fuzzy_match(haystack, needle)
if type(haystack) == "table" then
return fuzzy_match_items(haystack, needle)
end
return system.fuzzy_match(haystack, needle)
end
function common.path_suggest(text)
local path, name = text:match("^(.-)([^/\\]*)$")
local files = system.list_dir(path == "" and "." or path) or {}
local res = {}
for _, file in ipairs(files) do
file = path .. file
local info = system.get_file_info(file)
if info then
if info.type == "dir" then
file = file .. PATHSEP
end
if file:lower():find(text:lower(), nil, true) == 1 then
table.insert(res, file)
end
end
end
return res
end
function common.match_pattern(text, pattern, ...)
if type(pattern) == "string" then
return text:find(pattern, ...)
end
for _, p in ipairs(pattern) do
local s, e = common.match_pattern(text, p, ...)
if s then return s, e end
end
return false
end
function common.draw_text(font, color, text, align, x,y,w,h)
local tw, th = font:get_width(text), font:get_height(text)
if align == "center" then
x = x + (w - tw) / 2
elseif align == "right" then
x = x + (w - tw)
end
y = common.round(y + (h - th) / 2)
return renderer.draw_text(font, text, x, y, color), y + th
end
function common.bench(name, fn, ...)
local start = system.get_time()
local res = fn(...)
local t = system.get_time() - start
local ms = t * 1000
local per = (t / (1 / 60)) * 100
print(string.format("*** %-16s : %8.3fms %6.2f%%", name, ms, per))
return res
end
return common

View File

@ -0,0 +1,20 @@
local config = {}
config.project_scan_rate = 5
config.fps = 60
config.max_log_items = 80
config.message_timeout = 3
config.mouse_wheel_scroll = 50 * SCALE
config.file_size_limit = 10
config.ignore_files = "^%."
config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3
config.max_undos = 10000
config.highlight_current_line = true
config.line_height = 1.2
config.indent_size = 2
config.tab_type = "soft"
config.line_limit = 80
return config

View File

@ -0,0 +1,80 @@
local core = require "core"
local config = require "core.config"
local tokenizer = require "core.tokenizer"
local Object = require "core.object"
local Highlighter = Object:extend()
function Highlighter:new(doc)
self.doc = doc
self:reset()
-- init incremental syntax highlighting
core.add_thread(function()
while true do
if self.first_invalid_line > self.max_wanted_line then
self.max_wanted_line = 0
coroutine.yield(1 / config.fps)
else
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line)
for i = self.first_invalid_line, max do
local state = (i > 1) and self.lines[i - 1].state
local line = self.lines[i]
if not (line and line.init_state == state) then
self.lines[i] = self:tokenize_line(i, state)
end
end
self.first_invalid_line = max + 1
core.redraw = true
coroutine.yield()
end
end
end, self)
end
function Highlighter:reset()
self.lines = {}
self.first_invalid_line = 1
self.max_wanted_line = 0
end
function Highlighter:invalidate(idx)
self.first_invalid_line = math.min(self.first_invalid_line, idx)
self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines)
end
function Highlighter:tokenize_line(idx, state)
local res = {}
res.init_state = state
res.text = self.doc.lines[idx]
res.tokens, res.state = tokenizer.tokenize(self.doc.syntax, res.text, state)
return res
end
function Highlighter:get_line(idx)
local line = self.lines[idx]
if not line or line.text ~= self.doc.lines[idx] then
local prev = self.lines[idx - 1]
line = self:tokenize_line(idx, prev and prev.state)
self.lines[idx] = line
end
self.max_wanted_line = math.max(self.max_wanted_line, idx)
return line
end
function Highlighter:each_token(idx)
return tokenizer.each_token(self:get_line(idx).tokens)
end
return Highlighter

View File

@ -0,0 +1,393 @@
local Object = require "core.object"
local Highlighter = require "core.doc.highlighter"
local syntax = require "core.syntax"
local config = require "core.config"
local common = require "core.common"
local Doc = Object:extend()
local function split_lines(text)
local res = {}
for line in (text .. "\n"):gmatch("(.-)\n") do
table.insert(res, line)
end
return res
end
local function splice(t, at, remove, insert)
insert = insert or {}
local offset = #insert - remove
local old_len = #t
if offset < 0 then
for i = at - offset, old_len - offset do
t[i + offset] = t[i]
end
elseif offset > 0 then
for i = old_len, at, -1 do
t[i + offset] = t[i]
end
end
for i, item in ipairs(insert) do
t[at + i - 1] = item
end
end
function Doc:new(filename)
self:reset()
if filename then
self:load(filename)
end
end
function Doc:reset()
self.lines = { "\n" }
self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } }
self.undo_stack = { idx = 1 }
self.redo_stack = { idx = 1 }
self.clean_change_id = 1
self.highlighter = Highlighter(self)
self:reset_syntax()
end
function Doc:reset_syntax()
local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
local syn = syntax.get(self.filename or "", header)
if self.syntax ~= syn then
self.syntax = syn
self.highlighter:reset()
end
end
function Doc:load(filename)
local fp = assert( io.open(filename, "rb") )
self:reset()
self.filename = filename
self.lines = {}
for line in fp:lines() do
if line:byte(-1) == 13 then
line = line:sub(1, -2)
self.crlf = true
end
table.insert(self.lines, line .. "\n")
end
if #self.lines == 0 then
table.insert(self.lines, "\n")
end
fp:close()
self:reset_syntax()
end
function Doc:save(filename)
filename = filename or assert(self.filename, "no filename set to default to")
local fp = assert( io.open(filename, "wb") )
for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line)
end
fp:close()
self.filename = filename or self.filename
self:reset_syntax()
self:clean()
end
function Doc:get_name()
return self.filename or "unsaved"
end
function Doc:is_dirty()
return self.clean_change_id ~= self:get_change_id()
end
function Doc:clean()
self.clean_change_id = self:get_change_id()
end
function Doc:get_change_id()
return self.undo_stack.idx
end
function Doc:set_selection(line1, col1, line2, col2, swap)
assert(not line2 == not col2, "expected 2 or 4 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
self.selection.a.line, self.selection.a.col = line1, col1
self.selection.b.line, self.selection.b.col = line2, col2
end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2
or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
function Doc:get_selection(sort)
local a, b = self.selection.a, self.selection.b
if sort then
return sort_positions(a.line, a.col, b.line, b.col)
end
return a.line, a.col, b.line, b.col
end
function Doc:has_selection()
local a, b = self.selection.a, self.selection.b
return not (a.line == b.line and a.col == b.col)
end
function Doc:sanitize_selection()
self:set_selection(self:get_selection())
end
function Doc:sanitize_position(line, col)
line = common.clamp(line, 1, #self.lines)
col = common.clamp(col, 1, #self.lines[line])
return line, col
end
local function position_offset_func(self, line, col, fn, ...)
line, col = self:sanitize_position(line, col)
return fn(self, line, col, ...)
end
local function position_offset_byte(self, line, col, offset)
line, col = self:sanitize_position(line, col)
col = col + offset
while line > 1 and col < 1 do
line = line - 1
col = col + #self.lines[line]
end
while line < #self.lines and col > #self.lines[line] do
col = col - #self.lines[line]
line = line + 1
end
return self:sanitize_position(line, col)
end
local function position_offset_linecol(self, line, col, lineoffset, coloffset)
return self:sanitize_position(line + lineoffset, col + coloffset)
end
function Doc:position_offset(line, col, ...)
if type(...) ~= "number" then
return position_offset_func(self, line, col, ...)
elseif select("#", ...) == 1 then
return position_offset_byte(self, line, col, ...)
elseif select("#", ...) == 2 then
return position_offset_linecol(self, line, col, ...)
else
error("bad number of arguments")
end
end
function Doc:get_text(line1, col1, line2, col2)
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2)
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
if line1 == line2 then
return self.lines[line1]:sub(col1, col2 - 1)
end
local lines = { self.lines[line1]:sub(col1) }
for i = line1 + 1, line2 - 1 do
table.insert(lines, self.lines[i])
end
table.insert(lines, self.lines[line2]:sub(1, col2 - 1))
return table.concat(lines)
end
function Doc:get_char(line, col)
line, col = self:sanitize_position(line, col)
return self.lines[line]:sub(col, col)
end
local function push_undo(undo_stack, time, type, ...)
undo_stack[undo_stack.idx] = { type = type, time = time, ... }
undo_stack[undo_stack.idx - config.max_undos] = nil
undo_stack.idx = undo_stack.idx + 1
end
local function pop_undo(self, undo_stack, redo_stack)
-- pop command
local cmd = undo_stack[undo_stack.idx - 1]
if not cmd then return end
undo_stack.idx = undo_stack.idx - 1
-- handle command
if cmd.type == "insert" then
local line, col, text = table.unpack(cmd)
self:raw_insert(line, col, text, redo_stack, cmd.time)
elseif cmd.type == "remove" then
local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then
self.selection.a.line, self.selection.a.col = cmd[1], cmd[2]
self.selection.b.line, self.selection.b.col = cmd[3], cmd[4]
end
-- if next undo command is within the merge timeout then treat as a single
-- command and continue to execute it
local next = undo_stack[undo_stack.idx - 1]
if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then
return pop_undo(self, undo_stack, redo_stack)
end
end
function Doc:raw_insert(line, col, text, undo_stack, time)
-- split text into lines and merge with line at insertion point
local lines = split_lines(text)
local before = self.lines[line]:sub(1, col - 1)
local after = self.lines[line]:sub(col)
for i = 1, #lines - 1 do
lines[i] = lines[i] .. "\n"
end
lines[1] = before .. lines[1]
lines[#lines] = lines[#lines] .. after
-- splice lines into line array
splice(self.lines, line, 1, lines)
-- push undo
local line2, col2 = self:position_offset(line, col, #text)
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line)
self:sanitize_selection()
end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo
local text = self:get_text(line1, col1, line2, col2)
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text
local before = self.lines[line1]:sub(1, col1 - 1)
local after = self.lines[line2]:sub(col2)
-- splice line into line array
splice(self.lines, line1, line2 - line1 + 1, { before .. after })
-- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line1)
self:sanitize_selection()
end
function Doc:insert(line, col, text)
self.redo_stack = { idx = 1 }
line, col = self:sanitize_position(line, col)
self:raw_insert(line, col, text, self.undo_stack, system.get_time())
end
function Doc:remove(line1, col1, line2, col2)
self.redo_stack = { idx = 1 }
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2)
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time())
end
function Doc:undo()
pop_undo(self, self.undo_stack, self.redo_stack)
end
function Doc:redo()
pop_undo(self, self.redo_stack, self.undo_stack)
end
function Doc:text_input(text)
if self:has_selection() then
self:delete_to()
end
local line, col = self:get_selection()
self:insert(line, col, text)
self:move_to(#text)
end
function Doc:replace(fn)
local line1, col1, line2, col2, swap
local had_selection = self:has_selection()
if had_selection then
line1, col1, line2, col2, swap = self:get_selection(true)
else
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
end
local old_text = self:get_text(line1, col1, line2, col2)
local new_text, n = fn(old_text)
if old_text ~= new_text then
self:insert(line2, col2, new_text)
self:remove(line1, col1, line2, col2)
if had_selection then
line2, col2 = self:position_offset(line1, col1, #new_text)
self:set_selection(line1, col1, line2, col2, swap)
end
end
return n
end
function Doc:delete_to(...)
local line, col = self:get_selection(true)
if self:has_selection() then
self:remove(self:get_selection())
else
local line2, col2 = self:position_offset(line, col, ...)
self:remove(line, col, line2, col2)
line, col = sort_positions(line, col, line2, col2)
end
self:set_selection(line, col)
end
function Doc:move_to(...)
local line, col = self:get_selection()
self:set_selection(self:position_offset(line, col, ...))
end
function Doc:select_to(...)
local line, col, line2, col2 = self:get_selection()
line, col = self:position_offset(line, col, ...)
self:set_selection(line, col, line2, col2)
end
return Doc

View File

@ -0,0 +1,52 @@
local search = {}
local default_opt = {}
local function pattern_lower(str)
if str:sub(1, 1) == "%" then
return str
end
return str:lower()
end
local function init_args(doc, line, col, text, opt)
opt = opt or default_opt
line, col = doc:sanitize_position(line, col)
if opt.no_case then
if opt.pattern then
text = text:gsub("%%?.", pattern_lower)
else
text = text:lower()
end
end
return doc, line, col, text, opt
end
function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
for line = line, #doc.lines do
local line_text = doc.lines[line]
if opt.no_case then
line_text = line_text:lower()
end
local s, e = line_text:find(text, col, not opt.pattern)
if s then
return line, s, line, e + 1
end
col = 1
end
if opt.wrap then
opt = { no_case = opt.no_case, pattern = opt.pattern }
return search.find(doc, 1, 1, text, opt)
end
end
return search

View File

@ -0,0 +1,136 @@
local common = require "core.common"
local config = require "core.config"
-- functions for translating a Doc position to another position these functions
-- can be passed to Doc:move_to|select_to|delete_to()
local translate = {}
local function is_non_word(char)
return config.non_word_chars:find(char, nil, true)
end
function translate.previous_char(doc, line, col)
repeat
line, col = doc:position_offset(line, col, -1)
until not common.is_utf8_cont(doc:get_char(line, col))
return line, col
end
function translate.next_char(doc, line, col)
repeat
line, col = doc:position_offset(line, col, 1)
until not common.is_utf8_cont(doc:get_char(line, col))
return line, col
end
function translate.previous_word_start(doc, line, col)
local prev
while line > 1 or col > 1 do
local l, c = doc:position_offset(line, col, -1)
local char = doc:get_char(l, c)
if prev and prev ~= char or not is_non_word(char) then
break
end
prev, line, col = char, l, c
end
return translate.start_of_word(doc, line, col)
end
function translate.next_word_end(doc, line, col)
local prev
local end_line, end_col = translate.end_of_doc(doc, line, col)
while line < end_line or col < end_col do
local char = doc:get_char(line, col)
if prev and prev ~= char or not is_non_word(char) then
break
end
line, col = doc:position_offset(line, col, 1)
prev = char
end
return translate.end_of_word(doc, line, col)
end
function translate.start_of_word(doc, line, col)
while true do
local line2, col2 = doc:position_offset(line, col, -1)
local char = doc:get_char(line2, col2)
if is_non_word(char)
or line == line2 and col == col2 then
break
end
line, col = line2, col2
end
return line, col
end
function translate.end_of_word(doc, line, col)
while true do
local line2, col2 = doc:position_offset(line, col, 1)
local char = doc:get_char(line, col)
if is_non_word(char)
or line == line2 and col == col2 then
break
end
line, col = line2, col2
end
return line, col
end
function translate.previous_block_start(doc, line, col)
while true do
line = line - 1
if line <= 1 then
return 1, 1
end
if doc.lines[line-1]:find("^%s*$")
and not doc.lines[line]:find("^%s*$") then
return line, (doc.lines[line]:find("%S"))
end
end
end
function translate.next_block_end(doc, line, col)
while true do
if line >= #doc.lines then
return #doc.lines, 1
end
if doc.lines[line+1]:find("^%s*$")
and not doc.lines[line]:find("^%s*$") then
return line+1, #doc.lines[line+1]
end
line = line + 1
end
end
function translate.start_of_line(doc, line, col)
return line, 1
end
function translate.end_of_line(doc, line, col)
return line, math.huge
end
function translate.start_of_doc(doc, line, col)
return 1, 1
end
function translate.end_of_doc(doc, line, col)
return #doc.lines, #doc.lines[#doc.lines]
end
return translate

View File

@ -0,0 +1,383 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"
local View = require "core.view"
local DocView = View:extend()
local function move_to_line_offset(dv, line, col, offset)
local xo = dv.last_x_offset
if xo.line ~= line or xo.col ~= col then
xo.offset = dv:get_col_x_offset(line, col)
end
xo.line = line + offset
xo.col = dv:get_x_offset_col(line + offset, xo.offset)
return xo.line, xo.col
end
DocView.translate = {
["previous_page"] = function(doc, line, col, dv)
local min, max = dv:get_visible_line_range()
return line - (max - min), 1
end,
["next_page"] = function(doc, line, col, dv)
local min, max = dv:get_visible_line_range()
return line + (max - min), 1
end,
["previous_line"] = function(doc, line, col, dv)
if line == 1 then
return 1, 1
end
return move_to_line_offset(dv, line, col, -1)
end,
["next_line"] = function(doc, line, col, dv)
if line == #doc.lines then
return #doc.lines, math.huge
end
return move_to_line_offset(dv, line, col, 1)
end,
}
local blink_period = 0.8
function DocView:new(doc)
DocView.super.new(self)
self.cursor = "ibeam"
self.scrollable = true
self.doc = assert(doc)
self.font = "code_font"
self.last_x_offset = {}
self.blink_timer = 0
end
function DocView:try_close(do_close)
if self.doc:is_dirty()
and #core.get_views_referencing_doc(self.doc) == 1 then
core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item)
if item.text:match("^[cC]") then
do_close()
elseif item.text:match("^[sS]") then
self.doc:save()
do_close()
end
end, function(text)
local items = {}
if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end
if not text:find("^[^sS]") then table.insert(items, "Save And Close") end
return items
end)
else
do_close()
end
end
function DocView:get_name()
local post = self.doc:is_dirty() and "*" or ""
local name = self.doc:get_name()
return name:match("[^/%\\]*$") .. post
end
function DocView:get_scrollable_size()
return self:get_line_height() * (#self.doc.lines - 1) + self.size.y
end
function DocView:get_font()
return style[self.font]
end
function DocView:get_line_height()
return math.floor(self:get_font():get_height() * config.line_height)
end
function DocView:get_gutter_width()
return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2
end
function DocView:get_line_screen_position(idx)
local x, y = self:get_content_offset()
local lh = self:get_line_height()
local gw = self:get_gutter_width()
return x + gw, y + (idx-1) * lh + style.padding.y
end
function DocView:get_line_text_y_offset()
local lh = self:get_line_height()
local th = self:get_font():get_height()
return (lh - th) / 2
end
function DocView:get_visible_line_range()
local x, y, x2, y2 = self:get_content_bounds()
local lh = self:get_line_height()
local minline = math.max(1, math.floor(y / lh))
local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1)
return minline, maxline
end
function DocView:get_col_x_offset(line, col)
local text = self.doc.lines[line]
if not text then return 0 end
return self:get_font():get_width(text:sub(1, col - 1))
end
function DocView:get_x_offset_col(line, x)
local text = self.doc.lines[line]
local xoffset, last_i, i = 0, 1, 1
for char in common.utf8_chars(text) do
local w = self:get_font():get_width(char)
if xoffset >= x then
return (xoffset - x > w / 2) and last_i or i
end
xoffset = xoffset + w
last_i = i
i = i + #char
end
return #text
end
function DocView:resolve_screen_position(x, y)
local ox, oy = self:get_line_screen_position(1)
local line = math.floor((y - oy) / self:get_line_height()) + 1
line = common.clamp(line, 1, #self.doc.lines)
local col = self:get_x_offset_col(line, x - ox)
return line, col
end
function DocView:scroll_to_line(line, ignore_if_visible, instant)
local min, max = self:get_visible_line_range()
if not (ignore_if_visible and line > min and line < max) then
local lh = self:get_line_height()
self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2)
if instant then
self.scroll.y = self.scroll.to.y
end
end
end
function DocView:scroll_to_make_visible(line, col)
local min = self:get_line_height() * (line - 1)
local max = self:get_line_height() * (line + 2) - self.size.y
self.scroll.to.y = math.min(self.scroll.to.y, min)
self.scroll.to.y = math.max(self.scroll.to.y, max)
local gw = self:get_gutter_width()
local xoffset = self:get_col_x_offset(line, col)
local max = xoffset - self.size.x + gw + self.size.x / 5
self.scroll.to.x = math.max(0, max)
end
local function mouse_selection(doc, clicks, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then
line1, col1, line2, col2 = line2, col2, line1, col1
end
if clicks == 2 then
line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2)
elseif clicks == 3 then
if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then
doc:insert(math.huge, math.huge, "\n")
end
line1, col1, line2, col2 = line1, 1, line2 + 1, 1
end
if swap then
return line2, col2, line1, col1
end
return line1, col1, line2, col2
end
function DocView:on_mouse_pressed(button, x, y, clicks)
local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then
return
end
if keymap.modkeys["shift"] then
if clicks == 1 then
local line1, col1 = select(3, self.doc:get_selection())
local line2, col2 = self:resolve_screen_position(x, y)
self.doc:set_selection(line2, col2, line1, col1)
end
else
local line, col = self:resolve_screen_position(x, y)
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
self.mouse_selecting = { line, col, clicks = clicks }
end
self.blink_timer = 0
end
function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...)
if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then
self.cursor = "arrow"
else
self.cursor = "ibeam"
end
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2 = table.unpack(self.mouse_selecting)
local clicks = self.mouse_selecting.clicks
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2))
end
end
function DocView:on_mouse_released(button)
DocView.super.on_mouse_released(self, button)
self.mouse_selecting = nil
end
function DocView:on_text_input(text)
self.doc:text_input(text)
end
function DocView:update()
-- scroll to make caret visible and reset blink timer if it moved
local line, col = self.doc:get_selection()
if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then
if core.active_view == self then
self:scroll_to_make_visible(line, col)
end
self.blink_timer = 0
self.last_line, self.last_col = line, col
end
-- update blink timer
if self == core.active_view and not self.mouse_selecting then
local n = blink_period / 2
local prev = self.blink_timer
self.blink_timer = (self.blink_timer + 1 / config.fps) % blink_period
if (self.blink_timer > n) ~= (prev > n) then
core.redraw = true
end
end
DocView.super.update(self)
end
function DocView:draw_line_highlight(x, y)
local lh = self:get_line_height()
renderer.draw_rect(x, y, self.size.x, lh, style.line_highlight)
end
function DocView:draw_line_text(idx, x, y)
local tx, ty = x, y + self:get_line_text_y_offset()
local font = self:get_font()
for _, type, text in self.doc.highlighter:each_token(idx) do
local color = style.syntax[type]
tx = renderer.draw_text(font, text, tx, ty, color)
end
end
function DocView:draw_line_body(idx, x, y)
local line, col = self.doc:get_selection()
-- draw selection if it overlaps this line
local line1, col1, line2, col2 = self.doc:get_selection(true)
if idx >= line1 and idx <= line2 then
local text = self.doc.lines[idx]
if line1 ~= idx then col1 = 1 end
if line2 ~= idx then col2 = #text + 1 end
local x1 = x + self:get_col_x_offset(idx, col1)
local x2 = x + self:get_col_x_offset(idx, col2)
local lh = self:get_line_height()
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
end
-- draw line highlight if caret is on this line
if config.highlight_current_line and not self.doc:has_selection()
and line == idx and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y)
end
-- draw line's text
self:draw_line_text(idx, x, y)
-- draw caret if it overlaps this line
if line == idx and core.active_view == self
and self.blink_timer < blink_period / 2
and system.window_has_focus() then
local lh = self:get_line_height()
local x1 = x + self:get_col_x_offset(line, col)
renderer.draw_rect(x1, y, style.caret_width, lh, style.caret)
end
end
function DocView:draw_line_gutter(idx, x, y)
local color = style.line_number
local line1, _, line2, _ = self.doc:get_selection(true)
if idx >= line1 and idx <= line2 then
color = style.line_number2
end
local yoffset = self:get_line_text_y_offset()
x = x + style.padding.x
renderer.draw_text(self:get_font(), idx, x, y + yoffset, color)
end
function DocView:draw()
self:draw_background(style.background)
local font = self:get_font()
font:set_tab_width(font:get_width(" ") * config.indent_size)
local minline, maxline = self:get_visible_line_range()
local lh = self:get_line_height()
local _, y = self:get_line_screen_position(minline)
local x = self.position.x
for i = minline, maxline do
self:draw_line_gutter(i, x, y)
y = y + lh
end
local x, y = self:get_line_screen_position(minline)
local gw = self:get_gutter_width()
local pos = self.position
core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
for i = minline, maxline do
self:draw_line_body(i, x, y)
y = y + lh
end
core.pop_clip_rect()
self:draw_scrollbar()
end
return DocView

View File

@ -0,0 +1,480 @@
require "core.strict"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local command
local keymap
local RootView
local StatusView
local CommandView
local Doc
local core = {}
local function project_scan_thread()
local function diff_files(a, b)
if #a ~= #b then return true end
for i, v in ipairs(a) do
if b[i].filename ~= v.filename
or b[i].modified ~= v.modified then
return true
end
end
end
local function compare_file(a, b)
return a.filename < b.filename
end
local function get_files(path, t)
coroutine.yield()
t = t or {}
local size_limit = config.file_size_limit * 10e5
local all = system.list_dir(path) or {}
local dirs, files = {}, {}
for _, file in ipairs(all) do
if not common.match_pattern(file, config.ignore_files) then
local file = (path ~= "." and path .. PATHSEP or "") .. file
local info = system.get_file_info(file)
if info and info.size < size_limit then
info.filename = file
table.insert(info.type == "dir" and dirs or files, info)
end
end
end
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
get_files(f.filename, t)
end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t
end
while true do
-- get project files and replace previous table if the new table is
-- different
local t = get_files(".")
if diff_files(core.project_files, t) then
core.project_files = t
core.redraw = true
end
-- wait for next scan
coroutine.yield(config.project_scan_rate)
end
end
function core.init()
command = require "core.command"
keymap = require "core.keymap"
RootView = require "core.rootview"
StatusView = require "core.statusview"
CommandView = require "core.commandview"
Doc = require "core.doc"
local project_dir = EXEDIR
local files = {}
for i = 2, #ARGS do
local info = system.get_file_info(ARGS[i]) or {}
if info.type == "file" then
table.insert(files, system.absolute_path(ARGS[i]))
elseif info.type == "dir" then
project_dir = ARGS[i]
end
end
system.chdir(project_dir)
core.frame_start = 0
core.clip_rect_stack = {{ 0,0,0,0 }}
core.log_items = {}
core.docs = {}
core.threads = setmetatable({}, { __mode = "k" })
core.project_files = {}
core.redraw = true
core.root_view = RootView()
core.command_view = CommandView()
core.status_view = StatusView()
core.root_view.root_node:split("down", core.command_view, true)
core.root_view.root_node.b:split("down", core.status_view, true)
core.add_thread(project_scan_thread)
command.add_defaults()
local got_plugin_error = not core.load_plugins()
local got_user_error = not core.try(require, "user")
local got_project_error = not core.load_project_module()
for _, filename in ipairs(files) do
core.root_view:open_doc(core.open_doc(filename))
end
if got_plugin_error or got_user_error or got_project_error then
command.perform("core:open-log")
end
end
local temp_uid = (system.get_time() * 1000) % 0xffffffff
local temp_file_prefix = string.format(".lite_temp_%08x", temp_uid)
local temp_file_counter = 0
local function delete_temp_files()
for _, filename in ipairs(system.list_dir(EXEDIR)) do
if filename:find(temp_file_prefix, 1, true) == 1 then
os.remove(EXEDIR .. PATHSEP .. filename)
end
end
end
function core.temp_filename(ext)
temp_file_counter = temp_file_counter + 1
return EXEDIR .. PATHSEP .. temp_file_prefix
.. string.format("%06x", temp_file_counter) .. (ext or "")
end
function core.quit(force)
if force then
delete_temp_files()
os.exit()
end
local dirty_count = 0
local dirty_name
for _, doc in ipairs(core.docs) do
if doc:is_dirty() then
dirty_count = dirty_count + 1
dirty_name = doc:get_name()
end
end
if dirty_count > 0 then
local text
if dirty_count == 1 then
text = string.format("\"%s\" has unsaved changes. Quit anyway?", dirty_name)
else
text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count)
end
local confirm = system.show_confirm_dialog("Unsaved Changes", text)
if not confirm then return end
end
core.quit(true)
end
function core.load_plugins()
local no_errors = true
local files = system.list_dir(EXEDIR .. "/data/plugins")
for _, filename in ipairs(files) do
local modname = "plugins." .. filename:gsub(".lua$", "")
local ok = core.try(require, modname)
if ok then
core.log_quiet("Loaded plugin %q", modname)
else
no_errors = false
end
end
return no_errors
end
function core.load_project_module()
local filename = ".lite_project.lua"
if system.get_file_info(filename) then
return core.try(function()
local fn, err = loadfile(filename)
if not fn then error("Error when loading project module:\n\t" .. err) end
fn()
core.log_quiet("Loaded project module")
end)
end
return true
end
function core.reload_module(name)
local old = package.loaded[name]
package.loaded[name] = nil
local new = require(name)
if type(old) == "table" then
for k, v in pairs(new) do old[k] = v end
package.loaded[name] = old
end
end
function core.set_active_view(view)
assert(view, "Tried to set active view to nil")
if view ~= core.active_view then
core.last_active_view = core.active_view
core.active_view = view
end
end
function core.add_thread(f, weak_ref)
local key = weak_ref or #core.threads + 1
local fn = function() return core.try(f) end
core.threads[key] = { cr = coroutine.create(fn), wake = 0 }
end
function core.push_clip_rect(x, y, w, h)
local x2, y2, w2, h2 = table.unpack(core.clip_rect_stack[#core.clip_rect_stack])
local r, b, r2, b2 = x+w, y+h, x2+w2, y2+h2
x, y = math.max(x, x2), math.max(y, y2)
b, r = math.min(b, b2), math.min(r, r2)
w, h = r-x, b-y
table.insert(core.clip_rect_stack, { x, y, w, h })
renderer.set_clip_rect(x, y, w, h)
end
function core.pop_clip_rect()
table.remove(core.clip_rect_stack)
local x, y, w, h = table.unpack(core.clip_rect_stack[#core.clip_rect_stack])
renderer.set_clip_rect(x, y, w, h)
end
function core.open_doc(filename)
if filename then
-- try to find existing doc for filename
local abs_filename = system.absolute_path(filename)
for _, doc in ipairs(core.docs) do
if doc.filename
and abs_filename == system.absolute_path(doc.filename) then
return doc
end
end
end
-- no existing doc for filename; create new
local doc = Doc(filename)
table.insert(core.docs, doc)
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename)
return doc
end
function core.get_views_referencing_doc(doc)
local res = {}
local views = core.root_view.root_node:get_children()
for _, view in ipairs(views) do
if view.doc == doc then table.insert(res, view) end
end
return res
end
local function log(icon, icon_color, fmt, ...)
local text = string.format(fmt, ...)
if icon then
core.status_view:show_message(icon, icon_color, text)
end
local info = debug.getinfo(2, "Sl")
local at = string.format("%s:%d", info.short_src, info.currentline)
local item = { text = text, time = os.time(), at = at }
table.insert(core.log_items, item)
if #core.log_items > config.max_log_items then
table.remove(core.log_items, 1)
end
return item
end
function core.log(...)
return log("i", style.text, ...)
end
function core.log_quiet(...)
return log(nil, nil, ...)
end
function core.error(...)
return log("!", style.accent, ...)
end
function core.try(fn, ...)
local err
local ok, res = xpcall(fn, function(msg)
local item = core.error("%s", msg)
item.info = debug.traceback(nil, 2):gsub("\t", "")
err = msg
end, ...)
if ok then
return true, res
end
return false, err
end
function core.on_event(type, ...)
local did_keymap = false
if type == "textinput" then
core.root_view:on_text_input(...)
elseif type == "keypressed" then
did_keymap = keymap.on_key_pressed(...)
elseif type == "keyreleased" then
keymap.on_key_released(...)
elseif type == "mousemoved" then
core.root_view:on_mouse_moved(...)
elseif type == "mousepressed" then
core.root_view:on_mouse_pressed(...)
elseif type == "mousereleased" then
core.root_view:on_mouse_released(...)
elseif type == "mousewheel" then
core.root_view:on_mouse_wheel(...)
elseif type == "filedropped" then
local filename, mx, my = ...
local info = system.get_file_info(filename)
if info and info.type == "dir" then
system.exec(string.format("%q %q", EXEFILE, filename))
else
local ok, doc = core.try(core.open_doc, filename)
if ok then
local node = core.root_view.root_node:get_child_overlapping_point(mx, my)
node:set_active_view(node.active_view)
core.root_view:open_doc(doc)
end
end
elseif type == "quit" then
core.quit()
end
return did_keymap
end
function core.step()
-- handle events
local did_keymap = false
local mouse_moved = false
local mouse = { x = 0, y = 0, dx = 0, dy = 0 }
for type, a,b,c,d in system.poll_event do
if type == "mousemoved" then
mouse_moved = true
mouse.x, mouse.y = a, b
mouse.dx, mouse.dy = mouse.dx + c, mouse.dy + d
elseif type == "textinput" and did_keymap then
did_keymap = false
else
local _, res = core.try(core.on_event, type, a, b, c, d)
did_keymap = res or did_keymap
end
core.redraw = true
end
if mouse_moved then
core.try(core.on_event, "mousemoved", mouse.x, mouse.y, mouse.dx, mouse.dy)
end
local width, height = renderer.get_size()
-- update
core.root_view.size.x, core.root_view.size.y = width, height
core.root_view:update()
if not core.redraw then return false end
core.redraw = false
-- close unreferenced docs
for i = #core.docs, 1, -1 do
local doc = core.docs[i]
if #core.get_views_referencing_doc(doc) == 0 then
table.remove(core.docs, i)
core.log_quiet("Closed doc \"%s\"", doc:get_name())
end
end
-- update window title
local name = core.active_view:get_name()
local title = (name ~= "---") and (name .. " - lite") or "lite"
if title ~= core.window_title then
system.set_window_title(title)
core.window_title = title
end
-- draw
renderer.begin_frame()
core.clip_rect_stack[1] = { 0, 0, width, height }
renderer.set_clip_rect(table.unpack(core.clip_rect_stack[1]))
core.root_view:draw()
renderer.end_frame()
return true
end
local run_threads = coroutine.wrap(function()
while true do
local max_time = 1 / config.fps - 0.004
local ran_any_threads = false
for k, thread in pairs(core.threads) do
-- run thread
if thread.wake < system.get_time() then
local _, wait = assert(coroutine.resume(thread.cr))
if coroutine.status(thread.cr) == "dead" then
if type(k) == "number" then
table.remove(core.threads, k)
else
core.threads[k] = nil
end
elseif wait then
thread.wake = system.get_time() + wait
end
ran_any_threads = true
end
-- stop running threads if we're about to hit the end of frame
if system.get_time() - core.frame_start > max_time then
coroutine.yield()
end
end
if not ran_any_threads then coroutine.yield() end
end
end)
function core.run()
while true do
core.frame_start = system.get_time()
local did_redraw = core.step()
run_threads()
if not did_redraw and not system.window_has_focus() then
system.wait_event(0.25)
end
local elapsed = system.get_time() - core.frame_start
system.sleep(math.max(0, 1 / config.fps - elapsed))
end
end
function core.on_error(err)
-- write error to file
local fp = io.open(EXEDIR .. "/error.txt", "wb")
fp:write("Error: " .. tostring(err) .. "\n")
fp:write(debug.traceback(nil, 4))
fp:close()
-- save copy of all unsaved documents
for _, doc in ipairs(core.docs) do
if doc:is_dirty() and doc.filename then
doc:save(doc.filename .. "~")
end
end
end
return core

View File

@ -0,0 +1,186 @@
local command = require "core.command"
local keymap = {}
keymap.modkeys = {}
keymap.map = {}
keymap.reverse_map = {}
local modkey_map = {
["left ctrl"] = "ctrl",
["right ctrl"] = "ctrl",
["left shift"] = "shift",
["right shift"] = "shift",
["left alt"] = "alt",
["right alt"] = "altgr",
}
local modkeys = { "ctrl", "alt", "altgr", "shift" }
local function key_to_stroke(k)
local stroke = ""
for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then
stroke = stroke .. mk .. "+"
end
end
return stroke .. k
end
function keymap.add(map, overwrite)
for stroke, commands in pairs(map) do
if type(commands) == "string" then
commands = { commands }
end
if overwrite then
keymap.map[stroke] = commands
else
keymap.map[stroke] = keymap.map[stroke] or {}
for i = #commands, 1, -1 do
table.insert(keymap.map[stroke], 1, commands[i])
end
end
for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = stroke
end
end
end
function keymap.get_binding(cmd)
return keymap.reverse_map[cmd]
end
function keymap.on_key_pressed(k)
local mk = modkey_map[k]
if mk then
keymap.modkeys[mk] = true
-- work-around for windows where `altgr` is treated as `ctrl+alt`
if mk == "altgr" then
keymap.modkeys["ctrl"] = false
end
else
local stroke = key_to_stroke(k)
local commands = keymap.map[stroke]
if commands then
for _, cmd in ipairs(commands) do
local performed = command.perform(cmd)
if performed then break end
end
return true
end
end
return false
end
function keymap.on_key_released(k)
local mk = modkey_map[k]
if mk then
keymap.modkeys[mk] = false
end
end
keymap.add {
["ctrl+shift+p"] = "core:find-command",
["ctrl+p"] = "core:find-file",
["ctrl+o"] = "core:open-file",
["ctrl+n"] = "core:new-doc",
["alt+return"] = "core:toggle-fullscreen",
["alt+shift+j"] = "root:split-left",
["alt+shift+l"] = "root:split-right",
["alt+shift+i"] = "root:split-up",
["alt+shift+k"] = "root:split-down",
["alt+j"] = "root:switch-to-left",
["alt+l"] = "root:switch-to-right",
["alt+i"] = "root:switch-to-up",
["alt+k"] = "root:switch-to-down",
["ctrl+w"] = "root:close",
["ctrl+tab"] = "root:switch-to-next-tab",
["ctrl+shift+tab"] = "root:switch-to-previous-tab",
["ctrl+pageup"] = "root:move-tab-left",
["ctrl+pagedown"] = "root:move-tab-right",
["alt+1"] = "root:switch-to-tab-1",
["alt+2"] = "root:switch-to-tab-2",
["alt+3"] = "root:switch-to-tab-3",
["alt+4"] = "root:switch-to-tab-4",
["alt+5"] = "root:switch-to-tab-5",
["alt+6"] = "root:switch-to-tab-6",
["alt+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9",
["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find",
["shift+f3"] = "find-replace:previous-find",
["ctrl+g"] = "doc:go-to-line",
["ctrl+s"] = "doc:save",
["ctrl+shift+s"] = "doc:save-as",
["ctrl+z"] = "doc:undo",
["ctrl+y"] = "doc:redo",
["ctrl+x"] = "doc:cut",
["ctrl+c"] = "doc:copy",
["ctrl+v"] = "doc:paste",
["escape"] = { "command:escape", "doc:select-none" },
["tab"] = { "command:complete", "doc:indent" },
["shift+tab"] = "doc:unindent",
["backspace"] = "doc:backspace",
["shift+backspace"] = "doc:backspace",
["ctrl+backspace"] = "doc:delete-to-previous-word-start",
["ctrl+shift+backspace"] = "doc:delete-to-previous-word-start",
["delete"] = "doc:delete",
["shift+delete"] = "doc:delete",
["ctrl+delete"] = "doc:delete-to-next-word-end",
["ctrl+shift+delete"] = "doc:delete-to-next-word-end",
["return"] = { "command:submit", "doc:newline" },
["keypad enter"] = { "command:submit", "doc:newline" },
["ctrl+return"] = "doc:newline-below",
["ctrl+shift+return"] = "doc:newline-above",
["ctrl+j"] = "doc:join-lines",
["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-next", "doc:select-word" },
["ctrl+l"] = "doc:select-lines",
["ctrl+/"] = "doc:toggle-line-comments",
["ctrl+up"] = "doc:move-lines-up",
["ctrl+down"] = "doc:move-lines-down",
["ctrl+shift+d"] = "doc:duplicate-lines",
["ctrl+shift+k"] = "doc:delete-lines",
["left"] = "doc:move-to-previous-char",
["right"] = "doc:move-to-next-char",
["up"] = { "command:select-previous", "doc:move-to-previous-line" },
["down"] = { "command:select-next", "doc:move-to-next-line" },
["ctrl+left"] = "doc:move-to-previous-word-start",
["ctrl+right"] = "doc:move-to-next-word-end",
["ctrl+["] = "doc:move-to-previous-block-start",
["ctrl+]"] = "doc:move-to-next-block-end",
["home"] = "doc:move-to-start-of-line",
["end"] = "doc:move-to-end-of-line",
["ctrl+home"] = "doc:move-to-start-of-doc",
["ctrl+end"] = "doc:move-to-end-of-doc",
["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page",
["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line",
["shift+down"] = "doc:select-to-next-line",
["ctrl+shift+left"] = "doc:select-to-previous-word-start",
["ctrl+shift+right"] = "doc:select-to-next-word-end",
["ctrl+shift+["] = "doc:select-to-previous-block-start",
["ctrl+shift+]"] = "doc:select-to-next-block-end",
["shift+home"] = "doc:select-to-start-of-line",
["shift+end"] = "doc:select-to-end-of-line",
["ctrl+shift+home"] = "doc:select-to-start-of-doc",
["ctrl+shift+end"] = "doc:select-to-end-of-doc",
["shift+pageup"] = "doc:select-to-previous-page",
["shift+pagedown"] = "doc:select-to-next-page",
}
return keymap

View File

@ -0,0 +1,74 @@
local core = require "core"
local style = require "core.style"
local View = require "core.view"
local LogView = View:extend()
function LogView:new()
LogView.super.new(self)
self.last_item = core.log_items[#core.log_items]
self.scrollable = true
self.yoffset = 0
end
function LogView:get_name()
return "Log"
end
function LogView:update()
local item = core.log_items[#core.log_items]
if self.last_item ~= item then
self.last_item = item
self.scroll.to.y = 0
self.yoffset = -(style.font:get_height() + style.padding.y)
end
self:move_towards("yoffset", 0)
LogView.super.update(self)
end
local function draw_text_multiline(font, text, x, y, color)
local th = font:get_height()
local resx, resy = x, y
for line in text:gmatch("[^\n]+") do
resy = y
resx = renderer.draw_text(style.font, line, x, y, color)
y = y + th
end
return resx, resy
end
function LogView:draw()
self:draw_background(style.background)
local ox, oy = self:get_content_offset()
local th = style.font:get_height()
local y = oy + style.padding.y + self.yoffset
for i = #core.log_items, 1, -1 do
local x = ox + style.padding.x
local item = core.log_items[i]
local time = os.date(nil, item.time)
x = renderer.draw_text(style.font, time, x, y, style.dim)
x = x + style.padding.x
local subx = x
x, y = draw_text_multiline(style.font, item.text, x, y, style.text)
renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim)
y = y + th
if item.info then
subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim)
y = y + th
end
y = y + style.padding.y
end
end
return LogView

View File

@ -0,0 +1,58 @@
local Object = {}
Object.__index = Object
function Object:new()
end
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
function Object:implement(...)
for _, cls in pairs({...}) do
for k, v in pairs(cls) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
end
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
function Object:__tostring()
return "Object"
end
function Object:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
return Object

View File

@ -0,0 +1,504 @@
local core = require "core"
local common = require "core.common"
local style = require "core.style"
local keymap = require "core.keymap"
local Object = require "core.object"
local View = require "core.view"
local DocView = require "core.docview"
local EmptyView = View:extend()
local function draw_text(x, y, color)
local th = style.big_font:get_height()
local dh = th + style.padding.y * 2
x = renderer.draw_text(style.big_font, "lite", x, y + (dh - th) / 2, color)
x = x + style.padding.x
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
local lines = {
{ fmt = "%s to run a command", cmd = "core:find-command" },
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
}
th = style.font:get_height()
y = y + (dh - th * 2 - style.padding.y) / 2
local w = 0
for _, line in ipairs(lines) do
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
y = y + th + style.padding.y
end
return w, dh
end
function EmptyView:draw()
self:draw_background(style.background)
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
local y = self.position.y + (self.size.y - h) / 2
draw_text(x, y, style.dim)
end
local Node = Object:extend()
function Node:new(type)
self.type = type or "leaf"
self.position = { x = 0, y = 0 }
self.size = { x = 0, y = 0 }
self.views = {}
self.divider = 0.5
if self.type == "leaf" then
self:add_view(EmptyView())
end
end
function Node:propagate(fn, ...)
self.a[fn](self.a, ...)
self.b[fn](self.b, ...)
end
function Node:on_mouse_moved(x, y, ...)
self.hovered_tab = self:get_tab_overlapping_point(x, y)
if self.type == "leaf" then
self.active_view:on_mouse_moved(x, y, ...)
else
self:propagate("on_mouse_moved", x, y, ...)
end
end
function Node:on_mouse_released(...)
if self.type == "leaf" then
self.active_view:on_mouse_released(...)
else
self:propagate("on_mouse_released", ...)
end
end
function Node:consume(node)
for k, _ in pairs(self) do self[k] = nil end
for k, v in pairs(node) do self[k] = v end
end
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
function Node:split(dir, view, locked)
assert(self.type == "leaf", "Tried to split non-leaf node")
local type = assert(type_map[dir], "Invalid direction")
local last_active = core.active_view
local child = Node()
child:consume(self)
self:consume(Node(type))
self.a = child
self.b = Node()
if view then self.b:add_view(view) end
if locked then
self.b.locked = locked
core.set_active_view(last_active)
end
if dir == "up" or dir == "left" then
self.a, self.b = self.b, self.a
end
return child
end
function Node:close_active_view(root)
local do_close = function()
if #self.views > 1 then
local idx = self:get_view_idx(self.active_view)
table.remove(self.views, idx)
self:set_active_view(self.views[idx] or self.views[#self.views])
else
local parent = self:get_parent_node(root)
local is_a = (parent.a == self)
local other = parent[is_a and "b" or "a"]
if other:get_locked_size() then
self.views = {}
self:add_view(EmptyView())
else
parent:consume(other)
local p = parent
while p.type ~= "leaf" do
p = p[is_a and "a" or "b"]
end
p:set_active_view(p.active_view)
end
end
core.last_active_view = nil
end
self.active_view:try_close(do_close)
end
function Node:add_view(view)
assert(self.type == "leaf", "Tried to add view to non-leaf node")
assert(not self.locked, "Tried to add view to locked node")
if self.views[1] and self.views[1]:is(EmptyView) then
table.remove(self.views)
end
table.insert(self.views, view)
self:set_active_view(view)
end
function Node:set_active_view(view)
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
self.active_view = view
core.set_active_view(view)
end
function Node:get_view_idx(view)
for i, v in ipairs(self.views) do
if v == view then return i end
end
end
function Node:get_node_for_view(view)
for _, v in ipairs(self.views) do
if v == view then return self end
end
if self.type ~= "leaf" then
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
end
end
function Node:get_parent_node(root)
if root.a == self or root.b == self then
return root
elseif root.type ~= "leaf" then
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
end
end
function Node:get_children(t)
t = t or {}
for _, view in ipairs(self.views) do
table.insert(t, view)
end
if self.a then self.a:get_children(t) end
if self.b then self.b:get_children(t) end
return t
end
function Node:get_divider_overlapping_point(px, py)
if self.type ~= "leaf" then
local p = 6
local x, y, w, h = self:get_divider_rect()
x, y = x - p, y - p
w, h = w + p * 2, h + p * 2
if px > x and py > y and px < x + w and py < y + h then
return self
end
return self.a:get_divider_overlapping_point(px, py)
or self.b:get_divider_overlapping_point(px, py)
end
end
function Node:get_tab_overlapping_point(px, py)
if #self.views == 1 then return nil end
local x, y, w, h = self:get_tab_rect(1)
if px >= x and py >= y and px < x + w * #self.views and py < y + h then
return math.floor((px - x) / w) + 1
end
end
function Node:get_child_overlapping_point(x, y)
local child
if self.type == "leaf" then
return self
elseif self.type == "hsplit" then
child = (x < self.b.position.x) and self.a or self.b
elseif self.type == "vsplit" then
child = (y < self.b.position.y) and self.a or self.b
end
return child:get_child_overlapping_point(x, y)
end
function Node:get_tab_rect(idx)
local tw = math.min(style.tab_width, math.ceil(self.size.x / #self.views))
local h = style.font:get_height() + style.padding.y * 2
return self.position.x + (idx-1) * tw, self.position.y, tw, h
end
function Node:get_divider_rect()
local x, y = self.position.x, self.position.y
if self.type == "hsplit" then
return x + self.a.size.x, y, style.divider_size, self.size.y
elseif self.type == "vsplit" then
return x, y + self.a.size.y, self.size.x, style.divider_size
end
end
function Node:get_locked_size()
if self.type == "leaf" then
if self.locked then
local size = self.active_view.size
return size.x, size.y
end
else
local x1, y1 = self.a:get_locked_size()
local x2, y2 = self.b:get_locked_size()
if x1 and x2 then
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
return x1 + x2 + dsx, y1 + y2 + dsy
end
end
end
local function copy_position_and_size(dst, src)
dst.position.x, dst.position.y = src.position.x, src.position.y
dst.size.x, dst.size.y = src.size.x, src.size.y
end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both
local function calc_split_sizes(self, x, y, x1, x2)
local n
local ds = (x1 and x1 < 1 or x2 and x2 < 1) and 0 or style.divider_size
if x1 then
n = x1 + ds
elseif x2 then
n = self.size[x] - x2
else
n = math.floor(self.size[x] * self.divider)
end
self.a.position[x] = self.position[x]
self.a.position[y] = self.position[y]
self.a.size[x] = n - ds
self.a.size[y] = self.size[y]
self.b.position[x] = self.position[x] + n
self.b.position[y] = self.position[y]
self.b.size[x] = self.size[x] - n
self.b.size[y] = self.size[y]
end
function Node:update_layout()
if self.type == "leaf" then
local av = self.active_view
if #self.views > 1 then
local _, _, _, th = self:get_tab_rect(1)
av.position.x, av.position.y = self.position.x, self.position.y + th
av.size.x, av.size.y = self.size.x, self.size.y - th
else
copy_position_and_size(av, self)
end
else
local x1, y1 = self.a:get_locked_size()
local x2, y2 = self.b:get_locked_size()
if self.type == "hsplit" then
calc_split_sizes(self, "x", "y", x1, x2)
elseif self.type == "vsplit" then
calc_split_sizes(self, "y", "x", y1, y2)
end
self.a:update_layout()
self.b:update_layout()
end
end
function Node:update()
if self.type == "leaf" then
for _, view in ipairs(self.views) do
view:update()
end
else
self.a:update()
self.b:update()
end
end
function Node:draw_tabs()
local x, y, _, h = self:get_tab_rect(1)
local ds = style.divider_size
core.push_clip_rect(x, y, self.size.x, h)
renderer.draw_rect(x, y, self.size.x, h, style.background2)
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
for i, view in ipairs(self.views) do
local x, y, w, h = self:get_tab_rect(i)
local text = view:get_name()
local color = style.dim
if view == self.active_view then
color = style.text
renderer.draw_rect(x, y, w, h, style.background)
renderer.draw_rect(x + w, y, ds, h, style.divider)
renderer.draw_rect(x - ds, y, ds, h, style.divider)
end
if i == self.hovered_tab then
color = style.text
end
core.push_clip_rect(x, y, w, h)
x, w = x + style.padding.x, w - style.padding.x * 2
local align = style.font:get_width(text) > w and "left" or "center"
common.draw_text(style.font, color, text, align, x, y, w, h)
core.pop_clip_rect()
end
core.pop_clip_rect()
end
function Node:draw()
if self.type == "leaf" then
if #self.views > 1 then
self:draw_tabs()
end
local pos, size = self.active_view.position, self.active_view.size
core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
self.active_view:draw()
core.pop_clip_rect()
else
local x, y, w, h = self:get_divider_rect()
renderer.draw_rect(x, y, w, h, style.divider)
self:propagate("draw")
end
end
local RootView = View:extend()
function RootView:new()
RootView.super.new(self)
self.root_node = Node()
self.deferred_draws = {}
self.mouse = { x = 0, y = 0 }
end
function RootView:defer_draw(fn, ...)
table.insert(self.deferred_draws, 1, { fn = fn, ... })
end
function RootView:get_active_node()
return self.root_node:get_node_for_view(core.active_view)
end
function RootView:open_doc(doc)
local node = self:get_active_node()
if node.locked and core.last_active_view then
core.set_active_view(core.last_active_view)
node = self:get_active_node()
end
assert(not node.locked, "Cannot open doc on locked node")
for i, view in ipairs(node.views) do
if view.doc == doc then
node:set_active_view(node.views[i])
return view
end
end
local view = DocView(doc)
node:add_view(view)
self.root_node:update_layout()
view:scroll_to_line(view.doc:get_selection(), true, true)
return view
end
function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y)
if div then
self.dragged_divider = div
return
end
local node = self.root_node:get_child_overlapping_point(x, y)
local idx = node:get_tab_overlapping_point(x, y)
if idx then
node:set_active_view(node.views[idx])
if button == "middle" then
node:close_active_view(self.root_node)
end
else
core.set_active_view(node.active_view)
node.active_view:on_mouse_pressed(button, x, y, clicks)
end
end
function RootView:on_mouse_released(...)
if self.dragged_divider then
self.dragged_divider = nil
end
self.root_node:on_mouse_released(...)
end
function RootView:on_mouse_moved(x, y, dx, dy)
if self.dragged_divider then
local node = self.dragged_divider
if node.type == "hsplit" then
node.divider = node.divider + dx / node.size.x
else
node.divider = node.divider + dy / node.size.y
end
node.divider = common.clamp(node.divider, 0.01, 0.99)
return
end
self.mouse.x, self.mouse.y = x, y
self.root_node:on_mouse_moved(x, y, dx, dy)
local node = self.root_node:get_child_overlapping_point(x, y)
local div = self.root_node:get_divider_overlapping_point(x, y)
if div then
system.set_cursor(div.type == "hsplit" and "sizeh" or "sizev")
elseif node:get_tab_overlapping_point(x, y) then
system.set_cursor("arrow")
else
system.set_cursor(node.active_view.cursor)
end
end
function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y)
node.active_view:on_mouse_wheel(...)
end
function RootView:on_text_input(...)
core.active_view:on_text_input(...)
end
function RootView:update()
copy_position_and_size(self.root_node, self)
self.root_node:update()
self.root_node:update_layout()
end
function RootView:draw()
self.root_node:draw()
while #self.deferred_draws > 0 do
local t = table.remove(self.deferred_draws)
t.fn(table.unpack(t))
end
end
return RootView

View File

@ -0,0 +1,141 @@
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local style = require "core.style"
local DocView = require "core.docview"
local LogView = require "core.logview"
local View = require "core.view"
local StatusView = View:extend()
StatusView.separator = " "
StatusView.separator2 = " | "
function StatusView:new()
StatusView.super.new(self)
self.message_timeout = 0
self.message = {}
end
function StatusView:on_mouse_pressed()
core.set_active_view(core.last_active_view)
if system.get_time() < self.message_timeout
and not core.active_view:is(LogView) then
command.perform "core:open-log"
end
end
function StatusView:show_message(icon, icon_color, text)
self.message = {
icon_color, style.icon_font, icon,
style.dim, style.font, StatusView.separator2, style.text, text
}
self.message_timeout = system.get_time() + config.message_timeout
end
function StatusView:update()
self.size.y = style.font:get_height() + style.padding.y * 2
if system.get_time() < self.message_timeout then
self.scroll.to.y = self.size.y
else
self.scroll.to.y = 0
end
StatusView.super.update(self)
end
local function draw_items(self, items, x, y, draw_fn)
local font = style.font
local color = style.text
for _, item in ipairs(items) do
if type(item) == "userdata" then
font = item
elseif type(item) == "table" then
color = item
else
x = draw_fn(font, color, item, nil, x, y, 0, self.size.y)
end
end
return x
end
local function text_width(font, _, text, _, x)
return x + font:get_width(text)
end
function StatusView:draw_items(items, right_align, yoffset)
local x, y = self:get_content_offset()
y = y + (yoffset or 0)
if right_align then
local w = draw_items(self, items, 0, 0, text_width)
x = x + self.size.x - w - style.padding.x
draw_items(self, items, x, y, common.draw_text)
else
x = x + style.padding.x
draw_items(self, items, x, y, common.draw_text)
end
end
function StatusView:get_items()
if getmetatable(core.active_view) == DocView then
local dv = core.active_view
local line, col = dv.doc:get_selection()
local dirty = dv.doc:is_dirty()
return {
dirty and style.accent or style.text, style.icon_font, "f",
style.dim, style.font, self.separator2, style.text,
dv.doc.filename and style.text or style.dim, dv.doc:get_name(),
style.text,
self.separator,
"line: ", line,
self.separator,
col > config.line_limit and style.accent or style.text, "col: ", col,
style.text,
self.separator,
string.format("%d%%", line / #dv.doc.lines * 100),
}, {
style.icon_font, "g",
style.font, style.dim, self.separator2, style.text,
#dv.doc.lines, " lines",
self.separator,
dv.doc.crlf and "CRLF" or "LF"
}
end
return {}, {
style.icon_font, "g",
style.font, style.dim, self.separator2,
#core.docs, style.text, " / ",
#core.project_files, " files"
}
end
function StatusView:draw()
self:draw_background(style.background2)
if self.message then
self:draw_items(self.message, false, self.size.y)
end
local left, right = self:get_items()
self:draw_items(left)
self:draw_items(right, true)
end
return StatusView

View File

@ -0,0 +1,26 @@
local strict = {}
strict.defined = {}
-- used to define a global variable
function global(t)
for k, v in pairs(t) do
strict.defined[k] = true
rawset(_G, k, v)
end
end
function strict.__newindex(t, k, v)
error("cannot set undefined variable: " .. k, 2)
end
function strict.__index(t, k)
if not strict.defined[k] then
error("cannot get undefined variable: " .. k, 2)
end
end
setmetatable(_G, strict)

View File

@ -0,0 +1,42 @@
local common = require "core.common"
local style = {}
style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) }
style.divider_size = common.round(1 * SCALE)
style.scrollbar_size = common.round(4 * SCALE)
style.caret_width = common.round(2 * SCALE)
style.tab_width = common.round(170 * SCALE)
style.font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 14 * SCALE)
style.big_font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 34 * SCALE)
style.icon_font = renderer.font.load(EXEDIR .. "/data/fonts/icons.ttf", 14 * SCALE)
style.code_font = renderer.font.load(EXEDIR .. "/data/fonts/monospace.ttf", 13.5 * SCALE)
style.background = { common.color "#2e2e32" }
style.background2 = { common.color "#252529" }
style.background3 = { common.color "#252529" }
style.text = { common.color "#97979c" }
style.caret = { common.color "#93DDFA" }
style.accent = { common.color "#e1e1e6" }
style.dim = { common.color "#525257" }
style.divider = { common.color "#202024" }
style.selection = { common.color "#48484f" }
style.line_number = { common.color "#525259" }
style.line_number2 = { common.color "#83838f" }
style.line_highlight = { common.color "#343438" }
style.scrollbar = { common.color "#414146" }
style.scrollbar2 = { common.color "#4b4b52" }
style.syntax = {}
style.syntax["normal"] = { common.color "#e1e1e6" }
style.syntax["symbol"] = { common.color "#e1e1e6" }
style.syntax["comment"] = { common.color "#676b6f" }
style.syntax["keyword"] = { common.color "#E58AC9" }
style.syntax["keyword2"] = { common.color "#F77483" }
style.syntax["number"] = { common.color "#FFA94D" }
style.syntax["literal"] = { common.color "#FFA94D" }
style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" }
style.syntax["function"] = { common.color "#93DDFA" }
return style

View File

@ -0,0 +1,30 @@
local common = require "core.common"
local syntax = {}
syntax.items = {}
local plain_text_syntax = { patterns = {}, symbols = {} }
function syntax.add(t)
table.insert(syntax.items, t)
end
local function find(string, field)
for i = #syntax.items, 1, -1 do
local t = syntax.items[i]
if common.match_pattern(string, t[field] or {}) then
return t
end
end
end
function syntax.get(filename, header)
return find(filename, "files")
or find(header, "headers")
or plain_text_syntax
end
return syntax

View File

@ -0,0 +1,112 @@
local tokenizer = {}
local function push_token(t, type, text)
local prev_type = t[#t-1]
local prev_text = t[#t]
if prev_type and (prev_type == type or prev_text:find("^%s*$")) then
t[#t-1] = type
t[#t] = prev_text .. text
else
table.insert(t, type)
table.insert(t, text)
end
end
local function is_escaped(text, idx, esc)
local byte = esc:byte()
local count = 0
for i = idx - 1, 1, -1 do
if text:byte(i) ~= byte then break end
count = count + 1
end
return count % 2 == 1
end
local function find_non_escaped(text, pattern, offset, esc)
while true do
local s, e = text:find(pattern, offset)
if not s then break end
if esc and is_escaped(text, s, esc) then
offset = e + 1
else
return s, e
end
end
end
function tokenizer.tokenize(syntax, text, state)
local res = {}
local i = 1
if #syntax.patterns == 0 then
return { "normal", text }
end
while i <= #text do
-- continue trying to match the end pattern of a pair if we have a state set
if state then
local p = syntax.patterns[state]
local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3])
if s then
push_token(res, p.type, text:sub(i, e))
state = nil
i = e + 1
else
push_token(res, p.type, text:sub(i))
break
end
end
-- find matching pattern
local matched = false
for n, p in ipairs(syntax.patterns) do
local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern
local s, e = text:find("^" .. pattern, i)
if s then
-- matched pattern; make and add token
local t = text:sub(s, e)
push_token(res, syntax.symbols[t] or p.type, t)
-- update state if this was a start|end pattern pair
if type(p.pattern) == "table" then
state = n
end
-- move cursor past this token
i = e + 1
matched = true
break
end
end
-- consume character if we didn't match
if not matched then
push_token(res, "normal", text:sub(i, i))
i = i + 1
end
end
return res, state
end
local function iter(t, i)
i = i + 2
local type, text = t[i], t[i+1]
if type then
return i, type, text
end
end
function tokenizer.each_token(t)
return iter, t, -1
end
return tokenizer

View File

@ -0,0 +1,151 @@
local core = require "core"
local config = require "core.config"
local style = require "core.style"
local common = require "core.common"
local Object = require "core.object"
local View = Object:extend()
function View:new()
self.position = { x = 0, y = 0 }
self.size = { x = 0, y = 0 }
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } }
self.cursor = "arrow"
self.scrollable = false
end
function View:move_towards(t, k, dest, rate)
if type(t) ~= "table" then
return self:move_towards(self, t, k, dest, rate)
end
local val = t[k]
if math.abs(val - dest) < 0.5 then
t[k] = dest
else
t[k] = common.lerp(val, dest, rate or 0.5)
end
if val ~= dest then
core.redraw = true
end
end
function View:try_close(do_close)
do_close()
end
function View:get_name()
return "---"
end
function View:get_scrollable_size()
return math.huge
end
function View:get_scrollbar_rect()
local sz = self:get_scrollable_size()
if sz <= self.size.y or sz == math.huge then
return 0, 0, 0, 0
end
local h = math.max(20, self.size.y * self.size.y / sz)
return
self.position.x + self.size.x - style.scrollbar_size,
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y),
style.scrollbar_size,
h
end
function View:scrollbar_overlaps_point(x, y)
local sx, sy, sw, sh = self:get_scrollbar_rect()
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh
end
function View:on_mouse_pressed(button, x, y, clicks)
if self:scrollbar_overlaps_point(x, y) then
self.dragging_scrollbar = true
return true
end
end
function View:on_mouse_released(button, x, y)
self.dragging_scrollbar = false
end
function View:on_mouse_moved(x, y, dx, dy)
if self.dragging_scrollbar then
local delta = self:get_scrollable_size() / self.size.y * dy
self.scroll.to.y = self.scroll.to.y + delta
end
self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y)
end
function View:on_text_input(text)
-- no-op
end
function View:on_mouse_wheel(y)
if self.scrollable then
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
end
end
function View:get_content_bounds()
local x = self.scroll.x
local y = self.scroll.y
return x, y, x + self.size.x, y + self.size.y
end
function View:get_content_offset()
local x = common.round(self.position.x - self.scroll.x)
local y = common.round(self.position.y - self.scroll.y)
return x, y
end
function View:clamp_scroll_position()
local max = self:get_scrollable_size() - self.size.y
self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max)
end
function View:update()
self:clamp_scroll_position()
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3)
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3)
end
function View:draw_background(color)
local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y
renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
end
function View:draw_scrollbar()
local x, y, w, h = self:get_scrollbar_rect()
local highlight = self.hovered_scrollbar or self.dragging_scrollbar
local color = highlight and style.scrollbar2 or style.scrollbar
renderer.draw_rect(x, y, w, h, color)
end
function View:draw()
end
return View

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,284 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local command = require "core.command"
local style = require "core.style"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"
local RootView = require "core.rootview"
local DocView = require "core.docview"
config.autocomplete_max_suggestions = 6
local autocomplete = {}
autocomplete.map = {}
local mt = { __tostring = function(t) return t.text end }
function autocomplete.add(t)
local items = {}
for text, info in pairs(t.items) do
info = (type(info) == "string") and info
table.insert(items, setmetatable({ text = text, info = info }, mt))
end
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
end
core.add_thread(function()
local cache = setmetatable({}, { __mode = "k" })
local function get_symbols(doc)
local i = 1
local s = {}
while i < #doc.lines do
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
s[sym] = true
end
i = i + 1
if i % 100 == 0 then coroutine.yield() end
end
return s
end
local function cache_is_valid(doc)
local c = cache[doc]
return c and c.last_change_id == doc:get_change_id()
end
while true do
local symbols = {}
-- lift all symbols from all docs
for _, doc in ipairs(core.docs) do
-- update the cache if the doc has changed since the last iteration
if not cache_is_valid(doc) then
cache[doc] = {
last_change_id = doc:get_change_id(),
symbols = get_symbols(doc)
}
end
-- update symbol set with doc's symbol set
for sym in pairs(cache[doc].symbols) do
symbols[sym] = true
end
coroutine.yield()
end
-- update symbols list
autocomplete.add { name = "open-docs", items = symbols }
-- wait for next scan
local valid = true
while valid do
coroutine.yield(1)
for _, doc in ipairs(core.docs) do
if not cache_is_valid(doc) then
valid = false
end
end
end
end
end)
local partial = ""
local suggestions_idx = 1
local suggestions = {}
local last_line, last_col
local function reset_suggestions()
suggestions_idx = 1
suggestions = {}
end
local function update_suggestions()
local doc = core.active_view.doc
local filename = doc and doc.filename or ""
-- get all relevant suggestions for given filename
local items = {}
for _, v in pairs(autocomplete.map) do
if common.match_pattern(filename, v.files) then
for _, item in pairs(v.items) do
table.insert(items, item)
end
end
end
-- fuzzy match, remove duplicates and store
items = common.fuzzy_match(items, partial)
local j = 1
for i = 1, config.autocomplete_max_suggestions do
suggestions[i] = items[j]
while items[j] and items[i].text == items[j].text do
items[i].info = items[i].info or items[j].info
j = j + 1
end
end
end
local function get_partial_symbol()
local doc = core.active_view.doc
local line2, col2 = doc:get_selection()
local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
return doc:get_text(line1, col1, line2, col2)
end
local function get_active_view()
if getmetatable(core.active_view) == DocView then
return core.active_view
end
end
local function get_suggestions_rect(av)
if #suggestions == 0 then
return 0, 0, 0, 0
end
local line, col = av.doc:get_selection()
local x, y = av:get_line_screen_position(line)
x = x + av:get_col_x_offset(line, col - #partial)
y = y + av:get_line_height() + style.padding.y
local font = av:get_font()
local th = font:get_height()
local max_width = 0
for _, s in ipairs(suggestions) do
local w = font:get_width(s.text)
if s.info then
w = w + style.font:get_width(s.info) + style.padding.x
end
max_width = math.max(max_width, w)
end
return
x - style.padding.x,
y - style.padding.y,
max_width + style.padding.x * 2,
#suggestions * (th + style.padding.y) + style.padding.y
end
local function draw_suggestions_box(av)
-- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av)
renderer.draw_rect(rx, ry, rw, rh, style.background3)
-- draw text
local font = av:get_font()
local lh = font:get_height() + style.padding.y
local y = ry + style.padding.y / 2
for i, s in ipairs(suggestions) do
local color = (i == suggestions_idx) and style.accent or style.text
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
if s.info then
color = (i == suggestions_idx) and style.text or style.dim
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
end
y = y + lh
end
end
-- patch event logic into RootView
local on_text_input = RootView.on_text_input
local update = RootView.update
local draw = RootView.draw
RootView.on_text_input = function(...)
on_text_input(...)
local av = get_active_view()
if av then
-- update partial symbol and suggestions
partial = get_partial_symbol()
if #partial >= 3 then
update_suggestions()
last_line, last_col = av.doc:get_selection()
else
reset_suggestions()
end
-- scroll if rect is out of bounds of view
local _, y, _, h = get_suggestions_rect(av)
local limit = av.position.y + av.size.y
if y + h > limit then
av.scroll.to.y = av.scroll.y + y + h - limit
end
end
end
RootView.update = function(...)
update(...)
local av = get_active_view()
if av then
-- reset suggestions if caret was moved
local line, col = av.doc:get_selection()
if line ~= last_line or col ~= last_col then
reset_suggestions()
end
end
end
RootView.draw = function(...)
draw(...)
local av = get_active_view()
if av then
-- draw suggestions box after everything else
core.root_view:defer_draw(draw_suggestions_box, av)
end
end
local function predicate()
return get_active_view() and #suggestions > 0
end
command.add(predicate, {
["autocomplete:complete"] = function()
local doc = core.active_view.doc
local line, col = doc:get_selection()
local text = suggestions[suggestions_idx].text
doc:insert(line, col, text)
doc:remove(line, col, line, col - #partial)
doc:set_selection(line, col + #text - #partial)
reset_suggestions()
end,
["autocomplete:previous"] = function()
suggestions_idx = math.max(suggestions_idx - 1, 1)
end,
["autocomplete:next"] = function()
suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
end,
["autocomplete:cancel"] = function()
reset_suggestions()
end,
})
keymap.add {
["tab"] = "autocomplete:complete",
["up"] = "autocomplete:previous",
["down"] = "autocomplete:next",
["escape"] = "autocomplete:cancel",
}
return autocomplete

View File

@ -0,0 +1,61 @@
local core = require "core"
local config = require "core.config"
local Doc = require "core.doc"
local times = setmetatable({}, { __mode = "k" })
local function update_time(doc)
local info = system.get_file_info(doc.filename)
times[doc] = info.modified
end
local function reload_doc(doc)
local fp = io.open(doc.filename, "r")
local text = fp:read("*a")
fp:close()
local sel = { doc:get_selection() }
doc:remove(1, 1, math.huge, math.huge)
doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", ""))
doc:set_selection(table.unpack(sel))
update_time(doc)
doc:clean()
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end
core.add_thread(function()
while true do
-- check all doc modified times
for _, doc in ipairs(core.docs) do
local info = system.get_file_info(doc.filename or "")
if info and times[doc] ~= info.modified then
reload_doc(doc)
end
coroutine.yield()
end
-- wait for next scan
coroutine.yield(config.project_scan_rate)
end
end)
-- patch `Doc.save|load` to store modified time
local load = Doc.load
local save = Doc.save
Doc.load = function(self, ...)
local res = load(self, ...)
update_time(self)
return res
end
Doc.save = function(self, ...)
local res = save(self, ...)
update_time(self)
return res
end

View File

@ -0,0 +1,59 @@
local syntax = require "core.syntax"
syntax.add {
files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" },
comment = "//",
patterns = {
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { "#", "[^\\]\n" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "-?0x%x+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" },
{ pattern = "-?%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["if"] = "keyword",
["then"] = "keyword",
["else"] = "keyword",
["elseif"] = "keyword",
["do"] = "keyword",
["while"] = "keyword",
["for"] = "keyword",
["break"] = "keyword",
["continue"] = "keyword",
["return"] = "keyword",
["goto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["typedef"] = "keyword",
["enum"] = "keyword",
["extern"] = "keyword",
["static"] = "keyword",
["volatile"] = "keyword",
["const"] = "keyword",
["inline"] = "keyword",
["switch"] = "keyword",
["case"] = "keyword",
["default"] = "keyword",
["auto"] = "keyword",
["const"] = "keyword",
["void"] = "keyword",
["int"] = "keyword2",
["short"] = "keyword2",
["long"] = "keyword2",
["float"] = "keyword2",
["double"] = "keyword2",
["char"] = "keyword2",
["unsigned"] = "keyword2",
["bool"] = "keyword2",
["true"] = "literal",
["false"] = "literal",
["NULL"] = "literal",
},
}

View File

@ -0,0 +1,23 @@
local syntax = require "core.syntax"
syntax.add {
files = { "%.css$" },
patterns = {
{ pattern = "\\.", type = "normal" },
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "[%a][%w-]*%s*%f[:]", type = "keyword" },
{ pattern = "#%x+", type = "string" },
{ pattern = "-?%d+[%d%.]*p[xt]", type = "number" },
{ pattern = "-?%d+[%d%.]*deg", type = "number" },
{ pattern = "-?%d+[%d%.]*", type = "number" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "#[%a][%w_-]*", type = "keyword2" },
{ pattern = "@[%a][%w_-]*", type = "keyword2" },
{ pattern = "%.[%a][%w_-]*", type = "keyword2" },
{ pattern = "[{}:]", type = "operator" },
},
symbols = {},
}

View File

@ -0,0 +1,67 @@
local syntax = require "core.syntax"
syntax.add {
files = { "%.js$", "%.json$", "%.cson$" },
comment = "//",
patterns = {
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["async"] = "keyword",
["await"] = "keyword",
["break"] = "keyword",
["case"] = "keyword",
["catch"] = "keyword",
["class"] = "keyword",
["const"] = "keyword",
["continue"] = "keyword",
["debugger"] = "keyword",
["default"] = "keyword",
["delete"] = "keyword",
["do"] = "keyword",
["else"] = "keyword",
["export"] = "keyword",
["extends"] = "keyword",
["finally"] = "keyword",
["for"] = "keyword",
["function"] = "keyword",
["get"] = "keyword",
["if"] = "keyword",
["import"] = "keyword",
["in"] = "keyword",
["instanceof"] = "keyword",
["let"] = "keyword",
["new"] = "keyword",
["return"] = "keyword",
["set"] = "keyword",
["static"] = "keyword",
["super"] = "keyword",
["switch"] = "keyword",
["throw"] = "keyword",
["try"] = "keyword",
["typeof"] = "keyword",
["var"] = "keyword",
["void"] = "keyword",
["while"] = "keyword",
["with"] = "keyword",
["yield"] = "keyword",
["true"] = "literal",
["false"] = "literal",
["null"] = "literal",
["undefined"] = "literal",
["arguments"] = "keyword2",
["Infinity"] = "keyword2",
["NaN"] = "keyword2",
["this"] = "keyword2",
},
}

View File

@ -0,0 +1,50 @@
local syntax = require "core.syntax"
syntax.add {
files = "%.lua$",
headers = "^#!.*[ /]lua",
comment = "--",
patterns = {
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "%[%[", "%]%]" }, type = "string" },
{ pattern = { "%-%-%[%[", "%]%]"}, type = "comment" },
{ pattern = "%-%-.-\n", type = "comment" },
{ pattern = "-?0x%x+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "<%a+>", type = "keyword2" },
{ pattern = "%.%.%.?", type = "operator" },
{ pattern = "[<>~=]=", type = "operator" },
{ pattern = "[%+%-=/%*%^%%#<>]", type = "operator" },
{ pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "::[%a_][%w_]*::", type = "function" },
},
symbols = {
["if"] = "keyword",
["then"] = "keyword",
["else"] = "keyword",
["elseif"] = "keyword",
["end"] = "keyword",
["do"] = "keyword",
["function"] = "keyword",
["repeat"] = "keyword",
["until"] = "keyword",
["while"] = "keyword",
["for"] = "keyword",
["break"] = "keyword",
["return"] = "keyword",
["local"] = "keyword",
["in"] = "keyword",
["not"] = "keyword",
["and"] = "keyword",
["or"] = "keyword",
["goto"] = "keyword",
["self"] = "keyword2",
["true"] = "literal",
["false"] = "literal",
["nil"] = "literal",
},
}

View File

@ -0,0 +1,21 @@
local syntax = require "core.syntax"
syntax.add {
files = { "%.md$", "%.markdown$" },
patterns = {
{ pattern = "\\.", type = "normal" },
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "`", "`", "\\" }, type = "string" },
{ pattern = { "~~", "~~", "\\" }, type = "keyword2" },
{ pattern = "%-%-%-+", type = "comment" },
{ pattern = "%*%s+", type = "operator" },
{ pattern = { "%*", "[%*\n]", "\\" }, type = "operator" },
{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" },
{ pattern = "#.-\n", type = "keyword" },
{ pattern = "!?%[.-%]%(.-%)", type = "function" },
{ pattern = "https?://%S+", type = "function" },
},
symbols = { },
}

View File

@ -0,0 +1,55 @@
local syntax = require "core.syntax"
syntax.add {
files = "%.py$",
headers = "^#!.*[ /]python",
comment = "#",
patterns = {
{ pattern = { "#", "\n" }, type = "comment" },
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
{ pattern = { '"""', '"""' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["class"] = "keyword",
["finally"] = "keyword",
["is"] = "keyword",
["return"] = "keyword",
["continue"] = "keyword",
["for"] = "keyword",
["lambda"] = "keyword",
["try"] = "keyword",
["def"] = "keyword",
["from"] = "keyword",
["nonlocal"] = "keyword",
["while"] = "keyword",
["and"] = "keyword",
["global"] = "keyword",
["not"] = "keyword",
["with"] = "keyword",
["as"] = "keyword",
["elif"] = "keyword",
["if"] = "keyword",
["or"] = "keyword",
["else"] = "keyword",
["import"] = "keyword",
["pass"] = "keyword",
["break"] = "keyword",
["except"] = "keyword",
["in"] = "keyword",
["del"] = "keyword",
["raise"] = "keyword",
["yield"] = "keyword",
["assert"] = "keyword",
["self"] = "keyword2",
["None"] = "literal",
["True"] = "literal",
["False"] = "literal",
}
}

View File

@ -0,0 +1,21 @@
local syntax = require "core.syntax"
syntax.add {
files = { "%.xml$", "%.html?$" },
headers = "<%?xml",
patterns = {
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.]*f?", type = "number" },
{ pattern = "-?%.?%d+f?", type = "number" },
{ pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" },
{ pattern = "%f[^<][%a_][%w_]*", type = "function" },
{ pattern = "%f[^<]/[%a_][%w_]*", type = "function" },
{ pattern = "[%a_][%w_]*", type = "keyword" },
{ pattern = "[/<>=]", type = "operator" },
},
symbols = {},
}

View File

@ -0,0 +1,69 @@
local core = require "core"
local command = require "core.command"
local keymap = require "core.keymap"
local handled_events = {
["keypressed"] = true,
["keyreleased"] = true,
["textinput"] = true,
}
local state = "stopped"
local event_buffer = {}
local modkeys = {}
local on_event = core.on_event
core.on_event = function(type, ...)
local res = on_event(type, ...)
if state == "recording" and handled_events[type] then
table.insert(event_buffer, { type, ... })
end
return res
end
local function clone(t)
local res = {}
for k, v in pairs(t) do res[k] = v end
return res
end
local function predicate()
return state ~= "playing"
end
command.add(predicate, {
["macro:toggle-record"] = function()
if state == "stopped" then
state = "recording"
event_buffer = {}
modkeys = clone(keymap.modkeys)
core.log("Recording macro...")
else
state = "stopped"
core.log("Stopped recording macro (%d events)", #event_buffer)
end
end,
["macro:play"] = function()
state = "playing"
core.log("Playing macro... (%d events)", #event_buffer)
local mk = keymap.modkeys
keymap.modkeys = clone(modkeys)
for _, ev in ipairs(event_buffer) do
on_event(table.unpack(ev))
core.root_view:update()
end
keymap.modkeys = mk
state = "stopped"
end,
})
keymap.add {
["ctrl+shift+;"] = "macro:toggle-record",
["ctrl+;"] = "macro:play",
}

View File

@ -0,0 +1,271 @@
local core = require "core"
local common = require "core.common"
local keymap = require "core.keymap"
local command = require "core.command"
local style = require "core.style"
local View = require "core.view"
local ResultsView = View:extend()
function ResultsView:new(text, fn)
ResultsView.super.new(self)
self.scrollable = true
self.brightness = 0
self:begin_search(text, fn)
end
function ResultsView:get_name()
return "Search Results"
end
local function find_all_matches_in_file(t, filename, fn)
local fp = io.open(filename)
if not fp then return t end
local n = 1
for line in fp:lines() do
local s = fn(line)
if s then
table.insert(t, { file = filename, text = line, line = n, col = s })
core.redraw = true
end
if n % 100 == 0 then coroutine.yield() end
n = n + 1
core.redraw = true
end
fp:close()
end
function ResultsView:begin_search(text, fn)
self.search_args = { text, fn }
self.results = {}
self.last_file_idx = 1
self.query = text
self.searching = true
self.selected_idx = 0
core.add_thread(function()
for i, file in ipairs(core.project_files) do
if file.type == "file" then
find_all_matches_in_file(self.results, file.filename, fn)
end
self.last_file_idx = i
end
self.searching = false
self.brightness = 100
core.redraw = true
end, self.results)
self.scroll.to.y = 0
end
function ResultsView:refresh()
self:begin_search(table.unpack(self.search_args))
end
function ResultsView:on_mouse_moved(mx, my, ...)
ResultsView.super.on_mouse_moved(self, mx, my, ...)
self.selected_idx = 0
for i, item, x,y,w,h in self:each_visible_result() do
if mx >= x and my >= y and mx < x + w and my < y + h then
self.selected_idx = i
break
end
end
end
function ResultsView:on_mouse_pressed(...)
local caught = ResultsView.super.on_mouse_pressed(self, ...)
if not caught then
self:open_selected_result()
end
end
function ResultsView:open_selected_result()
local res = self.results[self.selected_idx]
if not res then
return
end
core.try(function()
local dv = core.root_view:open_doc(core.open_doc(res.file))
core.root_view.root_node:update_layout()
dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true)
end)
end
function ResultsView:update()
self:move_towards("brightness", 0, 0.1)
ResultsView.super.update(self)
end
function ResultsView:get_results_yoffset()
return style.font:get_height() + style.padding.y * 3
end
function ResultsView:get_line_height()
return style.padding.y + style.font:get_height()
end
function ResultsView:get_scrollable_size()
return self:get_results_yoffset() + #self.results * self:get_line_height()
end
function ResultsView:get_visible_results_range()
local lh = self:get_line_height()
local oy = self:get_results_yoffset()
local min = math.max(1, math.floor((self.scroll.y - oy) / lh))
return min, min + math.floor(self.size.y / lh) + 1
end
function ResultsView:each_visible_result()
return coroutine.wrap(function()
local lh = self:get_line_height()
local x, y = self:get_content_offset()
local min, max = self:get_visible_results_range()
y = y + self:get_results_yoffset() + lh * (min - 1)
for i = min, max do
local item = self.results[i]
if not item then break end
coroutine.yield(i, item, x, y, self.size.x, lh)
y = y + lh
end
end)
end
function ResultsView:scroll_to_make_selected_visible()
local h = self:get_line_height()
local y = self:get_results_yoffset() + h * (self.selected_idx - 1)
self.scroll.to.y = math.min(self.scroll.to.y, y)
self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y)
end
function ResultsView:draw()
self:draw_background(style.background)
-- status
local ox, oy = self:get_content_offset()
local x, y = ox + style.padding.x, oy + style.padding.y
local per = self.last_file_idx / #core.project_files
local text
if self.searching then
text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
per * 100, self.last_file_idx, #core.project_files,
#self.results, self.query)
else
text = string.format("Found %d matches for %q",
#self.results, self.query)
end
local color = common.lerp(style.text, style.accent, self.brightness / 100)
renderer.draw_text(style.font, text, x, y, color)
-- horizontal line
local yoffset = self:get_results_yoffset()
local x = ox + style.padding.x
local w = self.size.x - style.padding.x * 2
local h = style.divider_size
local color = common.lerp(style.dim, style.text, self.brightness / 100)
renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, color)
if self.searching then
renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text)
end
-- results
local y1, y2 = self.position.y, self.position.y + self.size.y
for i, item, x,y,w,h in self:each_visible_result() do
local color = style.text
if i == self.selected_idx then
color = style.accent
renderer.draw_rect(x, y, w, h, style.line_highlight)
end
x = x + style.padding.x
local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col)
x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h)
x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h)
end
self:draw_scrollbar()
end
local function begin_search(text, fn)
if text == "" then
core.error("Expected non-empty string")
return
end
local rv = ResultsView(text, fn)
core.root_view:get_active_node():add_view(rv)
end
command.add(nil, {
["project-search:find"] = function()
core.command_view:enter("Find Text In Project", function(text)
text = text:lower()
begin_search(text, function(line_text)
return line_text:lower():find(text, nil, true)
end)
end)
end,
["project-search:find-pattern"] = function()
core.command_view:enter("Find Pattern In Project", function(text)
begin_search(text, function(line_text) return line_text:find(text) end)
end)
end,
["project-search:fuzzy-find"] = function()
core.command_view:enter("Fuzzy Find Text In Project", function(text)
begin_search(text, function(line_text)
return common.fuzzy_match(line_text, text) and 1
end)
end)
end,
})
command.add(ResultsView, {
["project-search:select-previous"] = function()
local view = core.active_view
view.selected_idx = math.max(view.selected_idx - 1, 1)
view:scroll_to_make_selected_visible()
end,
["project-search:select-next"] = function()
local view = core.active_view
view.selected_idx = math.min(view.selected_idx + 1, #view.results)
view:scroll_to_make_selected_visible()
end,
["project-search:open-selected"] = function()
core.active_view:open_selected_result()
end,
["project-search:refresh"] = function()
core.active_view:refresh()
end,
})
keymap.add {
["f5"] = "project-search:refresh",
["ctrl+shift+f"] = "project-search:find",
["up"] = "project-search:select-previous",
["down"] = "project-search:select-next",
["return"] = "project-search:open-selected",
}

View File

@ -0,0 +1,30 @@
local core = require "core"
local command = require "core.command"
local keymap = require "core.keymap"
local escapes = {
["\\"] = "\\\\",
["\""] = "\\\"",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
["\b"] = "\\b",
}
local function replace(chr)
return escapes[chr] or string.format("\\x%02x", chr:byte())
end
command.add("core.docview", {
["quote:quote"] = function()
core.active_view.doc:replace(function(text)
return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"'
end)
end,
})
keymap.add {
["ctrl+'"] = "quote:quote",
}

View File

@ -0,0 +1,63 @@
local core = require "core"
local config = require "core.config"
local command = require "core.command"
local keymap = require "core.keymap"
local function wordwrap_text(text, limit)
local t = {}
local n = 0
for word in text:gmatch("%S+") do
if n + #word > limit then
table.insert(t, "\n")
n = 0
elseif #t > 0 then
table.insert(t, " ")
end
table.insert(t, word)
n = n + #word + 1
end
return table.concat(t)
end
command.add("core.docview", {
["reflow:reflow"] = function()
local doc = core.active_view.doc
doc:replace(function(text)
local prefix_set = "[^%w\n%[%](){}`'\"]*"
-- get line prefix and trailing whitespace
local prefix1 = text:match("^\n*" .. prefix_set)
local prefix2 = text:match("\n(" .. prefix_set .. ")", #prefix1+1)
local trailing = text:match("%s*$")
if not prefix2 or prefix2 == "" then
prefix2 = prefix1
end
-- strip all line prefixes and trailing whitespace
text = text:sub(#prefix1+1, -#trailing - 1):gsub("\n" .. prefix_set, "\n")
-- split into blocks, wordwrap and join
local line_limit = config.line_limit - #prefix1
local blocks = {}
text = text:gsub("\n\n", "\0")
for block in text:gmatch("%Z+") do
table.insert(blocks, wordwrap_text(block, line_limit))
end
text = table.concat(blocks, "\n\n")
-- add prefix to start of lines
text = prefix1 .. text:gsub("\n", "\n" .. prefix2) .. trailing
return text
end)
end,
})
keymap.add {
["ctrl+shift+q"] = "reflow:reflow"
}

View File

@ -0,0 +1,60 @@
local core = require "core"
local command = require "core.command"
local translate = require "core.doc.translate"
local function gmatch_to_array(text, ptn)
local res = {}
for x in text:gmatch(ptn) do
table.insert(res, x)
end
return res
end
local function tabularize_lines(lines, delim)
local rows = {}
local cols = {}
-- split lines at delimiters and get maximum width of columns
local ptn = "[^" .. delim:sub(1,1):gsub("%W", "%%%1") .. "]+"
for i, line in ipairs(lines) do
rows[i] = gmatch_to_array(line, ptn)
for j, col in ipairs(rows[i]) do
cols[j] = math.max(#col, cols[j] or 0)
end
end
-- pad columns with space
for _, row in ipairs(rows) do
for i = 1, #row - 1 do
row[i] = row[i] .. string.rep(" ", cols[i] - #row[i])
end
end
-- write columns back to lines array
for i, line in ipairs(lines) do
lines[i] = table.concat(rows[i], delim)
end
end
command.add("core.docview", {
["tabularize:tabularize"] = function()
core.command_view:enter("Tabularize On Delimiter", function(delim)
if delim == "" then delim = " " end
local doc = core.active_view.doc
local line1, col1, line2, col2, swap = doc:get_selection(true)
line1, col1 = doc:position_offset(line1, col1, translate.start_of_line)
line2, col2 = doc:position_offset(line2, col2, translate.end_of_line)
doc:set_selection(line1, col1, line2, col2, swap)
doc:replace(function(text)
local lines = gmatch_to_array(text, "[^\n]*\n?")
tabularize_lines(lines, delim)
return table.concat(lines)
end)
end)
end,
})

View File

@ -0,0 +1,197 @@
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local View = require "core.view"
config.treeview_size = 200 * SCALE
local function get_depth(filename)
local n = 0
for sep in filename:gmatch("[\\/]") do
n = n + 1
end
return n
end
local TreeView = View:extend()
function TreeView:new()
TreeView.super.new(self)
self.scrollable = true
self.visible = true
self.init_size = true
self.cache = {}
end
function TreeView:get_cached(item)
local t = self.cache[item.filename]
if not t then
t = {}
t.filename = item.filename
t.abs_filename = system.absolute_path(item.filename)
t.name = t.filename:match("[^\\/]+$")
t.depth = get_depth(t.filename)
t.type = item.type
self.cache[t.filename] = t
end
return t
end
function TreeView:get_name()
return "Project"
end
function TreeView:get_item_height()
return style.font:get_height() + style.padding.y
end
function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed
if core.project_files ~= self.last_project_files then
for _, v in pairs(self.cache) do
v.skip = nil
end
self.last_project_files = core.project_files
end
end
function TreeView:each_item()
return coroutine.wrap(function()
self:check_cache()
local ox, oy = self:get_content_offset()
local y = oy + style.padding.y
local w = self.size.x
local h = self:get_item_height()
local i = 1
while i <= #core.project_files do
local item = core.project_files[i]
local cached = self:get_cached(item)
coroutine.yield(cached, ox, y, w, h)
y = y + h
i = i + 1
if not cached.expanded then
if cached.skip then
i = cached.skip
else
local depth = cached.depth
while i <= #core.project_files do
local filename = core.project_files[i].filename
if get_depth(filename) <= depth then break end
i = i + 1
end
cached.skip = i
end
end
end
end)
end
function TreeView:on_mouse_moved(px, py)
self.hovered_item = nil
for item, x,y,w,h in self:each_item() do
if px > x and py > y and px <= x + w and py <= y + h then
self.hovered_item = item
break
end
end
end
function TreeView:on_mouse_pressed(button, x, y)
if not self.hovered_item then
return
elseif self.hovered_item.type == "dir" then
self.hovered_item.expanded = not self.hovered_item.expanded
else
core.try(function()
core.root_view:open_doc(core.open_doc(self.hovered_item.filename))
end)
end
end
function TreeView:update()
-- update width
local dest = self.visible and config.treeview_size or 0
if self.init_size then
self.size.x = dest
self.init_size = false
else
self:move_towards(self.size, "x", dest)
end
TreeView.super.update(self)
end
function TreeView:draw()
self:draw_background(style.background2)
local icon_width = style.icon_font:get_width("D")
local spacing = style.font:get_width(" ") * 2
local doc = core.active_view.doc
local active_filename = doc and system.absolute_path(doc.filename or "")
for item, x,y,w,h in self:each_item() do
local color = style.text
-- highlight active_view doc
if item.abs_filename == active_filename then
color = style.accent
end
-- hovered item background
if item == self.hovered_item then
renderer.draw_rect(x, y, w, h, style.line_highlight)
color = style.accent
end
-- icons
x = x + item.depth * style.padding.x + style.padding.x
if item.type == "dir" then
local icon1 = item.expanded and "-" or "+"
local icon2 = item.expanded and "D" or "d"
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
x = x + style.padding.x
common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h)
x = x + icon_width
else
x = x + style.padding.x
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
x = x + icon_width
end
-- text
x = x + spacing
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
end
end
-- init
local view = TreeView()
local node = core.root_view:get_active_node()
node:split("left", view, true)
-- register commands and keymap
command.add(nil, {
["treeview:toggle"] = function()
view.visible = not view.visible
end,
})
keymap.add { ["ctrl+\\"] = "treeview:toggle" }

View File

@ -0,0 +1,36 @@
local core = require "core"
local command = require "core.command"
local Doc = require "core.doc"
local function trim_trailing_whitespace(doc)
local cline, ccol = doc:get_selection()
for i = 1, #doc.lines do
local old_text = doc:get_text(i, 1, i, math.huge)
local new_text = old_text:gsub("%s*$", "")
-- don't remove whitespace which would cause the caret to reposition
if cline == i and ccol > #new_text then
new_text = old_text:sub(1, ccol - 1)
end
if old_text ~= new_text then
doc:insert(i, 1, new_text)
doc:remove(i, #new_text + 1, i, math.huge)
end
end
end
command.add("core.docview", {
["trim-whitespace:trim-trailing-whitespace"] = function()
trim_trailing_whitespace(core.active_view.doc)
end,
})
local save = Doc.save
Doc.save = function(self, ...)
trim_trailing_whitespace(self)
save(self, ...)
end

View File

@ -0,0 +1,28 @@
local style = require "core.style"
local common = require "core.common"
style.background = { common.color "#343233" }
style.background2 = { common.color "#2c2a2b" }
style.background3 = { common.color "#2c2a2b" }
style.text = { common.color "#c4b398" }
style.caret = { common.color "#61efce" }
style.accent = { common.color "#ffd152" }
style.dim = { common.color "#615d5f" }
style.divider = { common.color "#242223" }
style.selection = { common.color "#454244" }
style.line_number = { common.color "#454244" }
style.line_number2 = { common.color "#615d5f" }
style.line_highlight = { common.color "#383637" }
style.scrollbar = { common.color "#454344" }
style.scrollbar2 = { common.color "#524F50" }
style.syntax["normal"] = { common.color "#efdab9" }
style.syntax["symbol"] = { common.color "#efdab9" }
style.syntax["comment"] = { common.color "#615d5f" }
style.syntax["keyword"] = { common.color "#d36e2d" }
style.syntax["keyword2"] = { common.color "#ef6179" }
style.syntax["number"] = { common.color "#ffd152" }
style.syntax["literal"] = { common.color "#ffd152" }
style.syntax["string"] = { common.color "#ffd152" }
style.syntax["operator"] = { common.color "#efdab9" }
style.syntax["function"] = { common.color "#61efce" }

View File

@ -0,0 +1,28 @@
local style = require "core.style"
local common = require "core.common"
style.background = { common.color "#fbfbfb" }
style.background2 = { common.color "#f2f2f2" }
style.background3 = { common.color "#f2f2f2" }
style.text = { common.color "#404040" }
style.caret = { common.color "#fc1785" }
style.accent = { common.color "#fc1785" }
style.dim = { common.color "#b0b0b0" }
style.divider = { common.color "#e8e8e8" }
style.selection = { common.color "#b7dce8" }
style.line_number = { common.color "#d0d0d0" }
style.line_number2 = { common.color "#808080" }
style.line_highlight = { common.color "#f2f2f2" }
style.scrollbar = { common.color "#e0e0e0" }
style.scrollbar2 = { common.color "#c0c0c0" }
style.syntax["normal"] = { common.color "#181818" }
style.syntax["symbol"] = { common.color "#181818" }
style.syntax["comment"] = { common.color "#22a21f" }
style.syntax["keyword"] = { common.color "#fb6620" }
style.syntax["keyword2"] = { common.color "#fc1785" }
style.syntax["number"] = { common.color "#1586d2" }
style.syntax["literal"] = { common.color "#1586d2" }
style.syntax["string"] = { common.color "#1586d2" }
style.syntax["operator"] = { common.color "#fb6620" }
style.syntax["function"] = { common.color "#fc1785" }

View File

@ -0,0 +1,13 @@
-- put user settings here
-- this module will be loaded after everything else when the application starts
local keymap = require "core.keymap"
local config = require "core.config"
local style = require "core.style"
-- light theme:
-- require "user.colors.summer"
-- key binding:
-- keymap.add { ["ctrl+escape"] = "core:quit" }

BIN
tools/lite/lite Normal file

Binary file not shown.

BIN
tools/lite/lite.exe Normal file

Binary file not shown.