aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changelog.md26
-rw-r--r--data/core/commands/core.lua21
-rw-r--r--data/core/commands/findreplace.lua62
-rw-r--r--data/core/commands/root.lua6
-rw-r--r--data/core/common.lua51
-rw-r--r--data/core/config.lua5
-rw-r--r--data/core/doc/highlighter.lua16
-rw-r--r--data/core/doc/init.lua6
-rw-r--r--data/core/doc/search.lua65
-rw-r--r--data/core/docview.lua4
-rw-r--r--data/core/init.lua528
-rw-r--r--data/core/keymap.lua1
-rw-r--r--data/core/regex.lua4
-rw-r--r--data/core/rootview.lua44
-rw-r--r--data/core/syntax.lua2
-rw-r--r--data/core/tokenizer.lua10
-rw-r--r--data/core/view.lua2
-rw-r--r--data/plugins/autoreload.lua26
-rw-r--r--data/plugins/language_c.lua1
-rw-r--r--data/plugins/language_cpp.lua1
-rw-r--r--data/plugins/language_css.lua1
-rw-r--r--data/plugins/language_html.lua1
-rw-r--r--data/plugins/language_js.lua1
-rw-r--r--data/plugins/language_lua.lua1
-rw-r--r--data/plugins/language_md.lua1
-rw-r--r--data/plugins/language_python.lua23
-rw-r--r--data/plugins/language_xml.lua1
-rw-r--r--data/plugins/scale.lua4
-rw-r--r--data/plugins/treeview.lua49
-rw-r--r--lib/dmon/dmon.h1591
-rw-r--r--lib/dmon/dmon_extra.h162
-rw-r--r--lib/dmon/meson.build1
-rw-r--r--licenses/licenses.md27
-rw-r--r--meson.build10
-rw-r--r--resources/notes-dmon-integration.md54
-rw-r--r--scripts/package.sh2
-rw-r--r--src/api/regex.c7
-rw-r--r--src/api/renderer.c27
-rw-r--r--src/api/system.c154
-rw-r--r--src/dirmonitor.c59
-rw-r--r--src/dirmonitor.h15
-rw-r--r--src/main.c5
-rw-r--r--src/meson.build5
-rw-r--r--src/rencache.c4
44 files changed, 2782 insertions, 304 deletions
diff --git a/changelog.md b/changelog.md
index 57ab9646..c81c7dbe 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,31 @@
This files document the changes done in Lite XL for each release.
+### 2.0.3
+
+Replace periodic rescan of project folder with a notification based system using the
+[dmon library](https://github.com/septag/dmon). Improves performance especially for
+large project folders since the application no longer needs to rescan.
+The application also reports immediatly any change in the project directory even
+when the application is unfocused.
+
+Improved find-replace reverse and forward search.
+
+Fixed a bug in incremental syntax highlighting affecting documents with multiple-lines
+comments or strings.
+
+The application now always shows the tabs in the documents' view even when a single
+document is opened. Can be changed with the option `config.always_show_tabs`.
+
+Fix problem with numeric keypad function keys not properly working.
+
+Fix problem with pixel not correctly drawn at the window's right edge.
+
+Treat correctly and open network paths on Windows.
+
+Add some improvements for very slow network filesystems.
+
+Fix problem with python syntax highliting, contributed by @dflock.
+
### 2.0.2
Fix problem project directory when starting the application from Launcher on macOS.
diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua
index 432ded89..ad0d4b10 100644
--- a/data/core/commands/core.lua
+++ b/data/core/commands/core.lua
@@ -6,10 +6,12 @@ local LogView = require "core.logview"
local fullscreen = false
+local restore_title_view = false
local function suggest_directory(text)
text = common.home_expand(text)
- return common.home_encode_list(text == "" and core.recent_projects or common.dir_path_suggest(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))
end
command.add(nil, {
@@ -27,9 +29,12 @@ command.add(nil, {
["core:toggle-fullscreen"] = function()
fullscreen = not fullscreen
+ if fullscreen then
+ restore_title_view = core.title_view.visible
+ end
system.set_window_mode(fullscreen and "fullscreen" or "normal")
- core.show_title_bar(not fullscreen)
- core.title_view:configure_hit_test(not fullscreen)
+ core.show_title_bar(not fullscreen and restore_title_view)
+ core.title_view:configure_hit_test(not fullscreen and restore_title_view)
end,
["core:reload-module"] = function()
@@ -66,8 +71,8 @@ command.add(nil, {
end,
["core:find-file"] = function()
- if core.project_files_limit then
- return command.perform "core:open-file"
+ if not core.project_files_number() then
+ return command.perform "core:open-file"
end
local files = {}
for dir, item in core.get_project_files() do
@@ -149,7 +154,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) .. PATHSEP)
+ core.command_view:set_text(common.home_encode(dirname))
end
core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text))
@@ -166,7 +171,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) .. PATHSEP)
+ core.command_view:set_text(common.home_encode(dirname))
end
core.command_view:enter("Open Project", function(text, item)
text = common.home_expand(item and item.text or text)
@@ -191,8 +196,6 @@ command.add(nil, {
return
end
core.add_project_directory(system.absolute_path(text))
- -- TODO: add the name of directory to prioritize
- core.reschedule_project_scan()
end, suggest_directory)
end,
diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua
index d1af0d88..8fcb8dbc 100644
--- a/data/core/commands/findreplace.lua
+++ b/data/core/commands/findreplace.lua
@@ -15,7 +15,8 @@ local find_regex = config.find_regex or false
local found_expression
local function doc()
- return core.active_view:is(DocView) and core.active_view.doc or last_view.doc
+ local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView)
+ return is_DocView and core.active_view.doc or (last_view and last_view.doc)
end
local function get_find_tooltip()
@@ -117,7 +118,7 @@ local function has_selection()
end
local function has_unique_selection()
- if not core.active_view:is(DocView) then return false end
+ if not doc() then return false end
local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end
@@ -142,7 +143,7 @@ local function is_in_any_selection(line, col)
return false
end
-local function select_next(all)
+local function select_add_next(all)
local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
local text = doc():get_text(l1, c1, l2, c2)
@@ -161,21 +162,28 @@ local function select_next(all)
end
end
-command.add(has_unique_selection, {
- ["find-replace:select-next"] = function()
- local l1, c1, l2, c2 = doc():get_selection(true)
- local text = doc():get_text(l1, c1, l2, c2)
+local function select_next(reverse)
+ local l1, c1, l2, c2 = doc():get_selection(true)
+ local text = doc():get_text(l1, c1, l2, c2)
+ if reverse then
+ l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
+ else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
- if l2 then doc():set_selection(l2, c2, l1, c1) end
- end,
- ["find-replace:select-add-next"] = function() select_next(false) end,
- ["find-replace:select-add-all"] = function() select_next(true) end
+ end
+ if l2 then doc():set_selection(l2, c2, l1, c1) end
+end
+
+command.add(has_unique_selection, {
+ ["find-replace:select-next"] = select_next,
+ ["find-replace:select-previous"] = function() select_next(true) end,
+ ["find-replace:select-add-next"] = select_add_next,
+ ["find-replace:select-add-all"] = function() select_add_next(true) end
})
command.add("core.docview", {
["find-replace:find"] = function()
- find("Find Text", function(doc, line, col, text, case_sensitive, find_regex)
- local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex }
+ find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse)
+ local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse }
return search.find(doc, line, col, text, opt)
end)
end,
@@ -221,29 +229,29 @@ command.add(valid_for_finding, {
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
- local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex)
+ local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
if line1 then
- if last_view.doc ~= doc() then
- last_finds = {}
- end
- if #last_finds >= max_last_finds then
- table.remove(last_finds, 1)
- end
- table.insert(last_finds, { sl1, sc1, sl2, sc2 })
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
+ else
+ core.error("Couldn't find %q", last_text)
end
end
end,
["find-replace:previous-find"] = function()
- local sel = table.remove(last_finds)
- if not sel or doc() ~= last_view.doc then
- core.error("No previous finds")
- return
+ if not last_fn then
+ core.error("No find to continue from")
+ else
+ local sl1, sc1, sl2, sc2 = doc():get_selection(true)
+ local line1, col1, line2, col2 = last_fn(doc(), sl1, sc1, last_text, case_sensitive, find_regex, true)
+ if line1 then
+ doc():set_selection(line2, col2, line1, col1)
+ last_view:scroll_to_line(line2, true)
+ else
+ core.error("Couldn't find %q", last_text)
+ end
end
- doc():set_selection(table.unpack(sel))
- last_view:scroll_to_line(sel[3], true)
end,
})
diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua
index 8f2536b8..5bf18390 100644
--- a/data/core/commands/root.lua
+++ b/data/core/commands/root.lua
@@ -113,7 +113,8 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end
local node = core.root_view.root_node:get_child_overlapping_point(x, y)
- if not node:get_locked_size() then
+ local sx, sy = node:get_locked_size()
+ if not sx and not sy then
core.set_active_view(node.active_view)
end
end
@@ -121,7 +122,8 @@ end
command.add(function()
local node = core.root_view:get_active_node()
- return not node:get_locked_size()
+ local sx, sy = node:get_locked_size()
+ return not sx and not sy
end, t)
command.add(nil, {
diff --git a/data/core/common.lua b/data/core/common.lua
index 1a1b22cd..ab21b758 100644
--- a/data/core/common.lua
+++ b/data/core/common.lua
@@ -1,8 +1,8 @@
local common = {}
-function common.is_utf8_cont(char)
- local byte = char:byte()
+function common.is_utf8_cont(s, offset)
+ local byte = s:byte(offset or 1)
return byte >= 0x80 and byte < 0xc0
end
@@ -280,24 +280,61 @@ local function split_on_slash(s, sep_pattern)
end
+-- The filename argument given to the function is supposed to
+-- come from system.absolute_path and as such should be an
+-- absolute path without . or .. elements.
+-- This function exists because on Windows the drive letter returned
+-- by system.absolute_path is sometimes with a lower case and sometimes
+-- with an upper case to we normalize to upper case.
+function common.normalize_volume(filename)
+ if not filename then return end
+ if PATHSEP == '\\' then
+ local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
+ if drive then
+ return drive:upper() .. rem
+ end
+ end
+ return filename
+end
+
+
function common.normalize_path(filename)
if not filename then return end
+ local volume
if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
- local drive, rem = filename:match('^([a-zA-Z])(:.*)')
- filename = drive and drive:upper() .. rem or filename
+ local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
+ if drive then
+ volume, filename = drive:upper(), rem
+ else
+ drive, rem = filename:match('^(\\\\[^\\]+\\[^\\]+\\)(.*)')
+ if drive then
+ volume, filename = drive, rem
+ end
+ end
+ else
+ local relpath = filename:match('^/(.+)')
+ if relpath then
+ volume, filename = "/", relpath
+ end
end
local parts = split_on_slash(filename, PATHSEP)
local accu = {}
for _, part in ipairs(parts) do
- if part == '..' and #accu > 0 and accu[#accu] ~= ".." then
- table.remove(accu)
+ if part == '..' then
+ if #accu > 0 and accu[#accu] ~= ".." then
+ table.remove(accu)
+ elseif volume then
+ error("invalid path " .. volume .. filename)
+ else
+ table.insert(accu, part)
+ end
elseif part ~= '.' then
table.insert(accu, part)
end
end
local npath = table.concat(accu, PATHSEP)
- return npath == "" and PATHSEP or npath
+ return (volume or "") .. (npath == "" and PATHSEP or npath)
end
diff --git a/data/core/config.lua b/data/core/config.lua
index faffc27e..71e83994 100644
--- a/data/core/config.lua
+++ b/data/core/config.lua
@@ -1,6 +1,5 @@
local config = {}
-config.project_scan_rate = 5
config.fps = 60
config.max_log_items = 80
config.message_timeout = 5
@@ -12,8 +11,8 @@ config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3
config.max_undos = 10000
-config.max_tabs = 10
-config.always_show_tabs = false
+config.max_tabs = 8
+config.always_show_tabs = true
config.highlight_current_line = true
config.line_height = 1.2
config.indent_size = 2
diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua
index 4cb703da..9ba7b634 100644
--- a/data/core/doc/highlighter.lua
+++ b/data/core/doc/highlighter.lua
@@ -1,4 +1,5 @@
local core = require "core"
+local common = require "core.common"
local config = require "core.config"
local tokenizer = require "core.tokenizer"
local Object = require "core.object"
@@ -40,6 +41,13 @@ end
function Highlighter:reset()
self.lines = {}
+ self:soft_reset()
+end
+
+function Highlighter:soft_reset()
+ for i=1,#self.lines do
+ self.lines[i] = false
+ end
self.first_invalid_line = 1
self.max_wanted_line = 0
end
@@ -51,16 +59,16 @@ end
function Highlighter:insert_notify(line, n)
self:invalidate(line)
+ local blanks = { }
for i = 1, n do
- table.insert(self.lines, line, nil)
+ blanks[i] = false
end
+ common.splice(self.lines, line, 0, blanks)
end
function Highlighter:remove_notify(line, n)
self:invalidate(line)
- for i = 1, n do
- table.remove(self.lines, line)
- end
+ common.splice(self.lines, line, n)
end
diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua
index 640e9fd5..03dcc31e 100644
--- a/data/core/doc/init.lua
+++ b/data/core/doc/init.lua
@@ -47,7 +47,7 @@ function Doc:reset_syntax()
local syn = syntax.get(self.filename or "", header)
if self.syntax ~= syn then
self.syntax = syn
- self.highlighter:reset()
+ self.highlighter:soft_reset()
end
end
@@ -62,12 +62,15 @@ function Doc:load(filename)
local fp = assert( io.open(filename, "rb") )
self:reset()
self.lines = {}
+ local i = 1
for line in fp:lines() do
if line:byte(-1) == 13 then
line = line:sub(1, -2)
self.crlf = true
end
table.insert(self.lines, line .. "\n")
+ self.highlighter.lines[i] = false
+ i = i + 1
end
if #self.lines == 0 then
table.insert(self.lines, "\n")
@@ -306,6 +309,7 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then
self.selections = { table.unpack(cmd) }
+ self:sanitize_selection()
end
modified = modified or (cmd.type ~= "selection")
diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua
index 04090673..8395769a 100644
--- a/data/core/doc/search.lua
+++ b/data/core/doc/search.lua
@@ -22,37 +22,62 @@ local function init_args(doc, line, col, text, opt)
return doc, line, col, text, opt
end
+-- This function is needed to uniform the behavior of
+-- `regex:cmatch` and `string.find`.
+local function regex_func(text, re, index, _)
+ local s, e = re:cmatch(text, index)
+ return s, e and e - 1
+end
+
+local function rfind(func, text, pattern, index, plain)
+ local s, e = func(text, pattern, 1, plain)
+ local last_s, last_e
+ if index < 0 then index = #text - index + 1 end
+ while e and e <= index do
+ last_s, last_e = s, e
+ s, e = func(text, pattern, s + 1, plain)
+ end
+ return last_s, last_e
+end
+
function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
-
- local re
+ local plain = not opt.pattern
+ local pattern = text
+ local search_func = string.find
if opt.regex then
- re = regex.compile(text, opt.no_case and "i" or "")
+ pattern = regex.compile(text, opt.no_case and "i" or "")
+ search_func = regex_func
end
- for line = line, #doc.lines do
+ local start, finish, step = line, #doc.lines, 1
+ if opt.reverse then
+ start, finish, step = line, 1, -1
+ end
+ for line = start, finish, step do
local line_text = doc.lines[line]
- if opt.regex then
- local s, e = re:cmatch(line_text, col)
- if s then
- return line, s, line, e
- end
- col = 1
+ if opt.no_case and not opt.regex then
+ line_text = line_text:lower()
+ end
+ local s, e
+ if opt.reverse then
+ s, e = rfind(search_func, line_text, pattern, col - 1, plain)
else
- if opt.no_case then
- line_text = line_text:lower()
- end
- local s, e = line_text:find(text, col, true)
- if s then
- return line, s, line, e + 1
- end
- col = 1
+ s, e = search_func(line_text, pattern, col, plain)
+ end
+ if s then
+ return line, s, line, e + 1
end
+ col = opt.reverse and -1 or 1
end
if opt.wrap then
- opt = { no_case = opt.no_case, regex = opt.regex }
- return search.find(doc, 1, 1, text, opt)
+ opt = { no_case = opt.no_case, regex = opt.regex, reverse = opt.reverse }
+ if opt.reverse then
+ return search.find(doc, #doc.lines, #doc.lines[#doc.lines], text, opt)
+ else
+ return search.find(doc, 1, 1, text, opt)
+ end
end
end
diff --git a/data/core/docview.lua b/data/core/docview.lua
index 60ef62bc..487a7784 100644
--- a/data/core/docview.lua
+++ b/data/core/docview.lua
@@ -410,7 +410,9 @@ function DocView:draw()
local pos = self.position
x, y = self:get_line_screen_position(minline)
- core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
+ -- the clip below ensure we don't write on the gutter region. On the
+ -- right side it is redundant with the Node's clip.
+ core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y)
for i = minline, maxline do
self:draw_line_body(i, x, y)
y = y + lh
diff --git a/data/core/init.lua b/data/core/init.lua
index d07f1cbc..daec49a6 100644
--- a/data/core/init.lua
+++ b/data/core/init.lua
@@ -36,7 +36,7 @@ end
local function update_recents_project(action, dir_path_abs)
- local dirname = common.normalize_path(dir_path_abs)
+ local dirname = common.normalize_volume(dir_path_abs)
if not dirname then return end
local recents = core.recent_projects
local n = #recents
@@ -52,23 +52,13 @@ local function update_recents_project(action, dir_path_abs)
end
-function core.reschedule_project_scan()
- if core.project_scan_thread_id then
- core.threads[core.project_scan_thread_id].wake = 0
- end
-end
-
-
function core.set_project_dir(new_dir, change_project_fn)
local chdir_ok = pcall(system.chdir, new_dir)
if chdir_ok then
if change_project_fn then change_project_fn() end
- core.project_dir = common.normalize_path(new_dir)
+ core.project_dir = common.normalize_volume(new_dir)
core.project_directories = {}
core.add_project_directory(new_dir)
- core.project_files = {}
- core.project_files_limit = false
- core.reschedule_project_scan()
return true
end
return false
@@ -102,6 +92,29 @@ local function compare_file(a, b)
return a.filename < b.filename
end
+
+-- compute a file's info entry completed with "filename" to be used
+-- in project scan or falsy if it shouldn't appear in the list.
+local function get_project_file_info(root, file)
+ 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)
+ end
+end
+
+
+-- Predicate function to inhibit directory recursion in get_directory_files
+-- based on a time limit and the number of files.
+local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
+ local n_limit = entries_count <= config.max_project_files
+ local t_limit = t_elapsed < 20 / config.fps
+ return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
+end
+
+
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string.
@@ -110,34 +123,31 @@ 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(root, path, t, recursive, begin_hook)
+local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end
- local size_limit = config.file_size_limit * 10e5
+ local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
+ local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
- local entries_count = 0
- local max_entries = config.max_project_files
for _, file in ipairs(all) do
- if not common.match_pattern(file, config.ignore_files) then
- local file = path .. PATHSEP .. file
- local info = system.get_file_info(root .. file)
- if info and info.size < size_limit then
- info.filename = strip_leading_path(file)
- table.insert(info.type == "dir" and dirs or files, info)
- entries_count = entries_count + 1
- if recursive and entries_count > max_entries then return nil, entries_count end
- end
+ local info = get_project_file_info(root, path .. PATHSEP .. file)
+ if info then
+ table.insert(info.type == "dir" and dirs or files, info)
+ entries_count = entries_count + 1
end
end
+ local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
- if recursive and entries_count <= max_entries then
- local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive)
- entries_count = entries_count + subdir_count
- f.scanned = true
+ 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)
+ recurse_complete = recurse_complete and complete
+ entries_count = n
+ else
+ recurse_complete = false
end
end
@@ -146,135 +156,319 @@ local function get_directory_files(root, path, t, recursive, begin_hook)
table.insert(t, f)
end
- return t, entries_count
+ return t, recurse_complete, entries_count
end
-local function project_scan_thread()
- local function diff_files(a, b)
- if #a ~= #b then return true end
- for i, v in ipairs(a) do
- if b[i].filename ~= v.filename
- or b[i].modified ~= v.modified then
- return true
- end
+
+function core.project_subdir_set_show(dir, filename, show)
+ dir.shown_subdir[filename] = show
+ if dir.files_limit and PLATFORM == "Linux" then
+ local fullpath = dir.name .. PATHSEP .. filename
+ local watch_fn = show and system.watch_dir_add or system.watch_dir_rm
+ local success = watch_fn(dir.watch_id, fullpath)
+ if not success then
+ core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
end
end
+end
- while true do
- -- get project files and replace previous table if the new table is
- -- different
- local i = 1
- while not core.project_files_limit and i <= #core.project_directories do
- local dir = core.project_directories[i]
- local t, entries_count = get_directory_files(dir.name, "", {}, true)
- if diff_files(dir.files, t) then
- if entries_count > config.max_project_files then
- core.project_files_limit = true
- core.status_view:show_message("!", style.accent,
- "Too many files in project directory: stopped reading at "..
- config.max_project_files.." files. For more information see "..
- "usage.md at github.com/franko/lite-xl."
- )
- end
- dir.files = t
- core.redraw = true
- end
- if dir.name == core.project_dir then
- core.project_files = dir.files
- end
- i = i + 1
+
+function core.project_subdir_is_shown(dir, filename)
+ return not dir.files_limit or dir.shown_subdir[filename]
+end
+
+
+local function show_max_files_warning(dir)
+ local message = dir.slow_filesystem and
+ "Filesystem is too slow: project files will not be indexed." or
+ "Too many files in project directory: stopped reading at "..
+ config.max_project_files.." files. For more information see "..
+ "usage.md at github.com/franko/lite-xl."
+ core.status_view:show_message("!", style.accent, message)
+end
+
+
+local function file_search(files, info)
+ local filename, type = info.filename, info.type
+ local inf, sup = 1, #files
+ 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
+ sup = curr - 1
+ else
+ inf = curr
end
+ end
+ repeat
+ 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)
+ return inf, false
+end
- -- wait for next scan
- coroutine.yield(config.project_scan_rate)
+
+local function project_scan_add_entry(dir, fileinfo)
+ local index, match = file_search(dir.files, fileinfo)
+ if not match then
+ table.insert(dir.files, index, fileinfo)
+ dir.is_dirty = true
end
end
-function core.is_project_folder(dirname)
- for _, dir in ipairs(core.project_directories) do
- if dir.name == dirname then
- return true
+local function files_info_equal(a, b)
+ return a.filename == b.filename and a.type == b.type
+end
+
+-- for "a" inclusive from i1 + 1 and i1 + n
+local function files_list_match(a, i1, n, b)
+ if n ~= #b then return false end
+ for i = 1, n do
+ if not files_info_equal(a[i1 + i], b[i]) then
+ return false
end
end
- return false
+ return true
end
+-- arguments like for files_list_match
+local function files_list_replace(as, i1, n, bs)
+ local m = #bs
+ local i, j = 1, 1
+ while i <= m or i <= n do
+ local a, b = as[i1 + i], bs[j]
+ if i > n or (j <= m and not files_info_equal(a, b) and
+ not system.path_compare(a.filename, a.type, b.filename, b.type))
+ then
+ table.insert(as, i1 + i, b)
+ i, j, n = i + 1, j + 1, n + 1
+ elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then
+ table.remove(as, i1 + i)
+ n = n - 1
+ else
+ i, j = i + 1, j + 1
+ end
+ end
+end
-function core.scan_project_folder(dirname, filename)
- for _, dir in ipairs(core.project_directories) do
- if dir.name == dirname then
- for i, file in ipairs(dir.files) do
- local file = dir.files[i]
- if file.filename == filename then
- if file.scanned then return end
- local new_files = get_directory_files(dirname, PATHSEP .. filename, {})
- for j, new_file in ipairs(new_files) do
- table.insert(dir.files, i + j, new_file)
- end
- file.scanned = true
- return
+local function project_subdir_bounds(dir, filename)
+ local index, n = 0, #dir.files
+ for i, file in ipairs(dir.files) do
+ local file = dir.files[i]
+ if file.filename == filename then
+ index, n = i, #dir.files - i
+ for j = 1, #dir.files - i do
+ if not common.path_belongs_to(dir.files[i + j].filename, filename) then
+ n = j - 1
+ break
end
end
+ return index, n, file
+ end
+ end
+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 index, n = 0, #dir.files
+ if filename_rooted ~= "" then
+ local filename = strip_leading_path(filename_rooted)
+ index, n = project_subdir_bounds(dir, filename)
+ end
+
+ if not files_list_match(dir.files, index, n, new_files) then
+ files_list_replace(dir.files, index, n, new_files)
+ dir.is_dirty = true
+ return true
+ end
+end
+
+
+local function add_dir_scan_thread(dir)
+ core.add_thread(function()
+ while true do
+ local has_changes = rescan_project_subdir(dir, "")
+ if has_changes then
+ core.redraw = true -- we run without an event, from a thread
+ end
+ coroutine.yield(5)
end
+ end)
+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")
+ end
+ local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred)
+ 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
+ core.dir_rescan_add_job(dir, ".")
end
end
-local function find_project_files_co(root, path)
- local size_limit = config.file_size_limit * 10e5
+function core.add_project_directory(path)
+ -- top directories has a file-like "item" but the item.filename
+ -- will be simply the name of the directory, without its path.
+ -- The field item.topdir will identify it as a top level directory.
+ path = common.normalize_volume(path)
+ local dir = {
+ name = path,
+ item = {filename = common.basename(path), type = "dir", topdir = true},
+ files_limit = false,
+ is_dirty = true,
+ shown_subdir = {},
+ }
+ table.insert(core.project_directories, dir)
+ scan_project_folder(#core.project_directories)
+ if path == core.project_dir then
+ core.project_files = dir.files
+ end
+ core.redraw = true
+end
+
+
+function core.update_project_subdir(dir, filename, expanded)
+ local index, n, file = project_subdir_bounds(dir, filename)
+ if index then
+ local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
+ files_list_replace(dir.files, index, n, new_files)
+ dir.is_dirty = true
+ return true
+ end
+end
+
+
+-- Find files and directories recursively reading from the filesystem.
+-- Filter files and yields file's directory and info table. This latter
+-- is filled to be like required by project directories "files" list.
+local function find_files_rec(root, path)
local all = system.list_dir(root .. path) or {}
for _, file in ipairs(all) do
- if not common.match_pattern(file, config.ignore_files) then
- local file = path .. PATHSEP .. file
- local info = system.get_file_info(root .. file)
- if info and info.size < size_limit then
- info.filename = strip_leading_path(file)
- if info.type == "file" then
- coroutine.yield(root, info)
- else
- find_project_files_co(root, PATHSEP .. info.filename)
- end
+ local file = path .. PATHSEP .. file
+ local info = system.get_file_info(root .. file)
+ if info then
+ info.filename = strip_leading_path(file)
+ if info.type == "file" then
+ coroutine.yield(root, info)
+ else
+ find_files_rec(root, PATHSEP .. info.filename)
end
end
end
end
+-- Iterator function to list all project files
local function project_files_iter(state)
local dir = core.project_directories[state.dir_index]
- state.file_index = state.file_index + 1
- while dir and state.file_index > #dir.files do
- state.dir_index = state.dir_index + 1
- state.file_index = 1
- dir = core.project_directories[state.dir_index]
+ if state.co then
+ -- We have a coroutine to fetch for files, use the coroutine.
+ -- Used for directories that exceeds the files nuumber limit.
+ local ok, name, file = coroutine.resume(state.co, dir.name, "")
+ if ok and name then
+ return name, file
+ else
+ -- The coroutine terminated, increment file/dir counter to scan
+ -- next project directory.
+ state.co = false
+ state.file_index = 1
+ state.dir_index = state.dir_index + 1
+ dir = core.project_directories[state.dir_index]
+ end
+ else
+ -- Increase file/dir counter
+ state.file_index = state.file_index + 1
+ while dir and state.file_index > #dir.files do
+ state.dir_index = state.dir_index + 1
+ state.file_index = 1
+ dir = core.project_directories[state.dir_index]
+ end
end
if not dir then return end
+ if dir.files_limit then
+ -- The current project directory is files limited: create a couroutine
+ -- to read files from the filesystem.
+ state.co = coroutine.create(find_files_rec)
+ return project_files_iter(state)
+ end
return dir.name, dir.files[state.file_index]
end
function core.get_project_files()
- if core.project_files_limit then
- return coroutine.wrap(function()
- for _, dir in ipairs(core.project_directories) do
- find_project_files_co(dir.name, "")
- end
- end)
- else
- local state = { dir_index = 1, file_index = 0 }
- return project_files_iter, state
- end
+ local state = { dir_index = 1, file_index = 0 }
+ return project_files_iter, state
end
function core.project_files_number()
- if not core.project_files_limit then
- local n = 0
- for i = 1, #core.project_directories do
- n = n + #core.project_directories[i].files
+ local n = 0
+ for i = 1, #core.project_directories do
+ if core.project_directories[i].files_limit then return end
+ n = n + #core.project_directories[i].files
+ end
+ return n
+end
+
+
+local function project_dir_by_watch_id(watch_id)
+ for i = 1, #core.project_directories do
+ if core.project_directories[i].watch_id == watch_id then
+ return core.project_directories[i]
+ end
+ end
+end
+
+
+local function project_scan_remove_file(dir, filepath)
+ local fileinfo = { filename = filepath }
+ for _, filetype in ipairs {"dir", "file"} do
+ fileinfo.type = filetype
+ local index, match = file_search(dir.files, fileinfo)
+ if match then
+ table.remove(dir.files, index)
+ dir.is_dirty = true
+ return
end
- return n
+ end
+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 fileinfo then
+ project_scan_add_entry(dir, fileinfo)
end
end
@@ -371,19 +565,6 @@ function core.load_user_directory()
end
-function core.add_project_directory(path)
- -- top directories has a file-like "item" but the item.filename
- -- will be simply the name of the directory, without its path.
- -- The field item.topdir will identify it as a top level directory.
- path = common.normalize_path(path)
- table.insert(core.project_directories, {
- name = path,
- item = {filename = common.basename(path), type = "dir", topdir = true},
- files = {}
- })
-end
-
-
function core.remove_project_directory(path)
-- skip the fist directory because it is the project's directory
for i = 2, #core.project_directories do
@@ -422,9 +603,9 @@ function core.init()
Doc = require "core.doc"
if PATHSEP == '\\' then
- USERDIR = common.normalize_path(USERDIR)
- DATADIR = common.normalize_path(DATADIR)
- EXEDIR = common.normalize_path(EXEDIR)
+ USERDIR = common.normalize_volume(USERDIR)
+ DATADIR = common.normalize_volume(DATADIR)
+ EXEDIR = common.normalize_volume(EXEDIR)
end
do
@@ -509,7 +690,6 @@ function core.init()
cur_node = cur_node:split("down", core.command_view, {y = true})
cur_node = cur_node:split("down", core.status_view, {y = true})
- core.project_scan_thread_id = core.add_thread(project_scan_thread)
command.add_defaults()
local got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins()
@@ -520,6 +700,12 @@ function core.init()
end
local got_project_error = not core.load_project_module()
+ -- We assume we have just a single project directory here. Now that StatusView
+ -- is there show max files warning if needed.
+ if core.project_directories[1].files_limit then
+ show_max_files_warning(core.project_directories[1])
+ end
+
for _, filename in ipairs(files) do
core.root_view:open_doc(core.open_doc(filename))
end
@@ -910,6 +1096,84 @@ function core.try(fn, ...)
return false, err
end
+local scheduled_rescan = {}
+
+function core.has_pending_rescan()
+ for _ in pairs(scheduled_rescan) do
+ return true
+ end
+end
+
+
+function core.dir_rescan_add_job(dir, filepath)
+ local dirpath = filepath:match("^(.+)[/\\].+$")
+ local dirpath_rooted = dirpath and PATHSEP .. dirpath or ""
+ local abs_dirpath = dir.name .. dirpath_rooted
+ if dirpath then
+ -- check if the directory is in the project files list, if not exit
+ local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"})
+ -- Note that is dir_match is false dir_index greaten than the last valid index.
+ -- We use dir_index to index dir.files below only if dir_match is true.
+ if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end
+ end
+ local new_time = system.get_time() + 1
+
+ -- evaluate new rescan request versus existing rescan
+ local remove_list = {}
+ for _, rescan in pairs(scheduled_rescan) do
+ if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then
+ -- abs_dirpath is a subpath of a scan already ongoing: skip
+ rescan.time_limit = new_time
+ return
+ elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then
+ -- abs_dirpath already cover this rescan: add to the list of rescan to be removed
+ table.insert(remove_list, rescan.abs_path)
+ end
+ end
+ for _, key_path in ipairs(remove_list) do
+ scheduled_rescan[key_path] = nil
+ end
+
+ scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time}
+ core.add_thread(function()
+ while true do
+ local rescan = scheduled_rescan[abs_dirpath]
+ if not rescan then return end
+ if system.get_time() > rescan.time_limit then
+ local has_changes = rescan_project_subdir(rescan.dir, rescan.path)
+ if has_changes then
+ core.redraw = true -- we run without an event, from a thread
+ rescan.time_limit = new_time
+ else
+ scheduled_rescan[rescan.abs_path] = nil
+ return
+ end
+ end
+ coroutine.yield(0.2)
+ end
+ end)
+end
+
+
+-- no-op but can be overrided by plugins
+function core.on_dirmonitor_modify()
+end
+
+
+function core.on_dir_change(watch_id, action, filepath)
+ local dir = project_dir_by_watch_id(watch_id)
+ if not dir then return end
+ core.dir_rescan_add_job(dir, filepath)
+ if action == "delete" then
+ project_scan_remove_file(dir, filepath)
+ elseif action == "create" then
+ project_scan_add_file(dir, filepath)
+ core.on_dirmonitor_modify(dir, filepath);
+ elseif action == "modify" then
+ core.on_dirmonitor_modify(dir, filepath);
+ end
+end
+
function core.on_event(type, ...)
local did_keymap = false
@@ -950,6 +1214,8 @@ function core.on_event(type, ...)
end
elseif type == "focuslost" then
core.root_view:on_focus_lost(...)
+ elseif type == "dirchange" then
+ core.on_dir_change(...)
elseif type == "quit" then
core.quit()
end
@@ -1056,7 +1322,7 @@ function core.run()
while true do
core.frame_start = system.get_time()
local did_redraw = core.step()
- local need_more_work = run_threads()
+ local need_more_work = run_threads() or core.has_pending_rescan()
if core.restart_request or core.quit_request then break end
if not did_redraw and not need_more_work then
idle_iterations = idle_iterations + 1
diff --git a/data/core/keymap.lua b/data/core/keymap.lua
index fd552f19..b076629b 100644
--- a/data/core/keymap.lua
+++ b/data/core/keymap.lua
@@ -210,6 +210,7 @@ keymap.add_direct {
["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
["ctrl+f3"] = "find-replace:select-next",
+ ["ctrl+shift+f3"] = "find-replace:select-previous",
["ctrl+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments",
diff --git a/data/core/regex.lua b/data/core/regex.lua
index 69203cbd..637d23fd 100644
--- a/data/core/regex.lua
+++ b/data/core/regex.lua
@@ -1,4 +1,3 @@
-
-- So that in addition to regex.gsub(pattern, string), we can also do
-- pattern:gsub(string).
regex.__index = function(table, key) return regex[key]; end
@@ -6,7 +5,8 @@ regex.__index = function(table, key) return regex[key]; end
regex.match = function(pattern_string, string, offset, options)
local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string)
- return regex.cmatch(pattern, string, offset or 1, options or 0)
+ local s, e = regex.cmatch(pattern, string, offset or 1, options or 0)
+ return s, e and e - 1
end
-- Will iterate back through any UTF-8 bytes so that we don't replace bits
diff --git a/data/core/rootview.lua b/data/core/rootview.lua
index 07f8b7bf..49da2923 100644
--- a/data/core/rootview.lua
+++ b/data/core/rootview.lua
@@ -149,10 +149,17 @@ function Node:remove_view(root, view)
else
locked_size = locked_size_y
end
- if self.is_primary_node or locked_size then
+ local next_primary
+ if self.is_primary_node then
+ next_primary = core.root_view:select_next_primary_node()
+ end
+ if locked_size or (self.is_primary_node and not next_primary) then
self.views = {}
self:add_view(EmptyView())
else
+ if other == next_primary then
+ next_primary = parent
+ end
parent:consume(other)
local p = parent
while p.type ~= "leaf" do
@@ -160,7 +167,7 @@ function Node:remove_view(root, view)
end
p:set_active_view(p.active_view)
if self.is_primary_node then
- p.is_primary_node = true
+ next_primary.is_primary_node = true
end
end
end
@@ -411,15 +418,8 @@ end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
- local n
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
- if x1 then
- n = x1 + ds
- elseif x2 then
- n = self.size[x] - x2
- else
- n = math.floor(self.size[x] * self.divider)
- end
+ local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
self.a.position[x] = self.position[x]
self.a.position[y] = self.position[y]
self.a.size[x] = n - ds
@@ -602,7 +602,7 @@ function Node:draw()
self:draw_tabs()
end
local pos, size = self.active_view.position, self.active_view.size
- core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
+ core.push_clip_rect(pos.x, pos.y, size.x, size.y)
self.active_view:draw()
core.pop_clip_rect()
else
@@ -682,6 +682,10 @@ end
function Node:resize(axis, value)
+ -- the application works fine with non-integer values but to have pixel-perfect
+ -- placements of view elements, like the scrollbar, we round the value to be
+ -- an integer.
+ value = math.floor(value)
if self.type == 'leaf' then
-- If it is not locked we don't accept the
-- resize operation here because for proportional panes the resize is
@@ -826,6 +830,24 @@ function RootView:get_primary_node()
end
+local function select_next_primary_node(node)
+ if node.is_primary_node then return end
+ if node.type ~= "leaf" then
+ return select_next_primary_node(node.a) or select_next_primary_node(node.b)
+ else
+ local lx, ly = node:get_locked_size()
+ if not lx and not ly then
+ return node
+ end
+ end
+end
+
+
+function RootView:select_next_primary_node()
+ return select_next_primary_node(self.root_node)
+end
+
+
function RootView:open_doc(doc)
local node = self:get_active_node_default()
for i, view in ipairs(node.views) do
diff --git a/data/core/syntax.lua b/data/core/syntax.lua
index a763ac78..de8ec9d0 100644
--- a/data/core/syntax.lua
+++ b/data/core/syntax.lua
@@ -22,7 +22,7 @@ end
function syntax.get(filename, header)
return find(filename, "files")
- or find(header, "headers")
+ or (header and find(header, "headers"))
or plain_text_syntax
end
diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua
index bdf6197b..f77fed44 100644
--- a/data/core/tokenizer.lua
+++ b/data/core/tokenizer.lua
@@ -1,4 +1,5 @@
local syntax = require "core.syntax"
+local common = require "core.common"
local tokenizer = {}
@@ -142,8 +143,13 @@ function tokenizer.tokenize(incoming_syntax, text, state)
code = p._regex
end
repeat
- res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) }
- or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) }
+ local next = res[2] + 1
+ -- go to the start of the next utf-8 character
+ while text:byte(next) and common.is_utf8_cont(text, next) do
+ next = next + 1
+ end
+ res = p.pattern and { text:find(at_start and "^" .. code or code, next) }
+ or { regex.match(code, text, next, at_start and regex.ANCHORED or 0) }
if res[1] and close and target[3] then
local count = 0
for i = res[1] - 1, 1, -1 do
diff --git a/data/core/view.lua b/data/core/view.lua
index d6d1bcbc..4b787d46 100644
--- a/data/core/view.lua
+++ b/data/core/view.lua
@@ -136,7 +136,7 @@ end
function View:draw_background(color)
local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y
- renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
+ renderer.draw_rect(x, y, w, h, color)
end
diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua
index e772666f..9978092e 100644
--- a/data/plugins/autoreload.lua
+++ b/data/plugins/autoreload.lua
@@ -3,7 +3,6 @@ local core = require "core"
local config = require "core.config"
local Doc = require "core.doc"
-
local times = setmetatable({}, { __mode = "k" })
local function update_time(doc)
@@ -11,7 +10,6 @@ local function update_time(doc)
times[doc] = info.modified
end
-
local function reload_doc(doc)
local fp = io.open(doc.filename, "r")
local text = fp:read("*a")
@@ -27,23 +25,19 @@ local function reload_doc(doc)
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end
+local on_modify = core.on_dirmonitor_modify
-core.add_thread(function()
- while true do
- -- check all doc modified times
- for _, doc in ipairs(core.docs) do
- local info = system.get_file_info(doc.filename or "")
- if info and times[doc] ~= info.modified then
- reload_doc(doc)
- end
- coroutine.yield()
+core.on_dirmonitor_modify = function(dir, filepath)
+ local abs_filename = dir.name .. PATHSEP .. filepath
+ for _, doc in ipairs(core.docs) do
+ local info = system.get_file_info(doc.filename or "")
+ if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then
+ reload_doc(doc)
+ break
end
-
- -- wait for next scan
- coroutine.yield(config.project_scan_rate)
end
-end)
-
+ on_modify(dir, filepath)
+end
-- patch `Doc.save|load` to store modified time
local load = Doc.load
diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua
index 44c3b895..b0a4dec5 100644
--- a/data/plugins/language_c.lua
+++ b/data/plugins/language_c.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "C",
files = { "%.c$", "%.h$", "%.inl$" },
comment = "//",
patterns = {
diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua
index 499a09db..8d6aef4b 100644
--- a/data/plugins/language_cpp.lua
+++ b/data/plugins/language_cpp.lua
@@ -4,6 +4,7 @@ pcall(require, "plugins.language_c")
local syntax = require "core.syntax"
syntax.add {
+ name = "C++",
files = {
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua
index 222e2f94..395e375c 100644
--- a/data/plugins/language_css.lua
+++ b/data/plugins/language_css.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "CSS",
files = { "%.css$" },
patterns = {
{ pattern = "\\.", type = "normal" },
diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua
index cebb3f1a..1f4515bc 100644
--- a/data/plugins/language_html.lua
+++ b/data/plugins/language_html.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "HTML",
files = { "%.html?$" },
patterns = {
{
diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua
index dd1151eb..d9515d52 100644
--- a/data/plugins/language_js.lua
+++ b/data/plugins/language_js.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$" },
comment = "//",
patterns = {
diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua
index 165633b6..5c770d43 100644
--- a/data/plugins/language_lua.lua
+++ b/data/plugins/language_lua.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "Lua",
files = "%.lua$",
headers = "^#!.*[ /]lua",
comment = "--",
diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua
index 3c1c329a..62cb8a86 100644
--- a/data/plugins/language_md.lua
+++ b/data/plugins/language_md.lua
@@ -4,6 +4,7 @@ local syntax = require "core.syntax"
syntax.add {
+ name = "Markdown",
files = { "%.md$", "%.markdown$" },
patterns = {
{ pattern = "\\.", type = "normal" },
diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua
index 252a0d14..f1430fb1 100644
--- a/data/plugins/language_python.lua
+++ b/data/plugins/language_python.lua
@@ -2,20 +2,21 @@
local syntax = require "core.syntax"
syntax.add {
- files = { "%.py$", "%.pyw$", "%.rpy$" },
+ name = "Python",
+ files = { "%.py$", "%.pyw$" },
headers = "^#!.*[ /]python",
comment = "#",
patterns = {
- { pattern = { "#", "\n" }, type = "comment" },
- { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
- { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
- { pattern = { '"""', '"""' }, type = "string" },
- { pattern = "0x[%da-fA-F]+", type = "number" },
- { pattern = "-?%d+[%d%.eE]*", type = "number" },
- { pattern = "-?%.?%d+", type = "number" },
- { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
- { pattern = "[%a_][%w_]*%f[(]", type = "function" },
- { pattern = "[%a_][%w_]*", type = "symbol" },
+ { pattern = { "#", "\n" }, type = "comment" },
+ { pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
+ { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
+ { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
+ { pattern = "0x[%da-fA-F]+", type = "number" },
+ { pattern = "-?%d+[%d%.eE]*", type = "number" },
+ { pattern = "-?%.?%d+", type = "number" },
+ { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
+ { pattern = "[%a_][%w_]*%f[(]", type = "function" },
+ { pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["class"] = "keyword",
diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua
index 95e310bb..c858d3cf 100644
--- a/data/plugins/language_xml.lua
+++ b/data/plugins/language_xml.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "XML",
files = { "%.xml$" },
headers = "<%?xml",
patterns = {
diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua
index 56eabbb0..616ee40b 100644
--- a/data/plugins/scale.lua
+++ b/data/plugins/scale.lua
@@ -54,6 +54,10 @@ local function set_scale(scale)
renderer.font.set_size(font, s * font:get_size())
end
+ for _, font in pairs(style.syntax_fonts) do
+ renderer.font.set_size(font, s * font:get_size())
+ end
+
-- restore scroll positions
for view, n in pairs(scrolls) do
view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua
index 70dca08f..659393ec 100644
--- a/data/plugins/treeview.lua
+++ b/data/plugins/treeview.lua
@@ -41,7 +41,6 @@ function TreeView:new()
self.init_size = true
self.target_size = default_treeview_size
self.cache = {}
- self.last = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
end
@@ -54,7 +53,7 @@ function TreeView:set_target_size(axis, value)
end
-function TreeView:get_cached(item, dirname)
+function TreeView:get_cached(dir, item, dirname)
local dir_cache = self.cache[dirname]
if not dir_cache then
dir_cache = {}
@@ -80,6 +79,7 @@ function TreeView:get_cached(item, dirname)
end
t.name = basename
t.type = item.type
+ t.dir = dir -- points to top level "dir" item
dir_cache[cache_name] = t
end
return t
@@ -104,18 +104,13 @@ end
function TreeView:check_cache()
- -- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do
local dir = core.project_directories[i]
- local last_files = self.last[dir.name]
- if not last_files then
- self.last[dir.name] = dir.files
- else
- if dir.files ~= last_files then
- self:invalidate_cache(dir.name)
- self.last[dir.name] = dir.files
- end
+ -- invalidate cache's skip values if directory is declared dirty
+ if dir.is_dirty and self.cache[dir.name] then
+ self:invalidate_cache(dir.name)
end
+ dir.is_dirty = false
end
end
@@ -131,14 +126,14 @@ function TreeView:each_item()
for k = 1, #core.project_directories do
local dir = core.project_directories[k]
- local dir_cached = self:get_cached(dir.item, dir.name)
+ local dir_cached = self:get_cached(dir, dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1
y = y + h
local i = 1
while i <= #dir.files and dir_cached.expanded do
local item = dir.files[i]
- local cached = self:get_cached(item, dir.name)
+ local cached = self:get_cached(dir, item, dir.name)
coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1
@@ -206,7 +201,6 @@ local function create_directory_in(item)
core.error("cannot create directory %q: %s", dirname, err)
end
item.expanded = true
- core.reschedule_project_scan()
end)
end
@@ -223,26 +217,17 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item)
else
- if core.project_files_limit and not hovered_item.expanded then
- local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
- local index = 0
- -- The loop below is used to find the first match starting from the end
- -- in case there are multiple matches.
- while index and index + #filename < #abs_filename do
- index = string.find(abs_filename, filename, index + 1, true)
- end
- -- we assume here index is not nil because the abs_filename must contain the
- -- relative filename
- local dirname = string.sub(abs_filename, 1, index - 2)
- if core.is_project_folder(dirname) then
- core.scan_project_folder(dirname, filename)
- self:invalidate_cache(dirname)
- end
- end
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)
+ end
end
else
core.try(function()
+ if core.last_active_view and core.active_view == self then
+ core.set_active_view(core.last_active_view)
+ end
local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
@@ -470,7 +455,6 @@ command.add(function() return view.hovered_item ~= nil end, {
else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
- core.reschedule_project_scan()
end, common.path_suggest)
end,
@@ -485,7 +469,6 @@ command.add(function() return view.hovered_item ~= nil end, {
file:write("")
file:close()
core.root_view:open_doc(core.open_doc(doc_filename))
- core.reschedule_project_scan()
core.log("Created %s", doc_filename)
end, common.path_suggest)
end,
@@ -498,7 +481,6 @@ command.add(function() return view.hovered_item ~= nil end, {
core.command_view:enter("Folder Name", function(filename)
local dir_path = core.project_dir .. PATHSEP .. filename
common.mkdirp(dir_path)
- core.reschedule_project_scan()
core.log("Created %s", dir_path)
end, common.path_suggest)
end,
@@ -535,7 +517,6 @@ command.add(function() return view.hovered_item ~= nil end, {
return
end
end
- core.reschedule_project_scan()
core.log("Deleted \"%s\"", filename)
end
end
diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h
new file mode 100644
index 00000000..2bc9e0c3
--- /dev/null
+++ b/lib/dmon/dmon.h
@@ -0,0 +1,1591 @@
+#ifndef __DMON_H__
+#define __DMON_H__
+
+//
+// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
+// License: https://github.com/septag/dmon#license-bsd-2-clause
+//
+// Portable directory monitoring library
+// watches directories for file or directory changes.
+//
+// Usage:
+// define DMON_IMPL and include this file to use it:
+// #define DMON_IMPL
+// #include "dmon.h"
+//
+// dmon_init():
+// Call this once at the start of your program.
+// This will start a low-priority monitoring thread
+// dmon_deinit():
+// Call this when your work with dmon is finished, usually on program terminate
+// This will free resources and stop the monitoring thread
+// dmon_watch:
+// Watch for directories
+// You can watch multiple directories by calling this function multiple times
+// rootdir: root directory to monitor
+// watch_cb: callback function to receive events.
+// NOTE that this function is called from another thread, so you should
+// beware of data races in your application when accessing data within this
+// callback
+// flags: watch flags, see dmon_watch_flags_t
+// user_data: user pointer that is passed to callback function
+// Returns the Id of the watched directory after successful call, or returns Id=0 if error
+// dmon_unwatch:
+// Remove the directory from watch list
+//
+// see test.c for the basic example
+//
+// Configuration:
+// You can customize some low-level functionality like malloc and logging by overriding macros:
+//
+// DMON_MALLOC, DMON_FREE, DMON_REALLOC:
+// define these macros to override memory allocations
+// default is 'malloc', 'free' and 'realloc'
+// 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
+// DMON_API_DECL, DMON_API_IMPL
+// define these to provide your own API declerations. (for example: static)
+// default is nothing (which is extern in C language )
+// DMON_MAX_PATH
+// Maximum size of path characters
+// default is 260 characters
+// DMON_MAX_WATCHES
+// Maximum number of watch directories
+// default is 64
+//
+// TODO:
+// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files
+// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS
+// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES
+//
+// History:
+// 1.0.0 First version. working Win32/Linux backends
+// 1.1.0 MacOS backend
+// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall
+// 1.1.2 Eliminate some win32 dead code
+// 1.1.3 Fixed select not resetting causing high cpu usage on linux
+// 1.2.1 inotify (linux) fixes and improvements, added extra functionality header for linux
+// to manually add/remove directories manually to the watch handle, in case of large file sets
+//
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifndef DMON_API_DECL
+# define DMON_API_DECL
+#endif
+
+#ifndef DMON_API_IMPL
+# define DMON_API_IMPL
+#endif
+
+typedef struct { uint32_t id; } dmon_watch_id;
+
+// Pass these flags to `dmon_watch`
+typedef enum dmon_watch_flags_t {
+ DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories
+ DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only)
+ DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet
+ DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet
+} dmon_watch_flags;
+
+// Action is what operation performed on the file. this value is provided by watch callback
+typedef enum dmon_action_t {
+ DMON_ACTION_CREATE = 1,
+ DMON_ACTION_DELETE,
+ DMON_ACTION_MODIFY,
+ DMON_ACTION_MOVE
+} dmon_action;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DMON_API_DECL void dmon_init(void);
+DMON_API_DECL void dmon_deinit(void);
+
+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);
+DMON_API_DECL void dmon_unwatch(dmon_watch_id id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef DMON_IMPL
+
+#define DMON_OS_WINDOWS 0
+#define DMON_OS_MACOS 0
+#define DMON_OS_LINUX 0
+
+#if defined(_WIN32) || defined(_WIN64)
+# undef DMON_OS_WINDOWS
+# define DMON_OS_WINDOWS 1
+#elif defined(__linux__)
+# undef DMON_OS_LINUX
+# define DMON_OS_LINUX 1
+#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__)
+# undef DMON_OS_MACOS
+# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__
+#else
+# define DMON_OS 0
+# error "unsupported platform"
+#endif
+
+#if DMON_OS_WINDOWS
+# ifndef WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN
+# endif
+# ifndef NOMINMAX
+# define NOMINMAX
+# endif
+# include <windows.h>
+# include <intrin.h>
+# ifdef _MSC_VER
+# pragma intrinsic(_InterlockedExchange)
+# endif
+#elif DMON_OS_LINUX
+# ifndef __USE_MISC
+# define __USE_MISC
+# endif
+# include <dirent.h>
+# include <errno.h>
+# include <fcntl.h>
+# include <linux/limits.h>
+# include <pthread.h>
+# include <sys/inotify.h>
+# include <sys/stat.h>
+# include <sys/time.h>
+# include <time.h>
+# include <unistd.h>
+# include <stdlib.h>
+#elif DMON_OS_MACOS
+# include <pthread.h>
+# include <CoreServices/CoreServices.h>
+# include <sys/time.h>
+# include <sys/stat.h>
+# include <dispatch/dispatch.h>
+#endif
+
+#ifndef DMON_MALLOC
+# include <stdlib.h>
+# define DMON_MALLOC(size) malloc(size)
+# define DMON_FREE(ptr) free(ptr)
+# define DMON_REALLOC(ptr, size) realloc(ptr, size)
+#endif
+
+#ifndef DMON_ASSERT
+# include <assert.h>
+# define DMON_ASSERT(e) assert(e)
+#endif
+
+#ifndef DMON_LOG_ERROR
+# include <stdio.h>
+# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0)
+#endif
+
+#ifndef DMON_LOG_DEBUG
+# ifndef NDEBUG
+# include <stdio.h>
+# define DMON_LOG_DEBUG(s) do { puts(s); } while(0)
+# else
+# define DMON_LOG_DEBUG(s)
+# endif
+#endif
+
+#ifndef DMON_MAX_WATCHES
+# define DMON_MAX_WATCHES 64
+#endif
+
+#ifndef DMON_MAX_PATH
+# define DMON_MAX_PATH 260
+#endif
+
+#define _DMON_UNUSED(x) (void)(x)
+
+#ifndef _DMON_PRIVATE
+# if defined(__GNUC__) || defined(__clang__)
+# define _DMON_PRIVATE __attribute__((unused)) static
+# else
+# define _DMON_PRIVATE static
+# endif
+#endif
+
+#include <string.h>
+
+#ifndef _DMON_LOG_ERRORF
+# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0);
+#endif
+
+#ifndef _DMON_LOG_DEBUGF
+# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0);
+#endif
+
+#ifndef dmon__min
+# define dmon__min(a, b) ((a) < (b) ? (a) : (b))
+#endif
+
+#ifndef dmon__max
+# define dmon__max(a, b) ((a) > (b) ? (a) : (b))
+#endif
+
+#ifndef dmon__swap
+# define dmon__swap(a, b, _type) \
+ do { \
+ _type tmp = a; \
+ a = b; \
+ b = tmp; \
+ } while (0)
+#endif
+
+#ifndef dmon__make_id
+# ifdef __cplusplus
+# define dmon__make_id(id) {id}
+# else
+# define dmon__make_id(id) (dmon_watch_id) {id}
+# endif
+#endif // dmon__make_id
+
+_DMON_PRIVATE bool dmon__isrange(char ch, char from, char to)
+{
+ return (uint8_t)(ch - from) <= (uint8_t)(to - from);
+}
+
+_DMON_PRIVATE bool dmon__isupperchar(char ch)
+{
+ return dmon__isrange(ch, 'A', 'Z');
+}
+
+_DMON_PRIVATE char dmon__tolowerchar(char ch)
+{
+ return ch + (dmon__isupperchar(ch) ? 0x20 : 0);
+}
+
+_DMON_PRIVATE char* dmon__tolower(char* dst, int dst_sz, const char* str)
+{
+ int offset = 0;
+ int dst_max = dst_sz - 1;
+ while (*str && offset < dst_max) {
+ dst[offset++] = dmon__tolowerchar(*str);
+ ++str;
+ }
+ dst[offset] = '\0';
+ return dst;
+}
+
+_DMON_PRIVATE char* dmon__strcpy(char* dst, int dst_sz, const char* src)
+{
+ DMON_ASSERT(dst);
+ DMON_ASSERT(src);
+
+ const int32_t len = (int32_t)strlen(src);
+ const int32_t _max = dst_sz - 1;
+ const int32_t num = (len < _max ? len : _max);
+ memcpy(dst, src, num);
+ dst[num] = '\0';
+
+ return dst;
+}
+
+_DMON_PRIVATE char* dmon__unixpath(char* dst, int size, const char* path)
+{
+ size_t len = strlen(path);
+ len = dmon__min(len, (size_t)size - 1);
+
+ for (size_t i = 0; i < len; i++) {
+ if (path[i] != '\\')
+ dst[i] = path[i];
+ else
+ dst[i] = '/';
+ }
+ dst[len] = '\0';
+ return dst;
+}
+
+#if DMON_OS_LINUX || DMON_OS_MACOS
+_DMON_PRIVATE char* dmon__strcat(char* dst, int dst_sz, const char* src)
+{
+ int len = (int)strlen(dst);
+ return dmon__strcpy(dst + len, dst_sz - len, src);
+}
+#endif // DMON_OS_LINUX || DMON_OS_MACOS
+
+// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h
+#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0)
+#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v))
+#define stb_sb_pop(a) (stb__sbn(a)--)
+#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0)
+#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)])
+#define stb_sb_last(a) ((a)[stb__sbn(a)-1])
+#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0)
+
+#define stb__sbraw(a) ((int *) (a) - 2)
+#define stb__sbm(a) stb__sbraw(a)[0]
+#define stb__sbn(a) stb__sbraw(a)[1]
+
+#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a))
+#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0)
+#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a))))
+
+static void * stb__sbgrowf(void *arr, int increment, int itemsize)
+{
+ int dbl_cur = arr ? 2*stb__sbm(arr) : 0;
+ int min_needed = stb_sb_count(arr) + increment;
+ int m = dbl_cur > min_needed ? dbl_cur : min_needed;
+ int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2);
+ if (p) {
+ if (!arr)
+ p[1] = 0;
+ p[0] = m;
+ return p+2;
+ } else {
+ return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later
+ }
+}
+
+// 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*);
+
+#if DMON_OS_WINDOWS
+// IOCP (windows)
+#ifdef UNICODE
+# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size)
+#else
+# define _DMON_WINAPI_STR(name, size) const char* _##name = name
+#endif
+
+typedef struct dmon__win32_event {
+ char filepath[DMON_MAX_PATH];
+ DWORD action;
+ dmon_watch_id watch_id;
+ bool skip;
+} dmon__win32_event;
+
+typedef struct dmon__watch_state {
+ dmon_watch_id id;
+ OVERLAPPED overlapped;
+ HANDLE dir_handle;
+ uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx
+ DWORD notify_filter;
+ dmon__watch_cb* watch_cb;
+ uint32_t watch_flags;
+ void* user_data;
+ char rootdir[DMON_MAX_PATH];
+ char old_filepath[DMON_MAX_PATH];
+} dmon__watch_state;
+
+typedef struct dmon__state {
+ int num_watches;
+ dmon__watch_state watches[DMON_MAX_WATCHES];
+ HANDLE thread_handle;
+ CRITICAL_SECTION mutex;
+ volatile LONG modify_watches;
+ dmon__win32_event* events;
+ bool quit;
+} dmon__state;
+
+static bool _dmon_init;
+static dmon__state _dmon;
+
+_DMON_PRIVATE bool dmon__refresh_watch(dmon__watch_state* watch)
+{
+ return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer),
+ (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE,
+ watch->notify_filter, NULL, &watch->overlapped, NULL) != 0;
+}
+
+_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch)
+{
+ CancelIo(watch->dir_handle);
+ CloseHandle(watch->overlapped.hEvent);
+ CloseHandle(watch->dir_handle);
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+_DMON_PRIVATE void dmon__win32_process_events(void)
+{
+ for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) {
+ dmon__win32_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+
+ if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) {
+ // remove duplicate modifies on a single file
+ for (int j = i + 1; j < c; j++) {
+ dmon__win32_event* check_ev = &_dmon.events[j];
+ if (check_ev->action == FILE_ACTION_MODIFIED &&
+ strcmp(ev->filepath, check_ev->filepath) == 0) {
+ check_ev->skip = true;
+ }
+ }
+ }
+ }
+
+ // trigger user callbacks
+ for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) {
+ dmon__win32_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+ dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1];
+
+ if(watch == NULL || watch->watch_cb == NULL) {
+ continue;
+ }
+
+ switch (ev->action) {
+ case FILE_ACTION_ADDED:
+ watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL,
+ watch->user_data);
+ break;
+ case FILE_ACTION_MODIFIED:
+ watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL,
+ watch->user_data);
+ break;
+ case FILE_ACTION_RENAMED_OLD_NAME: {
+ // find the first occurance of the NEW_NAME
+ // this is somewhat API flaw that we have no reference for relating old and new files
+ for (int j = i + 1; j < c; j++) {
+ dmon__win32_event* check_ev = &_dmon.events[j];
+ if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) {
+ watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir,
+ check_ev->filepath, ev->filepath, watch->user_data);
+ break;
+ }
+ }
+ } break;
+ case FILE_ACTION_REMOVED:
+ watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL,
+ watch->user_data);
+ break;
+ }
+ }
+ stb_sb_reset(_dmon.events);
+}
+
+_DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg)
+{
+ _DMON_UNUSED(arg);
+ HANDLE wait_handles[DMON_MAX_WATCHES];
+
+ SYSTEMTIME starttm;
+ GetSystemTime(&starttm);
+ uint64_t msecs_elapsed = 0;
+
+ while (!_dmon.quit) {
+ if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) {
+ Sleep(10);
+ continue;
+ }
+
+ if (_dmon.num_watches == 0) {
+ Sleep(10);
+ LeaveCriticalSection(&_dmon.mutex);
+ continue;
+ }
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__watch_state* watch = &_dmon.watches[i];
+ wait_handles[i] = watch->overlapped.hEvent;
+ }
+
+ DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10);
+ DMON_ASSERT(wait_result != WAIT_FAILED);
+ if (wait_result != WAIT_TIMEOUT) {
+ 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)) {
+ char filepath[DMON_MAX_PATH];
+ PFILE_NOTIFY_INFORMATION notify;
+ size_t offset = 0;
+
+ if (bytes == 0) {
+ dmon__refresh_watch(watch);
+ LeaveCriticalSection(&_dmon.mutex);
+ continue;
+ }
+
+ do {
+ notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset];
+
+ int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName,
+ notify->FileNameLength / sizeof(WCHAR),
+ filepath, DMON_MAX_PATH - 1, NULL, NULL);
+ filepath[count] = TEXT('\0');
+ dmon__unixpath(filepath, sizeof(filepath), filepath);
+
+ // TODO: ignore directories if flag is set
+
+ if (stb_sb_count(_dmon.events) == 0) {
+ msecs_elapsed = 0;
+ }
+ dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false };
+ dmon__strcpy(wev.filepath, sizeof(wev.filepath), filepath);
+ stb_sb_push(_dmon.events, wev);
+
+ offset += notify->NextEntryOffset;
+ } while (notify->NextEntryOffset > 0);
+
+ if (!_dmon.quit) {
+ dmon__refresh_watch(watch);
+ }
+ }
+ } // if (WaitForMultipleObjects)
+
+ SYSTEMTIME tm;
+ GetSystemTime(&tm);
+ LONG dt =
+ (tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds);
+ starttm = tm;
+ msecs_elapsed += dt;
+ if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) {
+ dmon__win32_process_events();
+ msecs_elapsed = 0;
+ }
+
+ LeaveCriticalSection(&_dmon.mutex);
+ }
+ return 0;
+}
+
+
+DMON_API_IMPL void dmon_init(void)
+{
+ DMON_ASSERT(!_dmon_init);
+ InitializeCriticalSection(&_dmon.mutex);
+
+ _dmon.thread_handle =
+ CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL);
+ DMON_ASSERT(_dmon.thread_handle);
+ _dmon_init = true;
+}
+
+
+DMON_API_IMPL void dmon_deinit(void)
+{
+ DMON_ASSERT(_dmon_init);
+ _dmon.quit = true;
+ if (_dmon.thread_handle != INVALID_HANDLE_VALUE) {
+ WaitForSingleObject(_dmon.thread_handle, INFINITE);
+ CloseHandle(_dmon.thread_handle);
+ }
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ DeleteCriticalSection(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ _dmon_init = false;
+}
+
+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)
+{
+ DMON_ASSERT(watch_cb);
+ DMON_ASSERT(rootdir && rootdir[0]);
+
+ _InterlockedExchange(&_dmon.modify_watches, 1);
+ EnterCriticalSection(&_dmon.mutex);
+
+ DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ watch->id = dmon__make_id(id);
+ watch->watch_flags = flags;
+ watch->watch_cb = watch_cb;
+ watch->user_data = user_data;
+
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir);
+ dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir);
+ size_t rootdir_len = strlen(watch->rootdir);
+ if (watch->rootdir[rootdir_len - 1] != '/') {
+ watch->rootdir[rootdir_len] = '/';
+ watch->rootdir[rootdir_len + 1] = '\0';
+ }
+
+ _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH);
+ watch->dir_handle =
+ CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
+ if (watch->dir_handle != INVALID_HANDLE_VALUE) {
+ watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE |
+ 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)) {
+ dmon__unwatch(watch);
+ DMON_LOG_ERROR("ReadDirectoryChanges failed");
+ LeaveCriticalSection(&_dmon.mutex);
+ _InterlockedExchange(&_dmon.modify_watches, 0);
+ return dmon__make_id(0);
+ }
+ } else {
+ _DMON_LOG_ERRORF("Could not open: %s", rootdir);
+ LeaveCriticalSection(&_dmon.mutex);
+ _InterlockedExchange(&_dmon.modify_watches, 0);
+ return dmon__make_id(0);
+ }
+
+ LeaveCriticalSection(&_dmon.mutex);
+ _InterlockedExchange(&_dmon.modify_watches, 0);
+ return dmon__make_id(id);
+}
+
+DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
+{
+ DMON_ASSERT(id.id > 0);
+
+ _InterlockedExchange(&_dmon.modify_watches, 1);
+ EnterCriticalSection(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ LeaveCriticalSection(&_dmon.mutex);
+ _InterlockedExchange(&_dmon.modify_watches, 0);
+}
+
+#elif DMON_OS_LINUX
+// inotify linux backend
+#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024)
+
+typedef struct dmon__watch_subdir {
+ char rootdir[DMON_MAX_PATH];
+} dmon__watch_subdir;
+
+typedef struct dmon__inotify_event {
+ char filepath[DMON_MAX_PATH];
+ uint32_t mask;
+ uint32_t cookie;
+ dmon_watch_id watch_id;
+ bool skip;
+} dmon__inotify_event;
+
+typedef struct dmon__watch_state {
+ dmon_watch_id id;
+ int fd;
+ uint32_t watch_flags;
+ dmon__watch_cb* watch_cb;
+ void* user_data;
+ char rootdir[DMON_MAX_PATH];
+ dmon__watch_subdir* subdirs;
+ int* wds;
+} dmon__watch_state;
+
+typedef struct dmon__state {
+ dmon__watch_state watches[DMON_MAX_WATCHES];
+ dmon__inotify_event* events;
+ int num_watches;
+ pthread_t thread_handle;
+ pthread_mutex_t mutex;
+ bool quit;
+} dmon__state;
+
+static bool _dmon_init;
+static dmon__state _dmon;
+
+_DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask,
+ bool followlinks, dmon__watch_state* watch)
+{
+ struct dirent* entry;
+ DIR* dir = opendir(dirname);
+ DMON_ASSERT(dir);
+
+ char watchdir[DMON_MAX_PATH];
+
+ while ((entry = readdir(dir)) != NULL) {
+ bool entry_valid = false;
+ if (entry->d_type == DT_DIR) {
+ if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) {
+ dmon__strcpy(watchdir, sizeof(watchdir), dirname);
+ dmon__strcat(watchdir, sizeof(watchdir), entry->d_name);
+ entry_valid = true;
+ }
+ } else if (followlinks && entry->d_type == DT_LNK) {
+ char linkpath[PATH_MAX];
+ dmon__strcpy(watchdir, sizeof(watchdir), dirname);
+ dmon__strcat(watchdir, sizeof(watchdir), entry->d_name);
+ char* r = realpath(watchdir, linkpath);
+ _DMON_UNUSED(r);
+ DMON_ASSERT(r);
+ dmon__strcpy(watchdir, sizeof(watchdir), linkpath);
+ entry_valid = true;
+ }
+
+ // add sub-directory to watch dirs
+ if (entry_valid) {
+ int watchdir_len = (int)strlen(watchdir);
+ if (watchdir[watchdir_len - 1] != '/') {
+ watchdir[watchdir_len] = '/';
+ watchdir[watchdir_len + 1] = '\0';
+ }
+ int wd = inotify_add_watch(fd, watchdir, mask);
+ _DMON_UNUSED(wd);
+ DMON_ASSERT(wd != -1);
+
+ dmon__watch_subdir subdir;
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
+ if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
+ }
+
+ stb_sb_push(watch->subdirs, subdir);
+ stb_sb_push(watch->wds, wd);
+
+ // recurse
+ dmon__watch_recursive(watchdir, fd, mask, followlinks, watch);
+ }
+ }
+ closedir(dir);
+}
+
+_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd)
+{
+ const int* wds = watch->wds;
+ for (int i = 0, c = stb_sb_count(wds); i < c; i++) {
+ if (wd == wds[i]) {
+ return watch->subdirs[i].rootdir;
+ }
+ }
+
+ return NULL;
+}
+
+_DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname)
+{
+ struct dirent* entry;
+ DIR* dir = opendir(dirname);
+ DMON_ASSERT(dir);
+
+ char newdir[DMON_MAX_PATH];
+ while ((entry = readdir(dir)) != NULL) {
+ bool entry_valid = false;
+ bool is_dir = false;
+ if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) {
+ dmon__strcpy(newdir, sizeof(newdir), dirname);
+ dmon__strcat(newdir, sizeof(newdir), entry->d_name);
+ is_dir = (entry->d_type == DT_DIR);
+ entry_valid = true;
+ }
+
+ // add sub-directory to watch dirs
+ if (entry_valid) {
+ dmon__watch_subdir subdir;
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir);
+ if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir));
+ }
+
+ dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0), 0, watch->id, false };
+ dmon__strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir);
+ stb_sb_push(_dmon.events, dev);
+ }
+ }
+ closedir(dir);
+}
+
+_DMON_PRIVATE void dmon__inotify_process_events(void)
+{
+ for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) {
+ dmon__inotify_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+
+ // remove redundant modify events on a single file
+ if (ev->mask & IN_MODIFY) {
+ for (int j = i + 1; j < c; j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
+ ev->skip = true;
+ break;
+ } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) {
+ // in some cases, particularly when created files under sub directories
+ // there can be two modify events for a single subdir one with trailing slash and one without
+ // remove traling slash from both cases and test
+ int l1 = (int)strlen(ev->filepath);
+ int l2 = (int)strlen(check_ev->filepath);
+ if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0';
+ if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0';
+ if (strcmp(ev->filepath, check_ev->filepath) == 0) {
+ ev->skip = true;
+ break;
+ }
+ }
+ }
+ } else if (ev->mask & IN_CREATE) {
+ bool loop_break = false;
+ for (int j = i + 1; j < c && !loop_break; j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) {
+ // there is a case where some programs (like gedit):
+ // when we save, it creates a temp file, and moves it to the file being modified
+ // search for these cases and remove all of them
+ for (int k = j + 1; k < c; k++) {
+ dmon__inotify_event* third_ev = &_dmon.events[k];
+ if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) {
+ third_ev->mask = IN_MODIFY; // change to modified
+ ev->skip = check_ev->skip = true;
+ loop_break = true;
+ break;
+ }
+ }
+ } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
+ // Another case is that file is copied. CREATE and MODIFY happens sequentially
+ // so we ignore MODIFY event
+ check_ev->skip = true;
+ }
+ }
+ } else if (ev->mask & IN_MOVED_FROM) {
+ bool move_valid = false;
+ for (int j = i + 1; j < c; j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) {
+ move_valid = true;
+ break;
+ }
+ }
+
+ // in some environments like nautilus file explorer:
+ // when a file is deleted, it is moved to recycle bin
+ // so if the destination of the move is not valid, it's probably DELETE
+ if (!move_valid) {
+ ev->mask = IN_DELETE;
+ }
+ } else if (ev->mask & IN_MOVED_TO) {
+ bool move_valid = false;
+ for (int j = 0; j < i; j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) {
+ move_valid = true;
+ break;
+ }
+ }
+
+ // in some environments like nautilus file explorer:
+ // when a file is deleted, it is moved to recycle bin, on undo it is moved back it
+ // so if the destination of the move is not valid, it's probably CREATE
+ if (!move_valid) {
+ ev->mask = IN_CREATE;
+ }
+ } else if (ev->mask & IN_DELETE) {
+ for (int j = i + 1; j < c; j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ // if the file is DELETED and then MODIFIED after, just ignore the modify event
+ if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
+ check_ev->skip = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // trigger user callbacks
+ for (int i = 0; i < stb_sb_count(_dmon.events); i++) {
+ dmon__inotify_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+ dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1];
+
+ if(watch == NULL || watch->watch_cb == NULL) {
+ continue;
+ }
+
+ if (ev->mask & IN_CREATE) {
+ if (ev->mask & IN_ISDIR) {
+ if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) {
+ char watchdir[DMON_MAX_PATH];
+ dmon__strcpy(watchdir, sizeof(watchdir), watch->rootdir);
+ dmon__strcat(watchdir, sizeof(watchdir), ev->filepath);
+ dmon__strcat(watchdir, sizeof(watchdir), "/");
+ uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
+ int wd = inotify_add_watch(watch->fd, watchdir, mask);
+ // Removing the assertion below because it was giving errors for some reason
+ // when building a new package.
+ // _DMON_UNUSED(wd);
+ // DMON_ASSERT(wd != -1);
+ if (wd == -1) continue;
+
+ dmon__watch_subdir subdir;
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
+ if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
+ }
+
+ stb_sb_push(watch->subdirs, subdir);
+ stb_sb_push(watch->wds, wd);
+
+ // some directories may be already created, for instance, with the command: mkdir -p
+ // so we will enumerate them manually and add them to the events
+ dmon__gather_recursive(watch, watchdir);
+ ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated
+ }
+ }
+ watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data);
+ }
+ else if (ev->mask & IN_MODIFY) {
+ watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data);
+ }
+ else if (ev->mask & IN_MOVED_FROM) {
+ for (int j = i + 1; j < stb_sb_count(_dmon.events); j++) {
+ dmon__inotify_event* check_ev = &_dmon.events[j];
+ if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) {
+ watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir,
+ check_ev->filepath, ev->filepath, watch->user_data);
+ break;
+ }
+ }
+ }
+ else if (ev->mask & IN_DELETE) {
+ watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data);
+ }
+ }
+
+ stb_sb_reset(_dmon.events);
+}
+
+static void* dmon__thread(void* arg)
+{
+ _DMON_UNUSED(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);
+
+ while (!_dmon.quit) {
+ nanosleep(&req, &rem);
+ if (_dmon.num_watches == 0 || 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++) {
+ dmon__watch_state* watch = &_dmon.watches[i];
+ FD_SET(watch->fd, &rfds);
+ }
+
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 100000;
+ if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) {
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__watch_state* watch = &_dmon.watches[i];
+ if (FD_ISSET(watch->fd, &rfds)) {
+ ssize_t offset = 0;
+ ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE);
+ if (len <= 0) {
+ continue;
+ }
+
+ while (offset < len) {
+ struct inotify_event* iev = (struct inotify_event*)&buff[offset];
+
+ const char *subdir = dmon__find_subdir(watch, iev->wd);
+ if (subdir) {
+ char filepath[DMON_MAX_PATH];
+ dmon__strcpy(filepath, sizeof(filepath), subdir);
+ dmon__strcat(filepath, sizeof(filepath), iev->name);
+
+ // TODO: ignore directories if flag is set
+
+ if (stb_sb_count(_dmon.events) == 0) {
+ usecs_elapsed = 0;
+ }
+ dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false };
+ dmon__strcpy(dev.filepath, sizeof(dev.filepath), filepath);
+ stb_sb_push(_dmon.events, dev);
+ }
+
+ offset += sizeof(struct inotify_event) + iev->len;
+ }
+ }
+ }
+ }
+
+ struct timeval tm;
+ gettimeofday(&tm, 0);
+ long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec;
+ starttm = tm;
+ usecs_elapsed += dt;
+ if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) {
+ dmon__inotify_process_events();
+ usecs_elapsed = 0;
+ }
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ }
+ return 0x0;
+}
+
+_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch)
+{
+ close(watch->fd);
+ stb_sb_free(watch->subdirs);
+ stb_sb_free(watch->wds);
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+DMON_API_IMPL void dmon_init(void)
+{
+ DMON_ASSERT(!_dmon_init);
+ pthread_mutex_init(&_dmon.mutex, NULL);
+
+ int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL);
+ _DMON_UNUSED(r);
+ DMON_ASSERT(r == 0 && "pthread_create failed");
+ _dmon_init = true;
+}
+
+DMON_API_IMPL void dmon_deinit(void)
+{
+ DMON_ASSERT(_dmon_init);
+ _dmon.quit = true;
+ pthread_join(_dmon.thread_handle, NULL);
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ pthread_mutex_destroy(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ _dmon_init = false;
+}
+
+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)
+{
+ DMON_ASSERT(watch_cb);
+ DMON_ASSERT(rootdir && rootdir[0]);
+
+ pthread_mutex_lock(&_dmon.mutex);
+
+ DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ watch->id = dmon__make_id(id);
+ watch->watch_flags = flags;
+ watch->watch_cb = watch_cb;
+ watch->user_data = user_data;
+
+ 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);
+ pthread_mutex_unlock(&_dmon.mutex);
+ return dmon__make_id(0);
+ }
+
+
+ if (S_ISLNK(root_st.st_mode)) {
+ if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) {
+ char linkpath[PATH_MAX];
+ char* r = realpath(rootdir, linkpath);
+ _DMON_UNUSED(r);
+ DMON_ASSERT(r);
+
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath);
+ } else {
+ _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS",
+ rootdir);
+ pthread_mutex_unlock(&_dmon.mutex);
+ return dmon__make_id(0);
+ }
+ } else {
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir);
+ }
+
+ // add trailing slash
+ int rootdir_len = (int)strlen(watch->rootdir);
+ if (watch->rootdir[rootdir_len - 1] != '/') {
+ watch->rootdir[rootdir_len] = '/';
+ watch->rootdir[rootdir_len + 1] = '\0';
+ }
+
+ watch->fd = inotify_init();
+ if (watch->fd < -1) {
+ DMON_LOG_ERROR("could not create inotify instance");
+ pthread_mutex_unlock(&_dmon.mutex);
+ return dmon__make_id(0);
+ }
+
+ 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);
+ pthread_mutex_unlock(&_dmon.mutex);
+ return dmon__make_id(0);
+ }
+ dmon__watch_subdir subdir;
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry
+ stb_sb_push(watch->subdirs, subdir);
+ stb_sb_push(watch->wds, wd);
+
+ // recursive mode: enumarate all child directories and add them to watch
+ if (flags & DMON_WATCHFLAGS_RECURSIVE) {
+ dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask,
+ (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch);
+ }
+
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ return dmon__make_id(id);
+}
+
+DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
+{
+ DMON_ASSERT(id.id > 0);
+
+ pthread_mutex_lock(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ pthread_mutex_unlock(&_dmon.mutex);
+}
+#elif DMON_OS_MACOS
+// FSEvents MacOS backend
+typedef struct dmon__fsevent_event {
+ char filepath[DMON_MAX_PATH];
+ uint64_t event_id;
+ long event_flags;
+ dmon_watch_id watch_id;
+ bool skip;
+ bool move_valid;
+} dmon__fsevent_event;
+
+typedef struct dmon__watch_state {
+ dmon_watch_id id;
+ uint32_t watch_flags;
+ FSEventStreamRef fsev_stream_ref;
+ dmon__watch_cb* watch_cb;
+ void* user_data;
+ char rootdir[DMON_MAX_PATH];
+ char rootdir_unmod[DMON_MAX_PATH];
+ bool init;
+} dmon__watch_state;
+
+typedef struct dmon__state {
+ dmon__watch_state watches[DMON_MAX_WATCHES];
+ dmon__fsevent_event* events;
+ int num_watches;
+ volatile int modify_watches;
+ pthread_t thread_handle;
+ dispatch_semaphore_t thread_sem;
+ pthread_mutex_t mutex;
+ CFRunLoopRef cf_loop_ref;
+ CFAllocatorRef cf_alloc_ref;
+ bool quit;
+} dmon__state;
+
+union dmon__cast_userdata {
+ void* ptr;
+ uint32_t id;
+};
+
+static bool _dmon_init;
+static dmon__state _dmon;
+
+_DMON_PRIVATE void* dmon__cf_malloc(CFIndex size, CFOptionFlags hints, void* info)
+{
+ _DMON_UNUSED(hints);
+ _DMON_UNUSED(info);
+ return DMON_MALLOC(size);
+}
+
+_DMON_PRIVATE void dmon__cf_free(void* ptr, void* info)
+{
+ _DMON_UNUSED(info);
+ DMON_FREE(ptr);
+}
+
+_DMON_PRIVATE void* dmon__cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info)
+{
+ _DMON_UNUSED(hints);
+ _DMON_UNUSED(info);
+ return DMON_REALLOC(ptr, (size_t)newsize);
+}
+
+_DMON_PRIVATE void dmon__fsevent_process_events(void)
+{
+ for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) {
+ dmon__fsevent_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+
+ // remove redundant modify events on a single file
+ if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
+ for (int j = i + 1; j < c; j++) {
+ dmon__fsevent_event* check_ev = &_dmon.events[j];
+ if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) &&
+ strcmp(ev->filepath, check_ev->filepath) == 0) {
+ ev->skip = true;
+ break;
+ }
+ }
+ } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) {
+ for (int j = i + 1; j < c; j++) {
+ dmon__fsevent_event* check_ev = &_dmon.events[j];
+ if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) &&
+ check_ev->event_id == (ev->event_id + 1)) {
+ ev->move_valid = check_ev->move_valid = true;
+ break;
+ }
+ }
+
+ // in some environments like finder file explorer:
+ // when a file is deleted, it is moved to recycle bin
+ // so if the destination of the move is not valid, it's probably DELETE or CREATE
+ // decide CREATE if file exists
+ if (!ev->move_valid) {
+ ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed;
+
+ char abs_filepath[DMON_MAX_PATH];
+ dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id-1];
+ dmon__strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir);
+ dmon__strcat(abs_filepath, sizeof(abs_filepath), ev->filepath);
+
+ struct stat root_st;
+ if (stat(abs_filepath, &root_st) != 0) {
+ ev->event_flags |= kFSEventStreamEventFlagItemRemoved;
+ } else {
+ ev->event_flags |= kFSEventStreamEventFlagItemCreated;
+ }
+ }
+ }
+ }
+
+ // trigger user callbacks
+ for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) {
+ dmon__fsevent_event* ev = &_dmon.events[i];
+ if (ev->skip) {
+ continue;
+ }
+ dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1];
+
+ if(watch == NULL || watch->watch_cb == NULL) {
+ continue;
+ }
+
+ if (ev->event_flags & kFSEventStreamEventFlagItemCreated) {
+ watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL,
+ watch->user_data);
+ } else if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
+ watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL,
+ watch->user_data);
+ } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) {
+ for (int j = i + 1; j < c; j++) {
+ dmon__fsevent_event* check_ev = &_dmon.events[j];
+ if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) {
+ watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod,
+ check_ev->filepath, ev->filepath, watch->user_data);
+ break;
+ }
+ }
+ } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) {
+ watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL,
+ watch->user_data);
+ }
+ }
+
+ stb_sb_reset(_dmon.events);
+}
+
+static void* dmon__thread(void* arg)
+{
+ _DMON_UNUSED(arg);
+
+ struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) };
+ struct timespec rem = { 0, 0 };
+
+ _dmon.cf_loop_ref = CFRunLoopGetCurrent();
+ dispatch_semaphore_signal(_dmon.thread_sem);
+
+ while (!_dmon.quit) {
+ if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) {
+ nanosleep(&req, &rem);
+ continue;
+ }
+
+ if (_dmon.num_watches == 0) {
+ nanosleep(&req, &rem);
+ pthread_mutex_unlock(&_dmon.mutex);
+ continue;
+ }
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__watch_state* watch = &_dmon.watches[i];
+ if (!watch->init) {
+ DMON_ASSERT(watch->fsev_stream_ref);
+ FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref,
+ kCFRunLoopDefaultMode);
+ FSEventStreamStart(watch->fsev_stream_ref);
+
+ watch->init = true;
+ }
+ }
+
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut);
+ dmon__fsevent_process_events();
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ }
+
+ CFRunLoopStop(_dmon.cf_loop_ref);
+ _dmon.cf_loop_ref = NULL;
+ return 0x0;
+}
+
+_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch)
+{
+ if (watch->fsev_stream_ref) {
+ FSEventStreamStop(watch->fsev_stream_ref);
+ FSEventStreamInvalidate(watch->fsev_stream_ref);
+ FSEventStreamRelease(watch->fsev_stream_ref);
+ watch->fsev_stream_ref = NULL;
+ }
+
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+DMON_API_IMPL void dmon_init(void)
+{
+ DMON_ASSERT(!_dmon_init);
+ pthread_mutex_init(&_dmon.mutex, NULL);
+
+ CFAllocatorContext cf_alloc_ctx = { 0 };
+ cf_alloc_ctx.allocate = dmon__cf_malloc;
+ cf_alloc_ctx.deallocate = dmon__cf_free;
+ cf_alloc_ctx.reallocate = dmon__cf_realloc;
+ _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx);
+
+ _dmon.thread_sem = dispatch_semaphore_create(0);
+ DMON_ASSERT(_dmon.thread_sem);
+
+ int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL);
+ _DMON_UNUSED(r);
+ DMON_ASSERT(r == 0 && "pthread_create failed");
+
+ // wait for thread to initialize loop object
+ dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER);
+
+ _dmon_init = true;
+}
+
+DMON_API_IMPL void dmon_deinit(void)
+{
+ DMON_ASSERT(_dmon_init);
+ _dmon.quit = true;
+ pthread_join(_dmon.thread_handle, NULL);
+
+ dispatch_release(_dmon.thread_sem);
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ pthread_mutex_destroy(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ if (_dmon.cf_alloc_ref) {
+ CFRelease(_dmon.cf_alloc_ref);
+ }
+
+ _dmon_init = false;
+}
+
+_DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data,
+ size_t num_events, void* event_paths,
+ const FSEventStreamEventFlags event_flags[],
+ const FSEventStreamEventId event_ids[])
+{
+ _DMON_UNUSED(stream_ref);
+
+ union dmon__cast_userdata _userdata;
+ _userdata.ptr = user_data;
+ dmon_watch_id watch_id = dmon__make_id(_userdata.id);
+ DMON_ASSERT(watch_id.id > 0);
+ dmon__watch_state* watch = &_dmon.watches[watch_id.id - 1];
+ char abs_filepath[DMON_MAX_PATH];
+ char abs_filepath_lower[DMON_MAX_PATH];
+
+ for (size_t i = 0; i < num_events; i++) {
+ const char* filepath = ((const char**)event_paths)[i];
+ long flags = (long)event_flags[i];
+ uint64_t event_id = (uint64_t)event_ids[i];
+ dmon__fsevent_event ev;
+ memset(&ev, 0x0, sizeof(ev));
+
+ dmon__strcpy(abs_filepath, sizeof(abs_filepath), filepath);
+ dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath);
+
+ // normalize path, so it would be the same on both MacOS file-system types (case/nocase)
+ dmon__tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath);
+ DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower);
+
+ // strip the root dir from the begining
+ dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir));
+
+ ev.event_flags = flags;
+ ev.event_id = event_id;
+ ev.watch_id = watch_id;
+ stb_sb_push(_dmon.events, ev);
+ }
+}
+
+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)
+{
+ DMON_ASSERT(watch_cb);
+ DMON_ASSERT(rootdir && rootdir[0]);
+
+ __sync_lock_test_and_set(&_dmon.modify_watches, 1);
+ pthread_mutex_lock(&_dmon.mutex);
+
+ DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ watch->id = dmon__make_id(id);
+ watch->watch_flags = flags;
+ watch->watch_cb = watch_cb;
+ watch->user_data = user_data;
+
+ 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);
+ pthread_mutex_unlock(&_dmon.mutex);
+ __sync_lock_test_and_set(&_dmon.modify_watches, 0);
+ return dmon__make_id(0);
+ }
+
+ if (S_ISLNK(root_st.st_mode)) {
+ if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) {
+ char linkpath[PATH_MAX];
+ char* r = realpath(rootdir, linkpath);
+ _DMON_UNUSED(r);
+ DMON_ASSERT(r);
+
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath);
+ } else {
+ _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir);
+ pthread_mutex_unlock(&_dmon.mutex);
+ __sync_lock_test_and_set(&_dmon.modify_watches, 0);
+ return dmon__make_id(0);
+ }
+ } else {
+ char rootdir_abspath[DMON_MAX_PATH];
+ if (realpath(rootdir, rootdir_abspath) != NULL) {
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath);
+ } else {
+ dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir);
+ }
+ }
+
+ dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir);
+
+ // add trailing slash
+ int rootdir_len = (int)strlen(watch->rootdir);
+ if (watch->rootdir[rootdir_len - 1] != '/') {
+ watch->rootdir[rootdir_len] = '/';
+ watch->rootdir[rootdir_len + 1] = '\0';
+ }
+
+ dmon__strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir);
+ dmon__tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir);
+
+ // create FS objects
+ CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8);
+ CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL);
+
+ FSEventStreamContext ctx;
+ union dmon__cast_userdata userdata;
+ userdata.id = id;
+ ctx.version = 0;
+ ctx.info = userdata.ptr;
+ ctx.retain = NULL;
+ ctx.release = NULL;
+ ctx.copyDescription = NULL;
+ watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, dmon__fsevent_callback, &ctx,
+ cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25,
+ kFSEventStreamCreateFlagFileEvents);
+
+
+ CFRelease(cf_dirarr);
+ CFRelease(cf_dir);
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ __sync_lock_test_and_set(&_dmon.modify_watches, 0);
+ return dmon__make_id(id);
+}
+
+DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
+{
+ DMON_ASSERT(id.id > 0);
+
+ __sync_lock_test_and_set(&_dmon.modify_watches, 1);
+ pthread_mutex_lock(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ __sync_lock_test_and_set(&_dmon.modify_watches, 0);
+}
+
+#endif
+
+#endif // DMON_IMPL
+#endif // __DMON_H__
diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h
new file mode 100644
index 00000000..4b321034
--- /dev/null
+++ b/lib/dmon/dmon_extra.h
@@ -0,0 +1,162 @@
+#ifndef __DMON_EXTRA_H__
+#define __DMON_EXTRA_H__
+
+//
+// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
+// License: https://github.com/septag/dmon#license-bsd-2-clause
+//
+// Extra header functionality for dmon.h for the backend based on inotify
+//
+// Add/Remove directory functions:
+// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir
+// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir
+// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take
+// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one
+// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user
+// will be reached. The default maximum is 8192.
+// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the
+// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched.
+// The function dmon_watch_add and dmon_watch_rm are used to this purpose.
+//
+
+#ifndef __DMON_H__
+#error "Include 'dmon.h' before including this file"
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir);
+DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir);
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef DMON_IMPL
+#if DMON_OS_LINUX
+DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
+{
+ DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
+
+ bool skip_lock = pthread_self() == _dmon.thread_handle;
+
+ if (!skip_lock)
+ pthread_mutex_lock(&_dmon.mutex);
+
+ dmon__watch_state* watch = &_dmon.watches[id.id - 1];
+
+ // check if the directory exists
+ // if watchdir contains absolute/root-included path, try to strip the rootdir from it
+ // else, we assume that watchdir is correct, so save it as it is
+ struct stat st;
+ dmon__watch_subdir subdir;
+ if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) {
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
+ if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
+ }
+ } else {
+ char fullpath[DMON_MAX_PATH];
+ dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
+ dmon__strcat(fullpath, sizeof(fullpath), watchdir);
+ if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) {
+ _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return false;
+ }
+ dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
+ }
+
+ int dirlen = (int)strlen(subdir.rootdir);
+ if (subdir.rootdir[dirlen - 1] != '/') {
+ subdir.rootdir[dirlen] = '/';
+ subdir.rootdir[dirlen + 1] = '\0';
+ }
+
+ // check that the directory is not already added
+ for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) {
+ if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) {
+ _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir);
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return false;
+ }
+ }
+
+ const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
+ char fullpath[DMON_MAX_PATH];
+ dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
+ dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir);
+ int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask);
+ if (wd == -1) {
+ _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno);
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return false;
+ }
+
+ stb_sb_push(watch->subdirs, subdir);
+ stb_sb_push(watch->wds, wd);
+
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+
+ return true;
+}
+
+DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
+{
+ DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
+
+ bool skip_lock = pthread_self() == _dmon.thread_handle;
+
+ if (!skip_lock)
+ pthread_mutex_lock(&_dmon.mutex);
+
+ dmon__watch_state* watch = &_dmon.watches[id.id - 1];
+
+ char subdir[DMON_MAX_PATH];
+ dmon__strcpy(subdir, sizeof(subdir), watchdir);
+ if (strstr(subdir, watch->rootdir) == subdir) {
+ dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir));
+ }
+
+ int dirlen = (int)strlen(subdir);
+ if (subdir[dirlen - 1] != '/') {
+ subdir[dirlen] = '/';
+ subdir[dirlen + 1] = '\0';
+ }
+
+ int i, c = stb_sb_count(watch->subdirs);
+ for (i = 0; i < c; i++) {
+ if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) {
+ break;
+ }
+ }
+ if (i >= c) {
+ _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return false;
+ }
+ inotify_rm_watch(watch->fd, watch->wds[i]);
+
+ /* Remove entry from subdirs and wds by swapping position with the last entry */
+ watch->subdirs[i] = stb_sb_last(watch->subdirs);
+ stb_sb_pop(watch->subdirs);
+
+ watch->wds[i] = stb_sb_last(watch->wds);
+ stb_sb_pop(watch->wds);
+
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return true;
+}
+#endif // DMON_OS_LINUX
+#endif // DMON_IMPL
+
+#endif // __DMON_EXTRA_H__
+
diff --git a/lib/dmon/meson.build b/lib/dmon/meson.build
new file mode 100644
index 00000000..83edd1c9
--- /dev/null
+++ b/lib/dmon/meson.build
@@ -0,0 +1 @@
+lite_includes += include_directories('.')
diff --git a/licenses/licenses.md b/licenses/licenses.md
index 8005c4a7..928d88d9 100644
--- a/licenses/licenses.md
+++ b/licenses/licenses.md
@@ -22,6 +22,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+## septag/dmon
+
+Copyright 2019 Sepehr Taghdisian. All rights reserved.
+
+https://github.com/septag/dmon
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
## Fira Sans
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
diff --git a/meson.build b/meson.build
index 53883266..8456fe60 100644
--- a/meson.build
+++ b/meson.build
@@ -1,6 +1,6 @@
project('lite-xl',
['c'],
- version : '2.0.2',
+ version : '2.0.3',
license : 'MIT',
meson_version : '>= 0.54',
default_options : ['c_std=gnu11']
@@ -23,6 +23,7 @@ endif
cc = meson.get_compiler('c')
+lite_includes = []
lite_cargs = []
# On macos we need to use the SDL renderer to support retina displays
if get_option('renderer') or host_machine.system() == 'darwin'
@@ -45,6 +46,7 @@ endif
if not get_option('source-only')
libm = cc.find_library('m', required : false)
libdl = cc.find_library('dl', required : false)
+ threads_dep = dependency('threads')
lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'],
default_options: ['shared=false', 'use_readline=false', 'app=false']
)
@@ -58,7 +60,7 @@ if not get_option('source-only')
]
)
- lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep]
+ lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep, threads_dep]
if host_machine.system() == 'windows'
# Note that we need to explicitly add the windows socket DLL because
@@ -118,10 +120,8 @@ configure_file(
install_dir : lite_datadir / 'core',
)
-#===============================================================================
-# Targets
-#===============================================================================
if not get_option('source-only')
+ subdir('lib/dmon')
subdir('src')
subdir('scripts')
endif
diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md
new file mode 100644
index 00000000..5179df40
--- /dev/null
+++ b/resources/notes-dmon-integration.md
@@ -0,0 +1,54 @@
+
+`core.set_project_dir`:
+ Reset project directories and set its directory.
+ It chdir into the directory, empty the `core.project_directories` and add
+ the given directory.
+ `core.add_project_directory`:
+ Add a new top-level directory to the project.
+ Also called from modules and commands outside core.init.
+ local function `scan_project_folder`:
+ Scan all files for a given top-level project directory.
+ Can emit a warning about file limit.
+ Called only from within core.init module.
+
+`core.scan_project_subdir`: (before was named `core.scan_project_folder`)
+ scan a single folder, without recursion. Used when too many files.
+
+Local function `scan_project_folder`:
+ Populate the project folder top directory. Done only once when the directory
+ is added to the project.
+
+`core.add_project_directory`:
+ Add a new top-level folder to the project.
+
+`core.set_project_dir`:
+ Set the initial project directory.
+
+`core.dir_rescan_add_job`:
+ Add a job to rescan after an elapsed time a project's subdirectory to fix for any
+ changes.
+
+Local function `rescan_project_subdir`:
+ Rescan a project's subdirectory, compare to the current version and patch the list if
+ a difference is found.
+
+
+`core.project_scan_thread`:
+ Should disappear now that we use dmon.
+
+
+`core.project_scan_topdir`:
+ New function to scan a top level project folder.
+
+
+`config.project_scan_rate`:
+`core.project_scan_thread_id`:
+`core.reschedule_project_scan`:
+`core.project_files_limit`:
+ A eliminer.
+
+`core.get_project_files`:
+ To be fixed. Use `find_project_files_co` for a single directory
+
+In TreeView remove usage of self.last to detect new scan that changed the files list.
+
diff --git a/scripts/package.sh b/scripts/package.sh
index 1370aee8..21a8cc91 100644
--- a/scripts/package.sh
+++ b/scripts/package.sh
@@ -186,7 +186,7 @@ main() {
rm -rf "${dest_dir}"
- DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}"
+ DESTDIR="$(pwd)/${dest_dir}" meson install --skip-subprojects -C "${build_dir}"
local data_dir="$(pwd)/${dest_dir}/data"
local exe_file="$(pwd)/${dest_dir}/lite-xl"
diff --git a/src/api/regex.c b/src/api/regex.c
index 1043b1c5..9f6bd3ee 100644
--- a/src/api/regex.c
+++ b/src/api/regex.c
@@ -68,8 +68,11 @@ static int f_pcre_match(lua_State *L) {
int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL);
if (rc < 0) {
pcre2_match_data_free(md);
- if (rc != PCRE2_ERROR_NOMATCH)
- luaL_error(L, "regex matching error %d", rc);
+ if (rc != PCRE2_ERROR_NOMATCH) {
+ PCRE2_UCHAR buffer[120];
+ pcre2_get_error_message(rc, buffer, sizeof(buffer));
+ luaL_error(L, "regex matching error %d: %s", rc, buffer);
+ }
return 0;
}
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md);
diff --git a/src/api/renderer.c b/src/api/renderer.c
index 60256118..61057f78 100644
--- a/src/api/renderer.c
+++ b/src/api/renderer.c
@@ -174,23 +174,30 @@ static int f_end_frame(lua_State *L) {
}
+static RenRect rect_to_grid(lua_Number x, lua_Number y, lua_Number w, lua_Number h) {
+ int x1 = (int) (x + 0.5), y1 = (int) (y + 0.5);
+ int x2 = (int) (x + w + 0.5), y2 = (int) (y + h + 0.5);
+ return (RenRect) {x1, y1, x2 - x1, y2 - y1};
+}
+
+
static int f_set_clip_rect(lua_State *L) {
- RenRect rect;
- rect.x = luaL_checknumber(L, 1);
- rect.y = luaL_checknumber(L, 2);
- rect.width = luaL_checknumber(L, 3);
- rect.height = luaL_checknumber(L, 4);
+ lua_Number x = luaL_checknumber(L, 1);
+ lua_Number y = luaL_checknumber(L, 2);
+ lua_Number w = luaL_checknumber(L, 3);
+ lua_Number h = luaL_checknumber(L, 4);
+ RenRect rect = rect_to_grid(x, y, w, h);
rencache_set_clip_rect(rect);
return 0;
}
static int f_draw_rect(lua_State *L) {
- RenRect rect;
- rect.x = luaL_checknumber(L, 1);
- rect.y = luaL_checknumber(L, 2);
- rect.width = luaL_checknumber(L, 3);
- rect.height = luaL_checknumber(L, 4);
+ lua_Number x = luaL_checknumber(L, 1);
+ lua_Number y = luaL_checknumber(L, 2);
+ lua_Number w = luaL_checknumber(L, 3);
+ lua_Number h = luaL_checknumber(L, 4);
+ RenRect rect = rect_to_grid(x, y, w, h);
RenColor color = checkcolor(L, 5, 255);
rencache_draw_rect(rect, color);
return 0;
diff --git a/src/api/system.c b/src/api/system.c
index dc87b723..c68caa3e 100644
--- a/src/api/system.c
+++ b/src/api/system.c
@@ -6,11 +6,14 @@
#include <errno.h>
#include <sys/stat.h>
#include "api.h"
+#include "dirmonitor.h"
#include "rencache.h"
#ifdef _WIN32
#include <direct.h>
#include <windows.h>
#include <fileapi.h>
+#elif __linux__
+ #include <sys/vfs.h>
#endif
extern SDL_Window *window;
@@ -238,6 +241,26 @@ top:
lua_pushnumber(L, e.wheel.y);
return 2;
+ case SDL_USEREVENT:
+ lua_pushstring(L, "dirchange");
+ lua_pushnumber(L, e.user.code >> 16);
+ switch (e.user.code & 0xffff) {
+ case DMON_ACTION_DELETE:
+ lua_pushstring(L, "delete");
+ break;
+ case DMON_ACTION_CREATE:
+ lua_pushstring(L, "create");
+ break;
+ case DMON_ACTION_MODIFY:
+ lua_pushstring(L, "modify");
+ break;
+ default:
+ return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff);
+ }
+ lua_pushstring(L, e.user.data1);
+ free(e.user.data1);
+ return 4;
+
default:
goto top;
}
@@ -526,6 +549,45 @@ static int f_get_file_info(lua_State *L) {
return 1;
}
+#if __linux__
+// https://man7.org/linux/man-pages/man2/statfs.2.html
+
+struct f_type_names {
+ uint32_t magic;
+ const char *name;
+};
+
+static struct f_type_names fs_names[] = {
+ { 0xef53, "ext2/ext3" },
+ { 0x6969, "nfs" },
+ { 0x65735546, "fuse" },
+ { 0x517b, "smb" },
+ { 0xfe534d42, "smb2" },
+ { 0x52654973, "reiserfs" },
+ { 0x01021994, "tmpfs" },
+ { 0x858458f6, "ramfs" },
+ { 0x5346544e, "ntfs" },
+ { 0x0, NULL },
+};
+
+static int f_get_fs_type(lua_State *L) {
+ const char *path = luaL_checkstring(L, 1);
+ struct statfs buf;
+ int status = statfs(path, &buf);
+ if (status != 0) {
+ return luaL_error(L, "error calling statfs on %s", path);
+ }
+ for (int i = 0; fs_names[i].magic; i++) {
+ if (fs_names[i].magic == buf.f_type) {
+ lua_pushstring(L, fs_names[i].name);
+ return 1;
+ }
+ }
+ lua_pushstring(L, "unknown");
+ return 1;
+}
+#endif
+
static int f_mkdir(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
@@ -709,6 +771,91 @@ static int f_load_native_plugin(lua_State *L) {
return result;
}
+static int f_watch_dir(lua_State *L) {
+ const char *path = luaL_checkstring(L, 1);
+ const int recursive = lua_toboolean(L, 2);
+ uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0);
+ dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL);
+ if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); }
+ lua_pushnumber(L, watch_id.id);
+ return 1;
+}
+
+#if __linux__
+static int f_watch_dir_add(lua_State *L) {
+ dmon_watch_id watch_id;
+ watch_id.id = luaL_checkinteger(L, 1);
+ const char *subdir = luaL_checkstring(L, 2);
+ lua_pushboolean(L, dmon_watch_add(watch_id, subdir));
+ return 1;
+}
+
+static int f_watch_dir_rm(lua_State *L) {
+ dmon_watch_id watch_id;
+ watch_id.id = luaL_checkinteger(L, 1);
+ const char *subdir = luaL_checkstring(L, 2);
+ lua_pushboolean(L, dmon_watch_rm(watch_id, subdir));
+ return 1;
+}
+#endif
+
+#ifdef _WIN32
+#define PATHSEP '\\'
+#else
+#define PATHSEP '/'
+#endif
+
+/* Special purpose filepath compare function. Corresponds to the
+ order used in the TreeView view of the project's files. Returns true iff
+ path1 < path2 in the TreeView order. */
+static int f_path_compare(lua_State *L) {
+ const char *path1 = luaL_checkstring(L, 1);
+ const char *type1_s = luaL_checkstring(L, 2);
+ const char *path2 = luaL_checkstring(L, 3);
+ const char *type2_s = luaL_checkstring(L, 4);
+ const int len1 = strlen(path1), len2 = strlen(path2);
+ int type1 = strcmp(type1_s, "dir") != 0;
+ int type2 = strcmp(type2_s, "dir") != 0;
+ /* Find the index of the common part of the path. */
+ int offset = 0, i;
+ for (i = 0; i < len1 && i < len2; i++) {
+ if (path1[i] != path2[i]) break;
+ if (path1[i] == PATHSEP) {
+ offset = i + 1;
+ }
+ }
+ /* If a path separator is present in the name after the common part we consider
+ the entry like a directory. */
+ if (strchr(path1 + offset, PATHSEP)) {
+ type1 = 0;
+ }
+ if (strchr(path2 + offset, PATHSEP)) {
+ type2 = 0;
+ }
+ /* If types are different "dir" types comes before "file" types. */
+ if (type1 != type2) {
+ lua_pushboolean(L, type1 < type2);
+ return 1;
+ }
+ /* If types are the same compare the files' path alphabetically. */
+ int cfr = 0;
+ int len_min = (len1 < len2 ? len1 : len2);
+ for (int j = offset; j <= len_min; j++) {
+ if (path1[j] == path2[j]) continue;
+ if (path1[j] == 0 || path2[j] == 0) {
+ cfr = (path1[j] == 0);
+ } else if (path1[j] == PATHSEP || path2[j] == PATHSEP) {
+ /* For comparison we treat PATHSEP as if it was the string terminator. */
+ cfr = (path1[j] == PATHSEP);
+ } else {
+ cfr = (path1[j] < path2[j]);
+ }
+ break;
+ }
+ lua_pushboolean(L, cfr);
+ return 1;
+}
+
static const luaL_Reg lib[] = {
{ "poll_event", f_poll_event },
@@ -737,6 +884,13 @@ static const luaL_Reg lib[] = {
{ "fuzzy_match", f_fuzzy_match },
{ "set_window_opacity", f_set_window_opacity },
{ "load_native_plugin", f_load_native_plugin },
+ { "watch_dir", f_watch_dir },
+ { "path_compare", f_path_compare },
+#if __linux__
+ { "watch_dir_add", f_watch_dir_add },
+ { "watch_dir_rm", f_watch_dir_rm },
+ { "get_fs_type", f_get_fs_type },
+#endif
{ NULL, NULL }
};
diff --git a/src/dirmonitor.c b/src/dirmonitor.c
new file mode 100644
index 00000000..0063e400
--- /dev/null
+++ b/src/dirmonitor.c
@@ -0,0 +1,59 @@
+#include <stdio.h>
+#include <string.h>
+
+#include <SDL.h>
+
+#define DMON_IMPL
+#include "dmon.h"
+#include "dmon_extra.h"
+
+#include "dirmonitor.h"
+
+static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) {
+ SDL_Event ev;
+ const int size = strlen(filepath) + 1;
+ /* The string allocated below should be deallocated as soon as the event is
+ treated in the SDL main loop. */
+ char *new_filepath = malloc(size);
+ if (!new_filepath) return;
+ memcpy(new_filepath, filepath, size);
+#ifdef _WIN32
+ for (int i = 0; i < size; i++) {
+ if (new_filepath[i] == '/') {
+ new_filepath[i] = '\\';
+ }
+ }
+#endif
+ SDL_zero(ev);
+ ev.type = SDL_USEREVENT;
+ ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff);
+ ev.user.data1 = new_filepath;
+ SDL_PushEvent(&ev);
+}
+
+void dirmonitor_init() {
+ dmon_init();
+ /* In theory we should register our user event but since we
+ have just one type of user event this is not really needed. */
+ /* sdl_dmon_event_type = SDL_RegisterEvents(1); */
+}
+
+void dirmonitor_deinit() {
+ dmon_deinit();
+}
+
+void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
+ const char *filepath, const char *oldfilepath, void *user)
+{
+ (void) rootdir;
+ (void) user;
+ switch (action) {
+ case DMON_ACTION_MOVE:
+ send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath);
+ send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath);
+ break;
+ default:
+ send_sdl_event(watch_id, action, filepath);
+ }
+}
+
diff --git a/src/dirmonitor.h b/src/dirmonitor.h
new file mode 100644
index 00000000..074a9ae8
--- /dev/null
+++ b/src/dirmonitor.h
@@ -0,0 +1,15 @@
+#ifndef DIRMONITOR_H
+#define DIRMONITOR_H
+
+#include <stdint.h>
+
+#include "dmon.h"
+#include "dmon_extra.h"
+
+void dirmonitor_init();
+void dirmonitor_deinit();
+void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
+ const char *filepath, const char *oldfilepath, void *user);
+
+#endif
+
diff --git a/src/main.c b/src/main.c
index 99aa580f..9fcbeab6 100644
--- a/src/main.c
+++ b/src/main.c
@@ -14,6 +14,8 @@
#include <mach-o/dyld.h>
#endif
+#include "dirmonitor.h"
+
SDL_Window *window;
@@ -106,6 +108,8 @@ int main(int argc, char **argv) {
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm);
+ dirmonitor_init();
+
window = SDL_CreateWindow(
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
@@ -188,6 +192,7 @@ init_lua:
lua_close(L);
ren_free_window_resources();
+ dirmonitor_deinit();
return EXIT_SUCCESS;
}
diff --git a/src/meson.build b/src/meson.build
index 6fefd257..1eaf87fd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -4,6 +4,7 @@ lite_sources = [
'api/regex.c',
'api/system.c',
'api/process.c',
+ 'dirmonitor.c',
'renderer.c',
'renwindow.c',
'rencache.c',
@@ -18,11 +19,11 @@ elif host_machine.system() == 'darwin'
lite_sources += 'bundle_open.m'
endif
-lite_include = include_directories('.')
+lite_includes += include_directories('.')
executable('lite-xl',
lite_sources + lite_rc,
- include_directories: [lite_include],
+ include_directories: lite_includes,
dependencies: lite_deps,
c_args: lite_cargs,
objc_args: lite_cargs,
diff --git a/src/rencache.c b/src/rencache.c
index c3254cd0..e9339ecb 100644
--- a/src/rencache.c
+++ b/src/rencache.c
@@ -123,7 +123,9 @@ void rencache_set_clip_rect(RenRect rect) {
void rencache_draw_rect(RenRect rect, RenColor color) {
- if (!rects_overlap(screen_rect, rect)) { return; }
+ if (!rects_overlap(screen_rect, rect) || rect.width == 0 || rect.height == 0) {
+ return;
+ }
Command *cmd = push_command(DRAW_RECT, COMMAND_BARE_SIZE);
if (cmd) {
cmd->rect = rect;