Compare commits

...

3 Commits

Author SHA1 Message Date
Francesco Abbate 27c9a3181f Add missing project implementation file 2021-06-08 11:20:57 +02:00
Francesco Abbate 26de50b583 Fix apparent problem with load_session* 2021-06-07 09:04:17 +02:00
Francesco Abbate e786951e85 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
2021-06-04 17:20:44 +02:00
13 changed files with 564 additions and 470 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
@ -18,47 +19,51 @@ local core = {}
local function load_session() local function load_session()
local ok, t = pcall(dofile, USERDIR .. "/session.lua") local ok, t = pcall(dofile, USERDIR .. "/session.lua")
if ok then if ok then
return t.recents, t.window, t.window_mode return t.recents, t.recents_open, 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
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 +71,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 +159,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 +185,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 +225,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 +254,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 +304,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 +388,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 +438,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 +451,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 +467,51 @@ 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
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 +534,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 +640,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 +779,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 +833,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",

227
data/core/project.lua Normal file
View File

@ -0,0 +1,227 @@
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local DocView = require "core.docview"
local project = {}
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
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 ok, doc = pcall(core.open_doc, t.filename)
if not ok then
return DocView(core.open_doc())
end
local dv = DocView(doc)
if t.text then doc:insert(1, 1, t.text) end
doc:set_selection(table.unpack(t.selection))
dv.last_line, dv.last_col = 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
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
for _, v in ipairs(t.views) do
local view = load_view(v)
if v.active then res = view end
node:add_view(view)
end
if t.active_view then
node:set_active_view(node.views[t.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
function project.save_workspace(filename)
local root = get_unlocked_root(core.root_view.root_node)
local fp = io.open(filename, "w")
if fp then
local node_text = common.serialize(save_node(root))
local topdir_entries = {}
for _, entry in ipairs(core.project_entries) do
if entry.item.topdir then
table.insert(topdir_entries, {path = entry.name, type = entry.item.type})
end
end
local project_entries_text = common.serialize(topdir_entries)
fp:write(string.format(
"return { project_name = %q, working_dir = %q, documents = %s, project_entries = %s }\n",
core.project_name, core.working_dir, node_text, project_entries_text))
fp:close()
end
end
function project.load(name)
core.project_name = name
local filename = common.path_join(USERDIR, "projects", name .. ".lua")
project.load_workspace(filename)
core.log("Loaded project %s.", core.project_name)
core.reschedule_project_scan()
end
function project.save(name)
name = name or core.project_name
local filename = common.path_join(USERDIR, "projects", name .. ".lua")
save_workspace(filename)
core.log("Saved project %s.", core.project_name)
end
function project.load_workspace(filename)
local load = loadfile(filename)
local workspace = load and load()
-- FIXME: decide, error or return a success code
if not workspace then error("Cannot load workspace") end
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
core.project_name = workspace.project_name
core.project_entries = {}
for _, entry in ipairs(workspace.project_entries) do
if entry.type == "dir" then
core.add_project_directory(entry.path)
elseif entry.type == "dir" then
core.add_project_file(entry.path)
end
end
system.chdir(workspace.working_dir)
end
end
function project.list()
local all = system.list_dir(USERDIR .. PATHSEP .. "projects")
end
local function suggest_directory(text)
text = common.home_expand(text)
return common.home_encode_list(text == "" and core.recents_open.dir or common.dir_path_suggest(text))
end
command.add(nil, {
["project:save-as"] = function()
local entry = core.project_entries[1]
if entry then
core.command_view:set_text(entry.item.filename)
end
core.command_view:enter("Save Project As", function(text)
-- FIXME: add sanity check of project name.
core.project_name = text
project.save()
end)
end,
["project:save"] = function()
if core.project_name == "" then
core.command_view:enter("Save Project As", function(text)
core.project_name = text
end)
end
project.save()
end,
["project:load"] = function()
core.command_view:enter("Load Project", function(text)
project.load(text)
core.set_recent_project(core.project_name)
end)
end,
["project:open-directory"] = function()
core.command_view:enter("Open Directory", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text))
local path_stat = system.get_file_info(text)
if not path_stat or path_stat.type ~= 'dir' then
core.error("Cannot open folder %q", text)
return
end
core.confirm_close_all(core.new_project_from_directory, text)
end, suggest_directory)
end,
})
return project

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)

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

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

View File

@ -0,0 +1,21 @@
## Session file
stores:
- `core.recent_projects`
- `core.recents_open`
- window's size and mode
### Rational
It just stores the window's mode and the list of recents projects and recently
opened file.
Maybe we should not have recent projects, all existing projects should be
listed. Also the window's size and mode should be part of a project.