Compare commits

...

19 Commits

Author SHA1 Message Date
Francesco Abbate 78bb96cf72 Fix a few problems related to the rebase 2021-06-04 17:16:28 +02:00
Francesco Abbate abf00254f1 Fix a few things 2021-06-04 16:32:51 +02:00
Francesco Abbate 52325d20dd WIP: implement new project commands
Implement commands to load a project from a directory and integrate
the project module within the core modules replacing the workspace
plugin.

Needs polishing but the basic functionalities are there.
2021-06-04 16:32:25 +02:00
Francesco Abbate 9e29b90318 Add first commands to load/save projects 2021-06-04 16:28:17 +02:00
Francesco Abbate dd802f9e17 Fix accidental variable shadowing 2021-06-04 16:28:17 +02:00
Francesco Abbate 89bb106d32 Improve code project filename resolution 2021-06-04 16:28:13 +02:00
Francesco Abbate 4621ee2e7f Accept relative filenames in command find-file
Use name within project entries to resolve filenames in
command find-file.
2021-06-04 16:26:18 +02:00
Francesco Abbate 6dd5743be8 Bring back a simplified workspace plugin
Now it only save one file corresponding to the workspace in use.
2021-06-04 16:26:17 +02:00
Francesco Abbate 2871f91da8 Fix commande core:remove-directory 2021-06-04 16:26:17 +02:00
Francesco Abbate d84246c61d Fix application start for project-based approach 2021-06-04 16:26:16 +02:00
Francesco Abbate 07a6cb23bc Let the command open-file open a directory as well 2021-06-04 16:26:15 +02:00
Francesco Abbate 0866f5dba9 Remove debug messages from treeview 2021-06-04 16:26:15 +02:00
Francesco Abbate 82231dbc98 Fix a few more things about filenames 2021-06-04 16:26:14 +02:00
Francesco Abbate 2aac1b8179 Fix doc opening to use absolute filenames
Now we store doc.filename only and removed doc.abs_filename. The former
doc.filename is always the absolute filename.

We use now the variable core.working_dir to transform relative path into
absolute ones.
2021-06-04 16:26:11 +02:00
Francesco Abbate 0a53425381 WIP implementation of project-based schema
Roughly works but need more work and polishing. The workspace plugin
is temporarily disable waiting to be fixed.

Now the application does no longer 'chdir' into the project directory
and we removed the concept of project's directory (core.project_dir)
and project's files (core.project_files). Instead we have always a
project that can contain zero, one or many directories or files.
No directory is special within a project, there is no longer a concept
of project's directory.

WIP adapting open-file command to open a directory

Crash when adding a file into the project
2021-06-04 16:10:57 +02:00
Francesco Abbate e8fd785227 Fix problem with previous commit
Desastrous problem where core.normalize_path was removing the leading /.
2021-06-04 16:03:49 +02:00
Francesco Abbate 527b11029e Create new document if file doesn't exist
If a non-existing file is specified with the command "core:open-file"
a new document is opened with the given filename provided the directory
already exists.

The flag new_file is set to true in the Doc instance.

The file will be actually created only when the "save" command is used.

The document will be marked with the "*" event when no changes are done
to mean that it is a new file and is not yet saved.

The function common.normalize_path now process the .. and . in the
filename. Before was not needed because system.absolute_path already
get rid of them but now we need to have the absolute path of files
that not yet exists so we cannot use system.absolute_path.
2021-06-04 16:03:46 +02:00
Francesco Abbate 0d48f9e8b5 Remove duplicate normalize_path function
Use the function defined in the "common" module.

Move the check for not-nil filename from common.normalize_path
to core.open_doc. In this latter the filename can be nil if a
new unnamed document is created.
2021-06-04 16:01:28 +02:00
liquidev 7e369da486 Support for changing fonts per syntax group (#178) 2021-06-04 16:01:25 +02:00
14 changed files with 598 additions and 479 deletions

View File

@ -9,16 +9,6 @@ The "Project Search: Find" will work by searching all the files present in the p
Implemented changing fonts per syntax group by @liquidev.
Example user module snippet that makes all comments italic:
```lua
local style = require "core.style"
-- italic.ttf must be provided by the user
local italic = renderer.font.load("italic.ttf", 14)
style.syntax_fonts["comment"] = italic
```
Improved indentation behavior by @adamharrison.
Fix bug with close button not working in borderless window mode.
@ -70,6 +60,10 @@ Both kind of tags can appear in new plugins in the form:
where the old tag needs to appear at the end for compatibility.
=======
### 1.16.8
>>>>>>> b76905e (Support for changing fonts per syntax group (#178))
### 1.16.7
Add support for retina displays on Mac OS X.

View File

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

View File

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

View File

@ -108,9 +108,6 @@ function common.path_suggest(text)
file = path .. file
local info = system.get_file_info(file)
if info then
if info.type == "dir" then
file = file .. PATHSEP
end
if file:lower():find(text:lower(), nil, true) == 1 then
table.insert(res, file)
end
@ -196,6 +193,16 @@ function common.serialize(val)
end
function common.path_join(...)
local n = select('#', ...)
local accu = select(1, ...)
for i = 2, n do
accu = accu .. PATHSEP .. select(i, ...)
end
return accu
end
function common.basename(path)
-- a path should never end by / or \ except if it is '/' (unix root) or
-- 'X:\' (windows drive)
@ -203,6 +210,12 @@ function common.basename(path)
end
-- can return nil if there is no directory part in the path
function common.dirname(path)
return path:match("(.+)[\\/][^\\/]+$")
end
function common.home_encode(text)
if HOME and string.find(text, HOME, 1, true) == 1 then
local dir_pos = #HOME + 1
@ -230,18 +243,11 @@ function common.home_expand(text)
end
function common.normalize_path(filename)
if filename and PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
return drive and drive:upper() .. rem or filename
end
return filename
end
local function split_on_slash(s, sep_pattern)
local t = {}
if s:match("^[/\\]") then
t[#t + 1] = ""
end
for fragment in string.gmatch(s, "([^/\\]+)") do
t[#t + 1] = fragment
end
@ -249,8 +255,27 @@ local function split_on_slash(s, sep_pattern)
end
function common.normalize_path(filename)
if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
filename = drive and drive:upper() .. rem or filename
end
local parts = split_on_slash(filename, PATHSEP)
local accu = {}
for _, part in ipairs(parts) do
if part == '..' then
table.remove(accu)
elseif part ~= '.' then
table.insert(accu, part)
end
end
return table.concat(accu, PATHSEP)
end
function common.path_belongs_to(filename, path)
return filename and string.find(filename, path .. PATHSEP, 1, true) == 1
return string.find(filename, path .. PATHSEP, 1, true) == 1
end

View File

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

View File

@ -88,9 +88,9 @@ end
function DocView:get_filename()
if self.doc.abs_filename then
if self.doc.filename then
local post = self.doc:is_dirty() and "*" or ""
return common.home_encode(self.doc.abs_filename) .. post
return common.home_encode(self.doc.filename) .. post
end
return self:get_name()
end

View File

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

View File

@ -105,7 +105,7 @@ keymap.add_direct {
["ctrl+p"] = "core:find-file",
["ctrl+o"] = "core:open-file",
["ctrl+n"] = "core:new-doc",
["ctrl+shift+c"] = "core:change-project-folder",
["ctrl+shift+c"] = "project:open-directory",
["ctrl+shift+o"] = "core:open-project-folder",
["alt+return"] = "core:toggle-fullscreen",

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

@ -0,0 +1,221 @@
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
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.font, style.dim, self.separator2,
#core.docs, style.text, " / ",
#core.project_files, " files"
"(NYI) files"
--- #core.project_files, " files"
}
end

View File

@ -23,8 +23,8 @@ function ResultsView:get_name()
end
local function find_all_matches_in_file(t, filename, fn)
local fp = io.open(filename)
local function find_all_matches_in_file(t, dirpath, dirname, filename, fn)
local fp = io.open(dirpath .. PATHSEP .. filename)
if not fp then return t end
local n = 1
for line in fp:lines() do
@ -33,7 +33,7 @@ local function find_all_matches_in_file(t, filename, fn)
-- Insert maximum 256 characters. If we insert more, for compiled files, which can have very long lines
-- things tend to get sluggish. If our line is longer than 80 characters, begin to truncate the thing.
local start_index = math.max(s - 80, 1)
table.insert(t, { file = filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s })
table.insert(t, { file = dirname .. PATHSEP .. filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s })
core.redraw = true
end
if n % 100 == 0 then coroutine.yield() end
@ -54,10 +54,9 @@ function ResultsView:begin_search(text, fn)
core.add_thread(function()
local i = 1
for dir_name, file in core.get_project_files() do
if file.type == "file" then
local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP))
find_all_matches_in_file(self.results, path .. file.filename, fn)
for dirpath, dirname, item in core.get_project_files() do
if item.type == "file" then
find_all_matches_in_file(self.results, dirpath, dirname, item.filename, fn)
end
self.last_file_idx = i
i = i + 1
@ -102,7 +101,8 @@ function ResultsView:open_selected_result()
return
end
core.try(function()
local dv = core.root_view:open_doc(core.open_doc(res.file))
local filename = core.resolve_project_filename(res.file)
local dv = core.root_view:open_doc(core.open_doc(filename))
core.root_view.root_node:update_layout()
dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true)
@ -241,7 +241,7 @@ command.add(nil, {
core.command_view:enter("Find Regex In Project", function(text)
local re = regex.compile(text, "i")
begin_search(text, function(line_text)
return regex.cmatch(re, line_text)
return regex.cmatch(re, line_text)
end)
end)
end,
@ -276,22 +276,22 @@ command.add(ResultsView, {
["project-search:refresh"] = function()
core.active_view:refresh()
end,
["project-search:move-to-previous-page"] = function()
local view = core.active_view
view.scroll.to.y = view.scroll.to.y - view.size.y
end,
["project-search:move-to-next-page"] = function()
local view = core.active_view
view.scroll.to.y = view.scroll.to.y + view.size.y
end,
["project-search:move-to-start-of-doc"] = function()
local view = core.active_view
view.scroll.to.y = 0
end,
["project-search:move-to-end-of-doc"] = function()
local view = core.active_view
view.scroll.to.y = view:get_scrollable_size()

View File

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

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,42 @@
`core.project_directories` => `core.project_entries`
- use a new `type` field to indicate it is a directory or a file
`core.{project_dir,project_files}` => removed
`core.set_project_dir` => removed
`core.on_enter_project` => decide what to do
No longer use `chdir` command.
## New functions
`core.add_project_file`
## Modified functions
- `core.add_project_directory`
- `project_files_iter` local function in `core/init.lua`
Function `remove_project_directory` is renamed to `remove_project_entry`.
## Broken
workspace plugin is not working for the moment.
Number of files show in statusview.
## To be done
- When using "core:find-file" do not display the full path of the file
- FIX the workspace plugin
- FIX number of files display in statusview
- Add a function to add a file into the project
- Add logic to do not show treeview if it contains only a single file
- Modify "core:open-file" to accept a directory
- Modify "core:open-file" to accept a non-existing file name (new file)
## Misc observations
When performing adding directory, pressing enter does not use the item => to be fixed.
The function `system.chdir` is no longer used and could be removed, in theory.