diff --git a/changelog.md b/changelog.md index afb92d26..eb203747 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,14 @@ -Lite XL is following closely [rxi/lite](https://github.com/rxi/lite) but with some enhancements. - This files document the changes done in Lite XL for each release. -### next release +### 1.16.11 + +When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. +The application remains functional and the directories can be explored without using too much memory. +In this operating mode the files of the project are not indexed so the command "Core: Find File" will act as the "Core: Open File" command. +The "Project Search: Find" will work by searching all the files present in the project directory even if they are not indexed. + +Implemented changing fonts per syntax group by @liquidev. -[#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 @@ -15,6 +19,12 @@ local italic = renderer.font.load("italic.ttf", 14) style.syntax_fonts["comment"] = italic ``` +Improved indentation behavior by @adamharrison. + +Fix bug with close button not working in borderless window mode. + +Fix problem with normalization of filename for opened documents. + ### 1.16.10 Improved syntax highlight system thanks to @liquidev and @adamharrison. diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index a7d9030d..ac30fe20 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,6 +66,9 @@ command.add(nil, { end, ["core:find-file"] = function() + if core.project_files_limit then + return command.perform "core:open-file" + end local files = {} for dir, item in core.get_project_files() do if item.type == "file" then diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index d1f1b261..9f4ec0a5 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -33,33 +33,6 @@ local function doc_multiline_selection(sort) return line1, col1, line2, col2, swap end - -local function insert_at_start_of_selected_lines(text, skip_empty) - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - for line = line1, line2 do - local line_text = doc().lines[line] - if (not skip_empty or line_text:find("%S")) then - doc():insert(line, 1, text) - end - end - doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap) -end - - -local function remove_from_start_of_selected_lines(text, skip_empty) - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - for line = line1, line2 do - local line_text = doc().lines[line] - if line_text:sub(1, #text) == text - and (not skip_empty or line_text:find("%S")) - then - doc():remove(line, 1, line, #text + 1) - end - end - doc():set_selection(line1, col1 - #text, line2, col2 - #text, swap) -end - - local function append_line_if_last_line(line) if line >= #doc().lines then doc():insert(line, math.huge, "\n") @@ -107,7 +80,7 @@ end -- and remove the appropriate amount of spaces (or a tab). local function indent_text(unindent) local text = get_indent_string() - local line1, col1, line2, col2, swap = doc():get_selection(true) + local line1, col1, line2, col2, swap = doc_multiline_selection(true) local _, se = doc().lines[line1]:find("^[ \t]+") local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) if unindent or doc():has_selection() or in_beginning_whitespace then @@ -286,19 +259,31 @@ local commands = { ["doc:toggle-line-comments"] = function() local comment = doc().syntax.comment if not comment then return end + local indentation = get_indent_string() local comment_text = comment .. " " - local line1, _, line2 = doc():get_selection(true) + local line1, _, line2 = doc_multiline_selection(true) local uncomment = true + local start_offset = math.huge for line = line1, line2 do local text = doc().lines[line] - if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then + local s = text:find("%S") + local cs, ce = text:find(comment_text, s, true) + if s and cs ~= s then uncomment = false + start_offset = math.min(start_offset, s) end end - if uncomment then - remove_from_start_of_selected_lines(comment_text, true) - else - insert_at_start_of_selected_lines(comment_text, true) + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") + if uncomment then + local cs, ce = text:find(comment_text, s, true) + if ce then + doc():remove(line, cs, line, ce + 1) + end + elseif s then + doc():insert(line, start_offset, comment_text) + end end end, @@ -346,9 +331,10 @@ local commands = { end, ["doc:save-as"] = function() + local last_doc = core.last_active_view and core.last_active_view.doc if doc().filename then core.command_view:set_text(doc().filename) - elseif core.last_active_view and core.last_active_view.doc then + elseif last_doc and last_doc.filename then local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$") core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP) end diff --git a/data/core/config.lua b/data/core/config.lua index 74ca3d2f..36a67add 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -3,7 +3,7 @@ local config = {} config.project_scan_rate = 5 config.fps = 60 config.max_log_items = 80 -config.message_timeout = 3 +config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE config.file_size_limit = 10 config.ignore_files = "^%." diff --git a/data/core/init.lua b/data/core/init.lua index 6d6183c2..6be03800 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -68,6 +68,7 @@ function core.set_project_dir(new_dir, change_project_fn) core.project_directories = {} core.add_project_directory(new_dir) core.project_files = {} + core.project_files_limit = false core.reschedule_project_scan() return true end @@ -95,6 +96,57 @@ local function strip_trailing_slash(filename) return filename end +local function compare_file(a, b) + return a.filename < b.filename +end + +-- "root" will by an absolute path without trailing '/' +-- "path" will be a path starting with '/' and without trailing '/' +-- or the empty string. +-- It will identifies a sub-path within "root. +-- The current path location will therefore always be: root .. path. +-- When recursing "root" will always be the same, only "path" will change. +-- Returns a list of file "items". In eash item the "filename" will be the +-- complete file path relative to "root" *without* the trailing '/'. +local function get_directory_files(root, path, t, recursive, begin_hook) + if begin_hook then begin_hook() end + local size_limit = config.file_size_limit * 10e5 + local all = system.list_dir(root .. path) or {} + local dirs, files = {}, {} + + local entries_count = 0 + local max_entries = config.max_project_files + for _, file in ipairs(all) do + if not common.match_pattern(file, config.ignore_files) then + local file = path .. PATHSEP .. file + local info = system.get_file_info(root .. file) + if info and info.size < size_limit then + info.filename = strip_leading_path(file) + table.insert(info.type == "dir" and dirs or files, info) + entries_count = entries_count + 1 + if recursive and entries_count > max_entries then return nil, entries_count end + end + end + end + + table.sort(dirs, compare_file) + for _, f in ipairs(dirs) do + table.insert(t, f) + if recursive and entries_count <= max_entries then + local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive) + entries_count = entries_count + subdir_count + f.scanned = true + end + end + + table.sort(files, compare_file) + for _, f in ipairs(files) do + table.insert(t, f) + end + + return t, entries_count +end + local function project_scan_thread() local function diff_files(a, b) if #a ~= #b then return true end @@ -106,71 +158,21 @@ local function project_scan_thread() end end - local function compare_file(a, b) - return a.filename < b.filename - end - - -- "root" will by an absolute path without trailing '/' - -- "path" will be a path starting with '/' and without trailing '/' - -- or the empty string. - -- It will identifies a sub-path within "root. - -- The current path location will therefore always be: root .. path. - -- When recursing "root" will always be the same, only "path" will change. - -- Returns a list of file "items". In eash item the "filename" will be the - -- complete file path relative to "root" *without* the trailing '/'. - local function get_files(root, path, t) - coroutine.yield() - t = t or {} - local size_limit = config.file_size_limit * 10e5 - local all = system.list_dir(root .. path) or {} - local dirs, files = {}, {} - - local entries_count = 0 - local max_entries = config.max_project_files - for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 - if entries_count > max_entries then break end - end - end - end - - table.sort(dirs, compare_file) - for _, f in ipairs(dirs) do - table.insert(t, f) - if entries_count <= max_entries then - local subdir_t, subdir_count = get_files(root, PATHSEP .. f.filename, t) - entries_count = entries_count + subdir_count - end - end - - table.sort(files, compare_file) - for _, f in ipairs(files) do - table.insert(t, f) - end - - return t, entries_count - end - while true do -- get project files and replace previous table if the new table is -- different - for i = 1, #core.project_directories do + local i = 1 + while not core.project_files_limit and i <= #core.project_directories do local dir = core.project_directories[i] - local t, entries_count = get_files(dir.name, "") + local t, entries_count = get_directory_files(dir.name, "", {}, true) if diff_files(dir.files, t) then if entries_count > config.max_project_files then + core.project_files_limit = true core.status_view:show_message("!", style.accent, - "Too many files in project directory: 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.") + "Too many files in project directory: stopped reading at ".. + config.max_project_files.." files. For more information see ".. + "usage.md at github.com/franko/lite-xl." + ) end dir.files = t core.redraw = true @@ -178,6 +180,7 @@ local function project_scan_thread() if dir.name == core.project_dir then core.project_files = dir.files end + i = i + 1 end -- wait for next scan @@ -186,6 +189,46 @@ local function project_scan_thread() end +function core.scan_project_folder(dirname, filename) + for _, dir in ipairs(core.project_directories) do + if dir.name == dirname then + for i, file in ipairs(dir.files) do + local file = dir.files[i] + if file.filename == filename then + if file.scanned then return end + local new_files = get_directory_files(dirname, PATHSEP .. filename, {}) + for j, new_file in ipairs(new_files) do + table.insert(dir.files, i + j, new_file) + end + file.scanned = true + return + end + end + end + end +end + + +local function find_project_files_co(root, path) + local size_limit = config.file_size_limit * 10e5 + local all = system.list_dir(root .. path) or {} + for _, file in ipairs(all) do + if not common.match_pattern(file, config.ignore_files) then + local file = path .. PATHSEP .. file + local info = system.get_file_info(root .. file) + if info and info.size < size_limit then + info.filename = strip_leading_path(file) + if info.type == "file" then + coroutine.yield(root, info) + else + find_project_files_co(root, PATHSEP .. info.filename) + end + end + end + end +end + + local function project_files_iter(state) local dir = core.project_directories[state.dir_index] state.file_index = state.file_index + 1 @@ -200,17 +243,27 @@ end function core.get_project_files() - local state = { dir_index = 1, file_index = 0 } - return project_files_iter, state + if core.project_files_limit then + return coroutine.wrap(function() + for _, dir in ipairs(core.project_directories) do + find_project_files_co(dir.name, "") + end + end) + else + local state = { dir_index = 1, file_index = 0 } + return project_files_iter, state + end end function core.project_files_number() - local n = 0 - for i = 1, #core.project_directories do - n = n + #core.project_directories[i].files + if not core.project_files_limit then + local n = 0 + for i = 1, #core.project_directories do + n = n + #core.project_directories[i].files + end + return n end - return n end @@ -270,7 +323,7 @@ local style = require "core.style" ------------------------------- Fonts ---------------------------------------- -- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Medium.ttf", 13 * SCALE) +-- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) -- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) -- -- font names used by lite: @@ -353,6 +406,20 @@ local function whitespace_replacements() end +local function reload_on_user_module_save() + -- auto-realod style when user's module is saved by overriding Doc:Save() + local doc_save = Doc.save + local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") + function Doc:save(filename) + doc_save(self) + if self.abs_filename == user_filename then + core.reload_module("core.style") + core.load_user_directory() + end + end +end + + function core.init() command = require "core.command" keymap = require "core.keymap" @@ -374,7 +441,7 @@ function core.init() local recent_projects, window_position, window_mode = load_session() if window_mode == "normal" then system.set_window_size(table.unpack(window_position)) - else + elseif window_mode == "maximized" then system.set_window_mode("maximized") end core.recent_projects = recent_projects @@ -495,6 +562,8 @@ function core.init() if item.text == "Exit" then os.exit(1) end end) end + + reload_on_user_module_save() end @@ -554,13 +623,19 @@ do end +-- DEPRECATED function core.doc_save_hooks = {} function core.add_save_hook(fn) + core.error("The function core.add_save_hook is deprecated." .. + " Modules should now directly override the Doc:save function.") core.doc_save_hooks[#core.doc_save_hooks + 1] = fn end +-- DEPRECATED function function core.on_doc_save(filename) + -- for backward compatibility in modules. Hooks are deprecated, the function Doc:save + -- should be directly overidded. for _, hook in ipairs(core.doc_save_hooks) do hook(filename) end @@ -1042,15 +1117,4 @@ function core.on_error(err) end -core.add_save_hook(function(filename) - local doc = core.active_view.doc - local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") - if doc and doc:is(Doc) and doc.abs_filename == user_filename then - core.reload_module("core.style") - core.load_user_directory() - end -end) - - - return core diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 7cdc78cb..8b3a91ae 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -272,6 +272,7 @@ end function Node:get_scroll_button_index(px, py) + if #self.views == 1 then return end for i = 1, 2 do local x, y, w, h = self:get_scroll_button_rect(i) if px >= x and px < x + w and py >= y and py < y + h then diff --git a/data/core/start.lua b/data/core/start.lua index 17e12bde..513fda22 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,5 @@ --- this file is used by lite-xl to setup the Lua environment --- when starting -VERSION = "1.16.10" +-- this file is used by lite-xl to setup the Lua environment when starting +VERSION = "1.16.11" MOD_VERSION = "1" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE diff --git a/data/core/style.lua b/data/core/style.lua index 7001cdc1..60df7c73 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -21,8 +21,8 @@ style.tab_width = common.round(170 * SCALE) -- -- On High DPI monitor or non RGB monitor you may consider using antialiasing grayscale instead. -- The antialiasing grayscale with full hinting is interesting for crisp font rendering. -style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Medium.ttf", 13 * SCALE) -style.big_font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Medium.ttf", 40 * SCALE) +style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) +style.big_font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 40 * SCALE) style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) style.icon_big_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 20 * SCALE, {antialiasing="grayscale", hinting="full"}) style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) diff --git a/data/fonts/FiraSans-Medium.ttf b/data/fonts/FiraSans-Medium.ttf deleted file mode 100644 index fb9c257c..00000000 Binary files a/data/fonts/FiraSans-Medium.ttf and /dev/null differ diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 63fe8a52..9e7ed93c 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -76,7 +76,7 @@ local function get_non_empty_lines(syntax, lines) end -local auto_detect_max_lines = 200 +local auto_detect_max_lines = 100 local function detect_indent_stat(doc) local stat = {} @@ -99,29 +99,11 @@ local function detect_indent_stat(doc) end -local doc_on_text_change = Doc.on_text_change -local adjust_threshold = 4 - -local current_on_text_change = nil - local function update_cache(doc) local type, size, score = detect_indent_stat(doc) - cache[doc] = { type = type, size = size, confirmed = (score >= adjust_threshold) } + local score_threshold = 4 + cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) } doc.indent_info = cache[doc] - if score < adjust_threshold and doc_on_text_change then - current_on_text_change = function(self, ...) - update_cache(self) - end - elseif score >= adjust_threshold and doc_on_text_change then - current_on_text_change = nil - end -end - -function Doc.on_text_change(...) - if current_on_text_change then - current_on_text_change(...) - end - doc_on_text_change(...) end @@ -129,6 +111,14 @@ local new = Doc.new function Doc:new(...) new(self, ...) update_cache(self) + if not cache[self].confirmed then + core.add_thread(function () + while not cache[self].confirmed do + update_cache(self) + coroutine.yield(1) + end + end, self) + end end local clean = Doc.clean diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 9b9c34b3..45399ed0 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -170,12 +170,17 @@ function ResultsView:draw() local ox, oy = self:get_content_offset() local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() - local per = self.last_file_idx / files_number + local per = files_number and self.last_file_idx / files_number or 1 local text if self.searching then - text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", - per * 100, self.last_file_idx, files_number, - #self.results, self.query) + if files_number then + text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", + per * 100, self.last_file_idx, files_number, + #self.results, self.query) + else + text = string.format("Searching (%d files, %d matches) for %q...", + self.last_file_idx, #self.results, self.query) + end else text = string.format("Found %d matches for %q", #self.results, self.query) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 73f4708c..8615343a 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -93,6 +93,13 @@ function TreeView:get_item_height() end +function TreeView:invalidate_cache(dirname) + for _, v in pairs(self.cache[dirname]) do + v.skip = nil + end +end + + function TreeView:check_cache() -- invalidate cache's skip values if project_files has changed for i = 1, #core.project_directories do @@ -102,9 +109,7 @@ function TreeView:check_cache() self.last[dir.name] = dir.files else if dir.files ~= last_files then - for _, v in pairs(self.cache[dir.name]) do - v.skip = nil - end + self:invalidate_cache(dir.name) self.last[dir.name] = dir.files end end @@ -208,17 +213,25 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) if caught then return end - if not self.hovered_item then + local hovered_item = self.hovered_item + if not hovered_item then return - elseif self.hovered_item.type == "dir" then + elseif hovered_item.type == "dir" then if keymap.modkeys["ctrl"] and button == "left" then - create_directory_in(self.hovered_item) + create_directory_in(hovered_item) else - self.hovered_item.expanded = not self.hovered_item.expanded + if core.project_files_limit and not hovered_item.expanded then + local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename + local index = string.find(abs_filename, filename, 1, true) + local dirname = string.sub(abs_filename, 1, index - 2) + core.scan_project_folder(dirname, filename) + self:invalidate_cache(dirname) + end + hovered_item.expanded = not hovered_item.expanded end else core.try(function() - local doc_filename = common.relative_path(core.project_dir, self.hovered_item.abs_filename) + local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) core.root_view:open_doc(core.open_doc(doc_filename)) end) end diff --git a/doc/usage.md b/doc/usage.md index d92ea707..90f6dc8d 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -71,6 +71,13 @@ The project module can be edited by running the `core:open-project-module` command — if the module does not exist for the current project when the command is run it will be created. +## Big directories +Often projects contain compiled, bundled or downloaded files which you don't want to edit. These files can be excluded from projects by configuring `config.ignore_files`. Such a configuration might look like `config.ignore_files = { "^%.", "node_modules" }`. This will exclude the `node_modules` folder and any file starting with `.`. You can add this to a user or project module. + +If a project has more files than the maximum (configured with `config.max_project_files`) lite-xl will switch to a different mode where files are lazily loaded. + +_Note: Because of lazy loading `core:find-file` will open `core:open-file` instead._ + ## Add directories to a project In addition to the project directories it is possible to add other directories