From 23f83857c5634a3db3c9945261145d0b9f4f6705 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 24 Nov 2021 05:03:42 +0100 Subject: [PATCH 01/57] Don't search if there are no files --- data/core/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/core/init.lua b/data/core/init.lua index 4a16e6b7..6f3754e4 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -191,6 +191,7 @@ end local function file_search(files, info) local filename, type = info.filename, info.type local inf, sup = 1, #files + if sup <= 0 then return 1, false end while sup - inf > 8 do local curr = math.floor((inf + sup) / 2) if system.path_compare(filename, type, files[curr].filename, files[curr].type) then From e512c576379fd806f83a89ef0a274f6ed95920d7 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 22:35:11 -0500 Subject: [PATCH 02/57] Added an exclusion for lineguide in the commandview. --- data/plugins/lineguide.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 61debbff..41109e4f 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -2,20 +2,22 @@ local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local CommandView = require "core.commandview" local draw_overlay = DocView.draw_overlay function DocView:draw_overlay(...) - local ns = ("n"):rep(config.line_limit) - local ss = self:get_font():subpixel_scale() - local offset = self:get_font():get_width_subpixel(ns) / ss - local x = self:get_line_screen_position(1) + offset - local y = self.position.y - local w = math.ceil(SCALE * 1) - local h = self.size.y - - local color = style.guide or style.selection - renderer.draw_rect(x, y, w, h, color) - + if not self:is(CommandView) then + local ns = ("n"):rep(config.line_limit) + local ss = self:get_font():subpixel_scale() + local offset = self:get_font():get_width_subpixel(ns) / ss + local x = self:get_line_screen_position(1) + offset + local y = self.position.y + local w = math.ceil(SCALE * 1) + local h = self.size.y + + local color = style.guide or style.selection + renderer.draw_rect(x, y, w, h, color) + end draw_overlay(self, ...) end From 405bd1c2bd5c18b32e7a81adf1857897cec42d27 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 26 Nov 2021 13:45:13 +0100 Subject: [PATCH 03/57] Fix logic in project's file insertion The function "file_search" in core.init was sometimes giving a wrong index value, off by one. The problem happened for example when the entry to search was "less than" the first entry, the function returned a value of two instead of one as expected. The bug was easily observed creating a new directory with a name that comes as the first in alphabetical order within the project. --- data/core/init.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 6f3754e4..f5d38ac0 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -191,7 +191,6 @@ end local function file_search(files, info) local filename, type = info.filename, info.type local inf, sup = 1, #files - if sup <= 0 then return 1, false end while sup - inf > 8 do local curr = math.floor((inf + sup) / 2) if system.path_compare(filename, type, files[curr].filename, files[curr].type) then @@ -200,12 +199,12 @@ local function file_search(files, info) inf = curr end end - repeat + while inf <= sup and not system.path_compare(filename, type, files[inf].filename, files[inf].type) do if files[inf].filename == filename then return inf, true end inf = inf + 1 - until inf > sup or system.path_compare(filename, type, files[inf].filename, files[inf].type) + end return inf, false end From 37c00c877a5e21827b00c8f134da7ba7dc507abd Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 20 Dec 2021 11:03:49 +0100 Subject: [PATCH 04/57] Ensure TreeView cache entry is removed on delete Address issue: https://github.com/lite-xl/lite-xl/issues/689 Attempt to provide a more accurate fix to commit: 59f64088e1e88f2f2a29e4d5418ccdf781fdc12e For this latter what happens is that any change inside a directory cause the corresponding entry to be folded in the TreeView. The new change is more accurate because we remove only the stale entry corresponding to the delete event and we do not reset the cache of the parent directory using the modify event. --- data/core/init.lua | 5 +++-- data/plugins/treeview.lua | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index f5d38ac0..9e4676f0 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1164,8 +1164,8 @@ end -- no-op but can be overrided by plugins -function core.on_dirmonitor_modify() -end +function core.on_dirmonitor_modify() end +function core.on_dirmonitor_delete() end function core.on_dir_change(watch_id, action, filepath) @@ -1174,6 +1174,7 @@ function core.on_dir_change(watch_id, action, filepath) 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); diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 77b6732f..2909768c 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -42,6 +42,19 @@ function TreeView:new() self.target_size = default_treeview_size self.cache = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } + self:add_core_hooks() +end + + +function TreeView:add_core_hooks() + -- When a file or directory is deleted we delete the corresponding cache entry + -- because if the entry is recreated we may use wrong information from cache. + local on_delete = core.on_dirmonitor_delete + core.on_dirmonitor_delete = function(dir, filepath) + local cache = self.cache[dir.name] + if cache then cache[filepath] = nil end + on_delete(dir, filepath) + end end From 29318be9c71e1be290e7507e9f8b1c9445aad1b0 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 11 Dec 2021 03:43:33 +0100 Subject: [PATCH 05/57] Consume unmatched character correctly We must consume the whole UTF-8 character, not just a single byte. --- data/core/tokenizer.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index d95baeb1..57c17a0b 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -237,8 +237,13 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- consume character if we didn't match if not matched then - push_token(res, "normal", text:sub(i, i)) - i = i + 1 + local n = 0 + -- reach the next character + while text:byte(i + n + 1) and common.is_utf8_cont(text, i + n + 1) do + n = n + 1 + end + push_token(res, "normal", text:sub(i, i + n)) + i = i + n + 1 end end From 3109263c5d5ca0009c604ad8de1f51ac9d38bb78 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 20 Dec 2021 14:42:48 +0100 Subject: [PATCH 06/57] Call dmon_unwatch when changing project Fix a conspicuous omission to call the dmon_unwatch function when changing project directory. This uncovered a bug or a quirk of the dmon library where the watch_ids can change as a result of calling dmon_unwatch because they are just indexes on a contiguous array. Use a workaround to always unwatch the first valid watch_id N times. --- data/core/commands/core.lua | 5 ++++- data/core/init.lua | 16 ++++++++++++++++ src/api/system.c | 8 ++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index ad0d4b10..16b371f3 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -164,7 +164,10 @@ command.add(nil, { core.error("Cannot open folder %q", text) return end - core.confirm_close_docs(core.docs, core.open_folder_project, text) + core.confirm_close_docs(core.docs, function(dirpath) + core.close_current_project() + core.open_folder_project(dirpath) + end, text) end, suggest_directory) end, diff --git a/data/core/init.lua b/data/core/init.lua index 9e4676f0..928df227 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -64,6 +64,22 @@ function core.set_project_dir(new_dir, change_project_fn) return false end +function core.close_current_project() + -- When using system.unwatch_dir we need to pass the watch_id provided by dmon. + -- In reality when unwatching a directory the dmon library shifts the other watch_id + -- values so the actual watch_id changes. To workaround this problem we assume the + -- first watch_id is always 1 and the watch_id are continguous and we unwatch the + -- first watch_id repeateadly up to the number of watch_ids. + local watch_id_max = 0 + for _, project_dir in ipairs(core.project_directories) do + if project_dir.watch_id and project_dir.watch_id > watch_id_max then + watch_id_max = project_dir.watch_id + end + end + for i = 1, watch_id_max do + system.unwatch_dir(1) + end +end function core.open_folder_project(dir_path_abs) if core.set_project_dir(dir_path_abs, core.on_quit_project) then diff --git a/src/api/system.c b/src/api/system.c index 0ebf78f1..4c14843b 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -723,6 +723,13 @@ static int f_watch_dir(lua_State *L) { 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; @@ -826,6 +833,7 @@ static const luaL_Reg lib[] = { { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, { "watch_dir", f_watch_dir }, + { "unwatch_dir", f_unwatch_dir }, { "path_compare", f_path_compare }, #if __linux__ { "watch_dir_add", f_watch_dir_add }, From 50247fcd929dd931e98de1b4c0c7f2e91fc3fab3 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 20 Dec 2021 16:19:15 +0100 Subject: [PATCH 07/57] Move to 2.0.4 version number --- changelog.md | 11 +++++++++++ meson.build | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index c81c7dbe..06a75b70 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ This files document the changes done in Lite XL for each release. +### 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/meson.build b/meson.build index 613204b7..ccdfdd09 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c', 'cpp'], - version : '2.0.3', + version : '2.0.4', license : 'MIT', meson_version : '>= 0.54', default_options : ['c_std=gnu11', 'cpp_std=c++03'] From 1f0785b73ff0cffd0926c3086baa408a82f01722 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 28 Dec 2021 10:59:01 +0100 Subject: [PATCH 08/57] Scan project folder after project module is loaded Otherwise the initial scan of the project folder is done without taking into account the config.ignore_files directives. --- data/core/init.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 928df227..60c80ec1 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -52,13 +52,15 @@ local function update_recents_project(action, dir_path_abs) end -function core.set_project_dir(new_dir, change_project_fn) +function core.set_project_dir(new_dir, change_project_fn, defer_add) local chdir_ok = pcall(system.chdir, new_dir) if chdir_ok then if change_project_fn then change_project_fn() end core.project_dir = common.normalize_volume(new_dir) core.project_directories = {} - core.add_project_directory(new_dir) + if not defer_add then + core.add_project_directory(new_dir) + end return true end return false @@ -679,7 +681,9 @@ function core.init() core.blink_timer = core.blink_start local project_dir_abs = system.absolute_path(project_dir) - local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) + -- We prevent set_project_dir below to effectively add and scan the directory becaese tha + -- project module and its ignore files is not yet loaded. + local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs, nil, true) if set_project_ok then if project_dir_explicit then update_recents_project("add", project_dir_abs) @@ -689,7 +693,7 @@ 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, nil, true) then system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") os.exit(1) end @@ -726,6 +730,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 From 0f1b84040dd823823c17e925e1ed10380b7737c1 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 28 Dec 2021 12:25:48 +0100 Subject: [PATCH 09/57] No longer store autocomplete options in config Plugins should not store private stuff in core.config because this latter can be reloaded due to user changing preferences. --- data/plugins/autocomplete.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index fde9487e..d324733f 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -10,7 +10,7 @@ local RootView = require "core.rootview" local DocView = require "core.docview" local Doc = require "core.doc" -config.plugins.autocomplete = { +local autocomplete_options = { -- Amount of characters that need to be written for autocomplete min_len = 3, -- The max amount of visible items @@ -192,7 +192,7 @@ local function update_suggestions() -- fuzzy match, remove duplicates and store items = common.fuzzy_match(items, partial) local j = 1 - for i = 1, config.plugins.autocomplete.max_suggestions do + for i = 1, autocomplete_options.max_suggestions do suggestions[i] = items[j] while items[j] and items[i].text == items[j].text do items[i].info = items[i].info or items[j].info @@ -235,7 +235,7 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) end - local ah = config.plugins.autocomplete.max_height + local ah = autocomplete_options.max_height local max_items = #suggestions if max_items > ah then @@ -294,7 +294,7 @@ local function draw_suggestions_box(av) return end - local ah = config.plugins.autocomplete.max_height + local ah = autocomplete_options.max_height -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) @@ -355,7 +355,7 @@ local function show_autocomplete() -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= config.plugins.autocomplete.min_len or triggered_manually then + if #partial >= autocomplete_options.min_len or triggered_manually then update_suggestions() if not triggered_manually then @@ -469,7 +469,7 @@ function autocomplete.complete(completions, on_close) end function autocomplete.can_complete() - if #partial >= config.plugins.autocomplete.min_len then + if #partial >= autocomplete_options.min_len then return true end return false From 05b003eeb523816f4e87991f0fd06be8d1353d1b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 28 Dec 2021 14:32:39 +0100 Subject: [PATCH 10/57] Avoid references to project's dir in TreeView It is not a good practice to keep a reference to the project's directory object outside of the "core" module itself. The TreeView was using such a reference in the cache item for each file or directory entry. Replace the reference to the object with the absolute name of the project directory. --- data/core/init.lua | 9 +++++++++ data/plugins/treeview.lua | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 60c80ec1..aac9a1b2 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -371,6 +371,15 @@ function core.add_project_directory(path) 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) local index, n, file = project_subdir_bounds(dir, filename) if index then diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 2909768c..ffe93ca5 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -92,7 +92,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 @@ -231,9 +231,10 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) 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 + core.update_project_subdir(hovered_dir, hovered_item.filename, hovered_item.expanded) + core.project_subdir_set_show(hovered_dir, hovered_item.filename, hovered_item.expanded) end end else From 2cf3c6f7476d891cf3eb548409c109de315b3c6b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 28 Dec 2021 12:32:25 +0100 Subject: [PATCH 11/57] Ensure project reload when changing project module Changes in project's module required an application restart to work. Now the project will be re-scanned when the project's module changes. In addition ensure borderless window config is changed when changed in user's preferences. --- data/core/init.lua | 62 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index aac9a1b2..481fc431 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -368,6 +368,36 @@ 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) + -- The shown_subdir is only used on linux for very large directories. + -- replay them on the newly scanned project. + for subdir, show in pairs(save_project_dirs[i].shown_subdir) do + for j = 1, #dir.files do + if dir.files[j].filename == subdir then + -- the instructions above match when happens in TreeView:on_mouse_pressed + core.update_project_subdir(dir, subdir, show) + core.project_subdir_set_show(dir, subdir, show) + break + end + end + end + end end @@ -613,15 +643,33 @@ local function whitespace_replacements() 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 reload_customizations() + core.reload_module("core.style") + core.reload_module("core.config") + core.reload_module("core.keymap") + core.load_user_directory() + core.load_project_module() + rescan_project_directories() + configure_borderless_window() +end + + +local function add_config_files_hooks() -- auto-realod style when user's module is saved by overriding Doc:Save() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") + local module_filename = system.absolute_path(".lite_project.lua") function Doc:save(filename, abs_filename) doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename 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() end end end @@ -760,9 +808,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 = { @@ -786,7 +832,7 @@ function core.init() end) end - reload_on_user_module_save() + add_config_files_hooks() end From 8550049db81453ab2437a562b3d793a8a00f8275 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 28 Dec 2021 16:39:40 +0100 Subject: [PATCH 12/57] Draw NagView in overlay mode The NagView takes some actual space in the Y and when it appears it cause the documents' content to be displaced. The movement of the documents' content is annoying and should be avoided so we draw the NagView entirely in overlay mode using defer draw and we always keep its y size to zero to don't affect the other application contents. --- data/core/nagview.lua | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 3d448cd4..35ae9b43 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] @@ -123,19 +124,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 +173,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 From 88ed312f6b6af17788e2aa2d73ccfe51f66d89e7 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Dec 2021 16:00:53 +0100 Subject: [PATCH 13/57] Fix NagView missing mouse events --- data/core/nagview.lua | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 35ae9b43..fca6c306 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -104,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 @@ -202,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) From adaf0235415c6c18fa907467e12a1f614fa2b001 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Dec 2021 08:19:44 +0100 Subject: [PATCH 14/57] Always watch/unwatch subdirectories on all systems Simplifies and uniformize the logic on the Lua side for the setting of directories' watches. Now we always use the methods: systems.watch_dir_add / rm on all the project's directories at any depth when we are not in files limit mode. In files limited mode the functions systems.watch_dir_add/rm are called only on the expanded folders. The shown_subdir table is also updated only in files limited mode. On the C side, using the dmon library, we remove the recursive argument from the system.watch_dir and we always call it recursively except on Linux. At the same time the functions: systems.watch_dir_add / rm are provided but as dummy functions that does nothing except on Linux where they work as before to add / remove sub-directories in the inotify watch. In this was on the Lua side we always act we if the watches needed to be set for each sub-directory explicitly, independently of the system. The important improvement introduced is that we always avoid calling dmon_watch recursively on Linux. This latter thing is problematic with inotify and is therefore avoided on Linux. On the other side we simplifies the logic on the Lua side and remove conditions based on the OS used. --- data/core/init.lua | 100 +++++++++++++++++++++++++++++++++------------ src/api/system.c | 24 ++++++++++- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 481fc431..803f1946 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -180,10 +180,9 @@ 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) + local success = (show and system.watch_dir_add or system.watch_dir_rm)(dir.watch_id, fullpath) if not success then core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") end @@ -228,9 +227,14 @@ end local function project_scan_add_entry(dir, fileinfo) + assert(not dir.force_rescan, "should be used on when force_rescan is unset") local index, match = file_search(dir.files, fileinfo) if not match then table.insert(dir.files, index, fileinfo) + if fileinfo.type == "dir" and not dir.files_limit then + -- ASSUMPTION: dir.force_rescan is FALSE + system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename) + end dir.is_dirty = true end end @@ -252,7 +256,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, insert_hook) local m = #bs local i, j = 1, 1 while i <= m or i <= n do @@ -262,6 +266,7 @@ 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 insert_hook then insert_hook(b) end elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then table.remove(as, i1 + i) n = n - 1 @@ -297,7 +302,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 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 @@ -316,35 +333,54 @@ local function add_dir_scan_thread(dir) end) end + +local function folder_add_subdirs_watch(dir) + for _, fileinfo in ipairs(dir.files) do + if fileinfo.type == "dir" then + system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename) + end + end +end + + -- Populate a project folder top directory by scanning the filesystem. 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 + dir.watch_id = system.watch_dir(dir.name) end local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred) + -- If dir.files_limit is set to TRUE it means that: + -- * we will not read recursively all the project files and we don't index them + -- * we read only the files for the subdirectories that are opened/expanded in the + -- TreeView + -- * we add a subdirectory watch only to the directories that are opened/expanded + -- * we set the values in the shown_subdir table + -- + -- If dir.files_limit is set to FALSE it means that: + -- * we will read recursively all the project files and we index them + -- * all the list of project files is always complete and kept updated when + -- changes happen on the disk + -- * all the subdirectories at any depth must have a watch using system.watch_dir_add + -- * we DO NOT set the values in the shown_subdir table + -- + -- * If force_rescan is set to TRUE no watch are used in any case. if not complete then dir.slow_filesystem = not complete and (entries_count <= config.max_project_files) dir.files_limit = true - if 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 @@ -385,15 +421,16 @@ local function rescan_project_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) - -- The shown_subdir is only used on linux for very large directories. - -- replay them on the newly scanned project. - for subdir, show in pairs(save_project_dirs[i].shown_subdir) do - for j = 1, #dir.files do - if dir.files[j].filename == subdir then - -- the instructions above match when happens in TreeView:on_mouse_pressed - core.update_project_subdir(dir, subdir, show) - core.project_subdir_set_show(dir, subdir, show) - break + if dir.files_limit then + for subdir, show in pairs(save_project_dirs[i].shown_subdir) do + for j = 1, #dir.files do + if dir.files[j].filename == subdir then + -- The instructions below match when happens in TreeView:on_mouse_pressed. + -- We perform the operations only once iff the subdir is in dir.files. + core.update_project_subdir(dir, subdir, show) + core.project_subdir_set_show(dir, subdir, show) + break + end end end end @@ -411,9 +448,15 @@ end function core.update_project_subdir(dir, filename, expanded) + assert(dir.files_limit, "function should be called only when directory is in files limit mode") local index, n, file = project_subdir_bounds(dir, filename) if index then local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {} + -- ASSUMPTION: core.update_project_subdir is called only when dir.files_limit is true + -- NOTE: we may add new directories below but we do not need to call + -- system.watch_dir_add because the current function is called only + -- in dir.files_limit mode and in this latter case we don't need to + -- add watch to new, unfolded, subdirectories. files_list_replace(dir.files, index, n, new_files) dir.is_dirty = true return true @@ -510,6 +553,9 @@ local function project_scan_remove_file(dir, filepath) local index, match = file_search(dir.files, fileinfo) if match then table.remove(dir.files, index) + if dir.files_limit and filetype == "dir" then + dir.shown_subdir[filepath] = nil + end dir.is_dirty = true return end diff --git a/src/api/system.c b/src/api/system.c index 4c14843b..955d0ee2 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -584,6 +584,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 @@ -715,8 +720,14 @@ static int f_set_window_opacity(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); + /* 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 = 0; +#else + const uint32_t dmon_flags = DMON_WATCHFLAGS_RECURSIVE; +#endif 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"); } lua_pushnumber(L, watch_id.id); @@ -746,6 +757,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 @@ -839,6 +855,10 @@ static const luaL_Reg lib[] = { { "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 } }; From fd074ff39a69238bb72b37373babd6043e30a5d8 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Dec 2021 12:11:01 +0100 Subject: [PATCH 15/57] Fix problem when opening project's module document It wasn't fine to call core.open_doc without filename argument and later call Doc:save without providing both the filename and the absolute filename. It was giving a Doc in an inconsistent status where self.filename was set but not self.abs_filename. Added an asset to detect early the problem if ever happens again. In turn the problem prevented the project's module hook to work if the file was newly created. --- data/core/commands/core.lua | 11 +++-------- data/core/doc/init.lua | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 16b371f3..3242e2ef 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -141,14 +141,9 @@ 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) - end + local doc = core.open_doc(".lite_project.lua") + core.root_view:open_doc(doc) + doc:save() end, ["core:change-project-folder"] = function() diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 06cde9f9..7b97db35 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -85,6 +85,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 From 9578359b2b2f0402b840c86f33a9bba13593c586 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Dec 2021 15:45:09 +0100 Subject: [PATCH 16/57] Remove inotify recursive directory monitoring We no longer use in Lite XL recursive directory monitoring as it was a source of problems. To enforce this at compile time we use the preprocessor to remove the implementation of recursive monitoring for the Linux implementation only. --- lib/dmon/dmon.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 2bc9e0c3..f04e27d1 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -169,6 +169,8 @@ 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 #elif DMON_OS_MACOS # include # include @@ -710,6 +712,10 @@ typedef struct 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. */ +#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 +770,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 +784,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 +817,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 +928,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 +958,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) { @@ -1158,11 +1169,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); From 68aea8851014206654f1195dc0e50799718110e3 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Dec 2021 22:24:43 +0100 Subject: [PATCH 17/57] Fix error with ignore_files There was a double error because the config.ignore_files was used at two differect places in different ways. Now we apply coherently the original rule to apply config.ignore_files to the basename of each file or directory. --- data/core/init.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 803f1946..24083f0a 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -111,15 +111,20 @@ local function compare_file(a, b) end +local function fileinfo_pass_filter(info) + local basename = common.basename(info.filename) + return (info.size < config.file_size_limit * 1e6 and + not common.match_pattern(basename, config.ignore_files)) +end + + -- compute a file's info entry completed with "filename" to be used -- in project scan or falsy if it shouldn't appear in the list. local function get_project_file_info(root, file) local info = system.get_file_info(root .. file) if info then info.filename = strip_leading_path(file) - return (info.size < config.file_size_limit * 1e6 and - not common.match_pattern(info.filename, config.ignore_files) - and info) + return fileinfo_pass_filter(info) and info end end @@ -564,12 +569,8 @@ end local function project_scan_add_file(dir, filepath) - for fragment in string.gmatch(filepath, "([^/\\]+)") do - if common.match_pattern(fragment, config.ignore_files) then - return - end - end local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) + if not fileinfo_pass_filter(fileinfo) then return end if fileinfo then project_scan_add_entry(dir, fileinfo) end From 85d26adb6276fad3e3fe6091941c9b512fb10c55 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Dec 2021 23:57:23 +0100 Subject: [PATCH 18/57] Fix project's module loading when changing project Fix a bag of subtle problem about when loading the project module. We need to load the project's module before to scan the project directory. --- data/core/init.lua | 50 ++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 24083f0a..922715c4 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -52,18 +52,14 @@ local function update_recents_project(action, dir_path_abs) end -function core.set_project_dir(new_dir, change_project_fn, defer_add) +function core.set_project_dir(new_dir, change_project_fn) local chdir_ok = pcall(system.chdir, new_dir) if chdir_ok then if change_project_fn then change_project_fn() end core.project_dir = common.normalize_volume(new_dir) core.project_directories = {} - if not defer_add then - core.add_project_directory(new_dir) - end - return true end - return false + return chdir_ok end function core.close_current_project() @@ -83,13 +79,22 @@ function core.close_current_project() end end + +local function reload_customizations() + core.reload_module("core.style") + core.reload_module("core.config") + core.reload_module("core.keymap") + core.load_user_directory() + core.load_project_module() +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 @@ -570,10 +575,8 @@ end local function project_scan_add_file(dir, filepath) local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) - if not fileinfo_pass_filter(fileinfo) then return end - if fileinfo then - project_scan_add_entry(dir, fileinfo) - end + if not fileinfo or not fileinfo_pass_filter(fileinfo) then return end + project_scan_add_entry(dir, fileinfo) end @@ -697,26 +700,16 @@ local function configure_borderless_window() end -local function reload_customizations() - core.reload_module("core.style") - core.reload_module("core.config") - core.reload_module("core.keymap") - core.load_user_directory() - core.load_project_module() - rescan_project_directories() - configure_borderless_window() -end - - local function add_config_files_hooks() -- auto-realod style when user's module is saved by overriding Doc:Save() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") - local module_filename = system.absolute_path(".lite_project.lua") function Doc:save(filename, abs_filename) doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename or self.abs_filename == module_filename then + if self.abs_filename == user_filename or self.abs_filename == core.project_module_filename then reload_customizations() + rescan_project_directories() + configure_borderless_window() end end end @@ -787,7 +780,7 @@ function core.init() local project_dir_abs = system.absolute_path(project_dir) -- We prevent set_project_dir below to effectively add and scan the directory becaese tha -- project module and its ignore files is not yet loaded. - local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs, nil, true) + 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 update_recents_project("add", project_dir_abs) @@ -797,7 +790,7 @@ 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, nil, true) then + if not core.set_project_dir(project_dir_abs) then system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") os.exit(1) end @@ -1028,6 +1021,7 @@ end function core.load_project_module() local filename = ".lite_project.lua" + core.project_module_filename = system.absolute_path(filename) if system.get_file_info(filename) then return core.try(function() local fn, err = loadfile(filename) From 03350cc14b44e8734af8419535a99bebea11fae6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 31 Dec 2021 00:20:52 +0100 Subject: [PATCH 19/57] Restore config.plugins when reloading config Some plugins store options in: config.plugins. so we restore all the kay-values of config.plugins when reloading the user preferences. --- data/core/init.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 922715c4..5d51c4cb 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -81,11 +81,18 @@ end local function reload_customizations() - core.reload_module("core.style") - core.reload_module("core.config") - core.reload_module("core.keymap") - core.load_user_directory() - core.load_project_module() + core.reload_module("core.style") + core.reload_module("core.keymap") + local plugins_save = {} + for k, v in pairs(config.plugins) do + plugins_save[k] = v + end + core.reload_module("core.config") + for k, v in pairs(plugins_save) do + config.plugins[k] = v + end + core.load_user_directory() + core.load_project_module() end From 445c79bb52120fa874dbc711aa1d4390c4d02848 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 31 Dec 2021 00:22:49 +0100 Subject: [PATCH 20/57] Revert "No longer store autocomplete options in config" This reverts commit 0f1b84040dd823823c17e925e1ed10380b7737c1. The new mechanism to save config.plugins upon user's configuration reload let us stay compatible with existing plugins. --- data/plugins/autocomplete.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index d324733f..fde9487e 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -10,7 +10,7 @@ local RootView = require "core.rootview" local DocView = require "core.docview" local Doc = require "core.doc" -local autocomplete_options = { +config.plugins.autocomplete = { -- Amount of characters that need to be written for autocomplete min_len = 3, -- The max amount of visible items @@ -192,7 +192,7 @@ local function update_suggestions() -- fuzzy match, remove duplicates and store items = common.fuzzy_match(items, partial) local j = 1 - for i = 1, autocomplete_options.max_suggestions do + for i = 1, config.plugins.autocomplete.max_suggestions do suggestions[i] = items[j] while items[j] and items[i].text == items[j].text do items[i].info = items[i].info or items[j].info @@ -235,7 +235,7 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) end - local ah = autocomplete_options.max_height + local ah = config.plugins.autocomplete.max_height local max_items = #suggestions if max_items > ah then @@ -294,7 +294,7 @@ local function draw_suggestions_box(av) return end - local ah = autocomplete_options.max_height + local ah = config.plugins.autocomplete.max_height -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) @@ -355,7 +355,7 @@ local function show_autocomplete() -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= autocomplete_options.min_len or triggered_manually then + if #partial >= config.plugins.autocomplete.min_len or triggered_manually then update_suggestions() if not triggered_manually then @@ -469,7 +469,7 @@ function autocomplete.complete(completions, on_close) end function autocomplete.can_complete() - if #partial >= autocomplete_options.min_len then + if #partial >= config.plugins.autocomplete.min_len then return true end return false From bf578d5ee4485f79a40b2507fece8227c46fa730 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 3 Jan 2022 18:50:43 +0100 Subject: [PATCH 21/57] Fix logic for file create event When we get a file or directory creation event we need to ensure that all the parent directories pass the ignore_files test. --- data/core/init.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 5d51c4cb..4074043e 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -582,8 +582,16 @@ end local function project_scan_add_file(dir, filepath) local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) - if not fileinfo or not fileinfo_pass_filter(fileinfo) then return end - project_scan_add_entry(dir, fileinfo) + if fileinfo then + repeat + filepath = common.dirname(filepath) + local parent_info = filepath and get_project_file_info(dir.name, PATHSEP .. filepath) + if filepath and not parent_info then + return + end + until not parent_info + project_scan_add_entry(dir, fileinfo) + end end From f3cf7ac9c7d60a9d23f031ab526d0a279c2fa410 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 4 Jan 2022 18:06:30 +0100 Subject: [PATCH 22/57] Do not reload core.keymap module Avoid reloading the core.keymap module when user's config or project module change. The reason is the plugins like autocomplete can add keymaps and the additions from plugins would be lost. Close issue #793 --- data/core/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 4074043e..b97ab2d6 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -82,7 +82,6 @@ end local function reload_customizations() core.reload_module("core.style") - core.reload_module("core.keymap") local plugins_save = {} for k, v in pairs(config.plugins) do plugins_save[k] = v From 9929ca9c2d83231872a2b5fb05f4c27f41d092c0 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 5 Jan 2022 23:21:47 +0100 Subject: [PATCH 23/57] Fix logic with project directories suggestions Attempt to fix issue #791. The logic set with the previous commit for suggest_directory is similar to the one we use except the previous expression was false do to operator precedence for "and" versus "or". With the modification here, when opening a project directory, we suggest the recently used projects if the text is equal to dirname(project_dir) + "/" which happens to be the text the command view is initially set to. In addition we do the same if text is "". If the condition is not met we return the suggestions from common.dir_path_suggest to match the text entered. Works well on Linux but may not solve the problem on Windows, it should be tested. --- data/core/commands/core.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 3242e2ef..29626c86 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -10,8 +10,9 @@ 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 command.add(nil, { @@ -149,7 +150,7 @@ command.add(nil, { ["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)) @@ -169,7 +170,7 @@ command.add(nil, { ["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) From 1b57107352dc8c26999febb93e46cb5338a00255 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 5 Jan 2022 23:32:26 +0100 Subject: [PATCH 24/57] Fix problem with special file types For special file types like the ones in /dev/ the info entry's type is neither file neither dir. We prevent these kind of files from being listed in the project. --- data/core/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index b97ab2d6..aba10d8f 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -133,7 +133,9 @@ end -- in project scan or falsy if it shouldn't appear in the list. local function get_project_file_info(root, file) local info = system.get_file_info(root .. file) - if info then + -- info 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) and info end From 1e7075ca9fb59c4f237d747ee7e7fd06baaf183a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 5 Jan 2022 23:42:47 +0100 Subject: [PATCH 25/57] Do not force choosing project dir to suggestion When changing or opening a project directory do not take the selected item from suggestion but simply the entered text as it is. Otherwise the user may be unable to choose a directory if the text matches the beginning of suggestion. Close #791 --- data/core/commands/core.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 29626c86..971b95f1 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -152,8 +152,8 @@ command.add(nil, { if dirname then 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)) + core.command_view:enter("Change Project Folder", function(text) + text = system.absolute_path(common.home_expand(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 @@ -172,8 +172,8 @@ command.add(nil, { if dirname then 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) + core.command_view:enter("Open Project", function(text) + text = common.home_expand(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) From 7ded5c819929e6e06a9fc21cd964ebbfb22542d1 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 6 Jan 2022 18:00:15 +0100 Subject: [PATCH 26/57] Fix problem when reloading project directory --- data/core/init.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index aba10d8f..52f4112a 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -440,13 +440,27 @@ local function rescan_project_directories() for i = 1, n do -- add again the directories in the project local dir = core.add_project_directory(save_project_dirs[i].name) if dir.files_limit then - for subdir, show in pairs(save_project_dirs[i].shown_subdir) do + -- 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. - core.update_project_subdir(dir, subdir, show) + -- 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 From 143b389365aec6bd3b16800d9f54cb92f87f42e7 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 7 Jan 2022 10:40:26 +0100 Subject: [PATCH 27/57] Clear TreeView cache when closing project --- data/plugins/treeview.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index ffe93ca5..9f59bc80 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -402,6 +402,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 common.basename(core.project_dir) == path end From 5b154c189fd6213b7e9bd2f5197e2550607f86f6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 3 Jan 2022 15:05:57 +0100 Subject: [PATCH 28/57] First version of paths in ignore_files Works correctly and the logic seems sound even if somewhat quirky. `^%.` match any file of directory whose basename begins with a dot. `^/node_modules$/"` match a directory named `node_modules` at the project's root. Note that the final '/' needs to be at the end. The '/' after the '^' needs to be there to trigger a match of the full path filename so we are sure it is at the root. PROBLEM: the '/' to trigger full path match could be in a pattern's special expression like: [^/] `^%.git$/` match any directory name '.git' anywhere in the project. `^/%.git$/` match a directory named '.git' only at the project's root. `^/subprojects/.+/` match any directory in a top-level folder named "subprojects". `^/build/` match any top level directory whose name begins with "build" PROBLEM: may be surprising, one may expects it matches only a directory named 'build'. It actually acts like it was `^/build.*/`. --- data/core/init.lua | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 52f4112a..fe4c75be 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -122,10 +122,37 @@ local function compare_file(a, b) end + + + + local function fileinfo_pass_filter(info) + if info.size >= config.file_size_limit * 1e6 then return false end local basename = common.basename(info.filename) - return (info.size < config.file_size_limit * 1e6 and - not common.match_pattern(basename, config.ignore_files)) + -- replace '\' with '/' for Windows where PATHSEP = '\' + local fullname = "/" .. info.filename:gsub("\\", "/") + local ipatterns = config.ignore_files + -- config.ignore_files could be a simple string... + if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end + for _, pattern in ipairs(ipatterns) do + local is_path_like = pattern:match("/[^/]") -- contains a slash but not at the end + local dir_pass = true + if pattern:match("(.+)/$") then + dir_pass = (info.type == "dir") + -- the final '/' should not participate to the match. + pattern = pattern:match("(.+)/$") + end + if is_path_like then + if fullname:match(pattern) and dir_pass then + return false + end + else + if basename:match(pattern) and dir_pass then + return false + end + end + end + return true end From 295c65da92cd7bb3690f04629ebbffda5ff6c6a0 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 3 Jan 2022 17:49:15 +0100 Subject: [PATCH 29/57] Use compiled ignore_files pattern Try to digest the ignore_files pattern before potentially processing a lot of files because it may be expensive. --- data/core/init.lua | 64 +++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index fe4c75be..c17f0498 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -122,34 +122,32 @@ local function compare_file(a, b) end +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 + local match_dir = pattern:match("(.+)/$") + compiled[i] = { + use_path = pattern:match("/[^/]"), -- contains a slash but not at the end + match_dir = match_dir, + pattern = match_dir or pattern + } + end + return compiled +end - - -local function fileinfo_pass_filter(info) +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("\\", "/") - local ipatterns = config.ignore_files - -- config.ignore_files could be a simple string... - if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end - for _, pattern in ipairs(ipatterns) do - local is_path_like = pattern:match("/[^/]") -- contains a slash but not at the end - local dir_pass = true - if pattern:match("(.+)/$") then - dir_pass = (info.type == "dir") - -- the final '/' should not participate to the match. - pattern = pattern:match("(.+)/$") - end - if is_path_like then - if fullname:match(pattern) and dir_pass then - return false - end - else - if basename:match(pattern) and dir_pass then - return false - end + for _, compiled in ipairs(ignore_compiled) do + local pass_dir = (not compiled.match_dir or info.type == "dir") + if (compiled.use_path and fullname or basename):match(compiled.pattern) and pass_dir then + return false end end return true @@ -158,13 +156,13 @@ 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) -- 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) and info + return fileinfo_pass_filter(info, ignore_compiled) and info end end @@ -186,15 +184,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 @@ -206,7 +205,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 @@ -339,7 +338,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) @@ -396,7 +395,7 @@ local function scan_project_folder(index) if not dir.force_rescan then dir.watch_id = system.watch_dir(dir.name) end - local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred) + 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 @@ -510,7 +509,7 @@ 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 @@ -623,11 +622,12 @@ end local function project_scan_add_file(dir, filepath) - 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 repeat filepath = common.dirname(filepath) - local parent_info = filepath and get_project_file_info(dir.name, PATHSEP .. filepath) + local parent_info = filepath and get_project_file_info(dir.name, PATHSEP .. filepath, ignore) if filepath and not parent_info then return end From a703840068c59758bfbde2e207d19d5c10600b2e Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 3 Jan 2022 23:43:12 +0100 Subject: [PATCH 30/57] Add some comments for ignore_files logic --- data/core/init.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index c17f0498..fcd66734 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -122,6 +122,7 @@ 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 = {} @@ -131,8 +132,8 @@ local function compile_ignore_files() local match_dir = pattern:match("(.+)/$") compiled[i] = { use_path = pattern:match("/[^/]"), -- contains a slash but not at the end - match_dir = match_dir, - pattern = match_dir or pattern + match_dir = match_dir, -- to be used as a boolen value + pattern = match_dir or pattern -- get the actual pattern } end return compiled @@ -625,11 +626,13 @@ 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 + return -- parent directory does match ignore_files rules: stop there end until not parent_info project_scan_add_entry(dir, fileinfo) From 5032e7352e0570644a35f805df1df9d203836677 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 4 Jan 2022 00:25:14 +0100 Subject: [PATCH 31/57] Write an initial project module if not present --- data/core/commands/core.lua | 3 +++ data/core/init.lua | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 971b95f1..2ea34b2d 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -142,6 +142,9 @@ command.add(nil, { end, ["core:open-project-module"] = function() + 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() diff --git a/data/core/init.lua b/data/core/init.lua index fcd66734..67efe11e 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -715,6 +715,46 @@ 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 include the path. +-- +-- Here some examples: +-- +-- "^%." match any file of directory whose basename begins with a dot. +-- +-- When there is an '/' at the end the pattern will only match directories. The final +-- '/' is removed from the pattern to match the file's or directory's name. +-- +-- "^%.git$/" match any directory named ".git" anywhere in the project. +-- +-- If a "/" appears anywhere in the pattern (except at the end) 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$/" 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() From 7473fbf32c35838511703a3ed79bb2a273812a89 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 6 Jan 2022 00:10:14 +0100 Subject: [PATCH 32/57] Fix undue asserts in dmon_extra Some asserts are placed in case that can effectively occur so we remove the assertion and we return false. In turn we adapt the logic accordingly so when false is returned to add a watch we do not open that directory. --- data/core/init.lua | 8 ++++---- data/plugins/treeview.lua | 8 +++++--- lib/dmon/dmon_extra.h | 4 ---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 67efe11e..05838192 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -224,14 +224,14 @@ end function core.project_subdir_set_show(dir, filename, show) - dir.shown_subdir[filename] = show if dir.files_limit and not dir.force_rescan then local fullpath = dir.name .. PATHSEP .. filename - local success = (show and system.watch_dir_add or system.watch_dir_rm)(dir.watch_id, fullpath) - if not success then - core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") + 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 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 9f59bc80..93c43189 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -230,12 +230,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 local hovered_dir = core.project_dir_by_name(hovered_item.dir_name) if hovered_dir and hovered_dir.files_limit then - core.update_project_subdir(hovered_dir, hovered_item.filename, hovered_item.expanded) - core.project_subdir_set_show(hovered_dir, hovered_item.filename, hovered_item.expanded) + 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() diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h index 4b321034..cbacdc93 100644 --- a/lib/dmon/dmon_extra.h +++ b/lib/dmon/dmon_extra.h @@ -62,7 +62,6 @@ 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); if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -79,7 +78,6 @@ 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); if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -92,7 +90,6 @@ 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); if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -137,7 +134,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; From 1520c12580d91a6455031b07efc2fc37deb8959a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 8 Jan 2022 10:55:43 +0100 Subject: [PATCH 33/57] Remove DMON_LOG_ERROR to return an error code --- data/core/init.lua | 7 +++- lib/dmon/dmon.h | 78 +++++++++++++++++++++++++------------------ lib/dmon/dmon_extra.h | 2 ++ src/api/system.c | 13 ++++++-- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 05838192..0d47af14 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -394,7 +394,12 @@ local function scan_project_folder(index) local fstype = system.get_fs_type(dir.name) dir.force_rescan = (fstype == "nfs" or fstype == "fuse") if not dir.force_rescan then - dir.watch_id = system.watch_dir(dir.name) + 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: diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index f04e27d1..496463f1 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,21 @@ 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_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 +123,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 @@ -171,6 +179,7 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # 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 @@ -191,11 +200,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 @@ -225,10 +229,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 @@ -358,6 +358,19 @@ 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", +}; + +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 @@ -503,13 +516,13 @@ _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) } DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); - DMON_ASSERT(wait_result != WAIT_FAILED); - if (wait_result != WAIT_TIMEOUT) { + // FIXME: do not check for WAIT_ABANDONED_, check if that can happen. + if (wait_result != WAIT_TIMEOUT && wait_result != WAIT_FAILED) { 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; @@ -598,7 +611,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]); @@ -632,17 +645,17 @@ 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"); + *error_code = DMON_ERROR_WATCH_DIR; LeaveCriticalSection(&_dmon.mutex); _InterlockedExchange(&_dmon.modify_watches, 0); return dmon__make_id(0); } } else { - _DMON_LOG_ERRORF("Could not open: %s", rootdir); + *error_code = DMON_ERROR_OPEN_DIR; LeaveCriticalSection(&_dmon.mutex); _InterlockedExchange(&_dmon.modify_watches, 0); return dmon__make_id(0); @@ -714,7 +727,9 @@ 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. */ + * 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) @@ -1099,7 +1114,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]); @@ -1118,7 +1133,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); } @@ -1133,8 +1148,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); } @@ -1151,7 +1165,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); } @@ -1159,7 +1173,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); } @@ -1491,7 +1505,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]); @@ -1511,7 +1525,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); @@ -1526,7 +1540,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 cbacdc93..9b201e17 100644 --- a/lib/dmon/dmon_extra.h +++ b/lib/dmon/dmon_extra.h @@ -52,6 +52,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) { diff --git a/src/api/system.c b/src/api/system.c index 955d0ee2..1a862f78 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -724,12 +724,19 @@ static int f_watch_dir(lua_State *L) { * 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 = 0; + 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_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"); } + 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_pushnumber(L, watch_id.id); return 1; } From 39366d3a097b1f5d9fe1118da71f9a1c2e8b1dfe Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 8 Jan 2022 17:15:09 +0100 Subject: [PATCH 34/57] Ensure project rescan thread is terminated When changing a project we need to ensure that the old threads are no longer run. --- data/core/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 0d47af14..2f1ced31 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -369,13 +369,14 @@ end local function add_dir_scan_thread(dir) core.add_thread(function() while true do + print("DEBUG: running rescan on", dir.name) local has_changes = rescan_project_subdir(dir, "") if has_changes then core.redraw = true -- we run without an event, from a thread end coroutine.yield(5) end - end) + end, dir) end From f0aea5b1a4a78f9bf61063bd4bd7d35f49f7c6aa Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 8 Jan 2022 17:22:23 +0100 Subject: [PATCH 35/57] Report error codes from dmon_watch_add --- lib/dmon/dmon.h | 4 +++- lib/dmon/dmon_extra.h | 8 ++++++-- src/api/system.c | 9 ++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 496463f1..1a7eed3b 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -107,7 +107,8 @@ typedef enum dmon_error_enum { DMON_ERROR_OPEN_DIR, DMON_ERROR_MONITOR_FAIL, DMON_ERROR_UNSUPPORTED_SYMLINK, - DMON_ERROR_END, + DMON_ERROR_SUBDIR_LOCATION, + DMON_ERROR_END } dmon_error; #ifdef __cplusplus @@ -364,6 +365,7 @@ static const char *dmon__errors[] = { "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) { diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h index 9b201e17..2252c88e 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,7 +36,7 @@ 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); @@ -64,6 +64,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) { + *error_code = DMON_ERROR_UNSUPPORTED_SYMLINK; if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -80,6 +81,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) { + *error_code = DMON_ERROR_SUBDIR_LOCATION; if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -92,6 +94,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) { + *error_code = DMON_ERROR_WATCH_DIR; if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; @@ -136,6 +139,7 @@ DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) } } if (i >= c) { + *error_code = DMON_ERROR_SUBDIR_LOCATION; if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; diff --git a/src/api/system.c b/src/api/system.c index 1a862f78..45776b51 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -753,7 +753,14 @@ 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; } From 6584bdfd33d7e255a816e07e35e56737734c3cd6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 8 Jan 2022 18:24:44 +0100 Subject: [PATCH 36/57] On Windows wait indefinitely in dmon thread Avoid waiting with a finite timeout and wait indefinitely in dmon thread. When we need to unwatch we send a signal to a special event meant to wakeup the waiting thread. --- lib/dmon/dmon.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 1a7eed3b..d27d7aea 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -409,6 +409,7 @@ typedef struct dmon__state { volatile LONG modify_watches; dmon__win32_event* events; bool quit; + HANDLE wake_event; } dmon__state; static bool _dmon_init; @@ -494,7 +495,7 @@ _DMON_PRIVATE void dmon__win32_process_events(void) _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); @@ -517,9 +518,12 @@ _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); - // FIXME: do not check for WAIT_ABANDONED_, check if that can happen. - if (wait_result != WAIT_TIMEOUT && wait_result != WAIT_FAILED) { + const int n = _dmon.num_watches; + wait_handles[n] = _dmon.wake_event; + DWORD wait_result = WaitForMultipleObjects(n + 1, wait_handles, FALSE, INFINITE); + DMON_ASSERT(wait_result != WAIT_TIMEOUT); + // NOTE: maybe we should check for WAIT_ABANDONED_ values if that can happen. + if (wait_result != WAIT_FAILED && wait_result != WAIT_OBJECT_0 + n) { dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; DWORD bytes; @@ -586,6 +590,7 @@ DMON_API_IMPL void dmon_init(void) _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; } @@ -673,6 +678,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) DMON_ASSERT(id.id > 0); _InterlockedExchange(&_dmon.modify_watches, 1); + SetEvent(_dmon.wake_event); EnterCriticalSection(&_dmon.mutex); int index = id.id - 1; From 44a8dc320b0acbe6e929f905e455353527bd3c65 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 8 Jan 2022 18:49:48 +0100 Subject: [PATCH 37/57] Fix some errors with previous commits --- lib/dmon/dmon_extra.h | 1 - src/api/system.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h index 2252c88e..9c6411af 100644 --- a/lib/dmon/dmon_extra.h +++ b/lib/dmon/dmon_extra.h @@ -139,7 +139,6 @@ DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) } } if (i >= c) { - *error_code = DMON_ERROR_SUBDIR_LOCATION; if (!skip_lock) pthread_mutex_unlock(&_dmon.mutex); return false; diff --git a/src/api/system.c b/src/api/system.c index 45776b51..4e134e91 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -754,7 +754,7 @@ static int f_watch_dir_add(lua_State *L) { 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) + int success = dmon_watch_add(watch_id, subdir, &error_code); if (!success) { lua_pushboolean(L, 0); lua_pushstring(L, dmon_error_str(error_code)); From 19ec86d971e17065dbcf980bd3dcf6f481b575f8 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 9 Jan 2022 00:56:11 +0100 Subject: [PATCH 38/57] Do not use timeout in dmon linux select Wait indefinitely in select and wake-up the thread when needed. --- lib/dmon/dmon.h | 43 +++++++++++++++++++++++++++++++++++-------- lib/dmon/dmon_extra.h | 10 ++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index d27d7aea..326459e3 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -727,6 +727,8 @@ typedef struct dmon__state { int num_watches; pthread_t thread_handle; pthread_mutex_t mutex; + int wait_flag; + int wake_event_pipe[2]; bool quit; } dmon__state; @@ -1012,29 +1014,39 @@ static void* dmon__thread(void* arg) static uint8_t buff[_DMON_TEMP_BUFFSIZE]; struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; struct timespec rem = { 0, 0 }; - struct timeval timeout; uint64_t usecs_elapsed = 0; struct timeval starttm; gettimeofday(&starttm, 0); + int debug_count = 0; while (!_dmon.quit) { nanosleep(&req, &rem); - if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + if (_dmon.num_watches == 0 || _dmon.wait_flag == 1 || 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)) { + if (select(nfds + 1, &rfds, NULL, NULL, 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)) { @@ -1084,6 +1096,16 @@ static void* dmon__thread(void* arg) return 0x0; } +_DMON_PRIVATE void dmon__mutex_wakeup_lock() { + _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; +} + _DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) { close(watch->fd); @@ -1097,6 +1119,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"); @@ -1107,12 +1132,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; @@ -1127,7 +1154,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, 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); @@ -1206,7 +1233,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); diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h index 9c6411af..97631520 100644 --- a/lib/dmon/dmon_extra.h +++ b/lib/dmon/dmon_extra.h @@ -42,8 +42,9 @@ DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir, dmon_e 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]; @@ -115,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]; From 9be22f0b8d854fedead9be00eb9db21ee8931432 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 9 Jan 2022 01:16:31 +0100 Subject: [PATCH 39/57] Attempt to fix dmon critical section for windows Should fix commit bb12f085f3. When taking the critical section we should always send the event to wakeup the events thread. In addition use TryEnterCriticalSection to send the event only if needed reducing the number of spurious events sent. --- lib/dmon/dmon.h | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 326459e3..a10be33a 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -595,11 +595,19 @@ DMON_API_IMPL void dmon_init(void) _dmon_init = true; } +static void dmon__enter_critical_wakeup() { + _InterlockedExchange(&_dmon.modify_watches, 1); + if (TryEnterCriticalSection(&_dmon.mutex) == 0) { + SetEvent(_dmon.wake_event); + EnterCriticalSection(&_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); @@ -609,6 +617,7 @@ DMON_API_IMPL void dmon_deinit(void) dmon__unwatch(&_dmon.watches[i]); } + LeaveCriticalSection(&_dmon.mutex); DeleteCriticalSection(&_dmon.mutex); stb_sb_free(_dmon.events); _dmon_init = false; @@ -623,8 +632,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, 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); @@ -677,9 +685,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) { DMON_ASSERT(id.id > 0); - _InterlockedExchange(&_dmon.modify_watches, 1); - SetEvent(_dmon.wake_event); - EnterCriticalSection(&_dmon.mutex); + dmon__enter_critical_wakeup(); int index = id.id - 1; DMON_ASSERT(index < _dmon.num_watches); From 648b977c1e5d5a07afd6c2be87748a2570ee2e3b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 9 Jan 2022 13:01:30 +0100 Subject: [PATCH 40/57] Use a timeout in dmon thread with pending events --- lib/dmon/dmon.h | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index a10be33a..1ff9dc53 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -520,10 +520,10 @@ _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) const int n = _dmon.num_watches; wait_handles[n] = _dmon.wake_event; - DWORD wait_result = WaitForMultipleObjects(n + 1, wait_handles, FALSE, INFINITE); - DMON_ASSERT(wait_result != WAIT_TIMEOUT); + const int n_pending = stb_sb_count(_dmon.events); + DWORD wait_result = WaitForMultipleObjects(n + 1, wait_handles, FALSE, n_pending > 0 ? 10 : INFINITE); // NOTE: maybe we should check for WAIT_ABANDONED_ values if that can happen. - if (wait_result != WAIT_FAILED && wait_result != WAIT_OBJECT_0 + n) { + if (wait_result >= WAIT_OBJECT_0 && wait_result < WAIT_OBJECT_0 + n) { dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; DWORD bytes; @@ -595,7 +595,7 @@ DMON_API_IMPL void dmon_init(void) _dmon_init = true; } -static void dmon__enter_critical_wakeup() { +static void dmon__enter_critical_wakeup(void) { _InterlockedExchange(&_dmon.modify_watches, 1); if (TryEnterCriticalSection(&_dmon.mutex) == 0) { SetEvent(_dmon.wake_event); @@ -1020,6 +1020,7 @@ static void* dmon__thread(void* arg) static uint8_t buff[_DMON_TEMP_BUFFSIZE]; struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; struct timespec rem = { 0, 0 }; + struct timeval timeout; uint64_t usecs_elapsed = 0; struct timeval starttm; @@ -1048,7 +1049,10 @@ static void* dmon__thread(void* arg) if (wake_fd > nfds) nfds = wake_fd; - if (select(nfds + 1, &rfds, NULL, NULL, NULL)) { + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + 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); @@ -1102,7 +1106,7 @@ static void* dmon__thread(void* arg) return 0x0; } -_DMON_PRIVATE void dmon__mutex_wakeup_lock() { +_DMON_PRIVATE void dmon__mutex_wakeup_lock(void) { _dmon.wait_flag = 1; if (pthread_mutex_trylock(&_dmon.mutex) != 0) { char send_char = 1; From 827f3f876dedab369643795a3c13cbc20eba405b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 9 Jan 2022 20:01:57 +0100 Subject: [PATCH 41/57] Remove remaining debug code fragment --- lib/dmon/dmon.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 1ff9dc53..3f2bc0c5 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -1026,7 +1026,6 @@ static void* dmon__thread(void* arg) struct timeval starttm; gettimeofday(&starttm, 0); - int debug_count = 0; while (!_dmon.quit) { nanosleep(&req, &rem); if (_dmon.num_watches == 0 || _dmon.wait_flag == 1 || pthread_mutex_trylock(&_dmon.mutex) != 0) { From 656a89c4945aa77698ebd2462575f490e233a242 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 9 Jan 2022 20:22:39 +0100 Subject: [PATCH 42/57] Fix checks when opening new project directory --- data/core/commands/core.lua | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 2ea34b2d..524352fc 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -15,6 +15,15 @@ local function suggest_directory(text) 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, { ["core:quit"] = function() core.quit() @@ -156,17 +165,17 @@ command.add(nil, { core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) end core.command_view:enter("Change Project Folder", function(text) - text = system.absolute_path(common.home_expand(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) + 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 + 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, text) + end, abs_path) end, suggest_directory) end, @@ -176,13 +185,17 @@ command.add(nil, { core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) end core.command_view:enter("Open Project", function(text) - text = common.home_expand(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) + 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, From 4cdd42de1a5aaa62d50a9b40c8f7af4142710f9d Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 10 Jan 2022 09:54:47 +0100 Subject: [PATCH 43/57] Ensure config.plugins are restored on new config When a user's or project's module configuration file is changed we make sure that the config.plugins fields are all restored so that all plugins already loaded can continue to work. --- data/core/init.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 2f1ced31..7a44f0ae 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -81,17 +81,24 @@ end local function reload_customizations() - core.reload_module("core.style") + -- 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 - core.load_user_directory() - core.load_project_module() end From ae1890d29a6aeb28be698fcafd3180e63a085de5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 10 Jan 2022 16:26:39 +0100 Subject: [PATCH 44/57] Fix project files reading with symlink --- data/core/init.lua | 64 +++++++++++++++++++++++++++++++--------------- src/api/system.c | 8 ++++++ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 7a44f0ae..315f8c95 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -278,20 +278,6 @@ local function file_search(files, info) end -local function project_scan_add_entry(dir, fileinfo) - assert(not dir.force_rescan, "should be used on when force_rescan is unset") - local index, match = file_search(dir.files, fileinfo) - if not match then - table.insert(dir.files, index, fileinfo) - if fileinfo.type == "dir" and not dir.files_limit then - -- ASSUMPTION: dir.force_rescan is FALSE - system.watch_dir_add(dir.watch_id, dir.name .. PATHSEP .. fileinfo.filename) - end - dir.is_dirty = true - end -end - - local function files_info_equal(a, b) return a.filename == b.filename and a.type == b.type end @@ -308,7 +294,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, insert_hook) +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 @@ -318,8 +304,9 @@ local function files_list_replace(as, i1, n, bs, insert_hook) then table.insert(as, i1 + i, b) i, j, n = i + 1, j + 1, n + 1 - if insert_hook then insert_hook(b) end + 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 @@ -328,6 +315,29 @@ local function files_list_replace(as, i1, n, bs, insert_hook) 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 @@ -362,11 +372,11 @@ local function rescan_project_subdir(dir, filename_rooted) -- we missed some directory creation event from the directory monitoring which -- almost never happens. With inotify is at least theoretically possible. local need_subdir_watches = not dir.files_limit and not dir.force_rescan - files_list_replace(dir.files, index, n, new_files, need_subdir_watches and function(fileinfo) + 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) + end}) dir.is_dirty = true return true end @@ -624,10 +634,22 @@ local function project_scan_remove_file(dir, filepath) fileinfo.type = filetype local index, match = file_search(dir.files, fileinfo) if match then - table.remove(dir.files, index) - if dir.files_limit and filetype == "dir" then - dir.shown_subdir[filepath] = nil + 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 diff --git a/src/api/system.c b/src/api/system.c index 4e134e91..10288eba 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -544,6 +544,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; } From 2456452f65872a3e0e1ab36dbe8f6eca5faf5fef Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 13 Jan 2022 16:38:20 +0100 Subject: [PATCH 45/57] Fix error to close view when deleting a file --- data/core/commands/doc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index b8ce2cb5..37582474 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -383,7 +383,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) From 7e9b2f30da444cd65a7c3a5c82d1270142dd3b58 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 13 Jan 2022 16:43:37 +0100 Subject: [PATCH 46/57] Treat final '/' or '/$' in ignore rule as part of the pattern Evolve the rule for directory in ignore_files to be more natural and easy to understand. When a final '/' or '/$' is found we consider the pattern to match a directory and the pattern is not modifed. In turns, is used, before matching a directory's name a final '/' is appended to its name before checking if it matches the pattern. With the previous rule a final '/' in the pattern meant also a directory but the '/' was removed from the pattern. --- data/core/init.lua | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 315f8c95..25bcded9 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -136,11 +136,11 @@ local function compile_ignore_files() -- config.ignore_files could be a simple string... if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end for i, pattern in ipairs(ipatterns) do - local match_dir = pattern:match("(.+)/$") compiled[i] = { - use_path = pattern:match("/[^/]"), -- contains a slash but not at the end - match_dir = match_dir, -- to be used as a boolen value - pattern = match_dir or pattern -- get the actual pattern + 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 @@ -153,9 +153,15 @@ local function fileinfo_pass_filter(info, ignore_compiled) -- replace '\' with '/' for Windows where PATHSEP = '\' local fullname = "/" .. info.filename:gsub("\\", "/") for _, compiled in ipairs(ignore_compiled) do - local pass_dir = (not compiled.match_dir or info.type == "dir") - if (compiled.use_path and fullname or basename):match(compiled.pattern) and pass_dir then - return false + 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 @@ -765,23 +771,25 @@ local config = require "core.config" -- config.ignore_files = {"^%.", } -- Patterns are normally applied to the file's or directory's name, without --- its path. See below about how to include the path. +-- 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 '/' at the end the pattern will only match directories. The final --- '/' is removed from the pattern to match the file's or directory's name. +-- 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$/" match any directory named ".git" anywhere in the project. +-- "^%.git/" matches any directory named ".git" anywhere in the project. -- --- If a "/" appears anywhere in the pattern (except at the end) 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. +-- 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$/" match a directory named "node_modules" at the project's root. --- "^/build/" match any top level directory whose name _begins_ with "build" +-- "^/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. From 2dd154edeb2fd5d80d412e1b34bf24486b74bed3 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 18 Jan 2022 10:42:20 +0100 Subject: [PATCH 47/57] Remove remaining debug message --- data/core/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 25bcded9..6ac6af9c 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -392,7 +392,6 @@ end local function add_dir_scan_thread(dir) core.add_thread(function() while true do - print("DEBUG: running rescan on", dir.name) local has_changes = rescan_project_subdir(dir, "") if has_changes then core.redraw = true -- we run without an event, from a thread From cd83df1abfd99a1db1d7bc2cd69ee7f754e051b7 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 19 Jan 2022 18:18:25 +0100 Subject: [PATCH 48/57] Bump version and changelog to prepare 2.0.5 release --- changelog.md | 39 +++++++++++++++++++++++++++++++++++++++ meson.build | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 06a75b70..fef160b6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,44 @@ 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. diff --git a/meson.build b/meson.build index ccdfdd09..3e6a8a4b 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c', 'cpp'], - version : '2.0.4', + version : '2.0.5', license : 'MIT', meson_version : '>= 0.54', default_options : ['c_std=gnu11', 'cpp_std=c++03'] From 428c757a13addd48f048a302ecdf11d8c29a753f Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 22 Jan 2022 12:02:59 -0500 Subject: [PATCH 49/57] Implemented @guldoman's suggestion for how to close file handles. --- src/api/process.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index cabcbf17..3113843d 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -59,9 +59,9 @@ typedef enum { #ifdef _WIN32 static volatile long PipeSerialNumber; - static void close_fd(HANDLE handle) { CloseHandle(handle); } + static void close_fd(HANDLE* handle) { if (*handle) CloseHandle(*handle); *handle = NULL; } #else - static void close_fd(int fd) { close(fd); } + static void close_fd(int* fd) { if (*fd) close(*fd); *fd = 0; } #endif static bool poll_process(process_t* proc, int timeout) { @@ -90,12 +90,8 @@ static bool poll_process(process_t* proc, int timeout) { if (timeout) SDL_Delay(5); } while (timeout == WAIT_INFINITE || SDL_GetTicks() - ticks < timeout); - if (!proc->running) { - close_fd(proc->child_pipes[STDIN_FD ][1]); - close_fd(proc->child_pipes[STDOUT_FD][0]); - close_fd(proc->child_pipes[STDERR_FD][0]); + if (!proc->running) return false; - } return true; } @@ -273,7 +269,7 @@ static int process_start(lua_State* L) { for (size_t i = 0; i < env_len; ++i) free((char*)env[i]); for (int stream = 0; stream < 3; ++stream) - close_fd(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); + close_fd(&self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); self->running = true; return 1; } @@ -350,7 +346,7 @@ static int f_write(lua_State* L) { static int f_close_stream(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); int stream = luaL_checknumber(L, 2); - close_fd(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); + close_fd(&self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); lua_pushboolean(L, 1); return 1; } @@ -417,7 +413,14 @@ static int self_signal(lua_State* L, signal_e sig) { static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); } static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); } static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); } -static int f_gc(lua_State* L) { return self_signal(L, SIGNAL_TERM); } +static int f_gc(lua_State* L) { + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + signal_process(self, SIGNAL_TERM); + close_fd(&self->child_pipes[STDIN_FD ][1]); + close_fd(&self->child_pipes[STDOUT_FD][0]); + close_fd(&self->child_pipes[STDERR_FD][0]); + return 0; +} static int f_running(lua_State* L) { process_t* self = (process_t*)luaL_checkudata(L, 1, API_TYPE_PROCESS); From ed4128bc65d24c068b5018077c7755b0bc1352e5 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 22 Jan 2022 12:36:30 -0500 Subject: [PATCH 50/57] Added in support for env on linux. --- src/api/process.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index 3113843d..6b4b1bc2 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -117,7 +117,7 @@ static bool signal_process(process_t* proc, signal_e sig) { static int process_start(lua_State* L) { size_t env_len = 0, key_len, val_len; - const char *cmd[256], *env[256] = { NULL }, *cwd = NULL; + const char *cmd[256], *env_names[256] = { NULL }, *env_values[256] = { NULL }, *cwd = NULL; bool detach = false; int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; luaL_checktype(L, 1, LUA_TTABLE); @@ -141,9 +141,12 @@ static int process_start(lua_State* L) { while (lua_next(L, -2) != 0) { const char* key = luaL_checklstring(L, -2, &key_len); const char* val = luaL_checklstring(L, -1, &val_len); - env[env_len] = malloc(key_len+val_len+2); - snprintf((char*)env[env_len++], key_len+val_len+2, "%s=%s", key, val); + env_names[env_len] = malloc(key_len+1); + strcpy((char*)env_names[env_len], key); + env_values[env_len] = malloc(val_len+1); + strcpy((char*)env_values[env_len], val); lua_pop(L, 1); + ++env_len; } } else lua_pop(L, 1); @@ -158,7 +161,6 @@ static int process_start(lua_State* L) { return luaL_error(L, "redirect to handles, FILE* and paths are not supported"); } } - env[env_len] = NULL; process_t* self = lua_newuserdata(L, sizeof(process_t)); memset(self, 0, sizeof(process_t)); @@ -224,11 +226,9 @@ static int process_start(lua_State* L) { strcat(commandLine, cmd[i]); } for (size_t i = 0; i < env_len; ++i) { - size_t len = strlen(env[i]); - if (offset + len >= sizeof(environmentBlock)) + if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock)) break; - memcpy(&environmentBlock[offset], env[i], len); - offset += len; + offset += snprintf(&environmentBlock[offset], sizeof(environmentBlock) - offset, "%s=%s", env_names[i], env_values[i]); environmentBlock[offset++] = 0; } environmentBlock[offset++] = 0; @@ -259,15 +259,19 @@ static int process_start(lua_State* L) { dup2(self->child_pipes[new_fds[stream]][new_fds[stream] == STDIN_FD ? 0 : 1], stream); close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); } - if ((!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) - execvp((const char*)cmd[0], (char* const*)cmd); + int set; + for (set = 0; set < env_len && setenv(env_names[set], env_values[set], 1) == 0; ++set); + if (set == env_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) + execvp((const char*)cmd[0], (char* const*)cmd); const char* msg = strerror(errno); int result = write(STDERR_FD, msg, strlen(msg)+1); - exit(result == strlen(msg)+1 ? -1 : -2); + _exit(result == strlen(msg)+1 ? -1 : -2); } #endif - for (size_t i = 0; i < env_len; ++i) - free((char*)env[i]); + for (size_t i = 0; i < env_len; ++i) { + free((char*)env_names[i]); + free((char*)env_values[i]); + } for (int stream = 0; stream < 3; ++stream) close_fd(&self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); self->running = true; From f9ad83e53e9b4ebc15562b11e31e6786ca145f61 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 22 Jan 2022 13:34:47 -0500 Subject: [PATCH 51/57] Fixed windows not converting utf8 environment block to utf16. --- src/api/process.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index 6b4b1bc2..b85db34a 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -215,16 +215,18 @@ static int process_start(lua_State* L) { siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0]; siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1]; siStartInfo.hStdError = self->child_pipes[STDERR_FD][1]; - char commandLine[32767] = { 0 }, environmentBlock[32767]; - int offset = 0; + char commandLine[32767] = { 0 }, environmentBlock[32767], wideEnvironmentBlock[32767*2]; strcpy(commandLine, cmd[0]); + int offset = 0; for (size_t i = 1; i < cmd_len; ++i) { size_t len = strlen(cmd[i]); - if (offset + len + 1 >= sizeof(commandLine)) + offset += len + 1; + if (offset >= sizeof(commandLine)) break; strcat(commandLine, " "); strcat(commandLine, cmd[i]); } + offset = 0; for (size_t i = 0; i < env_len; ++i) { if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock)) break; @@ -232,7 +234,9 @@ static int process_start(lua_State* L) { environmentBlock[offset++] = 0; } environmentBlock[offset++] = 0; - if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? environmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) + if (env_len > 0) + MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, environmentBlock, offset, (LPWSTR)wideEnvironmentBlock, sizeof(wideEnvironmentBlock)); + if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? wideEnvironmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) return luaL_error(L, "Error creating a process: %d.", GetLastError()); self->pid = (long)self->process_information.dwProcessId; if (detach) From f7193c4fa24d4d600a34d6d8cbff20c1073be380 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 22 Jan 2022 21:46:02 +0100 Subject: [PATCH 52/57] Remove unused whitespace_replacements function --- data/core/init.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 38003179..8810d4b2 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -827,14 +827,6 @@ function core.remove_project_directory(path) end -local function whitespace_replacements() - local r = renderer.replacements.new() - r:add(" ", "·") - r:add("\t", "»") - return r -end - - local function configure_borderless_window() system.set_window_bordered(not config.borderless) core.title_view:configure_hit_test(config.borderless) From bc9f8a4075373a9d597fe75049c8189699f5bae9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 21 Jan 2022 11:08:16 +0100 Subject: [PATCH 53/57] Use new mutex in dmon to avoid possible lock-up We rely on one variable _dmon.modify_watches shared between thread to ensure that we don't lock with the dmon polling thread waiting indefinitely and helding a lock. To ensure that the polling thread sees modifications done to 'modify_watches' we use an additional mutex that act as a memory barrier. --- lib/dmon/dmon.h | 60 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 3f2bc0c5..84258bad 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -159,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 @@ -406,7 +402,8 @@ 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; @@ -492,6 +489,13 @@ _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); @@ -502,7 +506,8 @@ _DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) 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; } @@ -587,6 +592,7 @@ 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); @@ -596,11 +602,20 @@ DMON_API_IMPL void dmon_init(void) } static void dmon__enter_critical_wakeup(void) { - _InterlockedExchange(&_dmon.modify_watches, 1); + 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) @@ -617,8 +632,9 @@ DMON_API_IMPL void dmon_deinit(void) dmon__unwatch(&_dmon.watches[i]); } - LeaveCriticalSection(&_dmon.mutex); + dmon__leave_critical_wakeup(); DeleteCriticalSection(&_dmon.mutex); + DeleteCriticalSection(&_dmon.modify_watches_mutex); stb_sb_free(_dmon.events); _dmon_init = false; } @@ -665,19 +681,16 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, !dmon__refresh_watch(watch)) { dmon__unwatch(watch); *error_code = DMON_ERROR_WATCH_DIR; - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + dmon__leave_critical_wakeup(); return dmon__make_id(0); } } else { *error_code = DMON_ERROR_OPEN_DIR; - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); + 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); } @@ -696,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 @@ -733,7 +745,8 @@ typedef struct dmon__state { int num_watches; pthread_t thread_handle; pthread_mutex_t mutex; - int wait_flag; + volatile int wait_flag; + pthread_mutex_t wait_flag_mutex; int wake_event_pipe[2]; bool quit; } dmon__state; @@ -1013,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); @@ -1028,7 +1048,9 @@ static void* dmon__thread(void* arg) while (!_dmon.quit) { nanosleep(&req, &rem); - if (_dmon.num_watches == 0 || _dmon.wait_flag == 1 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + if (_dmon.num_watches == 0 || + dmon__safe_get_wait_flag() || + pthread_mutex_trylock(&_dmon.mutex) != 0) { continue; } @@ -1106,6 +1128,7 @@ static void* dmon__thread(void* arg) } _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; @@ -1113,6 +1136,7 @@ _DMON_PRIVATE void dmon__mutex_wakeup_lock(void) { 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) From 3e39da071da130e3539227107d273f6d1b6a79ae Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 24 Jan 2022 09:31:40 +0100 Subject: [PATCH 54/57] Fix problem with project module save hook --- data/core/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 8810d4b2..366d1048 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -839,8 +839,9 @@ local function add_config_files_hooks() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") function Doc:save(filename, abs_filename) + local module_filename = system.absolute_path(".lite_project.lua") doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename or self.abs_filename == core.project_module_filename then + if self.abs_filename == user_filename or self.abs_filename == module_filename then reload_customizations() rescan_project_directories() configure_borderless_window() @@ -1158,7 +1159,6 @@ end function core.load_project_module() local filename = ".lite_project.lua" - core.project_module_filename = system.absolute_path(filename) if system.get_file_info(filename) then return core.try(function() local fn, err = loadfile(filename) From 3a53b05b29b409d0602ab91e97d45f182106e41e Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 25 Jan 2022 14:14:56 +0100 Subject: [PATCH 55/57] Do no error out on malformed ignore patterns --- data/core/init.lua | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 366d1048..743d6ca1 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -136,12 +136,15 @@ local function compile_ignore_files() -- 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 - } + -- 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 From 3773a812bd3c65f30d45ba18ee0734bdc19701f4 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Fri, 28 Jan 2022 15:39:57 -0500 Subject: [PATCH 56/57] Incorporate realtakase's suggestions. --- src/api/process.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index cabcbf17..8f8a724b 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -330,8 +330,9 @@ static int f_write(lua_State* L) { #if _WIN32 DWORD dwWritten; if (!WriteFile(self->child_pipes[STDIN_FD][1], data, data_size, &dwWritten, NULL)) { + int lastError = GetLastError(); signal_process(self, SIGNAL_TERM); - return luaL_error(L, "error writing to process: %d", GetLastError()); + return luaL_error(L, "error writing to process: %d", lastError); } length = dwWritten; #else @@ -339,8 +340,9 @@ static int f_write(lua_State* L) { if (length < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) length = 0; else if (length < 0) { + const char* lastError = strerror(errno); signal_process(self, SIGNAL_TERM); - return luaL_error(L, "error writing to process: %s", strerror(errno)); + return luaL_error(L, "error writing to process: %s", lastError); } #endif lua_pushinteger(L, length); From af76f544bef0b65245d0ace610517474a5a33299 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 29 Jan 2022 15:19:22 -0500 Subject: [PATCH 57/57] Fixing performance regression. Due to the way the hashes work, we must 0 out the whole thing. --- src/rencache.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rencache.c b/src/rencache.c index 5d464226..5686d984 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -95,7 +95,7 @@ static Command* push_command(int type, int size) { return NULL; } command_buf_idx = n; - memset(cmd, 0, COMMAND_BARE_SIZE); + memset(cmd, 0, size); cmd->type = type; cmd->size = size; return cmd;