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
This commit is contained in:
Francesco Abbate 2021-06-04 17:20:44 +02:00
parent c5acd030a1
commit e786951e85
11 changed files with 331 additions and 469 deletions

View File

@ -70,15 +70,15 @@ command.add(nil, {
return command.perform "core:open-file" return command.perform "core:open-file"
end end
local files = {} 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 if item.type == "file" then
local path = (dir == core.project_dir and "" or dir .. PATHSEP) table.insert(files, dirname .. PATHSEP .. item.filename)
table.insert(files, common.home_encode(path .. item.filename))
end end
end end
core.command_view:enter("Open File From Project", function(text, item) core.command_view:enter("Open File From Project", function(text, item)
text = item and item.text or text 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) end, function(text)
return common.fuzzy_match_with_recents(files, core.visited_files, text) return common.fuzzy_match_with_recents(files, core.visited_files, text)
end) end)
@ -90,25 +90,36 @@ command.add(nil, {
["core:open-file"] = function() ["core:open-file"] = function()
local view = core.active_view local view = core.active_view
if view.doc and view.doc.abs_filename then if view.doc and view.doc.filename then
local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") core.command_view:set_text(common.home_encode(view.doc.filename))
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
end end
core.command_view:enter("Open File", function(text) core.command_view:enter("Open File", function(text)
local filename = system.absolute_path(common.home_expand(text)) local filename = common.normalize_path(core.working_dir_absolute_path(common.home_expand(text)))
core.root_view:open_doc(core.open_doc(filename)) 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) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text))) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, nil, function(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 if err then
core.error("Cannot open file %q: %q", text, err) if err:find("No such file", 1, true) then
elseif path_stat.type == 'dir' then -- check if the containing directory exists
core.error("Cannot open %q, is a folder", text) 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 else
return true return true
end end
@ -137,10 +148,10 @@ command.add(nil, {
end end
end, end,
["core:change-project-folder"] = function() --[[ ["core:change-project-folder"] = function()
core.command_view:enter("Change Project Folder", function(text, item) core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text)) 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) local path_stat = system.get_file_info(text)
if not path_stat or path_stat.type ~= 'dir' then if not path_stat or path_stat.type ~= 'dir' then
core.error("Cannot open folder %q", text) core.error("Cannot open folder %q", text)
@ -160,11 +171,11 @@ command.add(nil, {
end end
system.exec(string.format("%q %q", EXEFILE, text)) system.exec(string.format("%q %q", EXEFILE, text))
end, suggest_directory) end, suggest_directory)
end, end,]]
["core:add-directory"] = function() ["core:add-directory"] = function()
core.command_view:enter("Add Directory", function(text) core.command_view:enter("Add Directory", function(text, item)
text = common.home_expand(text) text = common.home_expand(item and item.text or text)
local path_stat, err = system.get_file_info(text) local path_stat, err = system.get_file_info(text)
if not path_stat then if not path_stat then
core.error("cannot open %q: %s", text, err) core.error("cannot open %q: %s", text, err)
@ -181,13 +192,16 @@ command.add(nil, {
["core:remove-directory"] = function() ["core:remove-directory"] = function()
local dir_list = {} local dir_list = {}
local n = #core.project_directories local n = #core.project_entries
for i = n, 2, -1 do for i = n, 1, -1 do
dir_list[n - i + 1] = core.project_directories[i].name local entry = core.project_entries[i]
if entry.item.type == "dir" then
dir_list[n - i + 1] = entry.name
end
end end
core.command_view:enter("Remove Directory", function(text, item) core.command_view:enter("Remove Directory", function(text, item)
text = common.home_expand(item and item.text or text) 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) core.error("No directory %q to be removed", text)
end end
end, function(text) end, function(text)

View File

@ -41,7 +41,7 @@ end
local function save(filename) 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 local saved_filename = doc().filename
core.on_doc_save(saved_filename) core.on_doc_save(saved_filename)
core.log("Saved \"%s\"", saved_filename) core.log("Saved \"%s\"", saved_filename)
@ -356,12 +356,14 @@ local commands = {
end end
core.command_view:set_text(old_filename) core.command_view:set_text(old_filename)
core.command_view:enter("Rename", function(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) core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
if filename ~= old_filename then if filename ~= old_filename then
os.remove(old_filename) os.remove(old_filename)
end end
end, common.path_suggest) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end)
end, end,
} }

View File

@ -108,9 +108,6 @@ function common.path_suggest(text)
file = path .. file file = path .. file
local info = system.get_file_info(file) local info = system.get_file_info(file)
if info then if info then
if info.type == "dir" then
file = file .. PATHSEP
end
if file:lower():find(text:lower(), nil, true) == 1 then if file:lower():find(text:lower(), nil, true) == 1 then
table.insert(res, file) table.insert(res, file)
end end
@ -196,6 +193,16 @@ function common.serialize(val)
end 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) function common.basename(path)
-- a path should never end by / or \ except if it is '/' (unix root) or -- a path should never end by / or \ except if it is '/' (unix root) or
-- 'X:\' (windows drive) -- 'X:\' (windows drive)
@ -203,6 +210,12 @@ function common.basename(path)
end 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) function common.home_encode(text)
if HOME and string.find(text, HOME, 1, true) == 1 then if HOME and string.find(text, HOME, 1, true) == 1 then
local dir_pos = #HOME + 1 local dir_pos = #HOME + 1
@ -230,18 +243,11 @@ function common.home_expand(text)
end 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 function split_on_slash(s, sep_pattern)
local t = {} local t = {}
if s:match("^[/\\]") then
t[#t + 1] = ""
end
for fragment in string.gmatch(s, "([^/\\]+)") do for fragment in string.gmatch(s, "([^/\\]+)") do
t[#t + 1] = fragment t[#t + 1] = fragment
end end
@ -249,8 +255,27 @@ local function split_on_slash(s, sep_pattern)
end 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) 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 end

View File

@ -36,10 +36,14 @@ local function splice(t, at, remove, insert)
end end
function Doc:new(filename) function Doc:new(filename, new_file)
self.new_file = new_file
self:reset() self:reset()
if filename then if filename then
self:load(filename) self.filename = filename
if not new_file then
self:load(filename)
end
end end
end end
@ -65,16 +69,9 @@ function Doc:reset_syntax()
end end
function Doc:set_filename(filename)
self.filename = filename
self.abs_filename = system.absolute_path(filename)
end
function Doc:load(filename) function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert( io.open(filename, "rb") )
self:reset() self:reset()
self:set_filename(filename)
self.lines = {} self.lines = {}
for line in fp:lines() do for line in fp:lines() do
if line:byte(-1) == 13 then if line:byte(-1) == 13 then
@ -92,16 +89,18 @@ end
function Doc:save(filename) 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") ) local fp = assert( io.open(filename, "wb") )
for _, line in ipairs(self.lines) do for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line) fp:write(line)
end end
fp:close() fp:close()
if filename then self.filename = filename
self:set_filename(filename) self.new_file = false
end
self:reset_syntax() self:reset_syntax()
self:clean() self:clean()
end end
@ -113,7 +112,7 @@ end
function Doc:is_dirty() 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 end

View File

@ -88,9 +88,9 @@ end
function DocView:get_filename() function DocView:get_filename()
if self.doc.abs_filename then if self.doc.filename then
local post = self.doc:is_dirty() and "*" or "" 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 end
return self:get_name() return self:get_name()
end end

View File

@ -4,6 +4,7 @@ local common = require "core.common"
local config = require "core.config" local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local command local command
local project
local keymap local keymap
local RootView local RootView
local StatusView local StatusView
@ -20,45 +21,64 @@ local function load_session()
if ok then if ok then
return t.recents, t.window, t.window_mode return t.recents, t.window, t.window_mode
end end
return {} return {}, {dir={}, file={}}
end end
local function save_session() local function save_session()
local fp = io.open(USERDIR .. "/session.lua", "w") local fp = io.open(USERDIR .. "/session.lua", "w")
if fp then if fp then
fp:write("return {recents=", common.serialize(core.recent_projects), fp:write(string.format(
", window=", common.serialize(table.pack(system.get_window_size())), "return { recent_projects= %s, recents_open= %s, window= %s, window_mode= %s}\n",
", window_mode=", common.serialize(system.get_window_mode()), common.serialize(core.recent_projects),
"}\n") common.serialize(core.recents_open),
common.serialize(table.pack(system.get_window_size())),
common.serialize(system.get_window_mode())
))
fp:close() fp:close()
end end
end end
local function normalize_path(s) local function update_recents(recents, action, name)
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 n = #recents local n = #recents
for i = 1, n do for i = 1, n do
if dirname == recents[i] then if name == recents[i] then
table.remove(recents, i) table.remove(recents, i)
break break
end end
end end
if action == "add" then if action == "add" then
table.insert(recents, 1, dirname) table.insert(recents, 1, name)
end end
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() function core.reschedule_project_scan()
if core.project_scan_thread_id then if core.project_scan_thread_id then
core.threads[core.project_scan_thread_id].wake = 0 core.threads[core.project_scan_thread_id].wake = 0
@ -66,28 +86,14 @@ function core.reschedule_project_scan()
end end
function core.set_project_dir(new_dir, change_project_fn) function core.new_project_from_directory(dir_path_abs)
local chdir_ok = pcall(system.chdir, new_dir) core.root_view:close_all_docviews()
if chdir_ok then core.project_entries = {}
if change_project_fn then change_project_fn() end core.add_project_directory(dir_path_abs)
core.project_dir = normalize_path(new_dir) system.chdir(dir_path_abs)
core.project_directories = {} core.working_dir = dir_path_abs
core.add_project_directory(new_dir) core.set_recent_open("dir", dir_path_abs)
core.project_files = {} core.reschedule_project_scan()
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
end end
@ -168,23 +174,21 @@ local function project_scan_thread()
-- get project files and replace previous table if the new table is -- get project files and replace previous table if the new table is
-- different -- different
local i = 1 local i = 1
while not core.project_files_limit and i <= #core.project_directories do while not core.project_files_limit and i <= #core.project_entries do
local dir = core.project_directories[i] local dir = core.project_entries[i]
local t, entries_count = get_directory_files(dir.name, "", {}, true) if dir.item.type == 'dir' then
if diff_files(dir.files, t) then local t, entries_count = get_directory_files(dir.name, "", {}, true)
if entries_count > config.max_project_files then if diff_files(dir.files, t) then
core.project_files_limit = true if entries_count > config.max_project_files then
core.status_view:show_message("!", style.accent, core.project_files_limit = true
core.status_view:show_message("!", style.accent,
"Too many files in project directory: stopped reading at ".. "Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see ".. 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 end
dir.files = t
core.redraw = true
end
if dir.name == core.project_dir then
core.project_files = dir.files
end end
i = i + 1 i = i + 1
end end
@ -196,8 +200,8 @@ end
function core.scan_project_folder(dirname, filename) function core.scan_project_folder(dirname, filename)
for _, dir in ipairs(core.project_directories) do for _, dir in ipairs(core.project_entries) do
if dir.name == dirname then if dir.item.type == 'dir' and dir.name == dirname then
for i, file in ipairs(dir.files) do for i, file in ipairs(dir.files) do
local file = dir.files[i] local file = dir.files[i]
if file.filename == filename then if file.filename == filename then
@ -236,15 +240,15 @@ end
local function project_files_iter(state) 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 state.file_index = state.file_index + 1
while dir and state.file_index > #dir.files do while dir and state.file_index > #dir.files do
state.dir_index = state.dir_index + 1 state.dir_index = state.dir_index + 1
state.file_index = 1 state.file_index = 1
dir = core.project_directories[state.dir_index] dir = core.project_entries[state.dir_index]
end end
if not dir then return 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 end
@ -265,14 +269,36 @@ end
function core.project_files_number() function core.project_files_number()
if not core.project_files_limit then if not core.project_files_limit then
local n = 0 local n = 0
for i = 1, #core.project_directories do for i = 1, #core.project_entries do
n = n + #core.project_directories[i].files n = n + #core.project_entries[i].files
end end
return n return n
end end
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 -- create a directory using mkdir but may need to create the parent
-- directories as well. -- directories as well.
local function create_user_directory() local function create_user_directory()
@ -293,7 +319,7 @@ local function create_user_directory()
error("cannot create directory: \"" .. dirname_create .. "\"") error("cannot create directory: \"" .. dirname_create .. "\"")
end end
end end
for _, modname in ipairs {'plugins', 'colors', 'fonts'} do for _, modname in ipairs {'plugins', 'projects', 'colors', 'fonts'} do
local subdirname = dirname_create .. '/' .. modname local subdirname = dirname_create .. '/' .. modname
if not system.mkdir(subdirname) then if not system.mkdir(subdirname) then
error("cannot create directory: \"" .. subdirname .. "\"") error("cannot create directory: \"" .. subdirname .. "\"")
@ -377,26 +403,37 @@ function core.load_user_directory()
end) end)
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) function core.add_project_directory(path)
-- top directories has a file-like "item" but the item.filename -- top directories has a file-like "item" but the item.filename
-- will be simply the name of the directory, without its path. -- will be simply the name of the directory, without its path.
-- The field item.topdir will identify it as a top level directory. -- The field item.topdir will identify it as a top level directory.
path = normalize_path(path) path = common.normalize_path(path)
table.insert(core.project_directories, { local entry = {
name = path, name = path,
item = {filename = common.basename(path), type = "dir", topdir = true}, item = {filename = common.basename(path), type = "dir", topdir = true},
files = {} files = {}
}) }
table.insert(core.project_entries, entry)
end end
function core.remove_project_directory(path) function core.remove_project_entry(path)
-- skip the fist directory because it is the project's directory -- skip the fist directory because it is the project's directory
for i = 2, #core.project_directories do for i = 2, #core.project_entries do
local dir = core.project_directories[i] local dir = core.project_entries[i]
if dir.name == path then if dir.name == path then
table.remove(core.project_directories, i) table.remove(core.project_entries, i)
return true return true
end end
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() -- auto-realod style when user's module is saved by overriding Doc:Save()
local doc_save = Doc.save local doc_save = Doc.save
local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua")
function Doc:save(filename, abs_filename) function Doc:save(filename)
doc_save(self, filename, abs_filename) doc_save(self, filename)
if self.abs_filename == user_filename then if self.filename == user_filename then
core.reload_module("core.style") core.reload_module("core.style")
core.load_user_directory() core.load_user_directory()
end end
@ -429,6 +466,7 @@ end
function core.init() function core.init()
command = require "core.command" command = require "core.command"
keymap = require "core.keymap" keymap = require "core.keymap"
project = require "core.project"
RootView = require "core.rootview" RootView = require "core.rootview"
StatusView = require "core.statusview" StatusView = require "core.statusview"
TitleView = require "core.titleview" TitleView = require "core.titleview"
@ -444,62 +482,52 @@ function core.init()
end end
do 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 if window_mode == "normal" then
system.set_window_size(table.unpack(window_position)) system.set_window_size(table.unpack(window_position))
elseif window_mode == "maximized" then elseif window_mode == "maximized" then
system.set_window_mode("maximized") system.set_window_mode("maximized")
end end
core.recent_projects = recent_projects
end 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.log_items = {}
core.docs = {} core.docs = {}
core.window_mode = "normal" core.project_entries = {}
core.threads = setmetatable({}, { __mode = "k" }) core.project_name = ""
core.blink_start = system.get_time()
core.blink_timer = core.blink_start
local project_dir_abs = system.absolute_path(project_dir) local init_files = {}
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) local delayed_errors = {}
if set_project_ok then for i = 2, #ARGS do
if project_dir_explicit then local filename = strip_trailing_slash(ARGS[i])
update_recents_project("add", project_dir_abs) local info = system.get_file_info(filename)
end if info and info.type == "file" then
else filename = system.absolute_path(filename)
if not project_dir_explicit then if filename then
update_recents_project("remove", project_dir) core.add_project_file(filename)
end table.insert(init_files, filename)
project_dir_abs = system.absolute_path(".") end
if not core.set_project_dir(project_dir_abs) then elseif info and info.type == "dir" then
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") filename = system.absolute_path(filename)
os.exit(1) 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
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.redraw = true
core.visited_files = {} core.visited_files = {}
core.restart_request = false core.restart_request = false
@ -522,21 +550,29 @@ function core.init()
core.project_scan_thread_id = core.add_thread(project_scan_thread) core.project_scan_thread_id = core.add_thread(project_scan_thread)
command.add_defaults() 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 got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins() 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() 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)) core.root_view:open_doc(core.open_doc(filename))
end end
if delayed_error then for _, error_msg in ipairs(delayed_errors) do
core.error(delayed_error) core.error(error_msg)
end end
if not plugins_success or got_user_error or got_project_error then 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 "") .. string.format("%06x", temp_file_counter) .. (ext or "")
end end
-- override to perform an operation before quitting or entering the
-- current project function core.on_quit_project()
do local filename = USERDIR .. PATHSEP .. "workspace.lua"
local do_nothing = function() end core.try(project.save_workspace, filename)
core.on_quit_project = do_nothing end
core.on_enter_project = do_nothing
function core.on_enter_project(new_dir)
-- FIXME: check the logic
-- core.try(project.load_workspace, USERDIR .. PATHSEP .. "workspace.lua")
end end
@ -755,6 +795,7 @@ end
function core.set_visited(filename) function core.set_visited(filename)
filename = core.as_project_filename(filename) or common.home_encode(filename)
for i = 1, #core.visited_files do for i = 1, #core.visited_files do
if core.visited_files[i] == filename then if core.visited_files[i] == filename then
table.remove(core.visited_files, i) table.remove(core.visited_files, i)
@ -808,29 +849,41 @@ function core.pop_clip_rect()
renderer.set_clip_rect(x, y, w, h) renderer.set_clip_rect(x, y, w, h)
end end
-- The function below works like system.absolute_path except it
function core.normalize_to_project_dir(filename) -- doesn't fail if the file does not exist. We consider that the
filename = common.normalize_path(filename) -- current dir is core.working_dir so relative filename are considered
if common.path_belongs_to(filename, core.project_dir) then -- to be in core.working_dir.
filename = common.relative_path(core.project_dir, filename) -- 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 end
return filename end
function core.normalize_to_working_dir(filename)
filename = common.normalize_path(filename)
return core.working_dir_absolute_path(filename)
end end
function core.open_doc(filename) function core.open_doc(filename)
local new_file = not filename or not system.get_file_info(filename)
if filename then if filename then
-- normalize filename and set absolute filename then
-- try to find existing doc for filename -- 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 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 return doc
end end
end end
end end
-- no existing doc for filename; create new -- no existing doc for filename; create new
filename = core.normalize_to_project_dir(filename) local doc = Doc(filename, new_file)
local doc = Doc(filename)
table.insert(core.docs, doc) table.insert(core.docs, doc)
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename)
return doc return doc

View File

@ -105,7 +105,7 @@ keymap.add_direct {
["ctrl+p"] = "core:find-file", ["ctrl+p"] = "core:find-file",
["ctrl+o"] = "core:open-file", ["ctrl+o"] = "core:open-file",
["ctrl+n"] = "core:new-doc", ["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", ["ctrl+shift+o"] = "core:open-project-folder",
["alt+return"] = "core:toggle-fullscreen", ["alt+return"] = "core:toggle-fullscreen",

View File

@ -138,7 +138,8 @@ function StatusView:get_items()
style.icon_font, "g", style.icon_font, "g",
style.font, style.dim, self.separator2, style.font, style.dim, self.separator2,
#core.docs, style.text, " / ", #core.docs, style.text, " / ",
#core.project_files, " files" "(NYI) files"
--- #core.project_files, " files"
} }
end end

View File

@ -23,8 +23,8 @@ function ResultsView:get_name()
end end
local function find_all_matches_in_file(t, filename, fn) local function find_all_matches_in_file(t, dirpath, dirname, filename, fn)
local fp = io.open(filename) local fp = io.open(dirpath .. PATHSEP .. filename)
if not fp then return t end if not fp then return t end
local n = 1 local n = 1
for line in fp:lines() do 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 -- 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. -- 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) 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 core.redraw = true
end end
if n % 100 == 0 then coroutine.yield() end if n % 100 == 0 then coroutine.yield() end
@ -54,10 +54,9 @@ function ResultsView:begin_search(text, fn)
core.add_thread(function() core.add_thread(function()
local i = 1 local i = 1
for dir_name, file in core.get_project_files() do for dirpath, dirname, item in core.get_project_files() do
if file.type == "file" then if item.type == "file" then
local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) find_all_matches_in_file(self.results, dirpath, dirname, item.filename, fn)
find_all_matches_in_file(self.results, path .. file.filename, fn)
end end
self.last_file_idx = i self.last_file_idx = i
i = i + 1 i = i + 1
@ -102,7 +101,8 @@ function ResultsView:open_selected_result()
return return
end end
core.try(function() 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() core.root_view.root_node:update_layout()
dv.doc:set_selection(res.line, res.col) dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true) 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) core.command_view:enter("Find Regex In Project", function(text)
local re = regex.compile(text, "i") local re = regex.compile(text, "i")
begin_search(text, function(line_text) begin_search(text, function(line_text)
return regex.cmatch(re, line_text) return regex.cmatch(re, line_text)
end) end)
end) end)
end, end,
@ -276,22 +276,22 @@ command.add(ResultsView, {
["project-search:refresh"] = function() ["project-search:refresh"] = function()
core.active_view:refresh() core.active_view:refresh()
end, end,
["project-search:move-to-previous-page"] = function() ["project-search:move-to-previous-page"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view.scroll.to.y - view.size.y view.scroll.to.y = view.scroll.to.y - view.size.y
end, end,
["project-search:move-to-next-page"] = function() ["project-search:move-to-next-page"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view.scroll.to.y + view.size.y view.scroll.to.y = view.scroll.to.y + view.size.y
end, end,
["project-search:move-to-start-of-doc"] = function() ["project-search:move-to-start-of-doc"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = 0 view.scroll.to.y = 0
end, end,
["project-search:move-to-end-of-doc"] = function() ["project-search:move-to-end-of-doc"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view:get_scrollable_size() view.scroll.to.y = view:get_scrollable_size()

View File

@ -64,19 +64,27 @@ function TreeView:get_cached(item, dirname)
local t = dir_cache[cache_name] local t = dir_cache[cache_name]
if not t then if not t then
t = {} t = {}
local basename = common.basename(item.filename) if item.type == 'file' and item.topdir then
if item.topdir then t.filename = item.filename
t.filename = basename
t.expanded = true
t.depth = 0 t.depth = 0
t.abs_filename = dirname t.abs_filename = dirname
t.name = item.filename
t.type = item.type
else else
t.filename = item.filename local basename = common.basename(item.filename)
t.depth = get_depth(item.filename) if item.topdir then
t.abs_filename = dirname .. PATHSEP .. item.filename 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 end
t.name = basename
t.type = item.type
dir_cache[cache_name] = t dir_cache[cache_name] = t
end end
return t return t
@ -102,8 +110,8 @@ end
function TreeView:check_cache() function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed -- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do for i = 1, #core.project_entries do
local dir = core.project_directories[i] local dir = core.project_entries[i]
local last_files = self.last[dir.name] local last_files = self.last[dir.name]
if not last_files then if not last_files then
self.last[dir.name] = dir.files self.last[dir.name] = dir.files
@ -126,8 +134,8 @@ function TreeView:each_item()
local w = self.size.x local w = self.size.x
local h = self:get_item_height() local h = self:get_item_height()
for k = 1, #core.project_directories do for k = 1, #core.project_entries do
local dir = core.project_directories[k] local dir = core.project_entries[k]
local dir_cached = self:get_cached(dir.item, dir.name) local dir_cached = self:get_cached(dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h) coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1 count_lines = count_lines + 1
@ -173,13 +181,13 @@ end
function TreeView:on_mouse_moved(px, py, ...) function TreeView:on_mouse_moved(px, py, ...)
TreeView.super.on_mouse_moved(self, px, py, ...) TreeView.super.on_mouse_moved(self, px, py, ...)
if self.dragging_scrollbar then return end if self.dragging_scrollbar then return end
local item_changed, tooltip_changed local item_changed, tooltip_changed
for item, x,y,w,h in self:each_item() do 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 if px > x and py > y and px <= x + w and py <= y + h then
item_changed = true item_changed = true
self.hovered_item = item self.hovered_item = item
x,y,w,h = self:get_text_bounding_box(item, x,y,w,h) 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 if px > x and py > y and px <= x + w and py <= y + h then
tooltip_changed = true tooltip_changed = true
@ -231,7 +239,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
end end
else else
core.try(function() 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)) core.root_view:open_doc(core.open_doc(doc_filename))
end) end)
end end
@ -247,7 +255,7 @@ function TreeView:update()
else else
self:move_towards(self.size, "x", dest) self:move_towards(self.size, "x", dest)
end end
local duration = system.get_time() - self.tooltip.begin local duration = system.get_time() - self.tooltip.begin
if self.hovered_item and self.tooltip.x and duration > tooltip_delay then if self.hovered_item and self.tooltip.x and duration > tooltip_delay then
self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate) self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate)

View File

@ -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