aboutsummaryrefslogtreecommitdiff
path: root/data
diff options
context:
space:
mode:
Diffstat (limited to 'data')
-rw-r--r--data/core/command.lua9
-rw-r--r--data/core/commands/core.lua21
-rw-r--r--data/core/commands/doc.lua80
-rw-r--r--data/core/commands/findreplace.lua79
-rw-r--r--data/core/commands/root.lua22
-rw-r--r--data/core/common.lua51
-rw-r--r--data/core/config.lua6
-rw-r--r--data/core/contextmenu.lua10
-rw-r--r--data/core/doc/highlighter.lua16
-rw-r--r--data/core/doc/init.lua49
-rw-r--r--data/core/doc/search.lua65
-rw-r--r--data/core/docview.lua100
-rw-r--r--data/core/init.lua544
-rw-r--r--data/core/keymap-macos.lua7
-rw-r--r--data/core/keymap.lua60
-rw-r--r--data/core/regex.lua4
-rw-r--r--data/core/rootview.lua81
-rw-r--r--data/core/start.lua5
-rw-r--r--data/core/statusview.lua6
-rw-r--r--data/core/syntax.lua4
-rw-r--r--data/core/tokenizer.lua12
-rw-r--r--data/core/view.lua8
-rw-r--r--data/plugins/autoreload.lua26
-rw-r--r--data/plugins/contextmenu.lua16
-rw-r--r--data/plugins/detectindent.lua34
-rw-r--r--data/plugins/drawwhitespace.lua16
-rw-r--r--data/plugins/language_c.lua1
-rw-r--r--data/plugins/language_cpp.lua1
-rw-r--r--data/plugins/language_css.lua1
-rw-r--r--data/plugins/language_html.lua1
-rw-r--r--data/plugins/language_js.lua5
-rw-r--r--data/plugins/language_lua.lua1
-rw-r--r--data/plugins/language_md.lua17
-rw-r--r--data/plugins/language_python.lua21
-rw-r--r--data/plugins/language_xml.lua1
-rw-r--r--data/plugins/lineguide.lua21
-rw-r--r--data/plugins/projectsearch.lua3
-rw-r--r--data/plugins/scale.lua17
-rw-r--r--data/plugins/treeview.lua84
39 files changed, 992 insertions, 513 deletions
diff --git a/data/core/command.lua b/data/core/command.lua
index 7915e16d..2cf851da 100644
--- a/data/core/command.lua
+++ b/data/core/command.lua
@@ -41,11 +41,14 @@ function command.get_all_valid()
return res
end
+function command.is_valid(name, ...)
+ return command.map[name] and command.map[name].predicate(...)
+end
-local function perform(name)
+local function perform(name, ...)
local cmd = command.map[name]
- if cmd and cmd.predicate() then
- cmd.perform()
+ if cmd and cmd.predicate(...) then
+ cmd.perform(...)
return true
end
return false
diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua
index 432ded89..ad0d4b10 100644
--- a/data/core/commands/core.lua
+++ b/data/core/commands/core.lua
@@ -6,10 +6,12 @@ local LogView = require "core.logview"
local fullscreen = false
+local restore_title_view = false
local function suggest_directory(text)
text = common.home_expand(text)
- return common.home_encode_list(text == "" and core.recent_projects or common.dir_path_suggest(text))
+ return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir)))
+ and core.recent_projects or common.dir_path_suggest(text))
end
command.add(nil, {
@@ -27,9 +29,12 @@ command.add(nil, {
["core:toggle-fullscreen"] = function()
fullscreen = not fullscreen
+ if fullscreen then
+ restore_title_view = core.title_view.visible
+ end
system.set_window_mode(fullscreen and "fullscreen" or "normal")
- core.show_title_bar(not fullscreen)
- core.title_view:configure_hit_test(not fullscreen)
+ core.show_title_bar(not fullscreen and restore_title_view)
+ core.title_view:configure_hit_test(not fullscreen and restore_title_view)
end,
["core:reload-module"] = function()
@@ -66,8 +71,8 @@ command.add(nil, {
end,
["core:find-file"] = function()
- if core.project_files_limit then
- return command.perform "core:open-file"
+ if not core.project_files_number() then
+ return command.perform "core:open-file"
end
local files = {}
for dir, item in core.get_project_files() do
@@ -149,7 +154,7 @@ command.add(nil, {
["core:change-project-folder"] = function()
local dirname = common.dirname(core.project_dir)
if dirname then
- core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
+ core.command_view:set_text(common.home_encode(dirname))
end
core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text))
@@ -166,7 +171,7 @@ command.add(nil, {
["core:open-project-folder"] = function()
local dirname = common.dirname(core.project_dir)
if dirname then
- core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
+ core.command_view:set_text(common.home_encode(dirname))
end
core.command_view:enter("Open Project", function(text, item)
text = common.home_expand(item and item.text or text)
@@ -191,8 +196,6 @@ command.add(nil, {
return
end
core.add_project_directory(system.absolute_path(text))
- -- TODO: add the name of directory to prioritize
- core.reschedule_project_scan()
end, suggest_directory)
end,
diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua
index b8ce2cb5..c3063f97 100644
--- a/data/core/commands/doc.lua
+++ b/data/core/commands/doc.lua
@@ -16,14 +16,6 @@ local function doc()
end
-local function get_indent_string()
- if config.tab_type == "hard" then
- return "\t"
- end
- return string.rep(" ", config.indent_size)
-end
-
-
local function doc_multiline_selections(sort)
local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
return function()
@@ -82,15 +74,17 @@ local function split_cursor(direction)
core.blink_reset()
end
-local commands = {
- ["doc:undo"] = function()
- doc():undo()
- end,
-
- ["doc:redo"] = function()
- doc():redo()
- end,
+local function set_cursor(x, y, snap_type)
+ local line, col = dv():resolve_screen_position(x, y)
+ doc():set_selection(line, col, line, col)
+ if snap_type == "word" or snap_type == "lines" then
+ command.perform("doc:select-" .. snap_type)
+ end
+ dv().mouse_selecting = { line, col, snap_type }
+ core.blink_reset()
+end
+local selection_commands = {
["doc:cut"] = function()
cut_or_copy(true)
end,
@@ -99,6 +93,21 @@ local commands = {
cut_or_copy(false)
end,
+ ["doc:select-none"] = function()
+ local line, col = doc():get_selection()
+ doc():set_selection(line, col)
+ end
+}
+
+local commands = {
+ ["doc:undo"] = function()
+ doc():undo()
+ end,
+
+ ["doc:redo"] = function()
+ doc():redo()
+ end,
+
["doc:paste"] = function()
local clipboard = system.get_clipboard()
-- If the clipboard has changed since our last look, use that instead
@@ -147,11 +156,12 @@ local commands = {
end,
["doc:backspace"] = function()
+ local _, indent_size = doc():get_indent_info()
for idx, line1, col1, line2, col2 in doc():get_selections() do
if line1 == line2 and col1 == col2 then
local text = doc():get_text(line1, 1, line1, col1)
- if #text >= config.indent_size and text:find("^ *$") then
- doc():delete_to_cursor(idx, 0, -config.indent_size)
+ if #text >= indent_size and text:find("^ *$") then
+ doc():delete_to_cursor(idx, 0, -indent_size)
return
end
end
@@ -163,11 +173,6 @@ local commands = {
doc():set_selection(1, 1, math.huge, math.huge)
end,
- ["doc:select-none"] = function()
- local line, col = doc():get_selection()
- doc():set_selection(line, col)
- end,
-
["doc:select-lines"] = function()
for idx, line1, _, line2 in doc():get_selections(true) do
append_line_if_last_line(line2)
@@ -261,7 +266,7 @@ local commands = {
["doc:toggle-line-comments"] = function()
local comment = doc().syntax.comment
if not comment then return end
- local indentation = get_indent_string()
+ local indentation = doc():get_indent_string()
local comment_text = comment .. " "
for idx, line1, _, line2 in doc_multiline_selections(true) do
local uncomment = true
@@ -388,6 +393,30 @@ local commands = {
os.remove(filename)
core.log("Removed \"%s\"", filename)
end,
+
+ ["doc:select-to-cursor"] = function(x, y, clicks)
+ local line1, col1 = select(3, doc():get_selection())
+ local line2, col2 = dv():resolve_screen_position(x, y)
+ dv().mouse_selecting = { line1, col1, nil }
+ doc():set_selection(line2, col2, line1, col1)
+ end,
+
+ ["doc:set-cursor"] = function(x, y)
+ set_cursor(x, y, "set")
+ end,
+
+ ["doc:set-cursor-word"] = function(x, y)
+ set_cursor(x, y, "word")
+ end,
+
+ ["doc:set-cursor-line"] = function(x, y, clicks)
+ set_cursor(x, y, "lines")
+ end,
+
+ ["doc:split-cursor"] = function(x, y, clicks)
+ local line, col = dv():resolve_screen_position(x, y)
+ doc():add_selection(line, col, line, col)
+ end,
["doc:create-cursor-previous-line"] = function()
split_cursor(-1)
@@ -447,3 +476,6 @@ commands["doc:move-to-next-char"] = function()
end
command.add("core.docview", commands)
+command.add(function()
+ return core.active_view:is(DocView) and core.active_view.doc:has_any_selection()
+end ,selection_commands)
diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua
index f8e8e45a..0801b745 100644
--- a/data/core/commands/findreplace.lua
+++ b/data/core/commands/findreplace.lua
@@ -7,15 +7,15 @@ local DocView = require "core.docview"
local CommandView = require "core.commandview"
local StatusView = require "core.statusview"
-local max_last_finds = 50
-local last_finds, last_view, last_fn, last_text, last_sel
+local last_view, last_fn, last_text, last_sel
local case_sensitive = config.find_case_sensitive or false
local find_regex = config.find_regex or false
local found_expression
local function doc()
- return core.active_view:is(DocView) and core.active_view.doc or last_view.doc
+ local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView)
+ return is_DocView and core.active_view.doc or (last_view and last_view.doc)
end
local function get_find_tooltip()
@@ -37,7 +37,7 @@ local function update_preview(sel, search_fn, text)
last_view:scroll_to_line(line2, true)
found_expression = true
else
- last_view.doc:set_selection(unpack(sel))
+ last_view.doc:set_selection(table.unpack(sel))
found_expression = false
end
end
@@ -53,8 +53,8 @@ end
local function find(label, search_fn)
- last_view, last_sel, last_finds = core.active_view,
- { core.active_view.doc:get_selection() }, {}
+ last_view, last_sel = core.active_view,
+ { core.active_view.doc:get_selection() }
local text = last_view.doc:get_text(unpack(last_sel))
found_expression = false
@@ -69,8 +69,8 @@ local function find(label, search_fn)
last_fn, last_text = search_fn, text
else
core.error("Couldn't find %q", text)
- last_view.doc:set_selection(unpack(last_sel))
- last_view:scroll_to_make_visible(unpack(last_sel))
+ last_view.doc:set_selection(table.unpack(last_sel))
+ last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end, function(text)
update_preview(last_sel, search_fn, text)
@@ -79,8 +79,8 @@ local function find(label, search_fn)
end, function(explicit)
core.status_view:remove_tooltip()
if explicit then
- last_view.doc:set_selection(unpack(last_sel))
- last_view:scroll_to_make_visible(unpack(last_sel))
+ last_view.doc:set_selection(table.unpack(last_sel))
+ last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end)
end
@@ -117,7 +117,7 @@ local function has_selection()
end
local function has_unique_selection()
- if not core.active_view:is(DocView) then return false end
+ if not doc() then return false end
local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end
@@ -142,7 +142,7 @@ local function is_in_any_selection(line, col)
return false
end
-local function select_next(all)
+local function select_add_next(all)
local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
local text = doc():get_text(l1, c1, l2, c2)
@@ -161,21 +161,28 @@ local function select_next(all)
end
end
-command.add(has_unique_selection, {
- ["find-replace:select-next"] = function()
- local l1, c1, l2, c2 = doc():get_selection(true)
- local text = doc():get_text(l1, c1, l2, c2)
+local function select_next(reverse)
+ local l1, c1, l2, c2 = doc():get_selection(true)
+ local text = doc():get_text(l1, c1, l2, c2)
+ if reverse then
+ l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
+ else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
- if l2 then doc():set_selection(l2, c2, l1, c1) end
- end,
- ["find-replace:select-add-next"] = function() select_next(false) end,
- ["find-replace:select-add-all"] = function() select_next(true) end
+ end
+ if l2 then doc():set_selection(l2, c2, l1, c1) end
+end
+
+command.add(has_unique_selection, {
+ ["find-replace:select-next"] = select_next,
+ ["find-replace:select-previous"] = function() select_next(true) end,
+ ["find-replace:select-add-next"] = select_add_next,
+ ["find-replace:select-add-all"] = function() select_add_next(true) end
})
command.add("core.docview", {
["find-replace:find"] = function()
- find("Find Text", function(doc, line, col, text, case_sensitive, find_regex)
- local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex }
+ find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse)
+ local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse }
return search.find(doc, line, col, text, opt)
end)
end,
@@ -221,29 +228,29 @@ command.add(valid_for_finding, {
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
- local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex)
+ local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
if line1 then
- if last_view.doc ~= doc() then
- last_finds = {}
- end
- if #last_finds >= max_last_finds then
- table.remove(last_finds, 1)
- end
- table.insert(last_finds, { sl1, sc1, sl2, sc2 })
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
+ else
+ core.error("Couldn't find %q", last_text)
end
end
end,
["find-replace:previous-find"] = function()
- local sel = table.remove(last_finds)
- if not sel or doc() ~= last_view.doc then
- core.error("No previous finds")
- return
+ if not last_fn then
+ core.error("No find to continue from")
+ else
+ local sl1, sc1, sl2, sc2 = doc():get_selection(true)
+ local line1, col1, line2, col2 = last_fn(doc(), sl1, sc1, last_text, case_sensitive, find_regex, true)
+ if line1 then
+ doc():set_selection(line2, col2, line1, col1)
+ last_view:scroll_to_line(line2, true)
+ else
+ core.error("Couldn't find %q", last_text)
+ end
end
- doc():set_selection(table.unpack(sel))
- last_view:scroll_to_line(sel[3], true)
end,
})
diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua
index e41c723d..5bf18390 100644
--- a/data/core/commands/root.lua
+++ b/data/core/commands/root.lua
@@ -3,6 +3,7 @@ local style = require "core.style"
local DocView = require "core.docview"
local command = require "core.command"
local common = require "core.common"
+local config = require "core.config"
local t = {
@@ -63,7 +64,7 @@ local t = {
table.insert(node.views, idx + 1, core.active_view)
end
end,
-
+
["root:shrink"] = function()
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node)
@@ -76,7 +77,7 @@ local t = {
local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and 0.1 or -0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
- end,
+ end
}
@@ -112,7 +113,8 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end
local node = core.root_view.root_node:get_child_overlapping_point(x, y)
- if not node:get_locked_size() then
+ local sx, sy = node:get_locked_size()
+ if not sx and not sy then
core.set_active_view(node.active_view)
end
end
@@ -120,5 +122,17 @@ end
command.add(function()
local node = core.root_view:get_active_node()
- return not node:get_locked_size()
+ local sx, sy = node:get_locked_size()
+ return not sx and not sy
end, t)
+
+command.add(nil, {
+ ["root:scroll"] = function(delta)
+ local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view
+ if view and view.scrollable then
+ view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll
+ return true
+ end
+ return false
+ end
+})
diff --git a/data/core/common.lua b/data/core/common.lua
index 1a1b22cd..ab21b758 100644
--- a/data/core/common.lua
+++ b/data/core/common.lua
@@ -1,8 +1,8 @@
local common = {}
-function common.is_utf8_cont(char)
- local byte = char:byte()
+function common.is_utf8_cont(s, offset)
+ local byte = s:byte(offset or 1)
return byte >= 0x80 and byte < 0xc0
end
@@ -280,24 +280,61 @@ local function split_on_slash(s, sep_pattern)
end
+-- The filename argument given to the function is supposed to
+-- come from system.absolute_path and as such should be an
+-- absolute path without . or .. elements.
+-- This function exists because on Windows the drive letter returned
+-- by system.absolute_path is sometimes with a lower case and sometimes
+-- with an upper case to we normalize to upper case.
+function common.normalize_volume(filename)
+ if not filename then return end
+ if PATHSEP == '\\' then
+ local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
+ if drive then
+ return drive:upper() .. rem
+ end
+ end
+ return filename
+end
+
+
function common.normalize_path(filename)
if not filename then return end
+ local volume
if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
- local drive, rem = filename:match('^([a-zA-Z])(:.*)')
- filename = drive and drive:upper() .. rem or filename
+ local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
+ if drive then
+ volume, filename = drive:upper(), rem
+ else
+ drive, rem = filename:match('^(\\\\[^\\]+\\[^\\]+\\)(.*)')
+ if drive then
+ volume, filename = drive, rem
+ end
+ end
+ else
+ local relpath = filename:match('^/(.+)')
+ if relpath then
+ volume, filename = "/", relpath
+ end
end
local parts = split_on_slash(filename, PATHSEP)
local accu = {}
for _, part in ipairs(parts) do
- if part == '..' and #accu > 0 and accu[#accu] ~= ".." then
- table.remove(accu)
+ if part == '..' then
+ if #accu > 0 and accu[#accu] ~= ".." then
+ table.remove(accu)
+ elseif volume then
+ error("invalid path " .. volume .. filename)
+ else
+ table.insert(accu, part)
+ end
elseif part ~= '.' then
table.insert(accu, part)
end
end
local npath = table.concat(accu, PATHSEP)
- return npath == "" and PATHSEP or npath
+ return (volume or "") .. (npath == "" and PATHSEP or npath)
end
diff --git a/data/core/config.lua b/data/core/config.lua
index 7cf23925..71e83994 100644
--- a/data/core/config.lua
+++ b/data/core/config.lua
@@ -1,6 +1,5 @@
local config = {}
-config.project_scan_rate = 5
config.fps = 60
config.max_log_items = 80
config.message_timeout = 5
@@ -12,8 +11,8 @@ config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3
config.max_undos = 10000
-config.max_tabs = 10
-config.always_show_tabs = false
+config.max_tabs = 8
+config.always_show_tabs = true
config.highlight_current_line = true
config.line_height = 1.2
config.indent_size = 2
@@ -28,6 +27,7 @@ config.disable_blink = false
config.draw_whitespace = false
config.borderless = false
config.tab_close_button = true
+config.max_clicks = 3
-- Disable plugin loading setting to false the config entry
-- of the same name.
diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua
index 36247597..9db35bb3 100644
--- a/data/core/contextmenu.lua
+++ b/data/core/contextmenu.lua
@@ -49,7 +49,7 @@ function ContextMenu:register(predicate, items)
local width, height = 0, 0 --precalculate the size of context menu
for i, item in ipairs(items) do
if item ~= DIVIDER then
- item.info = keymap.reverse_map[item.command]
+ item.info = keymap.get_binding(item.command)
end
local lw, lh = get_item_size(item)
width = math.max(width, lw)
@@ -66,9 +66,13 @@ function ContextMenu:show(x, y)
for _, items in ipairs(self.itemset) do
if items.predicate(x, y) then
items_list.width = math.max(items_list.width, items.items.width)
- items_list.height = items_list.height + items.items.height
+ items_list.height = items_list.height
for _, subitems in ipairs(items.items) do
- table.insert(items_list, subitems)
+ if not subitems.command or command.is_valid(subitems.command) then
+ local lw, lh = get_item_size(subitems)
+ items_list.height = items_list.height + lh
+ table.insert(items_list, subitems)
+ end
end
end
end
diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua
index 4cb703da..9ba7b634 100644
--- a/data/core/doc/highlighter.lua
+++ b/data/core/doc/highlighter.lua
@@ -1,4 +1,5 @@
local core = require "core"
+local common = require "core.common"
local config = require "core.config"
local tokenizer = require "core.tokenizer"
local Object = require "core.object"
@@ -40,6 +41,13 @@ end
function Highlighter:reset()
self.lines = {}
+ self:soft_reset()
+end
+
+function Highlighter:soft_reset()
+ for i=1,#self.lines do
+ self.lines[i] = false
+ end
self.first_invalid_line = 1
self.max_wanted_line = 0
end
@@ -51,16 +59,16 @@ end
function Highlighter:insert_notify(line, n)
self:invalidate(line)
+ local blanks = { }
for i = 1, n do
- table.insert(self.lines, line, nil)
+ blanks[i] = false
end
+ common.splice(self.lines, line, 0, blanks)
end
function Highlighter:remove_notify(line, n)
self:invalidate(line)
- for i = 1, n do
- table.remove(self.lines, line)
- end
+ common.splice(self.lines, line, n)
end
diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua
index 920e2669..f324b6d3 100644
--- a/data/core/doc/init.lua
+++ b/data/core/doc/init.lua
@@ -47,7 +47,7 @@ function Doc:reset_syntax()
local syn = syntax.get(self.filename or "", header)
if self.syntax ~= syn then
self.syntax = syn
- self.highlighter:reset()
+ self.highlighter:soft_reset()
end
end
@@ -62,12 +62,15 @@ function Doc:load(filename)
local fp = assert( io.open(filename, "rb") )
self:reset()
self.lines = {}
+ local i = 1
for line in fp:lines() do
if line:byte(-1) == 13 then
line = line:sub(1, -2)
self.crlf = true
end
table.insert(self.lines, line .. "\n")
+ self.highlighter.lines[i] = false
+ i = i + 1
end
if #self.lines == 0 then
table.insert(self.lines, "\n")
@@ -115,6 +118,14 @@ function Doc:clean()
end
+function Doc:get_indent_info()
+ if not self.indent_info then return config.tab_type, config.indent_size, false end
+ return self.indent_info.type or config.tab_type,
+ self.indent_info.size or config.indent_size,
+ self.indent_info.confirmed
+end
+
+
function Doc:get_change_id()
return self.undo_stack.idx
end
@@ -146,6 +157,13 @@ function Doc:has_selection()
return line1 ~= line2 or col1 ~= col2
end
+function Doc:has_any_selection()
+ for idx, line1, col1, line2, col2 in self:get_selections() do
+ if line1 ~= line2 or col1 ~= col2 then return true end
+ end
+ return false
+end
+
function Doc:sanitize_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do
self:set_selections(idx, line1, col1, line2, col2)
@@ -202,9 +220,9 @@ local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then
- return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4))
+ return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4))
else
- return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4)
+ return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4)
end
end
@@ -305,7 +323,8 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then
- self.selections = { unpack(cmd) }
+ self.selections = { table.unpack(cmd) }
+ self:sanitize_selection()
end
modified = modified or (cmd.type ~= "selection")
@@ -348,7 +367,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
-- push undo
local line2, col2 = self:position_offset(line, col, #text)
- push_undo(undo_stack, time, "selection", unpack(self.selections))
+ push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds
@@ -360,7 +379,7 @@ end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo
local text = self:get_text(line1, col1, line2, col2)
- push_undo(undo_stack, time, "selection", unpack(self.selections))
+ push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text
@@ -486,19 +505,21 @@ end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
-local function get_indent_string()
- if config.tab_type == "hard" then
+function Doc:get_indent_string()
+ local indent_type, indent_size = self:get_indent_info()
+ if indent_type == "hard" then
return "\t"
end
- return string.rep(" ", config.indent_size)
+ return string.rep(" ", indent_size)
end
-- returns the size of the original indent, and the indent
-- in your config format, rounded either up or down
-local function get_line_indent(line, rnd_up)
+function Doc:get_line_indent(line, rnd_up)
local _, e = line:find("^[ \t]+")
- local soft_tab = string.rep(" ", config.indent_size)
- if config.tab_type == "hard" then
+ local indent_type, indent_size = self:get_indent_info()
+ local soft_tab = string.rep(" ", indent_size)
+ if indent_type == "hard" then
local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or ""
return e, indent:gsub(" +", rnd_up and "\t" or "")
else
@@ -520,14 +541,14 @@ end
-- * if you are unindenting, the cursor will jump to the start of the line,
-- and remove the appropriate amount of spaces (or a tab).
function Doc:indent_text(unindent, line1, col1, line2, col2)
- local text = get_indent_string()
+ local text = self:get_indent_string()
local _, se = self.lines[line1]:find("^[ \t]+")
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
local has_selection = line1 ~= line2 or col1 ~= col2
if unindent or has_selection or in_beginning_whitespace then
local l1d, l2d = #self.lines[line1], #self.lines[line2]
for line = line1, line2 do
- local e, rnded = get_line_indent(self.lines[line], unindent)
+ local e, rnded = self:get_line_indent(self.lines[line], unindent)
self:remove(line, 1, line, (e or 0) + 1)
self:insert(line, 1,
unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua
index 04090673..8395769a 100644
--- a/data/core/doc/search.lua
+++ b/data/core/doc/search.lua
@@ -22,37 +22,62 @@ local function init_args(doc, line, col, text, opt)
return doc, line, col, text, opt
end
+-- This function is needed to uniform the behavior of
+-- `regex:cmatch` and `string.find`.
+local function regex_func(text, re, index, _)
+ local s, e = re:cmatch(text, index)
+ return s, e and e - 1
+end
+
+local function rfind(func, text, pattern, index, plain)
+ local s, e = func(text, pattern, 1, plain)
+ local last_s, last_e
+ if index < 0 then index = #text - index + 1 end
+ while e and e <= index do
+ last_s, last_e = s, e
+ s, e = func(text, pattern, s + 1, plain)
+ end
+ return last_s, last_e
+end
+
function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
-
- local re
+ local plain = not opt.pattern
+ local pattern = text
+ local search_func = string.find
if opt.regex then
- re = regex.compile(text, opt.no_case and "i" or "")
+ pattern = regex.compile(text, opt.no_case and "i" or "")
+ search_func = regex_func
end
- for line = line, #doc.lines do
+ local start, finish, step = line, #doc.lines, 1
+ if opt.reverse then
+ start, finish, step = line, 1, -1
+ end
+ for line = start, finish, step do
local line_text = doc.lines[line]
- if opt.regex then
- local s, e = re:cmatch(line_text, col)
- if s then
- return line, s, line, e
- end
- col = 1
+ if opt.no_case and not opt.regex then
+ line_text = line_text:lower()
+ end
+ local s, e
+ if opt.reverse then
+ s, e = rfind(search_func, line_text, pattern, col - 1, plain)
else
- if opt.no_case then
- line_text = line_text:lower()
- end
- local s, e = line_text:find(text, col, true)
- if s then
- return line, s, line, e + 1
- end
- col = 1
+ s, e = search_func(line_text, pattern, col, plain)
+ end
+ if s then
+ return line, s, line, e + 1
end
+ col = opt.reverse and -1 or 1
end
if opt.wrap then
- opt = { no_case = opt.no_case, regex = opt.regex }
- return search.find(doc, 1, 1, text, opt)
+ opt = { no_case = opt.no_case, regex = opt.regex, reverse = opt.reverse }
+ if opt.reverse then
+ return search.find(doc, #doc.lines, #doc.lines[#doc.lines], text, opt)
+ else
+ return search.find(doc, 1, 1, text, opt)
+ end
end
end
diff --git a/data/core/docview.lua b/data/core/docview.lua
index 4e95c359..a4587670 100644
--- a/data/core/docview.lua
+++ b/data/core/docview.lua
@@ -225,51 +225,6 @@ function DocView:scroll_to_make_visible(line, col)
end
-local function mouse_selection(doc, clicks, line1, col1, line2, col2)
- local swap = line2 < line1 or line2 == line1 and col2 <= col1
- if swap then
- line1, col1, line2, col2 = line2, col2, line1, col1
- end
- if clicks % 4 == 2 then
- line1, col1 = translate.start_of_word(doc, line1, col1)
- line2, col2 = translate.end_of_word(doc, line2, col2)
- elseif clicks % 4 == 3 then
- if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then
- doc:insert(math.huge, math.huge, "\n")
- end
- line1, col1, line2, col2 = line1, 1, line2 + 1, 1
- end
- if swap then
- return line2, col2, line1, col1
- end
- return line1, col1, line2, col2
-end
-
-
-function DocView:on_mouse_pressed(button, x, y, clicks)
- local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
- if caught then
- return
- end
- if keymap.modkeys["shift"] then
- if clicks % 2 == 1 then
- local line1, col1 = select(3, self.doc:get_selection())
- local line2, col2 = self:resolve_screen_position(x, y)
- self.doc:set_selection(line2, col2, line1, col1)
- end
- else
- local line, col = self:resolve_screen_position(x, y)
- if keymap.modkeys["ctrl"] then
- self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col))
- else
- self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
- end
- self.mouse_selecting = { line, col, clicks = clicks }
- end
- core.blink_reset()
-end
-
-
function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...)
@@ -281,8 +236,7 @@ function DocView:on_mouse_moved(x, y, ...)
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
- local l2, c2 = table.unpack(self.mouse_selecting)
- local clicks = self.mouse_selecting.clicks
+ local l2, c2, snap_type = table.unpack(self.mouse_selecting)
if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { }
@@ -290,12 +244,33 @@ function DocView:on_mouse_moved(x, y, ...)
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end
else
- self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2))
+ if snap_type then
+ l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2)
+ end
+ self.doc:set_selection(l1, c1, l2, c2)
end
end
end
+function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
+ local swap = line2 < line1 or line2 == line1 and col2 <= col1
+ if swap then
+ line1, col1, line2, col2 = line2, col2, line1, col1
+ end
+ if snap_type == "word" then
+ line1, col1 = translate.start_of_word(doc, line1, col1)
+ line2, col2 = translate.end_of_word(doc, line2, col2)
+ elseif snap_type == "lines" then
+ col1, col2 = 1, math.huge
+ end
+ if swap then
+ return line2, col2, line1, col1
+ end
+ return line1, col1, line2, col2
+end
+
+
function DocView:on_mouse_released(button)
DocView.super.on_mouse_released(self, button)
self.mouse_selecting = nil
@@ -354,6 +329,18 @@ function DocView:draw_caret(x, y)
end
function DocView:draw_line_body(idx, x, y)
+ -- draw highlight if any selection ends on this line
+ local draw_highlight = false
+ for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
+ if line1 == idx then
+ draw_highlight = true
+ break
+ end
+ end
+ if draw_highlight and config.highlight_current_line and core.active_view == self then
+ self:draw_line_highlight(x + self.scroll.x, y)
+ end
+
-- draw selection if it overlaps this line
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
if idx >= line1 and idx <= line2 then
@@ -368,15 +355,6 @@ function DocView:draw_line_body(idx, x, y)
end
end
end
- local draw_highlight = nil
- for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
- -- draw line highlight if caret is on this line
- if draw_highlight ~= false and config.highlight_current_line
- and line1 == idx and core.active_view == self then
- draw_highlight = (line1 == line2 and col1 == col2)
- end
- end
- if draw_highlight then self:draw_line_highlight(x + self.scroll.x, y) end
-- draw line's text
self:draw_line_text(idx, x, y)
@@ -417,8 +395,8 @@ end
function DocView:draw()
self:draw_background(style.background)
-
- self:get_font():set_tab_size(config.indent_size)
+ local _, indent_size = self.doc:get_indent_info()
+ self:get_font():set_tab_size(indent_size)
local minline, maxline = self:get_visible_line_range()
local lh = self:get_line_height()
@@ -432,7 +410,9 @@ function DocView:draw()
local pos = self.position
x, y = self:get_line_screen_position(minline)
- core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
+ -- the clip below ensure we don't write on the gutter region. On the
+ -- right side it is redundant with the Node's clip.
+ core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y)
for i = minline, maxline do
self:draw_line_body(i, x, y)
y = y + lh
diff --git a/data/core/init.lua b/data/core/init.lua
index 6ef0ab7e..aadccc66 100644
--- a/data/core/init.lua
+++ b/data/core/init.lua
@@ -36,7 +36,7 @@ end
local function update_recents_project(action, dir_path_abs)
- local dirname = common.normalize_path(dir_path_abs)
+ local dirname = common.normalize_volume(dir_path_abs)
if not dirname then return end
local recents = core.recent_projects
local n = #recents
@@ -52,23 +52,13 @@ local function update_recents_project(action, dir_path_abs)
end
-function core.reschedule_project_scan()
- if core.project_scan_thread_id then
- core.threads[core.project_scan_thread_id].wake = 0
- end
-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 = common.normalize_path(new_dir)
+ core.project_dir = common.normalize_volume(new_dir)
core.project_directories = {}
core.add_project_directory(new_dir)
- core.project_files = {}
- core.project_files_limit = false
- core.reschedule_project_scan()
return true
end
return false
@@ -102,6 +92,29 @@ local function compare_file(a, b)
return a.filename < b.filename
end
+
+-- compute a file's info entry completed with "filename" to be used
+-- in project scan or falsy if it shouldn't appear in the list.
+local function get_project_file_info(root, file)
+ local info = system.get_file_info(root .. file)
+ if info then
+ info.filename = strip_leading_path(file)
+ return (info.size < config.file_size_limit * 1e6 and
+ not common.match_pattern(common.basename(info.filename), config.ignore_files)
+ and info)
+ end
+end
+
+
+-- Predicate function to inhibit directory recursion in get_directory_files
+-- based on a time limit and the number of files.
+local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
+ local n_limit = entries_count <= config.max_project_files
+ local t_limit = t_elapsed < 20 / config.fps
+ return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
+end
+
+
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string.
@@ -110,34 +123,31 @@ end
-- 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)
+local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end
- local size_limit = config.file_size_limit * 10e5
+ local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
+ local t_elapsed = system.get_time() - t0
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
+ local info = get_project_file_info(root, path .. PATHSEP .. file)
+ if info then
+ table.insert(info.type == "dir" and dirs or files, info)
+ entries_count = entries_count + 1
end
end
+ local recurse_complete = true
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
+ if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
+ local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook)
+ recurse_complete = recurse_complete and complete
+ entries_count = n
+ else
+ recurse_complete = false
end
end
@@ -146,135 +156,319 @@ local function get_directory_files(root, path, t, recursive, begin_hook)
table.insert(t, f)
end
- return t, entries_count
+ return t, recurse_complete, entries_count
end
-local function project_scan_thread()
- local function diff_files(a, b)
- if #a ~= #b then return true end
- for i, v in ipairs(a) do
- if b[i].filename ~= v.filename
- or b[i].modified ~= v.modified then
- return true
- end
+
+function core.project_subdir_set_show(dir, filename, show)
+ dir.shown_subdir[filename] = show
+ if dir.files_limit and PLATFORM == "Linux" then
+ local fullpath = dir.name .. PATHSEP .. filename
+ local watch_fn = show and system.watch_dir_add or system.watch_dir_rm
+ local success = watch_fn(dir.watch_id, fullpath)
+ if not success then
+ core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
end
end
+end
- while true do
- -- get project files and replace previous table if the new table is
- -- different
- local i = 1
- while not core.project_files_limit and i <= #core.project_directories do
- local dir = core.project_directories[i]
- local t, entries_count = get_directory_files(dir.name, "", {}, true)
- if diff_files(dir.files, t) then
- if entries_count > config.max_project_files then
- core.project_files_limit = true
- core.status_view:show_message("!", style.accent,
- "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
- end
- if dir.name == core.project_dir then
- core.project_files = dir.files
- end
- i = i + 1
+
+function core.project_subdir_is_shown(dir, filename)
+ return not dir.files_limit or dir.shown_subdir[filename]
+end
+
+
+local function show_max_files_warning(dir)
+ local message = dir.slow_filesystem and
+ "Filesystem is too slow: project files will not be indexed." or
+ "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."
+ core.status_view:show_message("!", style.accent, message)
+end
+
+
+local function file_search(files, info)
+ local filename, type = info.filename, info.type
+ local inf, sup = 1, #files
+ while sup - inf > 8 do
+ local curr = math.floor((inf + sup) / 2)
+ if system.path_compare(filename, type, files[curr].filename, files[curr].type) then
+ sup = curr - 1
+ else
+ inf = curr
end
+ end
+ while inf <= sup and not system.path_compare(filename, type, files[inf].filename, files[inf].type) do
+ if files[inf].filename == filename then
+ return inf, true
+ end
+ inf = inf + 1
+ end
+ return inf, false
+end
- -- wait for next scan
- coroutine.yield(config.project_scan_rate)
+
+local function project_scan_add_entry(dir, fileinfo)
+ local index, match = file_search(dir.files, fileinfo)
+ if not match then
+ table.insert(dir.files, index, fileinfo)
+ dir.is_dirty = true
end
end
-function core.is_project_folder(dirname)
- for _, dir in ipairs(core.project_directories) do
- if dir.name == dirname then
- return true
+local function files_info_equal(a, b)
+ return a.filename == b.filename and a.type == b.type
+end
+
+-- for "a" inclusive from i1 + 1 and i1 + n
+local function files_list_match(a, i1, n, b)
+ if n ~= #b then return false end
+ for i = 1, n do
+ if not files_info_equal(a[i1 + i], b[i]) then
+ return false
end
end
- return false
+ return true
end
+-- arguments like for files_list_match
+local function files_list_replace(as, i1, n, bs)
+ local m = #bs
+ local i, j = 1, 1
+ while i <= m or i <= n do
+ local a, b = as[i1 + i], bs[j]
+ if i > n or (j <= m and not files_info_equal(a, b) and
+ not system.path_compare(a.filename, a.type, b.filename, b.type))
+ then
+ table.insert(as, i1 + i, b)
+ i, j, n = i + 1, j + 1, n + 1
+ elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then
+ table.remove(as, i1 + i)
+ n = n - 1
+ else
+ i, j = i + 1, j + 1
+ end
+ end
+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
+local function project_subdir_bounds(dir, filename)
+ local index, n = 0, #dir.files
+ for i, file in ipairs(dir.files) do
+ local file = dir.files[i]
+ if file.filename == filename then
+ index, n = i, #dir.files - i
+ for j = 1, #dir.files - i do
+ if not common.path_belongs_to(dir.files[i + j].filename, filename) then
+ n = j - 1
+ break
end
end
+ return index, n, file
end
end
end
+local function rescan_project_subdir(dir, filename_rooted)
+ local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield)
+ local index, n = 0, #dir.files
+ if filename_rooted ~= "" then
+ local filename = strip_leading_path(filename_rooted)
+ index, n = project_subdir_bounds(dir, filename)
+ end
+
+ if not files_list_match(dir.files, index, n, new_files) then
+ files_list_replace(dir.files, index, n, new_files)
+ dir.is_dirty = true
+ return true
+ end
+end
+
+
+local function add_dir_scan_thread(dir)
+ core.add_thread(function()
+ while true do
+ local has_changes = rescan_project_subdir(dir, "")
+ if has_changes then
+ core.redraw = true -- we run without an event, from a thread
+ end
+ coroutine.yield(5)
+ end
+ end)
+end
-local function find_project_files_co(root, path)
- local size_limit = config.file_size_limit * 10e5
+-- Populate a project folder top directory by scanning the filesystem.
+local function scan_project_folder(index)
+ local dir = core.project_directories[index]
+ if PLATFORM == "Linux" then
+ local fstype = system.get_fs_type(dir.name)
+ dir.force_rescan = (fstype == "nfs" or fstype == "fuse")
+ end
+ local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred)
+ if not complete then
+ dir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
+ dir.files_limit = true
+ if not dir.force_rescan then
+ -- Watch non-recursively on Linux only.
+ -- The reason is recursively watching with dmon on linux
+ -- doesn't work on very large directories.
+ dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux")
+ end
+ if core.status_view then -- May be not yet initialized.
+ show_max_files_warning(dir)
+ end
+ else
+ if not dir.force_rescan then
+ dir.watch_id = system.watch_dir(dir.name, true)
+ end
+ end
+ dir.files = t
+ if dir.force_rescan then
+ add_dir_scan_thread(dir)
+ else
+ core.dir_rescan_add_job(dir, ".")
+ end
+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 = common.normalize_volume(path)
+ local dir = {
+ name = path,
+ item = {filename = common.basename(path), type = "dir", topdir = true},
+ files_limit = false,
+ is_dirty = true,
+ shown_subdir = {},
+ }
+ table.insert(core.project_directories, dir)
+ scan_project_folder(#core.project_directories)
+ if path == core.project_dir then
+ core.project_files = dir.files
+ end
+ core.redraw = true
+end
+
+
+function core.update_project_subdir(dir, filename, expanded)
+ local index, n, file = project_subdir_bounds(dir, filename)
+ if index then
+ local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
+ files_list_replace(dir.files, index, n, new_files)
+ dir.is_dirty = true
+ return true
+ end
+end
+
+
+-- Find files and directories recursively reading from the filesystem.
+-- Filter files and yields file's directory and info table. This latter
+-- is filled to be like required by project directories "files" list.
+local function find_files_rec(root, path)
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
+ local file = path .. PATHSEP .. file
+ local info = system.get_file_info(root .. file)
+ if info then
+ info.filename = strip_leading_path(file)
+ if info.type == "file" then
+ coroutine.yield(root, info)
+ else
+ find_files_rec(root, PATHSEP .. info.filename)
end
end
end
end
+-- Iterator function to list all project files
local function project_files_iter(state)
local dir = core.project_directories[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]
+ if state.co then
+ -- We have a coroutine to fetch for files, use the coroutine.
+ -- Used for directories that exceeds the files nuumber limit.
+ local ok, name, file = coroutine.resume(state.co, dir.name, "")
+ if ok and name then
+ return name, file
+ else
+ -- The coroutine terminated, increment file/dir counter to scan
+ -- next project directory.
+ state.co = false
+ state.file_index = 1
+ state.dir_index = state.dir_index + 1
+ dir = core.project_directories[state.dir_index]
+ end
+ else
+ -- Increase file/dir counter
+ 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]
+ end
end
if not dir then return end
+ if dir.files_limit then
+ -- The current project directory is files limited: create a couroutine
+ -- to read files from the filesystem.
+ state.co = coroutine.create(find_files_rec)
+ return project_files_iter(state)
+ end
return dir.name, dir.files[state.file_index]
end
function core.get_project_files()
- 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
+ local state = { dir_index = 1, file_index = 0 }
+ return project_files_iter, state
end
function core.project_files_number()
- if not core.project_files_limit then
- local n = 0
- for i = 1, #core.project_directories do
- n = n + #core.project_directories[i].files
+ local n = 0
+ for i = 1, #core.project_directories do
+ if core.project_directories[i].files_limit then return end
+ n = n + #core.project_directories[i].files
+ end
+ return n
+end
+
+
+local function project_dir_by_watch_id(watch_id)
+ for i = 1, #core.project_directories do
+ if core.project_directories[i].watch_id == watch_id then
+ return core.project_directories[i]
+ end
+ end
+end
+
+
+local function project_scan_remove_file(dir, filepath)
+ local fileinfo = { filename = filepath }
+ for _, filetype in ipairs {"dir", "file"} do
+ fileinfo.type = filetype
+ local index, match = file_search(dir.files, fileinfo)
+ if match then
+ table.remove(dir.files, index)
+ dir.is_dirty = true
+ return
end
- return n
+ end
+end
+
+
+local function project_scan_add_file(dir, filepath)
+ for fragment in string.gmatch(filepath, "([^/\\]+)") do
+ if common.match_pattern(fragment, config.ignore_files) then
+ return
+ end
+ end
+ local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath)
+ if fileinfo then
+ project_scan_add_entry(dir, fileinfo)
end
end
@@ -371,19 +565,6 @@ function core.load_user_directory()
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 = common.normalize_path(path)
- table.insert(core.project_directories, {
- name = path,
- item = {filename = common.basename(path), type = "dir", topdir = true},
- files = {}
- })
-end
-
-
function core.remove_project_directory(path)
-- skip the fist directory because it is the project's directory
for i = 2, #core.project_directories do
@@ -422,9 +603,9 @@ function core.init()
Doc = require "core.doc"
if PATHSEP == '\\' then
- USERDIR = common.normalize_path(USERDIR)
- DATADIR = common.normalize_path(DATADIR)
- EXEDIR = common.normalize_path(EXEDIR)
+ USERDIR = common.normalize_volume(USERDIR)
+ DATADIR = common.normalize_volume(DATADIR)
+ EXEDIR = common.normalize_volume(EXEDIR)
end
do
@@ -509,7 +690,6 @@ function core.init()
cur_node = cur_node:split("down", core.command_view, {y = true})
cur_node = cur_node:split("down", core.status_view, {y = true})
- core.project_scan_thread_id = core.add_thread(project_scan_thread)
command.add_defaults()
local got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins()
@@ -520,6 +700,12 @@ function core.init()
end
local got_project_error = not core.load_project_module()
+ -- We assume we have just a single project directory here. Now that StatusView
+ -- is there show max files warning if needed.
+ if core.project_directories[1].files_limit then
+ show_max_files_warning(core.project_directories[1])
+ end
+
for _, filename in ipairs(files) do
core.root_view:open_doc(core.open_doc(filename))
end
@@ -675,16 +861,18 @@ function core.load_plugins()
userdir = {dir = USERDIR, plugins = {}},
datadir = {dir = DATADIR, plugins = {}},
}
- local files = {}
+ local files, ordered = {}, {}
for _, root_dir in ipairs {DATADIR, USERDIR} do
local plugin_dir = root_dir .. "/plugins"
for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
+ if not files[filename] then table.insert(ordered, filename) end
files[filename] = plugin_dir -- user plugins will always replace system plugins
end
end
+ table.sort(ordered)
- for filename, plugin_dir in pairs(files) do
- local basename = filename:match("(.-)%.lua$") or filename
+ for _, filename in ipairs(ordered) do
+ local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
if is_lua_file then
if not version_match then
@@ -908,6 +1096,84 @@ function core.try(fn, ...)
return false, err
end
+local scheduled_rescan = {}
+
+function core.has_pending_rescan()
+ for _ in pairs(scheduled_rescan) do
+ return true
+ end
+end
+
+
+function core.dir_rescan_add_job(dir, filepath)
+ local dirpath = filepath:match("^(.+)[/\\].+$")
+ local dirpath_rooted = dirpath and PATHSEP .. dirpath or ""
+ local abs_dirpath = dir.name .. dirpath_rooted
+ if dirpath then
+ -- check if the directory is in the project files list, if not exit
+ local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"})
+ -- Note that is dir_match is false dir_index greaten than the last valid index.
+ -- We use dir_index to index dir.files below only if dir_match is true.
+ if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end
+ end
+ local new_time = system.get_time() + 1
+
+ -- evaluate new rescan request versus existing rescan
+ local remove_list = {}
+ for _, rescan in pairs(scheduled_rescan) do
+ if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then
+ -- abs_dirpath is a subpath of a scan already ongoing: skip
+ rescan.time_limit = new_time
+ return
+ elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then
+ -- abs_dirpath already cover this rescan: add to the list of rescan to be removed
+ table.insert(remove_list, rescan.abs_path)
+ end
+ end
+ for _, key_path in ipairs(remove_list) do
+ scheduled_rescan[key_path] = nil
+ end
+
+ scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time}
+ core.add_thread(function()
+ while true do
+ local rescan = scheduled_rescan[abs_dirpath]
+ if not rescan then return end
+ if system.get_time() > rescan.time_limit then
+ local has_changes = rescan_project_subdir(rescan.dir, rescan.path)
+ if has_changes then
+ core.redraw = true -- we run without an event, from a thread
+ rescan.time_limit = new_time
+ else
+ scheduled_rescan[rescan.abs_path] = nil
+ return
+ end
+ end
+ coroutine.yield(0.2)
+ end
+ end)
+end
+
+
+-- no-op but can be overrided by plugins
+function core.on_dirmonitor_modify(dir, filepath)
+end
+
+
+function core.on_dir_change(watch_id, action, filepath)
+ local dir = project_dir_by_watch_id(watch_id)
+ if not dir then return end
+ core.dir_rescan_add_job(dir, filepath)
+ if action == "delete" then
+ project_scan_remove_file(dir, filepath)
+ elseif action == "create" then
+ project_scan_add_file(dir, filepath)
+ core.on_dirmonitor_modify(dir, filepath);
+ elseif action == "modify" then
+ core.on_dirmonitor_modify(dir, filepath);
+ end
+end
+
function core.on_event(type, ...)
local did_keymap = false
@@ -920,11 +1186,15 @@ function core.on_event(type, ...)
elseif type == "mousemoved" then
core.root_view:on_mouse_moved(...)
elseif type == "mousepressed" then
- core.root_view:on_mouse_pressed(...)
+ if not core.root_view:on_mouse_pressed(...) then
+ did_keymap = keymap.on_mouse_pressed(...)
+ end
elseif type == "mousereleased" then
core.root_view:on_mouse_released(...)
elseif type == "mousewheel" then
- core.root_view:on_mouse_wheel(...)
+ if not core.root_view:on_mouse_wheel(...) then
+ did_keymap = keymap.on_mouse_wheel(...)
+ end
elseif type == "resized" then
core.window_mode = system.get_window_mode()
elseif type == "minimized" or type == "maximized" or type == "restored" then
@@ -944,6 +1214,8 @@ function core.on_event(type, ...)
end
elseif type == "focuslost" then
core.root_view:on_focus_lost(...)
+ elseif type == "dirchange" then
+ core.on_dir_change(...)
elseif type == "quit" then
core.quit()
end
@@ -1050,7 +1322,7 @@ function core.run()
while true do
core.frame_start = system.get_time()
local did_redraw = core.step()
- local need_more_work = run_threads()
+ local need_more_work = run_threads() or core.has_pending_rescan()
if core.restart_request or core.quit_request then break end
if not did_redraw and not need_more_work then
idle_iterations = idle_iterations + 1
diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua
index 53a20468..b0bd41a5 100644
--- a/data/core/keymap-macos.lua
+++ b/data/core/keymap-macos.lua
@@ -32,6 +32,8 @@ local function keymap_macos(keymap)
["cmd+7"] = "root:switch-to-tab-7",
["cmd+8"] = "root:switch-to-tab-8",
["cmd+9"] = "root:switch-to-tab-9",
+ ["wheel"] = "root:scroll",
+
["cmd+f"] = "find-replace:find",
["cmd+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find",
@@ -93,6 +95,11 @@ local function keymap_macos(keymap)
["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page",
+ ["shift+1lclick"] = "doc:select-to-cursor",
+ ["ctrl+1lclick"] = "doc:split-cursor",
+ ["1lclick"] = "doc:set-cursor",
+ ["2lclick"] = "doc:set-cursor-word",
+ ["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line",
diff --git a/data/core/keymap.lua b/data/core/keymap.lua
index 7f7c0fe2..b076629b 100644
--- a/data/core/keymap.lua
+++ b/data/core/keymap.lua
@@ -1,4 +1,5 @@
local command = require "core.command"
+local config = require "core.config"
local keymap = {}
keymap.modkeys = {}
@@ -30,7 +31,8 @@ function keymap.add_direct(map)
end
keymap.map[stroke] = commands
for _, cmd in ipairs(commands) do
- keymap.reverse_map[cmd] = stroke
+ keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
+ table.insert(keymap.reverse_map[cmd], stroke)
end
end
end
@@ -52,18 +54,43 @@ function keymap.add(map, overwrite)
end
end
for _, cmd in ipairs(commands) do
- keymap.reverse_map[cmd] = stroke
+ keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
+ table.insert(keymap.reverse_map[cmd], stroke)
end
end
end
+local function remove_only(tbl, k, v)
+ for key, values in pairs(tbl) do
+ if key == k then
+ if v then
+ for i, value in ipairs(values) do
+ if value == v then
+ table.remove(values, i)
+ end
+ end
+ else
+ tbl[key] = nil
+ end
+ break
+ end
+ end
+end
+
+
+function keymap.unbind(key, cmd)
+ remove_only(keymap.map, key, cmd)
+ remove_only(keymap.reverse_map, cmd, key)
+end
+
+
function keymap.get_binding(cmd)
- return keymap.reverse_map[cmd]
+ return table.unpack(keymap.reverse_map[cmd] or {})
end
-function keymap.on_key_pressed(k)
+function keymap.on_key_pressed(k, ...)
local mk = modkey_map[k]
if mk then
keymap.modkeys[mk] = true
@@ -73,18 +100,30 @@ function keymap.on_key_pressed(k)
end
else
local stroke = key_to_stroke(k)
- local commands = keymap.map[stroke]
+ local commands, performed = keymap.map[stroke]
if commands then
for _, cmd in ipairs(commands) do
- local performed = command.perform(cmd)
+ performed = command.perform(cmd, ...)
if performed then break end
end
- return true
+ return performed
end
end
return false
end
+function keymap.on_mouse_wheel(delta, ...)
+ return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...)
+ or keymap.on_key_pressed("wheel", delta, ...))
+end
+
+function keymap.on_mouse_pressed(button, x, y, clicks)
+ local click_number = (((clicks - 1) % config.max_clicks) + 1)
+ return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or
+ keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or
+ keymap.on_key_pressed(click_number .. "click", x, y, clicks) or
+ keymap.on_key_pressed("click", x, y, clicks))
+end
function keymap.on_key_released(k)
local mk = modkey_map[k]
@@ -133,6 +172,7 @@ keymap.add_direct {
["alt+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9",
+ ["wheel"] = "root:scroll",
["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace",
@@ -170,6 +210,7 @@ keymap.add_direct {
["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
["ctrl+f3"] = "find-replace:select-next",
+ ["ctrl+shift+f3"] = "find-replace:select-previous",
["ctrl+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments",
@@ -193,6 +234,11 @@ keymap.add_direct {
["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page",
+ ["shift+1lclick"] = "doc:select-to-cursor",
+ ["ctrl+1lclick"] = "doc:split-cursor",
+ ["1lclick"] = "doc:set-cursor",
+ ["2lclick"] = "doc:set-cursor-word",
+ ["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line",
diff --git a/data/core/regex.lua b/data/core/regex.lua
index 69203cbd..637d23fd 100644
--- a/data/core/regex.lua
+++ b/data/core/regex.lua
@@ -1,4 +1,3 @@
-
-- So that in addition to regex.gsub(pattern, string), we can also do
-- pattern:gsub(string).
regex.__index = function(table, key) return regex[key]; end
@@ -6,7 +5,8 @@ regex.__index = function(table, key) return regex[key]; end
regex.match = function(pattern_string, string, offset, options)
local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string)
- return regex.cmatch(pattern, string, offset or 1, options or 0)
+ local s, e = regex.cmatch(pattern, string, offset or 1, options or 0)
+ return s, e and e - 1
end
-- Will iterate back through any UTF-8 bytes so that we don't replace bits
diff --git a/data/core/rootview.lua b/data/core/rootview.lua
index 0d219474..97caca1c 100644
--- a/data/core/rootview.lua
+++ b/data/core/rootview.lua
@@ -149,10 +149,17 @@ function Node:remove_view(root, view)
else
locked_size = locked_size_y
end
- if self.is_primary_node or locked_size then
+ local next_primary
+ if self.is_primary_node then
+ next_primary = core.root_view:select_next_primary_node()
+ end
+ if locked_size or (self.is_primary_node and not next_primary) then
self.views = {}
self:add_view(EmptyView())
else
+ if other == next_primary then
+ next_primary = parent
+ end
parent:consume(other)
local p = parent
while p.type ~= "leaf" do
@@ -160,7 +167,7 @@ function Node:remove_view(root, view)
end
p:set_active_view(p.active_view)
if self.is_primary_node then
- p.is_primary_node = true
+ next_primary.is_primary_node = true
end
end
end
@@ -411,15 +418,8 @@ end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
- local n
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
- if x1 then
- n = x1 + ds
- elseif x2 then
- n = self.size[x] - x2
- else
- n = math.floor(self.size[x] * self.divider)
- end
+ local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
self.a.position[x] = self.position[x]
self.a.position[y] = self.position[y]
self.a.size[x] = n - ds
@@ -602,7 +602,7 @@ function Node:draw()
self:draw_tabs()
end
local pos, size = self.active_view.position, self.active_view.size
- core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
+ core.push_clip_rect(pos.x, pos.y, size.x, size.y)
self.active_view:draw()
core.pop_clip_rect()
else
@@ -682,6 +682,10 @@ end
function Node:resize(axis, value)
+ -- the application works fine with non-integer values but to have pixel-perfect
+ -- placements of view elements, like the scrollbar, we round the value to be
+ -- an integer.
+ value = math.floor(value)
if self.type == 'leaf' then
-- If it is not locked we don't accept the
-- resize operation here because for proportional panes the resize is
@@ -826,6 +830,24 @@ function RootView:get_primary_node()
end
+local function select_next_primary_node(node)
+ if node.is_primary_node then return end
+ if node.type ~= "leaf" then
+ return select_next_primary_node(node.a) or select_next_primary_node(node.b)
+ else
+ local lx, ly = node:get_locked_size()
+ if not lx and not ly then
+ return node
+ end
+ end
+end
+
+
+function RootView:select_next_primary_node()
+ return select_next_primary_node(self.root_node)
+end
+
+
function RootView:open_doc(doc)
local node = self:get_active_node_default()
for i, view in ipairs(node.views) do
@@ -855,30 +877,30 @@ end
function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y)
- if div then
+ local node = self.root_node:get_child_overlapping_point(x, y)
+ if div and (node and not node.active_view:scrollbar_overlaps_point(x, y)) then
self.dragged_divider = div
- return
+ return true
end
- local node = self.root_node:get_child_overlapping_point(x, y)
if node.hovered_scroll_button > 0 then
node:scroll_tabs(node.hovered_scroll_button)
- return
+ return true
end
local idx = node:get_tab_overlapping_point(x, y)
if idx then
if button == "middle" or node.hovered_close == idx then
node:close_view(self.root_node, node.views[idx])
+ return true
else
if button == "left" then
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
end
node:set_active_view(node.views[idx])
+ return true
end
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
core.set_active_view(node.active_view)
- if not self.on_view_mouse_pressed(button, x, y, clicks) then
- node.active_view:on_mouse_pressed(button, x, y, clicks)
- end
+ return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks)
end
end
@@ -1000,17 +1022,18 @@ function RootView:on_mouse_moved(x, y, dx, dy)
self.root_node:on_mouse_moved(x, y, dx, dy)
- local node = self.root_node:get_child_overlapping_point(x, y)
+ self.overlapping_node = self.root_node:get_child_overlapping_point(x, y)
+
local div = self.root_node:get_divider_overlapping_point(x, y)
- local tab_index = node and node:get_tab_overlapping_point(x, y)
- if node and node:get_scroll_button_index(x, y) then
+ local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
+ if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
core.request_cursor("arrow")
- elseif div then
+ elseif div and (self.overlapping_node and not self.overlapping_node.active_view:scrollbar_overlaps_point(x, y)) then
core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
elseif tab_index then
core.request_cursor("arrow")
- elseif node then
- core.request_cursor(node.active_view.cursor)
+ elseif self.overlapping_node then
+ core.request_cursor(self.overlapping_node.active_view.cursor)
end
end
@@ -1018,7 +1041,7 @@ end
function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y)
- node.active_view:on_mouse_wheel(...)
+ return node.active_view:on_mouse_wheel(...)
end
@@ -1100,10 +1123,10 @@ function RootView:update_drag_overlay()
if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then
local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y)
self:set_drag_overlay(self.drag_overlay_tab,
- tab_x + (tab_index and 0 or tab_w), tab_y,
- style.caret_width, tab_h,
- -- avoid showing tab overlay moving between nodes
- over ~= self.drag_overlay_tab.last_over)
+ tab_x + (tab_index and 0 or tab_w), tab_y,
+ style.caret_width, tab_h,
+ -- avoid showing tab overlay moving between nodes
+ over ~= self.drag_overlay_tab.last_over)
self:set_show_overlay(self.drag_overlay, false)
self.drag_overlay_tab.last_over = over
else
diff --git a/data/core/start.lua b/data/core/start.lua
index 330c42f5..f3bc89c6 100644
--- a/data/core/start.lua
+++ b/data/core/start.lua
@@ -2,7 +2,7 @@
VERSION = "@PROJECT_VERSION@"
MOD_VERSION = "2"
-SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE
+SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE
PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
@@ -28,3 +28,6 @@ package.searchers = { package.searchers[1], package.searchers[2], function(modna
if not path then return nil end
return system.load_native_plugin, path
end }
+
+table.pack = table.pack or pack or function(...) return {...} end
+table.unpack = table.unpack or unpack
diff --git a/data/core/statusview.lua b/data/core/statusview.lua
index 3342bdb9..59773cf0 100644
--- a/data/core/statusview.lua
+++ b/data/core/statusview.lua
@@ -108,9 +108,9 @@ function StatusView:get_items()
local dv = core.active_view
local line, col = dv.doc:get_selection()
local dirty = dv.doc:is_dirty()
- local indent = dv.doc.indent_info
- local indent_label = (indent and indent.type == "hard") and "tabs: " or "spaces: "
- local indent_size = indent and tostring(indent.size) .. (indent.confirmed and "" or "*") or "unknown"
+ local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info()
+ local indent_label = (indent_type == "hard") and "tabs: " or "spaces: "
+ local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown"
return {
dirty and style.accent or style.text, style.icon_font, "f",
diff --git a/data/core/syntax.lua b/data/core/syntax.lua
index a763ac78..adecd0cd 100644
--- a/data/core/syntax.lua
+++ b/data/core/syntax.lua
@@ -3,7 +3,7 @@ local common = require "core.common"
local syntax = {}
syntax.items = {}
-local plain_text_syntax = { patterns = {}, symbols = {} }
+local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
function syntax.add(t)
@@ -22,7 +22,7 @@ end
function syntax.get(filename, header)
return find(filename, "files")
- or find(header, "headers")
+ or (header and find(header, "headers"))
or plain_text_syntax
end
diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua
index a20dba5e..f77fed44 100644
--- a/data/core/tokenizer.lua
+++ b/data/core/tokenizer.lua
@@ -1,4 +1,5 @@
local syntax = require "core.syntax"
+local common = require "core.common"
local tokenizer = {}
@@ -142,8 +143,13 @@ function tokenizer.tokenize(incoming_syntax, text, state)
code = p._regex
end
repeat
- res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) }
- or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) }
+ local next = res[2] + 1
+ -- go to the start of the next utf-8 character
+ while text:byte(next) and common.is_utf8_cont(text, next) do
+ next = next + 1
+ end
+ res = p.pattern and { text:find(at_start and "^" .. code or code, next) }
+ or { regex.match(code, text, next, at_start and regex.ANCHORED or 0) }
if res[1] and close and target[3] then
local count = 0
for i = res[1] - 1, 1, -1 do
@@ -155,7 +161,7 @@ function tokenizer.tokenize(incoming_syntax, text, state)
if count % 2 == 0 then break end
end
until not res[1] or not close or not target[3]
- return unpack(res)
+ return table.unpack(res)
end
while i <= #text do
diff --git a/data/core/view.lua b/data/core/view.lua
index d1374ee4..4b787d46 100644
--- a/data/core/view.lua
+++ b/data/core/view.lua
@@ -102,13 +102,9 @@ function View:on_text_input(text)
-- no-op
end
-
function View:on_mouse_wheel(y)
- if self.scrollable then
- self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
- end
-end
+end
function View:get_content_bounds()
local x = self.scroll.x
@@ -140,7 +136,7 @@ end
function View:draw_background(color)
local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y
- renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
+ renderer.draw_rect(x, y, w, h, color)
end
diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua
index e772666f..9978092e 100644
--- a/data/plugins/autoreload.lua
+++ b/data/plugins/autoreload.lua
@@ -3,7 +3,6 @@ local core = require "core"
local config = require "core.config"
local Doc = require "core.doc"
-
local times = setmetatable({}, { __mode = "k" })
local function update_time(doc)
@@ -11,7 +10,6 @@ local function update_time(doc)
times[doc] = info.modified
end
-
local function reload_doc(doc)
local fp = io.open(doc.filename, "r")
local text = fp:read("*a")
@@ -27,23 +25,19 @@ local function reload_doc(doc)
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end
+local on_modify = core.on_dirmonitor_modify
-core.add_thread(function()
- while true do
- -- check all doc modified times
- for _, doc in ipairs(core.docs) do
- local info = system.get_file_info(doc.filename or "")
- if info and times[doc] ~= info.modified then
- reload_doc(doc)
- end
- coroutine.yield()
+core.on_dirmonitor_modify = function(dir, filepath)
+ local abs_filename = dir.name .. PATHSEP .. filepath
+ for _, doc in ipairs(core.docs) do
+ local info = system.get_file_info(doc.filename or "")
+ if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then
+ reload_doc(doc)
+ break
end
-
- -- wait for next scan
- coroutine.yield(config.project_scan_rate)
end
-end)
-
+ on_modify(dir, filepath)
+end
-- patch `Doc.save|load` to store modified time
local load = Doc.load
diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua
index dc95567f..4b34dfd5 100644
--- a/data/plugins/contextmenu.lua
+++ b/data/plugins/contextmenu.lua
@@ -62,15 +62,15 @@ menu:register("core.logview", {
if require("plugins.scale") then
menu:register("core.docview", {
- { text = "Font +", command = "scale:increase" },
- { text = "Font -", command = "scale:decrease" },
- { text = "Font Reset", command = "scale:reset" },
+ { text = "Cut", command = "doc:cut" },
+ { text = "Copy", command = "doc:copy" },
+ { text = "Paste", command = "doc:paste" },
+ { text = "Font +", command = "scale:increase" },
+ { text = "Font -", command = "scale:decrease" },
+ { text = "Font Reset", command = "scale:reset" },
ContextMenu.DIVIDER,
- { text = "Find", command = "find-replace:find" },
- { text = "Replace", command = "find-replace:replace" },
- ContextMenu.DIVIDER,
- { text = "Find Pattern", command = "find-replace:find-pattern" },
- { text = "Replace Pattern", command = "find-replace:replace-pattern" },
+ { text = "Find", command = "find-replace:find" },
+ { text = "Replace", command = "find-replace:replace" }
})
end
diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua
index 20541c82..9ac29882 100644
--- a/data/plugins/detectindent.lua
+++ b/data/plugins/detectindent.lua
@@ -121,40 +121,17 @@ end
local clean = Doc.clean
function Doc:clean(...)
clean(self, ...)
- if not cache[self].confirmed then
+ local _, _, confirmed = self:get_indent_info()
+ if not confirmed then
update_cache(self)
end
end
-local function with_indent_override(doc, fn, ...)
- local c = cache[doc]
- if not c then
- return fn(...)
- end
- local type, size = config.tab_type, config.indent_size
- config.tab_type, config.indent_size = c.type, c.size or config.indent_size
- local r1, r2, r3 = fn(...)
- config.tab_type, config.indent_size = type, size
- return r1, r2, r3
-end
-
-
-local perform = command.perform
-function command.perform(...)
- return with_indent_override(core.active_view.doc, perform, ...)
-end
-
-
-local draw = DocView.draw
-function DocView:draw(...)
- return with_indent_override(self.doc, draw, self, ...)
-end
-
-
local function set_indent_type(doc, type)
+ local _, indent_size = doc:get_indent_info()
cache[doc] = {type = type,
- size = cache[doc].value or config.indent_size,
+ size = indent_size,
confirmed = true}
doc.indent_info = cache[doc]
end
@@ -180,7 +157,8 @@ end
local function set_indent_size(doc, size)
- cache[doc] = {type = cache[doc].type or config.tab_type,
+ local indent_type = doc:get_indent_info()
+ cache[doc] = {type = indent_type,
size = size,
confirmed = true}
doc.indent_info = cache[doc]
diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua
index da9d1b12..0004c7ea 100644
--- a/data/plugins/drawwhitespace.lua
+++ b/data/plugins/drawwhitespace.lua
@@ -7,26 +7,30 @@ local common = require "core.common"
local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(idx, x, y)
- local font = (self:get_font() or style.syntax_fonts["comment"])
- local color = style.syntax.comment
- local ty, tx = y + self:get_line_text_y_offset()
+ local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
+ local color = style.syntax.whitespace or style.syntax.comment
+ local ty = y + self:get_line_text_y_offset()
+ local tx
local text, offset, s, e = self.doc.lines[idx], 1
+ local x1, _, x2, _ = self:get_content_bounds()
+ local _offset = self:get_x_offset_col(idx, x1)
+ offset = _offset
while true do
s, e = text:find(" +", offset)
if not s then break end
tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color)
+ if tx > x + x2 then break end
offset = e + 1
end
- offset = 1
+ offset = _offset
while true do
s, e = text:find("\t", offset)
if not s then break end
tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, "»", tx, ty, color)
+ if tx > x + x2 then break end
offset = e + 1
end
draw_line_text(self, idx, x, y)
end
-
-
diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua
index 44c3b895..b0a4dec5 100644
--- a/data/plugins/language_c.lua
+++ b/data/plugins/language_c.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "C",
files = { "%.c$", "%.h$", "%.inl$" },
comment = "//",
patterns = {
diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua
index 499a09db..8d6aef4b 100644
--- a/data/plugins/language_cpp.lua
+++ b/data/plugins/language_cpp.lua
@@ -4,6 +4,7 @@ pcall(require, "plugins.language_c")
local syntax = require "core.syntax"
syntax.add {
+ name = "C++",
files = {
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua
index 222e2f94..395e375c 100644
--- a/data/plugins/language_css.lua
+++ b/data/plugins/language_css.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "CSS",
files = { "%.css$" },
patterns = {
{ pattern = "\\.", type = "normal" },
diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua
index cebb3f1a..1f4515bc 100644
--- a/data/plugins/language_html.lua
+++ b/data/plugins/language_html.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "HTML",
files = { "%.html?$" },
patterns = {
{
diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua
index 7556b00b..d9515d52 100644
--- a/data/plugins/language_js.lua
+++ b/data/plugins/language_js.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$" },
comment = "//",
patterns = {
@@ -11,8 +12,8 @@ syntax.add {
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" },
- { pattern = "0x[%da-fA-F]+", type = "number" },
- { pattern = "-?%d+[%d%.eE]*", type = "number" },
+ { pattern = "0x[%da-fA-F_]+n?", type = "number" },
+ { pattern = "-?%d+[%d%.eE_n]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua
index 165633b6..5c770d43 100644
--- a/data/plugins/language_lua.lua
+++ b/data/plugins/language_lua.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "Lua",
files = "%.lua$",
headers = "^#!.*[ /]lua",
comment = "--",
diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua
index 3c1c329a..e7c870ec 100644
--- a/data/plugins/language_md.lua
+++ b/data/plugins/language_md.lua
@@ -4,11 +4,11 @@ local syntax = require "core.syntax"
syntax.add {
+ name = "Markdown",
files = { "%.md$", "%.markdown$" },
patterns = {
{ pattern = "\\.", type = "normal" },
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
- { pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "```python", "```" }, type = "string", syntax = ".py" },
{ pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
@@ -25,6 +25,21 @@ syntax.add {
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" },
{ pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" },
+ { pattern = { "```c", "```" }, type = "string", syntax = ".c" },
+ { pattern = { "```julia", "```" }, type = "string", syntax = ".jl" },
+ { pattern = { "```rust", "```" }, type = "string", syntax = ".rs" },
+ { pattern = { "```dart", "```" }, type = "string", syntax = ".dart" },
+ { pattern = { "```v", "```" }, type = "string", syntax = ".v" },
+ { pattern = { "```toml", "```" }, type = "string", syntax = ".toml" },
+ { pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" },
+ { pattern = { "```php", "```" }, type = "string", syntax = ".php" },
+ { pattern = { "```nim", "```" }, type = "string", syntax = ".nim" },
+ { pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" },
+ { pattern = { "```rescript", "```" }, type = "string", syntax = ".res" },
+ { pattern = { "```moon", "```" }, type = "string", syntax = ".moon" },
+ { pattern = { "```go", "```" }, type = "string", syntax = ".go" },
+ { pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" },
+ { pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "`", "`", "\\" }, type = "string" },
diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua
index 252a0d14..8bc6fbd4 100644
--- a/data/plugins/language_python.lua
+++ b/data/plugins/language_python.lua
@@ -2,20 +2,21 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "Python",
files = { "%.py$", "%.pyw$", "%.rpy$" },
headers = "^#!.*[ /]python",
comment = "#",
patterns = {
- { pattern = { "#", "\n" }, type = "comment" },
- { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
- { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
- { pattern = { '"""', '"""' }, type = "string" },
- { pattern = "0x[%da-fA-F]+", type = "number" },
- { pattern = "-?%d+[%d%.eE]*", type = "number" },
- { pattern = "-?%.?%d+", type = "number" },
- { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
- { pattern = "[%a_][%w_]*%f[(]", type = "function" },
- { pattern = "[%a_][%w_]*", type = "symbol" },
+ { pattern = { "#", "\n" }, type = "comment" },
+ { pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
+ { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
+ { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
+ { pattern = "0x[%da-fA-F]+", type = "number" },
+ { pattern = "-?%d+[%d%.eE]*", type = "number" },
+ { pattern = "-?%.?%d+", type = "number" },
+ { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
+ { pattern = "[%a_][%w_]*%f[(]", type = "function" },
+ { pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["class"] = "keyword",
diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua
index 95e310bb..c858d3cf 100644
--- a/data/plugins/language_xml.lua
+++ b/data/plugins/language_xml.lua
@@ -2,6 +2,7 @@
local syntax = require "core.syntax"
syntax.add {
+ name = "XML",
files = { "%.xml$" },
headers = "<%?xml",
patterns = {
diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua
index 5a17c8ca..96745659 100644
--- a/data/plugins/lineguide.lua
+++ b/data/plugins/lineguide.lua
@@ -2,19 +2,20 @@
local config = require "core.config"
local style = require "core.style"
local DocView = require "core.docview"
+local CommandView = require "core.commandview"
local draw_overlay = DocView.draw_overlay
function DocView:draw_overlay(...)
- local ns = ("n"):rep(config.line_limit)
- local offset = self:get_font():get_width(ns)
- local x = self:get_line_screen_position(1) + offset
- local y = self.position.y
- local w = math.ceil(SCALE * 1)
- local h = self.size.y
-
- local color = style.guide or style.selection
- renderer.draw_rect(x, y, w, h, color)
-
+ if not self:is(CommandView) then
+ local offset = self:get_font():get_width("n") * config.line_limit
+ local x = self:get_line_screen_position(1) + offset
+ local y = self.position.y
+ local w = math.ceil(SCALE * 1)
+ local h = self.size.y
+
+ local color = style.guide or style.selection
+ renderer.draw_rect(x, y, w, h, color)
+ end
draw_overlay(self, ...)
end
diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua
index dda3a2d0..d0d75d7f 100644
--- a/data/plugins/projectsearch.lua
+++ b/data/plugins/projectsearch.lua
@@ -92,7 +92,7 @@ end
function ResultsView:on_mouse_pressed(...)
local caught = ResultsView.super.on_mouse_pressed(self, ...)
if not caught then
- self:open_selected_result()
+ return self:open_selected_result()
end
end
@@ -108,6 +108,7 @@ function ResultsView:open_selected_result()
dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true)
end)
+ return true
end
diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua
index b8384609..616ee40b 100644
--- a/data/plugins/scale.lua
+++ b/data/plugins/scale.lua
@@ -54,6 +54,10 @@ local function set_scale(scale)
renderer.font.set_size(font, s * font:get_size())
end
+ for _, font in pairs(style.syntax_fonts) do
+ renderer.font.set_size(font, s * font:get_size())
+ end
+
-- restore scroll positions
for view, n in pairs(scrolls) do
view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
@@ -67,17 +71,6 @@ local function get_scale()
return current_scale
end
-local on_mouse_wheel = RootView.on_mouse_wheel
-
-function RootView:on_mouse_wheel(d, ...)
- if keymap.modkeys["ctrl"] and config.plugins.scale.use_mousewheel then
- if d < 0 then command.perform "scale:decrease" end
- if d > 0 then command.perform "scale:increase" end
- else
- return on_mouse_wheel(self, d, ...)
- end
-end
-
local function res_scale()
set_scale(default_scale)
end
@@ -101,6 +94,8 @@ keymap.add {
["ctrl+0"] = "scale:reset",
["ctrl+-"] = "scale:decrease",
["ctrl+="] = "scale:increase",
+ ["ctrl+wheelup"] = "scale:increase",
+ ["ctrl+wheeldown"] = "scale:decrease"
}
return {
diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua
index fa3ab53a..4f5db701 100644
--- a/data/plugins/treeview.lua
+++ b/data/plugins/treeview.lua
@@ -41,8 +41,15 @@ function TreeView:new()
self.init_size = true
self.target_size = default_treeview_size
self.cache = {}
- self.last = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
+
+ local on_dirmonitor_modify = core.on_dirmonitor_modify
+ function core.on_dirmonitor_modify(dir, filepath)
+ if self.cache[dir.name] then
+ self.cache[dir.name][filepath] = nil
+ end
+ on_dirmonitor_modify(dir, filepath)
+ end
end
@@ -54,7 +61,7 @@ function TreeView:set_target_size(axis, value)
end
-function TreeView:get_cached(item, dirname)
+function TreeView:get_cached(dir, item, dirname)
local dir_cache = self.cache[dirname]
if not dir_cache then
dir_cache = {}
@@ -80,6 +87,7 @@ function TreeView:get_cached(item, dirname)
end
t.name = basename
t.type = item.type
+ t.dir = dir -- points to top level "dir" item
dir_cache[cache_name] = t
end
return t
@@ -104,18 +112,13 @@ 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]
- local last_files = self.last[dir.name]
- if not last_files then
- self.last[dir.name] = dir.files
- else
- if dir.files ~= last_files then
- self:invalidate_cache(dir.name)
- self.last[dir.name] = dir.files
- end
+ -- invalidate cache's skip values if directory is declared dirty
+ if dir.is_dirty and self.cache[dir.name] then
+ self:invalidate_cache(dir.name)
end
+ dir.is_dirty = false
end
end
@@ -131,14 +134,14 @@ function TreeView:each_item()
for k = 1, #core.project_directories do
local dir = core.project_directories[k]
- local dir_cached = self:get_cached(dir.item, dir.name)
+ local dir_cached = self:get_cached(dir, dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1
y = y + h
local i = 1
while i <= #dir.files and dir_cached.expanded do
local item = dir.files[i]
- local cached = self:get_cached(item, dir.name)
+ local cached = self:get_cached(dir, item, dir.name)
coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1
@@ -206,7 +209,6 @@ local function create_directory_in(item)
core.error("cannot create directory %q: %s", dirname, err)
end
item.expanded = true
- core.reschedule_project_scan()
end)
end
@@ -223,26 +225,17 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item)
else
- if core.project_files_limit and not hovered_item.expanded then
- local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
- local index = 0
- -- The loop below is used to find the first match starting from the end
- -- in case there are multiple matches.
- while index and index + #filename < #abs_filename do
- index = string.find(abs_filename, filename, index + 1, true)
- end
- -- we assume here index is not nil because the abs_filename must contain the
- -- relative filename
- local dirname = string.sub(abs_filename, 1, index - 2)
- if core.is_project_folder(dirname) then
- core.scan_project_folder(dirname, filename)
- self:invalidate_cache(dirname)
- end
- end
hovered_item.expanded = not hovered_item.expanded
+ if hovered_item.dir.files_limit then
+ core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
+ core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
+ end
end
else
core.try(function()
+ if core.last_active_view and core.active_view == self then
+ core.set_active_view(core.last_active_view)
+ end
local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
@@ -295,6 +288,12 @@ function TreeView:draw_tooltip()
end
+function TreeView:color_for_item(abs_filename)
+ -- other plugins can override this to customize the color of each icon
+ return nil
+end
+
+
function TreeView:draw()
self:draw_background(style.background2)
@@ -318,6 +317,9 @@ function TreeView:draw()
color = style.accent
end
+ -- allow for color overrides
+ local icon_color = self:color_for_item(item.abs_filename) or color
+
-- icons
x = x + item.depth * style.padding.x + style.padding.x
if item.type == "dir" then
@@ -325,11 +327,11 @@ function TreeView:draw()
local icon2 = item.expanded and "D" or "d"
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
x = x + style.padding.x
- common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h)
+ common.draw_text(style.icon_font, icon_color, icon2, nil, x, y, 0, h)
x = x + icon_width
else
x = x + style.padding.x
- common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
+ common.draw_text(style.icon_font, icon_color, "f", nil, x, y, 0, h)
x = x + icon_width
end
@@ -404,7 +406,7 @@ function RootView:draw(...)
end
local function is_project_folder(path)
- return common.basename(core.project_dir) == path
+ return core.project_dir == path
end
menu:register(function() return view.hovered_item end, {
@@ -415,7 +417,7 @@ menu:register(function() return view.hovered_item end, {
menu:register(
function()
return view.hovered_item
- and not is_project_folder(view.hovered_item.filename)
+ and not is_project_folder(view.hovered_item.abs_filename)
end,
{
{ text = "Rename", command = "treeview:rename" },
@@ -461,14 +463,12 @@ command.add(function() return view.hovered_item ~= nil end, {
else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
- core.reschedule_project_scan()
end, common.path_suggest)
end,
["treeview:new-file"] = function()
- local dir_name = view.hovered_item.filename
- if not is_project_folder(dir_name) then
- core.command_view:set_text(dir_name .. "/")
+ if not is_project_folder(view.hovered_item.abs_filename) then
+ core.command_view:set_text(view.hovered_item.filename .. "/")
end
core.command_view:enter("Filename", function(filename)
local doc_filename = core.project_dir .. PATHSEP .. filename
@@ -476,20 +476,17 @@ command.add(function() return view.hovered_item ~= nil end, {
file:write("")
file:close()
core.root_view:open_doc(core.open_doc(doc_filename))
- core.reschedule_project_scan()
core.log("Created %s", doc_filename)
end, common.path_suggest)
end,
["treeview:new-folder"] = function()
- local dir_name = view.hovered_item.filename
- if not is_project_folder(dir_name) then
- core.command_view:set_text(dir_name .. "/")
+ if not is_project_folder(view.hovered_item.abs_filename) then
+ core.command_view:set_text(view.hovered_item.filename .. "/")
end
core.command_view:enter("Folder Name", function(filename)
local dir_path = core.project_dir .. PATHSEP .. filename
common.mkdirp(dir_path)
- core.reschedule_project_scan()
core.log("Created %s", dir_path)
end, common.path_suggest)
end,
@@ -526,7 +523,6 @@ command.add(function() return view.hovered_item ~= nil end, {
return
end
end
- core.reschedule_project_scan()
core.log("Deleted \"%s\"", filename)
end
end