From e786951e8525e152a7babaaf3cbd762b56ca6b61 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 4 Jun 2021 17:20:44 +0200 Subject: [PATCH] Implement project based mode Currently works but needs some more work to be completed and to refine and cleanup a few things. With the new mode now: - it is possible to have files as top-level entries in a project - all directories open are on an equal basis - application can be started without a project directory - files from different top-level folders are identified in core:find-file using their top-level folder name as if it was a directory. While this works it is confusing as we are mixing real directories paths with top level directory names this latter being an application concept --- data/core/commands/core.lua | 66 ++++--- data/core/commands/doc.lua | 8 +- data/core/common.lua | 53 ++++-- data/core/doc/init.lua | 27 ++- data/core/docview.lua | 4 +- data/core/init.lua | 329 +++++++++++++++++++-------------- data/core/keymap.lua | 2 +- data/core/statusview.lua | 3 +- data/plugins/projectsearch.lua | 26 +-- data/plugins/treeview.lua | 42 +++-- data/plugins/workspace.lua | 240 ------------------------ 11 files changed, 331 insertions(+), 469 deletions(-) delete mode 100644 data/plugins/workspace.lua diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 859fb066..28d23cf2 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -70,15 +70,15 @@ command.add(nil, { return command.perform "core:open-file" end local files = {} - for dir, item in core.get_project_files() do + for dirpath, dirname, 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)) + table.insert(files, dirname .. PATHSEP .. item.filename) end end core.command_view:enter("Open File From Project", function(text, item) text = item and item.text or text - core.root_view:open_doc(core.open_doc(common.home_expand(text))) + local filename = core.resolve_project_filename(text) or common.home_expand(text) + core.root_view:open_doc(core.open_doc(filename)) end, function(text) return common.fuzzy_match_with_recents(files, core.visited_files, text) end) @@ -90,25 +90,36 @@ command.add(nil, { ["core:open-file"] = function() local view = core.active_view - if view.doc and view.doc.abs_filename then - local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") - if dirname then - dirname = core.normalize_to_project_dir(dirname) - local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP - core.command_view:set_text(text) - end + if view.doc and view.doc.filename then + core.command_view:set_text(common.home_encode(view.doc.filename)) end core.command_view:enter("Open File", function(text) - local filename = system.absolute_path(common.home_expand(text)) - core.root_view:open_doc(core.open_doc(filename)) + local filename = common.normalize_path(core.working_dir_absolute_path(common.home_expand(text))) + local info = system.get_file_info(filename) + if info and info.type == "dir" then + core.add_project_directory(filename) + core.set_recent_open("dir", filename) + core.reschedule_project_scan() + else + core.add_project_file(filename) + core.set_recent_open("file", filename) + core.root_view:open_doc(core.open_doc(filename)) + end end, function (text) return common.home_encode_list(common.path_suggest(common.home_expand(text))) end, nil, function(text) - local path_stat, err = system.get_file_info(common.home_expand(text)) + local filename = common.home_expand(text) + local info, err = system.get_file_info(filename) if err then - core.error("Cannot open file %q: %q", text, err) - elseif path_stat.type == 'dir' then - core.error("Cannot open %q, is a folder", text) + if err:find("No such file", 1, true) then + -- check if the containing directory exists + local dirname = common.dirname(filename) + local dir_info = dirname and system.get_file_info(dirname) + if not dirname or (dir_info and dir_info.type == 'dir') then + return true + end + end + core.error("Cannot open file %s: %s", text, err) else return true end @@ -137,10 +148,10 @@ command.add(nil, { end end, - ["core:change-project-folder"] = function() +--[[ ["core:change-project-folder"] = function() core.command_view:enter("Change Project Folder", function(text, item) text = system.absolute_path(common.home_expand(item and item.text or text)) - if text == core.project_dir then return end + if text == core.working_dir then return end local path_stat = system.get_file_info(text) if not path_stat or path_stat.type ~= 'dir' then core.error("Cannot open folder %q", text) @@ -160,11 +171,11 @@ command.add(nil, { end system.exec(string.format("%q %q", EXEFILE, text)) end, suggest_directory) - end, + end,]] ["core:add-directory"] = function() - core.command_view:enter("Add Directory", function(text) - text = common.home_expand(text) + core.command_view:enter("Add Directory", function(text, item) + text = common.home_expand(item and item.text or text) local path_stat, err = system.get_file_info(text) if not path_stat then core.error("cannot open %q: %s", text, err) @@ -181,13 +192,16 @@ command.add(nil, { ["core:remove-directory"] = function() local dir_list = {} - local n = #core.project_directories - for i = n, 2, -1 do - dir_list[n - i + 1] = core.project_directories[i].name + local n = #core.project_entries + for i = n, 1, -1 do + local entry = core.project_entries[i] + if entry.item.type == "dir" then + dir_list[n - i + 1] = entry.name + end end core.command_view:enter("Remove Directory", 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_entry(text) then core.error("No directory %q to be removed", text) end end, function(text) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 8165b426..bb3c9dab 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -41,7 +41,7 @@ end local function save(filename) - doc():save(filename and core.normalize_to_project_dir(filename)) + doc():save(filename and core.normalize_to_working_dir(filename)) local saved_filename = doc().filename core.on_doc_save(saved_filename) core.log("Saved \"%s\"", saved_filename) @@ -356,12 +356,14 @@ local commands = { end core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - doc():save(filename) + save(common.home_expand(filename)) core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) if filename ~= old_filename then os.remove(old_filename) end - end, common.path_suggest) + end, function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end) end, } diff --git a/data/core/common.lua b/data/core/common.lua index e8a989df..773b619f 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -108,9 +108,6 @@ function common.path_suggest(text) file = path .. file local info = system.get_file_info(file) if info then - if info.type == "dir" then - file = file .. PATHSEP - end if file:lower():find(text:lower(), nil, true) == 1 then table.insert(res, file) end @@ -196,6 +193,16 @@ function common.serialize(val) end +function common.path_join(...) + local n = select('#', ...) + local accu = select(1, ...) + for i = 2, n do + accu = accu .. PATHSEP .. select(i, ...) + end + return accu +end + + function common.basename(path) -- a path should never end by / or \ except if it is '/' (unix root) or -- 'X:\' (windows drive) @@ -203,6 +210,12 @@ function common.basename(path) end +-- can return nil if there is no directory part in the path +function common.dirname(path) + return path:match("(.+)[\\/][^\\/]+$") +end + + function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 @@ -230,18 +243,11 @@ function common.home_expand(text) end -function common.normalize_path(filename) - if filename and PATHSEP == '\\' then - filename = filename:gsub('[/\\]', '\\') - local drive, rem = filename:match('^([a-zA-Z])(:.*)') - return drive and drive:upper() .. rem or filename - end - return filename -end - - local function split_on_slash(s, sep_pattern) local t = {} + if s:match("^[/\\]") then + t[#t + 1] = "" + end for fragment in string.gmatch(s, "([^/\\]+)") do t[#t + 1] = fragment end @@ -249,8 +255,27 @@ local function split_on_slash(s, sep_pattern) end +function common.normalize_path(filename) + if PATHSEP == '\\' then + filename = filename:gsub('[/\\]', '\\') + local drive, rem = filename:match('^([a-zA-Z])(:.*)') + filename = drive and drive:upper() .. rem or filename + end + local parts = split_on_slash(filename, PATHSEP) + local accu = {} + for _, part in ipairs(parts) do + if part == '..' then + table.remove(accu) + elseif part ~= '.' then + table.insert(accu, part) + end + end + return table.concat(accu, PATHSEP) +end + + function common.path_belongs_to(filename, path) - return filename and string.find(filename, path .. PATHSEP, 1, true) == 1 + return string.find(filename, path .. PATHSEP, 1, true) == 1 end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 39fae9ca..ed561d23 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -36,10 +36,14 @@ local function splice(t, at, remove, insert) end -function Doc:new(filename) +function Doc:new(filename, new_file) + self.new_file = new_file self:reset() if filename then - self:load(filename) + self.filename = filename + if not new_file then + self:load(filename) + end end end @@ -65,16 +69,9 @@ function Doc:reset_syntax() end -function Doc:set_filename(filename) - self.filename = filename - self.abs_filename = system.absolute_path(filename) -end - - function Doc:load(filename) local fp = assert( io.open(filename, "rb") ) self:reset() - self:set_filename(filename) self.lines = {} for line in fp:lines() do if line:byte(-1) == 13 then @@ -92,16 +89,18 @@ end function Doc:save(filename) - filename = filename or assert(self.filename, "no filename set to default to") + if not filename then + assert(self.filename, "no filename set to default to") + filename = self.filename + end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) end fp:close() - if filename then - self:set_filename(filename) - end + self.filename = filename + self.new_file = false self:reset_syntax() self:clean() end @@ -113,7 +112,7 @@ end function Doc:is_dirty() - return self.clean_change_id ~= self:get_change_id() + return self.clean_change_id ~= self:get_change_id() or self.new_file end diff --git a/data/core/docview.lua b/data/core/docview.lua index 070ee0c4..6070a386 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -88,9 +88,9 @@ end function DocView:get_filename() - if self.doc.abs_filename then + if self.doc.filename then local post = self.doc:is_dirty() and "*" or "" - return common.home_encode(self.doc.abs_filename) .. post + return common.home_encode(self.doc.filename) .. post end return self:get_name() end diff --git a/data/core/init.lua b/data/core/init.lua index c16307c8..282f30d8 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -4,6 +4,7 @@ local common = require "core.common" local config = require "core.config" local style = require "core.style" local command +local project local keymap local RootView local StatusView @@ -20,45 +21,64 @@ local function load_session() if ok then return t.recents, t.window, t.window_mode end - return {} + return {}, {dir={}, file={}} end local function save_session() local fp = io.open(USERDIR .. "/session.lua", "w") if fp then - fp:write("return {recents=", common.serialize(core.recent_projects), - ", window=", common.serialize(table.pack(system.get_window_size())), - ", window_mode=", common.serialize(system.get_window_mode()), - "}\n") + fp:write(string.format( + "return { recent_projects= %s, recents_open= %s, window= %s, window_mode= %s}\n", + common.serialize(core.recent_projects), + common.serialize(core.recents_open), + common.serialize(table.pack(system.get_window_size())), + common.serialize(system.get_window_mode()) + )) fp:close() end end -local function normalize_path(s) - local drive, path = s:match("^([a-z]):([/\\].*)") - return drive and drive:upper() .. ":" .. path or s -end - - -local function update_recents_project(action, dir_path_abs) - local dirname = normalize_path(dir_path_abs) - if not dirname then return end - local recents = core.recent_projects +local function update_recents(recents, action, name) local n = #recents for i = 1, n do - if dirname == recents[i] then + if name == recents[i] then table.remove(recents, i) break end end if action == "add" then - table.insert(recents, 1, dirname) + table.insert(recents, 1, name) end end +function core.set_recent_project(name) + update_recents(core.recent_projects, "add", name) +end + + +function core.set_recent_open(type, filename) + update_recents(core.recents_open[type], "add", filename) +end + + +-- FIXME: remove or adapt +--[[ local function cleanup_recent_projects() + local recents = core.recent_projects + local i = 1 + while i <= #recents do + local info = system.get_file_info(recents[i]) + if not info or info.type ~= "dir" then + table.remove(recents, i) + else + i = i + 1 + end + end +end ]] + + function core.reschedule_project_scan() if core.project_scan_thread_id then core.threads[core.project_scan_thread_id].wake = 0 @@ -66,28 +86,14 @@ function core.reschedule_project_scan() 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 = normalize_path(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 -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() - update_recents_project("add", dir_path_abs) - core.on_enter_project(dir_path_abs) - end +function core.new_project_from_directory(dir_path_abs) + core.root_view:close_all_docviews() + core.project_entries = {} + core.add_project_directory(dir_path_abs) + system.chdir(dir_path_abs) + core.working_dir = dir_path_abs + core.set_recent_open("dir", dir_path_abs) + core.reschedule_project_scan() end @@ -168,23 +174,21 @@ local function project_scan_thread() -- 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, + while not core.project_files_limit and i <= #core.project_entries do + local dir = core.project_entries[i] + if dir.item.type == 'dir' then + 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." - ) + "usage.md at github.com/franko/lite-xl.") + end + dir.files = t + core.redraw = true end - dir.files = t - core.redraw = true - end - if dir.name == core.project_dir then - core.project_files = dir.files end i = i + 1 end @@ -196,8 +200,8 @@ end function core.scan_project_folder(dirname, filename) - for _, dir in ipairs(core.project_directories) do - if dir.name == dirname then + for _, dir in ipairs(core.project_entries) do + if dir.item.type == 'dir' and dir.name == dirname then for i, file in ipairs(dir.files) do local file = dir.files[i] if file.filename == filename then @@ -236,15 +240,15 @@ end local function project_files_iter(state) - local dir = core.project_directories[state.dir_index] + local dir = core.project_entries[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] + dir = core.project_entries[state.dir_index] end if not dir then return end - return dir.name, dir.files[state.file_index] + return dir.name, dir.item.filename, dir.files[state.file_index] end @@ -265,14 +269,36 @@ 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 + for i = 1, #core.project_entries do + n = n + #core.project_entries[i].files end return n end end +function core.resolve_project_filename(filename) + local dirname, basename = filename:match("(.-)[/\\](.+)") + for i = 1, #core.project_entries do + local dir = core.project_entries[i] + if dir.item.filename == dirname then + return dir.name .. PATHSEP .. basename + end + end +end + + +function core.as_project_filename(filename) + for i = 1, #core.project_entries do + local dir = core.project_entries[i] + if common.path_belongs_to(filename, dir.name) then + local dirpath = common.dirname(dir.name) + return filename:sub(#dirpath + 2) + end + end +end + + -- create a directory using mkdir but may need to create the parent -- directories as well. local function create_user_directory() @@ -293,7 +319,7 @@ local function create_user_directory() error("cannot create directory: \"" .. dirname_create .. "\"") end end - for _, modname in ipairs {'plugins', 'colors', 'fonts'} do + for _, modname in ipairs {'plugins', 'projects', 'colors', 'fonts'} do local subdirname = dirname_create .. '/' .. modname if not system.mkdir(subdirname) then error("cannot create directory: \"" .. subdirname .. "\"") @@ -377,26 +403,37 @@ function core.load_user_directory() end) end +function core.add_project_file(path) + path = common.normalize_path(path) + local entry = { + name = path, + item = {filename = common.basename(path), type = "file", topdir = true}, + files = {path} + } + table.insert(core.project_entries, entry) +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 = normalize_path(path) - table.insert(core.project_directories, { + path = common.normalize_path(path) + local entry = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, files = {} - }) + } + table.insert(core.project_entries, entry) end -function core.remove_project_directory(path) +function core.remove_project_entry(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] + for i = 2, #core.project_entries do + local dir = core.project_entries[i] if dir.name == path then - table.remove(core.project_directories, i) + table.remove(core.project_entries, i) return true end end @@ -416,9 +453,9 @@ local function reload_on_user_module_save() -- auto-realod style when user's module is saved by overriding Doc:Save() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") - function Doc:save(filename, abs_filename) - doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename then + function Doc:save(filename) + doc_save(self, filename) + if self.filename == user_filename then core.reload_module("core.style") core.load_user_directory() end @@ -429,6 +466,7 @@ end function core.init() command = require "core.command" keymap = require "core.keymap" + project = require "core.project" RootView = require "core.rootview" StatusView = require "core.statusview" TitleView = require "core.titleview" @@ -444,62 +482,52 @@ function core.init() end do - local recent_projects, window_position, window_mode = load_session() + -- FIXME: change the name for "recents_open" + local window_position, window_mode + core.recent_projects, core.recents_open, window_position, window_mode = load_session() if window_mode == "normal" then system.set_window_size(table.unpack(window_position)) elseif window_mode == "maximized" then system.set_window_mode("maximized") end - core.recent_projects = recent_projects end + -- cleanup_recent_projects() - local project_dir = core.recent_projects[1] or "." - local project_dir_explicit = false - local files = {} - local delayed_error - 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 == "file" then - local file_abs = system.absolute_path(arg_filename) - if file_abs then - table.insert(files, file_abs) - project_dir = file_abs:match("^(.+)[/\\].+$") - end - elseif info.type == "dir" then - project_dir = arg_filename - project_dir_explicit = true - else - delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) - end - end - - core.frame_start = 0 - core.clip_rect_stack = {{ 0,0,0,0 }} core.log_items = {} core.docs = {} - core.window_mode = "normal" - core.threads = setmetatable({}, { __mode = "k" }) - core.blink_start = system.get_time() - core.blink_timer = core.blink_start + core.project_entries = {} + core.project_name = "" - local project_dir_abs = system.absolute_path(project_dir) - local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) - if set_project_ok then - if project_dir_explicit then - update_recents_project("add", project_dir_abs) - end - else - if not project_dir_explicit then - update_recents_project("remove", project_dir) - end - project_dir_abs = system.absolute_path(".") - if not core.set_project_dir(project_dir_abs) then - system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") - os.exit(1) + local init_files = {} + local delayed_errors = {} + for i = 2, #ARGS do + local filename = strip_trailing_slash(ARGS[i]) + local info = system.get_file_info(filename) + if info and info.type == "file" then + filename = system.absolute_path(filename) + if filename then + core.add_project_file(filename) + table.insert(init_files, filename) + end + elseif info and info.type == "dir" then + filename = system.absolute_path(filename) + if filename then + core.add_project_directory(filename) + -- FIXME + -- update_recents(core.recents_open.dir, "add", filename) + end + else + local error_msg = string.format("error: invalid file or directory \"%s\"", ARGS[i]) + table.insert(delayed_errors, error_msg) end end + core.threads = setmetatable({}, { __mode = "k" }) + core.frame_start = 0 + core.clip_rect_stack = {{ 0,0,0,0 }} + core.window_mode = "normal" + core.blink_start = system.get_time() + core.blink_timer = core.blink_start core.redraw = true core.visited_files = {} core.restart_request = false @@ -522,21 +550,29 @@ function core.init() core.project_scan_thread_id = core.add_thread(project_scan_thread) command.add_defaults() + + for _, project_entry in ipairs(core.project_entries) do + if project_entry.item.type == "dir" then + core.log_quiet("Setting working directory to \"%s\"", project_entry.name) + system.chdir(project_entry.name) + core.working_dir = project_entry.name + break + end + end + if not core.working_dir then + core.working_dir = system.absolute_path(".") + end + local got_user_error = not core.load_user_directory() local plugins_success, plugins_refuse_list = core.load_plugins() - - do - local pdir, pname = project_dir_abs:match("(.*)[/\\\\](.*)") - core.log("Opening project %q from directory %s", pname, pdir) - end local got_project_error = not core.load_project_module() - for _, filename in ipairs(files) do + for _, filename in ipairs(init_files) do core.root_view:open_doc(core.open_doc(filename)) end - if delayed_error then - core.error(delayed_error) + for _, error_msg in ipairs(delayed_errors) do + core.error(error_msg) end if not plugins_success or got_user_error or got_project_error then @@ -620,12 +656,16 @@ function core.temp_filename(ext) .. 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 + +function core.on_quit_project() + local filename = USERDIR .. PATHSEP .. "workspace.lua" + core.try(project.save_workspace, filename) +end + + +function core.on_enter_project(new_dir) + -- FIXME: check the logic + -- core.try(project.load_workspace, USERDIR .. PATHSEP .. "workspace.lua") end @@ -755,6 +795,7 @@ end function core.set_visited(filename) + filename = core.as_project_filename(filename) or common.home_encode(filename) for i = 1, #core.visited_files do if core.visited_files[i] == filename then table.remove(core.visited_files, i) @@ -808,29 +849,41 @@ 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) +-- 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.working_dir so relative filename are considered +-- to be in core.working_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.working_dir_absolute_path(filename) + if filename:match('^%a:\\') or filename:find('/', 1, true) == 1 then + return filename + else + return core.working_dir .. PATHSEP .. filename end - return filename +end + +function core.normalize_to_working_dir(filename) + filename = common.normalize_path(filename) + return core.working_dir_absolute_path(filename) end function core.open_doc(filename) + local new_file = not filename or not system.get_file_info(filename) if filename then + -- normalize filename and set absolute filename then -- try to find existing doc for filename - local abs_filename = system.absolute_path(filename) + filename = core.normalize_to_working_dir(filename) for _, doc in ipairs(core.docs) do - if doc.abs_filename and abs_filename == doc.abs_filename then + if doc.filename and filename == doc.filename then return doc end end end -- no existing doc for filename; create new - filename = core.normalize_to_project_dir(filename) - local doc = Doc(filename) + local doc = Doc(filename, new_file) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) return doc diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 84bcd770..b7f1a14b 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -105,7 +105,7 @@ keymap.add_direct { ["ctrl+p"] = "core:find-file", ["ctrl+o"] = "core:open-file", ["ctrl+n"] = "core:new-doc", - ["ctrl+shift+c"] = "core:change-project-folder", + ["ctrl+shift+c"] = "project:open-directory", ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 58421c31..168ff6d9 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -138,7 +138,8 @@ function StatusView:get_items() style.icon_font, "g", style.font, style.dim, self.separator2, #core.docs, style.text, " / ", - #core.project_files, " files" + "(NYI) files" + --- #core.project_files, " files" } end diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 45399ed0..8f85661e 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -23,8 +23,8 @@ function ResultsView:get_name() end -local function find_all_matches_in_file(t, filename, fn) - local fp = io.open(filename) +local function find_all_matches_in_file(t, dirpath, dirname, filename, fn) + local fp = io.open(dirpath .. PATHSEP .. filename) if not fp then return t end local n = 1 for line in fp:lines() do @@ -33,7 +33,7 @@ local function find_all_matches_in_file(t, filename, fn) -- Insert maximum 256 characters. If we insert more, for compiled files, which can have very long lines -- things tend to get sluggish. If our line is longer than 80 characters, begin to truncate the thing. local start_index = math.max(s - 80, 1) - table.insert(t, { file = filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s }) + table.insert(t, { file = dirname .. PATHSEP .. filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s }) core.redraw = true end if n % 100 == 0 then coroutine.yield() end @@ -54,10 +54,9 @@ function ResultsView:begin_search(text, fn) core.add_thread(function() local i = 1 - for dir_name, file in core.get_project_files() do - if file.type == "file" then - local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) - find_all_matches_in_file(self.results, path .. file.filename, fn) + for dirpath, dirname, item in core.get_project_files() do + if item.type == "file" then + find_all_matches_in_file(self.results, dirpath, dirname, item.filename, fn) end self.last_file_idx = i i = i + 1 @@ -102,7 +101,8 @@ function ResultsView:open_selected_result() return end core.try(function() - local dv = core.root_view:open_doc(core.open_doc(res.file)) + local filename = core.resolve_project_filename(res.file) + local dv = core.root_view:open_doc(core.open_doc(filename)) core.root_view.root_node:update_layout() dv.doc:set_selection(res.line, res.col) dv:scroll_to_line(res.line, false, true) @@ -241,7 +241,7 @@ command.add(nil, { core.command_view:enter("Find Regex In Project", function(text) local re = regex.compile(text, "i") begin_search(text, function(line_text) - return regex.cmatch(re, line_text) + return regex.cmatch(re, line_text) end) end) end, @@ -276,22 +276,22 @@ command.add(ResultsView, { ["project-search:refresh"] = function() core.active_view:refresh() end, - + ["project-search:move-to-previous-page"] = function() local view = core.active_view view.scroll.to.y = view.scroll.to.y - view.size.y end, - + ["project-search:move-to-next-page"] = function() local view = core.active_view view.scroll.to.y = view.scroll.to.y + view.size.y end, - + ["project-search:move-to-start-of-doc"] = function() local view = core.active_view view.scroll.to.y = 0 end, - + ["project-search:move-to-end-of-doc"] = function() local view = core.active_view view.scroll.to.y = view:get_scrollable_size() diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 8615343a..47ab406e 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -64,19 +64,27 @@ function TreeView:get_cached(item, dirname) local t = dir_cache[cache_name] if not t then t = {} - local basename = common.basename(item.filename) - if item.topdir then - t.filename = basename - t.expanded = true + if item.type == 'file' and item.topdir then + t.filename = item.filename t.depth = 0 t.abs_filename = dirname + t.name = item.filename + t.type = item.type else - t.filename = item.filename - t.depth = get_depth(item.filename) - t.abs_filename = dirname .. PATHSEP .. item.filename + local basename = common.basename(item.filename) + if item.topdir then + t.filename = basename + t.expanded = true + t.depth = 0 + t.abs_filename = dirname + else + t.filename = item.filename + t.depth = get_depth(item.filename) + t.abs_filename = dirname .. PATHSEP .. item.filename + end + t.name = basename + t.type = item.type end - t.name = basename - t.type = item.type dir_cache[cache_name] = t end return t @@ -102,8 +110,8 @@ 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] + for i = 1, #core.project_entries do + local dir = core.project_entries[i] local last_files = self.last[dir.name] if not last_files then self.last[dir.name] = dir.files @@ -126,8 +134,8 @@ function TreeView:each_item() local w = self.size.x local h = self:get_item_height() - for k = 1, #core.project_directories do - local dir = core.project_directories[k] + for k = 1, #core.project_entries do + local dir = core.project_entries[k] local dir_cached = self:get_cached(dir.item, dir.name) coroutine.yield(dir_cached, ox, y, w, h) count_lines = count_lines + 1 @@ -173,13 +181,13 @@ end function TreeView:on_mouse_moved(px, py, ...) TreeView.super.on_mouse_moved(self, px, py, ...) if self.dragging_scrollbar then return end - + local item_changed, tooltip_changed for item, x,y,w,h in self:each_item() do if px > x and py > y and px <= x + w and py <= y + h then item_changed = true self.hovered_item = item - + x,y,w,h = self:get_text_bounding_box(item, x,y,w,h) if px > x and py > y and px <= x + w and py <= y + h then tooltip_changed = true @@ -231,7 +239,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) end else core.try(function() - local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) + local doc_filename = self.hovered_item.abs_filename core.root_view:open_doc(core.open_doc(doc_filename)) end) end @@ -247,7 +255,7 @@ function TreeView:update() else self:move_towards(self.size, "x", dest) end - + local duration = system.get_time() - self.tooltip.begin if self.hovered_item and self.tooltip.x and duration > tooltip_delay then self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate) diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua deleted file mode 100644 index 77efd7c9..00000000 --- a/data/plugins/workspace.lua +++ /dev/null @@ -1,240 +0,0 @@ --- mod-version:1 -- lite-xl 1.16 -local core = require "core" -local common = require "core.common" -local DocView = require "core.docview" -local LogView = require "core.logview" - - -local function workspace_files_for(project_dir) - local basename = common.basename(project_dir) - local workspace_dir = USERDIR .. PATHSEP .. "ws" - local info_wsdir = system.get_file_info(workspace_dir) - if not info_wsdir then - local ok, err = system.mkdir(workspace_dir) - if not ok then - error("cannot create workspace directory: %s", err) - end - end - return coroutine.wrap(function() - local files = system.list_dir(workspace_dir) or {} - local n = #basename - for _, file in ipairs(files) do - if file:sub(1, n) == basename then - local id = tonumber(file:sub(n + 1):match("^-(%d+)$")) - if id then - coroutine.yield(workspace_dir .. PATHSEP .. file, id) - end - end - end - end) -end - - -local function consume_workspace_file(project_dir) - for filename, id in workspace_files_for(project_dir) do - local load_f = loadfile(filename) - local workspace = load_f and load_f() - if workspace and workspace.path == project_dir then - os.remove(filename) - return workspace - end - end -end - - -local function get_workspace_filename(project_dir) - local id_list = {} - for filename, id in workspace_files_for(project_dir) do - id_list[id] = true - end - local id = 1 - while id_list[id] do - id = id + 1 - end - local basename = common.basename(project_dir) - return USERDIR .. PATHSEP .. "ws" .. PATHSEP .. basename .. "-" .. tostring(id) -end - - -local function has_no_locked_children(node) - if node.locked then return false end - if node.type == "leaf" then return true end - return has_no_locked_children(node.a) and has_no_locked_children(node.b) -end - - -local function get_unlocked_root(node) - if node.type == "leaf" then - return not node.locked and node - end - if has_no_locked_children(node) then - return node - end - return get_unlocked_root(node.a) or get_unlocked_root(node.b) -end - - -local function save_view(view) - local mt = getmetatable(view) - if mt == DocView then - return { - type = "doc", - active = (core.active_view == view), - filename = view.doc.filename, - selection = { view.doc:get_selection() }, - scroll = { x = view.scroll.to.x, y = view.scroll.to.y }, - text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge) - } - end - if mt == LogView then return end - for name, mod in pairs(package.loaded) do - if mod == mt then - return { - type = "view", - active = (core.active_view == view), - module = name - } - end - end -end - - -local function load_view(t) - if t.type == "doc" then - local dv - if not t.filename then - -- document not associated to a file - dv = DocView(core.open_doc()) - if t.text then dv.doc:insert(1, 1, t.text) end - else - -- we have a filename, try to read the file - local ok, doc = pcall(core.open_doc, t.filename) - if ok then - dv = DocView(doc) - end - end - -- doc view "dv" can be nil here if the filename associated to the document - -- cannot be read. - if dv and dv.doc then - dv.doc:set_selection(table.unpack(t.selection)) - dv.last_line, dv.last_col = dv.doc:get_selection() - dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x - dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y - end - return dv - end - return require(t.module)() -end - - -local function save_node(node) - local res = {} - res.type = node.type - if node.type == "leaf" then - res.views = {} - for _, view in ipairs(node.views) do - local t = save_view(view) - if t then - table.insert(res.views, t) - if node.active_view == view then - res.active_view = #res.views - end - end - end - else - res.divider = node.divider - res.a = save_node(node.a) - res.b = save_node(node.b) - end - return res -end - - -local function load_node(node, t) - if t.type == "leaf" then - local res - local active_view - for i, v in ipairs(t.views) do - local view = load_view(v) - if view then - if v.active then res = view end - node:add_view(view) - if t.active_view == i then - active_view = view - end - end - end - if active_view then - node:set_active_view(active_view) - end - return res - else - node:split(t.type == "hsplit" and "right" or "down") - node.divider = t.divider - local res1 = load_node(node.a, t.a) - local res2 = load_node(node.b, t.b) - return res1 or res2 - end -end - - -local function save_directories() - local project_dir = core.project_dir - 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) - end - return dir_list -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 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:close() - end -end - - -local function load_workspace() - local workspace = consume_workspace_file(core.project_dir) - if workspace then - local root = get_unlocked_root(core.root_view.root_node) - local active_view = load_node(root, workspace.documents) - if active_view then - 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)) - end - end -end - - -local run = core.run - -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() - 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) - core.try(load_workspace) - end - end - - core.run = run - return core.run(...) -end