Compare commits

...

19 Commits

Author SHA1 Message Date
Francesco Abbate 3ccfb94107 Fix a few things 2021-05-19 11:35:25 +02:00
Francesco Abbate ef4ad10326 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-05-18 17:20:40 +02:00
Francesco Abbate 9c83082ce8 Add first commands to load/save projects 2021-05-18 12:58:54 +02:00
Francesco Abbate 19623b908e Fix accidental variable shadowing 2021-05-18 12:57:56 +02:00
Francesco Abbate fd47d646c6 Improve code project filename resolution 2021-05-18 09:15:35 +02:00
Francesco Abbate 0cc87591e6 Accept relative filenames in command find-file
Use name within project entries to resolve filenames in
command find-file.
2021-05-17 23:10:15 +02:00
Francesco Abbate 239abe86fb Bring back a simplified workspace plugin
Now it only save one file corresponding to the workspace in use.
2021-05-14 17:48:01 +02:00
Francesco Abbate 8571076eb3 Fix commande core:remove-directory 2021-05-14 12:19:24 +02:00
Francesco Abbate befe837eb2 Fix application start for project-based approach 2021-05-14 10:59:42 +02:00
Francesco Abbate 5b15029e14 Let the command open-file open a directory as well 2021-05-13 17:04:58 +02:00
Francesco Abbate b497d3ea13 Remove debug messages from treeview 2021-05-13 14:58:13 +02:00
Francesco Abbate 948ce96e0d Fix a few more things about filenames 2021-05-13 14:57:55 +02:00
Francesco Abbate 803d6e0f8d 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-05-13 12:07:56 +02:00
Francesco Abbate ac0dba18de 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-05-11 19:38:33 +02:00
Francesco Abbate ea3a7e81be Fix problem with previous commit
Desastrous problem where core.normalize_path was removing the leading /.
2021-05-10 17:01:14 +02:00
Francesco Abbate 708c2983ef 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-05-10 16:44:27 +02:00
Francesco Abbate 0ce5680ef2 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-05-10 13:08:39 +02:00
Francesco Abbate 1c6325b40f Merge branch 'master' into 'dev' 2021-05-07 08:25:35 +02:00
liquidev b76905e78a
Support for changing fonts per syntax group (#178) 2021-05-05 23:08:10 +02:00
14 changed files with 514 additions and 320 deletions

View File

@ -2,6 +2,18 @@ Lite XL is following closely [rxi/lite](https://github.com/rxi/lite) but with so
This files document the changes done in Lite XL for each release. This files document the changes done in Lite XL for each release.
### next release
[#126](https://github.com/franko/lite-xl/issues/126): Implemented changing fonts per syntax group.
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
```
### 1.16.9 ### 1.16.9
Fix a bug related to nested panes resizing. Fix a bug related to nested panes resizing.

View File

@ -67,15 +67,15 @@ command.add(nil, {
["core:find-file"] = function() ["core:find-file"] = function()
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)
@ -87,19 +87,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
core.command_view:set_text(common.home_encode(view.doc.abs_filename)) core.command_view:set_text(common.home_encode(view.doc.filename))
end end
core.command_view:enter("Open File", function(text) core.command_view:enter("Open File", function(text)
core.root_view:open_doc(core.open_doc(common.home_expand(text))) 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) 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
@ -128,10 +145,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)
@ -151,11 +168,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)
@ -172,13 +189,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

@ -68,7 +68,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)
@ -323,12 +323,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
@ -228,18 +241,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
@ -247,8 +253,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
@ -141,29 +141,45 @@ end
function DocView:get_col_x_offset(line, col) function DocView:get_col_x_offset(line, col)
local text = self.doc.lines[line] local default_font = self:get_font()
if not text then return 0 end local column = 1
return self:get_font():get_width(text:sub(1, col - 1)) local xoffset = 0
for _, type, text in self.doc.highlighter:each_token(line) do
local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do
if column == col then
return xoffset / font:subpixel_scale()
end
xoffset = xoffset + font:get_width_subpixel(char)
column = column + #char
end
end
return xoffset / default_font:subpixel_scale()
end end
function DocView:get_x_offset_col(line, x) function DocView:get_x_offset_col(line, x)
local text = self.doc.lines[line] local line_text = self.doc.lines[line]
local xoffset, last_i, i = 0, 1, 1 local xoffset, last_i, i = 0, 1, 1
local subpixel_scale = self:get_font():subpixel_scale(); local default_font = self:get_font()
local subpixel_scale = default_font:subpixel_scale()
local x_subpixel = subpixel_scale * x + subpixel_scale / 2 local x_subpixel = subpixel_scale * x + subpixel_scale / 2
for char in common.utf8_chars(text) do for _, type, text in self.doc.highlighter:each_token(line) do
local w = self:get_font():get_width_subpixel(char) local font = style.syntax_fonts[type] or default_font
if xoffset >= subpixel_scale * x then for char in common.utf8_chars(text) do
return (xoffset - x_subpixel > w / 2) and last_i or i local w = font:get_width_subpixel(char)
if xoffset >= subpixel_scale * x then
return (xoffset - x_subpixel > w / 2) and last_i or i
end
xoffset = xoffset + w
last_i = i
i = i + #char
end end
xoffset = xoffset + w
last_i = i
i = i + #char
end end
return #text return #line_text
end end
@ -308,11 +324,12 @@ end
function DocView:draw_line_text(idx, x, y) function DocView:draw_line_text(idx, x, y)
local font = self:get_font() local default_font = self:get_font()
local subpixel_scale = font:subpixel_scale() local subpixel_scale = default_font:subpixel_scale()
local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset() local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(idx) do for _, type, text in self.doc.highlighter:each_token(idx) do
local color = style.syntax[type] local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font
if config.draw_whitespace then if config.draw_whitespace then
tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment) tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment)
else else

View File

@ -3,6 +3,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
@ -17,46 +18,65 @@ 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 return t.recent_projects, t.recents_open, t.window
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}\n",
"}\n") common.serialize(core.recent_projects),
common.serialize(core.recents_open),
common.serialize(table.pack(system.get_window_size()))
))
fp:close() fp:close()
end end
end end
local function normalize_path(s) local function update_recents(recents, action, name)
local drive, path = s:match("^([a-z]):([/\\].*)")
return drive and drive:upper() .. ":" .. path or s
end
local function update_recents_project(action, dir_path_abs)
local dirname = normalize_path(dir_path_abs)
if not dirname then return end
local recents = core.recent_projects
local n = #recents local n = #recents
for i = 1, n do for i = 1, n do
if dirname == recents[i] then if name == recents[i] then
table.remove(recents, i) table.remove(recents, i)
break break
end end
end end
if action == "add" then if action == "add" then
table.insert(recents, 1, dirname) table.insert(recents, 1, name)
end end
end end
function core.set_recent_project(name)
update_recents(core.recent_projects, "add", name)
end
function core.set_recent_open(type, filename)
update_recents(core.recents_open[type], "add", filename)
end
-- FIXME: remove or adapt
--[[ local function cleanup_recent_projects()
local recents = core.recent_projects
local i = 1
while i <= #recents do
local info = system.get_file_info(recents[i])
if not info or info.type ~= "dir" then
table.remove(recents, i)
else
i = i + 1
end
end
end ]]
function core.reschedule_project_scan() function core.reschedule_project_scan()
if core.project_scan_thread_id then if core.project_scan_thread_id then
core.threads[core.project_scan_thread_id].wake = 0 core.threads[core.project_scan_thread_id].wake = 0
@ -64,27 +84,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.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
@ -164,23 +171,22 @@ local function project_scan_thread()
while true do while true do
-- 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
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 t, entries_count = get_files(dir.name, "") if dir.item.type == 'dir' then
if diff_files(dir.files, t) then local t, entries_count = get_files(dir.name, "")
if entries_count > config.max_project_files then if diff_files(dir.files, t) then
core.status_view:show_message("!", style.accent, if entries_count > config.max_project_files then
"Too many files in project directory: stopping reading at ".. core.status_view:show_message("!", style.accent,
config.max_project_files.." files according to config.max_project_files. ".. "Too many files in project directory: stopping reading at "..
"Either tweak this variable, or ignore certain files/directories by ".. config.max_project_files.." files according to config.max_project_files. "..
"using the config.ignore_files variable in your user plugin or ".. "Either tweak this variable, or ignore certain files/directories by "..
"project config.") "using the config.ignore_files variable in your user plugin or "..
"project config.")
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
end end
@ -191,15 +197,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
@ -211,13 +217,35 @@ end
function core.project_files_number() function core.project_files_number()
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
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()
@ -238,7 +266,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 .. "\"")
@ -322,26 +350,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
@ -360,6 +399,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"
@ -375,60 +415,50 @@ function core.init()
end end
do do
local recent_projects, window_position = load_session() -- FIXME: change the name for "recents_open"
local window_position
core.recent_projects, core.recents_open, window_position = load_session()
if window_position then if window_position then
system.set_window_size(table.unpack(window_position)) system.set_window_size(table.unpack(window_position))
end end
core.recent_projects = recent_projects
end end
-- cleanup_recent_projects()
local project_dir = core.recent_projects[1] or "."
local project_dir_explicit = false
local files = {}
local delayed_error
for i = 2, #ARGS do
local arg_filename = strip_trailing_slash(ARGS[i])
local info = system.get_file_info(arg_filename) or {}
if info.type == "file" then
local file_abs = system.absolute_path(arg_filename)
if file_abs then
table.insert(files, file_abs)
project_dir = file_abs:match("^(.+)[/\\].+$")
end
elseif info.type == "dir" then
project_dir = arg_filename
project_dir_explicit = true
else
delayed_error = string.format("error: invalid file or directory %q", ARGS[i])
end
end
core.frame_start = 0
core.clip_rect_stack = {{ 0,0,0,0 }}
core.log_items = {} core.log_items = {}
core.docs = {} core.docs = {}
core.window_mode = "normal" core.project_entries = {}
core.threads = setmetatable({}, { __mode = "k" }) core.project_name = ""
core.blink_start = system.get_time()
core.blink_timer = core.blink_start
local project_dir_abs = system.absolute_path(project_dir) local init_files = {}
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) local delayed_errors = {}
if set_project_ok then for i = 2, #ARGS do
if project_dir_explicit then local filename = strip_trailing_slash(ARGS[i])
update_recents_project("add", project_dir_abs) local info = system.get_file_info(filename)
end if info and info.type == "file" then
else filename = system.absolute_path(filename)
if not project_dir_explicit then if filename then
update_recents_project("remove", project_dir) core.add_project_file(filename)
end table.insert(init_files, filename)
project_dir_abs = system.absolute_path(".") end
if not core.set_project_dir(project_dir_abs) then elseif info and info.type == "dir" then
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") filename = system.absolute_path(filename)
os.exit(1) if filename then
core.add_project_directory(filename)
-- FIXME
-- update_recents(core.recents_open.dir, "add", filename)
end
else
local error_msg = string.format("error: invalid file or directory \"%s\"", ARGS[i])
table.insert(delayed_errors, error_msg)
end end
end end
core.threads = setmetatable({}, { __mode = "k" })
core.frame_start = 0
core.clip_rect_stack = {{ 0,0,0,0 }}
core.window_mode = "normal"
core.blink_start = system.get_time()
core.blink_timer = core.blink_start
core.redraw = true core.redraw = true
core.visited_files = {} core.visited_files = {}
core.restart_request = false core.restart_request = false
@ -451,21 +481,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
@ -497,6 +535,13 @@ function core.init()
if item.text == "Exit" then os.exit(1) end if item.text == "Exit" then os.exit(1) end
end) end)
end end
if #core.project_entries == 0 then
local ws = core.try(project.load_workspace, USERDIR .. PATHSEP .. "workspace.lua")
if not ws then
core.log("Empty project: use the \"Core: Open File\" command to open a file or a directory.")
end
end
end end
@ -547,12 +592,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
@ -676,6 +725,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)
@ -729,29 +779,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
@ -1011,7 +1073,7 @@ end
core.add_save_hook(function(filename) core.add_save_hook(function(filename)
local doc = core.active_view.doc local doc = core.active_view.doc
if doc and doc:is(Doc) and doc.abs_filename == USERDIR .. PATHSEP .. "init.lua" then if doc and doc:is(Doc) and doc.filename == USERDIR .. PATHSEP .. "init.lua" then
core.reload_module("core.style") core.reload_module("core.style")
core.load_user_directory() core.load_user_directory()
end end

View File

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

View File

@ -1,59 +1,9 @@
-- mod-version:1 -- lite-xl 1.16
local core = require "core" local core = require "core"
local command = require "core.command"
local common = require "core.common" local common = require "core.common"
local DocView = require "core.docview" local DocView = require "core.docview"
local project = {}
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 load_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) local function has_no_locked_children(node)
if node.locked then return false end if node.locked then return false end
@ -160,63 +110,112 @@ local function load_node(node, t)
end end
local function save_directories() function project.save_workspace(filename)
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 root = get_unlocked_root(core.root_view.root_node)
local workspace_filename = get_workspace_filename(core.project_dir) local fp = io.open(filename, "w")
local fp = io.open(workspace_filename, "w")
if fp then if fp then
local node_text = common.serialize(save_node(root)) local node_text = common.serialize(save_node(root))
local dir_text = common.serialize(save_directories()) local topdir_entries = {}
fp:write(string.format("return { path = %q, documents = %s, directories = %s }\n", core.project_dir, node_text, dir_text)) 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() fp:close()
end end
end end
local function load_workspace() function project.load(name)
local workspace = load_workspace_file(core.project_dir) 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 if workspace then
local root = get_unlocked_root(core.root_view.root_node) local root = get_unlocked_root(core.root_view.root_node)
local active_view = load_node(root, workspace.documents) local active_view = load_node(root, workspace.documents)
if active_view then if active_view then
core.set_active_view(active_view) core.set_active_view(active_view)
end end
for i, dir_name in ipairs(workspace.directories) do core.project_name = workspace.project_name
core.add_project_directory(system.absolute_path(dir_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 end
system.chdir(workspace.working_dir)
end end
end end
local function suggest_directory(text)
local run = core.run text = common.home_expand(text)
return common.home_encode_list(text == "" and core.recents_open.dir or common.dir_path_suggest(text))
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 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

@ -57,4 +57,11 @@ style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" } style.syntax["operator"] = { common.color "#93DDFA" }
style.syntax["function"] = { common.color "#93DDFA" } style.syntax["function"] = { common.color "#93DDFA" }
-- This can be used to override fonts per syntax group.
-- The syntax highlighter will take existing values from this table and
-- override style.code_font on a per-token basis, so you can choose to eg.
-- render comments in an italic font if you want to.
style.syntax_fonts = {}
-- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options)
return style return style

View File

@ -23,14 +23,14 @@ 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
local s = fn(line) local s = fn(line)
if s then if s then
table.insert(t, { file = filename, text = line, line = n, col = s }) table.insert(t, { file = dirname .. PATHSEP .. filename, text = line, 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
@ -51,10 +51,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
@ -99,7 +98,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
@ -95,8 +103,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
@ -121,8 +129,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
@ -218,7 +226,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, self.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

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