diff --git a/changelog.md b/changelog.md index c81c7dbe..fef160b6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,55 @@ 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 Replace periodic rescan of project folder with a notification based system using the diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index ad0d4b10..524352fc 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -10,8 +10,18 @@ local restore_title_view = false local function suggest_directory(text) text = common.home_expand(text) - return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir))) - and core.recent_projects or common.dir_path_suggest(text)) + local basedir = common.dirname(core.project_dir) + 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 command.add(nil, { @@ -141,46 +151,51 @@ command.add(nil, { end, ["core:open-project-module"] = function() - local filename = ".lite_project.lua" - if system.get_file_info(filename) then - core.root_view:open_doc(core.open_doc(filename)) - else - local doc = core.open_doc() - core.root_view:open_doc(doc) - doc:save(filename) + if not system.get_file_info(".lite_project.lua") then + core.try(core.write_init_project_module, ".lite_project.lua") end + local doc = core.open_doc(".lite_project.lua") + core.root_view:open_doc(doc) + doc:save() end, ["core:change-project-folder"] = function() local dirname = common.dirname(core.project_dir) if dirname then - core.command_view:set_text(common.home_encode(dirname)) + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) end - core.command_view:enter("Change Project Folder", function(text, item) - text = system.absolute_path(common.home_expand(item and item.text or text)) - if text == core.project_dir then return end - local path_stat = system.get_file_info(text) - if not path_stat or path_stat.type ~= 'dir' then - core.error("Cannot open folder %q", text) + core.command_view:enter("Change Project Folder", function(text) + local path = common.home_expand(text) + local abs_path = check_directory_path(path) + if not abs_path then + core.error("Cannot open directory %q", path) return 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, ["core:open-project-folder"] = function() local dirname = common.dirname(core.project_dir) if dirname then - core.command_view:set_text(common.home_encode(dirname)) + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) end - core.command_view:enter("Open Project", function(text, item) - text = common.home_expand(item and item.text or text) - local path_stat = system.get_file_info(text) - if not path_stat or path_stat.type ~= 'dir' then - core.error("Cannot open folder %q", text) + core.command_view:enter("Open Project", function(text) + local path = common.home_expand(text) + local abs_path = check_directory_path(path) + if not abs_path then + core.error("Cannot open directory %q", path) return 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, diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index b57c7adc..d53e3638 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -489,7 +489,7 @@ local commands = { end for i,docview in ipairs(core.get_views_referencing_doc(doc())) do 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 os.remove(filename) core.log("Removed \"%s\"", filename) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 2c27fd31..ab4b18a0 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -84,6 +84,8 @@ function Doc:save(filename, abs_filename) assert(self.filename, "no filename set to default to") filename = self.filename abs_filename = self.abs_filename + else + assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path") end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do diff --git a/data/core/init.lua b/data/core/init.lua index 91fc59e8..743d6ca1 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -58,20 +58,56 @@ function core.set_project_dir(new_dir, change_project_fn) if change_project_fn then change_project_fn() end core.project_dir = common.normalize_volume(new_dir) core.project_directories = {} - core.add_project_directory(new_dir) - return true 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 function core.open_folder_project(dir_path_abs) if core.set_project_dir(dir_path_abs, core.on_quit_project) then core.root_view:close_all_docviews() + reload_customizations() update_recents_project("add", dir_path_abs) - if not core.load_project_module() then - command.perform("core:open-log") - end + core.add_project_directory(dir_path_abs) core.on_enter_project(dir_path_abs) end end @@ -93,15 +129,57 @@ local function compare_file(a, b) 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 -- 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) - 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) - return (info.size < config.file_size_limit * 1e6 and - not common.match_pattern(common.basename(info.filename), config.ignore_files) - and info) + return fileinfo_pass_filter(info, ignore_compiled) and info end end @@ -123,15 +201,16 @@ end -- When recursing "root" will always be the same, only "path" will change. -- Returns a list of file "items". In eash item the "filename" will be the -- complete file path relative to "root" *without* the trailing '/'. -local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook) +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) + 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 @@ -143,7 +222,7 @@ local function get_directory_files(dir, root, path, t, entries_count, recurse_pr for _, f in ipairs(dirs) do table.insert(t, f) if recurse_pred(dir, f.filename, entries_count, t_elapsed) then - local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook) + 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 @@ -161,15 +240,14 @@ end function core.project_subdir_set_show(dir, filename, show) - dir.shown_subdir[filename] = show - if dir.files_limit and PLATFORM == "Linux" then + if dir.files_limit and not dir.force_rescan then local fullpath = dir.name .. PATHSEP .. filename - local watch_fn = show and system.watch_dir_add or system.watch_dir_rm - local success = watch_fn(dir.watch_id, fullpath) - if not success then - core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") + 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 @@ -209,15 +287,6 @@ local function file_search(files, info) 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) return a.filename == b.filename and a.type == b.type end @@ -234,7 +303,7 @@ local function files_list_match(a, i1, n, b) end -- 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 i, j = 1, 1 while i <= m or i <= n do @@ -244,7 +313,9 @@ local function files_list_replace(as, i1, n, bs) then table.insert(as, i1 + i, b) 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 + if hook and hook.remove then hook.remove(as[i1 + i]) end table.remove(as, i1 + i) n = n - 1 else @@ -253,6 +324,29 @@ local function files_list_replace(as, i1, n, bs) 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 index, n = 0, #dir.files for i, file in ipairs(dir.files) do @@ -271,7 +365,7 @@ local function project_subdir_bounds(dir, filename) end local function rescan_project_subdir(dir, filename_rooted) - local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield) + local 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) @@ -279,7 +373,19 @@ local function rescan_project_subdir(dir, filename_rooted) end 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 return true end @@ -295,38 +401,62 @@ local function add_dir_scan_thread(dir) end coroutine.yield(5) end - 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] - if PLATFORM == "Linux" then - local fstype = system.get_fs_type(dir.name) - dir.force_rescan = (fstype == "nfs" or fstype == "fuse") + 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, "", {}, 0, timed_max_files_pred) + 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 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. show_max_files_warning(dir) end - else - if not dir.force_rescan then - dir.watch_id = system.watch_dir(dir.name, true) - end end dir.files = t if dir.force_rescan then add_dir_scan_thread(dir) else + if not dir.files_limit then + folder_add_subdirs_watch(dir) + end core.dir_rescan_add_job(dir, ".") end end @@ -350,13 +480,73 @@ function core.add_project_directory(path) core.project_files = dir.files end core.redraw = true + return dir +end + + +-- The function below is needed to reload the project directories +-- when the project's module changes. +local function rescan_project_directories() + local save_project_dirs = {} + local n = #core.project_directories + for i = 1, n do + local dir = core.project_directories[i] + save_project_dirs[i] = {name = dir.name, shown_subdir = dir.shown_subdir} + end + core.close_current_project() -- ensure we unwatch directories + core.project_directories = {} + for i = 1, n do -- add again the directories in the project + local dir = core.add_project_directory(save_project_dirs[i].name) + if dir.files_limit then + -- 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 function core.update_project_subdir(dir, filename, expanded) + assert(dir.files_limit, "function should be called only when directory is in files limit mode") local index, n, file = project_subdir_bounds(dir, filename) if index then - local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {} + 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) dir.is_dirty = true return true @@ -452,6 +642,21 @@ local function project_scan_remove_file(dir, filepath) 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 @@ -461,13 +666,18 @@ end local function project_scan_add_file(dir, filepath) - for fragment in string.gmatch(filepath, "([^/\\]+)") do - if common.match_pattern(fragment, config.ignore_files) then - return - end - end - local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) + 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 @@ -548,6 +758,48 @@ local style = require "core.style" 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 = {"^%.", } + +-- Patterns are normally applied to the file's or directory's name, without +-- its path. See below about how to apply filters on a path. +-- +-- Here some examples: +-- +-- "^%." match any file of directory whose basename begins with a dot. +-- +-- When there is an '/' or a '/$' at the end the pattern it will only match +-- directories. When using such a pattern a final '/' will be added to the name +-- of any directory entry before checking if it matches. +-- +-- "^%.git/" matches any directory named ".git" anywhere in the project. +-- +-- If a "/" appears anywhere in the pattern except if it appears at the end or +-- is immediately followed by a '$' then the pattern will be applied to the full +-- path of the file or directory. An initial "/" will be prepended to the file's +-- or directory's path to indicate the project's root. +-- +-- "^/node_modules/" will match a directory named "node_modules" at the project's root. +-- "^/build.*/" match any top level directory whose name begins with "build" +-- "^/subprojects/.+/" match any directory inside a top-level folder named "subprojects". + +-- You may activate some plugins on a pre-project base to override the user's settings. +-- config.plugins.trimwitespace = true +]]) + init_file:close() +end + function core.load_user_directory() return core.try(function() @@ -577,15 +829,25 @@ function core.remove_project_directory(path) return false 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() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") function Doc:save(filename, abs_filename) + local module_filename = system.absolute_path(".lite_project.lua") doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename then - core.reload_module("core.style") - core.load_user_directory() + if self.abs_filename == user_filename or self.abs_filename == module_filename then + reload_customizations() + rescan_project_directories() + configure_borderless_window() end end end @@ -656,6 +918,8 @@ function core.init() core.blink_timer = core.blink_start local project_dir_abs = system.absolute_path(project_dir) + -- We prevent set_project_dir below to effectively add and scan the directory becaese tha + -- project module and its ignore files is not yet loaded. local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) if set_project_ok then if project_dir_explicit then @@ -702,6 +966,9 @@ function core.init() end local got_project_error = not core.load_project_module() + -- We add the project directory now because the project's module is loaded. + core.add_project_directory(project_dir_abs) + -- We assume we have just a single project directory here. Now that StatusView -- is there show max files warning if needed. if core.project_directories[1].files_limit then @@ -720,9 +987,7 @@ function core.init() command.perform("core:open-log") end - system.set_window_bordered(not config.borderless) - core.title_view:configure_hit_test(config.borderless) - core.title_view.visible = config.borderless + configure_borderless_window() if #plugins_refuse_list.userdir.plugins > 0 or #plugins_refuse_list.datadir.plugins > 0 then local opt = { @@ -746,7 +1011,7 @@ function core.init() end) end - reload_on_user_module_save() + add_config_files_hooks() end diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 3d448cd4..fca6c306 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -16,6 +16,7 @@ local NagView = View:extend() function NagView:new() NagView.super.new(self) self.size.y = 0 + self.show_height = 0 self.force_focus = false self.queue = {} end @@ -50,16 +51,16 @@ function NagView:update() NagView.super.update(self) 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) else - self:move_towards(self.size, "y", 0) + self:move_towards(self, "show_height", 0) end end -function NagView:draw_overlay() +function NagView:dim_window_content() 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 core.root_view:defer_draw(function() renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) @@ -81,7 +82,7 @@ function NagView:each_option() bh = self:get_buttons_height() ox,oy = self:get_content_offset() 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 opt = self.options[i] @@ -103,13 +104,38 @@ function NagView:on_mouse_moved(mx, my, ...) 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) - 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 if mx >= x and my >= y and mx < x + w and my < y + h then self:change_hovered(i) command.perform "dialog:select" - break + return true end end end @@ -123,19 +149,21 @@ function NagView:on_text_input(text) end -function NagView:draw() - if self.size.y <= 0 or not self.title then return end +local function draw_nagview_message(self) + if self.show_height <= 0 or not self.title then return end - self:draw_overlay() - self:draw_background(style.nagbar) + self:dim_window_content() + -- draw message's background 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 -- if there are other items, show it if #self.queue > 0 then 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 end @@ -170,6 +198,10 @@ function NagView:draw() end end +function NagView:draw() + core.root_view:defer_draw(draw_nagview_message, self) +end + function NagView:get_message_height() local h = 0 for str in string.gmatch(self.message, "(.-)\n") do @@ -195,6 +227,12 @@ function NagView:next() self.force_focus = self.message ~= nil core.set_active_view(self.message ~= nil and self or 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 function NagView:show(title, message, options, on_select) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 4d1207f2..6a48dfe2 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -45,6 +45,7 @@ function TreeView:new() self.item_icon_width = 0 self.item_text_spacing = 0 + self:add_core_hooks() end @@ -95,7 +96,7 @@ function TreeView:get_cached(dir, item, dirname) end t.name = basename 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 end return t @@ -233,11 +234,14 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) if keymap.modkeys["ctrl"] and button == "left" then create_directory_in(hovered_item) else - hovered_item.expanded = not hovered_item.expanded - if hovered_item.dir.files_limit then - core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded) - core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded) + 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 end else core.try(function() @@ -451,6 +455,12 @@ function RootView:draw(...) menu:draw() 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) return core.project_dir == path end diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 2bc9e0c3..84258bad 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -44,9 +44,6 @@ // DMON_ASSERT: // define this to provide your own 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 // define this to provide your own extra debug logging mechanism // 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; +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 extern "C" { #endif +DMON_API_DECL const char *dmon_error_str(dmon_error err); + DMON_API_DECL void dmon_init(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, const char* rootdir, const char* filepath, 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); #ifdef __cplusplus @@ -150,10 +159,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # define NOMINMAX # endif # include -# include -# ifdef _MSC_VER -# pragma intrinsic(_InterlockedExchange) -# endif #elif DMON_OS_LINUX # ifndef __USE_MISC # define __USE_MISC @@ -169,6 +174,9 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # include # include # include +/* Recursive removed for Lite XL when using inotify. */ +# define LITE_XL_DISABLE_INOTIFY_RECURSIVE +# define DMON_LOG_DEBUG(s) #elif DMON_OS_MACOS # include # include @@ -189,11 +197,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # define DMON_ASSERT(e) assert(e) #endif -#ifndef DMON_LOG_ERROR -# include -# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) -#endif - #ifndef DMON_LOG_DEBUG # ifndef NDEBUG # include @@ -223,10 +226,6 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); #include -#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 # define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); #endif @@ -356,6 +355,20 @@ static void * stb__sbgrowf(void *arr, int increment, int itemsize) // 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*); +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 // IOCP (windows) #ifdef UNICODE @@ -389,9 +402,11 @@ typedef struct dmon__state { dmon__watch_state watches[DMON_MAX_WATCHES]; HANDLE thread_handle; CRITICAL_SECTION mutex; - volatile LONG modify_watches; + volatile int modify_watches; + CRITICAL_SECTION modify_watches_mutex; dmon__win32_event* events; bool quit; + HANDLE wake_event; } dmon__state; static bool _dmon_init; @@ -474,17 +489,25 @@ _DMON_PRIVATE void dmon__win32_process_events(void) 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_UNUSED(arg); - HANDLE wait_handles[DMON_MAX_WATCHES]; + HANDLE wait_handles[DMON_MAX_WATCHES + 1]; SYSTEMTIME starttm; GetSystemTime(&starttm); uint64_t msecs_elapsed = 0; while (!_dmon.quit) { - if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { + if (dmon__safe_get_modify_watches() || + !TryEnterCriticalSection(&_dmon.mutex)) { Sleep(10); continue; } @@ -500,14 +523,17 @@ _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) wait_handles[i] = watch->overlapped.hEvent; } - DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); - DMON_ASSERT(wait_result != WAIT_FAILED); - if (wait_result != WAIT_TIMEOUT) { + const int n = _dmon.num_watches; + wait_handles[n] = _dmon.wake_event; + 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_ 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_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); 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]; PFILE_NOTIFY_INFORMATION notify; size_t offset = 0; @@ -566,18 +592,37 @@ DMON_API_IMPL void dmon_init(void) { DMON_ASSERT(!_dmon_init); InitializeCriticalSection(&_dmon.mutex); + InitializeCriticalSection(&_dmon.modify_watches_mutex); _dmon.thread_handle = 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_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_ASSERT(_dmon_init); _dmon.quit = true; + dmon__enter_critical_wakeup(); if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { WaitForSingleObject(_dmon.thread_handle, INFINITE); CloseHandle(_dmon.thread_handle); @@ -587,7 +632,9 @@ DMON_API_IMPL void dmon_deinit(void) dmon__unwatch(&_dmon.watches[i]); } + dmon__leave_critical_wakeup(); DeleteCriticalSection(&_dmon.mutex); + DeleteCriticalSection(&_dmon.modify_watches_mutex); stb_sb_free(_dmon.events); _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, const char* dirname, const char* filename, 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(rootdir && rootdir[0]); - _InterlockedExchange(&_dmon.modify_watches, 1); - EnterCriticalSection(&_dmon.mutex); + dmon__enter_critical_wakeup(); 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_SIZE; 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_LOG_ERROR("ReadDirectoryChanges failed"); - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + *error_code = DMON_ERROR_WATCH_DIR; + dmon__leave_critical_wakeup(); return dmon__make_id(0); } } else { - _DMON_LOG_ERRORF("Could not open: %s", rootdir); - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + *error_code = DMON_ERROR_OPEN_DIR; + dmon__leave_critical_wakeup(); return dmon__make_id(0); } - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + dmon__leave_critical_wakeup(); return dmon__make_id(id); } @@ -655,8 +698,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) { DMON_ASSERT(id.id > 0); - _InterlockedExchange(&_dmon.modify_watches, 1); - EnterCriticalSection(&_dmon.mutex); + dmon__enter_critical_wakeup(); int index = id.id - 1; DMON_ASSERT(index < _dmon.num_watches); @@ -667,8 +709,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) } --_dmon.num_watches; - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + dmon__leave_critical_wakeup(); } #elif DMON_OS_LINUX @@ -704,12 +745,21 @@ typedef struct dmon__state { int num_watches; pthread_t thread_handle; pthread_mutex_t mutex; + volatile int wait_flag; + pthread_mutex_t wait_flag_mutex; + int wake_event_pipe[2]; bool quit; } dmon__state; static bool _dmon_init; 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, 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); } +#endif _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; } +#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE _DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname) { struct dirent* entry; @@ -809,6 +861,7 @@ _DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* } closedir(dir); } +#endif _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) { +# ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE if (ev->mask & IN_ISDIR) { if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { 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 } } +# endif watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); } else if (ev->mask & IN_MODIFY) { @@ -971,6 +1026,13 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) 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) { _DMON_UNUSED(arg); @@ -986,21 +1048,36 @@ static void* dmon__thread(void* arg) while (!_dmon.quit) { 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; } // Create read FD set fd_set 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]; 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_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++) { dmon__watch_state* watch = &_dmon.watches[i]; if (FD_ISSET(watch->fd, &rfds)) { @@ -1050,6 +1127,18 @@ static void* dmon__thread(void* arg) 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) { close(watch->fd); @@ -1063,6 +1152,9 @@ DMON_API_IMPL void dmon_init(void) DMON_ASSERT(!_dmon_init); 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); _DMON_UNUSED(r); DMON_ASSERT(r == 0 && "pthread_create failed"); @@ -1073,12 +1165,14 @@ DMON_API_IMPL void dmon_deinit(void) { DMON_ASSERT(_dmon_init); _dmon.quit = true; + dmon__mutex_wakeup_lock(); pthread_join(_dmon.thread_handle, NULL); for (int i = 0; i < _dmon.num_watches; i++) { dmon__unwatch(&_dmon.watches[i]); } + pthread_mutex_unlock(&_dmon.mutex); pthread_mutex_destroy(&_dmon.mutex); stb_sb_free(_dmon.events); _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, const char* dirname, const char* filename, 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(rootdir && rootdir[0]); - pthread_mutex_lock(&_dmon.mutex); + dmon__mutex_wakeup_lock(); 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; if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || (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); 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); } 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); return dmon__make_id(0); } @@ -1140,7 +1233,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, watch->fd = inotify_init(); if (watch->fd < -1) { - DMON_LOG_ERROR("could not create inotify instance"); + *error_code = DMON_ERROR_MONITOR_FAIL; pthread_mutex_unlock(&_dmon.mutex); 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; int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); 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); 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); // recursive mode: enumarate all child directories and add them to watch +#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE if (flags & DMON_WATCHFLAGS_RECURSIVE) { dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask, (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); } - +#endif pthread_mutex_unlock(&_dmon.mutex); return dmon__make_id(id); @@ -1172,7 +1266,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) { DMON_ASSERT(id.id > 0); - pthread_mutex_lock(&_dmon.mutex); + dmon__mutex_wakeup_lock(); int index = id.id - 1; 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, const char* dirname, const char* filename, 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(rootdir && rootdir[0]); @@ -1499,7 +1593,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, struct stat root_st; if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || (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); __sync_lock_test_and_set(&_dmon.modify_watches, 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); } 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); __sync_lock_test_and_set(&_dmon.modify_watches, 0); return dmon__make_id(0); diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h index 4b321034..97631520 100644 --- a/lib/dmon/dmon_extra.h +++ b/lib/dmon/dmon_extra.h @@ -27,7 +27,7 @@ extern "C" { #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); #ifdef __cplusplus @@ -36,14 +36,15 @@ DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); #ifdef DMON_IMPL #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); bool skip_lock = pthread_self() == _dmon.thread_handle; - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); + if (!skip_lock) { + dmon__mutex_wakeup_lock(); + } 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 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) { @@ -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__strcat(fullpath, sizeof(fullpath), watchdir); 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) pthread_mutex_unlock(&_dmon.mutex); 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 for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { 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) pthread_mutex_unlock(&_dmon.mutex); 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); int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); 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) pthread_mutex_unlock(&_dmon.mutex); 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; - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); + if (!skip_lock) { + dmon__mutex_wakeup_lock(); + } 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) { - _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; diff --git a/meson.build b/meson.build index 46ddcac8..aec0cdb1 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c'], - version : '2.0.3', + version : '2.0.5', license : 'MIT', meson_version : '>= 0.42', default_options : [ diff --git a/src/api/system.c b/src/api/system.c index cf7232e6..5bd94210 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -560,6 +560,14 @@ static int f_get_file_info(lua_State *L) { } 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; } @@ -600,6 +608,11 @@ static int f_get_fs_type(lua_State *L) { lua_pushstring(L, "unknown"); return 1; } +#else +static int f_return_unknown(lua_State *L) { + lua_pushstring(L, "unknown"); + return 1; +} #endif @@ -794,20 +807,47 @@ static int f_load_native_plugin(lua_State *L) { static int f_watch_dir(lua_State *L) { const char *path = luaL_checkstring(L, 1); - const int recursive = lua_toboolean(L, 2); - uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0); - dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL); - if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } + /* 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); - 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; } @@ -818,6 +858,11 @@ static int f_watch_dir_rm(lua_State *L) { 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 @@ -906,11 +951,16 @@ static const luaL_Reg lib[] = { { "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 } };