add many tools
This commit is contained in:
parent
faf4a2cb11
commit
d58efd789f
|
@ -0,0 +1 @@
|
||||||
|
.cache
|
|
@ -0,0 +1,2 @@
|
||||||
|
@echo off
|
||||||
|
call %VID_HOME%\tools\7z.bat %*
|
|
@ -0,0 +1,2 @@
|
||||||
|
@echo off
|
||||||
|
call %VID_HOME%\tools\cmakew.bat %*
|
|
@ -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%
|
|
@ -0,0 +1,2 @@
|
||||||
|
@echo off
|
||||||
|
call %VID_HOME%\tools\6.0g-mingw64.bat g++ %*
|
|
@ -0,0 +1,2 @@
|
||||||
|
@echo off
|
||||||
|
call %VID_HOME%\tools\6.0g-mingw64.bat gcc %*
|
|
@ -0,0 +1,2 @@
|
||||||
|
@echo off
|
||||||
|
call %VID_HOME%\tools\6.0g-mingw64.bat mingw32-make %*
|
|
@ -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
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
set VID_HOME=%~dp0
|
||||||
|
set PATH=%~dp0\bin;%PATH%
|
||||||
|
cmd /k
|
Binary file not shown.
|
@ -6,7 +6,7 @@ setlocal enableDelayedExpansion
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
@rem get script directory name
|
@rem get script directory name
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0\..\.cache
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
|
||||||
@rem strip trailing backslash from DIRNAME path to make it easier to work with
|
@rem strip trailing backslash from DIRNAME path to make it easier to work with
|
||||||
|
|
|
@ -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
|
|
@ -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
|
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
|
@rem get script directory name
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0\..\.cache
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
|
||||||
@rem strip trailing backslash from DIRNAME path to make it easier to work with
|
@rem strip trailing backslash from DIRNAME path to make it easier to work with
|
||||||
IF %DIRNAME:~-1%==\ SET DIRNAME=%DIRNAME:~0,-1%
|
IF %DIRNAME:~-1%==\ SET DIRNAME=%DIRNAME:~0,-1%
|
||||||
|
|
||||||
@rem get OS bitness
|
@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
|
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
|
@rem require 7zip to unzip the cmake dist because windows sucks
|
||||||
if not exist %UNZIP_EXE% (
|
if not exist %UNZIP_EXE% (
|
||||||
|
@ -36,11 +35,11 @@ if not exist %UNZIP_EXE% (
|
||||||
echo Installing 7zip
|
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
|
@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
|
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_ZIP=%TEMP%\cmake-%CMAKE_VERSION%.zip
|
||||||
set CMAKE_EXE=%CMAKE_DIR%\bin\cmake.exe
|
set CMAKE_EXE=%CMAKE_DIR%\bin\cmake.exe
|
||||||
|
|
||||||
|
@ -56,13 +55,13 @@ if not exist %CMAKE_EXE% (
|
||||||
|
|
||||||
echo Installing cmake
|
echo Installing cmake
|
||||||
|
|
||||||
%UNZIP_EXE% x %CMAKE_ZIP% -o%DIRNAME%\.cmakew -y
|
%UNZIP_EXE% x %CMAKE_ZIP% -o%DIRNAME% -y
|
||||||
del %CMAKE_ZIP%
|
del %CMAKE_ZIP%
|
||||||
|
|
||||||
if %OS_BITNESS%==64BIT (
|
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 (
|
) else (
|
||||||
ren %DIRNAME%\.cmakew\cmake-%CMAKE_VERSION%-win32-x86 cmake-%CMAKE_VERSION%
|
ren %DIRNAME%\cmake-%CMAKE_VERSION%-win32-x86 cmake-%CMAKE_VERSION%
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -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
|
|
@ -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,
|
||||||
|
})
|
|
@ -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,
|
||||||
|
})
|
|
@ -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)
|
|
@ -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,
|
||||||
|
})
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
@ -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
|
|
@ -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
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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 = {},
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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 = { },
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = {},
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
|
@ -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" }
|
|
@ -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
|
|
@ -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" }
|
|
@ -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" }
|
|
@ -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" }
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue