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.
### 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
Fix a bug related to nested panes resizing.

View File

@ -67,15 +67,15 @@ command.add(nil, {
["core:find-file"] = function()
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)
@ -87,19 +87,36 @@ command.add(nil, {
["core:open-file"] = function()
local view = core.active_view
if view.doc and view.doc.abs_filename then
core.command_view:set_text(common.home_encode(view.doc.abs_filename))
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)
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)
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
@ -128,10 +145,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)
@ -151,11 +168,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)
@ -172,13 +189,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

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

View File

@ -3,6 +3,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
@ -17,46 +18,65 @@ local core = {}
local function load_session()
local ok, t = pcall(dofile, USERDIR .. "/session.lua")
if ok then
return t.recents, t.window
return t.recent_projects, t.recents_open, t.window
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())),
"}\n")
fp:write(string.format(
"return { recent_projects= %s, recents_open= %s, window= %s}\n",
common.serialize(core.recent_projects),
common.serialize(core.recents_open),
common.serialize(table.pack(system.get_window_size()))
))
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
@ -64,27 +84,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.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
@ -164,23 +171,22 @@ local function project_scan_thread()
while true do
-- get project files and replace previous table if the new table is
-- different
for i = 1, #core.project_directories do
local dir = core.project_directories[i]
local t, entries_count = get_files(dir.name, "")
if diff_files(dir.files, t) then
if entries_count > config.max_project_files then
core.status_view:show_message("!", style.accent,
"Too many files in project directory: stopping reading at "..
config.max_project_files.." files according to config.max_project_files. "..
"Either tweak this variable, or ignore certain files/directories by "..
"using the config.ignore_files variable in your user plugin or "..
"project config.")
for i = 1, #core.project_entries do
local dir = core.project_entries[i]
if dir.item.type == 'dir' then
local t, entries_count = get_files(dir.name, "")
if diff_files(dir.files, t) then
if entries_count > config.max_project_files then
core.status_view:show_message("!", style.accent,
"Too many files in project directory: stopping reading at "..
config.max_project_files.." files according to config.max_project_files. "..
"Either tweak this variable, or ignore certain files/directories by "..
"using the config.ignore_files variable in your user plugin or "..
"project config.")
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
end
@ -191,15 +197,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
@ -211,13 +217,35 @@ end
function core.project_files_number()
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
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()
@ -238,7 +266,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 .. "\"")
@ -322,26 +350,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
@ -360,6 +399,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"
@ -375,60 +415,50 @@ function core.init()
end
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
system.set_window_size(table.unpack(window_position))
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
@ -451,21 +481,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
@ -497,6 +535,13 @@ function core.init()
if item.text == "Exit" then os.exit(1) 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
@ -547,12 +592,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
@ -676,6 +725,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)
@ -729,29 +779,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
@ -1011,7 +1073,7 @@ end
core.add_save_hook(function(filename)
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.load_user_directory()
end

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",

View File

@ -1,59 +1,9 @@
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local DocView = require "core.docview"
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 project = {}
local function has_no_locked_children(node)
if node.locked then return false end
@ -160,63 +110,112 @@ local function load_node(node, t)
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()
function project.save_workspace(filename)
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")
local fp = io.open(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))
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
local function load_workspace()
local workspace = load_workspace_file(core.project_dir)
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
for i, dir_name in ipairs(workspace.directories) do
core.add_project_directory(system.absolute_path(dir_name))
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 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(...)
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

@ -57,4 +57,11 @@ style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { 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

View File

@ -23,14 +23,14 @@ 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
local s = fn(line)
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
end
if n % 100 == 0 then coroutine.yield() end
@ -51,10 +51,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
@ -99,7 +98,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)

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
@ -95,8 +103,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
@ -121,8 +129,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
@ -218,7 +226,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
end
else
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))
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.