* fix delete_temp_files() deleting in EXEDIR but temp_filename() was creating temp files in USERDIR * make delete_temp_files() public so it can be used by plugins * add optional `dir` parameter to both delete_temp_files() and temp_filename() to allow specifying a different directory, this is for example useful when generting markdown previews, the temp file should be generated in the project dir in case the readme references images that are relative to it, so the web browser can find them.
1326 lines
41 KiB
Lua
1326 lines
41 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 dirwatch
|
|
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)
|
|
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 = {}
|
|
end
|
|
return chdir_ok
|
|
end
|
|
|
|
|
|
local function reload_customizations()
|
|
core.load_user_directory()
|
|
core.load_project_module()
|
|
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()
|
|
reload_customizations()
|
|
update_recents_project("add", dir_path_abs)
|
|
core.add_project_directory(dir_path_abs)
|
|
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
|
|
|
|
|
|
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 https://github.com/lite-xl/lite-xl."
|
|
if core.status_view then
|
|
core.status_view:show_message("!", style.accent, message)
|
|
end
|
|
end
|
|
|
|
|
|
-- bisects the sorted file list to get to things in ln(n)
|
|
local function file_bisect(files, is_superior, start_idx, end_idx)
|
|
local inf, sup = start_idx or 1, end_idx or #files
|
|
while sup - inf > 8 do
|
|
local curr = math.floor((inf + sup) / 2)
|
|
if is_superior(files[curr]) then
|
|
sup = curr - 1
|
|
else
|
|
inf = curr
|
|
end
|
|
end
|
|
while inf <= sup and not is_superior(files[inf]) do
|
|
inf = inf + 1
|
|
end
|
|
return inf
|
|
end
|
|
|
|
|
|
local function file_search(files, info)
|
|
local idx = file_bisect(files, function(file)
|
|
return system.path_compare(info.filename, info.type, file.filename, file.type)
|
|
end)
|
|
if idx > 1 and files[idx-1].filename == info.filename then
|
|
return idx - 1, true
|
|
end
|
|
return idx, false
|
|
end
|
|
|
|
|
|
local function files_info_equal(a, b)
|
|
return (a == nil and b == nil) or (a and b and a.filename == b.filename and a.type == b.type)
|
|
end
|
|
|
|
|
|
local function project_subdir_bounds(dir, filename, start_index)
|
|
local found = true
|
|
if not start_index then
|
|
start_index, found = file_search(dir.files, { type = "dir", filename = filename })
|
|
end
|
|
if found then
|
|
local end_index = file_bisect(dir.files, function(file)
|
|
return not common.path_belongs_to(file.filename, filename)
|
|
end, start_index + 1)
|
|
return start_index, end_index - start_index, dir.files[start_index]
|
|
end
|
|
end
|
|
|
|
|
|
-- Should be called on any directory that registers a change, or on a directory we open if we're over the file limit.
|
|
-- Uses relative paths at the project root (i.e. target = "", target = "first-level-directory", target = "first-level-directory/second-level-directory")
|
|
local function refresh_directory(topdir, target)
|
|
local directory_start_idx, directory_end_idx = 1, #topdir.files
|
|
if target and target ~= "" then
|
|
directory_start_idx, directory_end_idx = project_subdir_bounds(topdir, target)
|
|
directory_end_idx = directory_start_idx + directory_end_idx - 1
|
|
directory_start_idx = directory_start_idx + 1
|
|
end
|
|
|
|
local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), {}, 0, function() return false end)
|
|
local change = false
|
|
|
|
-- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that.
|
|
-- Unwatch just in case.
|
|
if files == nil then
|
|
topdir.watch:unwatch(topdir.name .. PATHSEP .. (target or ""))
|
|
return true
|
|
end
|
|
|
|
local new_idx, old_idx = 1, directory_start_idx
|
|
local new_directories = {}
|
|
-- Run through each sorted list and compare them. If we find a new entry, insert it and flag as new. If we're missing an entry
|
|
-- remove it and delete the entry from the list.
|
|
while old_idx <= directory_end_idx or new_idx <= #files do
|
|
local old_info, new_info = topdir.files[old_idx], files[new_idx]
|
|
if not files_info_equal(new_info, old_info) then
|
|
change = true
|
|
-- If we're a new file, and we exist *before* the other file in the list, then add to the list.
|
|
if not old_info or (new_info and system.path_compare(new_info.filename, new_info.type, old_info.filename, old_info.type)) then
|
|
table.insert(topdir.files, old_idx, new_info)
|
|
old_idx, new_idx = old_idx + 1, new_idx + 1
|
|
if new_info.type == "dir" then
|
|
table.insert(new_directories, new_info)
|
|
end
|
|
directory_end_idx = directory_end_idx + 1
|
|
else
|
|
-- If it's not there, remove the entry from the list as being out of order.
|
|
table.remove(topdir.files, old_idx)
|
|
if old_info.type == "dir" then
|
|
topdir.watch:unwatch(topdir.name .. PATHSEP .. old_info.filename)
|
|
end
|
|
directory_end_idx = directory_end_idx - 1
|
|
end
|
|
else
|
|
-- If this file is a directory, determine in ln(n) the size of the directory, and skip every file in it.
|
|
local size = old_info and old_info.type == "dir" and select(2, project_subdir_bounds(topdir, old_info.filename, old_idx)) or 1
|
|
old_idx, new_idx = old_idx + size, new_idx + 1
|
|
end
|
|
end
|
|
for i, v in ipairs(new_directories) do
|
|
topdir.watch:watch(topdir.name .. PATHSEP .. v.filename)
|
|
if not topdir.files_limit or core.project_subdir_is_shown(topdir, v.filename) then
|
|
refresh_directory(topdir, v.filename)
|
|
end
|
|
end
|
|
if change then
|
|
core.redraw = true
|
|
topdir.is_dirty = true
|
|
end
|
|
return change
|
|
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
|
|
|
|
|
|
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 topdir = {
|
|
name = path,
|
|
item = {filename = common.basename(path), type = "dir", topdir = true},
|
|
files_limit = false,
|
|
is_dirty = true,
|
|
shown_subdir = {},
|
|
watch_thread = nil,
|
|
watch = dirwatch.new()
|
|
}
|
|
table.insert(core.project_directories, topdir)
|
|
|
|
local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown"
|
|
topdir.force_scans = (fstype == "nfs" or fstype == "fuse")
|
|
local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred)
|
|
topdir.files = t
|
|
if not complete then
|
|
topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
|
|
topdir.files_limit = true
|
|
show_max_files_warning(topdir)
|
|
refresh_directory(topdir)
|
|
else
|
|
for i,v in ipairs(t) do
|
|
if v.type == "dir" then topdir.watch:watch(path .. PATHSEP .. v.filename) end
|
|
end
|
|
end
|
|
topdir.watch:watch(topdir.name)
|
|
-- each top level directory gets a watch thread. if the project is small, or
|
|
-- if the ablity to use directory watches hasn't been compromised in some way
|
|
-- either through error, or amount of files, then this should be incredibly
|
|
-- quick; essentially one syscall per check. Otherwise, this may take a bit of
|
|
-- time; the watch will yield in this coroutine after 0.01 second, for 0.1 seconds.
|
|
topdir.watch_thread = core.add_thread(function()
|
|
while true do
|
|
topdir.watch:check(function(target)
|
|
if target == topdir.name then return refresh_directory(topdir) end
|
|
local dirpath = target:sub(#topdir.name + 2)
|
|
local abs_dirpath = topdir.name .. PATHSEP .. dirpath
|
|
if dirpath then
|
|
-- check if the directory is in the project files list, if not exit.
|
|
local dir_index, dir_match = file_search(topdir.files, {filename = dirpath, type = "dir"})
|
|
if not dir_match or not core.project_subdir_is_shown(topdir, topdir.files[dir_index].filename) then return end
|
|
end
|
|
return refresh_directory(topdir, dirpath)
|
|
end, 0.01, 0.01)
|
|
coroutine.yield(0.05)
|
|
end
|
|
end)
|
|
|
|
if path == core.project_dir then
|
|
core.project_files = topdir.files
|
|
end
|
|
core.redraw = true
|
|
return topdir
|
|
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.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
|
|
-- We need to sort the list of shown subdirectories so that higher level
|
|
-- directories are populated first. We use the function system.path_compare
|
|
-- because it order the entries in the appropriate order.
|
|
-- TODO: we may consider storing the table shown_subdir as a sorted table
|
|
-- since the beginning.
|
|
local subdir_list = {}
|
|
for subdir in pairs(save_project_dirs[i].shown_subdir) do
|
|
table.insert(subdir_list, subdir)
|
|
end
|
|
table.sort(subdir_list, function(a, b) return system.path_compare(a, "dir", b, "dir") end)
|
|
for _, subdir in ipairs(subdir_list) do
|
|
local show = save_project_dirs[i].shown_subdir[subdir]
|
|
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.
|
|
-- In theory set_show below may fail and return false but is it is listed
|
|
-- there it means it succeeded before so we are optimistically assume it
|
|
-- will not fail for the sake of simplicity.
|
|
core.update_project_subdir(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")
|
|
dir.shown_subdir[filename] = expanded
|
|
return refresh_directory(dir, filename)
|
|
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)
|
|
elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) then
|
|
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
|
|
|
|
|
|
-- 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.write_init_project_module(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 project's module settings here.
|
|
-- This module will be loaded when opening a project, after the user module
|
|
-- configuration.
|
|
-- It will be automatically reloaded when saved.
|
|
|
|
local config = require "core.config"
|
|
|
|
-- you can add some patterns to ignore files within the project
|
|
-- config.ignore_files = {"^%.", <some-patterns>}
|
|
|
|
-- Patterns are normally applied to the file's or directory's name, without
|
|
-- its path. See below about how to apply filters on a path.
|
|
--
|
|
-- Here some examples:
|
|
--
|
|
-- "^%." match any file of directory whose basename begins with a dot.
|
|
--
|
|
-- When there is an '/' or a '/$' at the end the pattern it will only match
|
|
-- directories. When using such a pattern a final '/' will be added to the name
|
|
-- of any directory entry before checking if it matches.
|
|
--
|
|
-- "^%.git/" matches any directory named ".git" anywhere in the project.
|
|
--
|
|
-- If a "/" appears anywhere in the pattern except if it appears at the end or
|
|
-- is immediately followed by a '$' then the pattern will be applied to the full
|
|
-- path of the file or directory. An initial "/" will be prepended to the file's
|
|
-- or directory's path to indicate the project's root.
|
|
--
|
|
-- "^/node_modules/" will match a directory named "node_modules" at the project's root.
|
|
-- "^/build.*/" match any top level directory whose name begins with "build"
|
|
-- "^/subprojects/.+/" match any directory inside a top-level folder named "subprojects".
|
|
|
|
-- You may activate some plugins on a pre-project base to override the user's settings.
|
|
-- config.plugins.trimwitespace = true
|
|
]])
|
|
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 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 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")
|
|
function Doc:save(filename, abs_filename)
|
|
local module_filename = system.absolute_path(".lite_project.lua")
|
|
doc_save(self, filename, abs_filename)
|
|
if self.abs_filename == user_filename or self.abs_filename == module_filename then
|
|
reload_customizations()
|
|
rescan_project_directories()
|
|
configure_borderless_window()
|
|
end
|
|
end
|
|
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 common.normalize_path(filename)
|
|
elseif not core.project_dir then
|
|
local cwd = system.absolute_path(".")
|
|
return cwd .. PATHSEP .. common.normalize_path(filename)
|
|
else
|
|
return core.project_dir .. PATHSEP .. filename
|
|
end
|
|
end
|
|
|
|
|
|
function core.init()
|
|
command = require "core.command"
|
|
keymap = require "core.keymap"
|
|
dirwatch = require "core.dirwatch"
|
|
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 = {}
|
|
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 == "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
|
|
local file_abs = core.project_absolute_path(arg_filename)
|
|
if file_abs then
|
|
table.insert(files, file_abs)
|
|
project_dir = file_abs:match("^(.+)[/\\].+$")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
core.frame_start = 0
|
|
core.clip_rect_stack = {{ 0,0,0,0 }}
|
|
core.log_items = {}
|
|
core.docs = {}
|
|
core.cursor_clipboard = {}
|
|
core.cursor_clipboard_whole_line = {}
|
|
core.window_mode = "normal"
|
|
core.threads = setmetatable({}, { __mode = "k" })
|
|
core.blink_start = system.get_time()
|
|
core.blink_timer = core.blink_start
|
|
core.redraw = true
|
|
core.visited_files = {}
|
|
core.restart_request = false
|
|
core.quit_request = false
|
|
|
|
-- We load core views before plugins that may need them.
|
|
core.root_view = RootView()
|
|
core.command_view = CommandView()
|
|
core.status_view = StatusView()
|
|
core.nag_view = NagView()
|
|
core.title_view = TitleView()
|
|
|
|
-- Some plugins (eg: console) require the nodes to be initialized to defaults
|
|
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})
|
|
|
|
-- Load defaiult commands first so plugins can override them
|
|
command.add_defaults()
|
|
|
|
-- Load user module, plugins and project module
|
|
local got_user_error, got_project_error = not core.load_user_directory()
|
|
|
|
local project_dir_abs = system.absolute_path(project_dir)
|
|
-- We prevent set_project_dir below to effectively add and scan the directory because the
|
|
-- 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)
|
|
if set_project_ok then
|
|
got_project_error = not core.load_project_module()
|
|
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, function()
|
|
got_project_error = not core.load_project_module()
|
|
end) then
|
|
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd")
|
|
os.exit(1)
|
|
end
|
|
end
|
|
|
|
-- Load core plugins after user ones to let the user override them
|
|
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
|
|
|
|
-- 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 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/lite-xl/lite-xl-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 = math.floor(system.get_time() * 1000) % 0xffffffff
|
|
local temp_file_prefix = string.format(".lite_temp_%08x", tonumber(temp_uid))
|
|
local temp_file_counter = 0
|
|
|
|
function core.delete_temp_files(dir)
|
|
dir = type(dir) == "string" and common.normalize_path(dir) or USERDIR
|
|
for _, filename in ipairs(system.list_dir(dir)) do
|
|
if filename:find(temp_file_prefix, 1, true) == 1 then
|
|
os.remove(dir .. PATHSEP .. filename)
|
|
end
|
|
end
|
|
end
|
|
|
|
function core.temp_filename(ext, dir)
|
|
dir = type(dir) == "string" and common.normalize_path(dir) or USERDIR
|
|
temp_file_counter = temp_file_counter + 1
|
|
return dir .. 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
|
|
core.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, ordered = {}, {}
|
|
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
|
|
if not files[filename] then table.insert(ordered, filename) end
|
|
files[filename] = plugin_dir -- user plugins will always replace system plugins
|
|
end
|
|
end
|
|
table.sort(ordered)
|
|
|
|
for _, filename in ipairs(ordered) do
|
|
local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename
|
|
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
|
|
if is_lua_file then
|
|
if not config.skip_plugins_version and 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)
|
|
elseif 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 args = {...}
|
|
local fn = function() return core.try(f, table.unpack(args)) 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
|
|
|
|
|
|
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(level, show, fmt, ...)
|
|
local text = string.format(fmt, ...)
|
|
if show then
|
|
local s = style.log[level]
|
|
core.status_view:show_message(s.icon, s.color, text)
|
|
end
|
|
|
|
local info = debug.getinfo(2, "Sl")
|
|
local at = string.format("%s:%d", info.short_src, info.currentline)
|
|
local item = {
|
|
level = level,
|
|
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("INFO", true, ...)
|
|
end
|
|
|
|
|
|
function core.log_quiet(...)
|
|
return log("INFO", false, ...)
|
|
end
|
|
|
|
|
|
function core.error(...)
|
|
return log("ERROR", true, ...)
|
|
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] %s at %s", os.date(nil, item.time), item.level, 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.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
|
|
if not core.root_view:on_mouse_pressed(...) then
|
|
did_keymap = keymap.on_mouse_pressed(...)
|
|
end
|
|
elseif type == "mousereleased" then
|
|
core.root_view:on_mouse_released(...)
|
|
elseif type == "mousewheel" then
|
|
if not core.root_view:on_mouse_wheel(...) then
|
|
did_keymap = keymap.on_mouse_wheel(...)
|
|
end
|
|
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
|
|
if not core.root_view:on_file_dropped(...) 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
|
|
end
|
|
elseif type == "focuslost" then
|
|
core.root_view:on_focus_lost(...)
|
|
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
|