Simplifies and uniformize the logic on the Lua side for the setting of directories' watches. Now we always use the methods: systems.watch_dir_add / rm on all the project's directories at any depth when we are not in files limit mode. In files limited mode the functions systems.watch_dir_add/rm are called only on the expanded folders. The shown_subdir table is also updated only in files limited mode. On the C side, using the dmon library, we remove the recursive argument from the system.watch_dir and we always call it recursively except on Linux. At the same time the functions: systems.watch_dir_add / rm are provided but as dummy functions that does nothing except on Linux where they work as before to add / remove sub-directories in the inotify watch. In this was on the Lua side we always act we if the watches needed to be set for each sub-directory explicitly, independently of the system. The important improvement introduced is that we always avoid calling dmon_watch recursively on Linux. This latter thing is problematic with inotify and is therefore avoided on Linux. On the other side we simplifies the logic on the Lua side and remove conditions based on the OS used.
1506 lines
45 KiB
Lua
1506 lines
45 KiB
Lua
require "core.strict"
|
|
require "core.regex"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local style = require "core.style"
|
|
local command
|
|
local keymap
|
|
local RootView
|
|
local StatusView
|
|
local TitleView
|
|
local CommandView
|
|
local NagView
|
|
local DocView
|
|
local Doc
|
|
|
|
local core = {}
|
|
|
|
local function load_session()
|
|
local ok, t = pcall(dofile, USERDIR .. "/session.lua")
|
|
return ok and t or {}
|
|
end
|
|
|
|
|
|
local function save_session()
|
|
local fp = io.open(USERDIR .. "/session.lua", "w")
|
|
if fp then
|
|
fp:write("return {recents=", common.serialize(core.recent_projects),
|
|
", window=", common.serialize(table.pack(system.get_window_size())),
|
|
", window_mode=", common.serialize(system.get_window_mode()),
|
|
", previous_find=", common.serialize(core.previous_find),
|
|
", previous_replace=", common.serialize(core.previous_replace),
|
|
"}\n")
|
|
fp:close()
|
|
end
|
|
end
|
|
|
|
|
|
local function update_recents_project(action, dir_path_abs)
|
|
local dirname = common.normalize_volume(dir_path_abs)
|
|
if not dirname then return end
|
|
local recents = core.recent_projects
|
|
local n = #recents
|
|
for i = 1, n do
|
|
if dirname == recents[i] then
|
|
table.remove(recents, i)
|
|
break
|
|
end
|
|
end
|
|
if action == "add" then
|
|
table.insert(recents, 1, dirname)
|
|
end
|
|
end
|
|
|
|
|
|
function core.set_project_dir(new_dir, change_project_fn, defer_add)
|
|
local chdir_ok = pcall(system.chdir, new_dir)
|
|
if chdir_ok then
|
|
if change_project_fn then change_project_fn() end
|
|
core.project_dir = common.normalize_volume(new_dir)
|
|
core.project_directories = {}
|
|
if not defer_add then
|
|
core.add_project_directory(new_dir)
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function core.close_current_project()
|
|
-- When using system.unwatch_dir we need to pass the watch_id provided by dmon.
|
|
-- In reality when unwatching a directory the dmon library shifts the other watch_id
|
|
-- values so the actual watch_id changes. To workaround this problem we assume the
|
|
-- first watch_id is always 1 and the watch_id are continguous and we unwatch the
|
|
-- first watch_id repeateadly up to the number of watch_ids.
|
|
local watch_id_max = 0
|
|
for _, project_dir in ipairs(core.project_directories) do
|
|
if project_dir.watch_id and project_dir.watch_id > watch_id_max then
|
|
watch_id_max = project_dir.watch_id
|
|
end
|
|
end
|
|
for i = 1, watch_id_max do
|
|
system.unwatch_dir(1)
|
|
end
|
|
end
|
|
|
|
function core.open_folder_project(dir_path_abs)
|
|
if core.set_project_dir(dir_path_abs, core.on_quit_project) then
|
|
core.root_view:close_all_docviews()
|
|
update_recents_project("add", dir_path_abs)
|
|
if not core.load_project_module() then
|
|
command.perform("core:open-log")
|
|
end
|
|
core.on_enter_project(dir_path_abs)
|
|
end
|
|
end
|
|
|
|
|
|
local function strip_leading_path(filename)
|
|
return filename:sub(2)
|
|
end
|
|
|
|
local function strip_trailing_slash(filename)
|
|
if filename:match("[^:][/\\]$") then
|
|
return filename:sub(1, -2)
|
|
end
|
|
return filename
|
|
end
|
|
|
|
local function compare_file(a, b)
|
|
return a.filename < b.filename
|
|
end
|
|
|
|
|
|
-- compute a file's info entry completed with "filename" to be used
|
|
-- in project scan or falsy if it shouldn't appear in the list.
|
|
local function get_project_file_info(root, file)
|
|
local info = system.get_file_info(root .. file)
|
|
if info then
|
|
info.filename = strip_leading_path(file)
|
|
return (info.size < config.file_size_limit * 1e6 and
|
|
not common.match_pattern(info.filename, config.ignore_files)
|
|
and info)
|
|
end
|
|
end
|
|
|
|
|
|
-- Predicate function to inhibit directory recursion in get_directory_files
|
|
-- based on a time limit and the number of files.
|
|
local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
|
|
local n_limit = entries_count <= config.max_project_files
|
|
local t_limit = t_elapsed < 20 / config.fps
|
|
return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
|
|
end
|
|
|
|
|
|
-- "root" will by an absolute path without trailing '/'
|
|
-- "path" will be a path starting with '/' and without trailing '/'
|
|
-- or the empty string.
|
|
-- It will identifies a sub-path within "root.
|
|
-- The current path location will therefore always be: root .. path.
|
|
-- When recursing "root" will always be the same, only "path" will change.
|
|
-- Returns a list of file "items". In eash item the "filename" will be the
|
|
-- complete file path relative to "root" *without* the trailing '/'.
|
|
local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook)
|
|
if begin_hook then begin_hook() end
|
|
local t0 = system.get_time()
|
|
local all = system.list_dir(root .. path) or {}
|
|
local t_elapsed = system.get_time() - t0
|
|
local dirs, files = {}, {}
|
|
|
|
for _, file in ipairs(all) do
|
|
local info = get_project_file_info(root, path .. PATHSEP .. file)
|
|
if info then
|
|
table.insert(info.type == "dir" and dirs or files, info)
|
|
entries_count = entries_count + 1
|
|
end
|
|
end
|
|
|
|
local recurse_complete = true
|
|
table.sort(dirs, compare_file)
|
|
for _, f in ipairs(dirs) do
|
|
table.insert(t, f)
|
|
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
|
|
local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook)
|
|
recurse_complete = recurse_complete and complete
|
|
entries_count = n
|
|
else
|
|
recurse_complete = false
|
|
end
|
|
end
|
|
|
|
table.sort(files, compare_file)
|
|
for _, f in ipairs(files) do
|
|
table.insert(t, f)
|
|
end
|
|
|
|
return t, recurse_complete, entries_count
|
|
end
|
|
|
|
|
|
function core.project_subdir_set_show(dir, filename, show)
|
|
dir.shown_subdir[filename] = show
|
|
if dir.files_limit and not dir.force_rescan then
|
|
local fullpath = dir.name .. PATHSEP .. filename
|
|
local success = (show and system.watch_dir_add or system.watch_dir_rm)(dir.watch_id, fullpath)
|
|
if not success then
|
|
core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function core.project_subdir_is_shown(dir, filename)
|
|
return not dir.files_limit or dir.shown_subdir[filename]
|
|
end
|
|
|
|
|
|
local function show_max_files_warning(dir)
|
|
local message = dir.slow_filesystem and
|
|
"Filesystem is too slow: project files will not be indexed." or
|
|
"Too many files in project directory: stopped reading at "..
|
|
config.max_project_files.." files. For more information see "..
|
|
"usage.md at github.com/franko/lite-xl."
|
|
core.status_view:show_message("!", style.accent, message)
|
|
end
|
|
|
|
|
|
local function file_search(files, info)
|
|
local filename, type = info.filename, info.type
|
|
local inf, sup = 1, #files
|
|
while sup - inf > 8 do
|
|
local curr = math.floor((inf + sup) / 2)
|
|
if system.path_compare(filename, type, files[curr].filename, files[curr].type) then
|
|
sup = curr - 1
|
|
else
|
|
inf = curr
|
|
end
|
|
end
|
|
while inf <= sup and not system.path_compare(filename, type, files[inf].filename, files[inf].type) do
|
|
if files[inf].filename == filename then
|
|
return inf, true
|
|
end
|
|
inf = inf + 1
|
|
end
|
|
return inf, false
|
|
end
|
|
|
|
|
|
local function project_scan_add_entry(dir, fileinfo)
|
|
assert(not dir.force_rescan, "should be used on when force_rescan is unset")
|
|
local index, match = file_search(dir.files, fileinfo)
|
|
if not match then
|
|
table.insert(dir.files, index, fileinfo)
|
|
if fileinfo.type == "dir" and not dir.files_limit then
|
|
-- ASSUMPTION: dir.force_rescan is FALSE
|
|
system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename)
|
|
end
|
|
dir.is_dirty = true
|
|
end
|
|
end
|
|
|
|
|
|
local function files_info_equal(a, b)
|
|
return a.filename == b.filename and a.type == b.type
|
|
end
|
|
|
|
-- for "a" inclusive from i1 + 1 and i1 + n
|
|
local function files_list_match(a, i1, n, b)
|
|
if n ~= #b then return false end
|
|
for i = 1, n do
|
|
if not files_info_equal(a[i1 + i], b[i]) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- arguments like for files_list_match
|
|
local function files_list_replace(as, i1, n, bs, insert_hook)
|
|
local m = #bs
|
|
local i, j = 1, 1
|
|
while i <= m or i <= n do
|
|
local a, b = as[i1 + i], bs[j]
|
|
if i > n or (j <= m and not files_info_equal(a, b) and
|
|
not system.path_compare(a.filename, a.type, b.filename, b.type))
|
|
then
|
|
table.insert(as, i1 + i, b)
|
|
i, j, n = i + 1, j + 1, n + 1
|
|
if insert_hook then insert_hook(b) end
|
|
elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then
|
|
table.remove(as, i1 + i)
|
|
n = n - 1
|
|
else
|
|
i, j = i + 1, j + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local function project_subdir_bounds(dir, filename)
|
|
local index, n = 0, #dir.files
|
|
for i, file in ipairs(dir.files) do
|
|
local file = dir.files[i]
|
|
if file.filename == filename then
|
|
index, n = i, #dir.files - i
|
|
for j = 1, #dir.files - i do
|
|
if not common.path_belongs_to(dir.files[i + j].filename, filename) then
|
|
n = j - 1
|
|
break
|
|
end
|
|
end
|
|
return index, n, file
|
|
end
|
|
end
|
|
end
|
|
|
|
local function rescan_project_subdir(dir, filename_rooted)
|
|
local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield)
|
|
local index, n = 0, #dir.files
|
|
if filename_rooted ~= "" then
|
|
local filename = strip_leading_path(filename_rooted)
|
|
index, n = project_subdir_bounds(dir, filename)
|
|
end
|
|
|
|
if not files_list_match(dir.files, index, n, new_files) then
|
|
-- Since we are modifying the list of files we may add new directories and
|
|
-- when dir.files_limit is false we need to add a watch for each subdirectory.
|
|
-- We are therefore passing a insert hook function to the purpose of adding
|
|
-- a watch.
|
|
-- Note that the hook shold almost never be called, it happens only if
|
|
-- we missed some directory creation event from the directory monitoring which
|
|
-- almost never happens. With inotify is at least theoretically possible.
|
|
local need_subdir_watches = not dir.files_limit and not dir.force_rescan
|
|
files_list_replace(dir.files, index, n, new_files, need_subdir_watches and function(fileinfo)
|
|
if fileinfo.type == "dir" then
|
|
system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename)
|
|
end
|
|
end)
|
|
dir.is_dirty = true
|
|
return true
|
|
end
|
|
end
|
|
|
|
|
|
local function add_dir_scan_thread(dir)
|
|
core.add_thread(function()
|
|
while true do
|
|
local has_changes = rescan_project_subdir(dir, "")
|
|
if has_changes then
|
|
core.redraw = true -- we run without an event, from a thread
|
|
end
|
|
coroutine.yield(5)
|
|
end
|
|
end)
|
|
end
|
|
|
|
|
|
local function folder_add_subdirs_watch(dir)
|
|
for _, fileinfo in ipairs(dir.files) do
|
|
if fileinfo.type == "dir" then
|
|
system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Populate a project folder top directory by scanning the filesystem.
|
|
local function scan_project_folder(index)
|
|
local dir = core.project_directories[index]
|
|
local fstype = system.get_fs_type(dir.name)
|
|
dir.force_rescan = (fstype == "nfs" or fstype == "fuse")
|
|
if not dir.force_rescan then
|
|
dir.watch_id = system.watch_dir(dir.name)
|
|
end
|
|
local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred)
|
|
-- If dir.files_limit is set to TRUE it means that:
|
|
-- * we will not read recursively all the project files and we don't index them
|
|
-- * we read only the files for the subdirectories that are opened/expanded in the
|
|
-- TreeView
|
|
-- * we add a subdirectory watch only to the directories that are opened/expanded
|
|
-- * we set the values in the shown_subdir table
|
|
--
|
|
-- If dir.files_limit is set to FALSE it means that:
|
|
-- * we will read recursively all the project files and we index them
|
|
-- * all the list of project files is always complete and kept updated when
|
|
-- changes happen on the disk
|
|
-- * all the subdirectories at any depth must have a watch using system.watch_dir_add
|
|
-- * we DO NOT set the values in the shown_subdir table
|
|
--
|
|
-- * If force_rescan is set to TRUE no watch are used in any case.
|
|
if not complete then
|
|
dir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
|
|
dir.files_limit = true
|
|
if core.status_view then -- May be not yet initialized.
|
|
show_max_files_warning(dir)
|
|
end
|
|
end
|
|
dir.files = t
|
|
if dir.force_rescan then
|
|
add_dir_scan_thread(dir)
|
|
else
|
|
if not dir.files_limit then
|
|
folder_add_subdirs_watch(dir)
|
|
end
|
|
core.dir_rescan_add_job(dir, ".")
|
|
end
|
|
end
|
|
|
|
|
|
function core.add_project_directory(path)
|
|
-- top directories has a file-like "item" but the item.filename
|
|
-- will be simply the name of the directory, without its path.
|
|
-- The field item.topdir will identify it as a top level directory.
|
|
path = common.normalize_volume(path)
|
|
local dir = {
|
|
name = path,
|
|
item = {filename = common.basename(path), type = "dir", topdir = true},
|
|
files_limit = false,
|
|
is_dirty = true,
|
|
shown_subdir = {},
|
|
}
|
|
table.insert(core.project_directories, dir)
|
|
scan_project_folder(#core.project_directories)
|
|
if path == core.project_dir then
|
|
core.project_files = dir.files
|
|
end
|
|
core.redraw = true
|
|
return dir
|
|
end
|
|
|
|
|
|
-- The function below is needed to reload the project directories
|
|
-- when the project's module changes.
|
|
local function rescan_project_directories()
|
|
local save_project_dirs = {}
|
|
local n = #core.project_directories
|
|
for i = 1, n do
|
|
local dir = core.project_directories[i]
|
|
save_project_dirs[i] = {name = dir.name, shown_subdir = dir.shown_subdir}
|
|
end
|
|
core.close_current_project() -- ensure we unwatch directories
|
|
core.project_directories = {}
|
|
for i = 1, n do -- add again the directories in the project
|
|
local dir = core.add_project_directory(save_project_dirs[i].name)
|
|
if dir.files_limit then
|
|
for subdir, show in pairs(save_project_dirs[i].shown_subdir) do
|
|
for j = 1, #dir.files do
|
|
if dir.files[j].filename == subdir then
|
|
-- The instructions below match when happens in TreeView:on_mouse_pressed.
|
|
-- We perform the operations only once iff the subdir is in dir.files.
|
|
core.update_project_subdir(dir, subdir, show)
|
|
core.project_subdir_set_show(dir, subdir, show)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function core.project_dir_by_name(name)
|
|
for i = 1, #core.project_directories do
|
|
if core.project_directories[i].name == name then
|
|
return core.project_directories[i]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function core.update_project_subdir(dir, filename, expanded)
|
|
assert(dir.files_limit, "function should be called only when directory is in files limit mode")
|
|
local index, n, file = project_subdir_bounds(dir, filename)
|
|
if index then
|
|
local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
|
|
-- ASSUMPTION: core.update_project_subdir is called only when dir.files_limit is true
|
|
-- NOTE: we may add new directories below but we do not need to call
|
|
-- system.watch_dir_add because the current function is called only
|
|
-- in dir.files_limit mode and in this latter case we don't need to
|
|
-- add watch to new, unfolded, subdirectories.
|
|
files_list_replace(dir.files, index, n, new_files)
|
|
dir.is_dirty = true
|
|
return true
|
|
end
|
|
end
|
|
|
|
|
|
-- Find files and directories recursively reading from the filesystem.
|
|
-- Filter files and yields file's directory and info table. This latter
|
|
-- is filled to be like required by project directories "files" list.
|
|
local function find_files_rec(root, path)
|
|
local all = system.list_dir(root .. path) or {}
|
|
for _, file in ipairs(all) do
|
|
local file = path .. PATHSEP .. file
|
|
local info = system.get_file_info(root .. file)
|
|
if info then
|
|
info.filename = strip_leading_path(file)
|
|
if info.type == "file" then
|
|
coroutine.yield(root, info)
|
|
else
|
|
find_files_rec(root, PATHSEP .. info.filename)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Iterator function to list all project files
|
|
local function project_files_iter(state)
|
|
local dir = core.project_directories[state.dir_index]
|
|
if state.co then
|
|
-- We have a coroutine to fetch for files, use the coroutine.
|
|
-- Used for directories that exceeds the files nuumber limit.
|
|
local ok, name, file = coroutine.resume(state.co, dir.name, "")
|
|
if ok and name then
|
|
return name, file
|
|
else
|
|
-- The coroutine terminated, increment file/dir counter to scan
|
|
-- next project directory.
|
|
state.co = false
|
|
state.file_index = 1
|
|
state.dir_index = state.dir_index + 1
|
|
dir = core.project_directories[state.dir_index]
|
|
end
|
|
else
|
|
-- Increase file/dir counter
|
|
state.file_index = state.file_index + 1
|
|
while dir and state.file_index > #dir.files do
|
|
state.dir_index = state.dir_index + 1
|
|
state.file_index = 1
|
|
dir = core.project_directories[state.dir_index]
|
|
end
|
|
end
|
|
if not dir then return end
|
|
if dir.files_limit then
|
|
-- The current project directory is files limited: create a couroutine
|
|
-- to read files from the filesystem.
|
|
state.co = coroutine.create(find_files_rec)
|
|
return project_files_iter(state)
|
|
end
|
|
return dir.name, dir.files[state.file_index]
|
|
end
|
|
|
|
|
|
function core.get_project_files()
|
|
local state = { dir_index = 1, file_index = 0 }
|
|
return project_files_iter, state
|
|
end
|
|
|
|
|
|
function core.project_files_number()
|
|
local n = 0
|
|
for i = 1, #core.project_directories do
|
|
if core.project_directories[i].files_limit then return end
|
|
n = n + #core.project_directories[i].files
|
|
end
|
|
return n
|
|
end
|
|
|
|
|
|
local function project_dir_by_watch_id(watch_id)
|
|
for i = 1, #core.project_directories do
|
|
if core.project_directories[i].watch_id == watch_id then
|
|
return core.project_directories[i]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function project_scan_remove_file(dir, filepath)
|
|
local fileinfo = { filename = filepath }
|
|
for _, filetype in ipairs {"dir", "file"} do
|
|
fileinfo.type = filetype
|
|
local index, match = file_search(dir.files, fileinfo)
|
|
if match then
|
|
table.remove(dir.files, index)
|
|
if dir.files_limit and filetype == "dir" then
|
|
dir.shown_subdir[filepath] = nil
|
|
end
|
|
dir.is_dirty = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function project_scan_add_file(dir, filepath)
|
|
for fragment in string.gmatch(filepath, "([^/\\]+)") do
|
|
if common.match_pattern(fragment, config.ignore_files) then
|
|
return
|
|
end
|
|
end
|
|
local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath)
|
|
if fileinfo then
|
|
project_scan_add_entry(dir, fileinfo)
|
|
end
|
|
end
|
|
|
|
|
|
-- create a directory using mkdir but may need to create the parent
|
|
-- directories as well.
|
|
local function create_user_directory()
|
|
local success, err = common.mkdirp(USERDIR)
|
|
if not success then
|
|
error("cannot create directory \"" .. USERDIR .. "\": " .. err)
|
|
end
|
|
for _, modname in ipairs {'plugins', 'colors', 'fonts'} do
|
|
local subdirname = USERDIR .. PATHSEP .. modname
|
|
if not system.mkdir(subdirname) then
|
|
error("cannot create directory: \"" .. subdirname .. "\"")
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function write_user_init_file(init_filename)
|
|
local init_file = io.open(init_filename, "w")
|
|
if not init_file then error("cannot create file: \"" .. init_filename .. "\"") end
|
|
init_file:write([[
|
|
-- put user settings here
|
|
-- this module will be loaded after everything else when the application starts
|
|
-- it will be automatically reloaded when saved
|
|
|
|
local core = require "core"
|
|
local keymap = require "core.keymap"
|
|
local config = require "core.config"
|
|
local style = require "core.style"
|
|
|
|
------------------------------ Themes ----------------------------------------
|
|
|
|
-- light theme:
|
|
-- core.reload_module("colors.summer")
|
|
|
|
--------------------------- Key bindings -------------------------------------
|
|
|
|
-- key binding:
|
|
-- keymap.add { ["ctrl+escape"] = "core:quit" }
|
|
|
|
|
|
------------------------------- Fonts ----------------------------------------
|
|
|
|
-- customize fonts:
|
|
-- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE)
|
|
-- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE)
|
|
--
|
|
-- font names used by lite:
|
|
-- style.font : user interface
|
|
-- style.big_font : big text in welcome screen
|
|
-- style.icon_font : icons
|
|
-- style.icon_big_font : toolbar icons
|
|
-- style.code_font : code
|
|
--
|
|
-- the function to load the font accept a 3rd optional argument like:
|
|
--
|
|
-- {antialiasing="grayscale", hinting="full"}
|
|
--
|
|
-- possible values are:
|
|
-- antialiasing: grayscale, subpixel
|
|
-- hinting: none, slight, full
|
|
|
|
------------------------------ Plugins ----------------------------------------
|
|
|
|
-- enable or disable plugin loading setting config entries:
|
|
|
|
-- enable plugins.trimwhitespace, otherwise it is disable by default:
|
|
-- config.plugins.trimwhitespace = true
|
|
--
|
|
-- disable detectindent, otherwise it is enabled by default
|
|
-- config.plugins.detectindent = false
|
|
]])
|
|
init_file:close()
|
|
end
|
|
|
|
|
|
|
|
function core.load_user_directory()
|
|
return core.try(function()
|
|
local stat_dir = system.get_file_info(USERDIR)
|
|
if not stat_dir then
|
|
create_user_directory()
|
|
end
|
|
local init_filename = USERDIR .. "/init.lua"
|
|
local stat_file = system.get_file_info(init_filename)
|
|
if not stat_file then
|
|
write_user_init_file(init_filename)
|
|
end
|
|
dofile(init_filename)
|
|
end)
|
|
end
|
|
|
|
|
|
function core.remove_project_directory(path)
|
|
-- skip the fist directory because it is the project's directory
|
|
for i = 2, #core.project_directories do
|
|
local dir = core.project_directories[i]
|
|
if dir.name == path then
|
|
table.remove(core.project_directories, i)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
local function whitespace_replacements()
|
|
local r = renderer.replacements.new()
|
|
r:add(" ", "·")
|
|
r:add("\t", "»")
|
|
return r
|
|
end
|
|
|
|
|
|
local function configure_borderless_window()
|
|
system.set_window_bordered(not config.borderless)
|
|
core.title_view:configure_hit_test(config.borderless)
|
|
core.title_view.visible = config.borderless
|
|
end
|
|
|
|
|
|
local function reload_customizations()
|
|
core.reload_module("core.style")
|
|
core.reload_module("core.config")
|
|
core.reload_module("core.keymap")
|
|
core.load_user_directory()
|
|
core.load_project_module()
|
|
rescan_project_directories()
|
|
configure_borderless_window()
|
|
end
|
|
|
|
|
|
local function add_config_files_hooks()
|
|
-- auto-realod style when user's module is saved by overriding Doc:Save()
|
|
local doc_save = Doc.save
|
|
local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua")
|
|
local module_filename = system.absolute_path(".lite_project.lua")
|
|
function Doc:save(filename, abs_filename)
|
|
doc_save(self, filename, abs_filename)
|
|
if self.abs_filename == user_filename or self.abs_filename == module_filename then
|
|
reload_customizations()
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function core.init()
|
|
command = require "core.command"
|
|
keymap = require "core.keymap"
|
|
RootView = require "core.rootview"
|
|
StatusView = require "core.statusview"
|
|
TitleView = require "core.titleview"
|
|
CommandView = require "core.commandview"
|
|
NagView = require "core.nagview"
|
|
DocView = require "core.docview"
|
|
Doc = require "core.doc"
|
|
|
|
if PATHSEP == '\\' then
|
|
USERDIR = common.normalize_volume(USERDIR)
|
|
DATADIR = common.normalize_volume(DATADIR)
|
|
EXEDIR = common.normalize_volume(EXEDIR)
|
|
end
|
|
|
|
do
|
|
local session = load_session()
|
|
if session.window_mode == "normal" then
|
|
system.set_window_size(table.unpack(session.window))
|
|
elseif session.window_mode == "maximized" then
|
|
system.set_window_mode("maximized")
|
|
end
|
|
core.recent_projects = session.recents or {}
|
|
core.previous_find = session.previous_find or {}
|
|
core.previous_replace = session.previous_replace or {}
|
|
end
|
|
|
|
local project_dir = core.recent_projects[1] or "."
|
|
local project_dir_explicit = false
|
|
local files = {}
|
|
local delayed_error
|
|
for i = 2, #ARGS do
|
|
local arg_filename = strip_trailing_slash(ARGS[i])
|
|
local info = system.get_file_info(arg_filename) or {}
|
|
if info.type == "file" then
|
|
local file_abs = system.absolute_path(arg_filename)
|
|
if file_abs then
|
|
table.insert(files, file_abs)
|
|
project_dir = file_abs:match("^(.+)[/\\].+$")
|
|
end
|
|
elseif info.type == "dir" then
|
|
project_dir = arg_filename
|
|
project_dir_explicit = true
|
|
else
|
|
-- on macOS we can get an argument like "-psn_0_52353" that we just ignore.
|
|
if not ARGS[i]:match("^-psn") then
|
|
delayed_error = string.format("error: invalid file or directory %q", ARGS[i])
|
|
end
|
|
end
|
|
end
|
|
|
|
core.frame_start = 0
|
|
core.clip_rect_stack = {{ 0,0,0,0 }}
|
|
core.log_items = {}
|
|
core.docs = {}
|
|
core.window_mode = "normal"
|
|
core.threads = setmetatable({}, { __mode = "k" })
|
|
core.blink_start = system.get_time()
|
|
core.blink_timer = core.blink_start
|
|
|
|
local project_dir_abs = system.absolute_path(project_dir)
|
|
-- We prevent set_project_dir below to effectively add and scan the directory becaese tha
|
|
-- project module and its ignore files is not yet loaded.
|
|
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs, nil, true)
|
|
if set_project_ok then
|
|
if project_dir_explicit then
|
|
update_recents_project("add", project_dir_abs)
|
|
end
|
|
else
|
|
if not project_dir_explicit then
|
|
update_recents_project("remove", project_dir)
|
|
end
|
|
project_dir_abs = system.absolute_path(".")
|
|
if not core.set_project_dir(project_dir_abs, nil, true) then
|
|
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd")
|
|
os.exit(1)
|
|
end
|
|
end
|
|
|
|
core.redraw = true
|
|
core.visited_files = {}
|
|
core.restart_request = false
|
|
core.quit_request = false
|
|
core.replacements = whitespace_replacements()
|
|
|
|
core.root_view = RootView()
|
|
core.command_view = CommandView()
|
|
core.status_view = StatusView()
|
|
core.nag_view = NagView()
|
|
core.title_view = TitleView()
|
|
|
|
local cur_node = core.root_view.root_node
|
|
cur_node.is_primary_node = true
|
|
cur_node:split("up", core.title_view, {y = true})
|
|
cur_node = cur_node.b
|
|
cur_node:split("up", core.nag_view, {y = true})
|
|
cur_node = cur_node.b
|
|
cur_node = cur_node:split("down", core.command_view, {y = true})
|
|
cur_node = cur_node:split("down", core.status_view, {y = true})
|
|
|
|
command.add_defaults()
|
|
local got_user_error = not core.load_user_directory()
|
|
local plugins_success, plugins_refuse_list = core.load_plugins()
|
|
|
|
do
|
|
local pdir, pname = project_dir_abs:match("(.*)[/\\\\](.*)")
|
|
core.log("Opening project %q from directory %s", pname, pdir)
|
|
end
|
|
local got_project_error = not core.load_project_module()
|
|
|
|
-- We add the project directory now because the project's module is loaded.
|
|
core.add_project_directory(project_dir_abs)
|
|
|
|
-- We assume we have just a single project directory here. Now that StatusView
|
|
-- is there show max files warning if needed.
|
|
if core.project_directories[1].files_limit then
|
|
show_max_files_warning(core.project_directories[1])
|
|
end
|
|
|
|
for _, filename in ipairs(files) do
|
|
core.root_view:open_doc(core.open_doc(filename))
|
|
end
|
|
|
|
if delayed_error then
|
|
core.error(delayed_error)
|
|
end
|
|
|
|
if not plugins_success or got_user_error or got_project_error then
|
|
command.perform("core:open-log")
|
|
end
|
|
|
|
configure_borderless_window()
|
|
|
|
if #plugins_refuse_list.userdir.plugins > 0 or #plugins_refuse_list.datadir.plugins > 0 then
|
|
local opt = {
|
|
{ font = style.font, text = "Exit", default_no = true },
|
|
{ font = style.font, text = "Continue" , default_yes = true }
|
|
}
|
|
local msg = {}
|
|
for _, entry in pairs(plugins_refuse_list) do
|
|
if #entry.plugins > 0 then
|
|
msg[#msg + 1] = string.format("Plugins from directory \"%s\":\n%s", common.home_encode(entry.dir), table.concat(entry.plugins, "\n"))
|
|
end
|
|
end
|
|
core.nag_view:show(
|
|
"Refused Plugins",
|
|
string.format(
|
|
"Some plugins are not loaded due to version mismatch.\n\n%s.\n\n" ..
|
|
"Please download a recent version from https://github.com/franko/lite-plugins.",
|
|
table.concat(msg, ".\n\n")),
|
|
opt, function(item)
|
|
if item.text == "Exit" then os.exit(1) end
|
|
end)
|
|
end
|
|
|
|
add_config_files_hooks()
|
|
end
|
|
|
|
|
|
function core.confirm_close_docs(docs, close_fn, ...)
|
|
local dirty_count = 0
|
|
local dirty_name
|
|
for _, doc in ipairs(docs or 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 args = {...}
|
|
local opt = {
|
|
{ font = style.font, text = "Yes", default_yes = true },
|
|
{ font = style.font, text = "No" , default_no = true }
|
|
}
|
|
core.nag_view:show("Unsaved Changes", text, opt, function(item)
|
|
if item.text == "Yes" then close_fn(table.unpack(args)) end
|
|
end)
|
|
else
|
|
close_fn(...)
|
|
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 USERDIR .. PATHSEP .. temp_file_prefix
|
|
.. string.format("%06x", temp_file_counter) .. (ext or "")
|
|
end
|
|
|
|
-- override to perform an operation before quitting or entering the
|
|
-- current project
|
|
do
|
|
local do_nothing = function() end
|
|
core.on_quit_project = do_nothing
|
|
core.on_enter_project = do_nothing
|
|
end
|
|
|
|
|
|
local function quit_with_function(quit_fn, force)
|
|
if force then
|
|
delete_temp_files()
|
|
core.on_quit_project()
|
|
save_session()
|
|
quit_fn()
|
|
else
|
|
core.confirm_close_docs(core.docs, quit_with_function, quit_fn, true)
|
|
end
|
|
end
|
|
|
|
function core.quit(force)
|
|
quit_with_function(function() core.quit_request = true end, force)
|
|
end
|
|
|
|
|
|
function core.restart()
|
|
quit_with_function(function() core.restart_request = true end)
|
|
end
|
|
|
|
|
|
local function check_plugin_version(filename)
|
|
local info = system.get_file_info(filename)
|
|
if info ~= nil and info.type == "dir" then
|
|
filename = filename .. "/init.lua"
|
|
info = system.get_file_info(filename)
|
|
end
|
|
if not info or not filename:match("%.lua$") then return false end
|
|
local f = io.open(filename, "r")
|
|
if not f then return false end
|
|
local version_match = false
|
|
for line in f:lines() do
|
|
local mod_version = line:match('%-%-.*%f[%a]mod%-version%s*:%s*(%d+)')
|
|
if mod_version then
|
|
version_match = (mod_version == MOD_VERSION)
|
|
break
|
|
end
|
|
-- The following pattern is used for backward compatibility only
|
|
-- Future versions will look only at the mod-version tag.
|
|
local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$')
|
|
if version then
|
|
-- we consider the version tag 2.0 equivalent to mod-version:2
|
|
version_match = (version == '2.0' and MOD_VERSION == "2")
|
|
break
|
|
end
|
|
end
|
|
f:close()
|
|
return true, version_match
|
|
end
|
|
|
|
|
|
function core.load_plugins()
|
|
local no_errors = true
|
|
local refused_list = {
|
|
userdir = {dir = USERDIR, plugins = {}},
|
|
datadir = {dir = DATADIR, plugins = {}},
|
|
}
|
|
local files = {}
|
|
for _, root_dir in ipairs {DATADIR, USERDIR} do
|
|
local plugin_dir = root_dir .. "/plugins"
|
|
for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
|
|
files[filename] = plugin_dir -- user plugins will always replace system plugins
|
|
end
|
|
end
|
|
|
|
for filename, plugin_dir in pairs(files) do
|
|
local basename = filename:match("(.-)%.lua$") or filename
|
|
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
|
|
if is_lua_file then
|
|
if not version_match then
|
|
core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir)
|
|
local list = refused_list[plugin_dir:find(USERDIR, 1, true) == 1 and 'userdir' or 'datadir'].plugins
|
|
table.insert(list, filename)
|
|
end
|
|
if version_match and config.plugins[basename] ~= false then
|
|
local ok = core.try(require, "plugins." .. basename)
|
|
if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end
|
|
if not ok then
|
|
no_errors = false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return no_errors, refused_list
|
|
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_visited(filename)
|
|
for i = 1, #core.visited_files do
|
|
if core.visited_files[i] == filename then
|
|
table.remove(core.visited_files, i)
|
|
break
|
|
end
|
|
end
|
|
table.insert(core.visited_files, 1, filename)
|
|
end
|
|
|
|
|
|
function core.set_active_view(view)
|
|
assert(view, "Tried to set active view to nil")
|
|
if view ~= core.active_view then
|
|
if core.active_view and core.active_view.force_focus then
|
|
core.next_active_view = view
|
|
return
|
|
end
|
|
core.next_active_view = nil
|
|
if view.doc and view.doc.filename then
|
|
core.set_visited(view.doc.filename)
|
|
end
|
|
core.last_active_view = core.active_view
|
|
core.active_view = view
|
|
end
|
|
end
|
|
|
|
|
|
function core.show_title_bar(show)
|
|
core.title_view.visible = show
|
|
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 }
|
|
return key
|
|
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.normalize_to_project_dir(filename)
|
|
filename = common.normalize_path(filename)
|
|
if common.path_belongs_to(filename, core.project_dir) then
|
|
filename = common.relative_path(core.project_dir, filename)
|
|
end
|
|
return filename
|
|
end
|
|
|
|
|
|
-- The function below works like system.absolute_path except it
|
|
-- doesn't fail if the file does not exist. We consider that the
|
|
-- current dir is core.project_dir so relative filename are considered
|
|
-- to be in core.project_dir.
|
|
-- Please note that .. or . in the filename are not taken into account.
|
|
-- This function should get only filenames normalized using
|
|
-- common.normalize_path function.
|
|
function core.project_absolute_path(filename)
|
|
if filename:match('^%a:\\') or filename:find('/', 1, true) == 1 then
|
|
return filename
|
|
else
|
|
return core.project_dir .. PATHSEP .. filename
|
|
end
|
|
end
|
|
|
|
|
|
function core.open_doc(filename)
|
|
local new_file = not filename or not system.get_file_info(filename)
|
|
local abs_filename
|
|
if filename then
|
|
-- normalize filename and set absolute filename then
|
|
-- try to find existing doc for filename
|
|
filename = core.normalize_to_project_dir(filename)
|
|
abs_filename = core.project_absolute_path(filename)
|
|
for _, doc in ipairs(core.docs) do
|
|
if doc.abs_filename and abs_filename == doc.abs_filename then
|
|
return doc
|
|
end
|
|
end
|
|
end
|
|
-- no existing doc for filename; create new
|
|
local doc = Doc(filename, abs_filename, new_file)
|
|
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.get_log(i)
|
|
if i == nil then
|
|
local r = {}
|
|
for _, item in ipairs(core.log_items) do
|
|
table.insert(r, core.get_log(item))
|
|
end
|
|
return table.concat(r, "\n")
|
|
end
|
|
local item = type(i) == "number" and core.log_items[i] or i
|
|
local text = string.format("[%s] %s at %s", os.date(nil, item.time), item.text, item.at)
|
|
if item.info then
|
|
text = string.format("%s\n%s\n", text, item.info)
|
|
end
|
|
return text
|
|
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
|
|
|
|
local scheduled_rescan = {}
|
|
|
|
function core.has_pending_rescan()
|
|
for _ in pairs(scheduled_rescan) do
|
|
return true
|
|
end
|
|
end
|
|
|
|
|
|
function core.dir_rescan_add_job(dir, filepath)
|
|
local dirpath = filepath:match("^(.+)[/\\].+$")
|
|
local dirpath_rooted = dirpath and PATHSEP .. dirpath or ""
|
|
local abs_dirpath = dir.name .. dirpath_rooted
|
|
if dirpath then
|
|
-- check if the directory is in the project files list, if not exit
|
|
local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"})
|
|
-- Note that is dir_match is false dir_index greaten than the last valid index.
|
|
-- We use dir_index to index dir.files below only if dir_match is true.
|
|
if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end
|
|
end
|
|
local new_time = system.get_time() + 1
|
|
|
|
-- evaluate new rescan request versus existing rescan
|
|
local remove_list = {}
|
|
for _, rescan in pairs(scheduled_rescan) do
|
|
if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then
|
|
-- abs_dirpath is a subpath of a scan already ongoing: skip
|
|
rescan.time_limit = new_time
|
|
return
|
|
elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then
|
|
-- abs_dirpath already cover this rescan: add to the list of rescan to be removed
|
|
table.insert(remove_list, rescan.abs_path)
|
|
end
|
|
end
|
|
for _, key_path in ipairs(remove_list) do
|
|
scheduled_rescan[key_path] = nil
|
|
end
|
|
|
|
scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time}
|
|
core.add_thread(function()
|
|
while true do
|
|
local rescan = scheduled_rescan[abs_dirpath]
|
|
if not rescan then return end
|
|
if system.get_time() > rescan.time_limit then
|
|
local has_changes = rescan_project_subdir(rescan.dir, rescan.path)
|
|
if has_changes then
|
|
core.redraw = true -- we run without an event, from a thread
|
|
rescan.time_limit = new_time
|
|
else
|
|
scheduled_rescan[rescan.abs_path] = nil
|
|
return
|
|
end
|
|
end
|
|
coroutine.yield(0.2)
|
|
end
|
|
end)
|
|
end
|
|
|
|
|
|
-- no-op but can be overrided by plugins
|
|
function core.on_dirmonitor_modify() end
|
|
function core.on_dirmonitor_delete() end
|
|
|
|
|
|
function core.on_dir_change(watch_id, action, filepath)
|
|
local dir = project_dir_by_watch_id(watch_id)
|
|
if not dir then return end
|
|
core.dir_rescan_add_job(dir, filepath)
|
|
if action == "delete" then
|
|
project_scan_remove_file(dir, filepath)
|
|
core.on_dirmonitor_delete(dir, filepath)
|
|
elseif action == "create" then
|
|
project_scan_add_file(dir, filepath)
|
|
core.on_dirmonitor_modify(dir, filepath);
|
|
elseif action == "modify" then
|
|
core.on_dirmonitor_modify(dir, filepath);
|
|
end
|
|
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 == "resized" then
|
|
core.window_mode = system.get_window_mode()
|
|
elseif type == "minimized" or type == "maximized" or type == "restored" then
|
|
core.window_mode = type == "restored" and "normal" or type
|
|
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 == "focuslost" then
|
|
core.root_view:on_focus_lost(...)
|
|
elseif type == "dirchange" then
|
|
core.on_dir_change(...)
|
|
elseif type == "quit" then
|
|
core.quit()
|
|
end
|
|
return did_keymap
|
|
end
|
|
|
|
|
|
local function get_title_filename(view)
|
|
local doc_filename = view.get_filename and view:get_filename() or view:get_name()
|
|
return (doc_filename ~= "---") and doc_filename or ""
|
|
end
|
|
|
|
|
|
function core.compose_window_title(title)
|
|
return title == "" and "Lite XL" or title .. " - Lite XL"
|
|
end
|
|
|
|
|
|
function core.step()
|
|
-- handle events
|
|
local did_keymap = false
|
|
|
|
for type, a,b,c,d in system.poll_event do
|
|
if type == "textinput" and did_keymap then
|
|
did_keymap = false
|
|
elseif type == "mousemoved" then
|
|
core.try(core.on_event, type, a, b, c, d)
|
|
else
|
|
local _, res = core.try(core.on_event, type, a, b, c, d)
|
|
did_keymap = res or did_keymap
|
|
end
|
|
core.redraw = true
|
|
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)
|
|
doc:on_close()
|
|
end
|
|
end
|
|
|
|
-- update window title
|
|
local current_title = get_title_filename(core.active_view)
|
|
if current_title ~= core.window_title then
|
|
system.set_window_title(core.compose_window_title(current_title))
|
|
core.window_title = current_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 need_more_work = 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
|
|
else
|
|
need_more_work = true
|
|
end
|
|
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(true)
|
|
end
|
|
end
|
|
|
|
if not need_more_work then coroutine.yield(false) end
|
|
end
|
|
end)
|
|
|
|
|
|
function core.run()
|
|
local idle_iterations = 0
|
|
while true do
|
|
core.frame_start = system.get_time()
|
|
local did_redraw = core.step()
|
|
local need_more_work = run_threads() or core.has_pending_rescan()
|
|
if core.restart_request or core.quit_request then break end
|
|
if not did_redraw and not need_more_work then
|
|
idle_iterations = idle_iterations + 1
|
|
-- do not wait of events at idle_iterations = 1 to give a chance at core.step to run
|
|
-- and set "redraw" flag.
|
|
if idle_iterations > 1 then
|
|
if system.window_has_focus() then
|
|
-- keep running even with no events to make the cursor blinks
|
|
local t = system.get_time() - core.blink_start
|
|
local h = config.blink_period / 2
|
|
local dt = math.ceil(t / h) * h - t
|
|
system.wait_event(dt + 1 / config.fps)
|
|
else
|
|
system.wait_event()
|
|
end
|
|
end
|
|
else
|
|
idle_iterations = 0
|
|
local elapsed = system.get_time() - core.frame_start
|
|
system.sleep(math.max(0, 1 / config.fps - elapsed))
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function core.blink_reset()
|
|
core.blink_start = system.get_time()
|
|
end
|
|
|
|
|
|
function core.request_cursor(value)
|
|
core.cursor_change_req = value
|
|
end
|
|
|
|
|
|
function core.on_error(err)
|
|
-- write error to file
|
|
local fp = io.open(USERDIR .. "/error.txt", "wb")
|
|
fp:write("Error: " .. tostring(err) .. "\n")
|
|
fp:write(debug.traceback(nil, 4) .. "\n")
|
|
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
|