diff options
| author | Adam <adamdharrison@gmail.com> | 2025-03-15 10:17:17 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-15 15:17:17 +0100 |
| commit | e085c84e6caa18610ea07e7c7c31967ae2c1658d (patch) | |
| tree | e3d1bb8a711b0028c06b40ce0223e929eab2a4e1 /data | |
| parent | f30214464c0613c281c91be192c2ef8360f3543e (diff) | |
| download | lite-xl-e085c84e6caa18610ea07e7c7c31967ae2c1658d.tar.gz lite-xl-e085c84e6caa18610ea07e7c7c31967ae2c1658d.zip | |
Project Rework (#1455)
* Initial commit to clean up projects; spun off find-file to its own plugin, removed project limit, removed the concept of a project maintaining an ordered list of files, and allowed treeview to see things like hidden files and files not actually in the project.
Normalizing things, fixed typo.
Abstracted root project, and made things more in line with current master behaviour.
Removed unused legacy code, as well as ensured that we use absolute paths.
Fixed issue with backslahes on linux, will look at windows at some point.
Removed stray print.
Removed orphaned function.
Removed extraneous command.
Fixed the ability to close project folders.
Removed superceded function.
Applied jgm's suggestions.
* Bump modversion.
* Bumped modversion and changed a few minor things.
* Added in handling of ignored files.
* Fixed small issue.
* Fixed issue with absolute arguments.
* Removed home encoding; may revert this if I can find out why it was done.
* Fixed minor issue with file suggestions.
* Cleaned up treeview.
* Typo.
* Added in visible.
* Ensured that the appropriate project module is loaded.
* Fixed improper rebase.
* Abstracted out the storage system of the workspace plugin so other plugins can use it.
* Fixed double return.
* Fixed functional issue.
* Added documentation.
* Sumenko reports duplicate function definitions, unsure why.
* Fixed minor bug with workspace.
* Fixed switching projects on restart.
* Harmonized spacing around asserts, and fixed an issue forgetting to set has_restarted.
* Made project an object.
* Removing unecessary yields.
* Removed unecessary fallback.
* Removed unecessary line.
* Reveted backslash handling, as it doesn't seem to make any difference.
* Spacing.
* Only stonks.
* Removed uneeded error handling.
* Added in function to determine project by path, and added in deprecation warnings for legacy interface.
* Removed storage module.
* Typo.
* Changed to use deprecation log instead of regular warn so as to not spam logs.
* Fixed small bug with saving workspaces on project change.
* Update data/core/init.lua
---------
Diffstat (limited to 'data')
40 files changed, 498 insertions, 886 deletions
diff --git a/data/core/bit.lua b/data/core/bit.lua deleted file mode 100644 index e55fb9bf..00000000 --- a/data/core/bit.lua +++ /dev/null @@ -1,36 +0,0 @@ -local bit = {} - -local LUA_NBITS = 32 -local ALLONES = (~(((~0) << (LUA_NBITS - 1)) << 1)) - -local function trim(x) - return (x & ALLONES) -end - -local function mask(n) - return (~((ALLONES << 1) << ((n) - 1))) -end - -local function check_args(field, width) - assert(field >= 0, "field cannot be negative") - assert(width > 0, "width must be positive") - assert(field + width < LUA_NBITS and field + width >= 0, - "trying to access non-existent bits") -end - -function bit.extract(n, field, width) - local w = width or 1 - check_args(field, w) - local m = trim(n) - return m >> field & mask(w) -end - -function bit.replace(n, v, field, width) - local w = width or 1 - check_args(field, w) - local m = trim(n) - local x = v & mask(width); - return m & ~(mask(w) << field) | (x << field) -end - -return bit diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 878e6230..ece5f962 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -10,9 +10,9 @@ local restore_title_view = false local function suggest_directory(text) text = common.home_expand(text) - local basedir = common.dirname(core.project_dir) + local basedir = common.dirname(core.root_project().path) return common.home_encode_list((basedir and text == basedir .. PATHSEP or text == "") and - core.recent_projects or common.dir_path_suggest(text)) + core.recent_projects or common.dir_path_suggest(text, core.root_project().path)) end local function check_directory_path(path) @@ -86,28 +86,6 @@ command.add(nil, { }) end, - ["core:find-file"] = function() - 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 - if item.type == "file" then - local path = (dir == core.project_dir and "" or dir .. PATHSEP) - table.insert(files, common.home_encode(path .. item.filename)) - end - end - core.command_view:enter("Open File From Project", { - submit = function(text, item) - text = item and item.text or text - core.root_view:open_doc(core.open_doc(common.home_expand(text))) - end, - suggest = function(text) - return common.fuzzy_match_with_recents(files, core.visited_files, text) - end - }) - end, - ["core:new-doc"] = function() core.root_view:open_doc(core.open_doc()) end, @@ -127,20 +105,20 @@ command.add(nil, { local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") if dirname then dirname = core.normalize_to_project_dir(dirname) - text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP + text = dirname == core.root_project().path and "" or common.home_encode(dirname) .. PATHSEP end end core.command_view:enter("Open File", { text = text, submit = function(text) - local filename = system.absolute_path(common.home_expand(text)) + local filename = core.project_absolute_path(common.home_expand(text)) core.root_view:open_doc(core.open_doc(filename)) end, suggest = function (text) - return common.home_encode_list(common.path_suggest(common.home_expand(text))) - end, + return common.home_encode_list(common.path_suggest(common.home_expand(text), core.root_project() and core.root_project().path)) + end, validate = function(text) - local filename = common.home_expand(text) + local filename = core.project_absolute_path(common.home_expand(text)) local path_stat, err = system.get_file_info(filename) if err then if err:find("No such file", 1, true) then @@ -182,7 +160,7 @@ command.add(nil, { end, ["core:change-project-folder"] = function() - local dirname = common.dirname(core.project_dir) + local dirname = common.dirname(core.root_project().path) local text if dirname then text = common.home_encode(dirname) .. PATHSEP @@ -196,9 +174,9 @@ command.add(nil, { core.error("Cannot open directory %q", path) return end - if abs_path == core.project_dir then return end + if abs_path == core.root_project().path then return end core.confirm_close_docs(core.docs, function(dirpath) - core.open_folder_project(dirpath) + core.open_project(dirpath) end, abs_path) end, suggest = suggest_directory @@ -206,7 +184,7 @@ command.add(nil, { end, ["core:open-project-folder"] = function() - local dirname = common.dirname(core.project_dir) + local dirname = common.dirname(core.root_project().path) local text if dirname then text = common.home_encode(dirname) .. PATHSEP @@ -220,7 +198,7 @@ command.add(nil, { core.error("Cannot open directory %q", path) return end - if abs_path == core.project_dir then + if abs_path == core.root_project().path then core.error("Directory %q is currently opened", abs_path) return end @@ -242,7 +220,7 @@ command.add(nil, { core.error("%q is not a directory", text) return end - core.add_project_directory(system.absolute_path(text)) + core.add_project(system.absolute_path(text)) end, suggest = suggest_directory }) @@ -250,14 +228,14 @@ command.add(nil, { ["core:remove-directory"] = function() local dir_list = {} - local n = #core.project_directories + local n = #core.projects for i = n, 2, -1 do - dir_list[n - i + 1] = core.project_directories[i].name + dir_list[n - i + 1] = core.projects[i].name end core.command_view:enter("Remove Directory", { submit = function(text, item) text = common.home_expand(item and item.text or text) - if not core.remove_project_directory(text) then + if not core.remove_project(text) then core.error("No directory %q to be removed", text) end end, diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 66e88eea..2bae61d6 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -580,6 +580,7 @@ local commands = { elseif last_doc and last_doc.filename then local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$") text = core.normalize_to_project_dir(dirname) .. PATHSEP + if text == core.root_project().path then text = "" end end core.command_view:enter("Save As", { text = text, diff --git a/data/core/common.lua b/data/core/common.lua index 1a89f23f..83a5c8d0 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -282,10 +282,11 @@ end ---Returns a list of directories that are related to a path. ---@param text string The input path. +---@param root string The root directory. ---@return string[] -function common.dir_path_suggest(text) +function common.dir_path_suggest(text, root) local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$") - local files = system.list_dir(path == "" and "." or path) or {} + local files = system.list_dir(path == "" and root or path) or {} local res = {} for _, file in ipairs(files) do file = path .. file @@ -484,12 +485,7 @@ end ---@return string function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then - local dir_pos = #HOME + 1 - -- ensure we don't replace if the text is just "$HOME" or "$HOME/" so - -- it must have a "/" following the $HOME and some characters following. - if string.find(text, PATHSEP, dir_pos, true) == dir_pos and #text > dir_pos then - return "~" .. text:sub(dir_pos) - end + return "~" .. text:sub(#HOME + 1) end return text end diff --git a/data/core/config.lua b/data/core/config.lua index e564f42d..33db0b99 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -171,15 +171,6 @@ config.line_endings = PLATFORM == "Windows" and "crlf" or "lf" ---@type number config.line_limit = 80 ----Maximum number of project files to keep track of. ----If the number of files in the project exceeds this number, ----Lite XL will not be able to keep track of them. ----They will be not be searched when searching for files or text. ---- ----Defaults to 2000. ----@type number -config.max_project_files = 2000 - ---Enables/disables all transitions. --- ---Defaults to true. diff --git a/data/core/dirwatch.lua b/data/core/dirwatch.lua index eb8cb2e5..16aecd77 100644 --- a/data/core/dirwatch.lua +++ b/data/core/dirwatch.lua @@ -101,7 +101,13 @@ function dirwatch:check(change_callback, scan_time, wait_time) end change_callback(path) elseif self.reverse_watched[id] then - change_callback(self.reverse_watched[id]) + local path = self.reverse_watched[id] + change_callback(path) + local info = system.get_file_info(path) + if info and info.type == "file" then + self:unwatch(path) + self:watch(path) + end end end, function(err) last_error = err @@ -126,114 +132,4 @@ function dirwatch:check(change_callback, scan_time, wait_time) return had_change end - --- inspect config.ignore_files patterns and prepare ready to use entries. -local function compile_ignore_files() - local ipatterns = config.ignore_files - local compiled = {} - -- config.ignore_files could be a simple string... - if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end - for i, pattern in ipairs(ipatterns) do - -- we ignore malformed patterns that raise an error - if pcall(string.match, "a", pattern) then - table.insert(compiled, { - use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end - -- A '/' or '/$' at the end means we want to match a directory. - match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value - pattern = pattern -- get the actual pattern - }) - end - end - return compiled -end - - -local function fileinfo_pass_filter(info, ignore_compiled) - if info.size >= config.file_size_limit * 1e6 then return false end - local basename = common.basename(info.filename) - -- replace '\' with '/' for Windows where PATHSEP = '\' - local fullname = "/" .. info.filename:gsub("\\", "/") - for _, compiled in ipairs(ignore_compiled) do - local test = compiled.use_path and fullname or basename - if compiled.match_dir then - if info.type == "dir" and string.match(test .. "/", compiled.pattern) then - return false - end - else - if string.match(test, compiled.pattern) then - return false - end - end - end - return true -end - - -local function compare_file(a, b) - return system.path_compare(a.filename, a.type, b.filename, b.type) -end - - --- compute a file's info entry completed with "filename" to be used --- in project scan and return it or falsy if it shouldn't appear in the list. -local function get_project_file_info(root, file, ignore_compiled) - local info = system.get_file_info(root .. PATHSEP .. file) - -- In some cases info.type is nil even if info is valid. - -- This happens when it is neither a file nor a directory, - -- for example /dev/* entries on linux. - if info and info.type then - info.filename = file - return fileinfo_pass_filter(info, ignore_compiled) and info - end -end - - --- "root" will by an absolute path without trailing '/' --- "path" will be a path starting without '/' and without trailing '/' --- or the empty string. --- It identifies a sub-path within "root". --- The current path location will therefore always be: root .. '/' .. path. --- When recursing, "root" will always be the same, only "path" will change. --- Returns a list of file "items". In each item the "filename" will be the --- complete file path relative to "root" *without* the trailing '/', and without the starting '/'. -function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred) - local t = {} - local t0 = system.get_time() - local ignore_compiled = compile_ignore_files() - - local all = system.list_dir(root .. PATHSEP .. path) - if not all then return nil end - local entries = { } - for _, file in ipairs(all) do - local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled) - if info then - table.insert(entries, info) - end - end - table.sort(entries, compare_file) - - local recurse_complete = true - for _, info in ipairs(entries) do - table.insert(t, info) - entries_count = entries_count + 1 - if info.type == "dir" then - if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then - local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred) - recurse_complete = recurse_complete and complete - if n ~= nil then - entries_count = n - for _, info_rec in ipairs(t_rec) do - table.insert(t, info_rec) - end - end - else - recurse_complete = false - end - end - end - - return t, recurse_complete, entries_count -end - - return dirwatch diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 502da779..fc062997 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -25,7 +25,7 @@ function Doc:new(filename, abs_filename, new_file) if filename then self:set_filename(filename, abs_filename) if not new_file then - self:load(filename) + self:load(abs_filename) end end if new_file then @@ -89,7 +89,7 @@ end function Doc:reload() if self.filename then local sel = { self:get_selection() } - self:load(self.filename) + self:load(self.abs_filename) self:clean() self:set_selection(table.unpack(sel)) end @@ -109,15 +109,15 @@ function Doc:save(filename, abs_filename) -- On Windows, opening a hidden file with wb fails with a permission error. -- To get around this, we must open the file as r+b and truncate. -- Since r+b fails if file doesn't exist, fall back to wb. - fp = io.open(filename, "r+b") + fp = io.open(abs_filename, "r+b") if fp then system.ftruncate(fp) else -- file probably doesn't exist, create one - fp = assert ( io.open(filename, "wb") ) + fp = assert (io.open(abs_filename, "wb")) end else - fp = assert ( io.open(filename, "wb") ) + fp = assert (io.open(abs_filename, "wb")) end for _, line in ipairs(self.lines) do diff --git a/data/core/init.lua b/data/core/init.lua index 49732bca..dcdfd5ab 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -14,6 +14,7 @@ local CommandView local NagView local DocView local Doc +local Project local core = {} @@ -54,17 +55,6 @@ local function update_recents_project(action, dir_path_abs) 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_volume(new_dir) - core.project_directories = {} - end - return chdir_ok -end - - local function reload_customizations() local user_error = not core.load_user_directory() local project_error = not core.load_project_module() @@ -86,374 +76,47 @@ local function reload_customizations() end -function core.open_folder_project(dir_path_abs) - if core.set_project_dir(dir_path_abs, core.on_quit_project) then - core.root_view:close_all_docviews() - reload_customizations() - update_recents_project("add", dir_path_abs) - core.add_project_directory(dir_path_abs) - core.on_enter_project(dir_path_abs) - end -end - - -local function strip_leading_path(filename) - return filename:sub(2) -end - -local function strip_trailing_slash(filename) - if filename:match("[^:]["..PATHSEP.."]$") then - return filename:sub(1, -2) - end - return filename -end - - -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 https://github.com/lite-xl/lite-xl." - core.warn(message) -end - - --- bisects the sorted file list to get to things in ln(n) -local function file_bisect(files, is_superior, start_idx, end_idx) - local inf, sup = start_idx or 1, end_idx or #files - while sup - inf > 8 do - local curr = math.floor((inf + sup) / 2) - if is_superior(files[curr]) then - sup = curr - 1 - else - inf = curr - end - end - while inf <= sup and not is_superior(files[inf]) do - inf = inf + 1 - end - return inf -end - - -local function file_search(files, info) - local idx = file_bisect(files, function(file) - return system.path_compare(info.filename, info.type, file.filename, file.type) - end) - if idx > 1 and files[idx-1].filename == info.filename then - return idx - 1, true - end - return idx, false -end - - -local function files_info_equal(a, b) - return (a == nil and b == nil) or (a and b and a.filename == b.filename and a.type == b.type) -end - - -local function project_subdir_bounds(dir, filename, start_index) - local found = true - if not start_index then - start_index, found = file_search(dir.files, { type = "dir", filename = filename }) - end - if found then - local end_index = file_bisect(dir.files, function(file) - return not common.path_belongs_to(file.filename, filename) - end, start_index + 1) - return start_index, end_index - start_index, dir.files[start_index] - end -end - - --- Should be called on any directory that registers a change, or on a directory we open if we're over the file limit. --- Uses relative paths at the project root (i.e. target = "", target = "first-level-directory", target = "first-level-directory/second-level-directory") -local function refresh_directory(topdir, target) - local directory_start_idx, directory_end_idx = 1, #topdir.files - if target and target ~= "" then - directory_start_idx, directory_end_idx = project_subdir_bounds(topdir, target) - directory_end_idx = directory_start_idx + directory_end_idx - 1 - directory_start_idx = directory_start_idx + 1 - end - - local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), 0, function() return false end) - local change = false - - -- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that. - -- Unwatch just in case. - if files == nil then - topdir.watch:unwatch(topdir.name .. PATHSEP .. (target or "")) - return true - end - - local new_idx, old_idx = 1, directory_start_idx - local new_directories = {} - -- Run through each sorted list and compare them. If we find a new entry, insert it and flag as new. If we're missing an entry - -- remove it and delete the entry from the list. - while old_idx <= directory_end_idx or new_idx <= #files do - local old_info, new_info = topdir.files[old_idx], files[new_idx] - if not files_info_equal(new_info, old_info) then - change = true - -- If we're a new file, and we exist *before* the other file in the list, then add to the list. - if not old_info or (new_info and system.path_compare(new_info.filename, new_info.type, old_info.filename, old_info.type)) then - table.insert(topdir.files, old_idx, new_info) - old_idx, new_idx = old_idx + 1, new_idx + 1 - if new_info.type == "dir" then - table.insert(new_directories, new_info) - end - directory_end_idx = directory_end_idx + 1 - else - -- If it's not there, remove the entry from the list as being out of order. - table.remove(topdir.files, old_idx) - if old_info.type == "dir" then - topdir.watch:unwatch(topdir.name .. PATHSEP .. old_info.filename) - end - directory_end_idx = directory_end_idx - 1 - end - else - -- If this file is a directory, determine in ln(n) the size of the directory, and skip every file in it. - local size = old_info and old_info.type == "dir" and select(2, project_subdir_bounds(topdir, old_info.filename, old_idx)) or 1 - old_idx, new_idx = old_idx + size, new_idx + 1 - end - end - for i, v in ipairs(new_directories) do - topdir.watch:watch(topdir.name .. PATHSEP .. v.filename) - if not topdir.files_limit or core.project_subdir_is_shown(topdir, v.filename) then - refresh_directory(topdir, v.filename) - end - end - if change then - core.redraw = true - topdir.is_dirty = true - end - return change -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 - - -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 topdir = { - name = path, - item = {filename = common.basename(path), type = "dir", topdir = true}, - files_limit = false, - is_dirty = true, - shown_subdir = {}, - watch_thread = nil, - watch = dirwatch.new() - } - table.insert(core.project_directories, topdir) - - local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown" - topdir.force_scans = (fstype == "nfs" or fstype == "fuse") - local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", 0, timed_max_files_pred) - topdir.files = t - if not complete then - topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files) - topdir.files_limit = true - show_max_files_warning(topdir) - refresh_directory(topdir) - else - for i,v in ipairs(t) do - if v.type == "dir" then topdir.watch:watch(path .. PATHSEP .. v.filename) end - end - end - topdir.watch:watch(topdir.name) - -- each top level directory gets a watch thread. if the project is small, or - -- if the ablity to use directory watches hasn't been compromised in some way - -- either through error, or amount of files, then this should be incredibly - -- quick; essentially one syscall per check. Otherwise, this may take a bit of - -- time; the watch will yield in this coroutine after 0.01 second, for 0.1 seconds. - topdir.watch_thread = core.add_thread(function() - while true do - local changed = topdir.watch:check(function(target) - if target == topdir.name then return refresh_directory(topdir) end - local dirpath = target:sub(#topdir.name + 2) - local abs_dirpath = topdir.name .. PATHSEP .. dirpath - if dirpath then - -- check if the directory is in the project files list, if not exit. - local dir_index, dir_match = file_search(topdir.files, {filename = dirpath, type = "dir"}) - if not dir_match or not core.project_subdir_is_shown(topdir, topdir.files[dir_index].filename) then return end - end - return refresh_directory(topdir, dirpath) - end, 0.01, 0.01) - -- properly exit coroutine if project not open anymore to clear dir watch - local project_dir_open = false - for _, prj in ipairs(core.project_directories) do - if topdir == prj then - project_dir_open = true - break - end - end - if project_dir_open then - coroutine.yield(changed and 0 or 0.05) - else - return - end - end - end) - - if path == core.project_dir then - core.project_files = topdir.files - end +function core.add_project(project) + project = type(project) == "string" and Project(common.normalize_volume(project)) or project + table.insert(core.projects, project) core.redraw = true - return topdir + return project end --- The function below is needed to reload the project directories --- when the project's module changes. -function core.rescan_project_directories() - local save_project_dirs = {} - local n = #core.project_directories - for i = 1, n do - local dir = core.project_directories[i] - save_project_dirs[i] = {name = dir.name, shown_subdir = dir.shown_subdir} - end - core.project_directories = {} - for i = 1, n do -- add again the directories in the project - local dir = core.add_project_directory(save_project_dirs[i].name) - if dir.files_limit then - -- We need to sort the list of shown subdirectories so that higher level - -- directories are populated first. We use the function system.path_compare - -- because it order the entries in the appropriate order. - -- TODO: we may consider storing the table shown_subdir as a sorted table - -- since the beginning. - local subdir_list = {} - for subdir in pairs(save_project_dirs[i].shown_subdir) do - table.insert(subdir_list, subdir) - end - table.sort(subdir_list, function(a, b) return system.path_compare(a, "dir", b, "dir") end) - for _, subdir in ipairs(subdir_list) do - local show = save_project_dirs[i].shown_subdir[subdir] - for j = 1, #dir.files do - if dir.files[j].filename == subdir then - -- The instructions below match when happens in TreeView:on_mouse_pressed. - -- We perform the operations only once iff the subdir is in dir.files. - -- In theory set_show below may fail and return false but is it is listed - -- there it means it succeeded before so we are optimistically assume it - -- will not fail for the sake of simplicity. - core.update_project_subdir(dir, subdir, show) - break - end - end - end - end - end -end - - -function core.project_dir_by_name(name) - for i = 1, #core.project_directories do - if core.project_directories[i].name == name then - return core.project_directories[i] +function core.remove_project(project, force) + for i = (force and 1 or 2), #core.projects do + if project == core.projects[i] or project == core.projects[i].path then + local project = core.projects[i] + table.remove(core.projects, i) + return project end end + return false end -function core.update_project_subdir(dir, filename, expanded) - assert(dir.files_limit, "function should be called only when directory is in files limit mode") - dir.shown_subdir[filename] = expanded - if expanded then - dir.watch:watch(dir.name .. PATHSEP .. filename) - else - dir.watch:unwatch(dir.name .. PATHSEP .. filename) - end - return refresh_directory(dir, filename) +function core.set_project(project) + while #core.projects > 0 do core.remove_project(core.projects[#core.projects], true) end + return core.add_project(project) 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 - 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) - elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) then - find_files_rec(root, PATHSEP .. info.filename) - end - end - end +function core.open_project(project) + local project = core.set_project(project) + core.root_view:close_all_docviews() + reload_customizations() + update_recents_project("add", project.path) end --- Iterator function to list all project files -local function project_files_iter(state) - local 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() - local state = { dir_index = 1, file_index = 0 } - return project_files_iter, state -end - - -function core.project_files_number() - 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 +local function strip_trailing_slash(filename) + if filename:match("[^:]["..PATHSEP.."]$") then + return filename:sub(1, -2) end - return n + return filename end - -- create a directory using mkdir but may need to create the parent -- directories as well. local function create_user_directory() @@ -612,19 +275,6 @@ function core.load_user_directory() 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 - local dir = core.project_directories[i] - if dir.name == path then - table.remove(core.project_directories, i) - return true - end - end - return false -end - - function core.configure_borderless_window() system.set_window_bordered(not config.borderless) core.title_view:configure_hit_test(config.borderless) @@ -637,36 +287,16 @@ local function add_config_files_hooks() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") function Doc:save(filename, abs_filename) - local module_filename = system.absolute_path(".lite_project.lua") + local module_filename = core.project_absolute_path(".lite_project.lua") doc_save(self, filename, abs_filename) if self.abs_filename == user_filename or self.abs_filename == module_filename then reload_customizations() - core.rescan_project_directories() core.configure_borderless_window() end end end --- The function below works like system.absolute_path except it --- doesn't fail if the file does not exist. We consider that the --- current dir is core.project_dir so relative filename are considered --- to be in core.project_dir. --- Please note that .. or . in the filename are not taken into account. --- This function should get only filenames normalized using --- common.normalize_path function. -function core.project_absolute_path(filename) - if common.is_absolute_path(filename) then - return common.normalize_path(filename) - elseif not core.project_dir then - local cwd = system.absolute_path(".") - return cwd .. PATHSEP .. common.normalize_path(filename) - else - return core.project_dir .. PATHSEP .. filename - end -end - - function core.init() core.log_items = {} core.log_quiet("Lite XL version %s - mod-version %s", VERSION, MOD_VERSION_STRING) @@ -680,6 +310,7 @@ function core.init() TitleView = require "core.titleview" CommandView = require "core.commandview" NagView = require "core.nagview" + Project = require "core.project" DocView = require "core.docview" Doc = require "core.doc" @@ -708,19 +339,21 @@ function core.init() local project_dir = core.recent_projects[1] or "." local project_dir_explicit = false local files = {} - for i = 2, #ARGS do - local arg_filename = strip_trailing_slash(ARGS[i]) - local info = system.get_file_info(arg_filename) or {} - if info.type == "dir" then - project_dir = arg_filename - project_dir_explicit = true - else - -- on macOS we can get an argument like "-psn_0_52353" that we just ignore. - if not ARGS[i]:match("^-psn") then - local file_abs = core.project_absolute_path(arg_filename) - if file_abs then - table.insert(files, file_abs) - project_dir = file_abs:match("^(.+)[/\\].+$") + if not RESTARTED then + for i = 2, #ARGS do + local arg_filename = strip_trailing_slash(ARGS[i]) + local info = system.get_file_info(arg_filename) or {} + if info.type == "dir" then + project_dir = arg_filename + project_dir_explicit = true + else + -- on macOS we can get an argument like "-psn_0_52353" that we just ignore. + if not ARGS[i]:match("^-psn") then + local file_abs = common.is_absolute_path(arg_filename) and arg_filename or (system.absolute_path(".") .. PATHSEP .. common.normalize_path(arg_filename)) + if file_abs then + table.insert(files, file_abs) + project_dir = file_abs:match("^(.+)[/\\].+$") + end end end end @@ -729,6 +362,7 @@ function core.init() core.frame_start = 0 core.clip_rect_stack = {{ 0,0,0,0 }} core.docs = {} + core.projects = {} core.cursor_clipboard = {} core.cursor_clipboard_whole_line = {} core.window_mode = "normal" @@ -769,10 +403,9 @@ function core.init() local got_user_error, got_project_error = not core.load_user_directory() local project_dir_abs = system.absolute_path(project_dir) - -- We prevent set_project_dir below to effectively add and scan the directory because the + -- We prevent set_project below to effectively add and scan the directory because the -- project module and its ignore files is not yet loaded. - local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) - if set_project_ok then + if project_dir_abs and pcall(core.set_project, project_dir_abs) then got_project_error = not core.load_project_module() if project_dir_explicit then update_recents_project("add", project_dir_abs) @@ -782,12 +415,8 @@ function core.init() update_recents_project("remove", project_dir) end project_dir_abs = system.absolute_path(".") - if not core.set_project_dir(project_dir_abs, function() - got_project_error = not core.load_project_module() - end) then - system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") - os.exit(1) - end + core.set_project(project_dir_abs) + got_project_error = not core.load_project_module() end -- Load core and user plugins giving preference to user ones with same name. @@ -798,15 +427,6 @@ function core.init() core.log("Opening project %q from directory %s", pname, pdir) end - -- We add the project directory now because the project's module is loaded. - core.add_project_directory(project_dir_abs) - - -- We assume we have just a single project directory here. Now that StatusView - -- is there show max files warning if needed. - if core.project_directories[1].files_limit then - 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 @@ -900,33 +520,26 @@ function core.temp_filename(ext, dir) .. string.format("%06x", temp_file_counter) .. (ext or "") end --- override to perform an operation before quitting or entering the --- current project -do - local do_nothing = function() end - core.on_quit_project = do_nothing - core.on_enter_project = do_nothing -end - -local function quit_with_function(quit_fn, force) +function core.exit(quit_fn, force) if force then core.delete_temp_files() - core.on_quit_project() + while #core.projects > 0 do core.remove_project(core.projects[#core.projects], true) end save_session() quit_fn() else - core.confirm_close_docs(core.docs, quit_with_function, quit_fn, true) + core.confirm_close_docs(core.docs, core.exit, quit_fn, true) end end + function core.quit(force) - quit_with_function(function() core.quit_request = true end, force) + core.exit(function() core.quit_request = true end, force) end function core.restart() - quit_with_function(function() + core.exit(function() core.restart_request = true core.window:_persist() end) @@ -1074,7 +687,8 @@ end function core.load_project_module() - local filename = ".lite_project.lua" + local filename = core.root_project():absolute_path(".lite_project.lua") + if system.get_file_info(filename) then return core.try(function() local fn, err = loadfile(filename) @@ -1166,24 +780,26 @@ function core.pop_clip_rect() renderer.set_clip_rect(x, y, w, h) end - -function core.normalize_to_project_dir(filename) - filename = common.normalize_path(filename) - if common.path_belongs_to(filename, core.project_dir) then - filename = common.relative_path(core.project_dir, filename) +function core.root_project() return core.projects[1] end +function core.project_for_path(path) + for i, project in ipairs(core.projects) do + if project.path:find(path, 1, true) then return project end end - return filename + return nil end - +-- Legacy interface; do not use. Use a specific project instead. When in doubt, use root_project. +function core.normalize_to_project_dir(path) core.deprecation_log("core.normalize_to_project_dir") return core.root_project():normalize_path(path) end +function core.project_absolute_path(path) core.deprecation_log("core.project_absolute_path") return core.root_project() and core.root_project():absolute_path(path) or system.absolute_path(path) end function core.open_doc(filename) - local new_file = not filename or not system.get_file_info(filename) + local new_file = true local abs_filename if filename then -- normalize filename and set absolute filename then -- try to find existing doc for filename - filename = core.normalize_to_project_dir(filename) - abs_filename = core.project_absolute_path(filename) + filename = core.root_project():normalize_path(filename) + abs_filename = core.root_project():absolute_path(filename) + new_file = not system.get_file_info(abs_filename) for _, doc in ipairs(core.docs) do if doc.abs_filename and abs_filename == doc.abs_filename then return doc diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 965191ef..9183a74b 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -1,7 +1,6 @@ local function keymap_macos(keymap) keymap.add_direct { ["cmd+shift+p"] = "core:find-command", - ["cmd+p"] = "core:find-file", ["cmd+o"] = "core:open-file", ["cmd+n"] = "core:new-doc", ["cmd+shift+c"] = "core:change-project-folder", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 844a97e9..2fbd1c32 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -288,7 +288,6 @@ end keymap.add_direct { ["ctrl+shift+p"] = "core:find-command", - ["ctrl+p"] = "core:find-file", ["ctrl+o"] = "core:open-file", ["ctrl+n"] = "core:new-doc", ["ctrl+shift+c"] = "core:change-project-folder", diff --git a/data/core/project.lua b/data/core/project.lua new file mode 100644 index 00000000..5036afad --- /dev/null +++ b/data/core/project.lua @@ -0,0 +1,133 @@ +local Object = require "core.object" + +local Project = Object:extend() + +local core = require "core" +local common = require "core.common" +local config = require "core.config" + +-- inspect config.ignore_files patterns and prepare ready to use entries. +local function compile_ignore_files() + local ipatterns = config.ignore_files + local compiled = {} + -- config.ignore_files could be a simple string... + if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end + for i, pattern in ipairs(ipatterns) do + -- we ignore malformed pattern that raise an error + if pcall(string.match, "a", pattern) then + table.insert(compiled, { + use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end + -- An '/' or '/$' at the end means we want to match a directory. + match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value + pattern = pattern -- get the actual pattern + }) + end + end + return compiled +end + + +function Project:new(path) + self.path = path + self.name = common.basename(path) + self.compiled = compile_ignore_files() + return self +end + + +-- The function below works like system.absolute_path except it +-- doesn't fail if the file does not exist. We consider that the +-- current dir is core.project_dir so relative filename are considered +-- to be in core.project_dir. +-- Please note that .. or . in the filename are not taken into account. +-- This function should get only filenames normalized using +-- common.normalize_path function. +function Project:absolute_path(filename) + if common.is_absolute_path(filename) then + return common.normalize_path(filename) + elseif not self or not self.path then + local cwd = system.absolute_path(".") + return cwd .. PATHSEP .. common.normalize_path(filename) + else + return self.path .. PATHSEP .. filename + end +end + + +function Project:normalize_path(filename) + filename = common.normalize_path(filename) + if common.path_belongs_to(filename, self.path) then + filename = common.relative_path(self.path, filename) + end + return filename +end + + + +local function fileinfo_pass_filter(info, ignore_compiled) + if info.size >= config.file_size_limit * 1e6 then return false end + local basename = common.basename(info.filename) + -- replace '\' with '/' for Windows where PATHSEP = '\' + local fullname = "/" .. info.filename:gsub("\\", "/") + for _, compiled in ipairs(ignore_compiled) do + local test = compiled.use_path and fullname or basename + if compiled.match_dir then + if info.type == "dir" and string.match(test .. "/", compiled.pattern) then + return false + end + else + if string.match(test, compiled.pattern) then + return false + end + end + end + return true +end + + + +function Project:is_ignored(info, path) + -- info can be not nil but info.type may be nil if is neither a file neither + -- a directory, for example for /dev/* entries on linux. + if info and info.type then + if path then info.filename = path end + return not fileinfo_pass_filter(info, self.compiled) + end + return false +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. +function Project:get_file_info(path) + local info = system.get_file_info(path) + if self:is_ignored(info, path) then return nil end + return info +end + + +local function find_files_rec(project, path) + local all = system.list_dir(path) or {} + for _, file in ipairs(all) do + local file = path .. PATHSEP .. file + local info = project:get_file_info(file) + if info then + info.filename = file + if info.type == "file" then + coroutine.yield(project, info) + elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) and info.type then + find_files_rec(project, file) + end + end + end +end + + +function Project:files() + return coroutine.wrap(function() + find_files_rec(self, self.path) + end) +end + + + +return Project diff --git a/data/core/start.lua b/data/core/start.lua index 439f1cbc..51d9729e 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,6 @@ -- this file is used by lite-xl to setup the Lua environment when starting VERSION = "@PROJECT_VERSION@" -MOD_VERSION_MAJOR = 3 +MOD_VERSION_MAJOR = 4 MOD_VERSION_MINOR = 0 MOD_VERSION_PATCH = 0 MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH) @@ -111,8 +111,6 @@ function get_current_require_path() return require_stack[#require_stack] end -bit32 = bit32 or require "core.bit" - require "core.utf8string" require "core.process" diff --git a/data/core/statusview.lua b/data/core/statusview.lua index f8d951bb..864d9bb3 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -367,10 +367,7 @@ function StatusView:register_command_items() alignment = StatusView.Item.RIGHT, get_item = function() return { - style.icon_font, "g", - style.font, style.dim, self.separator2, - style.text, #core.docs, style.text, " / ", - #core.project_files, " files" + style.icon_font, "g" } end }) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 5f4c2cdf..65040575 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local config = require "core.config" diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index 7086fb35..49ef9923 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local Doc = require "core.doc" @@ -24,15 +24,9 @@ local watch = dirwatch.new() local times = setmetatable({}, { __mode = "k" }) local visible = setmetatable({}, { __mode = "k" }) -local function get_project_doc_watch(doc) - for _, v in ipairs(core.project_directories) do - if doc.abs_filename:find(v.name, 1, true) == 1 then return v.watch end - end - return watch -end - local function update_time(doc) - times[doc] = system.get_file_info(doc.filename).modified + local info = system.get_file_info(doc.filename) + times[doc] = info and info.modified end local function reload_doc(doc) @@ -73,8 +67,8 @@ end local function check_prompt_reload(doc) if doc and doc.deferred_reload then core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", { - { text = "Yes", default_yes = true }, - { text = "No", default_no = true } + { font = style.font, text = "Yes", default_yes = true }, + { font = style.font, text = "No" , default_no = true } }, function(item) if item.text == "Yes" then reload_doc(doc) end doc.deferred_reload = false @@ -86,30 +80,10 @@ local function doc_changes_visiblity(doc, visibility) if doc and visible[doc] ~= visibility and doc.abs_filename then visible[doc] = visibility if visibility then check_prompt_reload(doc) end - get_project_doc_watch(doc):watch(doc.abs_filename, visibility) + watch:watch(doc.abs_filename, visibility) end end -local on_check = dirwatch.check -function dirwatch:check(change_callback, ...) - on_check(self, function(dir) - for _, doc in ipairs(core.docs) do - if doc.abs_filename and (dir == common.dirname(doc.abs_filename) or dir == doc.abs_filename) then - local info = system.get_file_info(doc.filename or "") - if info and info.type == "file" and times[doc] ~= info.modified then - if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then - delayed_reload(doc, info.modified) - else - doc.deferred_reload = true - if doc == core.active_view.doc then check_prompt_reload(doc) end - end - end - end - end - change_callback(dir) - end, ...) -end - local core_set_active_view = core.set_active_view function core.set_active_view(view) core_set_active_view(view) @@ -125,9 +99,21 @@ end core.add_thread(function() while true do - -- because we already hook this function above; we only - -- need to check the file. - watch:check(function() end) + watch:check(function(file) + for i, doc in ipairs(core.docs) do + if doc.abs_filename == file then + local info = system.get_file_info(doc.filename or "") + if info and times[doc] ~= info.modified then + if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then + reload_doc(doc) + else + doc.deferred_reload = true + if doc == core.active_view.doc then check_prompt_reload(doc) end + end + end + end + end + end) coroutine.yield(0.05) end end) @@ -145,7 +131,7 @@ end Doc.save = function(self, ...) local res = save(self, ...) -- if starting with an unsaved document with a filename. - if not times[self] then get_project_doc_watch(self):watch(self.abs_filename, true) end + if not times[self] then watch:watch(self.abs_filename, true) end update_time(self) return res end diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 26afbb57..8b1fbd82 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9f22c66b..74d1fd11 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local common = require "core.common" diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 8b8567b3..4a12f73f 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local style = require "core.style" diff --git a/data/plugins/findfile.lua b/data/plugins/findfile.lua new file mode 100644 index 00000000..1b201362 --- /dev/null +++ b/data/plugins/findfile.lua @@ -0,0 +1,53 @@ +-- mod-version:4 + +local core = require "core" +local command = require "core.command" +local common = require "core.common" +local config = require "core.config" +local keymap = require "core.keymap" + +config.plugins.findfile = common.merge({ + file_limit = 20000, + max_search_time = 2.0 +}, config.plugins.findfile) + + +command.add(nil, { + ["core:find-file"] = function() + local files, complete = {}, false + local k = core.add_thread(function() + local start, total = system.get_time(), 0 + for i, project in ipairs(core.projects) do + for project, item in project:files() do + if complete or #files > config.plugins.findfile.file_limit then return end + table.insert(files, i == 1 and item.filename:sub(#project.path + 2) or common.home_encode(item.filename)) + local diff = system.get_time() - start + if diff > 2 / config.fps then + total = total + diff + if total > config.plugins.findfile.max_search_time then return end + coroutine.yield(0.1) + start = system.get_time() + end + end + end + end) + coroutine.resume(core.threads[k].cr) + core.command_view:enter("Open File From Project", { + submit = function(text, item) + text = item and item.text or text + core.root_view:open_doc(core.open_doc(common.home_expand(text))) + complete = true + end, + suggest = function(text) + return common.fuzzy_match_with_recents(files, core.visited_files, text) + end, + cancel = function() + complete = true + end + }) + end +}) + +keymap.add({ + [PLATFORM == "Mac OS X" and "cmd+p" or "ctrl+p"] = "core:find-file" +}) diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 2804fb7b..0d96ca3f 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" -- integer suffix combinations as a regex diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index 2bbc688e..1a066b9e 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" -- integer suffix combinations as a regex diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index a127dfac..6451adec 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index 6da1b45f..b553a228 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 1921f8d2..be3bc9f3 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" -- Regex pattern explanation: diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 55cc8adc..84a3c8ce 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index 544bff15..2d9a47f1 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" local style = require "core.style" local core = require "core" diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 8d67f57e..bbe13905 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" local function table_merge(a, b) diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index 125c260e..6042893d 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 5c4a9204..61989816 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local common = require "core.common" local command = require "core.command" local config = require "core.config" diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 0b9f0b76..7c93412a 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -1,4 +1,4 @@ --- mod-version:3 --priority:10 +-- mod-version:4 --priority:10 local core = require "core" local common = require "core.common" local DocView = require "core.docview" diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 9f3b8482..c95facfa 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index bcd36292..ae50b634 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" @@ -58,13 +58,14 @@ function ResultsView:begin_search(path, text, fn) core.add_thread(function() local i = 1 - for dir_name, file in core.get_project_files() do - if file.type == "file" and (not path or (dir_name .. "/" .. file.filename):find(path, 1, true) == 1) then - local truncated_path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) - find_all_matches_in_file(self.results, truncated_path .. file.filename, fn) + for k, project in ipairs(core.projects) do + for dir_name, file in project:files() do + if file.type == "file" and (not path or file.filename:find(path, 1, true) == 1) then + find_all_matches_in_file(self.results, file.filename, fn) + end + self.last_file_idx = i + i = i + 1 end - self.last_file_idx = i - i = i + 1 end self.searching = false self.brightness = 100 @@ -187,18 +188,10 @@ function ResultsView:draw() end local x, y = ox + style.padding.x, oy + style.padding.y - local files_number = core.project_files_number() - local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1) local text if self.searching then - if files_number then - text = string.format("Searching %.f%% (%d of %d files, %d matches) for %q...", - per * 100, self.last_file_idx, files_number, - #self.results, self.query) - else - text = string.format("Searching (%d files, %d matches) for %q...", - self.last_file_idx, #self.results, self.query) - end + text = string.format("Searching (%d files, %d matches) for %q...", + self.last_file_idx, #self.results, self.query) else text = string.format("Found %d matches for %q", #self.results, self.query) @@ -213,7 +206,7 @@ function ResultsView:draw() local color = common.lerp(style.dim, style.text, self.brightness / 100) renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, color) if self.searching then - renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text) + renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, style.text) end -- results @@ -227,7 +220,7 @@ function ResultsView:draw() renderer.draw_rect(x, y, w, h, style.line_highlight) end x = x + style.padding.x - local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col) + local text = string.format("%s at line %d (col %d): ", core.root_project():normalize_path(item.file), item.line, item.col) x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h) x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h) self.max_h_scroll = math.max(self.max_h_scroll, x) @@ -261,18 +254,6 @@ local function get_selected_text() end end - -local function normalize_path(path) - if not path then return nil end - path = common.normalize_path(path) - for i, project_dir in ipairs(core.project_directories) do - if common.path_belongs_to(path, project_dir.name) then - return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path) - end - end - return path -end - ---@class plugins.projectsearch local projectsearch = {} @@ -329,7 +310,7 @@ end command.add(nil, { ["project-search:find"] = function(path) - core.command_view:enter("Find Text In " .. (normalize_path(path) or "Project"), { + core.command_view:enter("Find Text In " .. (path or "Project"), { text = get_selected_text(), select_text = true, submit = function(text) @@ -339,7 +320,7 @@ command.add(nil, { end, ["project-search:find-regex"] = function(path) - core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), { + core.command_view:enter("Find Regex In " .. (path or "Project"), { submit = function(text) projectsearch.search_regex(text, path, true) end @@ -347,7 +328,7 @@ command.add(nil, { end, ["project-search:fuzzy-find"] = function(path) - core.command_view:enter("Fuzzy Find Text In " .. (normalize_path(path) or "Project"), { + core.command_view:enter("Fuzzy Find Text In " .. (path or "Project"), { text = get_selected_text(), select_text = true, submit = function(text) diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index 60f0cf1e..1e05cc71 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index e70b06f6..dbb23474 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 89a016b7..00f01395 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 5185fbf6..2d09e7bc 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index 9523066d..e131d967 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 2d812470..34fad584 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -10,6 +10,7 @@ local ContextMenu = require "core.contextmenu" local RootView = require "core.rootview" local CommandView = require "core.commandview" local DocView = require "core.docview" +local Dirwatch = require "core.dirwatch" config.plugins.treeview = common.merge({ -- Default treeview width @@ -17,7 +18,10 @@ config.plugins.treeview = common.merge({ highlight_focused_file = true, expand_dirs_to_focused_file = false, scroll_to_focused_file = false, - animate_scroll_to_focused_file = true + animate_scroll_to_focused_file = true, + show_hidden = false, + show_ignored = true, + visible = true }, config.plugins.treeview) local tooltip_offset = style.font:get_height() @@ -28,6 +32,7 @@ local tooltip_alpha_rate = 1 local function get_depth(filename) + if filename == "" then return 0 end local n = 1 for _ in filename:gmatch(PATHSEP) do n = n + 1 @@ -35,6 +40,7 @@ local function get_depth(filename) return n end + local function replace_alpha(color, alpha) local r, g, b = table.unpack(color) return { r, g, b, alpha } @@ -46,15 +52,19 @@ local TreeView = View:extend() function TreeView:new() TreeView.super.new(self) self.scrollable = true - self.visible = true + self.visible = config.plugins.treeview.visible self.init_size = true self.target_size = config.plugins.treeview.size + self.show_hidden = config.plugins.treeview.show_hidden + self.show_ignored = config.plugins.treeview.show_ignored self.cache = {} + self.expanded = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } self.last_scroll_y = 0 self.item_icon_width = 0 self.item_text_spacing = 0 + self.watches = { } end @@ -66,34 +76,57 @@ function TreeView:set_target_size(axis, value) end -function TreeView:get_cached(dir, item, dirname) - local dir_cache = self.cache[dirname] - if not dir_cache then - dir_cache = {} - self.cache[dirname] = dir_cache - end - -- to discriminate top directories from regular files or subdirectories - -- we add ':' at the end of the top directories' filename. it will be - -- used only to identify the entry into the cache. - local cache_name = item.filename .. (item.topdir and ":" or "") - local t = dir_cache[cache_name] - if not t or t.type ~= item.type then - t = {} - local basename = common.basename(item.filename) - if item.topdir then - t.filename = basename - t.expanded = true - t.depth = 0 - t.abs_filename = dirname + +function TreeView:get_cached(project, path) + local t = self.cache[path] + if not t then + if not self.watches[project] then self.watches[project] = Dirwatch.new() end + local truncated = path:sub(#project.path + 2) + local basename = common.basename(path) + local info + if self.show_ignored then + info = system.get_file_info(path) else - t.filename = item.filename - t.depth = get_depth(item.filename) - t.abs_filename = dirname .. PATHSEP .. item.filename + info = project:get_file_info(path) + end + if not info then return nil end + t = { + filename = basename, + depth = get_depth(truncated), + abs_filename = path, + project = project, + name = basename, + type = info.type, + project = project, + ignored = self.show_ignored and project:is_ignored(info, path) + } + if self.expanded[path] ~= nil then + t.expanded = self.expanded[path] + else + t.expanded = (info.type == "dir" and #truncated <= 1) + end + if t.expanded then self.watches[project]:watch(path) end + self.cache[path] = t + end + if t.expanded and t.type == "dir" and not t.files then + t.files = {} + for i, file in ipairs(system.list_dir(path)) do + local l = path .. PATHSEP .. file + local f + if self.show_ignored then + f = system.get_file_info(l) + else + f = project:get_file_info(l) + end + if f and f.type then + f.name = file + f.abs_filename = l + f.ignored = self.show_ignored and project:is_ignored(f, l) + table.insert(t.files, f) + end + self.cache[l] = nil end - t.name = basename - t.type = item.type - t.dir_name = dir.name -- points to top level "dir" item - dir_cache[cache_name] = t + table.sort(t.files, function(a, b) return system.path_compare(a.name, a.type, b.name, b.type) end) end return t end @@ -109,66 +142,29 @@ function TreeView:get_item_height() end -function TreeView:invalidate_cache(dirname) - for _, v in pairs(self.cache[dirname]) do - v.skip = nil - end -end - - -function TreeView:check_cache() - for i = 1, #core.project_directories do - local dir = core.project_directories[i] - -- 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) +function TreeView:get_items(project, path, x, y, w, h) + local dir = self:get_cached(project, path) + coroutine.yield(dir, x, y, w, h) + local count_lines = 1 + if dir and dir.files and dir.expanded then + for i, file in ipairs(dir.files) do + if self.show_hidden or not file.name:find("^%.") then + count_lines = count_lines + self:get_items(project, path .. PATHSEP .. file.name, x, y + count_lines * h, w, h) + end end - dir.is_dirty = false end + return count_lines end function TreeView:each_item() return coroutine.wrap(function() - self:check_cache() local count_lines = 0 local ox, oy = self:get_content_offset() - local y = oy + style.padding.y - local w = self.size.x local h = self:get_item_height() - - for k = 1, #core.project_directories do - local dir = core.project_directories[k] - 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 - if dir.files then -- if consumed max sys file descriptors this can be nil - while i <= #dir.files and dir_cached.expanded do - local item = dir.files[i] - local cached = self:get_cached(dir, item, dir.name) - - coroutine.yield(cached, ox, y, w, h) - count_lines = count_lines + 1 - y = y + h - i = i + 1 - - if not cached.expanded then - if cached.skip then - i = cached.skip - else - local depth = cached.depth - while i <= #dir.files do - if get_depth(dir.files[i].filename) <= depth then break end - i = i + 1 - end - cached.skip = i - end - end - end -- while files - end - end -- for directories + for k, project in ipairs(core.projects) do + count_lines = count_lines + self:get_items(project, project.path, ox, oy + style.padding.y + h * count_lines, self.size.x, h) + end self.count_lines = count_lines end) end @@ -372,6 +368,8 @@ function TreeView:get_item_icon(item, active, hovered) local color = style.text if active or hovered then color = style.accent + elseif item.ignored then + color = style.dim end return character, font, color end @@ -382,6 +380,8 @@ function TreeView:get_item_text(item, active, hovered) local color = style.text if active or hovered then color = style.accent + elseif item.ignored then + color = style.dim end return text, font, color end @@ -513,14 +513,13 @@ function TreeView:toggle_expand(toggle, item) else item.expanded = not item.expanded end - local hovered_dir = core.project_dir_by_name(item.dir_name) - if hovered_dir and hovered_dir.files_limit then - core.update_project_subdir(hovered_dir, item.depth == 0 and "" or item.filename, item.expanded) + self.expanded[item.abs_filename] = item.expanded + if self.watches[item.project] then + self.watches[item.project]:watch(item.abs_filename, item.expanded) end end end - function TreeView:open_doc(filename) core.root_view:open_doc(core.open_doc(filename)) end @@ -551,6 +550,26 @@ if config.plugins.toolbarview ~= false and toolbar_plugin then }) end + +local old_remove_project = core.remove_project +function core.remove_project(project, force) + local project = old_remove_project(project, force) + view.cache = {} + view.watches[project] = nil +end + +core.add_thread(function() + while true do + for k,v in pairs(view.watches) do + v:check(function(directory) + view.cache[directory] = nil + end) + end + coroutine.yield(0.01) + end +end) + + -- Add a context menu to the treeview local menu = ContextMenu() @@ -590,17 +609,12 @@ function core.on_quit_project() on_quit_project() end -local function is_project_folder(path) - for _,dir in pairs(core.project_directories) do - if dir.name == path then - return true - end - end - return false +local function is_project_folder(item) + return item.abs_filename == item.project.path end local function is_primary_project_folder(path) - return core.project_dir == path + return core.root_project().path == path end @@ -615,7 +629,7 @@ menu:register(function() return core.active_view:is(TreeView) and treeitem() end menu:register( function() local item = treeitem() - return core.active_view:is(TreeView) and item and not is_project_folder(item.abs_filename) + return core.active_view:is(TreeView) and item and not is_project_folder(item) end, { { text = "Rename", command = "treeview:rename" }, @@ -639,7 +653,7 @@ menu:register( local item = treeitem() return core.active_view:is(TreeView) and item and not is_primary_project_folder(item.abs_filename) - and is_project_folder(item.abs_filename) + and is_project_folder(item) end, { { text = "Remove directory", command = "treeview:remove-project-directory" }, @@ -654,6 +668,16 @@ command.add(nil, { view.visible = not view.visible end, + ["treeview:toggle-hidden"] = function() + view.show_hidden = not view.show_hidden + view.cache = {} + end, + + ["treeview:toggle-ignored"] = function() + view.show_ignored = not view.show_ignored + view.cache = {} + end, + ["treeview:toggle-focus"] = function() if not core.active_view:is(TreeView) then if core.active_view:is(CommandView) then @@ -781,9 +805,9 @@ command.add( ["treeview:delete"] = function(item) local filename = item.abs_filename local relfilename = item.filename - if item.dir_name ~= core.project_dir then + if item.project ~= core.root_project() then -- add secondary project dirs names to the file path to show - relfilename = common.basename(item.dir_name) .. PATHSEP .. relfilename + relfilename = common.basename(item.abs_filename) .. PATHSEP .. relfilename end local file_info = system.get_file_info(filename) local file_type = file_info.type == "dir" and "Directory" or "File" @@ -821,15 +845,12 @@ command.add( end, ["treeview:rename"] = function(item) - local old_filename = item.filename + local old_filename = core.normalize_to_project_dir(item.abs_filename) local old_abs_filename = item.abs_filename core.command_view:enter("Rename", { text = old_filename, submit = function(filename) - local abs_filename = filename - if not common.is_absolute_path(filename) then - abs_filename = item.dir_name .. PATHSEP .. filename - end + local abs_filename = item.project:absolute_path(filename) local res, err = os.rename(old_abs_filename, abs_filename) if res then -- successfully renamed for _, doc in ipairs(core.docs) do @@ -845,7 +866,7 @@ command.add( end end, suggest = function(text) - return common.path_suggest(text, item.dir_name) + return common.path_suggest(text, item.project and item.project.path) end }) end, @@ -861,10 +882,9 @@ command.add( end end core.command_view:enter("Filename", { - text = text, + text = not is_project_folder(item) and item.filename .. PATHSEP or "", submit = function(filename) - local doc_filename = item.dir_name .. PATHSEP .. filename - core.log(doc_filename) + local doc_filename = item.project:absolute_path(filename) local file = io.open(doc_filename, "a+") file:write("") file:close() @@ -872,7 +892,7 @@ command.add( core.log("Created %s", doc_filename) end, suggest = function(text) - return common.path_suggest(text, item.dir_name) + return common.path_suggest(text, item.project and item.project.path) end }) end, @@ -888,14 +908,14 @@ command.add( end end core.command_view:enter("Folder Name", { - text = text, + text = not is_project_folder(item) and item.filename .. PATHSEP or "", submit = function(filename) - local dir_path = item.dir_name .. PATHSEP .. filename + local dir_path = item.project:absolute_path(filename) common.mkdirp(dir_path) core.log("Created %s", dir_path) end, suggest = function(text) - return common.path_suggest(text, item.dir_name) + return common.path_suggest(text, item.project and item.project.path) end }) end, @@ -931,11 +951,11 @@ end command.add(function() local item = treeitem() return item - and not is_primary_project_folder(item.abs_filename) - and is_project_folder(item.abs_filename), item + and not is_primary_project_folder(item.abs_filename) + and is_project_folder(item), item end, { ["treeview:remove-project-directory"] = function(item) - core.remove_project_directory(item.dir_name) + core.remove_project(item.project) end, }) @@ -961,6 +981,8 @@ command.add( keymap.add { ["ctrl+\\"] = "treeview:toggle", + ["ctrl+h"] = "treeview:toggle-hidden", + ["ctrl+i"] = "treeview:toggle-ignored", ["up"] = "treeview:previous", ["down"] = "treeview:next", ["left"] = "treeview:collapse", diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index 8daa77e0..168e7552 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local common = require "core.common" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 3b3bd044..13782956 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local DocView = require "core.docview" @@ -166,7 +166,7 @@ local function load_node(node, t) active_view = view end if not view:is(DocView) then - view.scroll = v.scroll + view.scroll = v.scroll end end end @@ -185,10 +185,10 @@ end local function save_directories() - local project_dir = core.project_dir + local project_dir = core.root_project().path local dir_list = {} - for i = 2, #core.project_directories do - dir_list[#dir_list + 1] = common.relative_path(project_dir, core.project_directories[i].name) + for i = 2, #core.projects do + dir_list[#dir_list + 1] = common.relative_path(project_dir, core.projects[i].path) end return dir_list end @@ -196,19 +196,19 @@ end local function save_workspace() local root = get_unlocked_root(core.root_view.root_node) - local workspace_filename = get_workspace_filename(core.project_dir) + local workspace_filename = get_workspace_filename(core.root_project().path) local fp = io.open(workspace_filename, "w") if fp then local node_text = common.serialize(save_node(root)) local dir_text = common.serialize(save_directories()) - fp:write(string.format("return { path = %q, documents = %s, directories = %s }\n", core.project_dir, node_text, dir_text)) + fp:write(string.format("return { path = %q, documents = %s, directories = %s }\n", core.root_project().path, node_text, dir_text)) fp:close() end end local function load_workspace() - local workspace = consume_workspace_file(core.project_dir) + local workspace = consume_workspace_file(core.root_project().path) if workspace then local root = get_unlocked_root(core.root_view.root_node) local active_view = load_node(root, workspace.documents) @@ -216,7 +216,7 @@ local function load_workspace() core.set_active_view(active_view) end for i, dir_name in ipairs(workspace.directories) do - core.add_project_directory(system.absolute_path(dir_name)) + core.add_project(system.absolute_path(dir_name)) end end end @@ -228,17 +228,19 @@ function core.run(...) if #core.docs == 0 then core.try(load_workspace) - local on_quit_project = core.on_quit_project - function core.on_quit_project() + local set_project = core.set_project + function core.set_project(project) core.try(save_workspace) - on_quit_project() - end - - local on_enter_project = core.on_enter_project - function core.on_enter_project(new_dir) - on_enter_project(new_dir) + local project = set_project(project) core.try(load_workspace) + return project + end + local exit = core.exit + function core.exit(quit_fn, force) + if force then core.try(save_workspace) end + exit(quit_fn, force) end + end core.run = run |
