Merge pull request #809 from lite-xl/merge-master-2.0

Merge master 2.0
This commit is contained in:
Adam 2022-01-28 14:38:22 -05:00 committed by GitHub
commit 0a70b13a73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 704 additions and 177 deletions

View File

@ -1,5 +1,55 @@
This files document the changes done in Lite XL for each release. This files document the changes done in Lite XL for each release.
### 2.0.5
Revamp the project's user module so that modifications are immediately applied.
Add a mechanism to ignore files or directory based on their project's path.
The new mechanism is backward compatible.*
Essentially there are two mechanisms:
- if a '/' or a '/$' appear at the end of the pattern it will match only directories
- if a '/' appears anywhere in the pattern except at the end the pattern will be
applied to the path
In the first case, when the pattern corresponds to a directory, a '/' will be
appended to the name of each directory before checking the pattern.
In the second case, when the pattern corresponds to a path, the complete path of
the file or directory will be used with an initial '/' added to the path.
Fix several problems with the directory monitoring library.
Now the application should no longer assert when some related system call fails
and we fallback to rescan when an error happens.
On linux no longer use the recursive monitoring which was a source of problem.
Directory monitoring is now aware of symlinks and treat them appropriately.
Fix problem when encountering special files type on linux.
Improve directory monitoring so that the related thread actually waits without using
any CPU time when there are no events.
Improve the suggestion when changing project folder or opening a new one.
Now the previously used directory are suggested but if the path is changed the
actual existing directories that match the pattern are suggested.
In addition always use the text entered in the command view even if a suggested entry
is highlighted.
The NagView warning window now no longer moves the document content.
### 2.0.4
Fix some bugs related to newly introduced directory monitoring using the dmon library.
Fix a problem with plain text search using Lua patterns by error.
Fix a problem with visualization of UTF-8 characters that caused garbage characters
visualization.
Other fixes and improvements contributed by @Guldoman.
### 2.0.3 ### 2.0.3
Replace periodic rescan of project folder with a notification based system using the Replace periodic rescan of project folder with a notification based system using the

View File

@ -10,8 +10,18 @@ local restore_title_view = false
local function suggest_directory(text) local function suggest_directory(text)
text = common.home_expand(text) text = common.home_expand(text)
return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir))) local basedir = common.dirname(core.project_dir)
and core.recent_projects or common.dir_path_suggest(text)) return common.home_encode_list((basedir and text == basedir .. PATHSEP or text == "") and
core.recent_projects or common.dir_path_suggest(text))
end
local function check_directory_path(path)
local abs_path = system.absolute_path(path)
local info = abs_path and system.get_file_info(abs_path)
if not info or info.type ~= 'dir' then
return nil
end
return abs_path
end end
command.add(nil, { command.add(nil, {
@ -141,46 +151,51 @@ command.add(nil, {
end, end,
["core:open-project-module"] = function() ["core:open-project-module"] = function()
local filename = ".lite_project.lua" if not system.get_file_info(".lite_project.lua") then
if system.get_file_info(filename) then core.try(core.write_init_project_module, ".lite_project.lua")
core.root_view:open_doc(core.open_doc(filename))
else
local doc = core.open_doc()
core.root_view:open_doc(doc)
doc:save(filename)
end end
local doc = core.open_doc(".lite_project.lua")
core.root_view:open_doc(doc)
doc:save()
end, end,
["core:change-project-folder"] = function() ["core:change-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
if dirname then if dirname then
core.command_view:set_text(common.home_encode(dirname)) core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
end end
core.command_view:enter("Change Project Folder", function(text, item) core.command_view:enter("Change Project Folder", function(text)
text = system.absolute_path(common.home_expand(item and item.text or text)) local path = common.home_expand(text)
if text == core.project_dir then return end local abs_path = check_directory_path(path)
local path_stat = system.get_file_info(text) if not abs_path then
if not path_stat or path_stat.type ~= 'dir' then core.error("Cannot open directory %q", path)
core.error("Cannot open folder %q", text)
return return
end end
core.confirm_close_docs(core.docs, core.open_folder_project, text) if abs_path == core.project_dir then return end
core.confirm_close_docs(core.docs, function(dirpath)
core.close_current_project()
core.open_folder_project(dirpath)
end, abs_path)
end, suggest_directory) end, suggest_directory)
end, end,
["core:open-project-folder"] = function() ["core:open-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
if dirname then if dirname then
core.command_view:set_text(common.home_encode(dirname)) core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
end end
core.command_view:enter("Open Project", function(text, item) core.command_view:enter("Open Project", function(text)
text = common.home_expand(item and item.text or text) local path = common.home_expand(text)
local path_stat = system.get_file_info(text) local abs_path = check_directory_path(path)
if not path_stat or path_stat.type ~= 'dir' then if not abs_path then
core.error("Cannot open folder %q", text) core.error("Cannot open directory %q", path)
return return
end end
system.exec(string.format("%q %q", EXEFILE, text)) if abs_path == core.project_dir then
core.error("Directory %q is currently opened", abs_path)
return
end
system.exec(string.format("%q %q", EXEFILE, abs_path))
end, suggest_directory) end, suggest_directory)
end, end,

View File

@ -489,7 +489,7 @@ local commands = {
end end
for i,docview in ipairs(core.get_views_referencing_doc(doc())) do for i,docview in ipairs(core.get_views_referencing_doc(doc())) do
local node = core.root_view.root_node:get_node_for_view(docview) local node = core.root_view.root_node:get_node_for_view(docview)
node:close_view(core.root_view, docview) node:close_view(core.root_view.root_node, docview)
end end
os.remove(filename) os.remove(filename)
core.log("Removed \"%s\"", filename) core.log("Removed \"%s\"", filename)

View File

@ -84,6 +84,8 @@ function Doc:save(filename, abs_filename)
assert(self.filename, "no filename set to default to") assert(self.filename, "no filename set to default to")
filename = self.filename filename = self.filename
abs_filename = self.abs_filename abs_filename = self.abs_filename
else
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
end end
local fp = assert( io.open(filename, "wb") ) local fp = assert( io.open(filename, "wb") )
for _, line in ipairs(self.lines) do for _, line in ipairs(self.lines) do

View File

@ -58,20 +58,56 @@ function core.set_project_dir(new_dir, change_project_fn)
if change_project_fn then change_project_fn() end if change_project_fn then change_project_fn() end
core.project_dir = common.normalize_volume(new_dir) core.project_dir = common.normalize_volume(new_dir)
core.project_directories = {} core.project_directories = {}
core.add_project_directory(new_dir)
return true
end end
return false return chdir_ok
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
local function reload_customizations()
-- The logic is:
-- - the core.style and config modules are reloaded with the purpose of applying
-- the new user's and project's module configs
-- - inside the core.config the existing fields in config.plugins are preserved
-- because they are reserved to plugins configuration and plugins are already
-- loaded.
-- - plugins are not reloaded or unloaded
local plugins_save = {}
for k, v in pairs(config.plugins) do
plugins_save[k] = v
end
core.reload_module("core.style")
core.reload_module("core.config")
core.load_user_directory()
core.load_project_module()
for k, v in pairs(plugins_save) do
config.plugins[k] = v
end
end end
function core.open_folder_project(dir_path_abs) function core.open_folder_project(dir_path_abs)
if core.set_project_dir(dir_path_abs, core.on_quit_project) then if core.set_project_dir(dir_path_abs, core.on_quit_project) then
core.root_view:close_all_docviews() core.root_view:close_all_docviews()
reload_customizations()
update_recents_project("add", dir_path_abs) update_recents_project("add", dir_path_abs)
if not core.load_project_module() then core.add_project_directory(dir_path_abs)
command.perform("core:open-log")
end
core.on_enter_project(dir_path_abs) core.on_enter_project(dir_path_abs)
end end
end end
@ -93,15 +129,57 @@ local function compare_file(a, b)
end end
-- inspect config.ignore_files patterns and prepare ready to use entries.
local function compile_ignore_files()
local ipatterns = config.ignore_files
local compiled = {}
-- config.ignore_files could be a simple string...
if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end
for i, pattern in ipairs(ipatterns) do
-- we ignore malformed pattern that raise an error
if pcall(string.match, "a", pattern) then
table.insert(compiled, {
use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end
-- An '/' or '/$' at the end means we want to match a directory.
match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value
pattern = pattern -- get the actual pattern
})
end
end
return compiled
end
local function fileinfo_pass_filter(info, ignore_compiled)
if info.size >= config.file_size_limit * 1e6 then return false end
local basename = common.basename(info.filename)
-- replace '\' with '/' for Windows where PATHSEP = '\'
local fullname = "/" .. info.filename:gsub("\\", "/")
for _, compiled in ipairs(ignore_compiled) do
local test = compiled.use_path and fullname or basename
if compiled.match_dir then
if info.type == "dir" and string.match(test .. "/", compiled.pattern) then
return false
end
else
if string.match(test, compiled.pattern) then
return false
end
end
end
return true
end
-- compute a file's info entry completed with "filename" to be used -- 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. -- in project scan or falsy if it shouldn't appear in the list.
local function get_project_file_info(root, file) local function get_project_file_info(root, file, ignore_compiled)
local info = system.get_file_info(root .. file) local info = system.get_file_info(root .. file)
if info then -- info can be not nil but info.type may be nil if is neither a file neither
-- a directory, for example for /dev/* entries on linux.
if info and info.type then
info.filename = strip_leading_path(file) info.filename = strip_leading_path(file)
return (info.size < config.file_size_limit * 1e6 and return fileinfo_pass_filter(info, ignore_compiled) and info
not common.match_pattern(common.basename(info.filename), config.ignore_files)
and info)
end end
end end
@ -123,15 +201,16 @@ end
-- When recursing "root" will always be the same, only "path" will change. -- 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 -- Returns a list of file "items". In eash item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/'. -- complete file path relative to "root" *without* the trailing '/'.
local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook) local function get_directory_files(dir, root, path, t, ignore_compiled, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end if begin_hook then begin_hook() end
ignore_compiled = ignore_compiled or compile_ignore_files()
local t0 = system.get_time() local t0 = system.get_time()
local all = system.list_dir(root .. path) or {} local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0 local t_elapsed = system.get_time() - t0
local dirs, files = {}, {} local dirs, files = {}, {}
for _, file in ipairs(all) do for _, file in ipairs(all) do
local info = get_project_file_info(root, path .. PATHSEP .. file) local info = get_project_file_info(root, path .. PATHSEP .. file, ignore_compiled)
if info then if info then
table.insert(info.type == "dir" and dirs or files, info) table.insert(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1 entries_count = entries_count + 1
@ -143,7 +222,7 @@ local function get_directory_files(dir, root, path, t, entries_count, recurse_pr
for _, f in ipairs(dirs) do for _, f in ipairs(dirs) do
table.insert(t, f) table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then 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) local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, ignore_compiled, entries_count, recurse_pred, begin_hook)
recurse_complete = recurse_complete and complete recurse_complete = recurse_complete and complete
entries_count = n entries_count = n
else else
@ -161,15 +240,14 @@ end
function core.project_subdir_set_show(dir, filename, show) function core.project_subdir_set_show(dir, filename, show)
dir.shown_subdir[filename] = show if dir.files_limit and not dir.force_rescan then
if dir.files_limit and PLATFORM == "Linux" then
local fullpath = dir.name .. PATHSEP .. filename local fullpath = dir.name .. PATHSEP .. filename
local watch_fn = show and system.watch_dir_add or system.watch_dir_rm if not (show and system.watch_dir_add or system.watch_dir_rm)(dir.watch_id, fullpath) then
local success = watch_fn(dir.watch_id, fullpath) return false
if not success then
core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
end end
end end
dir.shown_subdir[filename] = show
return true
end end
@ -209,15 +287,6 @@ local function file_search(files, info)
end end
local function project_scan_add_entry(dir, fileinfo)
local index, match = file_search(dir.files, fileinfo)
if not match then
table.insert(dir.files, index, fileinfo)
dir.is_dirty = true
end
end
local function files_info_equal(a, b) local function files_info_equal(a, b)
return a.filename == b.filename and a.type == b.type return a.filename == b.filename and a.type == b.type
end end
@ -234,7 +303,7 @@ local function files_list_match(a, i1, n, b)
end end
-- arguments like for files_list_match -- arguments like for files_list_match
local function files_list_replace(as, i1, n, bs) local function files_list_replace(as, i1, n, bs, hook)
local m = #bs local m = #bs
local i, j = 1, 1 local i, j = 1, 1
while i <= m or i <= n do while i <= m or i <= n do
@ -244,7 +313,9 @@ local function files_list_replace(as, i1, n, bs)
then then
table.insert(as, i1 + i, b) table.insert(as, i1 + i, b)
i, j, n = i + 1, j + 1, n + 1 i, j, n = i + 1, j + 1, n + 1
if hook and hook.insert then hook.insert(b) end
elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then
if hook and hook.remove then hook.remove(as[i1 + i]) end
table.remove(as, i1 + i) table.remove(as, i1 + i)
n = n - 1 n = n - 1
else else
@ -253,6 +324,29 @@ local function files_list_replace(as, i1, n, bs)
end end
end end
local function project_scan_add_entry(dir, fileinfo)
assert(not dir.force_rescan, "should be used only when force_rescan is false")
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)
if fileinfo.symlink then
local new_files = get_directory_files(dir, dir.name, PATHSEP .. fileinfo.filename, {}, nil, 0, core.project_subdir_is_shown)
files_list_replace(dir.files, index, 0, new_files, {insert = function(info)
if info.type == "dir" then
system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. info.filename)
end
end})
end
end
dir.is_dirty = true
end
end
local function project_subdir_bounds(dir, filename) local function project_subdir_bounds(dir, filename)
local index, n = 0, #dir.files local index, n = 0, #dir.files
for i, file in ipairs(dir.files) do for i, file in ipairs(dir.files) do
@ -271,7 +365,7 @@ local function project_subdir_bounds(dir, filename)
end end
local function rescan_project_subdir(dir, filename_rooted) 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 new_files = get_directory_files(dir, dir.name, filename_rooted, {}, nil, 0, core.project_subdir_is_shown, coroutine.yield)
local index, n = 0, #dir.files local index, n = 0, #dir.files
if filename_rooted ~= "" then if filename_rooted ~= "" then
local filename = strip_leading_path(filename_rooted) local filename = strip_leading_path(filename_rooted)
@ -279,7 +373,19 @@ local function rescan_project_subdir(dir, filename_rooted)
end end
if not files_list_match(dir.files, index, n, new_files) then if not files_list_match(dir.files, index, n, new_files) then
files_list_replace(dir.files, index, n, new_files) -- 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 {insert = 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 dir.is_dirty = true
return true return true
end end
@ -295,38 +401,62 @@ local function add_dir_scan_thread(dir)
end end
coroutine.yield(5) coroutine.yield(5)
end end
end) end, dir)
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. -- Populate a project folder top directory by scanning the filesystem.
local function scan_project_folder(index) local function scan_project_folder(index)
local dir = core.project_directories[index] local dir = core.project_directories[index]
if PLATFORM == "Linux" then
local fstype = system.get_fs_type(dir.name) local fstype = system.get_fs_type(dir.name)
dir.force_rescan = (fstype == "nfs" or fstype == "fuse") dir.force_rescan = (fstype == "nfs" or fstype == "fuse")
if not dir.force_rescan then
local watch_err
dir.watch_id, watch_err = system.watch_dir(dir.name)
if not dir.watch_id then
core.log("Watch directory %s: %s", dir.name, watch_err)
dir.force_rescan = true
end end
local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred) end
local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, nil, 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 if not complete then
dir.slow_filesystem = not complete and (entries_count <= config.max_project_files) dir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
dir.files_limit = true dir.files_limit = true
if not dir.force_rescan then
-- Watch non-recursively on Linux only.
-- The reason is recursively watching with dmon on linux
-- doesn't work on very large directories.
dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux")
end
if core.status_view then -- May be not yet initialized. if core.status_view then -- May be not yet initialized.
show_max_files_warning(dir) show_max_files_warning(dir)
end end
else
if not dir.force_rescan then
dir.watch_id = system.watch_dir(dir.name, true)
end
end end
dir.files = t dir.files = t
if dir.force_rescan then if dir.force_rescan then
add_dir_scan_thread(dir) add_dir_scan_thread(dir)
else else
if not dir.files_limit then
folder_add_subdirs_watch(dir)
end
core.dir_rescan_add_job(dir, ".") core.dir_rescan_add_job(dir, ".")
end end
end end
@ -350,13 +480,73 @@ function core.add_project_directory(path)
core.project_files = dir.files core.project_files = dir.files
end end
core.redraw = true 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
-- 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.project_subdir_set_show(dir, subdir, show)
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 end
function core.update_project_subdir(dir, filename, expanded) 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) local index, n, file = project_subdir_bounds(dir, filename)
if index then if index then
local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {} local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, nil, 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) files_list_replace(dir.files, index, n, new_files)
dir.is_dirty = true dir.is_dirty = true
return true return true
@ -452,6 +642,21 @@ local function project_scan_remove_file(dir, filepath)
fileinfo.type = filetype fileinfo.type = filetype
local index, match = file_search(dir.files, fileinfo) local index, match = file_search(dir.files, fileinfo)
if match then if match then
if filetype == "dir" then
-- If the directory is a symlink it may get deleted and we will
-- never get dirmonitor events for the removal the files it contains.
-- We proceed to remove all the files that belong to the directory.
local _, n_subdir = project_subdir_bounds(dir, filepath)
files_list_replace(dir.files, index, n_subdir, {}, {
remove= function(fileinfo)
if fileinfo.type == "dir" then
system.watch_dir_rm(dir.watch_id, dir.name .. PATHSEP .. filepath)
end
end})
if dir.files_limit then
dir.shown_subdir[filepath] = nil
end
end
table.remove(dir.files, index) table.remove(dir.files, index)
dir.is_dirty = true dir.is_dirty = true
return return
@ -461,13 +666,18 @@ end
local function project_scan_add_file(dir, filepath) local function project_scan_add_file(dir, filepath)
for fragment in string.gmatch(filepath, "([^/\\]+)") do local ignore = compile_ignore_files()
if common.match_pattern(fragment, config.ignore_files) then local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath, ignore)
return
end
end
local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath)
if fileinfo then if fileinfo then
-- on Windows and MacOS we can get events from directories we are not following:
-- check if each parent directories pass the ignore_files rules.
repeat
filepath = common.dirname(filepath)
local parent_info = filepath and get_project_file_info(dir.name, PATHSEP .. filepath, ignore)
if filepath and not parent_info then
return -- parent directory does match ignore_files rules: stop there
end
until not parent_info
project_scan_add_entry(dir, fileinfo) project_scan_add_entry(dir, fileinfo)
end end
end end
@ -548,6 +758,48 @@ local style = require "core.style"
end 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() function core.load_user_directory()
return core.try(function() return core.try(function()
@ -577,15 +829,25 @@ function core.remove_project_directory(path)
return false return false
end end
local function reload_on_user_module_save()
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() -- auto-realod style when user's module is saved by overriding Doc:Save()
local doc_save = Doc.save local doc_save = Doc.save
local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua")
function Doc:save(filename, abs_filename) function Doc:save(filename, abs_filename)
local module_filename = system.absolute_path(".lite_project.lua")
doc_save(self, filename, abs_filename) doc_save(self, filename, abs_filename)
if self.abs_filename == user_filename then if self.abs_filename == user_filename or self.abs_filename == module_filename then
core.reload_module("core.style") reload_customizations()
core.load_user_directory() rescan_project_directories()
configure_borderless_window()
end end
end end
end end
@ -656,6 +918,8 @@ function core.init()
core.blink_timer = core.blink_start core.blink_timer = core.blink_start
local project_dir_abs = system.absolute_path(project_dir) 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) local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs)
if set_project_ok then if set_project_ok then
if project_dir_explicit then if project_dir_explicit then
@ -702,6 +966,9 @@ function core.init()
end end
local got_project_error = not core.load_project_module() 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 -- We assume we have just a single project directory here. Now that StatusView
-- is there show max files warning if needed. -- is there show max files warning if needed.
if core.project_directories[1].files_limit then if core.project_directories[1].files_limit then
@ -720,9 +987,7 @@ function core.init()
command.perform("core:open-log") command.perform("core:open-log")
end end
system.set_window_bordered(not config.borderless) configure_borderless_window()
core.title_view:configure_hit_test(config.borderless)
core.title_view.visible = config.borderless
if #plugins_refuse_list.userdir.plugins > 0 or #plugins_refuse_list.datadir.plugins > 0 then if #plugins_refuse_list.userdir.plugins > 0 or #plugins_refuse_list.datadir.plugins > 0 then
local opt = { local opt = {
@ -746,7 +1011,7 @@ function core.init()
end) end)
end end
reload_on_user_module_save() add_config_files_hooks()
end end

View File

@ -16,6 +16,7 @@ local NagView = View:extend()
function NagView:new() function NagView:new()
NagView.super.new(self) NagView.super.new(self)
self.size.y = 0 self.size.y = 0
self.show_height = 0
self.force_focus = false self.force_focus = false
self.queue = {} self.queue = {}
end end
@ -50,16 +51,16 @@ function NagView:update()
NagView.super.update(self) NagView.super.update(self)
if core.active_view == self and self.title then if core.active_view == self and self.title then
self:move_towards(self.size, "y", self:get_target_height()) self:move_towards(self, "show_height", self:get_target_height())
self:move_towards(self, "underline_progress", 1) self:move_towards(self, "underline_progress", 1)
else else
self:move_towards(self.size, "y", 0) self:move_towards(self, "show_height", 0)
end end
end end
function NagView:draw_overlay() function NagView:dim_window_content()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
oy = oy + self.size.y oy = oy + self.show_height
local w, h = core.root_view.size.x, core.root_view.size.y - oy local w, h = core.root_view.size.x, core.root_view.size.y - oy
core.root_view:defer_draw(function() core.root_view:defer_draw(function()
renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) renderer.draw_rect(ox, oy, w, h, style.nagbar_dim)
@ -81,7 +82,7 @@ function NagView:each_option()
bh = self:get_buttons_height() bh = self:get_buttons_height()
ox,oy = self:get_content_offset() ox,oy = self:get_content_offset()
ox = ox + self.size.x ox = ox + self.size.x
oy = oy + self.size.y - bh - style.padding.y oy = oy + self.show_height - bh - style.padding.y
for i = #self.options, 1, -1 do for i = #self.options, 1, -1 do
opt = self.options[i] opt = self.options[i]
@ -103,13 +104,38 @@ function NagView:on_mouse_moved(mx, my, ...)
end end
end end
-- Used to store saved value for RootView.on_view_mouse_pressed
local on_view_mouse_pressed
local function capture_mouse_pressed(nag_view)
-- RootView is loaded locally to avoid NagView and RootView being
-- mutually recursive
local RootView = require "core.rootview"
on_view_mouse_pressed = RootView.on_view_mouse_pressed
RootView.on_view_mouse_pressed = function(button, x, y, clicks)
local handled = NagView.on_mouse_pressed(nag_view, button, x, y, clicks)
return handled or on_view_mouse_pressed(button, x, y, clicks)
end
end
local function release_mouse_pressed()
local RootView = require "core.rootview"
if on_view_mouse_pressed then
RootView.on_view_mouse_pressed = on_view_mouse_pressed
on_view_mouse_pressed = nil
end
end
function NagView:on_mouse_pressed(button, mx, my, clicks) function NagView:on_mouse_pressed(button, mx, my, clicks)
if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end
for i, _, x,y,w,h in self:each_option() do for i, _, x,y,w,h in self:each_option() do
if mx >= x and my >= y and mx < x + w and my < y + h then if mx >= x and my >= y and mx < x + w and my < y + h then
self:change_hovered(i) self:change_hovered(i)
command.perform "dialog:select" command.perform "dialog:select"
break return true
end end
end end
end end
@ -123,19 +149,21 @@ function NagView:on_text_input(text)
end end
function NagView:draw() local function draw_nagview_message(self)
if self.size.y <= 0 or not self.title then return end if self.show_height <= 0 or not self.title then return end
self:draw_overlay() self:dim_window_content()
self:draw_background(style.nagbar)
-- draw message's background
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
renderer.draw_rect(ox, oy, self.size.x, self.show_height, style.nagbar)
ox = ox + style.padding.x ox = ox + style.padding.x
-- if there are other items, show it -- if there are other items, show it
if #self.queue > 0 then if #self.queue > 0 then
local str = string.format("[%d]", #self.queue) local str = string.format("[%d]", #self.queue)
ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.size.y) ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.show_height)
ox = ox + style.padding.x ox = ox + style.padding.x
end end
@ -170,6 +198,10 @@ function NagView:draw()
end end
end end
function NagView:draw()
core.root_view:defer_draw(draw_nagview_message, self)
end
function NagView:get_message_height() function NagView:get_message_height()
local h = 0 local h = 0
for str in string.gmatch(self.message, "(.-)\n") do for str in string.gmatch(self.message, "(.-)\n") do
@ -195,6 +227,12 @@ function NagView:next()
self.force_focus = self.message ~= nil self.force_focus = self.message ~= nil
core.set_active_view(self.message ~= nil and self or core.set_active_view(self.message ~= nil and self or
core.next_active_view or core.last_active_view) core.next_active_view or core.last_active_view)
if self.message ~= nil and self then
-- We add a hook to manage all the mouse_pressed events.
capture_mouse_pressed(self)
else
release_mouse_pressed()
end
end end
function NagView:show(title, message, options, on_select) function NagView:show(title, message, options, on_select)

View File

@ -45,6 +45,7 @@ function TreeView:new()
self.item_icon_width = 0 self.item_icon_width = 0
self.item_text_spacing = 0 self.item_text_spacing = 0
self:add_core_hooks() self:add_core_hooks()
end end
@ -95,7 +96,7 @@ function TreeView:get_cached(dir, item, dirname)
end end
t.name = basename t.name = basename
t.type = item.type t.type = item.type
t.dir = dir -- points to top level "dir" item t.dir_name = dir.name -- points to top level "dir" item
dir_cache[cache_name] = t dir_cache[cache_name] = t
end end
return t return t
@ -233,11 +234,14 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
if keymap.modkeys["ctrl"] and button == "left" then if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item) create_directory_in(hovered_item)
else else
hovered_item.expanded = not hovered_item.expanded local hovered_dir = core.project_dir_by_name(hovered_item.dir_name)
if hovered_item.dir.files_limit then if hovered_dir and hovered_dir.files_limit then
core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded) if not core.project_subdir_set_show(hovered_dir, hovered_item.filename, not hovered_item.expanded) then
core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded) return
end end
core.update_project_subdir(hovered_dir, hovered_item.filename, not hovered_item.expanded)
end
hovered_item.expanded = not hovered_item.expanded
end end
else else
core.try(function() core.try(function()
@ -451,6 +455,12 @@ function RootView:draw(...)
menu:draw() menu:draw()
end end
local on_quit_project = core.on_quit_project
function core.on_quit_project()
view.cache = {}
on_quit_project()
end
local function is_project_folder(path) local function is_project_folder(path)
return core.project_dir == path return core.project_dir == path
end end

View File

@ -44,9 +44,6 @@
// DMON_ASSERT: // DMON_ASSERT:
// define this to provide your own assert // define this to provide your own assert
// default is 'assert' // default is 'assert'
// DMON_LOG_ERROR:
// define this to provide your own logging mechanism
// default implementation logs to stdout and breaks the program
// DMON_LOG_DEBUG // DMON_LOG_DEBUG
// define this to provide your own extra debug logging mechanism // define this to provide your own extra debug logging mechanism
// default implementation logs to stdout in DEBUG and does nothing in other builds // default implementation logs to stdout in DEBUG and does nothing in other builds
@ -104,10 +101,22 @@ typedef enum dmon_action_t {
DMON_ACTION_MOVE DMON_ACTION_MOVE
} dmon_action; } dmon_action;
typedef enum dmon_error_enum {
DMON_SUCCESS = 0,
DMON_ERROR_WATCH_DIR,
DMON_ERROR_OPEN_DIR,
DMON_ERROR_MONITOR_FAIL,
DMON_ERROR_UNSUPPORTED_SYMLINK,
DMON_ERROR_SUBDIR_LOCATION,
DMON_ERROR_END
} dmon_error;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
DMON_API_DECL const char *dmon_error_str(dmon_error err);
DMON_API_DECL void dmon_init(void); DMON_API_DECL void dmon_init(void);
DMON_API_DECL void dmon_deinit(void); DMON_API_DECL void dmon_deinit(void);
@ -115,7 +124,7 @@ DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir,
void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, void (*watch_cb)(dmon_watch_id watch_id, dmon_action action,
const char* rootdir, const char* filepath, const char* rootdir, const char* filepath,
const char* oldfilepath, void* user), const char* oldfilepath, void* user),
uint32_t flags, void* user_data); uint32_t flags, void* user_data, dmon_error *error_code);
DMON_API_DECL void dmon_unwatch(dmon_watch_id id); DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
#ifdef __cplusplus #ifdef __cplusplus
@ -150,10 +159,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
# define NOMINMAX # define NOMINMAX
# endif # endif
# include <windows.h> # include <windows.h>
# include <intrin.h>
# ifdef _MSC_VER
# pragma intrinsic(_InterlockedExchange)
# endif
#elif DMON_OS_LINUX #elif DMON_OS_LINUX
# ifndef __USE_MISC # ifndef __USE_MISC
# define __USE_MISC # define __USE_MISC
@ -169,6 +174,9 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
# include <time.h> # include <time.h>
# include <unistd.h> # include <unistd.h>
# include <stdlib.h> # include <stdlib.h>
/* Recursive removed for Lite XL when using inotify. */
# define LITE_XL_DISABLE_INOTIFY_RECURSIVE
# define DMON_LOG_DEBUG(s)
#elif DMON_OS_MACOS #elif DMON_OS_MACOS
# include <pthread.h> # include <pthread.h>
# include <CoreServices/CoreServices.h> # include <CoreServices/CoreServices.h>
@ -189,11 +197,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
# define DMON_ASSERT(e) assert(e) # define DMON_ASSERT(e) assert(e)
#endif #endif
#ifndef DMON_LOG_ERROR
# include <stdio.h>
# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0)
#endif
#ifndef DMON_LOG_DEBUG #ifndef DMON_LOG_DEBUG
# ifndef NDEBUG # ifndef NDEBUG
# include <stdio.h> # include <stdio.h>
@ -223,10 +226,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
#include <string.h> #include <string.h>
#ifndef _DMON_LOG_ERRORF
# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0);
#endif
#ifndef _DMON_LOG_DEBUGF #ifndef _DMON_LOG_DEBUGF
# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); # define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0);
#endif #endif
@ -356,6 +355,20 @@ static void * stb__sbgrowf(void *arr, int increment, int itemsize)
// watcher callback (same as dmon.h's decleration) // watcher callback (same as dmon.h's decleration)
typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*);
static const char *dmon__errors[] = {
"Success",
"Error watching directory",
"Error opening directory",
"Error enabling monitoring",
"Error support for symlink disabled",
"Error not a subdirectory",
};
DMON_API_IMPL const char *dmon_error_str(dmon_error err) {
DMON_ASSERT(err >= 0 && err < DMON_ERROR_END);
return dmon__errors[(int) err];
}
#if DMON_OS_WINDOWS #if DMON_OS_WINDOWS
// IOCP (windows) // IOCP (windows)
#ifdef UNICODE #ifdef UNICODE
@ -389,9 +402,11 @@ typedef struct dmon__state {
dmon__watch_state watches[DMON_MAX_WATCHES]; dmon__watch_state watches[DMON_MAX_WATCHES];
HANDLE thread_handle; HANDLE thread_handle;
CRITICAL_SECTION mutex; CRITICAL_SECTION mutex;
volatile LONG modify_watches; volatile int modify_watches;
CRITICAL_SECTION modify_watches_mutex;
dmon__win32_event* events; dmon__win32_event* events;
bool quit; bool quit;
HANDLE wake_event;
} dmon__state; } dmon__state;
static bool _dmon_init; static bool _dmon_init;
@ -474,17 +489,25 @@ _DMON_PRIVATE void dmon__win32_process_events(void)
stb_sb_reset(_dmon.events); stb_sb_reset(_dmon.events);
} }
static int dmon__safe_get_modify_watches() {
EnterCriticalSection(&_dmon.modify_watches_mutex);
const int value = _dmon.modify_watches;
LeaveCriticalSection(&_dmon.modify_watches_mutex);
return value;
}
_DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg)
{ {
_DMON_UNUSED(arg); _DMON_UNUSED(arg);
HANDLE wait_handles[DMON_MAX_WATCHES]; HANDLE wait_handles[DMON_MAX_WATCHES + 1];
SYSTEMTIME starttm; SYSTEMTIME starttm;
GetSystemTime(&starttm); GetSystemTime(&starttm);
uint64_t msecs_elapsed = 0; uint64_t msecs_elapsed = 0;
while (!_dmon.quit) { while (!_dmon.quit) {
if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { if (dmon__safe_get_modify_watches() ||
!TryEnterCriticalSection(&_dmon.mutex)) {
Sleep(10); Sleep(10);
continue; continue;
} }
@ -500,14 +523,17 @@ _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg)
wait_handles[i] = watch->overlapped.hEvent; wait_handles[i] = watch->overlapped.hEvent;
} }
DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); const int n = _dmon.num_watches;
DMON_ASSERT(wait_result != WAIT_FAILED); wait_handles[n] = _dmon.wake_event;
if (wait_result != WAIT_TIMEOUT) { const int n_pending = stb_sb_count(_dmon.events);
DWORD wait_result = WaitForMultipleObjects(n + 1, wait_handles, FALSE, n_pending > 0 ? 10 : INFINITE);
// NOTE: maybe we should check for WAIT_ABANDONED_<n> values if that can happen.
if (wait_result >= WAIT_OBJECT_0 && wait_result < WAIT_OBJECT_0 + n) {
dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0];
DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped));
DWORD bytes; DWORD bytes;
if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { if (HasOverlappedIoCompleted(&watch->overlapped) &&
GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) {
char filepath[DMON_MAX_PATH]; char filepath[DMON_MAX_PATH];
PFILE_NOTIFY_INFORMATION notify; PFILE_NOTIFY_INFORMATION notify;
size_t offset = 0; size_t offset = 0;
@ -566,18 +592,37 @@ DMON_API_IMPL void dmon_init(void)
{ {
DMON_ASSERT(!_dmon_init); DMON_ASSERT(!_dmon_init);
InitializeCriticalSection(&_dmon.mutex); InitializeCriticalSection(&_dmon.mutex);
InitializeCriticalSection(&_dmon.modify_watches_mutex);
_dmon.thread_handle = _dmon.thread_handle =
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL); CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL);
_dmon.wake_event = CreateEvent(NULL, FALSE, FALSE, NULL);
DMON_ASSERT(_dmon.thread_handle); DMON_ASSERT(_dmon.thread_handle);
_dmon_init = true; _dmon_init = true;
} }
static void dmon__enter_critical_wakeup(void) {
EnterCriticalSection(&_dmon.modify_watches_mutex);
_dmon.modify_watches = 1;
if (TryEnterCriticalSection(&_dmon.mutex) == 0) {
SetEvent(_dmon.wake_event);
EnterCriticalSection(&_dmon.mutex);
}
LeaveCriticalSection(&_dmon.modify_watches_mutex);
}
static void dmon__leave_critical_wakeup(void) {
EnterCriticalSection(&_dmon.modify_watches_mutex);
_dmon.modify_watches = 0;
LeaveCriticalSection(&_dmon.modify_watches_mutex);
LeaveCriticalSection(&_dmon.mutex);
}
DMON_API_IMPL void dmon_deinit(void) DMON_API_IMPL void dmon_deinit(void)
{ {
DMON_ASSERT(_dmon_init); DMON_ASSERT(_dmon_init);
_dmon.quit = true; _dmon.quit = true;
dmon__enter_critical_wakeup();
if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { if (_dmon.thread_handle != INVALID_HANDLE_VALUE) {
WaitForSingleObject(_dmon.thread_handle, INFINITE); WaitForSingleObject(_dmon.thread_handle, INFINITE);
CloseHandle(_dmon.thread_handle); CloseHandle(_dmon.thread_handle);
@ -587,7 +632,9 @@ DMON_API_IMPL void dmon_deinit(void)
dmon__unwatch(&_dmon.watches[i]); dmon__unwatch(&_dmon.watches[i]);
} }
dmon__leave_critical_wakeup();
DeleteCriticalSection(&_dmon.mutex); DeleteCriticalSection(&_dmon.mutex);
DeleteCriticalSection(&_dmon.modify_watches_mutex);
stb_sb_free(_dmon.events); stb_sb_free(_dmon.events);
_dmon_init = false; _dmon_init = false;
} }
@ -596,13 +643,12 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, void (*watch_cb)(dmon_watch_id watch_id, dmon_action action,
const char* dirname, const char* filename, const char* dirname, const char* filename,
const char* oldname, void* user), const char* oldname, void* user),
uint32_t flags, void* user_data) uint32_t flags, void* user_data, dmon_error *error_code)
{ {
DMON_ASSERT(watch_cb); DMON_ASSERT(watch_cb);
DMON_ASSERT(rootdir && rootdir[0]); DMON_ASSERT(rootdir && rootdir[0]);
_InterlockedExchange(&_dmon.modify_watches, 1); dmon__enter_critical_wakeup();
EnterCriticalSection(&_dmon.mutex);
DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
@ -630,24 +676,21 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_SIZE; FILE_NOTIFY_CHANGE_SIZE;
watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE);
if (!dmon__refresh_watch(watch)) { if (watch->overlapped.hEvent == INVALID_HANDLE_VALUE ||
!dmon__refresh_watch(watch)) {
dmon__unwatch(watch); dmon__unwatch(watch);
DMON_LOG_ERROR("ReadDirectoryChanges failed"); *error_code = DMON_ERROR_WATCH_DIR;
LeaveCriticalSection(&_dmon.mutex); dmon__leave_critical_wakeup();
_InterlockedExchange(&_dmon.modify_watches, 0);
return dmon__make_id(0); return dmon__make_id(0);
} }
} else { } else {
_DMON_LOG_ERRORF("Could not open: %s", rootdir); *error_code = DMON_ERROR_OPEN_DIR;
LeaveCriticalSection(&_dmon.mutex); dmon__leave_critical_wakeup();
_InterlockedExchange(&_dmon.modify_watches, 0);
return dmon__make_id(0); return dmon__make_id(0);
} }
LeaveCriticalSection(&_dmon.mutex); dmon__leave_critical_wakeup();
_InterlockedExchange(&_dmon.modify_watches, 0);
return dmon__make_id(id); return dmon__make_id(id);
} }
@ -655,8 +698,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
{ {
DMON_ASSERT(id.id > 0); DMON_ASSERT(id.id > 0);
_InterlockedExchange(&_dmon.modify_watches, 1); dmon__enter_critical_wakeup();
EnterCriticalSection(&_dmon.mutex);
int index = id.id - 1; int index = id.id - 1;
DMON_ASSERT(index < _dmon.num_watches); DMON_ASSERT(index < _dmon.num_watches);
@ -667,8 +709,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
} }
--_dmon.num_watches; --_dmon.num_watches;
LeaveCriticalSection(&_dmon.mutex); dmon__leave_critical_wakeup();
_InterlockedExchange(&_dmon.modify_watches, 0);
} }
#elif DMON_OS_LINUX #elif DMON_OS_LINUX
@ -704,12 +745,21 @@ typedef struct dmon__state {
int num_watches; int num_watches;
pthread_t thread_handle; pthread_t thread_handle;
pthread_mutex_t mutex; pthread_mutex_t mutex;
volatile int wait_flag;
pthread_mutex_t wait_flag_mutex;
int wake_event_pipe[2];
bool quit; bool quit;
} dmon__state; } dmon__state;
static bool _dmon_init; static bool _dmon_init;
static dmon__state _dmon; static dmon__state _dmon;
/* Implementation of recursive monitoring was removed on Linux for the Lite XL
* application. It is never used with recent version of Lite XL starting from 2.0.5
* and recursive monitoring with inotify was always problematic and half-broken.
* Do not cover the new calling signature with error_code because not used by
* Lite XL. */
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
_DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask, _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask,
bool followlinks, dmon__watch_state* watch) bool followlinks, dmon__watch_state* watch)
{ {
@ -764,6 +814,7 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m
} }
closedir(dir); closedir(dir);
} }
#endif
_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd) _DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd)
{ {
@ -777,6 +828,7 @@ _DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int
return NULL; return NULL;
} }
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
_DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname) _DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname)
{ {
struct dirent* entry; struct dirent* entry;
@ -809,6 +861,7 @@ _DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char*
} }
closedir(dir); closedir(dir);
} }
#endif
_DMON_PRIVATE void dmon__inotify_process_events(void) _DMON_PRIVATE void dmon__inotify_process_events(void)
{ {
@ -919,6 +972,7 @@ _DMON_PRIVATE void dmon__inotify_process_events(void)
} }
if (ev->mask & IN_CREATE) { if (ev->mask & IN_CREATE) {
# ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
if (ev->mask & IN_ISDIR) { if (ev->mask & IN_ISDIR) {
if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) {
char watchdir[DMON_MAX_PATH]; char watchdir[DMON_MAX_PATH];
@ -948,6 +1002,7 @@ _DMON_PRIVATE void dmon__inotify_process_events(void)
ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated
} }
} }
# endif
watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data);
} }
else if (ev->mask & IN_MODIFY) { else if (ev->mask & IN_MODIFY) {
@ -971,6 +1026,13 @@ _DMON_PRIVATE void dmon__inotify_process_events(void)
stb_sb_reset(_dmon.events); stb_sb_reset(_dmon.events);
} }
_DMON_PRIVATE int dmon__safe_get_wait_flag() {
pthread_mutex_lock(&_dmon.wait_flag_mutex);
const int value = _dmon.wait_flag;
pthread_mutex_unlock(&_dmon.wait_flag_mutex);
return value;
}
static void* dmon__thread(void* arg) static void* dmon__thread(void* arg)
{ {
_DMON_UNUSED(arg); _DMON_UNUSED(arg);
@ -986,21 +1048,36 @@ static void* dmon__thread(void* arg)
while (!_dmon.quit) { while (!_dmon.quit) {
nanosleep(&req, &rem); nanosleep(&req, &rem);
if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { if (_dmon.num_watches == 0 ||
dmon__safe_get_wait_flag() ||
pthread_mutex_trylock(&_dmon.mutex) != 0) {
continue; continue;
} }
// Create read FD set // Create read FD set
fd_set rfds; fd_set rfds;
FD_ZERO(&rfds); FD_ZERO(&rfds);
for (int i = 0; i < _dmon.num_watches; i++) { const int n = _dmon.num_watches;
int nfds = 0;
for (int i = 0; i < n; i++) {
dmon__watch_state* watch = &_dmon.watches[i]; dmon__watch_state* watch = &_dmon.watches[i];
FD_SET(watch->fd, &rfds); FD_SET(watch->fd, &rfds);
if (watch->fd > nfds)
nfds = watch->fd;
} }
int wake_fd = _dmon.wake_event_pipe[0];
FD_SET(wake_fd, &rfds);
if (wake_fd > nfds)
nfds = wake_fd;
timeout.tv_sec = 0; timeout.tv_sec = 0;
timeout.tv_usec = 100000; timeout.tv_usec = 100000;
if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { const int n_pending = stb_sb_count(_dmon.events);
if (select(nfds + 1, &rfds, NULL, NULL, n_pending > 0 ? &timeout : NULL)) {
if (FD_ISSET(wake_fd, &rfds)) {
char read_char;
read(wake_fd, &read_char, 1);
}
for (int i = 0; i < _dmon.num_watches; i++) { for (int i = 0; i < _dmon.num_watches; i++) {
dmon__watch_state* watch = &_dmon.watches[i]; dmon__watch_state* watch = &_dmon.watches[i];
if (FD_ISSET(watch->fd, &rfds)) { if (FD_ISSET(watch->fd, &rfds)) {
@ -1050,6 +1127,18 @@ static void* dmon__thread(void* arg)
return 0x0; return 0x0;
} }
_DMON_PRIVATE void dmon__mutex_wakeup_lock(void) {
pthread_mutex_lock(&_dmon.wait_flag_mutex);
_dmon.wait_flag = 1;
if (pthread_mutex_trylock(&_dmon.mutex) != 0) {
char send_char = 1;
write(_dmon.wake_event_pipe[1], &send_char, 1);
pthread_mutex_lock(&_dmon.mutex);
}
_dmon.wait_flag = 0;
pthread_mutex_unlock(&_dmon.wait_flag_mutex);
}
_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) _DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch)
{ {
close(watch->fd); close(watch->fd);
@ -1063,6 +1152,9 @@ DMON_API_IMPL void dmon_init(void)
DMON_ASSERT(!_dmon_init); DMON_ASSERT(!_dmon_init);
pthread_mutex_init(&_dmon.mutex, NULL); pthread_mutex_init(&_dmon.mutex, NULL);
_dmon.wait_flag = 0;
int ret_pipe = pipe(_dmon.wake_event_pipe);
DMON_ASSERT(ret_pipe == 0);
int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL);
_DMON_UNUSED(r); _DMON_UNUSED(r);
DMON_ASSERT(r == 0 && "pthread_create failed"); DMON_ASSERT(r == 0 && "pthread_create failed");
@ -1073,12 +1165,14 @@ DMON_API_IMPL void dmon_deinit(void)
{ {
DMON_ASSERT(_dmon_init); DMON_ASSERT(_dmon_init);
_dmon.quit = true; _dmon.quit = true;
dmon__mutex_wakeup_lock();
pthread_join(_dmon.thread_handle, NULL); pthread_join(_dmon.thread_handle, NULL);
for (int i = 0; i < _dmon.num_watches; i++) { for (int i = 0; i < _dmon.num_watches; i++) {
dmon__unwatch(&_dmon.watches[i]); dmon__unwatch(&_dmon.watches[i]);
} }
pthread_mutex_unlock(&_dmon.mutex);
pthread_mutex_destroy(&_dmon.mutex); pthread_mutex_destroy(&_dmon.mutex);
stb_sb_free(_dmon.events); stb_sb_free(_dmon.events);
_dmon_init = false; _dmon_init = false;
@ -1088,12 +1182,12 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, void (*watch_cb)(dmon_watch_id watch_id, dmon_action action,
const char* dirname, const char* filename, const char* dirname, const char* filename,
const char* oldname, void* user), const char* oldname, void* user),
uint32_t flags, void* user_data) uint32_t flags, void* user_data, dmon_error *error_code)
{ {
DMON_ASSERT(watch_cb); DMON_ASSERT(watch_cb);
DMON_ASSERT(rootdir && rootdir[0]); DMON_ASSERT(rootdir && rootdir[0]);
pthread_mutex_lock(&_dmon.mutex); dmon__mutex_wakeup_lock();
DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
@ -1107,7 +1201,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
struct stat root_st; struct stat root_st;
if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) ||
(root_st.st_mode & S_IRUSR) != S_IRUSR) { (root_st.st_mode & S_IRUSR) != S_IRUSR) {
_DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); *error_code = DMON_ERROR_OPEN_DIR;
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return dmon__make_id(0); return dmon__make_id(0);
} }
@ -1122,8 +1216,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath);
} else { } else {
_DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", *error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
rootdir);
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return dmon__make_id(0); return dmon__make_id(0);
} }
@ -1140,7 +1233,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
watch->fd = inotify_init(); watch->fd = inotify_init();
if (watch->fd < -1) { if (watch->fd < -1) {
DMON_LOG_ERROR("could not create inotify instance"); *error_code = DMON_ERROR_MONITOR_FAIL;
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return dmon__make_id(0); return dmon__make_id(0);
} }
@ -1148,7 +1241,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask);
if (wd < 0) { if (wd < 0) {
_DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); *error_code = DMON_ERROR_WATCH_DIR;
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return dmon__make_id(0); return dmon__make_id(0);
} }
@ -1158,11 +1251,12 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
stb_sb_push(watch->wds, wd); stb_sb_push(watch->wds, wd);
// recursive mode: enumarate all child directories and add them to watch // recursive mode: enumarate all child directories and add them to watch
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
if (flags & DMON_WATCHFLAGS_RECURSIVE) { if (flags & DMON_WATCHFLAGS_RECURSIVE) {
dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask, dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask,
(flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch);
} }
#endif
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return dmon__make_id(id); return dmon__make_id(id);
@ -1172,7 +1266,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
{ {
DMON_ASSERT(id.id > 0); DMON_ASSERT(id.id > 0);
pthread_mutex_lock(&_dmon.mutex); dmon__mutex_wakeup_lock();
int index = id.id - 1; int index = id.id - 1;
DMON_ASSERT(index < _dmon.num_watches); DMON_ASSERT(index < _dmon.num_watches);
@ -1479,7 +1573,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, void (*watch_cb)(dmon_watch_id watch_id, dmon_action action,
const char* dirname, const char* filename, const char* dirname, const char* filename,
const char* oldname, void* user), const char* oldname, void* user),
uint32_t flags, void* user_data) uint32_t flags, void* user_data, dmon_error *error_code)
{ {
DMON_ASSERT(watch_cb); DMON_ASSERT(watch_cb);
DMON_ASSERT(rootdir && rootdir[0]); DMON_ASSERT(rootdir && rootdir[0]);
@ -1499,7 +1593,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
struct stat root_st; struct stat root_st;
if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) ||
(root_st.st_mode & S_IRUSR) != S_IRUSR) { (root_st.st_mode & S_IRUSR) != S_IRUSR) {
_DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); *error_code = DMON_ERROR_OPEN_DIR;
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
__sync_lock_test_and_set(&_dmon.modify_watches, 0); __sync_lock_test_and_set(&_dmon.modify_watches, 0);
return dmon__make_id(0); return dmon__make_id(0);
@ -1514,7 +1608,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath);
} else { } else {
_DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); *error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
__sync_lock_test_and_set(&_dmon.modify_watches, 0); __sync_lock_test_and_set(&_dmon.modify_watches, 0);
return dmon__make_id(0); return dmon__make_id(0);

View File

@ -27,7 +27,7 @@
extern "C" { extern "C" {
#endif #endif
DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir, dmon_error *error_code);
DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir);
#ifdef __cplusplus #ifdef __cplusplus
@ -36,14 +36,15 @@ DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir);
#ifdef DMON_IMPL #ifdef DMON_IMPL
#if DMON_OS_LINUX #if DMON_OS_LINUX
DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir, dmon_error *error_code)
{ {
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle; bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock) if (!skip_lock) {
pthread_mutex_lock(&_dmon.mutex); dmon__mutex_wakeup_lock();
}
dmon__watch_state* watch = &_dmon.watches[id.id - 1]; dmon__watch_state* watch = &_dmon.watches[id.id - 1];
@ -52,6 +53,8 @@ DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
// else, we assume that watchdir is correct, so save it as it is // else, we assume that watchdir is correct, so save it as it is
struct stat st; struct stat st;
dmon__watch_subdir subdir; dmon__watch_subdir subdir;
// FIXME: check if it is a symlink and respect DMON_WATCHFLAGS_FOLLOW_SYMLINKS
// to resolve the link.
if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
@ -62,7 +65,7 @@ DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), watchdir); dmon__strcat(fullpath, sizeof(fullpath), watchdir);
if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); *error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
if (!skip_lock) if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return false; return false;
@ -79,7 +82,7 @@ DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
// check that the directory is not already added // check that the directory is not already added
for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) {
if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) {
_DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); *error_code = DMON_ERROR_SUBDIR_LOCATION;
if (!skip_lock) if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return false; return false;
@ -92,7 +95,7 @@ DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir);
int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask);
if (wd == -1) { if (wd == -1) {
_DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); *error_code = DMON_ERROR_WATCH_DIR;
if (!skip_lock) if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return false; return false;
@ -113,8 +116,9 @@ DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
bool skip_lock = pthread_self() == _dmon.thread_handle; bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock) if (!skip_lock) {
pthread_mutex_lock(&_dmon.mutex); dmon__mutex_wakeup_lock();
}
dmon__watch_state* watch = &_dmon.watches[id.id - 1]; dmon__watch_state* watch = &_dmon.watches[id.id - 1];
@ -137,7 +141,6 @@ DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
} }
} }
if (i >= c) { if (i >= c) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
if (!skip_lock) if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_unlock(&_dmon.mutex);
return false; return false;

View File

@ -1,6 +1,6 @@
project('lite-xl', project('lite-xl',
['c'], ['c'],
version : '2.0.3', version : '2.0.5',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.42', meson_version : '>= 0.42',
default_options : [ default_options : [

View File

@ -560,6 +560,14 @@ static int f_get_file_info(lua_State *L) {
} }
lua_setfield(L, -2, "type"); lua_setfield(L, -2, "type");
#if __linux__
if (S_ISDIR(s.st_mode)) {
if (lstat(path, &s) == 0) {
lua_pushboolean(L, S_ISLNK(s.st_mode));
lua_setfield(L, -2, "symlink");
}
}
#endif
return 1; return 1;
} }
@ -600,6 +608,11 @@ static int f_get_fs_type(lua_State *L) {
lua_pushstring(L, "unknown"); lua_pushstring(L, "unknown");
return 1; return 1;
} }
#else
static int f_return_unknown(lua_State *L) {
lua_pushstring(L, "unknown");
return 1;
}
#endif #endif
@ -794,20 +807,47 @@ static int f_load_native_plugin(lua_State *L) {
static int f_watch_dir(lua_State *L) { static int f_watch_dir(lua_State *L) {
const char *path = luaL_checkstring(L, 1); const char *path = luaL_checkstring(L, 1);
const int recursive = lua_toboolean(L, 2); /* On linux we watch non-recursively and we add/remove each sub-directory explicitly
uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0); * using the function system.watch_dir_add/rm. On other systems we watch recursively
dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL); * and system.watch_dir_add/rm are dummy functions that always returns true. */
if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } #if __linux__
const uint32_t dmon_flags = DMON_WATCHFLAGS_FOLLOW_SYMLINKS;
#elif __APPLE__
const uint32_t dmon_flags = DMON_WATCHFLAGS_FOLLOW_SYMLINKS | DMON_WATCHFLAGS_RECURSIVE;
#else
const uint32_t dmon_flags = DMON_WATCHFLAGS_RECURSIVE;
#endif
dmon_error error;
dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL, &error);
if (watch_id.id == 0) {
lua_pushnil(L);
lua_pushstring(L, dmon_error_str(error));
return 2;
}
lua_pushinteger(L, watch_id.id); lua_pushinteger(L, watch_id.id);
return 1; return 1;
} }
static int f_unwatch_dir(lua_State *L) {
dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1);
dmon_unwatch(watch_id);
return 0;
}
#if __linux__ #if __linux__
static int f_watch_dir_add(lua_State *L) { static int f_watch_dir_add(lua_State *L) {
dmon_watch_id watch_id; dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1); watch_id.id = luaL_checkinteger(L, 1);
const char *subdir = luaL_checkstring(L, 2); const char *subdir = luaL_checkstring(L, 2);
lua_pushboolean(L, dmon_watch_add(watch_id, subdir)); dmon_error error_code;
int success = dmon_watch_add(watch_id, subdir, &error_code);
if (!success) {
lua_pushboolean(L, 0);
lua_pushstring(L, dmon_error_str(error_code));
return 2;
}
lua_pushboolean(L, 1);
return 1; return 1;
} }
@ -818,6 +858,11 @@ static int f_watch_dir_rm(lua_State *L) {
lua_pushboolean(L, dmon_watch_rm(watch_id, subdir)); lua_pushboolean(L, dmon_watch_rm(watch_id, subdir));
return 1; return 1;
} }
#else
static int f_return_true(lua_State *L) {
lua_pushboolean(L, 1);
return 1;
}
#endif #endif
#ifdef _WIN32 #ifdef _WIN32
@ -906,11 +951,16 @@ static const luaL_Reg lib[] = {
{ "set_window_opacity", f_set_window_opacity }, { "set_window_opacity", f_set_window_opacity },
{ "load_native_plugin", f_load_native_plugin }, { "load_native_plugin", f_load_native_plugin },
{ "watch_dir", f_watch_dir }, { "watch_dir", f_watch_dir },
{ "unwatch_dir", f_unwatch_dir },
{ "path_compare", f_path_compare }, { "path_compare", f_path_compare },
#if __linux__ #if __linux__
{ "watch_dir_add", f_watch_dir_add }, { "watch_dir_add", f_watch_dir_add },
{ "watch_dir_rm", f_watch_dir_rm }, { "watch_dir_rm", f_watch_dir_rm },
{ "get_fs_type", f_get_fs_type }, { "get_fs_type", f_get_fs_type },
#else
{ "watch_dir_add", f_return_true },
{ "watch_dir_rm", f_return_true },
{ "get_fs_type", f_return_unknown },
#endif #endif
{ NULL, NULL } { NULL, NULL }
}; };