Fix Project Scanning (#746)

Removed dmon, and replaced with logic that works across Linux, Mac, FreeBSD and Windows. Have tested on all platforms, and seems to work.

Co-authored-by: Jan200101 <sentrycraft123@gmail.com>
This commit is contained in:
Adam 2022-03-06 00:59:22 -05:00 committed by GitHub
parent 6cb403b450
commit f85612e0f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 588 additions and 2544 deletions

View File

@ -154,6 +154,7 @@ See the [licenses] file for details on licenses used by the required dependencie
[Get color themes]: https://github.com/lite-xl/lite-xl-colors
[changelog]: https://github.com/lite-xl/lite-xl/blob/master/changelog.md
[Lite XL plugins repository]: https://github.com/lite-xl/lite-xl-plugins
[plugins repository]: https://github.com/rxi/lite-plugins
[colors repository]: https://github.com/lite-xl/lite-xl-colors
[LICENSE]: LICENSE
[licenses]: licenses/licenses.md

View File

@ -173,7 +173,6 @@ command.add(nil, {
end
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)

220
data/core/dirwatch.lua Normal file
View File

@ -0,0 +1,220 @@
local common = require "core.common"
local config = require "core.config"
local dirwatch = {}
function dirwatch:__index(idx)
local value = rawget(self, idx)
if value ~= nil then return value end
return dirwatch[idx]
end
function dirwatch.new()
local t = {
scanned = {},
watched = {},
reverse_watched = {},
monitor = dirmonitor.new(),
windows_watch_top = nil,
windows_watch_count = 0
}
setmetatable(t, dirwatch)
return t
end
function dirwatch:scan(directory, bool)
if bool == false then return self:unwatch(directory) end
self.scanned[directory] = system.get_file_info(directory).modified
end
-- Should be called on every directory in a subdirectory.
-- In windows, this is a no-op for anything underneath a top-level directory,
-- but code should be called anyway, so we can ensure that we have a proper
-- experience across all platforms.
function dirwatch:watch(directory, bool)
if bool == false then return self:unwatch(directory) end
if not self.watched[directory] and not self.scanned[directory] then
if PLATFORM == "Windows" then
if not self.windows_watch_top or directory:find(self.windows_watch_top, 1, true) ~= 1 then
-- Get the highest level of directory that is common to this directory, and the original.
local target = directory
while self.windows_watch_top and self.windows_watch_top:find(target, 1, true) ~= 1 do
target = common.dirname(target)
end
if target ~= self.windows_watch_top then
local value = self.monitor:watch(target)
if value and value < 0 then
return self:scan(directory)
end
self.windows_watch_top = target
self.windows_watch_count = self.windows_watch_count + 1
end
end
self.watched[directory] = true
else
local value = self.monitor:watch(directory)
-- If for whatever reason, we can't watch this directory, revert back to scanning.
-- Don't bother trying to find out why, for now.
if value and value < 0 then
return self:scan(directory)
end
self.watched[directory] = value
self.reverse_watched[value] = directory
end
end
end
function dirwatch:unwatch(directory)
if self.watched[directory] then
if PLATFORM ~= "Windows" then
self.monitor.unwatch(directory)
self.reverse_watched[self.watched[directory]] = nil
else
self.windows_watch_count = self.windows_watch_count - 1
if self.windows_watch_count == 0 then
self.windows_watch_top = nil
self.monitor.unwatch(directory)
end
end
self.watched[directory] = nil
elseif self.scanned[directory] then
self.scanned[directory] = nil
end
end
-- designed to be run inside a coroutine.
function dirwatch:check(change_callback, scan_time, wait_time)
self.monitor:check(function(id)
if PLATFORM == "Windows" then
change_callback(common.dirname(self.windows_watch_top .. PATHSEP .. id))
elseif self.reverse_watched[id] then
change_callback(self.reverse_watched[id])
end
end)
local start_time = system.get_time()
for directory, old_modified in pairs(self.scanned) do
if old_modified then
local new_modified = system.get_file_info(directory).modified
if old_modified < new_modified then
change_callback(directory)
self.scanned[directory] = new_modified
end
end
if system.get_time() - start_time > scan_time then
coroutine.yield(wait_time)
start_time = system.get_time()
end
end
end
local function strip_leading_path(filename)
return filename:sub(2)
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
local function compare_file(a, b)
return a.filename < b.filename
end
-- compute a file's info entry completed with "filename" to be used
-- in project scan or falsy if it shouldn't appear in the list.
local function get_project_file_info(root, file, ignore_compiled)
local info = system.get_file_info(root .. file)
-- 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)
return fileinfo_pass_filter(info, ignore_compiled) and info
end
end
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string.
-- It will identifies a sub-path within "root.
-- The current path location will therefore always be: root .. path.
-- When recursing "root" will always be the same, only "path" will change.
-- Returns a list of file "items". In eash item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/'.
function dirwatch.get_directory_files(dir, root, path, t, entries_count, recurse_pred)
local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
local ignore_compiled = compile_ignore_files()
for _, file in ipairs(all) do
local info = get_project_file_info(root, path .. PATHSEP .. file, ignore_compiled)
if info then
table.insert(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1
end
end
local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local _, complete, n = dirwatch.get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred)
recurse_complete = recurse_complete and complete
entries_count = n
else
recurse_complete = false
end
end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count
end
return dirwatch

View File

@ -5,6 +5,7 @@ local config = require "core.config"
local style = require "core.style"
local command
local keymap
local dirwatch
local RootView
local StatusView
local TitleView
@ -62,23 +63,6 @@ function core.set_project_dir(new_dir, change_project_fn)
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:
@ -124,134 +108,6 @@ local function strip_trailing_slash(filename)
return filename
end
local function compare_file(a, b)
return a.filename < b.filename
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
compiled[i] = {
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
return compiled
end
local function safe_match(s, pattern)
local ok, match = pcall(string.match, s, pattern)
return ok and match
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 safe_match(test .. "/", compiled.pattern) then
return false
end
else
if safe_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
-- in project scan or falsy if it shouldn't appear in the list.
local function get_project_file_info(root, file, ignore_compiled)
local info = system.get_file_info(root .. file)
-- 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)
return fileinfo_pass_filter(info, ignore_compiled) and info
end
end
-- Predicate function to inhibit directory recursion in get_directory_files
-- based on a time limit and the number of files.
local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
local n_limit = entries_count <= config.max_project_files
local t_limit = t_elapsed < 20 / config.fps
return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
end
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string.
-- It will identifies a sub-path within "root.
-- The current path location will therefore always be: root .. path.
-- When recursing "root" will always be the same, only "path" will change.
-- Returns a list of file "items". In eash item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/'.
local function get_directory_files(dir, root, path, t, ignore_compiled, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end
ignore_compiled = ignore_compiled or compile_ignore_files()
local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
for _, file in ipairs(all) do
local info = get_project_file_info(root, path .. PATHSEP .. file, ignore_compiled)
if info then
table.insert(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1
end
end
local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, ignore_compiled, entries_count, recurse_pred, begin_hook)
recurse_complete = recurse_complete and complete
entries_count = n
else
recurse_complete = false
end
end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count
end
function core.project_subdir_set_show(dir, filename, show)
if dir.files_limit and not dir.force_rescan then
local fullpath = dir.name .. PATHSEP .. filename
if not (show and system.watch_dir_add or system.watch_dir_rm)(dir.watch_id, fullpath) then
return false
end
end
dir.shown_subdir[filename] = show
return true
end
function core.project_subdir_is_shown(dir, filename)
@ -264,8 +120,10 @@ local function show_max_files_warning(dir)
"Filesystem is too slow: project files will not be indexed." or
"Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see "..
"usage.md at github.com/lite-xl/lite-xl."
core.status_view:show_message("!", style.accent, message)
"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
@ -328,28 +186,6 @@ local function files_list_replace(as, i1, n, bs, hook)
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 index, n = 0, #dir.files
for i, file in ipairs(dir.files) do
@ -367,123 +203,148 @@ local function project_subdir_bounds(dir, filename)
end
end
local function rescan_project_subdir(dir, filename_rooted)
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
if filename_rooted ~= "" then
local filename = strip_leading_path(filename_rooted)
index, n = project_subdir_bounds(dir, filename)
end
if not files_list_match(dir.files, index, n, new_files) then
-- Since we are modifying the list of files we may add new directories and
-- when dir.files_limit is false we need to add a watch for each subdirectory.
-- We are therefore passing a insert hook function to the purpose of adding
-- a watch.
-- Note that the hook shold almost never be called, it happens only if
-- we missed some directory creation event from the directory monitoring which
-- almost never happens. With inotify is at least theoretically possible.
local need_subdir_watches = not dir.files_limit and not dir.force_rescan
files_list_replace(dir.files, index, n, new_files, need_subdir_watches and {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
return true
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
local function add_dir_scan_thread(dir)
core.add_thread(function()
while true do
local has_changes = rescan_project_subdir(dir, "")
if has_changes then
core.redraw = true -- we run without an event, from a thread
end
coroutine.yield(5)
end
end, dir)
end
local function folder_add_subdirs_watch(dir)
for _, fileinfo in ipairs(dir.files) do
if fileinfo.type == "dir" then
system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename)
end
end
end
-- Populate a project folder top directory by scanning the filesystem.
local function scan_project_folder(index)
local dir = core.project_directories[index]
local fstype = system.get_fs_type(dir.name)
dir.force_rescan = (fstype == "nfs" or fstype == "fuse")
if not dir.force_rescan then
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, "", {}, 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
dir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
dir.files_limit = true
if core.status_view then -- May be not yet initialized.
show_max_files_warning(dir)
end
end
dir.files = t
if dir.force_rescan then
add_dir_scan_thread(dir)
-- Should be called on any directory that registers a change.
-- Uses relative paths at the project root.
local function refresh_directory(topdir, target, expanded)
local index, n, directory
if target == "" then
index, n = 1, #topdir.files
directory = ""
else
if not dir.files_limit then
folder_add_subdirs_watch(dir)
index, n = project_subdir_bounds(topdir, target)
index = index + 1
n = index + n - 1
directory = (PATHSEP .. target)
end
if index then
local files
local change = false
if topdir.files_limit then
-- If we have the folders literally open on the side panel.
files = expanded and dirwatch.get_directory_files(topdir, topdir.name, directory, {}, 0, core.project_subdir_is_shown) or {}
change = true
else
-- If we're expecting to keep track of everything, go through the list and iteratively deal with directories.
files = dirwatch.get_directory_files(topdir, topdir.name, directory, {}, 0, function() return false end)
end
core.dir_rescan_add_job(dir, ".")
local new_idx, old_idx = 1, index
local new_directories = {}
local last_dir = nil
while old_idx <= n or new_idx <= #files do
local old_info, new_info = topdir.files[old_idx], files[new_idx]
if not new_info or not old_info or not last_dir or old_info.filename:sub(1, #last_dir + 1) ~= last_dir .. "/" then
if not new_info or not old_info or not files_info_equal(new_info, old_info) then
change = true
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)
new_idx = new_idx + 1
old_idx = old_idx + 1
if new_info.type == "dir" then
table.insert(new_directories, new_info)
end
n = n + 1
else
table.remove(topdir.files, old_idx)
if old_info.type == "dir" then
topdir.watch:unwatch(target .. PATHSEP .. old_info.filename)
end
n = n - 1
end
else
new_idx = new_idx + 1
old_idx = old_idx + 1
end
if old_info and old_info.type == "dir" then
last_dir = old_info.filename
end
else
old_idx = old_idx + 1
end
end
for i, v in ipairs(new_directories) do
topdir.watch:watch(target)
if refresh_directory(topdir, target .. PATHSEP .. v.filename) then
change = true
end
end
if change then
core.redraw = true
topdir.is_dirty = true
end
return change
end
end
function core.add_project_directory(path)
-- top directories has a file-like "item" but the item.filename
-- will be simply the name of the directory, without its path.
-- The field item.topdir will identify it as a top level directory.
path = common.normalize_volume(path)
local dir = {
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, dir)
scan_project_folder(#core.project_directories)
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, "", true)
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, "", true) 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, true)
end, 0.01, 0.01)
coroutine.yield(0.05)
end
end)
if path == core.project_dir then
core.project_files = dir.files
core.project_files = topdir.files
end
core.redraw = true
return dir
return topdir
end
@ -496,7 +357,6 @@ local function rescan_project_directories()
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)
@ -520,7 +380,6 @@ local function rescan_project_directories()
-- 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
@ -542,14 +401,10 @@ 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
local index, n, file = project_subdir_bounds(dir, filename)
if index then
local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 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.
local new_files = expanded and dirwatch.get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
files_list_replace(dir.files, index, n, new_files)
dir.is_dirty = true
return true
@ -569,7 +424,7 @@ local function find_files_rec(root, path)
info.filename = strip_leading_path(file)
if info.type == "file" then
coroutine.yield(root, info)
else
elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) then
find_files_rec(root, PATHSEP .. info.filename)
end
end
@ -630,62 +485,6 @@ function core.project_files_number()
end
local function project_dir_by_watch_id(watch_id)
for i = 1, #core.project_directories do
if core.project_directories[i].watch_id == watch_id then
return core.project_directories[i]
end
end
end
local function project_scan_remove_file(dir, filepath)
local fileinfo = { filename = filepath }
for _, filetype in ipairs {"dir", "file"} do
fileinfo.type = filetype
local index, match = file_search(dir.files, fileinfo)
if match then
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)
dir.is_dirty = true
return
end
end
end
local function project_scan_add_file(dir, filepath)
local ignore = compile_ignore_files()
local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath, ignore)
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)
end
end
-- create a directory using mkdir but may need to create the parent
-- directories as well.
local function create_user_directory()
@ -859,6 +658,7 @@ 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"
@ -919,6 +719,8 @@ function core.init()
core.threads = setmetatable({}, { __mode = "k" })
core.blink_start = system.get_time()
core.blink_timer = core.blink_start
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 becaese tha
@ -933,7 +735,9 @@ function core.init()
update_recents_project("remove", project_dir)
end
project_dir_abs = system.absolute_path(".")
if not core.set_project_dir(project_dir_abs) then
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
@ -960,14 +764,12 @@ function core.init()
cur_node = cur_node:split("down", core.status_view, {y = true})
command.add_defaults()
local got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins()
do
local pdir, pname = project_dir_abs:match("(.*)[/\\\\](.*)")
core.log("Opening project %q from directory %s", pname, pdir)
end
local got_project_error = not core.load_project_module()
-- We add the project directory now because the project's module is loaded.
core.add_project_directory(project_dir_abs)
@ -1381,78 +1183,6 @@ function core.has_pending_rescan()
end
end
function core.dir_rescan_add_job(dir, filepath)
local dirpath = filepath:match("^(.+)[/\\].+$")
local dirpath_rooted = dirpath and PATHSEP .. dirpath or ""
local abs_dirpath = dir.name .. dirpath_rooted
if dirpath then
-- check if the directory is in the project files list, if not exit
local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"})
-- Note that is dir_match is false dir_index greaten than the last valid index.
-- We use dir_index to index dir.files below only if dir_match is true.
if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end
end
local new_time = system.get_time() + 1
-- evaluate new rescan request versus existing rescan
local remove_list = {}
for _, rescan in pairs(scheduled_rescan) do
if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then
-- abs_dirpath is a subpath of a scan already ongoing: skip
rescan.time_limit = new_time
return
elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then
-- abs_dirpath already cover this rescan: add to the list of rescan to be removed
table.insert(remove_list, rescan.abs_path)
end
end
for _, key_path in ipairs(remove_list) do
scheduled_rescan[key_path] = nil
end
scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time}
core.add_thread(function()
while true do
local rescan = scheduled_rescan[abs_dirpath]
if not rescan then return end
if system.get_time() > rescan.time_limit then
local has_changes = rescan_project_subdir(rescan.dir, rescan.path)
if has_changes then
core.redraw = true -- we run without an event, from a thread
rescan.time_limit = new_time
else
scheduled_rescan[rescan.abs_path] = nil
return
end
end
coroutine.yield(0.2)
end
end)
end
-- no-op but can be overrided by plugins
function core.on_dirmonitor_modify(dir, filepath) end
function core.on_dirmonitor_delete(dir, filepath) end
function core.on_dir_change(watch_id, action, filepath)
local dir = project_dir_by_watch_id(watch_id)
if not dir then return end
core.dir_rescan_add_job(dir, filepath)
if action == "delete" then
project_scan_remove_file(dir, filepath)
core.on_dirmonitor_delete(dir, filepath)
elseif action == "create" then
project_scan_add_file(dir, filepath)
core.on_dirmonitor_modify(dir, filepath);
elseif action == "modify" then
core.on_dirmonitor_modify(dir, filepath);
end
end
function core.on_event(type, ...)
local did_keymap = false
if type == "textinput" then
@ -1494,8 +1224,6 @@ function core.on_event(type, ...)
end
elseif type == "focuslost" then
core.root_view:on_focus_lost(...)
elseif type == "dirchange" then
core.on_dir_change(...)
elseif type == "quit" then
core.quit()
end

View File

@ -236,9 +236,6 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
else
local hovered_dir = core.project_dir_by_name(hovered_item.dir_name)
if hovered_dir and hovered_dir.files_limit then
if not core.project_subdir_set_show(hovered_dir, hovered_item.filename, not hovered_item.expanded) then
return
end
core.update_project_subdir(hovered_dir, hovered_item.filename, not hovered_item.expanded)
end
hovered_item.expanded = not hovered_item.expanded
@ -592,7 +589,7 @@ command.add(function() return view.hovered_item ~= nil end, {
system.exec(string.format("start \"\" %q", hovered_item.abs_filename))
elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", hovered_item.abs_filename))
elseif PLATFORM == "Linux" then
elseif PLATFORM == "Linux" or string.find(PLATFORM, "BSD") then
system.exec(string.format("xdg-open %q", hovered_item.abs_filename))
end
end,

File diff suppressed because it is too large Load Diff

View File

@ -1,165 +0,0 @@
#ifndef __DMON_EXTRA_H__
#define __DMON_EXTRA_H__
//
// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
// License: https://github.com/septag/dmon#license-bsd-2-clause
//
// Extra header functionality for dmon.h for the backend based on inotify
//
// Add/Remove directory functions:
// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take
// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one
// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user
// will be reached. The default maximum is 8192.
// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the
// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched.
// The function dmon_watch_add and dmon_watch_rm are used to this purpose.
//
#ifndef __DMON_H__
#error "Include 'dmon.h' before including this file"
#endif
#ifdef __cplusplus
extern "C" {
#endif
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);
#ifdef __cplusplus
}
#endif
#ifdef DMON_IMPL
#if DMON_OS_LINUX
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);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock) {
dmon__mutex_wakeup_lock();
}
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
// check if the directory exists
// if watchdir contains absolute/root-included path, try to strip the rootdir from it
// else, we assume that watchdir is correct, so save it as it is
struct stat st;
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)) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
}
} else {
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), watchdir);
if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) {
*error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
}
int dirlen = (int)strlen(subdir.rootdir);
if (subdir.rootdir[dirlen - 1] != '/') {
subdir.rootdir[dirlen] = '/';
subdir.rootdir[dirlen + 1] = '\0';
}
// check that the directory is not already added
for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) {
if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) {
*error_code = DMON_ERROR_SUBDIR_LOCATION;
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
}
const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir);
int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask);
if (wd == -1) {
*error_code = DMON_ERROR_WATCH_DIR;
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
stb_sb_push(watch->subdirs, subdir);
stb_sb_push(watch->wds, wd);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
{
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock) {
dmon__mutex_wakeup_lock();
}
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
char subdir[DMON_MAX_PATH];
dmon__strcpy(subdir, sizeof(subdir), watchdir);
if (strstr(subdir, watch->rootdir) == subdir) {
dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir));
}
int dirlen = (int)strlen(subdir);
if (subdir[dirlen - 1] != '/') {
subdir[dirlen] = '/';
subdir[dirlen + 1] = '\0';
}
int i, c = stb_sb_count(watch->subdirs);
for (i = 0; i < c; i++) {
if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) {
break;
}
}
if (i >= c) {
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
inotify_rm_watch(watch->fd, watch->wds[i]);
/* Remove entry from subdirs and wds by swapping position with the last entry */
watch->subdirs[i] = stb_sb_last(watch->subdirs);
stb_sb_pop(watch->subdirs);
watch->wds[i] = stb_sb_last(watch->wds);
stb_sb_pop(watch->wds);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
#endif // DMON_OS_LINUX
#endif // DMON_IMPL
#endif // __DMON_EXTRA_H__

View File

@ -1 +0,0 @@
lite_includes += include_directories('.')

View File

@ -22,33 +22,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## septag/dmon
Copyright 2019 Sepehr Taghdisian. All rights reserved.
https://github.com/septag/dmon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Fira Sans
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.

View File

@ -69,9 +69,6 @@ endif
if not get_option('source-only')
libm = cc.find_library('m', required : false)
libdl = cc.find_library('dl', required : false)
threads_dep = dependency('threads')
lua_fallback = ['lua', 'lua_dep']
lua_quick_fallback = []
if get_option('wrap_mode') == 'forcefallback'
@ -97,7 +94,7 @@ if not get_option('source-only')
default_options: ['default_library=static']
)
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl, threads_dep]
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl]
endif
#===============================================================================
# Install Configuration
@ -151,7 +148,6 @@ configure_file(
)
if not get_option('source-only')
subdir('lib/dmon')
subdir('src')
subdir('scripts')
endif

View File

@ -1,54 +0,0 @@
`core.set_project_dir`:
Reset project directories and set its directory.
It chdir into the directory, empty the `core.project_directories` and add
the given directory.
`core.add_project_directory`:
Add a new top-level directory to the project.
Also called from modules and commands outside core.init.
local function `scan_project_folder`:
Scan all files for a given top-level project directory.
Can emit a warning about file limit.
Called only from within core.init module.
`core.scan_project_subdir`: (before was named `core.scan_project_folder`)
scan a single folder, without recursion. Used when too many files.
Local function `scan_project_folder`:
Populate the project folder top directory. Done only once when the directory
is added to the project.
`core.add_project_directory`:
Add a new top-level folder to the project.
`core.set_project_dir`:
Set the initial project directory.
`core.dir_rescan_add_job`:
Add a job to rescan after an elapsed time a project's subdirectory to fix for any
changes.
Local function `rescan_project_subdir`:
Rescan a project's subdirectory, compare to the current version and patch the list if
a difference is found.
`core.project_scan_thread`:
Should disappear now that we use dmon.
`core.project_scan_topdir`:
New function to scan a top level project folder.
`config.project_scan_rate`:
`core.project_scan_thread_id`:
`core.reschedule_project_scan`:
`core.project_files_limit`:
A eliminer.
`core.get_project_files`:
To be fixed. Use `find_project_files_co` for a single directory
In TreeView remove usage of self.last to detect new scan that changed the files list.

View File

@ -1,20 +1,23 @@
#include "api.h"
int luaopen_system(lua_State *L);
int luaopen_renderer(lua_State *L);
int luaopen_regex(lua_State *L);
int luaopen_process(lua_State *L);
int luaopen_dirmonitor(lua_State* L);
static const luaL_Reg libs[] = {
{ "system", luaopen_system },
{ "renderer", luaopen_renderer },
{ "regex", luaopen_regex },
{ "process", luaopen_process },
{ "system", luaopen_system },
{ "renderer", luaopen_renderer },
{ "regex", luaopen_regex },
{ "process", luaopen_process },
{ "dirmonitor", luaopen_dirmonitor },
{ NULL, NULL }
};
void api_load_libs(lua_State *L) {
for (int i = 0; libs[i].name; i++)
luaL_requiref(L, libs[i].name, libs[i].func, 1);
}

View File

@ -7,6 +7,7 @@
#define API_TYPE_FONT "Font"
#define API_TYPE_PROCESS "Process"
#define API_TYPE_DIRMONITOR "Dirmonitor"
#define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))

203
src/api/dirmonitor.c Normal file
View File

@ -0,0 +1,203 @@
#include "api.h"
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#elif __linux__
#include <sys/inotify.h>
#include <limits.h>
#else
#include <sys/event.h>
#endif
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
/*
This is *slightly* a clusterfuck. Normally, we'd
have windows wait on a list of handles like inotify,
however, MAXIMUM_WAIT_OBJECTS is 64. Yes, seriously.
So, for windows, we are recursive.
*/
struct dirmonitor {
int fd;
#if _WIN32
HANDLE handle;
char buffer[8192];
OVERLAPPED overlapped;
bool running;
#endif
};
struct dirmonitor* init_dirmonitor() {
struct dirmonitor* monitor = calloc(sizeof(struct dirmonitor), 1);
#ifndef _WIN32
#if __linux__
monitor->fd = inotify_init1(IN_NONBLOCK);
#else
monitor->fd = kqueue();
#endif
#endif
return monitor;
}
#if _WIN32
static void close_monitor_handle(struct dirmonitor* monitor) {
if (monitor->handle) {
if (monitor->running) {
BOOL result = CancelIoEx(monitor->handle, &monitor->overlapped);
DWORD error = GetLastError();
if (result == TRUE || error != ERROR_NOT_FOUND) {
DWORD bytes_transferred;
GetOverlappedResult( monitor->handle, &monitor->overlapped, &bytes_transferred, TRUE );
}
monitor->running = false;
}
CloseHandle(monitor->handle);
}
monitor->handle = NULL;
}
#endif
void deinit_dirmonitor(struct dirmonitor* monitor) {
#if _WIN32
close_monitor_handle(monitor);
#else
close(monitor->fd);
#endif
free(monitor);
}
int check_dirmonitor(struct dirmonitor* monitor, int (*change_callback)(int, const char*, void*), void* data) {
#if _WIN32
if (!monitor->running) {
if (ReadDirectoryChangesW(monitor->handle, monitor->buffer, sizeof(monitor->buffer), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, NULL, &monitor->overlapped, NULL) == 0)
return GetLastError();
monitor->running = true;
}
DWORD bytes_transferred;
if (!GetOverlappedResult(monitor->handle, &monitor->overlapped, &bytes_transferred, FALSE)) {
int error = GetLastError();
return error == ERROR_IO_PENDING || error == ERROR_IO_INCOMPLETE ? 0 : error;
}
monitor->running = false;
for (FILE_NOTIFY_INFORMATION* info = (FILE_NOTIFY_INFORMATION*)monitor->buffer; (char*)info < monitor->buffer + sizeof(monitor->buffer); info = (FILE_NOTIFY_INFORMATION*)((char*)info) + info->NextEntryOffset) {
change_callback(info->FileNameLength, (char*)info->FileName, data);
if (!info->NextEntryOffset)
break;
}
monitor->running = false;
return 0;
#elif __linux__
char buf[PATH_MAX + sizeof(struct inotify_event)];
while (1) {
ssize_t len = read(monitor->fd, buf, sizeof(buf));
if (len == -1 && errno != EAGAIN)
return errno;
if (len <= 0)
return 0;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + ((struct inotify_event*)ptr)->len)
change_callback(((const struct inotify_event *) ptr)->wd, NULL, data);
}
#else
struct kevent event;
while (1) {
struct timespec tm = {0};
int nev = kevent(monitor->fd, NULL, 0, &event, 1, &tm);
if (nev == -1)
return errno;
if (nev <= 0)
return 0;
change_callback(event.ident, NULL, data);
}
#endif
}
int add_dirmonitor(struct dirmonitor* monitor, const char* path) {
#if _WIN32
close_monitor_handle(monitor);
monitor->handle = CreateFileA(path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
if (monitor->handle && monitor->handle != INVALID_HANDLE_VALUE)
return 1;
monitor->handle = NULL;
return -1;
#elif __linux__
return inotify_add_watch(monitor->fd, path, IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO);
#else
int fd = open(path, O_RDONLY);
struct kevent change;
EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME, 0, (void*)path);
kevent(monitor->fd, &change, 1, NULL, 0, NULL);
return fd;
#endif
}
void remove_dirmonitor(struct dirmonitor* monitor, int fd) {
#if _WIN32
close_monitor_handle(monitor);
#elif __linux__
inotify_rm_watch(monitor->fd, fd);
#else
close(fd);
#endif
}
static int f_check_dir_callback(int watch_id, const char* path, void* L) {
#if _WIN32
char buffer[PATH_MAX*4];
int count = WideCharToMultiByte(CP_UTF8, 0, (WCHAR*)path, watch_id, buffer, PATH_MAX*4 - 1, NULL, NULL);
lua_pushlstring(L, buffer, count);
#else
lua_pushnumber(L, watch_id);
#endif
lua_pcall(L, 1, 1, 0);
int result = lua_toboolean(L, -1);
lua_pop(L, 1);
return !result;
}
static int f_dirmonitor_new(lua_State* L) {
struct dirmonitor** monitor = lua_newuserdata(L, sizeof(struct dirmonitor**));
*monitor = init_dirmonitor();
luaL_setmetatable(L, API_TYPE_DIRMONITOR);
return 1;
}
static int f_dirmonitor_gc(lua_State* L) {
deinit_dirmonitor(*((struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR)));
return 0;
}
static int f_dirmonitor_watch(lua_State *L) {
lua_pushnumber(L, add_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), luaL_checkstring(L, 2)));
return 1;
}
static int f_dirmonitor_unwatch(lua_State *L) {
remove_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), luaL_checknumber(L, 2));
return 0;
}
static int f_dirmonitor_check(lua_State* L) {
lua_pushnumber(L, check_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), f_check_dir_callback, L));
return 1;
}
static const luaL_Reg dirmonitor_lib[] = {
{ "new", f_dirmonitor_new },
{ "__gc", f_dirmonitor_gc },
{ "watch", f_dirmonitor_watch },
{ "unwatch", f_dirmonitor_unwatch },
{ "check", f_dirmonitor_check },
{NULL, NULL}
};
int luaopen_dirmonitor(lua_State* L) {
luaL_newmetatable(L, API_TYPE_DIRMONITOR);
luaL_setfuncs(L, dirmonitor_lib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
return 1;
}

View File

@ -7,7 +7,6 @@
#include <errno.h>
#include <sys/stat.h>
#include "api.h"
#include "../dirmonitor.h"
#include "../rencache.h"
#ifdef _WIN32
#include <direct.h>
@ -255,26 +254,6 @@ top:
lua_pushinteger(L, e.wheel.y);
return 2;
case SDL_USEREVENT:
lua_pushstring(L, "dirchange");
lua_pushinteger(L, e.user.code >> 16);
switch (e.user.code & 0xffff) {
case DMON_ACTION_DELETE:
lua_pushstring(L, "delete");
break;
case DMON_ACTION_CREATE:
lua_pushstring(L, "create");
break;
case DMON_ACTION_MODIFY:
lua_pushstring(L, "modify");
break;
default:
return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff);
}
lua_pushstring(L, e.user.data1);
free(e.user.data1);
return 4;
default:
goto top;
}
@ -592,29 +571,27 @@ static struct f_type_names fs_names[] = {
{ 0x0, NULL },
};
static int f_get_fs_type(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
struct statfs buf;
int status = statfs(path, &buf);
if (status != 0) {
return luaL_error(L, "error calling statfs on %s", path);
}
for (int i = 0; fs_names[i].magic; i++) {
if (fs_names[i].magic == buf.f_type) {
lua_pushstring(L, fs_names[i].name);
return 1;
}
}
lua_pushstring(L, "unknown");
return 1;
}
#else
static int f_return_unknown(lua_State *L) {
lua_pushstring(L, "unknown");
return 1;
}
#endif
static int f_get_fs_type(lua_State *L) {
#if __linux__
const char *path = luaL_checkstring(L, 1);
struct statfs buf;
int status = statfs(path, &buf);
if (status != 0) {
return luaL_error(L, "error calling statfs on %s", path);
}
for (int i = 0; fs_names[i].magic; i++) {
if (fs_names[i].magic == buf.f_type) {
lua_pushstring(L, fs_names[i].name);
return 1;
}
}
#endif
lua_pushstring(L, "unknown");
return 1;
}
static int f_mkdir(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
@ -815,66 +792,6 @@ static int f_load_native_plugin(lua_State *L) {
return result;
}
static int f_watch_dir(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
/* On linux we watch non-recursively and we add/remove each sub-directory explicitly
* using the function system.watch_dir_add/rm. On other systems we watch recursively
* and system.watch_dir_add/rm are dummy functions that always returns true. */
#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);
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__
static int f_watch_dir_add(lua_State *L) {
dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1);
const char *subdir = luaL_checkstring(L, 2);
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;
}
static int f_watch_dir_rm(lua_State *L) {
dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1);
const char *subdir = luaL_checkstring(L, 2);
lua_pushboolean(L, dmon_watch_rm(watch_id, subdir));
return 1;
}
#else
static int f_return_true(lua_State *L) {
lua_pushboolean(L, 1);
return 1;
}
#endif
#ifdef _WIN32
#define PATHSEP '\\'
#else
@ -961,18 +878,8 @@ static const luaL_Reg lib[] = {
{ "fuzzy_match", f_fuzzy_match },
{ "set_window_opacity", f_set_window_opacity },
{ "load_native_plugin", f_load_native_plugin },
{ "watch_dir", f_watch_dir },
{ "unwatch_dir", f_unwatch_dir },
{ "path_compare", f_path_compare },
#if __linux__
{ "watch_dir_add", f_watch_dir_add },
{ "watch_dir_rm", f_watch_dir_rm },
{ "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
{ NULL, NULL }
};

View File

@ -1,59 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <SDL.h>
#define DMON_IMPL
#include "dmon.h"
#include "dmon_extra.h"
#include "dirmonitor.h"
static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) {
SDL_Event ev;
const int size = strlen(filepath) + 1;
/* The string allocated below should be deallocated as soon as the event is
treated in the SDL main loop. */
char *new_filepath = malloc(size);
if (!new_filepath) return;
memcpy(new_filepath, filepath, size);
#ifdef _WIN32
for (int i = 0; i < size; i++) {
if (new_filepath[i] == '/') {
new_filepath[i] = '\\';
}
}
#endif
SDL_zero(ev);
ev.type = SDL_USEREVENT;
ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff);
ev.user.data1 = new_filepath;
SDL_PushEvent(&ev);
}
void dirmonitor_init() {
dmon_init();
/* In theory we should register our user event but since we
have just one type of user event this is not really needed. */
/* sdl_dmon_event_type = SDL_RegisterEvents(1); */
}
void dirmonitor_deinit() {
dmon_deinit();
}
void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
const char *filepath, const char *oldfilepath, void *user)
{
(void) rootdir;
(void) user;
switch (action) {
case DMON_ACTION_MOVE:
send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath);
send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath);
break;
default:
send_sdl_event(watch_id, action, filepath);
}
}

View File

@ -1,15 +0,0 @@
#ifndef DIRMONITOR_H
#define DIRMONITOR_H
#include <stdint.h>
#include "dmon.h"
#include "dmon_extra.h"
void dirmonitor_init();
void dirmonitor_deinit();
void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
const char *filepath, const char *oldfilepath, void *user);
#endif

View File

@ -7,15 +7,13 @@
#ifdef _WIN32
#include <windows.h>
#elif __linux__
#elif __linux__ || __FreeBSD__
#include <unistd.h>
#include <signal.h>
#elif __APPLE__
#include <mach-o/dyld.h>
#endif
#include "dirmonitor.h"
SDL_Window *window;
@ -108,8 +106,6 @@ int main(int argc, char **argv) {
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm);
dirmonitor_init();
window = SDL_CreateWindow(
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
@ -192,7 +188,6 @@ init_lua:
lua_close(L);
ren_free_window_resources();
dirmonitor_deinit();
return EXIT_SUCCESS;
}

View File

@ -1,10 +1,10 @@
lite_sources = [
'api/api.c',
'api/dirmonitor.c',
'api/renderer.c',
'api/regex.c',
'api/system.c',
'api/process.c',
'dirmonitor.c',
'renderer.c',
'renwindow.c',
'rencache.c',