From 3b3677ca4b21e8384fe505d842cd88feecf56ad4 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sat, 10 Jul 2021 22:47:03 +0800 Subject: [PATCH 001/135] add config.lineguide option --- data/plugins/lineguide.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 8ef3ee68..adc67389 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -6,16 +6,18 @@ local DocView = require "core.docview" local draw_overlay = DocView.draw_overlay function DocView:draw_overlay(...) - local ns = ("n"):rep(config.line_limit) - local ss = self:get_font():subpixel_scale() - local offset = self:get_font():get_width_subpixel(ns) / ss - 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 + if config.lineguide then + local ns = self:get_font():get_width_subpixel("n") * config.line_limit + local ss = self:get_font():subpixel_scale() + local offset = ns / ss + 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) + local color = style.guide or style.selection + renderer.draw_rect(x, y, w, h, color) + end draw_overlay(self, ...) end From 1725b3ab834f79020c520080ac5184c6d1dfb122 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Mon, 2 Aug 2021 09:54:55 +0800 Subject: [PATCH 002/135] revert config.lineguide option --- data/plugins/lineguide.lua | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index adc67389..b838eebb 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -6,18 +6,16 @@ local DocView = require "core.docview" local draw_overlay = DocView.draw_overlay function DocView:draw_overlay(...) - if config.lineguide then - local ns = self:get_font():get_width_subpixel("n") * config.line_limit - local ss = self:get_font():subpixel_scale() - local offset = ns / ss - 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 ns = self:get_font():get_width_subpixel("n") * config.line_limit + local ss = self:get_font():subpixel_scale() + local offset = ns / ss + 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 + local color = style.guide or style.selection + renderer.draw_rect(x, y, w, h, color) draw_overlay(self, ...) end From 30ccde896d1ffe37cbd8990e9b8aaef275e18935 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 29 Aug 2021 09:14:12 +0800 Subject: [PATCH 003/135] replace unpack() with table.unpack() I have no idea unpack() is still used and how it still worked. --- data/core/commands/findreplace.lua | 12 ++++++------ data/core/doc/init.lua | 10 +++++----- data/core/tokenizer.lua | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 6dd7ddae..d5d9f6a3 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -36,7 +36,7 @@ local function update_preview(sel, search_fn, text) last_view:scroll_to_line(line2, true) return true else - last_view.doc:set_selection(unpack(sel)) + last_view.doc:set_selection(table.unpack(sel)) return false end end @@ -44,7 +44,7 @@ end local function find(label, search_fn) last_view, last_sel, last_finds = core.active_view, { core.active_view.doc:get_selection() }, {} - local text, found = last_view.doc:get_text(unpack(last_sel)), false + local text, found = last_view.doc:get_text(table.unpack(last_sel)), false core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) @@ -55,8 +55,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) found = update_preview(last_sel, search_fn, text) @@ -64,8 +64,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 diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 4a231295..067cf9e6 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -198,9 +198,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 @@ -301,7 +301,7 @@ 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) } end modified = modified or (cmd.type ~= "selection") @@ -335,7 +335,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 @@ -347,7 +347,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 diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index a20dba5e..bdf6197b 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -155,7 +155,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 From 7e4236a82fc2a7cbef11a5f31fbc194e2f43521c Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 8 Sep 2021 23:09:20 +0800 Subject: [PATCH 004/135] add keymap.unbind(), update contextmenu to use keymap.get_binding() This is something probably people are looking for... --- data/core/contextmenu.lua | 2 +- data/core/keymap.lua | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua index 36247597..d6131cdf 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) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 2be0dfc7..759362d1 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -5,10 +5,9 @@ keymap.modkeys = {} keymap.map = {} keymap.reverse_map = {} -local macos = rawget(_G, "MACOS") -- Thanks to mathewmariani, taken from his lite-macos github repository. -local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic")) +local modkeys_os = require("core.modkeys-" .. (MACOS and "macos" or "generic")) local modkey_map = modkeys_os.map local modkeys = modkeys_os.keys @@ -30,14 +29,15 @@ 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 function keymap.add(map, overwrite) for stroke, commands in pairs(map) do - if macos then + if MACOS then stroke = stroke:gsub("%f[%a]ctrl%f[%A]", "cmd") end if type(commands) == "string" then @@ -52,14 +52,39 @@ 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(cmd, key) + 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 @@ -94,7 +119,7 @@ function keymap.on_key_released(k) end -if macos then +if MACOS then local keymap_macos = require("core.keymap-macos") keymap_macos(keymap) return keymap From 5cc23348a1a7d106aaee36c0eb88b42ed2c9b725 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 8 Sep 2021 23:17:50 +0800 Subject: [PATCH 005/135] reverse the order of args in keymap.unbind --- data/core/keymap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 759362d1..8fd2b05b 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -77,7 +77,7 @@ local function remove_only(tbl, k, v) end -function keymap.unbind(cmd, key) +function keymap.unbind(key, cmd) remove_only(keymap.map, key, cmd) remove_only(keymap.reverse_map, cmd, key) end From 86632b68dec51e84b2daed3ca73a2f262b071538 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 16 Sep 2021 23:26:11 +0200 Subject: [PATCH 006/135] Move single tab drawing to its own function --- data/core/rootview.lua | 91 +++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 0856a619..8a927077 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -513,6 +513,53 @@ function Node:update() end end +function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone) + local ds = style.divider_size + local dots_width = style.font:get_width("…") + local color = style.dim + local padding_y = style.padding.y + renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) + if standalone then + renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2) + end + if is_active then + color = style.text + renderer.draw_rect(x, y, w, h, style.background) + renderer.draw_rect(x + w, y, ds, h, style.divider) + renderer.draw_rect(x - ds, y, ds, h, style.divider) + end + local cx, cw, cspace = close_button_location(x, w) + local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button) + if show_close_button then + local close_style = is_close_hovered and style.text or style.dim + common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) + end + if is_hovered then + color = style.text + end + local padx = style.padding.x + -- Normally we should substract "cspace" from text_avail_width and from the + -- clipping width. It is the padding space we give to the left and right of the + -- close button. However, since we are using dots to terminate filenames, we + -- choose to ignore "cspace" accepting that the text can possibly "touch" the + -- close button. + local text_avail_width = cx - x - padx + core.push_clip_rect(x, y, cx - x, h) + x, w = x + padx, w - padx * 2 + local align = "center" + if style.font:get_width(text) > text_avail_width then + align = "left" + for i = 1, #text do + local reduced_text = text:sub(1, #text - i) + if style.font:get_width(reduced_text) + dots_width <= text_avail_width then + text = reduced_text .. "…" + break + end + end + end + common.draw_text(style.font, color, text, align, x, y, w, h) + core.pop_clip_rect() +end function Node:draw_tabs() local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1) @@ -537,47 +584,9 @@ function Node:draw_tabs() for i = self.tab_offset, self.tab_offset + tabs_number - 1 do local view = self.views[i] local x, y, w, h = self:get_tab_rect(i) - local text = view:get_name() - local color = style.dim - local padding_y = style.padding.y - renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) - if view == self.active_view then - color = style.text - renderer.draw_rect(x, y, w, h, style.background) - renderer.draw_rect(x + w, y, ds, h, style.divider) - renderer.draw_rect(x - ds, y, ds, h, style.divider) - end - local cx, cw, cspace = close_button_location(x, w) - local show_close_button = ((view == self.active_view or i == self.hovered_tab) and config.tab_close_button) - if show_close_button then - local close_style = self.hovered_close == i and style.text or style.dim - common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) - end - if i == self.hovered_tab then - color = style.text - end - local padx = style.padding.x - -- Normally we should substract "cspace" from text_avail_width and from the - -- clipping width. It is the padding space we give to the left and right of the - -- close button. However, since we are using dots to terminate filenames, we - -- choose to ignore "cspace" accepting that the text can possibly "touch" the - -- close button. - local text_avail_width = cx - x - padx - core.push_clip_rect(x, y, cx - x, h) - x, w = x + padx, w - padx * 2 - local align = "center" - if style.font:get_width(text) > text_avail_width then - align = "left" - for i = 1, #text do - local reduced_text = text:sub(1, #text - i) - if style.font:get_width(reduced_text) + dots_width <= text_avail_width then - text = reduced_text .. "…" - break - end - end - end - common.draw_text(style.font, color, text, align, x, y, w, h) - core.pop_clip_rect() + self:draw_tab(view:get_name(), view == self.active_view, + i == self.hovered_tab, i == self.hovered_close, + x, y, w, h) end core.pop_clip_rect() From dced6da03dbded2ca0814ca9a6a8a2e5ac698daa Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 17 Sep 2021 02:47:34 +0200 Subject: [PATCH 007/135] Implement tab drag and drop --- data/core/common.lua | 5 + data/core/rootview.lua | 272 +++++++++++++++++++++++++++++++++++++---- data/core/style.lua | 2 + 3 files changed, 256 insertions(+), 23 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 9f3102bb..1a1b22cd 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -41,6 +41,11 @@ function common.lerp(a, b, t) end +function common.distance(x1, y1, x2, y2) + return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) +end + + function common.color(str) local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") if r then diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8a927077..bf438aa5 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -712,6 +712,62 @@ function Node:resize(axis, value) end +function Node:get_split_type(mouse_x, mouse_y) + local x, y = self.position.x, self.position.y + local w, h = self.size.x, self.size.y + local _, _, _, tab_h = self:get_scroll_button_rect(1) + y = y + tab_h + h = h - tab_h + + local local_mouse_x = mouse_x - x + local local_mouse_y = mouse_y - y + + if local_mouse_y < 0 then + return "tab" + else + local left_pct = local_mouse_x * 100 / w + local top_pct = local_mouse_y * 100 / h + if left_pct <= 30 then + return "left" + elseif left_pct >= 70 then + return "right" + elseif top_pct <= 30 then + return "up" + elseif top_pct >= 70 then + return "down" + end + return "middle" + end +end + + +function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index) + local tab_index = self:get_tab_overlapping_point(x, y) + if not tab_index then + local first_tab_x = self:get_tab_rect(1) + if x < first_tab_x then + -- mouse before first visible tab + tab_index = self.tab_offset or 1 + else + -- mouse after last visible tab + tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0) + end + end + local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index) + if x > tab_x + tab_w / 2 and tab_index <= #self.views then + -- use next tab + tab_x = tab_x + tab_w + tab_index = tab_index + 1 + end + if self == dragged_node and dragged_index and tab_index > dragged_index then + -- the tab we are moving is counted in tab_index + tab_index = tab_index - 1 + tab_x = tab_x - tab_w + end + return tab_index, tab_x, tab_y, tab_w, tab_h +end + + local RootView = View:extend() function RootView:new() @@ -719,6 +775,14 @@ function RootView:new() self.root_node = Node() self.deferred_draws = {} self.mouse = { x = 0, y = 0 } + self.drag_overlay = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0, + base_color = style.drag_overlay, + color = { table.unpack(style.drag_overlay) } } + self.drag_overlay.to = { x = 0, y = 0, w = 0, h = 0 } + self.drag_overlay_tab = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0, + base_color = style.drag_overlay_tab, + color = { table.unpack(style.drag_overlay_tab) } } + self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 } end @@ -802,10 +866,12 @@ function RootView:on_mouse_pressed(button, x, y, clicks) if button == "middle" or node.hovered_close == idx then node:close_view(self.root_node, node.views[idx]) else - self.dragged_node = { node, idx } + 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]) end - else + 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) @@ -814,14 +880,73 @@ function RootView:on_mouse_pressed(button, x, y, clicks) end -function RootView:on_mouse_released(...) +function RootView:get_overlay_base_color(overlay) + if overlay == self.drag_overlay then + return style.drag_overlay + else + return style.drag_overlay_tab + end +end + + +function RootView:set_show_overlay(overlay, status) + overlay.visible = status + if status then -- reset colors + -- reload base_color + overlay.base_color = self:get_overlay_base_color(overlay) + overlay.color[1] = overlay.base_color[1] + overlay.color[2] = overlay.base_color[2] + overlay.color[3] = overlay.base_color[3] + overlay.color[4] = overlay.base_color[4] + overlay.opacity = 0 + end +end + + +function RootView:on_mouse_released(button, x, y, ...) if self.dragged_divider then self.dragged_divider = nil end if self.dragged_node then - self.dragged_node = nil + if button == "left" then + if self.dragged_node.dragging then + local node = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y) + local dragged_node = self.dragged_node.node + + if node and not node.locked + -- don't do anything if dragging onto own node, with only one view + and (node ~= dragged_node or #node.views > 1) then + local split_type = node:get_split_type(self.mouse.x, self.mouse.y) + local view = dragged_node.views[self.dragged_node.idx] + + if split_type ~= "middle" and split_type ~= "tab" then -- needs splitting + local new_node = node:split(split_type) + self.root_node:get_node_for_view(view):remove_view(self.root_node, view) + new_node:add_view(view) + elseif split_type == "middle" and node ~= dragged_node then -- move to other node + dragged_node:remove_view(self.root_node, view) + node:add_view(view) + self.root_node:get_node_for_view(view):set_active_view(view) + elseif split_type == "tab" then -- move besides other tabs + local tab_index = node:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y, dragged_node, self.dragged_node.idx) + dragged_node:remove_view(self.root_node, view) + node:add_view(view, tab_index) + self.root_node:get_node_for_view(view):set_active_view(view) + end + self.root_node:update_layout() + core.redraw = true + end + end + self:set_show_overlay(self.drag_overlay, false) + self:set_show_overlay(self.drag_overlay_tab, false) + if self.dragged_node and self.dragged_node.dragging then + core.request_cursor("arrow") + end + self.dragged_node = nil + end + else -- avoid sending on_mouse_released events when dragging tabs + self.root_node:on_mouse_released(button, x, y, ...) end - self.root_node:on_mouse_released(...) end @@ -857,6 +982,19 @@ function RootView:on_mouse_moved(x, y, dx, dy) end self.mouse.x, self.mouse.y = x, y + + local dn = self.dragged_node + if dn and not dn.dragging then + -- start dragging only after enough movement + dn.dragging = common.distance(x, y, dn.drag_start_x, dn.drag_start_y) > style.tab_width * .05 + if dn.dragging then + core.request_cursor("hand") + end + end + + -- avoid sending on_mouse_moved events when dragging tabs + if dn then return end + self.root_node:on_mouse_moved(x, y, dx, dy) local node = self.root_node:get_child_overlapping_point(x, y) @@ -871,24 +1009,6 @@ function RootView:on_mouse_moved(x, y, dx, dy) elseif node then core.request_cursor(node.active_view.cursor) end - if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index)) - and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then - local tab = self.dragged_node[1].views[self.dragged_node[2]] - if self.dragged_node[1] ~= node then - for i, v in ipairs(node.views) do if v.doc == tab.doc then tab = nil break end end - if tab then - self.dragged_node[1]:remove_view(self.root_node, tab) - node:add_view(tab, tab_index) - self.root_node:update_layout() - self.dragged_node = { node, tab_index or #node.views } - core.redraw = true - end - else - table.remove(self.dragged_node[1].views, self.dragged_node[2]) - table.insert(node.views, tab_index, tab) - self.dragged_node = { node, tab_index } - end - end end @@ -909,10 +1029,110 @@ function RootView:on_focus_lost(...) core.redraw = true end + +function RootView:interpolate_drag_overlay(overlay) + self:move_towards(overlay, "x", overlay.to.x) + self:move_towards(overlay, "y", overlay.to.y) + self:move_towards(overlay, "w", overlay.to.w) + self:move_towards(overlay, "h", overlay.to.h) + + self:move_towards(overlay, "opacity", overlay.visible and 100 or 0) + overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100 +end + + function RootView:update() copy_position_and_size(self.root_node, self) self.root_node:update() self.root_node:update_layout() + + self:update_drag_overlay() + self:interpolate_drag_overlay(self.drag_overlay) + self:interpolate_drag_overlay(self.drag_overlay_tab) +end + + +function RootView:set_drag_overlay(overlay, x, y, w, h, immediate) + overlay.to.x = x + overlay.to.y = y + overlay.to.w = w + overlay.to.h = h + if immediate then + overlay.x = x + overlay.y = y + overlay.w = w + overlay.h = h + end + if not overlay.visible then + self:set_show_overlay(overlay, true) + end +end + + +local function get_split_sizes(split_type, x, y, w, h) + if split_type == "left" then + w = w * .5 + elseif split_type == "right" then + x = x + w * .5 + w = w * .5 + elseif split_type == "up" then + h = h * .5 + elseif split_type == "down" then + y = y + h * .5 + h = h * .5 + end + return x, y, w, h +end + + +function RootView:update_drag_overlay() + if not (self.dragged_node and self.dragged_node.dragging) then return end + local over = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y) + if over and not over.locked then + local _, _, _, tab_h = over:get_scroll_button_rect(1) + local x, y = over.position.x, over.position.y + local w, h = over.size.x, over.size.y + local split_type = over:get_split_type(self.mouse.x, self.mouse.y) + + 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) + self:set_show_overlay(self.drag_overlay, false) + self.drag_overlay_tab.last_over = over + else + if (over ~= self.dragged_node.node or #over.views > 1) then + y = y + tab_h + h = h - tab_h + x, y, w, h = get_split_sizes(split_type, x, y, w, h) + end + self:set_drag_overlay(self.drag_overlay, x, y, w, h) + self:set_show_overlay(self.drag_overlay_tab, false) + end + else + self:set_show_overlay(self.drag_overlay, false) + self:set_show_overlay(self.drag_overlay_tab, false) + end +end + + +function RootView:draw_grabbed_tab() + local dn = self.dragged_node + local _,_, w, h = dn.node:get_tab_rect(dn.idx) + local x = self.mouse.x - w / 2 + local y = self.mouse.y - h / 2 + local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or "" + self.root_node:draw_tab(text, true, true, false, x, y, w, h, true) +end + + +function RootView:draw_drag_overlay(ov) + if ov.opacity > 0 then + renderer.draw_rect(ov.x, ov.y, ov.w, ov.h, ov.color) + end end @@ -922,6 +1142,12 @@ function RootView:draw() local t = table.remove(self.deferred_draws) t.fn(table.unpack(t)) end + + self:draw_drag_overlay(self.drag_overlay) + self:draw_drag_overlay(self.drag_overlay_tab) + if self.dragged_node and self.dragged_node.dragging then + self:draw_grabbed_tab() + end if core.cursor_change_req then system.set_cursor(core.cursor_change_req) core.cursor_change_req = nil diff --git a/data/core/style.lua b/data/core/style.lua index faca166e..9a6efb50 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -44,6 +44,8 @@ style.scrollbar2 = { common.color "#4b4b52" } style.nagbar = { common.color "#FF0000" } style.nagbar_text = { common.color "#FFFFFF" } style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } +style.drag_overlay = { common.color "rgba(255,255,255,0.1)" } +style.drag_overlay_tab = { common.color "#93DDFA" } style.syntax = {} style.syntax["normal"] = { common.color "#e1e1e6" } From d81794417087b4d136775a4e487a4047036438ed Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 17 Sep 2021 02:48:20 +0200 Subject: [PATCH 008/135] Force showing tabs when dragging them --- data/core/rootview.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index bf438aa5..0d219474 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -280,7 +280,10 @@ end function Node:should_show_tabs() if self.locked then return false end - if #self.views > 1 then return true + local dn = core.root_view.dragged_node + if #self.views > 1 + or (dn and dn.dragging) then -- show tabs while dragging + return true elseif config.always_show_tabs then return not self.views[1]:is(EmptyView) end From 0ff0ee2c617c612bac1da6743c2a3f0bf0f101e5 Mon Sep 17 00:00:00 2001 From: Francesco Date: Fri, 17 Sep 2021 22:38:09 +0200 Subject: [PATCH 009/135] Fix numpad fn keys (#532) * Fix the numeric keypad function keys As suggested in: https://github.com/lite-xl/lite-xl/issues/64 * Apply scancode lookup to KEY_UP events --- src/api/system.c | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/api/system.c b/src/api/system.c index 2f1bf763..f69de477 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -26,14 +26,11 @@ static const char* button_name(int button) { } -static char* key_name(char *dst, int sym) { - strcpy(dst, SDL_GetKeyName(sym)); - char *p = dst; +static void str_tolower(char *p) { while (*p) { *p = tolower(*p); p++; } - return dst; } struct HitTestInfo { @@ -93,6 +90,23 @@ static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *p return SDL_HITTEST_NORMAL; } +static const char *numpad[] = { "end", "down", "pagedown", "left", "", "right", "home", "up", "pageup", "ins", "delete" }; + +static const char *get_key_name(const SDL_Event *e, char *buf) { + SDL_Scancode scancode = e->key.keysym.scancode; + /* Is the scancode from the keypad and the number-lock off? + ** We assume that SDL_SCANCODE_KP_1 up to SDL_SCANCODE_KP_9 and SDL_SCANCODE_KP_0 + ** and SDL_SCANCODE_KP_PERIOD are declared in SDL2 in that order. */ + if (scancode >= SDL_SCANCODE_KP_1 && scancode <= SDL_SCANCODE_KP_1 + 10 && + !(KMOD_NUM & SDL_GetModState())) { + return numpad[scancode - SDL_SCANCODE_KP_1]; + } else { + strcpy(buf, SDL_GetKeyName(e->key.keysym.sym)); + str_tolower(buf); + return buf; + } +} + static int f_poll_event(lua_State *L) { char buf[16]; int mx, my, wx, wy; @@ -162,7 +176,7 @@ top: } #endif lua_pushstring(L, "keypressed"); - lua_pushstring(L, key_name(buf, e.key.keysym.sym)); + lua_pushstring(L, get_key_name(&e, buf)); return 2; case SDL_KEYUP: @@ -176,7 +190,7 @@ top: } #endif lua_pushstring(L, "keyreleased"); - lua_pushstring(L, key_name(buf, e.key.keysym.sym)); + lua_pushstring(L, get_key_name(&e, buf)); return 2; case SDL_TEXTINPUT: From e2f7c984de6d23a53e24a8dfaed21843edd161e9 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 17 Sep 2021 23:41:14 +0200 Subject: [PATCH 010/135] Reset syntax highlighting on file rename --- data/plugins/treeview.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 84d5dd28..26481398 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -453,6 +453,7 @@ command.add(function() return view.hovered_item ~= nil end, { for _, doc in ipairs(core.docs) do if doc.abs_filename and old_abs_filename == doc.abs_filename then doc:set_filename(filename, abs_filename) -- make doc point to the new filename + doc:reset_syntax() break -- only first needed end end From ab73f914add4d829d5c90f35fce35a7d38276990 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 18 Sep 2021 15:56:23 -0400 Subject: [PATCH 011/135] Added in custom runtime environment variable for ease of testing. (#538) --- src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 470f0b5e..67d260b2 100644 --- a/src/main.c +++ b/src/main.c @@ -155,7 +155,7 @@ init_lua: " local exedir = EXEFILE:match('^(.*)" LITE_PATHSEP_PATTERN LITE_NONPATHSEP_PATTERN "$')\n" " local prefix = exedir:match('^(.*)" LITE_PATHSEP_PATTERN "bin$')\n" " dofile((MACOS_RESOURCES or (prefix and prefix .. '/share/lite-xl' or exedir .. '/data')) .. '/core/start.lua')\n" - " core = require('core')\n" + " core = require(os.getenv('LITE_XL_RUNTIME') or 'core')\n" " core.init()\n" " core.run()\n" "end, function(err)\n" From 48475c70a09d42cfcfc2350c0855b3a43e15163c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 19 Sep 2021 18:42:36 +0200 Subject: [PATCH 012/135] Avoid unnecessary call to SDL_GetModState --- src/api/system.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/system.c b/src/api/system.c index f69de477..d84f86dd 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -98,7 +98,7 @@ static const char *get_key_name(const SDL_Event *e, char *buf) { ** We assume that SDL_SCANCODE_KP_1 up to SDL_SCANCODE_KP_9 and SDL_SCANCODE_KP_0 ** and SDL_SCANCODE_KP_PERIOD are declared in SDL2 in that order. */ if (scancode >= SDL_SCANCODE_KP_1 && scancode <= SDL_SCANCODE_KP_1 + 10 && - !(KMOD_NUM & SDL_GetModState())) { + !(e->key.keysym.mod & KMOD_NUM)) { return numpad[scancode - SDL_SCANCODE_KP_1]; } else { strcpy(buf, SDL_GetKeyName(e->key.keysym.sym)); From d067cc8577af7023f52dee4c62c44eeb37c1fca5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 19 Sep 2021 18:51:44 +0200 Subject: [PATCH 013/135] Scale custom syntax fonts for scale plugin Close #539. --- data/plugins/scale.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 8d16304b..acf3c7bb 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -56,6 +56,10 @@ local function set_scale(scale) renderer.font.set_size(style.code_font, s * style.code_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) From 34983668d89328bbee94f52d5d86f5ab3043b844 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 19 Sep 2021 23:50:50 +0200 Subject: [PATCH 014/135] Normalize to project dir in treeview open When left-clicking in a TreeView file we use now core.normalize_to_project_dir to normalize correctly the file name. --- data/plugins/treeview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 26481398..fa3ab53a 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -243,7 +243,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) end else core.try(function() - local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) + local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename) core.root_view:open_doc(core.open_doc(doc_filename)) end) end From 291616df3f3dad86fdc724dfa470cd67675c5b48 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 20 Sep 2021 23:50:06 -0400 Subject: [PATCH 015/135] Removed extra macros, used PLATFORM. Also removed MACOS, as it's redundant C code that's already encapsulated within PLATFORM. --- data/core/keymap.lua | 2 +- data/core/start.lua | 1 + src/main.c | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 50eadec6..7f7c0fe2 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -5,7 +5,7 @@ keymap.modkeys = {} keymap.map = {} keymap.reverse_map = {} -local macos = rawget(_G, "MACOS") +local macos = PLATFORM == "Mac OS X" -- Thanks to mathewmariani, taken from his lite-macos github repository. local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic")) diff --git a/data/core/start.lua b/data/core/start.lua index 71050057..24b6d978 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -19,3 +19,4 @@ package.path = DATADIR .. '/?.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path + diff --git a/src/main.c b/src/main.c index 67d260b2..6275be74 100644 --- a/src/main.c +++ b/src/main.c @@ -140,8 +140,6 @@ init_lua: lua_setglobal(L, "EXEFILE"); #ifdef __APPLE__ - lua_pushboolean(L, true); - lua_setglobal(L, "MACOS"); enable_momentum_scroll(); #ifdef MACOS_USE_BUNDLE set_macos_bundle_resources(L); From ed3ea35ed5f9ba403cdba0f8e5c94c769fca41db Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 26 Sep 2021 10:18:13 -0400 Subject: [PATCH 016/135] Potentially fixing issue with cache not invalidating on restart. (#548) --- src/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.c b/src/main.c index 6275be74..a7147e76 100644 --- a/src/main.c +++ b/src/main.c @@ -184,6 +184,7 @@ init_lua: lua_pcall(L, 0, 1, 0); if (lua_toboolean(L, -1)) { lua_close(L); + rencache_invalidate(); goto init_lua; } From 6aa316e3c3498cac448773597c2cc9aa9b257986 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 26 Sep 2021 10:21:57 -0400 Subject: [PATCH 017/135] Rearranged DPI calc so that on calc failure, returns 1. (#547) --- src/main.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main.c b/src/main.c index a7147e76..4eb1ec03 100644 --- a/src/main.c +++ b/src/main.c @@ -18,13 +18,12 @@ SDL_Window *window; static double get_scale(void) { -#ifdef __APPLE__ - return 1.0; -#else - float dpi = 96.0; - SDL_GetDisplayDPI(0, NULL, &dpi, NULL); - return dpi / 96.0; +#ifndef __APPLE__ + float dpi; + if (SDL_GetDisplayDPI(0, NULL, &dpi, NULL) == 0) + return dpi / 96.0; #endif + return 1.0; } From a97a3d80da737d47ac88bce331dff56cba14607e Mon Sep 17 00:00:00 2001 From: Not-a-web-Developer <47886897+Not-a-web-Developer@users.noreply.github.com> Date: Mon, 27 Sep 2021 19:29:25 +0530 Subject: [PATCH 018/135] fixed the build link in readme.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73204c57..71f856a1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ See the [licenses] file for details on licenses used by the required dependencie [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [lite]: https://github.com/rxi/lite [website]: https://lite-xl.github.io -[build]: https://lite-xl.github.io/en/build +[build]: https://lite-xl.github.io/en/documentation/build/ [Get Lite XL]: https://github.com/franko/lite-xl/releases/latest [Get plugins]: https://github.com/franko/lite-plugins [Get color themes]: https://github.com/rxi/lite-colors From 0b4d1e2bce99ba1ebae3722c183dd3b1d644f7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Andr=C3=A9=20Santoni?= Date: Thu, 30 Sep 2021 03:22:00 +0700 Subject: [PATCH 019/135] Fix the size and blurriness of the icon on OSX (#553) * Fix the size and blurriness of the icon on OSX * Don't nest ifndef * Fix --- resources/icons/icon.icns | Bin 37249 -> 44037 bytes src/main.c | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/icons/icon.icns b/resources/icons/icon.icns index 128045d82aaa1c8a01ec0df88e95b851ed9cb259..71fea32f04f3bbc83d1101c8c046eb7b9d3031dd 100644 GIT binary patch literal 44037 zcmeFZ1yEGc|2KU1vcS?yBe{Ttbf>@y5=tr}NGl>BQqm>6q!JQJsg#P8lytA8N{4ic z(kb2V#b5nro_FSr`Oovr^Nh^E-orh6&i8!JH$NxrtQ=hd;3K}Bm8cj1;IBN>(@`fQ zW+Vmxfb5#a)tk^K`urC`0R7FibD4xbV6HdSm4SjT=0&J*)!OLV18r@94|+xburNCS zeqIIoWrThK052T|;6b0T^Z%uT|9*>3$NSIoc}2Aw%0&&UO9Z$=ta7-gpKk z@iNE?i^ix=FHpr0zZCLxdlO)MV>=XFHY|H(mpj^_<@PL6W7XzMJR6^#rVRzzQa<0P~r?x`xVq86b&?Zlr|`(}0j z6Hy*zSkZ>YPi8rJc~6ucGd3i|J2EP2bM(zs4X^nS?qFj-jbv3l)FyQV(Qzq~p7@Fk zKsPls6y|j~bqiHsegY2)3K9`iqFZCC-0l;Vkc!P!X5Z(^o>FYy^JGBl^b=Mi6$y*z zi}qJK?0>JVIr4^sY&-j$&nov1{F?_aDT?xb(~9G{c(4%Tei$YGy_Y1!08TXnyg7U@ zl!YU{%q)X%K-8NlQ($^VPZ8LOjsQz!r;`NBYPpG7!18MFj3|z&yBVycd_qykZ-KIP z?Ah&PbwwmqR}*n1r{8YTd2&I#Jwm!aqP$ys0xWJp=!fs!CuTmwe4Qe{N+i zU=-Ya{YSys9l!p^7hbuMiaoZ-cpF?@rtc2eAy$Z14X}FDKp)bLsnIz^A)^|wjcc#H zAckDKJxrZTh-qFndHqz$2N7~xib^n1HcvuSh;U5wx0lk7Z!WxUW?i==f#aX<4%D-0 z{*l9MTRp6g&_^~CU5BlVa}|x1sXippq=$6+YW#b(UhQ8G&m7tnX#>;(aCIX@*-_D% z76Es@$0Y==>s;LEWzv?}494v^-DWeu$9eF=Kc|O0kNpzGA|`cOO>SDB(46IzM#BiV z9xY_RY(JLicUbF9Mc8S`WTL9hRx{y~4RlT7JpM5$d9oA6dUsTTqW#cN0(s`St(D{k zB)_c3zP0G{XAKwOe)ou8y7-L5t4+=K3=W2-rm`N{lUcY18`+jGGs#7YT@9k$0w!sf z)*Avz=*>eu*x~!aqDjfgTHD%0BqT~T4SJHDfrMYjw8>^^?4=bI^a6_SV#Bly4e9R{ zGpMPnyOs}$xnEa+Ch*b1w{PJJgDIr<5Vd#TF8kb&P1YLiF*X@1SMri8GdE7w19Efk zJ$y)PZ*RZ5rfbJ7JJqrw6QKjiY(xnmDJkhN;6?Ir@WqBKrlP@V^7z}}#MoGb9`CK~ zB+o!PQi7Q+B5BWc)|dK2Es4b$Dk>Hnj3-g$U((ug^zQ~bi`dLcd3!wzMX@eQ3Gc#F z%bX?-jfaP=H{J0tvKw!Zh=E9~%jKfI6$J}eE$?6jKjnX#%K3)ZG(7lYxSAg@kUaa4|MZS|$AJCyF~`miWiolr zWJ3vwiTW9OUeE3wOXskcF=u3#T)TJu&gGGhu}2nfQd6sE2Xpy>`n&G#)}qW$eoJ{P z^P90|b!>r72LnEXU*?GVUrwgHe$B}4Yo?z;J&|!DEM9QGmhE4*41wII_;GZ@lx?W+ z-MNLmrmAzbK-n_T&PqfY0Ep)PZ)`mVs)qSv>zfqswnK&g7F&<`FSh=?2SDRF+!LZe ze)Y=Nhqg5{Ix%Jz=^}CEy6F5mHJ6Ut|+%umaUu_g}8>=a)@tmJj zcrm`_A@4fhadten?D2pO171QC$AIsF|K|^k1Fhu<-yH{s)rSVwg?5a!&4p!=cGt_- zwD({=5+B3}f{qVWZj?rxoSf{HWN5Ymw+tLM5B>BBt3(NbVL3-s2s0m!pPphg;J8hi znwrZ2i*^!uu_E@^@Z%u=lz3~o{kT{y?@x1Bf%J5y-I8q9i`C7elXq{ch^6~qOFIeu z(0;-rXJKIQxIUW@-gj43rSpP*xjM%v#R3U-Y~flkEGZ@B48znD8LIas8YDqV=<0Si z2tUN?(d!u+?rk5}?k2u1DjQgdxSp2cHcXqHE4AQJLa$m4X3$}z<+aJfl)Jj0Q;1iS z+(vJ!^1gSVt%^kgm-~HUJ9xF%Xr}d>5>?lP5CA{@*T}w~I=r26VK#xl5xa`gC zuD6PmAbLRKI!_u1$M`9Ve&+yjx2k@%M*=b3JRgj&0!e%oe#w-;?}O%B5ePv4SAwuO zh*nHXdPoVg%pL!2tpN^m?f#1DcRX8T+aeLwg7%ELytL!NtV&Hfn z#tHn~dX_^ctKF%M5;xtVdv-NJd(l}2eS>cPLIr=Y!PcHYC5>rA!^cJMD3m0yaQx%P zz3kjvxxXc(P?%xjxdE5Vd8`3@#Rt^Is z8w=92vk_8{hGXEsX%q@gn@U;^qRcNxw=o60-oB2!_%-9~-ZN3$iwD9dMCI@tP#I$d zTOoHAsjNwDdMkTVReINZX(ckV+e*L$EP9iQxFV6;n3xF5H8Bs7t4NC`xe+eWMsQh< zByXBES=8o1vFT^{sd)^LuH)m0qggO3W2=2GmYsY=%h8dN8f^DFZnCrUnOPB#Uh8Ki zd6t9AjPp;=xiQR88>0xPb3DC)XF?7ZTdLtH{{PZ!?b~c z_&~G~nYIoNmo|i(k>Xb2fCns3V0YMo zJ+XQ_emUVCH0?Tqv@rl;g-%&eF+MMc2fsezF8_0s1>QIFr%t!L4L zWKPP?Pe_+WGCSzwe-gb5Bmg8mWz0k*#SrvkiZI=06mJ}(?;38dxo6$wW+oHa*uVSK3)(uJ!SpU zA%%2GnEdnn#O$<-7*6nq7~vQU^IDmYKdn7)ZedG?aZ}Fcd60_q71#Pz?NyeHdbS3DDE~xX2oMTjF&dPsd7BTKx| z2yZ+LAJQ_85m{oGrOi4cFB8lXeNkSW9avd0!?WfDNT1%H=Y?ZZ>g>}W!7;Gd73L72 zsKk@(WfUNSic5Ki1Uz;Nu%9Raf~%igX+T`pq@rvfEw?l2hBroR5Cx@{Hf5TvSr&B{6-Og}|VXo8lC;OV_V^ zz6BNyV#%jw_b!P|>KPi8wJdLGv;34VCwFcdbQ$CQsRt_%rx-OmogI7jEUp}#E}eAE zF+_P7*+U?H#+ejtC1JBlh$Jny$9Zh^pD0gM8~|tq{>LcqJUNE}f1*6o$Jr0gOaBcSJq-9yly}|( z0N5-sUIPG{=l_iICITX9=Kb3?*H>28u53_}x-idX-+mf!w{n(TnbgSrDJ{P`NBiU1 zfFha-55BVNnQBXHTnQ#z24pVpw zN)gBWrW-f0I;qpp%}Qd^HRt%`vUo{%y}H~)VY1Dfx5AO%ZnDh-PgNr%8i2w7*Zn~$ z!i4MJ{t&eL8pk!{V_si(GggZ7%+>hN^n|aqhW@$=E*zJgAcy+F^7YsIp)EdzYUZ*} zSx-YA@Bws!#XWgVqbeT0c-Jv< z*2}ga2esw{pP?Tu?e(ec2Nd>U7It!VSf;l2U}{1v+7`CumAa9%R&c9!hmd^b#Cid= z8W-6CjLIL4@U&Zd-;zH{yA-FM65(Q^z-#d1`>m`4k5A2u~c>k58F!FqnM%FdKx7tXbAaN1_sUC zxo3(b0Y9h(Exy~F2|vye&Y81T+TpM~3|?gV5Pi|R;~4=abR6${y5^_Q6%wo*rHH>u z!|BDZP1*v0+uRwWcGXz&j_xP#5km!{r1}gz>=?bE@oDn==kmn>!-d#Pxn@z}g}YY( zY&2yLC9c-Apmf{nl;tRmngpnu&xo1{SXouaJ|~JyBajfhmTUYBA5&lLbmeCJJtf3I z1$yV#`m01OF4 z7*INEV*uxT)f@^C21j53qmSVjdni`77rgF|M4-?B9CyzEZc2>;Ozm$h1wr}&q5!%) zj_FqbY}z>DCL0_?AOTpB5iM*8jsS*?Uml{6XbgbBwB#ZxxzRx7;M)O0EC9mMfWU24 zoFojymCPwVIdhnLU{XrF->fr|HM1 zdttwD*ptV{`!1Jgi2KO{AFJL!a92Hemsuhaxh-Ob#vpHUYYP@z=|?Jl!2$2%40CNfx)s3p~%~mQ>Orz>!CYGqw$O&muDC(Q`B?WV>8zBENhzQ)oygo z%%0^Obytc!kAs85?~SGa&JvsMg6E_k0>389!p-dbhwQv_kMI<(Ni`IsHa0;xu5Il7 zXE{{2XX! zVV50(J6w%~i%r(%u(85oe5g%NfsXg-viL^D_f*sKNN3rnzlJ)ahBS)vvd|ku`h>z#HbQm2n-U zjy?TY?O?RCUt0wVSbC3bayvEtp39!zkR*%Mp3ov4G~E|c1|+_`v-HzJtAl1iziA8! z8Q*`;D&U5Rl*NkM52%4k!-`)taLKo$ZpT`dT<_g&8N7fLVYf7wC^c6H<;qZd6LR`a z7swSWkiG_$#ljwai!>U5mipIXcP&I@l)+Cn&u8QNWlAqII~8t<36orVN4-?+l^ISG zz3hKtw=z{+PG-ykqOy}XmksCP+nH^`ohJz$8|B|Ro6fkb3zPg-C4hzfuO&mf8oHNmgL0x*i}Aly9Qi^yV-3S zBUX}51l2$x$rbA*Wx$jmi}K8X1Uuy{*MscT1thMD6XsoHM|R|;4sioZRXYN#N6kpD zlsr7T%znXFfX0wgzi(j(j$DUOY(nP-`E%gu+aX|VP#NMgCJ(PbQuf>k8E1F5Ag7lw z_>;FHE&?}y6{e+Fkis14a?@_VKGYz>MHfTtHqW5a70z%p5EtXgh8wL2H^ z#3tEIx0Xsd{-Q6PT)xco1St%n-kG}Jk1);1g z-0NNM$8r=b=6k}mrOJ*rygNpU%;4NaXqB8LR`zi$9DTp>P|)(DY25EGMNcH658R&b zCqi@L3T~393&#Ra9gPyXysF+rrXHXt*8|iaAC}~YAd9$|bgOoa6;o&5(L{h&nKup^ z{A;fAtmO*@V+bbP>(}g12leya>RiVzQr|l zxqsAJ+2ZnIobP?F`{?@VliSp7YnQK{NJgG;asw+&L!GQ0i?|Uib>CLn8DE`A+RGmw z`QKj)x^k4`p(Fo2+e>!jBWF~DG1n&#g9weUea}~zpG+iP2L}A9TTNiPJOo>vv)#?_ zY?O)yH+fjyRRiucOlYZlGlGUlklIDyObaj~D474D;O|kOcw|x#{OonO;%4ExoY@35 zyX6Hxa$-rSgJwBl?ZT#|2Js#7?Ff{sb|cLHCEERBx-^MH&k_8=LJm4M zk<5=qf4YGZzAABXy}g9rF_U9as!1#ZA)@Sna->?xn)s>1i*OZ8K9`t_@d z$bd6pA_>VLc=Y?*YtEYrl20=SS_OlF?4a_(g}h<`By6f`Vjhz=x`igMidYwsy`$ z;Au#&BYPQ8x!L;q{YR%?oq=dmD6%k(OK2B$nwS_F$#I(LTuHQE3gt@$vR}*z#b1BC zoL}*6>3g2FmyuFO5I&XxGGYwK7rc#H@K>IzC%YAA%A=#Bx$Ks+Cx>y_Bu8JK1F)Es z@}%Dyt*xy;zwsBoTn{)~|H*GrrQfm6$;o+d4uJ@|bYL{Y39qD>=G6Oiob|X$z;&)8 zHjLFrV+~4IM(XP{C%tCLE-*6o5j8$epitN-PR58^Xdh;9%~A1c9)sDfP7mv@_FsY| z(4k_2o8PeDB^HZaN=)W)wib22?0@(&bnNU)yRc__ET?+z70WGGQnO-`n-n1G>Fv-# zNtM&y-c8*>QBKe`4YR7dd#(ELgJPaxz@5PYGk)RPjlf6a`@y8-MLA0Om zTL&yj*f4Q!?m^Ywr0=h}*G>uQtOve}>+3x5dk-IR!>j_|Gx&lJN?q5+WvhNAnB1SJ zUjO#-PRSec%f4|hG`Rup*YMK_MMd`+JVCGte;jAJY3t;a=;SwRKF@s_?`Ctk91I!T zH=elG_tWGm`SNR~Wl4LvU%7b&_h^toQZVF&-aPZb2vbsgElr#$%l>v&VM0rU$pxsm z@|&L$NZd;v4A6ZeqnN*Abcq$_>OQ@dW`XO}6?LWpC@DML7d`Bql7otBx|b2J zbaA`wIm?Sg^TRTlh!%$H?-B4gO6UuZrCwmW6^}l?x$GTx1FiLe#>xT%JpDQh$C)_Q zadltbimVTjeuqgiy43m!inKY4l1YZEcP@CZmKn;@KJATvy8?y1qNA^>Fi>ptZ8loT zT!D7l?9sjEOkRFSqB}l5py?UflRxN4GP!q-`in%)Zx%*%KGL}vqc|dDh0}~I9x1poUqVzaw_ad z5)IM}rmMRoR|yd|F3>~A4JT(){OB}$Vs>!Q9dO%!9SB4r+4)Uqpl|*d440w~nq%>k zb{yxOI_R?aHrM$&uaH$@#!`o@(!Y|sW$!({m*I49vo?=3%jrVzyWg{HXC;?Uw(0KF zA4hz;*Fs!16UvmEwlD6Tik@_Fb>zQBXMQN?cp6nP@KMRF4o%NxOmGZsh{rGN}oD4u)+3u@^3(f`Jzp_MTxl z%n-^}42|P_P)KDcTfw$+BPt;#R`$gW;9-%_3?L+=uHa~)Y$ZP7Ju4y@%2t%qi;2Jj zDBCG0y|{yhT7n_LC~c&(DwIHFa^(<$?2u_e+CB=zKr@Kp7rdr`M6g4FHQI_AZVJha zsok|DEE)j`Ae`|vYC0TBpss7)q<||!P5$pm(lr8{?bBIkc#Jnt#r1KPzap;{#?bthX1OzUbLNCPbDvIL)-R-uU(-OqjN~IrGpS-^CqX>n;-G}u{Z!b^roV86_P?14u7FND&hvS!{ zDl+7}u9i-Yua(x*)iAaldyzt^7g$(cW}@7zx0#cT`*`t48MO?4(6uG-bE=KL=vk`$ zb@id2Zrk&^G0&avTZO$_DZ)wY6eM7jkY(3N8UJxw%viR2W_DywZ z>Oe`#dg)x8*L>lMm_G+u(11=Wz0gW!f!;={KIS%Aki=4#BBh1_&h3f+%DQm;aSb`k zlDBwN&^2qgCEr87@qwRHnOQLT3qkeaIR2tgWRRF-#?MhAdq8UKG$vxe;`&68YDAzM@}M<<%koe$QuKR903M`_J|;LNGo)=u9Tm zAO4^I{!n$^)gJ;uh(L&2=s#qm|33^tSU^DVzYHNvJ3Pus@LweHS5;m9(iQoiA(fR$ z)qM&7FXbvesmt2J|EhmJgtDrz|E?hb;$c$2pE(c{4-q347lYiQ_*f7TZlx>r_RT^|pf^7&_fLP8i}5|Mw^mk<{Z6hnyrq2}T_nIJ?aBuZ$A zbZ8!+^Ush}i!T3mfrRnID|_79lNE$|FT>9yaxav*)I!$DA4}bZfXBg zGi!gIjrljePLo6Q?JJn%69g`dcue{4t)|U$JUsA#S<+cs)a(YjzV2+bcHyeMToCMp$nbwq zGGhAX)l+_}L#M-N!gD#=)=H=RF*V)w?)yWQ$bg6jrn6l|hBJaG)`qnw3mH+-yN`O4 zC85pKmll>S&gR%Iu(CG19U*OCw(z+&KAkp@IzHMKRZsSWUQw4Cg-*``qv-q5hpc#? zCo7ANex%I_M@ErNm0Pzfoup@x6nf2qI2YndkJZ;YOpLE8`4mq+SKh1hShAKn)TgRH zZ@?7Pzl+-8JI$Y@+an*Bkr+5U{-FIjmCsPOy_X3#oh?_p=3Z!0ZuvRyi9&(K8T*rz zc#q|=1-6i%X^>9vR^je5*fuvz<+Bv$rOEnvZKj=VDV@z?<2-8$C#ZoBE)6qe^bHCh zT6DvDY6n6C7$ez;kSM(y*u+5eQT@K>2{py#qD>}Rmg_SUbO6UkshJus559HH|A#- z{R~@Zt?s9%6g!_Q?Lr3H>SF5Go!M27t;TqM-IF%68jqHK;^m`kcfhD(t+7IaQ6+#y z`qb%JiTZL49fK@1M-Mq-bSA@XT=Sm|AuI2-g!fCWjFc>YeMNc_7J6q4aQ=)>Un~9r z*AQ;!To!dkkx&2R3I@PF!tc8~x&L|nrB?#3#{6Vy=yH)wxBgU@M^T|jkud>gtE*-@ zMHKIJUF}^lM;Fo`pH-I*Svu_tE=IFaH`UqTAFBzFEHKk4={+c@VJQs%nlW0Sth z<7^9?&F&Qt$27Iy`m5M-nfa{y-iZVPszPdn(!S{QCg3Yq;v13a=QQ>EDga{HYN z1>9JR3jCe@cdtvm6$W6Qr2|Pp_?h^y+BU4tQ*$9~E}4u~bjTUUETt zr%qV956w^$(9`v}{6Y61Kn58e4B8M|Y2&RuL~pQyKZ1`@dsp`jjtkEZsn&#s5=YXk z0OV;-w%qX)t^ttvIw@s=(*Iz@EaE*yyo(UpV=UpEu?ljMs)gL0r{V-XCkJj zLQ8j9;N0BYjl;u6mqJHvC6o^zO1-{oYn#30+8{spFwEKO@+r@$u*6}VTl;b{`s4f; zXHL?ih413`$hskAPpXPH+|@ui92@2v({?GH9g8R`PQJ_Rs2%C;E#7D(=?;T-RUggd zWO; z4vW0EJ`{PbPo@%Lx3OncP5I8d;jtU1?Fk_oskS-c<6emhu-nQYJe(Q{gqIB5 zjDq*gV7K?bThxgiA9pttX4wd9b<)J=$e#$p<%e z|6UjpAyi%I>XmMYm->p@c~yEhjA@+?^!fGC40NYL0!z0XtOQL_A()nk#NfQbdN~1$ zCpT?$&@&|7#h-H&U_($OyAWUOK{Rj;8liq!R2dIwr-K`@Nn@EjS=-y*)Vq=Q-9;Cp5BD{`9Yy`c7!7=> zt|b@}#=7m`;`t3j+l6XA3hzmHU4l0+!)4^tEv(C#!8=3<;7)wMCBdbb0#>iDTuwwJ z^g^U{a)Qo>`_F?hULF{vh*@hl9MN$VlJ;kDL&zj7T8^~nLFX&y-(Qvb5`ma%2?cni z4vsYZCl?&SOo;@Z>+NH4$MC+a(@f38(C>MM?n2}e4TBF(*K&}~4%IrCc7kr^E`Fi` zNP)5sh%NV9h~5J+ju-q67qj;KIHr5+6sf?zI=l`dnOAKML`a!XXy-90O#_FBA@)JZ zg+jujTBk7J!=5W2{5?MbF|6PESPO$IhQl#dIr}VFS>pFVk@L)3EF3dn&2ITTWJ?N^ zq(K4?joeh$^3ifkmN69UmNAD3+UatThjxFMnFW6!u<;3Ld2c#$qBaJd0<|P(FlJ=W z<$MwT{ux#oo&V6O2>yTr1$12fYLB>e%hCto9B!!^kv#_YxrhR`5w}a=-#A2Q!Gb7X zFc|acmZ}nh)Eo^k_>pa6(B1eS!hkHq?wc_XaZ4E*^VMXxINUM*2W8AMykqJn3H&jV zq3bTP6^+4lz;7aK%TzSjQNbRSOBO=U=qVVkEBRc6kq-tjLUSd_SDrzo0VNAsrN%S5 zYb+G>wMTJ9j$b+NUT#L}ZK+$mWuaT~(?Mh(_~>QLkO1zv<;7H9Hy{D@aFrRa`eAjq zw^{pc=fcEYj>v)WEQ760rY95d4p_EHOH?8*0*vYjPdip`l-AwfDRNlo6B%H&Qo*33 zAdWE7j_DCG3-&~^TfVIl@pC=+es`=0>0BBeK>g(oBx_aGt!kD8V5bKuEGpKy?KcGp zOnl=v6<7sbNK$pHPX^JUf?6eFa1ua*b9Wc=;kn$9)ycnisF|(~Dx;$Q`^$=h3 z%Ue|}uR|glw~+#16NwNo;n@uea0Fu6Nc;c>LBq3g^L)-+a*a9k$RQnOcJCnpGikj> z-e4%{N%$Nx+#+!TP|$ZL&|4NON6b7z?R@VIH3~l9LMt*y3cVnWc&GvQfe71HHh2vu z;6VYTU6XM{w{*~mY}2o9NjMj%(rlksAb_=kx^KI=DGz>!Y)h`8xB|C`GmPDGnyUu} zZqXruIqhs1xI=>km+pAJ>r#i9mzAKVoW2eUhhV%?02{*Xi7Ak%1A37Zow`SbLUOC1 zYvVVbDPz84(ecn+@EK@Y5WT#GM)2C*b&J69Lo6=D%w7WF4}?(gj`45&L@%#Ek*Eso zHWtkdahE$pBGMSDRK{C!g=!%UgX~eV*W?gkgTX8neI_+AI8l6aV%Qpsvlgo1q8(K56Til?2SFQ0WntoQ8|au^z7K%!r|BXQSg`L5^`#&)W+p2P&ms$a> z?cVq8Wb^~5wFmf;Z>~d>$Me3>q55KhMto^#RWlNTs4(5>VFJtOkl;hJCqe5a7@)_- zIf4^C9giQ3G2Go0z`lhB_V!{Kq$78shVH&(cGjrOb3IGh6iIqNk|cT$@QGq5dSK|nS`-+;GzhQv*OetiGd}eq0>qTH?RRIgkhKiXJTlE2je>k zw9^N{KP-O88$*ug=h-x|xtuCeYbhfSV@K7*G3sF_MerbHPn?ZxoeQ15^(?^abK}NITSf4g;?CGi zM}!dB{AtHg&rcr>f-zl&svnI+>=JZ`XP_$lU#P;qKRKS;4WY z5wq_<^POF=3v0T|JFd8W>b!7Z(}+J)&w#ntm}8rr?Co9`hejBj1{54pqI&GHGO{8m z>@np}eH&^NjxCP{y9ffjX5Q`}c^RQ5R8T?v1Y<680~Y12P3lu}%mvje!Ics3s(qz= z>aN==K}HmnX3R(g89@oTEJB@>3nHS5bbW?8Z1H2OYL%zUi;RdU1~37Ni%nlknEl)L z#8Y`K-n(*OD#5$@MT70Q@a(W8#TWsLH|BEpAO{b$mQ$Lh>p~z1r62#5DExiX*(Lb6 z^AOhSt=qAx;su*Sy%Q~*JMa+?LKl*Rpg_e>1(CG&WBwSv0{c?aPBF{ zqN!Ajw~vpnETmhH2UQCZNQNTF*e<$EoJXr`bRf6nu`~}fjI4)@Brq_9SStjx+#m#} zAXvg@Zj9kOm@g4klD$&fmBS_48WfL;Cr`c zC5WyslN1Bt#E^$0ZXURG2@>9$FRwr-Sau+;T^aKXYrvbVSRaT1Hi%_XIiRSg7gRt0 zw|f7+XA4w0oL0+|2+2?j#h&|&3!jf6x(}EhVgW28*LK0b=~wGmsD~VG~2n z9VW$O95EDn65kzw4xYKw%0K}mEnWzwx_}^_xdPDp`e{)D23k`g!Ed+7{F8GqfQbI3 zLQbe_enhPRTt;<+_Wfl8I>^Eb#P>k(CZ90GQ5n^u-(?@004N%~HNrrITzCyRPU(jv zud<-8>!O`_{!lJV0=X^L&eFX5fdmhN*K$@j!I#%)IpoKbl#T$&mAx+2>-?d zwW{@+y2tiqVVch^UF?hx`B*jth4-*Qs%5;2EQnyY{2KV6=wv;*CesbVnW~w*@v(P^ zBz~yZZvVPsdHp=;I0{H3Yp0WJYP`GL_A+Wc2#PTXY@EI(6AKs>p(I{EFR(VAl%?Pg z!KhfRIy*n&&|+asaGKo9JgLGtH z7jJVG4Y3>h+r-NeyaYhW1&4rXrAS4_1qv9L44JHC+5u+*{umHvZ|>?NGZRd$aySwM zNvSn068L6(;UxDxdUQe9#6wwTZ``$Uf8UF%ExjL(7*K&6u6jz;r)Y3-|IB2{$Bu!K zQH&xzdW}vMgIj?neZ<>a5?cT+zBmh^H)pt=Z_EPa<<<3*B=n7Fk$2ac}uZHfRh-xc=b&;_hd}&zM zLbw@Q(u+W=EIsG$w{V>9wlvm_clvj`Fa_J`Mwomeq12@!M|;dB6>lNrO(sVd?ht^B zk8yVI&g%D0l=t(e4x>}r;nB~ZKM)a9fk43}$W46b&pykfPl6nbsf%jm+$;OWgZVTz zC_c?d?@QDnB)Tt%0`8lc2k}QhYHIf`pi6c>RM&s0e+D{o>Sa{t<4y{rPMOL&fGtWc zwx2Wq1nDe4{e;(q085V#9p{#lCO)i6J382aPJQ1Wu5__7YB){Z_`(?-?0>ZD-iY>z zssewRSu<`XJXz$Q_$_O;>asNTYqdC)Ql=bAwI0zoaP^2|bLwD=<)7ER+T=2U+Vi~Y&HRAPCL6@>4eA~iXS z9F3Zqnp}qETD~t2_7uv=Pj<>BKy`SZ<5><7_7dqE*Dl$+&?0~ohkEe00~Khr6xy$v zVjD-Hlfrasb$gS}-3n(69;>4)eRZh5Nf-ra4Qa(UbM}+G5cFtv5{*&*%`CICMGjpj zpZ?cPE%XhSj(-T(D~)@M<|_A!x%iI#bANn1iWKT;#Z;i0h3 zs|uHIm$HKS3y&@L-zgnBiPF^Olz8&gwFaT@Kw9wWwps&Zv|gg+1Y*b+p;O}Ae!pL& zkv43QoPD5124k+r7vD~y1s)nW20_kl2()v;1>%!5sVg1!ZfG`aV;Mkht%ViI>fQN^ zkga^y`5F>sXFqJtvYe^+ulO08fL(<-Pv*35;&@PcO7PUF7moPJAES(Z#k-$D;m`Zg zq(bPu0v;>;8}#*;IgE$O81!w>&K%d5+5V$5y@0$uL2$W)=*vS4fxaCioqfOBv$paR z+NbCAwKy~Yr ze*bFMMs$>a`_7aE=+(Wl9+`DJ92gMo7a+9NE2Gn(_cms6I(38Aypl-tmL)SxgXn>8 z2GPd5+^aCJnGpKpyC1E0mcxTs2M+isOtEhb8V`Rt<_C?M?zCB z1e!u9K_|j4KA~8wr?7KMB*b2UVwRu+%r8&JH=iKvs>^K98TrKk<*?n)jDdg)VxWqy z>fS6Bz<0Zdx#qRv&iYtIVF^8s6~oN`NJ}4tz@4RWaj1BSa*FU0l$PC``Xmq9y@P-z zx>Ud4*D7w5>D=d!StEirMNpaVI1cFec(MuG%* zXlSS$JB}>yBZV~*5;Z-1{{=se!`%utY094Jm^w_8=}2Ow7#P3`OM-e6v7O64G&XdSj;P zZvTgW3+w)a&ypF!EcF28*m_T1Gzh^$Bk9E_<$gfqZ01G*vTA??{K)_OvHy-_pv=TxG zH-gxXp%B@?+AS2ojzczgoX>c36T)5!5MD!OXFXMx*IUBg9YGNVZ$4zqM*9XTtn9?5 zS1YDich{$!gwg(nc$_@w^ft(bMPntEe6ce#Gez#pgU<6Qo}(lpzKZM6>INKCyawsoVEUUyovHZ90)udvccgy1IAd; zPI+3eVw{TAcSSS|BzF5w11+w^VE{`7-hAlJQ|QtGt^J6GO3}#Y&#SNL+3{yu!4dyV z=pGKtL)z|EK;qmkC>^$4@@KJ~h7*DdX0zj2iQgZAewz^%q|k-J5JtD~(GG+!aY4al z-ef$p1M&GZBGL{zY#Iy9YP5(y-luhc1z@bgkfqPasb(5(rQ z$BSuCgi6O8*ilE}Oeh#w2jPT1TzM0zMDLqm(o=q}`ztm|Ovd@Qon_K(1{k@~C@Y9< zvp?qKO5IA*OMB0LC!R@}dQP~f$ifQflI+W(A^@y8L}p#wcSl|Pn+S#r7lJT=x$!}9F(WTutLiL_x${_N86#MA`U`L%x0 zIIWFZNQg(zL5)l9bp)?=;qOtQLAUSNH z?6>5Fy@4HAxx*M;x#PrWl?N^}(VX$g51vU@DC+nV7eSGSTo_?rJbr)TxdbhtAQ{Q{ z*6Q_>Jn&qlEe`t<8x0}ZbsoH<#3)q1|HHXgl|EFw`t1X z3=pF)8I_rbt`4+3ye>^W3PJT73-6+X8xHecH-3xfZ-Q=ZX^K7>e$}-n-X-&{#M5%7 zQFSxsvjz|P{a&UD2|0_K^={xS0s`dsH5e4SG>>1Qryv!pzhR=64DVC(TiQS$-angI ze^McCF%7L5uN*6OvdL^VQ#W1XNudeCeJZ;D^+(sm#vG*HhvEfIcX*$i=h?}g=dOf7 z){xJuJcvSj>nQu%puR7`he-)JImX?U5o*Zky5E0-_UXEdxx_v-&le+fGk#E-0t|wJ zDFQR-s+>&ps7g$IpHRU0c7UI}(}gC_4J5hG7e!G$=VlK438@!Uh|9$A zZcvn54R}M>>izlFKOqD3`8ul^G$iG&T3~~|(K=$_cq;vC!LDNHygd`>lvS?7@ zIb?qRcJ-Hmf5qZ|o?fA!!z?-4lrK=9t1am*OTTo@_0Nssq`<0*f^#2q>m!un8Kw6k z2wj<;c)-#}9<7jN#G0f;p6>yH6L9y8d{|)`{^46Xy#k`$M*BJW|F8DWJRHjH58&_2 zSkoX%B5}()GsethOSWv46rn;QWGju!NZDqwmn`ie?rvy5)yb6zT~a`1 z^CM!u5fPotZUla9=#@CB$0OXH+0+?7v$+hw5rj6xhEVdm)vHWeVC;|l0IiqDgt$x0 zRGalFweWcTyK{$t-)=k96nN!7n19Og@TiuGDBH*#^i^WfGxM=pzMTuO6Li<8Cs;r_ z!?$eygD)`ck@Ot=*vN5oQ(Q5a-7<7+3%_~;(CBuyb6sq!<`7OdCNm2UCU((v;O^o= zB{FkH+uK=3dApll-U(kt_f8tx7DMgk5V!{gZ#2|)XLu7TdRBYVM>_Mw5) z@vS|1EM`4LnYe2*@Q66Dfz0f7b#yD+20SF)S3!YeO`9Jk0HHbo+&%GJvBb#CK!3nSo!96w<$TF^)1@Ed*%!cDsYVY~MjGpVP0bUX zUuxRX?=PnqzxU-&>R_jKomrE1qnnRzpFZ^1Mz52gjr7o%@`=R=T!B{v1Ts2kb2I5T8ycW@@)=CRSf{lJ2nLQFoY-CFHPX8EM_>46w8Y9FWQLn ztE2?Dej+<3sV+Qsqk%b`2fU}(A`IP6~W zm6IG(C(r^~g%e|x0ZtXBp&~-})-lb^UF~Vf{vSIg>KYt*)D3yktJ{c?pf!>3!K;+= z9j|_!*yzQYspe+ck7&=DW^)uF0bVHye)aijMrGSkm8UVn2z&9dkIHX)wk}?na&bB1 zSBR60%A)6PL0t=TYTDO21YPlT8*XY};jxw+*JtXz(}}&-u*h*w%yJ4|11upnr;m!F9S@63G%8yizI9Xl!L9CcS- z_`a-EZDA?1leBsyrLDH8B%-mD7#KMH;8Y^B|J^7d=o7rkC)!;n_t4x;M#7Vq+)k%H z6){iD^ht1zths8`w;c@jMdQP5MmnR_$b-Et(R?DI&pg5Pz%@g|=KND0;LwUZ-;sl+ zL{$O%RrWItUT;dJ_lRivP5Q$Bqq|mPG5@w7ZXrHjn0BxEsE#m8yHi}}JteQ%=G0J) zKrZEGzeM^t9Q<8zHJChz^$)VG$3|tgzVS+~D!p?)X_cwm>9xXVbWV)o`;7Dpx~AFJ z-s5fb&GETYmFg*=`&vHKi^4}^UA=-77bGg@s=3uX;`M(m3XB49b@}$*^)hnnHPtI+ z!)XPM@>UF@+JU+$W7BAK*W_<~sm!2LF}nlA*^x96j-)u%_38-RADin5C!f>|^W3h9 zDN|f1cbe$wt{W{ItDI-pjk3MLzQH(o15b6P;9uVpQK`inRp7SpSk08_oLOmeV142C@)B{32Ak+gw zJs{KrLOmeV142C@)B{32Ak+gwJs{KrLOmeV142C@)B{32Ak+gwJs{KrLOmdW1x!5$ z6G0&?=idbzL8u3WdO)ZLgnB@z2ZVb3$l2jP8s_rsj1?kO5~TS!Npc1P*RR63zv~LS!xrf{Ehy;`$05+M1bEm%+fZTa!G^T zeS^UR@Uf)t#z80?>>Ld3hmU3~86y~rKpjlF5po@#9*FwZO$ZjIAQvXT)Q^8H3||5V zhVXMRv43M=(ZMJHz0?Vdfz{KdP^lDnuoL>5`ZO$ZF|sl)`2_{}1;I-!V~g0amC?BW z=wV#}WjT@NvzGdY%Zs#{x?3JCE0QH-G(EeYPyJ(uU~KVPS{g3o?=aT`=^H%jwL9^;^E2!_N-Oed_ftuSp}S{c;yQOm@*!$;TCJS?~Z+w;+2C599Rqn zM+n6Se`$eNrV+TYw49V3%KG{=1DH0RwnT!&WKT_#QM#t4W@e^_|0Bv69-$~0q}$rq zni(u7(jO~3X!9qc5mvda>2j@5s+A*S>C1`8;Er1Q`i8pt-;KqM0o9;wLZhe(FcfIt zXFss5;=wBCX9gR@p2%&j|84Drug0@2NhPcIE1tFmu2H7@3r59IJ2bl6kd(ZO2!!=esOn)M8#7#?o*C}e~O&6#4 zs`Gf)DDQ6c|Bvq)OOwaaH;U`kkk2<(brlpGz~S+` zx2LKf^cW#|kwP{czfIXq=US^476LTw>>iZGu6uQD9V{@=MGwmml5^{MotY5M zqWbdxyjm zG24-z7o|_#6{|fJ&0a)YkMQ81Mkb5uLytlnm?hl(vhJN+x+%y ztdicAdZ~`bwKFHNhez_K=AcJX`%$7HP ztZ{s|2>(JyKiAWw-HfIIe4}jDhJtaTyhv1KL{3B93Gv8%1GO~|hISv#B5n ztoo@WOEUYNYrCkVWZkP*NR;k9SzAJU?W0G+7z~CwS7CtWy*r$fot$A$_}|B6@bV{c1wOMj<)J3Rj%A+(71b^ruR)Qg}5cn+qvc+_Ft7Fekj3t2NfqM-V3#5_+a&&Hcze>QKUv zr7U|+ z%JdRlK*krQ?fA2YJIM!!djamh!okVKy~0Y`*V&uo?n$(kcJOv`a|i#kl=kub*-F~e z@u;(t4@n4sh12Z?1@>6%a`tmRvhB-B&v|$)y*)jBsIWA?m1{-UiEY>?O+4Y@!9em0 z2nvfF*kge`=IlqZ0n0BP?U#1)a3GOVR|!$!R7C3PHB=5NCzZ>R=y2TGUQh%SIr}&{ z_&9iYx+;1*yEuD06P=tb-JLv%>^{ZUi_qy*t|m#TpElZj?up&d>T}hZWV^@0z}?9Q zG{nK%FZHKQR(mW=7Y*Q9B_^#vP}^y;*W#dq+s^^PVG*a##>Ab!a=WO!qO$6KW9!>N Twx>Yvu$Kq+OZs#1^?mZc#fzXH literal 37249 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 Date: Thu, 30 Sep 2021 19:38:32 +0800 Subject: [PATCH 020/135] Remove duplicate command declaration (#565) --- data/core/commands/doc.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index fb17e674..b8ce2cb5 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -168,16 +168,6 @@ local commands = { doc():set_selection(line, col) end, - - ["doc:indent"] = function() - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2) - if l1 then - doc():set_selections(idx, l1, c1, l2, c2) - end - end - end, - ["doc:select-lines"] = function() for idx, line1, _, line2 in doc():get_selections(true) do append_line_if_last_line(line2) From f6b9d9ab671fcd7e8df4f69ff8fc1034c66e57d6 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 30 Sep 2021 22:10:38 +0200 Subject: [PATCH 021/135] Add option to disable scrolling past the end (#566) --- data/core/config.lua | 1 + data/core/docview.lua | 3 +++ 2 files changed, 4 insertions(+) diff --git a/data/core/config.lua b/data/core/config.lua index caecdfcd..f11c8959 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -5,6 +5,7 @@ config.fps = 60 config.max_log_items = 80 config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE +config.scroll_past_end = true config.file_size_limit = 10 config.ignore_files = "^%." config.symbol_pattern = "[%a_][%w_]*" diff --git a/data/core/docview.lua b/data/core/docview.lua index 161eac47..c26393a1 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -98,6 +98,9 @@ end function DocView:get_scrollable_size() + if not config.scroll_past_end then + return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + end return self:get_line_height() * (#self.doc.lines - 1) + self.size.y end From 468229e4d0ffef92c506e7f56ce72d9f866db8a3 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 2 Oct 2021 03:24:35 +0200 Subject: [PATCH 022/135] Small cleanup of `scale` plugin --- data/plugins/scale.lua | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index acf3c7bb..37d4cac9 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -13,9 +13,6 @@ config.plugins.scale = { use_mousewheel = true } -local MINIMUM_SCALE = 0.25; - -local scale_level = 0 local scale_steps = 0.05 local current_scale = SCALE @@ -36,9 +33,6 @@ local function set_scale(scale) local s = scale / current_scale current_scale = scale - -- we set scale_level in case this was called by user - scale_level = (scale - default_scale) / scale_steps - if config.plugins.scale.mode == "ui" then SCALE = scale @@ -85,18 +79,15 @@ function RootView:on_mouse_wheel(d, ...) end local function res_scale() - scale_level = 0 set_scale(default_scale) end local function inc_scale() - scale_level = scale_level + 1 - set_scale(default_scale + scale_level * scale_steps) + set_scale(current_scale + scale_steps) end -local function dec_scale() - scale_level = scale_level - 1 - set_scale(math.max(default_scale + scale_level * scale_steps), MINIMUM_SCALE) +local function dec_scale() + set_scale(current_scale - scale_steps) end From 57bfb67f6aa7d4db61ce1382b3489dd3baf9f5bd Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 2 Oct 2021 16:38:10 +0200 Subject: [PATCH 023/135] Add option to disable caret blinking (#572) --- data/core/config.lua | 1 + data/core/docview.lua | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/core/config.lua b/data/core/config.lua index f11c8959..eb4dcdf6 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -24,6 +24,7 @@ config.max_project_files = 2000 config.transitions = true config.animation_rate = 1.0 config.blink_period = 0.8 +config.disable_blink = false config.draw_whitespace = false config.borderless = false config.tab_close_button = true diff --git a/data/core/docview.lua b/data/core/docview.lua index c26393a1..9a2972dc 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -411,10 +411,12 @@ function DocView:draw_overlay() local T = config.blink_period for _, line, col in self.doc:get_selections() do if line >= minline and line <= maxline - and (core.blink_timer - core.blink_start) % T < T / 2 and system.window_has_focus() then - local x, y = self:get_line_screen_position(line) - self:draw_caret(x + self:get_col_x_offset(line, col), y) + if config.disable_blink + or (core.blink_timer - core.blink_start) % T < T / 2 then + local x, y = self:get_line_screen_position(line) + self:draw_caret(x + self:get_col_x_offset(line, col), y) + end end end end From 20ddbd6e9f04690afd85be1c619452e626c74602 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 2 Oct 2021 16:38:45 +0200 Subject: [PATCH 024/135] Load project module on project change (#571) --- data/core/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/core/init.lua b/data/core/init.lua index af291767..7ab4629d 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -79,6 +79,9 @@ function core.open_folder_project(dir_path_abs) if core.set_project_dir(dir_path_abs, core.on_quit_project) then core.root_view:close_all_docviews() update_recents_project("add", dir_path_abs) + if not core.load_project_module() then + command.perform("core:open-log") + end core.on_enter_project(dir_path_abs) end end From 72c950338c9b393a178ceb9ef31cc2ed3004a5f9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 2 Oct 2021 18:44:05 +0200 Subject: [PATCH 025/135] Enable always show tabs by default --- data/core/config.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/config.lua b/data/core/config.lua index eb4dcdf6..36d35c0a 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -12,8 +12,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 From 358903157945a028afdee4d22a429b4be4a1e7d5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 2 Oct 2021 18:44:27 +0200 Subject: [PATCH 026/135] Bump version number --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 047ce90e..41736101 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c', 'cpp'], - version : '2.0.2', + version : '2.0.3', license : 'MIT', meson_version : '>= 0.54', default_options : ['c_std=gnu11', 'cpp_std=c++03'] From db3643653eb79befc9334ac355c17fce6552b860 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 2 Oct 2021 20:01:23 +0200 Subject: [PATCH 027/135] Sanitize selections after redo --- data/core/doc/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index aff31e94..26af4732 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -301,7 +301,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") From 7a435a568a1a1bf6a526f2e61f1195ecc07b3213 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 7 Oct 2021 19:03:16 +0200 Subject: [PATCH 028/135] Fix error in incremental syntax highlighter In the highlither thread We should accept a previously generated line tokenization past first_invalid_line only if the text is the same. The text can change because of insert or remove operations. Close #573. --- data/core/doc/highlighter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index e7650d01..72fba793 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -24,7 +24,7 @@ function Highlighter:new(doc) for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] - if not (line and line.init_state == state) then + if not (line and line.init_state == state and line.text == self.doc.lines[i]) then self.lines[i] = self:tokenize_line(i, state) end end From 92362586df7a2aa4451ef05e3812eef83468e985 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 7 Oct 2021 19:19:03 +0200 Subject: [PATCH 029/135] Improve highlither for document edits The syntax highlighter keep a cache of the documents like tokenization. In order to minimize the amount of tokenize re-computations we insert some emtty lines or remove some lines in the highlither lines corresponding to the lines added or removed to the document. --- data/core/doc/highlighter.lua | 15 ++++++++++++++- data/core/doc/init.lua | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index 72fba793..4cb703da 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -44,12 +44,25 @@ function Highlighter:reset() self.max_wanted_line = 0 end - function Highlighter:invalidate(idx) self.first_invalid_line = math.min(self.first_invalid_line, idx) self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) end +function Highlighter:insert_notify(line, n) + self:invalidate(line) + for i = 1, n do + table.insert(self.lines, line, nil) + end +end + +function Highlighter:remove_notify(line, n) + self:invalidate(line) + for i = 1, n do + table.remove(self.lines, line) + end +end + function Highlighter:tokenize_line(idx, state) local res = {} diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 26af4732..2e72907a 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -349,7 +349,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time) push_undo(undo_stack, time, "remove", line, col, line2, col2) -- update highlighter and assure selection is in bounds - self.highlighter:invalidate(line) + self.highlighter:insert_notify(line, #lines - 1) self:sanitize_selection() end @@ -376,7 +376,7 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) end -- update highlighter and assure selection is in bounds - self.highlighter:invalidate(line1) + self.highlighter:remove_notify(line1, line2 - line1) self:sanitize_selection() end From 9c43727ebc269cb0695f6d418e5bf88615677aaf Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 12 Jul 2021 18:21:27 +0200 Subject: [PATCH 030/135] Implement directory monitoring using septag/dmon Use a notification based directory monitoring based on the septag/dmon lirbary instead of periodically rescan the whole project's tree. --- data/core/commands/core.lua | 6 +- data/core/config.lua | 1 - data/core/init.lua | 476 ++++++-- data/plugins/autoreload.lua | 5 +- data/plugins/treeview.lua | 46 +- licenses/licenses.md | 27 + meson.build | 3 +- resources/notes-dmon-integration.md | 54 + src/api/system.c | 100 ++ src/dirmonitor.c | 60 + src/dirmonitor.h | 14 + src/dmon.h | 1706 +++++++++++++++++++++++++++ src/main.c | 5 + src/meson.build | 1 + 14 files changed, 2333 insertions(+), 171 deletions(-) create mode 100644 resources/notes-dmon-integration.md create mode 100644 src/dirmonitor.c create mode 100644 src/dirmonitor.h create mode 100644 src/dmon.h diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 432ded89..e836ea2f 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,8 +66,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 @@ -191,8 +191,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/config.lua b/data/core/config.lua index 36d35c0a..3de48ee1 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 diff --git a/data/core/init.lua b/data/core/init.lua index 7ab4629d..9ddf68e3 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -52,13 +52,6 @@ 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 @@ -66,9 +59,6 @@ function core.set_project_dir(new_dir, change_project_fn) core.project_dir = common.normalize_path(new_dir) core.project_directories = {} core.add_project_directory(new_dir) - core.project_files = {} - core.project_files_limit = false - core.reschedule_project_scan() return true end return false @@ -102,6 +92,20 @@ 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(info.filename, config.ignore_files) + and info) + end +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 +114,27 @@ 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, begin_hook, max_files) if begin_hook then begin_hook() end - local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} local dirs, files = {}, {} local entries_count = 0 - local max_entries = config.max_project_files for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 - if recursive and entries_count > max_entries then return nil, entries_count end - end + 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 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 (not max_files or entries_count <= max_files) and core.project_subdir_is_shown(dir, f.filename) then + local sub_limit = max_files and max_files - entries_count + local _, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, begin_hook, sub_limit) + entries_count = entries_count + n end end @@ -149,132 +146,289 @@ local function get_directory_files(root, path, t, recursive, begin_hook) return t, 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 - 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_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 - - -- wait for next scan - coroutine.yield(config.project_scan_rate) end end -function core.is_project_folder(dirname) - for _, dir in ipairs(core.project_directories) do - if dir.name == dirname then - return true - end - end - return false +function core.project_subdir_is_shown(dir, filename) + return not dir.files_limit or dir.shown_subdir[filename] 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 show_max_files_warning() + 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 + +-- Populate a project folder top directory by scanning the filesystem. +local function scan_project_folder(index) + local dir = core.project_directories[index] + local t, entries_count = get_directory_files(dir, dir.name, "", {}, nil, config.max_project_files) + if entries_count > config.max_project_files then + dir.files_limit = true + -- 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") + if core.status_view then -- May be not yet initialized. + show_max_files_warning() + end + else + dir.watch_id = system.watch_dir(dir.name, true) + end + dir.files = t + core.dir_rescan_add_job(dir, ".") +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) + 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 + + +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 + repeat + if files[inf].filename == filename then + return inf, true + end + inf = inf + 1 + until inf > sup or system.path_compare(filename, type, files[inf].filename, files[inf].type) + return inf, false +end + + +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 + + +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 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 + +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, {}, 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 -local function find_project_files_co(root, path) - local size_limit = config.file_size_limit * 10e5 + 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 + + +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, {}) 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 - return n + 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 + 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 +525,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 @@ -519,7 +660,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() @@ -530,6 +670,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() + end + for _, filename in ipairs(files) do core.root_view:open_doc(core.open_doc(filename)) end @@ -918,6 +1064,76 @@ 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 + + +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) + end +end + function core.on_event(type, ...) local did_keymap = false @@ -954,6 +1170,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 @@ -1060,7 +1278,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/plugins/autoreload.lua b/data/plugins/autoreload.lua index e772666f..55a2d99e 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -3,9 +3,10 @@ local core = require "core" local config = require "core.config" local Doc = require "core.doc" - local times = setmetatable({}, { __mode = "k" }) +local autoreload_scan_rate = 5 + local function update_time(doc) local info = system.get_file_info(doc.filename) times[doc] = info.modified @@ -40,7 +41,7 @@ core.add_thread(function() end -- wait for next scan - coroutine.yield(config.project_scan_rate) + coroutine.yield(autoreload_scan_rate) end end) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index fa3ab53a..d08db03e 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -41,7 +41,6 @@ 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 } end @@ -54,7 +53,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 +79,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 +104,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 +126,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 +201,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,23 +217,11 @@ 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() @@ -461,7 +443,6 @@ 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, @@ -476,7 +457,6 @@ 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, @@ -489,7 +469,6 @@ command.add(function() return view.hovered_item ~= nil 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 +505,6 @@ command.add(function() return view.hovered_item ~= nil end, { return end end - core.reschedule_project_scan() core.log("Deleted \"%s\"", filename) end end diff --git a/licenses/licenses.md b/licenses/licenses.md index 8005c4a7..928d88d9 100644 --- a/licenses/licenses.md +++ b/licenses/licenses.md @@ -22,6 +22,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## septag/dmon + +Copyright 2019 Sepehr Taghdisian. All rights reserved. + +https://github.com/septag/dmon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ## Fira Sans Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. diff --git a/meson.build b/meson.build index 41736101..f1d2b354 100644 --- a/meson.build +++ b/meson.build @@ -45,6 +45,7 @@ endif if not get_option('source-only') libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) + threads_dep = dependency('threads') lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], default_options: ['shared=false', 'use_readline=false', 'app=false'] ) @@ -57,7 +58,7 @@ if not get_option('source-only') ] ) - lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] + lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, threads_dep] if host_machine.system() == 'windows' # Note that we need to explicitly add the windows socket DLL because diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md new file mode 100644 index 00000000..5179df40 --- /dev/null +++ b/resources/notes-dmon-integration.md @@ -0,0 +1,54 @@ + +`core.set_project_dir`: + Reset project directories and set its directory. + It chdir into the directory, empty the `core.project_directories` and add + the given directory. + `core.add_project_directory`: + Add a new top-level directory to the project. + Also called from modules and commands outside core.init. + local function `scan_project_folder`: + Scan all files for a given top-level project directory. + Can emit a warning about file limit. + Called only from within core.init module. + +`core.scan_project_subdir`: (before was named `core.scan_project_folder`) + scan a single folder, without recursion. Used when too many files. + +Local function `scan_project_folder`: + Populate the project folder top directory. Done only once when the directory + is added to the project. + +`core.add_project_directory`: + Add a new top-level folder to the project. + +`core.set_project_dir`: + Set the initial project directory. + +`core.dir_rescan_add_job`: + Add a job to rescan after an elapsed time a project's subdirectory to fix for any + changes. + +Local function `rescan_project_subdir`: + Rescan a project's subdirectory, compare to the current version and patch the list if + a difference is found. + + +`core.project_scan_thread`: + Should disappear now that we use dmon. + + +`core.project_scan_topdir`: + New function to scan a top level project folder. + + +`config.project_scan_rate`: +`core.project_scan_thread_id`: +`core.reschedule_project_scan`: +`core.project_files_limit`: + A eliminer. + +`core.get_project_files`: + To be fixed. Use `find_project_files_co` for a single directory + +In TreeView remove usage of self.last to detect new scan that changed the files list. + diff --git a/src/api/system.c b/src/api/system.c index d84f86dd..5b72b4d8 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -6,6 +6,7 @@ #include #include #include "api.h" +#include "dirmonitor.h" #include "rencache.h" #ifdef _WIN32 #include @@ -236,6 +237,14 @@ top: lua_pushnumber(L, e.wheel.y); return 2; + case SDL_USEREVENT: + lua_pushstring(L, "dirchange"); + lua_pushnumber(L, e.user.code >> 16); + lua_pushstring(L, (e.user.code & 0xffff) == DMON_ACTION_DELETE ? "delete" : "create"); + lua_pushstring(L, e.user.data1); + free(e.user.data1); + return 4; + default: goto top; } @@ -651,6 +660,91 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +static int f_watch_dir(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + const int recursive = lua_toboolean(L, 2); + uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0); + dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL); + if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } + lua_pushnumber(L, watch_id.id); + return 1; +} + +#if __linux__ +static int f_watch_dir_add(lua_State *L) { + dmon_watch_id watch_id; + watch_id.id = luaL_checkinteger(L, 1); + const char *subdir = luaL_checkstring(L, 2); + lua_pushboolean(L, dmon_watch_add(watch_id, subdir)); + return 1; +} + +static int f_watch_dir_rm(lua_State *L) { + dmon_watch_id watch_id; + watch_id.id = luaL_checkinteger(L, 1); + const char *subdir = luaL_checkstring(L, 2); + lua_pushboolean(L, dmon_watch_rm(watch_id, subdir)); + return 1; +} +#endif + +#ifdef _WIN32 +#define PATHSEP '\\' +#else +#define PATHSEP '/' +#endif + +/* Special purpose filepath compare function. Corresponds to the + order used in the TreeView view of the project's files. Returns true iff + path1 < path2 in the TreeView order. */ +static int f_path_compare(lua_State *L) { + const char *path1 = luaL_checkstring(L, 1); + const char *type1_s = luaL_checkstring(L, 2); + const char *path2 = luaL_checkstring(L, 3); + const char *type2_s = luaL_checkstring(L, 4); + const int len1 = strlen(path1), len2 = strlen(path2); + int type1 = strcmp(type1_s, "dir") != 0; + int type2 = strcmp(type2_s, "dir") != 0; + /* Find the index of the common part of the path. */ + int offset = 0, i; + for (i = 0; i < len1 && i < len2; i++) { + if (path1[i] != path2[i]) break; + if (path1[i] == PATHSEP) { + offset = i + 1; + } + } + /* If a path separator is present in the name after the common part we consider + the entry like a directory. */ + if (strchr(path1 + offset, PATHSEP)) { + type1 = 0; + } + if (strchr(path2 + offset, PATHSEP)) { + type2 = 0; + } + /* If types are different "dir" types comes before "file" types. */ + if (type1 != type2) { + lua_pushboolean(L, type1 < type2); + return 1; + } + /* If types are the same compare the files' path alphabetically. */ + int cfr = 0; + int len_min = (len1 < len2 ? len1 : len2); + for (int j = offset; j <= len_min; j++) { + if (path1[j] == path2[j]) continue; + if (path1[j] == 0 || path2[j] == 0) { + cfr = (path1[j] == 0); + } else if (path1[j] == PATHSEP || path2[j] == PATHSEP) { + /* For comparison we treat PATHSEP as if it was the string terminator. */ + cfr = (path1[j] == PATHSEP); + } else { + cfr = (path1[j] < path2[j]); + } + break; + } + lua_pushboolean(L, cfr); + return 1; +} + static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -678,6 +772,12 @@ static const luaL_Reg lib[] = { { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, + { "watch_dir", f_watch_dir }, + { "path_compare", f_path_compare }, +#if __linux__ + { "watch_dir_add", f_watch_dir_add }, + { "watch_dir_rm", f_watch_dir_rm }, +#endif { NULL, NULL } }; diff --git a/src/dirmonitor.c b/src/dirmonitor.c new file mode 100644 index 00000000..eb3b185f --- /dev/null +++ b/src/dirmonitor.c @@ -0,0 +1,60 @@ +#include +#include + +#include + +#define DMON_IMPL +#include "dmon.h" + +#include "dirmonitor.h" + +static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) { + SDL_Event ev; + const int size = strlen(filepath) + 1; + /* The string allocated below should be deallocated as soon as the event is + treated in the SDL main loop. */ + char *new_filepath = malloc(size); + if (!new_filepath) return; + memcpy(new_filepath, filepath, size); +#ifdef _WIN32 + for (int i = 0; i < size; i++) { + if (new_filepath[i] == '/') { + new_filepath[i] = '\\'; + } + } +#endif + SDL_zero(ev); + ev.type = SDL_USEREVENT; + ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff); + ev.user.data1 = new_filepath; + SDL_PushEvent(&ev); +} + +void dirmonitor_init() { + dmon_init(); + /* In theory we should register our user event but since we + have just one type of user event this is not really needed. */ + /* sdl_dmon_event_type = SDL_RegisterEvents(1); */ +} + +void dirmonitor_deinit() { + dmon_deinit(); +} + +void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, + const char *filepath, const char *oldfilepath, void *user) +{ + (void) rootdir; + (void) user; + switch (action) { + case DMON_ACTION_MOVE: + send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath); + send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath); + break; + case DMON_ACTION_MODIFY: + break; + default: + send_sdl_event(watch_id, action, filepath); + } +} + diff --git a/src/dirmonitor.h b/src/dirmonitor.h new file mode 100644 index 00000000..ab9376c0 --- /dev/null +++ b/src/dirmonitor.h @@ -0,0 +1,14 @@ +#ifndef DIRMONITOR_H +#define DIRMONITOR_H + +#include + +#include "dmon.h" + +void dirmonitor_init(); +void dirmonitor_deinit(); +void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, + const char *filepath, const char *oldfilepath, void *user); + +#endif + diff --git a/src/dmon.h b/src/dmon.h new file mode 100644 index 00000000..1ccae446 --- /dev/null +++ b/src/dmon.h @@ -0,0 +1,1706 @@ +// +// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Portable directory monitoring library +// watches directories for file or directory changes. +// +// Usage: +// define DMON_IMPL and include this file to use it: +// #define DMON_IMPL +// #include "dmon.h" +// +// dmon_init(): +// Call this once at the start of your program. +// This will start a low-priority monitoring thread +// dmon_deinit(): +// Call this when your work with dmon is finished, usually on program terminate +// This will free resources and stop the monitoring thread +// dmon_watch: +// Watch for directories +// You can watch multiple directories by calling this function multiple times +// rootdir: root directory to monitor +// watch_cb: callback function to receive events. +// NOTE that this function is called from another thread, so you should +// beware of data races in your application when accessing data within this +// callback +// flags: watch flags, see dmon_watch_flags_t +// user_data: user pointer that is passed to callback function +// Returns the Id of the watched directory after successful call, or returns Id=0 if error +// dmon_unwatch: +// Remove the directory from watch list +// +// see test.c for the basic example +// +// Configuration: +// You can customize some low-level functionality like malloc and logging by overriding macros: +// +// DMON_MALLOC, DMON_FREE, DMON_REALLOC: +// define these macros to override memory allocations +// default is 'malloc', 'free' and 'realloc' +// DMON_ASSERT: +// define this to provide your own assert +// default is 'assert' +// DMON_LOG_ERROR: +// define this to provide your own logging mechanism +// default implementation logs to stdout and breaks the program +// DMON_LOG_DEBUG +// define this to provide your own extra debug logging mechanism +// default implementation logs to stdout in DEBUG and does nothing in other builds +// DMON_API_DECL, DMON_API_IMPL +// define these to provide your own API declerations. (for example: static) +// default is nothing (which is extern in C language ) +// DMON_MAX_PATH +// Maximum size of path characters +// default is 260 characters +// DMON_MAX_WATCHES +// Maximum number of watch directories +// default is 64 +// +// TODO: +// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files +// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS +// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES +// +// History: +// 1.0.0 First version. working Win32/Linux backends +// 1.1.0 MacOS backend +// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall +// 1.1.2 Eliminate some win32 dead code +// 1.1.3 Fixed select not resetting causing high cpu usage on linux +// +#ifndef __DMON_H__ +#define __DMON_H__ + +#include +#include + +#ifndef DMON_API_DECL +# define DMON_API_DECL +#endif + +#ifndef DMON_API_IMPL +# define DMON_API_IMPL +#endif + +typedef struct { uint32_t id; } dmon_watch_id; + +// Pass these flags to `dmon_watch` +typedef enum dmon_watch_flags_t { + DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories + DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only) + DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet + DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet +} dmon_watch_flags; + +// Action is what operation performed on the file. this value is provided by watch callback +typedef enum dmon_action_t { + DMON_ACTION_CREATE = 1, + DMON_ACTION_DELETE, + DMON_ACTION_MODIFY, + DMON_ACTION_MOVE +} dmon_action; + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL void dmon_init(void); +DMON_API_DECL void dmon_deinit(void); + +DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* rootdir, const char* filepath, + const char* oldfilepath, void* user), + uint32_t flags, void* user_data); +DMON_API_DECL void dmon_unwatch(dmon_watch_id id); +DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); +DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL + +#define DMON_OS_WINDOWS 0 +#define DMON_OS_MACOS 0 +#define DMON_OS_LINUX 0 + +#if defined(_WIN32) || defined(_WIN64) +# undef DMON_OS_WINDOWS +# define DMON_OS_WINDOWS 1 +#elif defined(__linux__) +# undef DMON_OS_LINUX +# define DMON_OS_LINUX 1 +#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) +# undef DMON_OS_MACOS +# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#else +# define DMON_OS 0 +# error "unsupported platform" +#endif + +#if DMON_OS_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +# ifdef _MSC_VER +# pragma intrinsic(_InterlockedExchange) +# endif +#elif DMON_OS_LINUX +# ifndef __USE_MISC +# define __USE_MISC +# endif +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#elif DMON_OS_MACOS +# include +# include +# include +# include +# include +#endif + +#ifndef DMON_MALLOC +# include +# define DMON_MALLOC(size) malloc(size) +# define DMON_FREE(ptr) free(ptr) +# define DMON_REALLOC(ptr, size) realloc(ptr, size) +#endif + +#ifndef DMON_ASSERT +# include +# define DMON_ASSERT(e) assert(e) +#endif + +#ifndef DMON_LOG_ERROR +# include +# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) +#endif + +#ifndef DMON_LOG_DEBUG +# ifndef NDEBUG +# include +# define DMON_LOG_DEBUG(s) do { puts(s); } while(0) +# else +# define DMON_LOG_DEBUG(s) +# endif +#endif + +#ifndef DMON_MAX_WATCHES +# define DMON_MAX_WATCHES 64 +#endif + +#ifndef DMON_MAX_PATH +# define DMON_MAX_PATH 260 +#endif + +#define _DMON_UNUSED(x) (void)(x) + +#ifndef _DMON_PRIVATE +# if defined(__GNUC__) || defined(__clang__) +# define _DMON_PRIVATE __attribute__((unused)) static +# else +# define _DMON_PRIVATE static +# endif +#endif + +#include + +#ifndef _DMON_LOG_ERRORF +# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0); +#endif + +#ifndef _DMON_LOG_DEBUGF +# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); +#endif + +#ifndef dmon__min +# define dmon__min(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef dmon__max +# define dmon__max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef dmon__swap +# define dmon__swap(a, b, _type) \ + do { \ + _type tmp = a; \ + a = b; \ + b = tmp; \ + } while (0) +#endif + +#ifndef dmon__make_id +# ifdef __cplusplus +# define dmon__make_id(id) {id} +# else +# define dmon__make_id(id) (dmon_watch_id) {id} +# endif +#endif // dmon__make_id + +_DMON_PRIVATE bool dmon__isrange(char ch, char from, char to) +{ + return (uint8_t)(ch - from) <= (uint8_t)(to - from); +} + +_DMON_PRIVATE bool dmon__isupperchar(char ch) +{ + return dmon__isrange(ch, 'A', 'Z'); +} + +_DMON_PRIVATE char dmon__tolowerchar(char ch) +{ + return ch + (dmon__isupperchar(ch) ? 0x20 : 0); +} + +_DMON_PRIVATE char* dmon__tolower(char* dst, int dst_sz, const char* str) +{ + int offset = 0; + int dst_max = dst_sz - 1; + while (*str && offset < dst_max) { + dst[offset++] = dmon__tolowerchar(*str); + ++str; + } + dst[offset] = '\0'; + return dst; +} + +_DMON_PRIVATE char* dmon__strcpy(char* dst, int dst_sz, const char* src) +{ + DMON_ASSERT(dst); + DMON_ASSERT(src); + + const int32_t len = (int32_t)strlen(src); + const int32_t _max = dst_sz - 1; + const int32_t num = (len < _max ? len : _max); + memcpy(dst, src, num); + dst[num] = '\0'; + + return dst; +} + +_DMON_PRIVATE char* dmon__unixpath(char* dst, int size, const char* path) +{ + size_t len = strlen(path); + len = dmon__min(len, (size_t)size - 1); + + for (size_t i = 0; i < len; i++) { + if (path[i] != '\\') + dst[i] = path[i]; + else + dst[i] = '/'; + } + dst[len] = '\0'; + return dst; +} + +#if DMON_OS_LINUX || DMON_OS_MACOS +_DMON_PRIVATE char* dmon__strcat(char* dst, int dst_sz, const char* src) +{ + int len = (int)strlen(dst); + return dmon__strcpy(dst + len, dst_sz - len, src); +} +#endif // DMON_OS_LINUX || DMON_OS_MACOS + +// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h +#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) +#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) +#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) +#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) +#define stb_sb_last(a) ((a)[stb__sbn(a)-1]) +#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0) + +#define stb__sbraw(a) ((int *) (a) - 2) +#define stb__sbm(a) stb__sbraw(a)[0] +#define stb__sbn(a) stb__sbraw(a)[1] + +#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a)) +#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0) +#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a)))) + +static void * stb__sbgrowf(void *arr, int increment, int itemsize) +{ + int dbl_cur = arr ? 2*stb__sbm(arr) : 0; + int min_needed = stb_sb_count(arr) + increment; + int m = dbl_cur > min_needed ? dbl_cur : min_needed; + int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2); + if (p) { + if (!arr) + p[1] = 0; + p[0] = m; + return p+2; + } else { + return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later + } +} + +// watcher callback (same as dmon.h's decleration) +typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); + +#if DMON_OS_WINDOWS +// IOCP (windows) +#ifdef UNICODE +# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size) +#else +# define _DMON_WINAPI_STR(name, size) const char* _##name = name +#endif + +typedef struct dmon__win32_event { + char filepath[DMON_MAX_PATH]; + DWORD action; + dmon_watch_id watch_id; + bool skip; +} dmon__win32_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + OVERLAPPED overlapped; + HANDLE dir_handle; + uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx + DWORD notify_filter; + dmon__watch_cb* watch_cb; + uint32_t watch_flags; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char old_filepath[DMON_MAX_PATH]; +} dmon__watch_state; + +typedef struct dmon__state { + int num_watches; + dmon__watch_state watches[DMON_MAX_WATCHES]; + HANDLE thread_handle; + CRITICAL_SECTION mutex; + volatile LONG modify_watches; + dmon__win32_event* events; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE bool dmon__refresh_watch(dmon__watch_state* watch) +{ + return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), + (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE, + watch->notify_filter, NULL, &watch->overlapped, NULL) != 0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + CancelIo(watch->dir_handle); + CloseHandle(watch->overlapped.hEvent); + CloseHandle(watch->dir_handle); + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +_DMON_PRIVATE void dmon__win32_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) { + // remove duplicate modifies on a single file + for (int j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_MODIFIED && + strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + } + } + } + } + + // trigger user callbacks + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + switch (ev->action) { + case FILE_ACTION_ADDED: + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_MODIFIED: + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_RENAMED_OLD_NAME: { + // find the first occurance of the NEW_NAME + // this is somewhat API flaw that we have no reference for relating old and new files + for (int j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } break; + case FILE_ACTION_REMOVED: + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + } + } + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) +{ + _DMON_UNUSED(arg); + HANDLE wait_handles[DMON_MAX_WATCHES]; + + SYSTEMTIME starttm; + GetSystemTime(&starttm); + uint64_t msecs_elapsed = 0; + + while (!_dmon.quit) { + if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { + Sleep(10); + continue; + } + + if (_dmon.num_watches == 0) { + Sleep(10); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + wait_handles[i] = watch->overlapped.hEvent; + } + + DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); + DMON_ASSERT(wait_result != WAIT_FAILED); + if (wait_result != WAIT_TIMEOUT) { + dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; + DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); + + DWORD bytes; + if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { + char filepath[DMON_MAX_PATH]; + PFILE_NOTIFY_INFORMATION notify; + size_t offset = 0; + + if (bytes == 0) { + dmon__refresh_watch(watch); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + do { + notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset]; + + int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName, + notify->FileNameLength / sizeof(WCHAR), + filepath, DMON_MAX_PATH - 1, NULL, NULL); + filepath[count] = TEXT('\0'); + dmon__unixpath(filepath, sizeof(filepath), filepath); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + msecs_elapsed = 0; + } + dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false }; + dmon__strcpy(wev.filepath, sizeof(wev.filepath), filepath); + stb_sb_push(_dmon.events, wev); + + offset += notify->NextEntryOffset; + } while (notify->NextEntryOffset > 0); + + if (!_dmon.quit) { + dmon__refresh_watch(watch); + } + } + } // if (WaitForMultipleObjects) + + SYSTEMTIME tm; + GetSystemTime(&tm); + LONG dt = + (tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds); + starttm = tm; + msecs_elapsed += dt; + if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) { + dmon__win32_process_events(); + msecs_elapsed = 0; + } + + LeaveCriticalSection(&_dmon.mutex); + } + return 0; +} + + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + InitializeCriticalSection(&_dmon.mutex); + + _dmon.thread_handle = + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL); + DMON_ASSERT(_dmon.thread_handle); + _dmon_init = true; +} + + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { + WaitForSingleObject(_dmon.thread_handle, INFINITE); + CloseHandle(_dmon.thread_handle); + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + DeleteCriticalSection(&_dmon.mutex); + stb_sb_free(_dmon.events); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir); + size_t rootdir_len = strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH); + watch->dir_handle = + CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (watch->dir_handle != INVALID_HANDLE_VALUE) { + watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE; + watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE); + + if (!dmon__refresh_watch(watch)) { + dmon__unwatch(watch); + DMON_LOG_ERROR("ReadDirectoryChanges failed"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + } else { + _DMON_LOG_ERRORF("Could not open: %s", rootdir); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); +} + +#elif DMON_OS_LINUX +// inotify linux backend +#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) + +typedef struct dmon__watch_subdir { + char rootdir[DMON_MAX_PATH]; +} dmon__watch_subdir; + +typedef struct dmon__inotify_event { + char filepath[DMON_MAX_PATH]; + uint32_t mask; + uint32_t cookie; + dmon_watch_id watch_id; + bool skip; +} dmon__inotify_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + int fd; + uint32_t watch_flags; + dmon__watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + dmon__watch_subdir* subdirs; + int* wds; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state watches[DMON_MAX_WATCHES]; + dmon__inotify_event* events; + int num_watches; + pthread_t thread_handle; + pthread_mutex_t mutex; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask, + bool followlinks, dmon__watch_state* watch) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char watchdir[DMON_MAX_PATH]; + + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + if (entry->d_type == DT_DIR) { + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + dmon__strcpy(watchdir, sizeof(watchdir), dirname); + dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); + entry_valid = true; + } + } else if (followlinks && entry->d_type == DT_LNK) { + char linkpath[PATH_MAX]; + dmon__strcpy(watchdir, sizeof(watchdir), dirname); + dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); + char* r = realpath(watchdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + dmon__strcpy(watchdir, sizeof(watchdir), linkpath); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + int watchdir_len = (int)strlen(watchdir); + if (watchdir[watchdir_len - 1] != '/') { + watchdir[watchdir_len] = '/'; + watchdir[watchdir_len + 1] = '\0'; + } + int wd = inotify_add_watch(fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recurse + dmon__watch_recursive(watchdir, fd, mask, followlinks, watch); + } + } + closedir(dir); +} + +DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + // check if the directory exists + // if watchdir contains absolute/root-included path, try to strip the rootdir from it + // else, we assume that watchdir is correct, so save it as it is + struct stat st; + dmon__watch_subdir subdir; + if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + } else { + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), watchdir); + if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + } + + int dirlen = (int)strlen(subdir.rootdir); + if (subdir.rootdir[dirlen - 1] != '/') { + subdir.rootdir[dirlen] = '/'; + subdir.rootdir[dirlen + 1] = '\0'; + } + + // check that the directory is not already added + for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { + if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { + _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + } + + const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); + int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); + if (wd == -1) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + + return true; +} + +DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + char subdir[DMON_MAX_PATH]; + dmon__strcpy(subdir, sizeof(subdir), watchdir); + if (strstr(subdir, watch->rootdir) == subdir) { + dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); + } + + int dirlen = (int)strlen(subdir); + if (subdir[dirlen - 1] != '/') { + subdir[dirlen] = '/'; + subdir[dirlen + 1] = '\0'; + } + + int i, c = stb_sb_count(watch->subdirs); + for (i = 0; i < c; i++) { + if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { + break; + } + } + if (i >= c) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + inotify_rm_watch(watch->fd, watch->wds[i]); + + for (int j = i; j < c - 1; j++) { + memcpy(watch->subdirs + j, watch->subdirs + j + 1, sizeof(dmon__watch_subdir)); + memcpy(watch->wds + j, watch->wds + j + 1, sizeof(int)); + } + stb__sbraw(watch->subdirs)[1] = c - 1; + stb__sbraw(watch->wds)[1] = c - 1; + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return true; +} + +_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd) +{ + const int* wds = watch->wds; + for (int i = 0, c = stb_sb_count(wds); i < c; i++) { + if (wd == wds[i]) { + return watch->subdirs[i].rootdir; + } + } + + return NULL; +} + +_DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char newdir[DMON_MAX_PATH]; + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + bool is_dir = false; + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + dmon__strcpy(newdir, sizeof(newdir), dirname); + dmon__strcat(newdir, sizeof(newdir), entry->d_name); + is_dir = (entry->d_type == DT_DIR); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir)); + } + + dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0), 0, watch->id, false }; + dmon__strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir); + stb_sb_push(_dmon.events, dev); + } + } + closedir(dir); +} + +_DMON_PRIVATE void dmon__inotify_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->mask & IN_MODIFY) { + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { + // in some cases, particularly when created files under sub directories + // there can be two modify events for a single subdir one with trailing slash and one without + // remove traling slash from both cases and test + int l1 = (int)strlen(ev->filepath); + int l2 = (int)strlen(check_ev->filepath); + if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; + if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0'; + if (strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } + } else if (ev->mask & IN_CREATE) { + bool loop_break = false; + for (int j = i + 1; j < c && !loop_break; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // there is a case where some programs (like gedit): + // when we save, it creates a temp file, and moves it to the file being modified + // search for these cases and remove all of them + for (int k = j + 1; k < c; k++) { + dmon__inotify_event* third_ev = &_dmon.events[k]; + if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { + third_ev->mask = IN_MODIFY; // change to modified + ev->skip = check_ev->skip = true; + loop_break = true; + break; + } + } + } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // Another case is that file is copied. CREATE and MODIFY happens sequentially + // so we ignore MODIFY event + check_ev->skip = true; + } + } + } else if (ev->mask & IN_MOVED_FROM) { + bool move_valid = false; + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE + if (!move_valid) { + ev->mask = IN_DELETE; + } + } else if (ev->mask & IN_MOVED_TO) { + bool move_valid = false; + for (int j = 0; j < i; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin, on undo it is moved back it + // so if the destination of the move is not valid, it's probably CREATE + if (!move_valid) { + ev->mask = IN_CREATE; + } + } else if (ev->mask & IN_DELETE) { + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + // if the file is DELETED and then MODIFIED after, just ignore the modify event + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + break; + } + } + } + } + + // trigger user callbacks + for (int i = 0; i < stb_sb_count(_dmon.events); i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->mask & IN_CREATE) { + if (ev->mask & IN_ISDIR) { + if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { + char watchdir[DMON_MAX_PATH]; + dmon__strcpy(watchdir, sizeof(watchdir), watch->rootdir); + dmon__strcat(watchdir, sizeof(watchdir), ev->filepath); + dmon__strcat(watchdir, sizeof(watchdir), "/"); + uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // some directories may be already created, for instance, with the command: mkdir -p + // so we will enumerate them manually and add them to the events + dmon__gather_recursive(watch, watchdir); + ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated + } + } + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MODIFY) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MOVED_FROM) { + for (int j = i + 1; j < stb_sb_count(_dmon.events); j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } + else if (ev->mask & IN_DELETE) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* dmon__thread(void* arg) +{ + _DMON_UNUSED(arg); + + static uint8_t buff[_DMON_TEMP_BUFFSIZE]; + struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; + struct timespec rem = { 0, 0 }; + struct timeval timeout; + uint64_t usecs_elapsed = 0; + + struct timeval starttm; + gettimeofday(&starttm, 0); + + while (!_dmon.quit) { + nanosleep(&req, &rem); + if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + continue; + } + + // Create read FD set + fd_set rfds; + FD_ZERO(&rfds); + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + FD_SET(watch->fd, &rfds); + } + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + if (FD_ISSET(watch->fd, &rfds)) { + ssize_t offset = 0; + ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE); + if (len <= 0) { + continue; + } + + while (offset < len) { + struct inotify_event* iev = (struct inotify_event*)&buff[offset]; + + const char *subdir = dmon__find_subdir(watch, iev->wd); + if (subdir) { + char filepath[DMON_MAX_PATH]; + dmon__strcpy(filepath, sizeof(filepath), subdir); + dmon__strcat(filepath, sizeof(filepath), iev->name); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + usecs_elapsed = 0; + } + dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false }; + dmon__strcpy(dev.filepath, sizeof(dev.filepath), filepath); + stb_sb_push(_dmon.events, dev); + } + + offset += sizeof(struct inotify_event) + iev->len; + } + } + } + } + + struct timeval tm; + gettimeofday(&tm, 0); + long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec; + starttm = tm; + usecs_elapsed += dt; + if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) { + dmon__inotify_process_events(); + usecs_elapsed = 0; + } + + pthread_mutex_unlock(&_dmon.mutex); + } + return 0x0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + close(watch->fd); + stb_sb_free(watch->subdirs); + stb_sb_free(watch->wds); + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", + rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + } else { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + watch->fd = inotify_init(); + if (watch->fd < -1) { + DMON_LOG_ERROR("could not create inotify instance"); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + + uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); + if (wd < 0) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recursive mode: enumarate all child directories and add them to watch + if (flags & DMON_WATCHFLAGS_RECURSIVE) { + dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask, + (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); + } + + + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + pthread_mutex_lock(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + pthread_mutex_unlock(&_dmon.mutex); +} +#elif DMON_OS_MACOS +// FSEvents MacOS backend +typedef struct dmon__fsevent_event { + char filepath[DMON_MAX_PATH]; + uint64_t event_id; + long event_flags; + dmon_watch_id watch_id; + bool skip; + bool move_valid; +} dmon__fsevent_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + uint32_t watch_flags; + FSEventStreamRef fsev_stream_ref; + dmon__watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char rootdir_unmod[DMON_MAX_PATH]; + bool init; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state watches[DMON_MAX_WATCHES]; + dmon__fsevent_event* events; + int num_watches; + volatile int modify_watches; + pthread_t thread_handle; + dispatch_semaphore_t thread_sem; + pthread_mutex_t mutex; + CFRunLoopRef cf_loop_ref; + CFAllocatorRef cf_alloc_ref; + bool quit; +} dmon__state; + +union dmon__cast_userdata { + void* ptr; + uint32_t id; +}; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void* dmon__cf_malloc(CFIndex size, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_MALLOC(size); +} + +_DMON_PRIVATE void dmon__cf_free(void* ptr, void* info) +{ + _DMON_UNUSED(info); + DMON_FREE(ptr); +} + +_DMON_PRIVATE void* dmon__cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_REALLOC(ptr, (size_t)newsize); +} + +_DMON_PRIVATE void dmon__fsevent_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) && + strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) && + check_ev->event_id == (ev->event_id + 1)) { + ev->move_valid = check_ev->move_valid = true; + break; + } + } + + // in some environments like finder file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE or CREATE + // decide CREATE if file exists + if (!ev->move_valid) { + ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; + + char abs_filepath[DMON_MAX_PATH]; + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id-1]; + dmon__strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir); + dmon__strcat(abs_filepath, sizeof(abs_filepath), ev->filepath); + + struct stat root_st; + if (stat(abs_filepath, &root_st) != 0) { + ev->event_flags |= kFSEventStreamEventFlagItemRemoved; + } else { + ev->event_flags |= kFSEventStreamEventFlagItemCreated; + } + } + } + } + + // trigger user callbacks + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->event_flags & kFSEventStreamEventFlagItemCreated) { + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* dmon__thread(void* arg) +{ + _DMON_UNUSED(arg); + + struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; + struct timespec rem = { 0, 0 }; + + _dmon.cf_loop_ref = CFRunLoopGetCurrent(); + dispatch_semaphore_signal(_dmon.thread_sem); + + while (!_dmon.quit) { + if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) { + nanosleep(&req, &rem); + continue; + } + + if (_dmon.num_watches == 0) { + nanosleep(&req, &rem); + pthread_mutex_unlock(&_dmon.mutex); + continue; + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + if (!watch->init) { + DMON_ASSERT(watch->fsev_stream_ref); + FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, + kCFRunLoopDefaultMode); + FSEventStreamStart(watch->fsev_stream_ref); + + watch->init = true; + } + } + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut); + dmon__fsevent_process_events(); + + pthread_mutex_unlock(&_dmon.mutex); + } + + CFRunLoopStop(_dmon.cf_loop_ref); + _dmon.cf_loop_ref = NULL; + return 0x0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + if (watch->fsev_stream_ref) { + FSEventStreamStop(watch->fsev_stream_ref); + FSEventStreamInvalidate(watch->fsev_stream_ref); + FSEventStreamRelease(watch->fsev_stream_ref); + watch->fsev_stream_ref = NULL; + } + + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + CFAllocatorContext cf_alloc_ctx = { 0 }; + cf_alloc_ctx.allocate = dmon__cf_malloc; + cf_alloc_ctx.deallocate = dmon__cf_free; + cf_alloc_ctx.reallocate = dmon__cf_realloc; + _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx); + + _dmon.thread_sem = dispatch_semaphore_create(0); + DMON_ASSERT(_dmon.thread_sem); + + int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + // wait for thread to initialize loop object + dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER); + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + dispatch_release(_dmon.thread_sem); + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + if (_dmon.cf_alloc_ref) { + CFRelease(_dmon.cf_alloc_ref); + } + + _dmon_init = false; +} + +_DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data, + size_t num_events, void* event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]) +{ + _DMON_UNUSED(stream_ref); + + union dmon__cast_userdata _userdata; + _userdata.ptr = user_data; + dmon_watch_id watch_id = dmon__make_id(_userdata.id); + DMON_ASSERT(watch_id.id > 0); + dmon__watch_state* watch = &_dmon.watches[watch_id.id - 1]; + char abs_filepath[DMON_MAX_PATH]; + char abs_filepath_lower[DMON_MAX_PATH]; + + for (size_t i = 0; i < num_events; i++) { + const char* filepath = ((const char**)event_paths)[i]; + long flags = (long)event_flags[i]; + uint64_t event_id = (uint64_t)event_ids[i]; + dmon__fsevent_event ev; + memset(&ev, 0x0, sizeof(ev)); + + dmon__strcpy(abs_filepath, sizeof(abs_filepath), filepath); + dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); + + // normalize path, so it would be the same on both MacOS file-system types (case/nocase) + dmon__tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); + DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); + + // strip the root dir from the begining + dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + + ev.event_flags = flags; + ev.event_id = event_id; + ev.watch_id = watch_id; + stb_sb_push(_dmon.events, ev); + } +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + } else { + char rootdir_abspath[DMON_MAX_PATH]; + if (realpath(rootdir, rootdir_abspath) != NULL) { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath); + } else { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + } + + dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + dmon__strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir); + dmon__tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // create FS objects + CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8); + CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL); + + FSEventStreamContext ctx; + union dmon__cast_userdata userdata; + userdata.id = id; + ctx.version = 0; + ctx.info = userdata.ptr; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, dmon__fsevent_callback, &ctx, + cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, + kFSEventStreamCreateFlagFileEvents); + + + CFRelease(cf_dirarr); + CFRelease(cf_dir); + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); +} + +#endif + +#endif // DMON_IMPL +#endif // __DMON_H__ diff --git a/src/main.c b/src/main.c index 182511d0..e0fcb107 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,8 @@ #include #endif +#include "dirmonitor.h" + SDL_Window *window; @@ -107,6 +109,8 @@ int main(int argc, char **argv) { SDL_DisplayMode dm; SDL_GetCurrentDisplayMode(0, &dm); + dirmonitor_init(); + window = SDL_CreateWindow( "", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); @@ -189,6 +193,7 @@ init_lua: lua_close(L); ren_free_window_resources(); + dirmonitor_deinit(); return EXIT_SUCCESS; } diff --git a/src/meson.build b/src/meson.build index 707e04e9..2da04fda 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,6 +6,7 @@ lite_sources = [ 'api/regex.c', 'api/system.c', 'api/process.c', + 'dirmonitor.c', 'renderer.c', 'renwindow.c', 'fontdesc.c', From bba42adc73bf592b9de4aa4a5d44410618ec59ae Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 8 Oct 2021 21:55:43 +0200 Subject: [PATCH 031/135] Adopt new version of dmon --- src/dirmonitor.c | 1 + src/dirmonitor.h | 1 + src/dmon.h | 130 ++----------------------------------- src/dmon_extra.h | 162 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 124 deletions(-) create mode 100644 src/dmon_extra.h diff --git a/src/dirmonitor.c b/src/dirmonitor.c index eb3b185f..958d463e 100644 --- a/src/dirmonitor.c +++ b/src/dirmonitor.c @@ -5,6 +5,7 @@ #define DMON_IMPL #include "dmon.h" +#include "dmon_extra.h" #include "dirmonitor.h" diff --git a/src/dirmonitor.h b/src/dirmonitor.h index ab9376c0..074a9ae8 100644 --- a/src/dirmonitor.h +++ b/src/dirmonitor.h @@ -4,6 +4,7 @@ #include #include "dmon.h" +#include "dmon_extra.h" void dirmonitor_init(); void dirmonitor_deinit(); diff --git a/src/dmon.h b/src/dmon.h index 1ccae446..ed10d11f 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -1,3 +1,6 @@ +#ifndef __DMON_H__ +#define __DMON_H__ + // // Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. // License: https://github.com/septag/dmon#license-bsd-2-clause @@ -68,9 +71,9 @@ // 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall // 1.1.2 Eliminate some win32 dead code // 1.1.3 Fixed select not resetting causing high cpu usage on linux +// 1.2.1 inotify (linux) fixes and improvements, added extra functionality header for linux +// to manually add/remove directories manually to the watch handle, in case of large file sets // -#ifndef __DMON_H__ -#define __DMON_H__ #include #include @@ -114,8 +117,6 @@ DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, const char* oldfilepath, void* user), uint32_t flags, void* user_data); DMON_API_DECL void dmon_unwatch(dmon_watch_id id); -DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); -DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); #ifdef __cplusplus } @@ -322,6 +323,7 @@ _DMON_PRIVATE char* dmon__strcat(char* dst, int dst_sz, const char* src) // stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h #define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) #define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) +#define stb_sb_pop(a) (stb__sbn(a)--) #define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) #define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) #define stb_sb_last(a) ((a)[stb__sbn(a)-1]) @@ -763,126 +765,6 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m closedir(dir); } -DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) -{ - DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); - - bool skip_lock = pthread_self() == _dmon.thread_handle; - - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); - - dmon__watch_state* watch = &_dmon.watches[id.id - 1]; - - // check if the directory exists - // if watchdir contains absolute/root-included path, try to strip the rootdir from it - // else, we assume that watchdir is correct, so save it as it is - struct stat st; - dmon__watch_subdir subdir; - if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); - } - } else { - char fullpath[DMON_MAX_PATH]; - dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); - dmon__strcat(fullpath, sizeof(fullpath), watchdir); - if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { - _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - } - - int dirlen = (int)strlen(subdir.rootdir); - if (subdir.rootdir[dirlen - 1] != '/') { - subdir.rootdir[dirlen] = '/'; - subdir.rootdir[dirlen + 1] = '\0'; - } - - // check that the directory is not already added - for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { - if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { - _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - } - - const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; - char fullpath[DMON_MAX_PATH]; - dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); - dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); - int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); - if (wd == -1) { - _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - - stb_sb_push(watch->subdirs, subdir); - stb_sb_push(watch->wds, wd); - - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - - return true; -} - -DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) -{ - DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); - - bool skip_lock = pthread_self() == _dmon.thread_handle; - - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); - - dmon__watch_state* watch = &_dmon.watches[id.id - 1]; - - char subdir[DMON_MAX_PATH]; - dmon__strcpy(subdir, sizeof(subdir), watchdir); - if (strstr(subdir, watch->rootdir) == subdir) { - dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); - } - - int dirlen = (int)strlen(subdir); - if (subdir[dirlen - 1] != '/') { - subdir[dirlen] = '/'; - subdir[dirlen + 1] = '\0'; - } - - int i, c = stb_sb_count(watch->subdirs); - for (i = 0; i < c; i++) { - if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { - break; - } - } - if (i >= c) { - _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - inotify_rm_watch(watch->fd, watch->wds[i]); - - for (int j = i; j < c - 1; j++) { - memcpy(watch->subdirs + j, watch->subdirs + j + 1, sizeof(dmon__watch_subdir)); - memcpy(watch->wds + j, watch->wds + j + 1, sizeof(int)); - } - stb__sbraw(watch->subdirs)[1] = c - 1; - stb__sbraw(watch->wds)[1] = c - 1; - - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return true; -} - _DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd) { const int* wds = watch->wds; diff --git a/src/dmon_extra.h b/src/dmon_extra.h new file mode 100644 index 00000000..4b321034 --- /dev/null +++ b/src/dmon_extra.h @@ -0,0 +1,162 @@ +#ifndef __DMON_EXTRA_H__ +#define __DMON_EXTRA_H__ + +// +// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Extra header functionality for dmon.h for the backend based on inotify +// +// Add/Remove directory functions: +// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take +// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one +// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user +// will be reached. The default maximum is 8192. +// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the +// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched. +// The function dmon_watch_add and dmon_watch_rm are used to this purpose. +// + +#ifndef __DMON_H__ +#error "Include 'dmon.h' before including this file" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); +DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL +#if DMON_OS_LINUX +DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + // check if the directory exists + // if watchdir contains absolute/root-included path, try to strip the rootdir from it + // else, we assume that watchdir is correct, so save it as it is + struct stat st; + dmon__watch_subdir subdir; + if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + } else { + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), watchdir); + if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + } + + int dirlen = (int)strlen(subdir.rootdir); + if (subdir.rootdir[dirlen - 1] != '/') { + subdir.rootdir[dirlen] = '/'; + subdir.rootdir[dirlen + 1] = '\0'; + } + + // check that the directory is not already added + for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { + if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { + _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + } + + const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); + int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); + if (wd == -1) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + + return true; +} + +DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + char subdir[DMON_MAX_PATH]; + dmon__strcpy(subdir, sizeof(subdir), watchdir); + if (strstr(subdir, watch->rootdir) == subdir) { + dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); + } + + int dirlen = (int)strlen(subdir); + if (subdir[dirlen - 1] != '/') { + subdir[dirlen] = '/'; + subdir[dirlen + 1] = '\0'; + } + + int i, c = stb_sb_count(watch->subdirs); + for (i = 0; i < c; i++) { + if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { + break; + } + } + if (i >= c) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + inotify_rm_watch(watch->fd, watch->wds[i]); + + /* Remove entry from subdirs and wds by swapping position with the last entry */ + watch->subdirs[i] = stb_sb_last(watch->subdirs); + stb_sb_pop(watch->subdirs); + + watch->wds[i] = stb_sb_last(watch->wds); + stb_sb_pop(watch->wds); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return true; +} +#endif // DMON_OS_LINUX +#endif // DMON_IMPL + +#endif // __DMON_EXTRA_H__ + From a9f6f01ed03594ff25b8af99f094785b35b4065d Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 8 Oct 2021 22:10:17 +0200 Subject: [PATCH 032/135] Move dmon files into lib/dmon --- {src => lib/dmon}/dmon.h | 0 {src => lib/dmon}/dmon_extra.h | 0 lib/font_renderer/meson.build | 2 +- meson.build | 5 ++--- src/meson.build | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) rename {src => lib/dmon}/dmon.h (100%) rename {src => lib/dmon}/dmon_extra.h (100%) diff --git a/src/dmon.h b/lib/dmon/dmon.h similarity index 100% rename from src/dmon.h rename to lib/dmon/dmon.h diff --git a/src/dmon_extra.h b/lib/dmon/dmon_extra.h similarity index 100% rename from src/dmon_extra.h rename to lib/dmon/dmon_extra.h diff --git a/lib/font_renderer/meson.build b/lib/font_renderer/meson.build index d596e152..569de826 100644 --- a/lib/font_renderer/meson.build +++ b/lib/font_renderer/meson.build @@ -9,7 +9,7 @@ font_renderer_sources = [ font_renderer_cdefs = ['-DFONT_RENDERER_HEIGHT_HACK'] -font_renderer_include = include_directories('.') +lite_includes += include_directories('.') libfontrenderer = static_library('fontrenderer', font_renderer_sources, diff --git a/meson.build b/meson.build index f1d2b354..613204b7 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,7 @@ endif cc = meson.get_compiler('c') +lite_includes = [] lite_cargs = [] # On macos we need to use the SDL renderer to support retina displays if get_option('renderer') or host_machine.system() == 'darwin' @@ -118,11 +119,9 @@ configure_file( install_dir : lite_datadir / 'core', ) -#=============================================================================== -# Targets -#=============================================================================== if not get_option('source-only') subdir('lib/font_renderer') + subdir('lib/dmon') subdir('src') subdir('scripts') endif diff --git a/src/meson.build b/src/meson.build index 2da04fda..503fdc48 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,11 +22,11 @@ elif host_machine.system() == 'darwin' lite_sources += 'bundle_open.m' endif -lite_include = include_directories('.') +lite_includes += include_directories('.') executable('lite-xl', lite_sources + lite_rc, - include_directories: [lite_include, font_renderer_include], + include_directories: lite_includes, dependencies: lite_deps, c_args: lite_cargs, objc_args: lite_cargs, From 911a3cee08c32cf7a2d17146494a9eff48073f64 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 8 Oct 2021 23:13:50 +0200 Subject: [PATCH 033/135] Report dmon modify events --- data/core/init.lua | 7 +++++++ src/api/system.c | 14 +++++++++++++- src/dirmonitor.c | 2 -- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 9ddf68e3..d964b624 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1123,6 +1123,10 @@ function core.dir_rescan_add_job(dir, filepath) end +function core.on_dirmonitor_modify() +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 @@ -1131,6 +1135,9 @@ function core.on_dir_change(watch_id, action, filepath) 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 diff --git a/src/api/system.c b/src/api/system.c index 5b72b4d8..c1a4abd3 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -240,7 +240,19 @@ top: case SDL_USEREVENT: lua_pushstring(L, "dirchange"); lua_pushnumber(L, e.user.code >> 16); - lua_pushstring(L, (e.user.code & 0xffff) == DMON_ACTION_DELETE ? "delete" : "create"); + switch (e.user.code & 0xffff) { + case DMON_ACTION_DELETE: + lua_pushstring(L, "delete"); + break; + case DMON_ACTION_CREATE: + lua_pushstring(L, "create"); + break; + case DMON_ACTION_MODIFY: + lua_pushstring(L, "modify"); + break; + default: + return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff); + } lua_pushstring(L, e.user.data1); free(e.user.data1); return 4; diff --git a/src/dirmonitor.c b/src/dirmonitor.c index 958d463e..0063e400 100644 --- a/src/dirmonitor.c +++ b/src/dirmonitor.c @@ -52,8 +52,6 @@ void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath); send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath); break; - case DMON_ACTION_MODIFY: - break; default: send_sdl_event(watch_id, action, filepath); } From 7dd5699c96f852c41204c927c3a8d5d9410fc100 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 8 Oct 2021 23:15:25 +0200 Subject: [PATCH 034/135] Use dmon events in reload plugin --- data/core/init.lua | 1 + data/plugins/autoreload.lua | 27 ++++++++++----------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index d964b624..9de283ad 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1123,6 +1123,7 @@ function core.dir_rescan_add_job(dir, filepath) end +-- no-op but can be overrided by plugins function core.on_dirmonitor_modify() end diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index 55a2d99e..9978092e 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -5,14 +5,11 @@ local Doc = require "core.doc" local times = setmetatable({}, { __mode = "k" }) -local autoreload_scan_rate = 5 - local function update_time(doc) local info = system.get_file_info(doc.filename) times[doc] = info.modified end - local function reload_doc(doc) local fp = io.open(doc.filename, "r") local text = fp:read("*a") @@ -28,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(autoreload_scan_rate) end -end) - + on_modify(dir, filepath) +end -- patch `Doc.save|load` to store modified time local load = Doc.load From a99dd947ed85aa86ca29d416a7bd07bdce4dee84 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 9 Oct 2021 14:37:33 +0200 Subject: [PATCH 035/135] Add missing meson file --- lib/dmon/meson.build | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/dmon/meson.build diff --git a/lib/dmon/meson.build b/lib/dmon/meson.build new file mode 100644 index 00000000..83edd1c9 --- /dev/null +++ b/lib/dmon/meson.build @@ -0,0 +1 @@ +lite_includes += include_directories('.') From 56eace627a0702da5713240b8e5e2544cb6e9aa7 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 10 Sep 2021 21:32:34 +0200 Subject: [PATCH 036/135] Add reverse option to `search.find` --- data/core/doc/search.lua | 71 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index 04090673..7ff2dca7 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -23,6 +23,45 @@ local function init_args(doc, line, col, text, opt) end +local function plain_rfind(text, pattern, index) + local len = #text + text = text:reverse() + pattern = pattern:reverse() + if index >= 0 then + index = len - index + 1 + else + index = index * -1 + end + local s, e = text:find(pattern, index, true) + return e and len - e + 1, s and len - s + 1 +end + + +local function rfind(text, pattern, index, plain) + if plain then return plain_rfind(text, pattern, index) end + local s, e = text:find(pattern) + 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 = text:find(pattern, s + 1) + end + return last_s, last_e +end + + +local function rcmatch(re, text, index) + local s, e = re:cmatch(text) + local last_s, last_e + if index < 0 then index = #text - index + 1 end + while e and e <= index + 1 do + last_s, last_e = s, e + s, e = re:cmatch(text, s + 1) + 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) @@ -30,29 +69,47 @@ function search.find(doc, line, col, text, opt) if opt.regex then re = regex.compile(text, opt.no_case and "i" or "") 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) + local s, e + if opt.reverse then + s, e = rcmatch(re, line_text, col - 1) + else + s, e = re:cmatch(line_text, col) + end if s then return line, s, line, e end - col = 1 + col = opt.reverse and -1 or 1 else if opt.no_case then line_text = line_text:lower() end - local s, e = line_text:find(text, col, true) + local s, e + if opt.reverse then + s, e = rfind(line_text, text, col - 1, true) + else + s, e = line_text:find(text, col, true) + end if s then return line, s, line, e + 1 end - col = 1 + col = opt.reverse and -1 or 1 end 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 From e7be9652c96a99a5e218bdfd84dc06290ca176bf Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 10 Sep 2021 21:54:50 +0200 Subject: [PATCH 037/135] Add `find-replace:select-previous` --- data/core/commands/findreplace.lua | 25 ++++++++++++++++--------- data/core/keymap.lua | 1 + 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index f8e8e45a..e1153fe5 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -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,15 +161,22 @@ 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", { diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 7f7c0fe2..0c2dd863 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -170,6 +170,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", From 1976facaf1a2b9934f3cfdb4524712c1418000ef Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 10 Sep 2021 21:58:11 +0200 Subject: [PATCH 038/135] Use reverse search for `find-replace:previous-find` --- data/core/commands/findreplace.lua | 39 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index e1153fe5..cff023d2 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -7,8 +7,7 @@ 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 @@ -53,8 +52,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 @@ -181,8 +180,8 @@ command.add(has_unique_selection, { 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, @@ -228,29 +227,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, }) From af925d603bc7632ff87918ad1ad080032fb3c0a5 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 11 Sep 2021 03:37:12 +0200 Subject: [PATCH 039/135] Fix `doc` selection in `findreplace` Use `last_view` if `active_view` is `CommandView`. --- data/core/commands/findreplace.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index cff023d2..67aa90d5 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -14,7 +14,8 @@ 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.doc end local function get_find_tooltip() From cfe0c79a0415ab9089f0cb6f3c1d421ce29b87fd Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 9 Oct 2021 00:24:48 +0200 Subject: [PATCH 040/135] Simplify reverse search Remove `plain_rfind` optimization. --- data/core/doc/search.lua | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index 7ff2dca7..babf9cc8 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -23,28 +23,13 @@ local function init_args(doc, line, col, text, opt) end -local function plain_rfind(text, pattern, index) - local len = #text - text = text:reverse() - pattern = pattern:reverse() - if index >= 0 then - index = len - index + 1 - else - index = index * -1 - end - local s, e = text:find(pattern, index, true) - return e and len - e + 1, s and len - s + 1 -end - - local function rfind(text, pattern, index, plain) - if plain then return plain_rfind(text, pattern, index) end - local s, e = text:find(pattern) + local s, e = text:find(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 = text:find(pattern, s + 1) + s, e = text:find(pattern, s + 1, plain) end return last_s, last_e end From 3a1274fd08dfc3d6a9c3a0e659e3bb856ed791c0 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 10 Oct 2021 00:42:30 +0200 Subject: [PATCH 041/135] Merge reverse find functions for lua patterns and regexes --- data/core/doc/search.lua | 68 +++++++++++++++------------------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index babf9cc8..b4c553c9 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -22,26 +22,20 @@ 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(text, pattern, index, plain) - local s, e = text:find(pattern, 1, plain) +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 = text:find(pattern, s + 1, plain) - end - return last_s, last_e -end - - -local function rcmatch(re, text, index) - local s, e = re:cmatch(text) - local last_s, last_e - if index < 0 then index = #text - index + 1 end - while e and e <= index + 1 do - last_s, last_e = s, e - s, e = re:cmatch(text, s + 1) + s, e = func(text, pattern, s + 1, plain) end return last_s, last_e end @@ -49,10 +43,11 @@ end function search.find(doc, line, col, text, opt) doc, line, col, text, opt = init_args(doc, line, col, text, opt) - - local re + 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 local start, finish, step = line, #doc.lines, 1 if opt.reverse then @@ -60,32 +55,19 @@ function search.find(doc, line, col, text, opt) end for line = start, finish, step do local line_text = doc.lines[line] - if opt.regex then - local s, e - if opt.reverse then - s, e = rcmatch(re, line_text, col - 1) - else - s, e = re:cmatch(line_text, col) - end - if s then - return line, s, line, e - end - col = opt.reverse and -1 or 1 - else - if opt.no_case then - line_text = line_text:lower() - end - local s, e - if opt.reverse then - s, e = rfind(line_text, text, col - 1, true) - else - s, e = line_text:find(text, col, true) - end - if s then - return line, s, line, e + 1 - end - col = opt.reverse and -1 or 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) + else + s, e = search_func(line_text, pattern, col) + end + if s then + return line, s, line, e + 1 + end + col = opt.reverse and -1 or 1 end if opt.wrap then From cb08c5cbb7e3679fa90d40d784d223fa197aeb5a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 10 Oct 2021 14:52:55 +0200 Subject: [PATCH 042/135] Fix dirty pixels problem on window's right side The last column of pixel on the window's right side isn't correctly drawn and pixels appear dirty and more noticeably when the a NagView message was previously shown, a stripe of red pixels remains on the right. We use now a more souding roundig scheme. Now the rectangles to clip or to draw are passed around as Lua numbers without any rounding. In turns, when the rect coordinates are passed to the renderer we ensure the border of the rect are correctly snapped to the pixel's grid. It works by computing the coordinates of the edges, round them to integers and then compute the rect's width based on the rounded coordinates values. --- data/core/rootview.lua | 2 +- data/core/view.lua | 2 +- src/api/renderer.c | 27 +++++++++++++++++---------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 0d219474..fa200aec 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -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, pos.x + size.x, pos.y + size.y) self.active_view:draw() core.pop_clip_rect() else diff --git a/data/core/view.lua b/data/core/view.lua index d1374ee4..5b4b3228 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -140,7 +140,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/src/api/renderer.c b/src/api/renderer.c index 8dc13ada..ec3bc3ba 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -49,23 +49,30 @@ static int f_end_frame(lua_State *L) { } +static RenRect rect_to_grid(lua_Number x, lua_Number y, lua_Number w, lua_Number h) { + int x1 = (int) (x + 0.5), y1 = (int) (y + 0.5); + int x2 = (int) (x + w + 0.5), y2 = (int) (y + h + 0.5); + return (RenRect) {x1, y1, x2 - x1, y2 - y1}; +} + + static int f_set_clip_rect(lua_State *L) { - RenRect rect; - rect.x = luaL_checknumber(L, 1); - rect.y = luaL_checknumber(L, 2); - rect.width = luaL_checknumber(L, 3); - rect.height = luaL_checknumber(L, 4); + lua_Number x = luaL_checknumber(L, 1); + lua_Number y = luaL_checknumber(L, 2); + lua_Number w = luaL_checknumber(L, 3); + lua_Number h = luaL_checknumber(L, 4); + RenRect rect = rect_to_grid(x, y, w, h); rencache_set_clip_rect(rect); return 0; } static int f_draw_rect(lua_State *L) { - RenRect rect; - rect.x = luaL_checknumber(L, 1); - rect.y = luaL_checknumber(L, 2); - rect.width = luaL_checknumber(L, 3); - rect.height = luaL_checknumber(L, 4); + lua_Number x = luaL_checknumber(L, 1); + lua_Number y = luaL_checknumber(L, 2); + lua_Number w = luaL_checknumber(L, 3); + lua_Number h = luaL_checknumber(L, 4); + RenRect rect = rect_to_grid(x, y, w, h); RenColor color = checkcolor(L, 5, 255); rencache_draw_rect(rect, color); return 0; From 0f8d7f32022723c8fde16060a3bd9fe81f14c339 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 10 Oct 2021 14:58:51 +0200 Subject: [PATCH 043/135] Do no add rencache a command for empty rectangles --- src/rencache.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rencache.c b/src/rencache.c index 31165e90..5a64a5ea 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -160,7 +160,9 @@ void rencache_set_clip_rect(RenRect rect) { void rencache_draw_rect(RenRect rect, RenColor color) { - if (!rects_overlap(screen_rect, rect)) { return; } + if (!rects_overlap(screen_rect, rect) || rect.width == 0 || rect.height == 0) { + return; + } Command *cmd = push_command(DRAW_RECT, COMMAND_BARE_SIZE); if (cmd) { cmd->rect = rect; From c7aa3ebe0116b08d16c50e0262f00f8c3dd54601 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 10 Oct 2021 21:44:16 +0200 Subject: [PATCH 044/135] Fix clipping error in docview --- data/core/docview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 9a2972dc..7dbfbccf 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -439,7 +439,7 @@ 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) + 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 From 8b634daa669ac2cf814cc7ec018ef9ddd8c97ec9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 10 Oct 2021 21:48:16 +0200 Subject: [PATCH 045/135] Use rounded value for node's size when splitting Rouding node's size to an integer value ensure drawing are pixel perfect in sizing. --- data/core/rootview.lua | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index fa200aec..3c4feda0 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -411,15 +411,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 = math.floor(x1 and x1 + ds or (x2 and self.size[x] - x2 or 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 From 0d2166c9cee35b8381a3467be2fb52a1e0d918fb Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 11 Oct 2021 09:25:38 +0200 Subject: [PATCH 046/135] Correct Node's clipping rectangle Fixing the Node's clipping rectangle make the clipping in DocView:draw() partially redundant. This latter is now no longer needed to clip on the right when drawing the document's lines but it still serves to the purpose of clipping on the left, before the gutter region. --- data/core/docview.lua | 2 ++ data/core/rootview.lua | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 7dbfbccf..a73bfc9c 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -439,6 +439,8 @@ function DocView:draw() local pos = self.position x, y = self:get_line_screen_position(minline) + -- 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) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 3c4feda0..f5b1e534 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -595,7 +595,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, pos.x + size.x, pos.y + size.y) + core.push_clip_rect(pos.x, pos.y, size.x, size.y) self.active_view:draw() core.pop_clip_rect() else From 3a71528087206cdd084e62424ad6539490fbf12d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 11 Oct 2021 22:18:02 +0200 Subject: [PATCH 047/135] Allow specifying offset for `common.is_utf8_cont` --- data/core/common.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 1a1b22cd..0d360640 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 From 038e335c8c1813b257e132a30f00a4d60ee0b153 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 11 Oct 2021 22:20:44 +0200 Subject: [PATCH 048/135] Show error message when `pcre2_match` fails --- src/api/regex.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/regex.c b/src/api/regex.c index 1043b1c5..9f6bd3ee 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -68,8 +68,11 @@ static int f_pcre_match(lua_State *L) { int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL); if (rc < 0) { pcre2_match_data_free(md); - if (rc != PCRE2_ERROR_NOMATCH) - luaL_error(L, "regex matching error %d", rc); + if (rc != PCRE2_ERROR_NOMATCH) { + PCRE2_UCHAR buffer[120]; + pcre2_get_error_message(rc, buffer, sizeof(buffer)); + luaL_error(L, "regex matching error %d: %s", rc, buffer); + } return 0; } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); From 1872e8214137b02e2d7c905d267b3d76297d6087 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 11 Oct 2021 22:32:50 +0200 Subject: [PATCH 049/135] Make `regex.match` return the appropriate `end` index This makes its behavior similar to `string.find`. --- data/core/regex.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 8a516d35ce4aeecc9f7b2879028a89f9d8816d11 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 11 Oct 2021 22:37:31 +0200 Subject: [PATCH 050/135] Correctly identify the start of the next character in `tokenizer` When moving to the next character, we have to consider that the current one might be multi-byte. --- data/core/tokenizer.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index a20dba5e..08a5ea31 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 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 From ef60b24f632cad403f9c7de382dade8dd8f1a358 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 16 Oct 2021 02:56:01 +0200 Subject: [PATCH 051/135] Check both values returned by `Node:get_locked_size` --- data/core/commands/root.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index e41c723d..49bf774a 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -112,7 +112,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 +121,6 @@ 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) From 780c8c6d0d7ac42e5e502fb7f6d5b40ef5a30837 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 16 Oct 2021 03:02:42 +0200 Subject: [PATCH 052/135] Improve check for `find-replace` commands using `has_unique_selection` --- data/core/commands/findreplace.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 67aa90d5..5d27aa69 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -15,7 +15,7 @@ local found_expression local function 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.doc + return is_DocView and core.active_view.doc or (last_view and last_view.doc) end local function get_find_tooltip() @@ -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 From 461533eabf0195394208b3ecc0562eb471c05358 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 20 Oct 2021 18:43:22 -0400 Subject: [PATCH 053/135] Handles occasions where our color bytes aren't in the order we expected. --- src/renderer.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/renderer.c b/src/renderer.c index 49261e19..aec945e0 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -246,6 +246,7 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor const char* end = text + strlen(text); unsigned char* destination_pixels = surface->pixels; int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height; + while (text < end) { unsigned int codepoint, r, g, b; text = utf8_to_codepoint(text, &codepoint); @@ -275,12 +276,12 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->subpixel ? 3 : 1)]; for (int x = glyph_start; x < glyph_end; ++x) { unsigned int destination_color = *destination_pixel; - SDL_Color dst = { (destination_color >> 16) & 0xFF, (destination_color >> 8) & 0xFF, (destination_color >> 0) & 0xFF, (destination_color >> 24) & 0xFF }; + SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift }; SDL_Color src = { *(font->subpixel ? source_pixel++ : source_pixel), *(font->subpixel ? source_pixel++ : source_pixel), *source_pixel++ }; r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025; g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; - *destination_pixel++ = dst.a << 24 | r << 16 | g << 8 | b; + *destination_pixel++ = dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift; } } } @@ -323,14 +324,15 @@ void ren_draw_rect(RenRect rect, RenColor color) { RenColor *d = (RenColor*) surface->pixels; d += x1 + y1 * surface->w; int dr = surface->w - (x2 - x1); - + unsigned int translated = SDL_MapRGB(surface->format, color.r, color.g, color.b); if (color.a == 0xff) { SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 }; - SDL_FillRect(surface, &rect, SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a)); + SDL_FillRect(surface, &rect, translated); } else { + RenColor translated_color = (RenColor){ translated & 0xFF, (translated >> 8) & 0xFF, (translated >> 16) & 0xFF, color.a }; for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++, d++) - *d = blend_pixel(*d, color); + *d = blend_pixel(*d, translated_color); d += dr; } } From f472c24c73289b59b2284c4b4b24b1fc7319ae49 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 12 Oct 2021 14:28:28 +0200 Subject: [PATCH 054/135] First attempt to treat correctly network volumes On windows paths belonging to network volumes will be gives like: \\address\share-name\path Now the code recognize these paths and treat them correctly. --- data/core/common.lua | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 1a1b22cd..ebd3df40 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -282,22 +282,41 @@ 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, filepath = "/", 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 From 9c52c420c595f8e71053f6b715c888dd8d08734c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 12 Oct 2021 16:08:06 +0200 Subject: [PATCH 055/135] Do not use normalize_path when not needed --- data/core/common.lua | 18 ++++++++++++++++++ data/core/init.lua | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index ebd3df40..3b1e7add 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -280,6 +280,24 @@ 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 diff --git a/data/core/init.lua b/data/core/init.lua index 9de283ad..eb3a49b9 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 @@ -56,7 +56,7 @@ 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) return true @@ -198,7 +198,7 @@ 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) + path = common.normalize_volume(path) local dir = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, @@ -572,9 +572,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 From 43374b036f7be3c44399fabbd593b8a99304328f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 12 Oct 2021 18:06:00 +0200 Subject: [PATCH 056/135] Fix problem with treeview x resizing The x size of the treeview plugin cannot really change expect if explicitly resized. The call to move_towards for x seems to raise a state where core.redraw is always set to true and this prevent the application to go idle. It is seen after the introduction of the dmon directory monitoring but it is not clear why it wasn't seen before. --- data/plugins/treeview.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index d08db03e..5202cc78 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -238,8 +238,6 @@ function TreeView:update() if self.init_size then self.size.x = dest self.init_size = false - else - self:move_towards(self.size, "x", dest) end local duration = system.get_time() - self.tooltip.begin From f18ac849fb908add8b1558f555ad378a247e85d7 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 12 Oct 2021 22:19:24 +0200 Subject: [PATCH 057/135] Fix error introduced with 43fc35d7 --- data/core/common.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/common.lua b/data/core/common.lua index 3b1e7add..99e6ee5a 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -315,7 +315,7 @@ function common.normalize_path(filename) else local relpath = filename:match('^/(.+)') if relpath then - volume, filepath = "/", relpath + volume, filename = "/", relpath end end local parts = split_on_slash(filename, PATHSEP) From 167e41de65f40b99a4a79194c0a92b587a76b18a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 14 Oct 2021 09:13:06 +0200 Subject: [PATCH 058/135] Fix problem with treeview keeping the editor busy Fix a problem introduced when fixing the dirty pixel problem, commit cb08c5c. The node, when determining the layout was rounding the size of the fixed-size view. In turns this latter was calling move_towards to the default_size it wanted. If default_size was non-integer the value vas never archieved because it was rounded during layout and move_towars was keeping the editor busy by setting the core.need_redraw flag. --- data/core/rootview.lua | 6 +++++- data/plugins/treeview.lua | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index f5b1e534..04a091c4 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -412,7 +412,7 @@ end -- 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 ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size - local n = math.floor(x1 and x1 + ds or (x2 and self.size[x] - x2 or self.size[x] * self.divider)) + 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 @@ -675,6 +675,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 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 5202cc78..d08db03e 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -238,6 +238,8 @@ function TreeView:update() if self.init_size then self.size.x = dest self.init_size = false + else + self:move_towards(self.size, "x", dest) end local duration = system.get_time() - self.tooltip.begin From e9c16c43674ecf912512905bc94802564419bee0 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 21 Oct 2021 11:07:37 +0200 Subject: [PATCH 059/135] Add a limit for very slow filesystems When adding a directory in a project we check if the filesystem is too slow. If it is too slow we act as if the projects was files-limited by the number of files but we show a specific warning. This solution is not perfect but for very low filesystem it can limit the problem. Otherwise the application would be totally irresponsive. --- data/core/init.lua | 47 ++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index eb3a49b9..3b8ee16d 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -106,6 +106,15 @@ local function get_project_file_info(root, file) 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. @@ -114,12 +123,13 @@ 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(dir, root, path, t, begin_hook, max_files) +local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook) if begin_hook then begin_hook() end + 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 for _, file in ipairs(all) do local info = get_project_file_info(root, path .. PATHSEP .. file) if info then @@ -128,13 +138,16 @@ local function get_directory_files(dir, root, path, t, begin_hook, max_files) end end + local recurse_complete = true table.sort(dirs, compare_file) for _, f in ipairs(dirs) do table.insert(t, f) - if (not max_files or entries_count <= max_files) and core.project_subdir_is_shown(dir, f.filename) then - local sub_limit = max_files and max_files - entries_count - local _, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, begin_hook, sub_limit) - entries_count = entries_count + n + 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 @@ -143,7 +156,7 @@ local function get_directory_files(dir, root, path, t, begin_hook, max_files) table.insert(t, f) end - return t, entries_count + return t, recurse_complete, entries_count end @@ -165,26 +178,28 @@ function core.project_subdir_is_shown(dir, filename) end -local function show_max_files_warning() - core.status_view:show_message("!", style.accent, +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 -- Populate a project folder top directory by scanning the filesystem. local function scan_project_folder(index) local dir = core.project_directories[index] - local t, entries_count = get_directory_files(dir, dir.name, "", {}, nil, config.max_project_files) - if entries_count > config.max_project_files then + 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 -- 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") if core.status_view then -- May be not yet initialized. - show_max_files_warning() + show_max_files_warning(dir) end else dir.watch_id = system.watch_dir(dir.name, true) @@ -298,7 +313,7 @@ local function project_subdir_bounds(dir, filename) end local function rescan_project_subdir(dir, filename_rooted) - local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, coroutine.yield) + 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) @@ -316,7 +331,7 @@ 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, {}) or {} + 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 @@ -673,7 +688,7 @@ function core.init() -- 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() + show_max_files_warning(core.project_directories[1]) end for _, filename in ipairs(files) do From 7bdfcd529e9950633f3117b690cd21431b1b6f9c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 12 Oct 2021 22:08:17 +0200 Subject: [PATCH 060/135] Add a function to detect filesystem type on linux --- src/api/system.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/api/system.c b/src/api/system.c index c1a4abd3..c84f5fee 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -12,6 +12,8 @@ #include #include #include +#elseif __linux__ + #include #endif extern SDL_Window *window; @@ -545,6 +547,45 @@ static int f_get_file_info(lua_State *L) { return 1; } +#if __linux__ +// https://man7.org/linux/man-pages/man2/statfs.2.html + +struct f_type_names { + uint32_t magic; + const char *name; +}; + +static struct f_type_names fs_names[] = { + { 0xef53, "ext2/ext3" }, + { 0x6969, "nfs" }, + { 0x65735546, "fuse" }, + { 0x517b, "smb" }, + { 0xfe534d42, "smb2" }, + { 0x52654973, "reiserfs" }, + { 0x01021994, "tmpfs" }, + { 0x858458f6, "ramfs" }, + { 0x5346544e, "ntfs" }, + { 0x0, NULL }, +}; + +static int f_get_fs_type(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + struct statfs buf; + int status = statfs(path, &buf); + if (status != 0) { + return luaL_error(L, "error calling statfs on %s", path); + } + for (int i = 0; fs_names[i].magic; i++) { + if (fs_names[i].magic == buf.f_type) { + lua_pushstring(L, fs_names[i].name); + return 1; + } + } + lua_pushstring(L, "unknown"); + return 1; +} +#endif + static int f_mkdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); @@ -789,6 +830,7 @@ static const luaL_Reg lib[] = { #if __linux__ { "watch_dir_add", f_watch_dir_add }, { "watch_dir_rm", f_watch_dir_rm }, + { "get_fs_type", f_get_fs_type }, #endif { NULL, NULL } }; From ddb6196e9e76920a07c3b5d9e8da30c370b05c6c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 21 Oct 2021 23:57:17 +0200 Subject: [PATCH 061/135] Force project rescan on network filesystems --- data/core/init.lua | 109 ++++++++++++++++++++++++++++----------------- src/api/system.c | 2 +- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 3b8ee16d..4a16e6b7 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -187,48 +187,6 @@ local function show_max_files_warning(dir) core.status_view:show_message("!", style.accent, message) end --- Populate a project folder top directory by scanning the filesystem. -local function scan_project_folder(index) - local dir = core.project_directories[index] - 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 - -- 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") - if core.status_view then -- May be not yet initialized. - show_max_files_warning(dir) - end - else - dir.watch_id = system.watch_dir(dir.name, true) - end - dir.files = t - core.dir_rescan_add_job(dir, ".") -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 - local function file_search(files, info) local filename, type = info.filename, info.type @@ -328,6 +286,73 @@ local function rescan_project_subdir(dir, filename_rooted) 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 + +-- 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 diff --git a/src/api/system.c b/src/api/system.c index c84f5fee..0ebf78f1 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -12,7 +12,7 @@ #include #include #include -#elseif __linux__ +#elif __linux__ #include #endif From 331b8e90ec95a8342832d6f9e889928d510d5aa9 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 23 Oct 2021 03:34:24 +0200 Subject: [PATCH 062/135] Select a new primary node when closing the current one The new primary node can be any non-locked leaf node that isn't already primary. --- data/core/rootview.lua | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 04a091c4..51eedc10 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 @@ -823,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 From ffb66cefd77a4ad2c90e698df440d3f09d08feef Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 23 Oct 2021 15:01:16 +0200 Subject: [PATCH 063/135] Fix python docstring highlighting From PR: https://github.com/lite-xl/lite-xl/pull/624 contributed by @dflock. --- data/plugins/language_python.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index e19caa63..60aa41a6 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -6,16 +6,16 @@ syntax.add { 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", From 5cdd8009109409cc45daff7b5364e5d43da95ca0 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 23 Oct 2021 15:03:09 +0200 Subject: [PATCH 064/135] Fix problem checking utf-8 cont at end of string --- data/core/tokenizer.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 08a5ea31..d95baeb1 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -145,7 +145,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) repeat local next = res[2] + 1 -- go to the start of the next utf-8 character - while common.is_utf8_cont(text, next) do + 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) } From d41aed61c92ca24941d1f6820ec55cb52c988bab Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 23 Oct 2021 15:16:51 +0200 Subject: [PATCH 065/135] Update changelog --- changelog.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/changelog.md b/changelog.md index 57ab9646..c81c7dbe 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,31 @@ This files document the changes done in Lite XL for each release. +### 2.0.3 + +Replace periodic rescan of project folder with a notification based system using the +[dmon library](https://github.com/septag/dmon). Improves performance especially for +large project folders since the application no longer needs to rescan. +The application also reports immediatly any change in the project directory even +when the application is unfocused. + +Improved find-replace reverse and forward search. + +Fixed a bug in incremental syntax highlighting affecting documents with multiple-lines +comments or strings. + +The application now always shows the tabs in the documents' view even when a single +document is opened. Can be changed with the option `config.always_show_tabs`. + +Fix problem with numeric keypad function keys not properly working. + +Fix problem with pixel not correctly drawn at the window's right edge. + +Treat correctly and open network paths on Windows. + +Add some improvements for very slow network filesystems. + +Fix problem with python syntax highliting, contributed by @dflock. + ### 2.0.2 Fix problem project directory when starting the application from Launcher on macOS. From 80d837b05b0713f9ce268f8c64b048537741220d Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 23 Oct 2021 15:46:30 +0200 Subject: [PATCH 066/135] Fix assert with dmon on directory deleting --- lib/dmon/dmon.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index ed10d11f..2bc9e0c3 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -927,8 +927,11 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) dmon__strcat(watchdir, sizeof(watchdir), "/"); uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; int wd = inotify_add_watch(watch->fd, watchdir, mask); - _DMON_UNUSED(wd); - DMON_ASSERT(wd != -1); + // Removing the assertion below because it was giving errors for some reason + // when building a new package. + // _DMON_UNUSED(wd); + // DMON_ASSERT(wd != -1); + if (wd == -1) continue; dmon__watch_subdir subdir; dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); From e68d6016f89a2e65846c2a1ac60179f849cdb5c3 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 23 Oct 2021 08:07:14 -0700 Subject: [PATCH 067/135] Add skip-subproject option in package script --- scripts/package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package.sh b/scripts/package.sh index 1370aee8..21a8cc91 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -186,7 +186,7 @@ main() { rm -rf "${dest_dir}" - DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}" + DESTDIR="$(pwd)/${dest_dir}" meson install --skip-subprojects -C "${build_dir}" local data_dir="$(pwd)/${dest_dir}/data" local exe_file="$(pwd)/${dest_dir}/lite-xl" From 92db048e7c4bb02967156bc9fcd26744e746b097 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 25 Oct 2021 00:18:20 +0200 Subject: [PATCH 068/135] Use plain search by default in `search.find` --- data/core/doc/search.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index b4c553c9..8395769a 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -43,6 +43,7 @@ end function search.find(doc, line, col, text, opt) doc, line, col, text, opt = init_args(doc, line, col, text, opt) + local plain = not opt.pattern local pattern = text local search_func = string.find if opt.regex then @@ -60,9 +61,9 @@ function search.find(doc, line, col, text, opt) end local s, e if opt.reverse then - s, e = rfind(search_func, line_text, pattern, col - 1) + s, e = rfind(search_func, line_text, pattern, col - 1, plain) else - s, e = search_func(line_text, pattern, col) + s, e = search_func(line_text, pattern, col, plain) end if s then return line, s, line, e + 1 From df665ddc3842dbe4677c1cbbafe55b80670f3c1c Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 25 Oct 2021 14:06:07 +0200 Subject: [PATCH 069/135] Use `header` to get syntax only when provided --- data/core/syntax.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/syntax.lua b/data/core/syntax.lua index a763ac78..de8ec9d0 100644 --- a/data/core/syntax.lua +++ b/data/core/syntax.lua @@ -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 From 065fe0769682e16e146849b0d98de4b31376af9e Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Mon, 25 Oct 2021 19:29:31 +0200 Subject: [PATCH 070/135] Add auto labeler workflow --- .github/labeler.yml | 35 +++++++++++++++++++++++++++ .github/workflows/auto_labeler_pr.yml | 16 ++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/auto_labeler_pr.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..a59bd3ec --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,35 @@ +"Category: CI": + - .github/workflows/* + +"Category: Meta": + - ./* + - .github/* + - .github/ISSUE_TEMPLATE/* + - .github/PULL_REQUEST_TEMPLATE/* + - .gitignore + +"Category: Build System": + - meson.build + - meson_options.txt + - subprojects/* + +"Category: Documentation": + - docs/* + +"Category: Resources": + - resources/* + +"Category: Themes": + - data/colors/* + +"Category: Lua Core": + - data/core/* + +"Category: Fonts": + - data/fonts/* + +"Category: Plugins": + - data/plugins/* + +"Category: C Core": + - src/* diff --git a/.github/workflows/auto_labeler_pr.yml b/.github/workflows/auto_labeler_pr.yml new file mode 100644 index 00000000..48ea1eeb --- /dev/null +++ b/.github/workflows/auto_labeler_pr.yml @@ -0,0 +1,16 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +permissions: + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Apply Type Label + uses: actions/labeler@v3 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: "" # works around actions/labeler#104 From 9e721937af962098386cff37803c7aeef4d99367 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Tue, 26 Oct 2021 00:12:16 +0200 Subject: [PATCH 071/135] Don't insert `nil` in highlighter lines table When `highlighter:insert_notify` was called, a hole in the array was created. If another call to `highlighter:insert_notify` happened before the hole was filled, a `Position out of bounds` error could have been raised. --- data/core/doc/highlighter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index 4cb703da..c77e1138 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -52,7 +52,7 @@ end function Highlighter:insert_notify(line, n) self:invalidate(line) for i = 1, n do - table.insert(self.lines, line, nil) + table.insert(self.lines, line, false) end end From d0ece3570580195dd3c5128d129056901537b54e Mon Sep 17 00:00:00 2001 From: obtusedev <66740598+obtusedev@users.noreply.github.com> Date: Tue, 2 Nov 2021 15:14:13 -0400 Subject: [PATCH 072/135] Add install instructions for prebuilt binaries --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 71f856a1..6ec3a73b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,53 @@ DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build Please note that the package is relocatable to any prefix and the option prefix affects only the place where the application is actually installed. +## Installing Prebuilt + +Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system. + +### Ubuntu + +Unzip the file and `cd` into the `lite-xl` directory: + +```sh +tar -xzf +cd lite-xl +``` + +Copy files over into appropriate directories: + +```sh +mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin +cp -r share $HOME/.local +``` + +If `$HOME/.local/bin` is not in PATH: + +```sh +echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc +``` + +To get the icon to show up in app launcher: + +```sh +xdg-desktop-menu forceupdate +``` + +You may need to logout and login again to see icon in app launcher. + +To uninstall just run: + +```sh +rm -f $HOME/.local/bin/lite-xl +rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \ + $HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \ + $HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \ + $HOME/.local/share/lite-xl \ + $HOME/.local/share/doc/lite-xl +``` + +You may need to `Alt + F2` and enter 'r' to see changes. + ## Contributing Any additional functionality that can be added through a plugin should be done From 3867ff2be7302c3a584bea2c0f4a2f8537ac0a4b Mon Sep 17 00:00:00 2001 From: Cukmekerb Date: Fri, 5 Nov 2021 14:35:58 -0700 Subject: [PATCH 073/135] Document bold, italic, and underlined font rendering --- docs/api/renderer.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index bb622131..6820a14d 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -19,6 +19,9 @@ renderer.color = {} ---@class renderer.fontoptions ---@field public antialiasing "'grayscale'" | "'subpixel'" ---@field public hinting "'slight'" | "'none'" | '"full"' +-- @field public bold boolean +-- @field public italic boolean +-- @field public underline boolean renderer.fontoptions = {} --- From 05dcddaeece0ca39cad2b5e093de4a9317b77923 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 13:14:48 -0500 Subject: [PATCH 074/135] Made plugin load order deterministic. --- data/core/init.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 6ef0ab7e..6dd54cd4 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -675,16 +675,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 i, 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 From a184ec9cc326d3ced15b080d2d38ff8d2aed2a7b Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 14:04:42 -0500 Subject: [PATCH 075/135] Improved heuristic to pay more attention to string length. --- src/api/system.c | 60 +++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/src/api/system.c b/src/api/system.c index bcd1b997..85d0db2a 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -591,56 +591,32 @@ static int f_exec(lua_State *L) { return 0; } - static int f_fuzzy_match(lua_State *L) { size_t strLen, ptnLen; const char *str = luaL_checklstring(L, 1, &strLen); const char *ptn = luaL_checklstring(L, 2, &ptnLen); - bool files = false; - if (lua_gettop(L) > 2 && lua_isboolean(L,3)) - files = lua_toboolean(L, 3); - - int score = 0; - int run = 0; - - // Match things *backwards*. This allows for better matching on filenames than the above + // If true match things *backwards*. This allows for better matching on filenames than the above // function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh // as the first result, rather than src/renderer.c. Clearly that's wrong. - if (files) { - const char* strEnd = str + strLen - 1; - const char* ptnEnd = ptn + ptnLen - 1; - while (strEnd >= str && ptnEnd >= ptn) { - while (*strEnd == ' ') { strEnd--; } - while (*ptnEnd == ' ') { ptnEnd--; } - if (tolower(*strEnd) == tolower(*ptnEnd)) { - score += run * 10 - (*strEnd != *ptnEnd); - run++; - ptnEnd--; - } else { - score -= 10; - run = 0; - } - strEnd--; + bool files = lua_gettop(L) > 2 && lua_isboolean(L,3) && lua_toboolean(L, 3); + int score = 0, run = 0, increment = files ? -1 : 1; + const char* strTarget = files ? str + strLen - 1 : str; + const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn; + while (*strTarget && *ptnTarget) { + while (*strTarget == ' ') { strTarget += increment; } + while (*ptnTarget == ' ') { ptnTarget += increment; } + if (tolower(*strTarget) == tolower(*ptnTarget)) { + score += run * 10 - (*strTarget != *ptnTarget); + run++; + ptnTarget += increment; + } else { + score -= 10; + run = 0; } - if (ptnEnd >= ptn) { return 0; } - } else { - while (*str && *ptn) { - while (*str == ' ') { str++; } - while (*ptn == ' ') { ptn++; } - if (tolower(*str) == tolower(*ptn)) { - score += run * 10 - (*str != *ptn); - run++; - ptn++; - } else { - score -= 10; - run = 0; - } - str++; - } - if (*ptn) { return 0; } + strTarget += increment; } - - lua_pushnumber(L, score - (int)strLen); + if (*ptnTarget) { return 0; } + lua_pushnumber(L, score - (int)strLen * 10); return 1; } From 934f9c05d42255f604af9e969001056c1a9e8a86 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 15:01:03 -0500 Subject: [PATCH 076/135] Screwed up checks. --- src/api/system.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/system.c b/src/api/system.c index 85d0db2a..c998fa05 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -602,9 +602,9 @@ static int f_fuzzy_match(lua_State *L) { int score = 0, run = 0, increment = files ? -1 : 1; const char* strTarget = files ? str + strLen - 1 : str; const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn; - while (*strTarget && *ptnTarget) { - while (*strTarget == ' ') { strTarget += increment; } - while (*ptnTarget == ' ') { ptnTarget += increment; } + while (strTarget >= str && ptnTarget >= ptn && *strTarget && *ptnTarget) { + while (strTarget >= str && *strTarget == ' ') { strTarget += increment; } + while (ptnTarget >= ptn && *ptnTarget == ' ') { ptnTarget += increment; } if (tolower(*strTarget) == tolower(*ptnTarget)) { score += run * 10 - (*strTarget != *ptnTarget); run++; @@ -615,7 +615,7 @@ static int f_fuzzy_match(lua_State *L) { } strTarget += increment; } - if (*ptnTarget) { return 0; } + if (ptnTarget >= ptn && *ptnTarget) { return 0; } lua_pushnumber(L, score - (int)strLen * 10); return 1; } From f99afcd29c777544f1f3bc4f9d8b2802a5b15c5d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 7 Nov 2021 23:12:03 +0100 Subject: [PATCH 077/135] In `TreeView` set correct active `View` before opening new `DocView` Before this, the new `DocView` was always added to the primary `Node`. With this, it gets added to the last focused `Node`. --- data/plugins/treeview.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index d08db03e..77b6732f 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -225,6 +225,9 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) 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) From 24669293c71a2a34ad9a4ba528ddcb45323350d3 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 17:54:42 -0500 Subject: [PATCH 078/135] Made it so that we originally start on the parent directory of the current project, but provide a list of recently used projects if on that directory. If a directory separator is added, then everything is as normal. --- data/core/commands/core.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index e836ea2f..2280f620 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -9,7 +9,8 @@ local fullscreen = 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, { @@ -149,7 +150,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 +167,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) From 5b8c08e93ae3cb79b4e0602e0cc74e90e6d695e5 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 17:57:15 -0500 Subject: [PATCH 079/135] Missing parentheses. --- data/core/commands/core.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 2280f620..62be4bc6 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -9,7 +9,7 @@ local fullscreen = false local function suggest_directory(text) text = common.home_expand(text) - return common.home_encode_list(text == "" or text == common.home_expand(common.dirname(core.project_dir)) + 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 From d22bf520edb9738cab266b9f8e438e396b69eb35 Mon Sep 17 00:00:00 2001 From: Takase <20792268+takase1121@users.noreply.github.com> Date: Mon, 8 Nov 2021 12:44:09 +0800 Subject: [PATCH 080/135] Keymap generator (#503) add keymap generator --- scripts/README.md | 1 + scripts/keymap-generator/dkjson.lua | 714 ++++++++++++++++++ scripts/keymap-generator/keymap-generator.lua | 75 ++ 3 files changed, 790 insertions(+) create mode 100644 scripts/keymap-generator/dkjson.lua create mode 100644 scripts/keymap-generator/keymap-generator.lua diff --git a/scripts/README.md b/scripts/README.md index a236599c..7d66fbca 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -22,6 +22,7 @@ Various scripts and configurations used to configure, build, and package Lite XL and run Lite XL, mainly useful for CI and documentation purpose. Preferably not to be used in user systems. - **fontello-config.json**: Used by the icons generator. +- **keymap-generator**: Generates a JSON file containing the keymap [1]: https://github.com/LinusU/node-appdmg [2]: https://docs.appimage.org/ diff --git a/scripts/keymap-generator/dkjson.lua b/scripts/keymap-generator/dkjson.lua new file mode 100644 index 00000000..fa50b9fa --- /dev/null +++ b/scripts/keymap-generator/dkjson.lua @@ -0,0 +1,714 @@ +-- Module options: +local always_try_using_lpeg = true +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1/5.2 + +Version 2.5 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2013 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = + pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.5" } + +if register_global_module_table then + _G[global_module_name] = json +end + +local _ENV = nil -- blocking globals in Lua 5.2 + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return "line " .. line .. ", column " .. (where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local len = strlen (str) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if not npos then break end + pos = npos + nt = nt + 1 + t[nt] = obj + until cont == 'last' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if not npos then break end + pos = npos + t[key] = obj + until cont == 'last' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected") + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected") + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + function json.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- use this function only once: + json.use_lpeg = function () return json end + + json.using_lpeg = true + + return json -- so you can get the module using json = require "dkjson".use_lpeg() +end + +if always_try_using_lpeg then + pcall (json.use_lpeg) +end + +return json + diff --git a/scripts/keymap-generator/keymap-generator.lua b/scripts/keymap-generator/keymap-generator.lua new file mode 100644 index 00000000..235d48c2 --- /dev/null +++ b/scripts/keymap-generator/keymap-generator.lua @@ -0,0 +1,75 @@ +#!/usr/bin/env lua +local dkjson = require "dkjson" + + +local function load_keymap(target, target_map, macos) + _G.MACOS = macos + package.loaded["core.keymap"] = nil + local keymap = require "core.keymap" + + if target then + keymap.map = {} + require(target) + end + + target_map = target_map or {} + -- keymap.reverse_map does not do this? + for key, actions in pairs(keymap.map) do + for _, action in ipairs(actions) do + target_map[action] = target_map[action] or {} + table.insert(target_map[action], key) + end + end + + return target_map +end + + +local function normalize(map) + local result = {} + for action, keys in pairs(map) do + local uniq = {} + local r = { combination = {}, action = action } + for _, v in ipairs(keys) do + if not uniq[v] then + uniq[v] = true + r.combination[#r.combination+1] = v + end + end + result[#result+1] = r + end + table.sort(result, function(a, b) return a.action < b.action end) + return result +end + + +local function process_module(mod, filename) + local map = {} + load_keymap(mod, map) + load_keymap(mod, map, true) + map = normalize(map) + local f = assert(io.open(filename, "wb")) + f:write(dkjson.encode(map, { indent = true })) + f:close() +end + + +print("Warning: this is not guaranteed to work outside lite-xl's own keymap. Proceed with caution") +local LITE_ROOT = arg[1] +if not LITE_ROOT then + error("LITE_ROOT is not given") +end +package.path = package.path .. ";" .. LITE_ROOT .. "/?.lua;" .. LITE_ROOT .. "/?/init.lua" + +-- fix core.command (because we don't want load the entire thing) +package.loaded["core.command"] = {} + +if #arg > 1 then + for i = 2, #arg do + process_module(arg[i], arg[i] .. ".json") + print(string.format("Exported keymap in %q.", arg[i])) + end +else + process_module(nil, "core.keymap.json") + print("Exported the default keymap.") +end From b3eef15e7ae5eab34e0e2fc731ddd17f9214149b Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 8 Nov 2021 16:00:24 +0100 Subject: [PATCH 081/135] Highlight any line that contains a caret Now lines with selections can be highlighted if they contain a caret. --- data/core/docview.lua | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 4e95c359..6621ef72 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -354,6 +354,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 +380,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) From 6bc4fbb238f75546cf217e7aacedf2afa341ce47 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Tue, 9 Nov 2021 22:21:45 +0100 Subject: [PATCH 082/135] Restore `TitleView` only when needed Before, every time the user came back from fullscreen, the `TitleView` was shown regardless of its previous status. --- data/core/commands/core.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 62be4bc6..ad0d4b10 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -6,6 +6,7 @@ local LogView = require "core.logview" local fullscreen = false +local restore_title_view = false local function suggest_directory(text) text = common.home_expand(text) @@ -28,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() From 2033b67daee1452005d384ad235c618b2567c7b3 Mon Sep 17 00:00:00 2001 From: Takase <20792268+takase1121@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:12:27 +0800 Subject: [PATCH 083/135] make labeler recursive this is mentioned by Jan in Discord. --- .github/labeler.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index a59bd3ec..41f66c7c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -14,16 +14,16 @@ - subprojects/* "Category: Documentation": - - docs/* + - docs/**/* "Category: Resources": - - resources/* + - resources/**/* "Category: Themes": - data/colors/* "Category: Lua Core": - - data/core/* + - data/core/**/* "Category: Fonts": - data/fonts/* @@ -32,4 +32,4 @@ - data/plugins/* "Category: C Core": - - src/* + - src/**/* From dfa48ddb514e14c154202615ec2bf406b7178a94 Mon Sep 17 00:00:00 2001 From: obtusedev <66740598+obtusedev@users.noreply.github.com> Date: Wed, 10 Nov 2021 18:56:32 -0500 Subject: [PATCH 084/135] Add install instructions for prebuilt binaries --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6ec3a73b..e6cd18dc 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ affects only the place where the application is actually installed. Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system. -### Ubuntu +### Linux Unzip the file and `cd` into the `lite-xl` directory: @@ -90,7 +90,13 @@ tar -xzf cd lite-xl ``` -Copy files over into appropriate directories: +To run lite-xl without installing: +```sh +cd bin +./lite-xl +``` + +To install lite-xl copy files over into appropriate directories: ```sh mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin @@ -118,11 +124,9 @@ rm -f $HOME/.local/bin/lite-xl rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \ $HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \ $HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \ - $HOME/.local/share/lite-xl \ - $HOME/.local/share/doc/lite-xl + $HOME/.local/share/lite-xl ``` -You may need to `Alt + F2` and enter 'r' to see changes. ## Contributing From 1376eaf54d3462a7dd144c1c8ca44ed48aaa07f9 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 14 Nov 2021 15:40:23 -0500 Subject: [PATCH 085/135] Made varaible anonymous. --- data/core/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 6dd54cd4..13d0907b 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -685,7 +685,7 @@ function core.load_plugins() end table.sort(ordered) - for i, filename in ipairs(ordered) do + 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 From 612818ca0564e38698b3df6779fc969b82aae0ed Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 18:22:43 -0400 Subject: [PATCH 086/135] Added in clicks to keymap. --- data/core/command.lua | 6 +++--- data/core/commands/doc.lua | 22 ++++++++++++++++++++++ data/core/docview.lua | 29 ++--------------------------- data/core/keymap.lua | 11 +++++++---- data/core/view.lua | 3 ++- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/data/core/command.lua b/data/core/command.lua index 7915e16d..2531fb96 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -42,10 +42,10 @@ function command.get_all_valid() 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/doc.lua b/data/core/commands/doc.lua index b8ce2cb5..db2f0428 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -388,6 +388,28 @@ local commands = { os.remove(filename) core.log("Removed \"%s\"", filename) end, + + ["doc:select-to-cursor"] = function(x, y, clicks) + if clicks % 2 == 1 then + local line1, col1 = select(3, doc():get_selection()) + local line2, col2 = dv():resolve_screen_position(x, y) + doc():set_selection(line2, col2, line1, col1) + end + end, + + ["doc:set-cursor"] = function(x, y, clicks) + local line, col = dv():resolve_screen_position(x, y) + doc():set_selection(dv():mouse_selection(doc(), clicks, line, col, line, col)) + dv().mouse_selecting = { line, col, clicks = clicks } + core.blink_reset() + end, + + ["doc:split-cursor"] = function(x, y, clicks) + local line, col = dv():resolve_screen_position(x, y) + doc():add_selection(dv():mouse_selection(doc(), clicks, line, col, line, col)) + dv().mouse_selecting = { line, col, clicks = clicks } + core.blink_reset() + end, ["doc:create-cursor-previous-line"] = function() split_cursor(-1) diff --git a/data/core/docview.lua b/data/core/docview.lua index 4e95c359..1c041a27 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -225,7 +225,7 @@ function DocView:scroll_to_make_visible(line, col) end -local function mouse_selection(doc, clicks, line1, col1, line2, col2) +function DocView: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 @@ -245,31 +245,6 @@ local function mouse_selection(doc, clicks, line1, col1, line2, col2) 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, ...) @@ -290,7 +265,7 @@ 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)) + self.doc:set_selection(self:mouse_selection(self.doc, clicks, l1, c1, l2, c2)) end end end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 7f7c0fe2..3b8a756e 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -63,7 +63,7 @@ function keymap.get_binding(cmd) 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,13 +73,13 @@ 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 @@ -193,6 +193,9 @@ keymap.add_direct { ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", + ["shift+lclick"] = "doc:select-to-cursor", + ["ctrl+lclick"] = "doc:split-cursor", + ["lclick"] = "doc:set-cursor", ["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/view.lua b/data/core/view.lua index d1374ee4..7b4f2e46 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -3,7 +3,7 @@ local config = require "core.config" local style = require "core.style" local common = require "core.common" local Object = require "core.object" - +local keymap = require "core.keymap" local View = Object:extend() @@ -81,6 +81,7 @@ function View:on_mouse_pressed(button, x, y, clicks) self.dragging_scrollbar = true return true end + return keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) end From 4a0d390a7cd06d4d4e23637e0b599f97db1bd793 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 18:29:00 -0400 Subject: [PATCH 087/135] Added in macos keys. --- data/core/keymap-macos.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 53a20468..bc69aaaa 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -93,6 +93,9 @@ local function keymap_macos(keymap) ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", + ["shift+lclick"] = "doc:select-to-cursor", + ["ctrl+lclick"] = "doc:split-cursor", + ["lclick"] = "doc:set-cursor", ["shift+left"] = "doc:select-to-previous-char", ["shift+right"] = "doc:select-to-next-char", ["shift+up"] = "doc:select-to-previous-line", From 6f53ee1b69c6f23aba2a425130975b9d707f8274 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 19:20:06 -0400 Subject: [PATCH 088/135] Added in double, and triple clicking. --- data/core/commands/doc.lua | 18 +++--------------- data/core/docview.lua | 26 +++++++------------------- data/core/keymap.lua | 2 ++ data/core/view.lua | 3 ++- 4 files changed, 14 insertions(+), 35 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index db2f0428..ee6f8680 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -388,27 +388,15 @@ local commands = { os.remove(filename) core.log("Removed \"%s\"", filename) end, - - ["doc:select-to-cursor"] = function(x, y, clicks) - if clicks % 2 == 1 then - local line1, col1 = select(3, doc():get_selection()) - local line2, col2 = dv():resolve_screen_position(x, y) - doc():set_selection(line2, col2, line1, col1) - end - end, - + ["doc:set-cursor"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) - doc():set_selection(dv():mouse_selection(doc(), clicks, line, col, line, col)) - dv().mouse_selecting = { line, col, clicks = clicks } - core.blink_reset() + doc():set_selection(line, col, line, col) end, ["doc:split-cursor"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) - doc():add_selection(dv():mouse_selection(doc(), clicks, line, col, line, col)) - dv().mouse_selecting = { line, col, clicks = clicks } - core.blink_reset() + doc():add_selection(line, col, line, col) end, ["doc:create-cursor-previous-line"] = function() diff --git a/data/core/docview.lua b/data/core/docview.lua index 1c041a27..fe440274 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -224,25 +224,13 @@ function DocView:scroll_to_make_visible(line, col) end end - -function DocView: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 +function DocView:on_mouse_pressed(button, x, y, clicks) + local line, col = self:resolve_screen_position(x, y) + self.mouse_selecting = { line, col, clicks = clicks } + if DocView.super.on_mouse_pressed(self, button, x, y, clicks) then + return 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 + core.blink_reset() end function DocView:on_mouse_moved(x, y, ...) @@ -265,7 +253,7 @@ 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(self:mouse_selection(self.doc, clicks, l1, c1, l2, c2)) + self.doc:set_selection(l1, c1, l2, c2) end end end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 3b8a756e..f7261ab6 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -196,6 +196,8 @@ keymap.add_direct { ["shift+lclick"] = "doc:select-to-cursor", ["ctrl+lclick"] = "doc:split-cursor", ["lclick"] = "doc:set-cursor", + ["dlclick"] = "doc:select-word", + ["tlclick"] = "doc:select-lines", ["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/view.lua b/data/core/view.lua index 7b4f2e46..d418d5f9 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -76,12 +76,13 @@ function View:scrollbar_overlaps_point(x, y) end +local click_prefixes = { "", "d", "t" } function View:on_mouse_pressed(button, x, y, clicks) if self:scrollbar_overlaps_point(x, y) then self.dragging_scrollbar = true return true end - return keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) + return keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) end From ce2ec9f4424c2d479064d706ee1e2b741ef53fce Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 19:42:04 -0400 Subject: [PATCH 089/135] Moved commands out to the outer event loop. --- data/core/commands/doc.lua | 6 ++++++ data/core/init.lua | 4 +++- data/core/keymap-macos.lua | 2 ++ data/core/keymap.lua | 4 ++++ data/core/view.lua | 4 +--- src/api/system.c | 8 +++++--- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index ee6f8680..b7677d99 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -389,6 +389,12 @@ local commands = { 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) + doc():set_selection(line2, col2, line1, col1) + end, + ["doc:set-cursor"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) doc():set_selection(line, col, line, col) diff --git a/data/core/init.lua b/data/core/init.lua index 13d0907b..a8aa0645 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -922,7 +922,9 @@ 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 diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index bc69aaaa..aa4cccec 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -96,6 +96,8 @@ local function keymap_macos(keymap) ["shift+lclick"] = "doc:select-to-cursor", ["ctrl+lclick"] = "doc:split-cursor", ["lclick"] = "doc:set-cursor", + ["dlclick"] = "doc:select-word", + ["tlclick"] = "doc:select-lines", ["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 f7261ab6..517a83ef 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -85,6 +85,10 @@ function keymap.on_key_pressed(k, ...) return false end +local click_prefixes = { "", "d", "t" } +function keymap.on_mouse_pressed(button, x, y, clicks) + return keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) +end function keymap.on_key_released(k) local mk = modkey_map[k] diff --git a/data/core/view.lua b/data/core/view.lua index d418d5f9..d1374ee4 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -3,7 +3,7 @@ local config = require "core.config" local style = require "core.style" local common = require "core.common" local Object = require "core.object" -local keymap = require "core.keymap" + local View = Object:extend() @@ -76,13 +76,11 @@ function View:scrollbar_overlaps_point(x, y) end -local click_prefixes = { "", "d", "t" } function View:on_mouse_pressed(button, x, y, clicks) if self:scrollbar_overlaps_point(x, y) then self.dragging_scrollbar = true return true end - return keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) end diff --git a/src/api/system.c b/src/api/system.c index c998fa05..4901404d 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -18,9 +18,11 @@ extern SDL_Window *window; static const char* button_name(int button) { switch (button) { - case 1 : return "left"; - case 2 : return "middle"; - case 3 : return "right"; + case SDL_BUTTON_LEFT : return "left"; + case SDL_BUTTON_MIDDLE : return "middle"; + case SDL_BUTTON_RIGHT : return "right"; + case SDL_BUTTON_X1 : return "x1"; + case SDL_BUTTON_X2 : return "x2"; default : return "?"; } } From 4e313d9fc57a9b838761b3b088f6b087d56cf9cb Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 20:00:06 -0400 Subject: [PATCH 090/135] Propogated mouse clicks correctly. --- data/core/rootview.lua | 2 +- data/plugins/projectsearch.lua | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 0d219474..d8f78024 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -877,7 +877,7 @@ function RootView:on_mouse_pressed(button, x, y, clicks) 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) + return node.active_view:on_mouse_pressed(button, x, y, clicks) end end 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 From 7905ddd26f91f546debdc95d919451a64fe129fc Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 20:02:55 -0400 Subject: [PATCH 091/135] Fixed propogation again. --- data/core/rootview.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index d8f78024..ad47ffd4 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -857,17 +857,18 @@ function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) if div 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} From 6bdcfc824d9beabfa18f61e0c99eff1c49ec9ac8 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 22:07:23 -0400 Subject: [PATCH 092/135] Rearranged things to make a bit more sense. --- data/core/commands/doc.lua | 15 +++++++++++++++ data/core/docview.lua | 10 ---------- data/core/keymap.lua | 4 ++-- data/core/rootview.lua | 1 + 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index b7677d99..90e0eee9 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -392,12 +392,27 @@ local commands = { ["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 } doc():set_selection(line2, col2, line1, col1) end, ["doc:set-cursor"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) doc():set_selection(line, col, line, col) + dv().mouse_selecting = { line, col } + core.blink_reset() + end, + + ["doc:set-cursor-word"] = function(x, y, clicks) + local line, col = dv():resolve_screen_position(x, y) + command.perform("doc:select-word") + dv().mouse_selecting = { line, col } + end, + + ["doc:set-cursor-line"] = function(x, y, clicks) + local line, col = dv():resolve_screen_position(x, y) + command.perform("doc:select-lines") + dv().mouse_selecting = { line, col } end, ["doc:split-cursor"] = function(x, y, clicks) diff --git a/data/core/docview.lua b/data/core/docview.lua index fe440274..eac5f835 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -224,15 +224,6 @@ function DocView:scroll_to_make_visible(line, col) end end -function DocView:on_mouse_pressed(button, x, y, clicks) - local line, col = self:resolve_screen_position(x, y) - self.mouse_selecting = { line, col, clicks = clicks } - if DocView.super.on_mouse_pressed(self, button, x, y, clicks) then - return - end - core.blink_reset() -end - function DocView:on_mouse_moved(x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...) @@ -245,7 +236,6 @@ 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 if keymap.modkeys["ctrl"] then if l1 > l2 then l1, l2 = l2, l1 end self.doc.selections = { } diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 517a83ef..3ecdd589 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -200,8 +200,8 @@ keymap.add_direct { ["shift+lclick"] = "doc:select-to-cursor", ["ctrl+lclick"] = "doc:split-cursor", ["lclick"] = "doc:set-cursor", - ["dlclick"] = "doc:select-word", - ["tlclick"] = "doc:select-lines", + ["dlclick"] = "doc:set-cursor-word", + ["tlclick"] = "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/rootview.lua b/data/core/rootview.lua index ad47ffd4..6c3d1d2d 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -874,6 +874,7 @@ function RootView:on_mouse_pressed(button, x, y, clicks) 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) From 1968d31b7c06bfb06c1c06e844fa3f038ceaec0e Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 22:09:54 -0400 Subject: [PATCH 093/135] Keymap. --- data/core/commands/doc.lua | 2 ++ data/core/keymap-macos.lua | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 90e0eee9..a564c6f6 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -405,12 +405,14 @@ local commands = { ["doc:set-cursor-word"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) + doc():set_selection(line, col, line, col) command.perform("doc:select-word") dv().mouse_selecting = { line, col } end, ["doc:set-cursor-line"] = function(x, y, clicks) local line, col = dv():resolve_screen_position(x, y) + doc():set_selection(line, col, line, col) command.perform("doc:select-lines") dv().mouse_selecting = { line, col } end, diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index aa4cccec..8d23e050 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -96,8 +96,8 @@ local function keymap_macos(keymap) ["shift+lclick"] = "doc:select-to-cursor", ["ctrl+lclick"] = "doc:split-cursor", ["lclick"] = "doc:set-cursor", - ["dlclick"] = "doc:select-word", - ["tlclick"] = "doc:select-lines", + ["dlclick"] = "doc:set-cursor-word", + ["tlclick"] = "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", From c04dc648ded0cd2c21a4c5895f3b6e951783c0dc Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 5 Oct 2021 22:13:16 -0400 Subject: [PATCH 094/135] Refactored things out. --- data/core/commands/doc.lua | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index a564c6f6..ba3d1f0c 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -82,6 +82,16 @@ local function split_cursor(direction) core.blink_reset() end +local function set_cursor(x, y, type) + local line, col = dv():resolve_screen_position(x, y) + doc():set_selection(line, col, line, col) + if type == "word" or type == "lines" then + command.perform("doc:select-" .. type) + end + dv().mouse_selecting = { line, col } + core.blink_reset() +end + local commands = { ["doc:undo"] = function() doc():undo() @@ -396,25 +406,16 @@ local commands = { doc():set_selection(line2, col2, line1, col1) end, - ["doc:set-cursor"] = function(x, y, clicks) - local line, col = dv():resolve_screen_position(x, y) - doc():set_selection(line, col, line, col) - dv().mouse_selecting = { line, col } - core.blink_reset() + ["doc:set-cursor"] = function(x, y) + set_cursor(x, y, "set") end, - ["doc:set-cursor-word"] = function(x, y, clicks) - local line, col = dv():resolve_screen_position(x, y) - doc():set_selection(line, col, line, col) - command.perform("doc:select-word") - dv().mouse_selecting = { line, col } - end, + ["doc:set-cursor-word"] = function(x, y) + set_cursor(x, y, "word") + end, ["doc:set-cursor-line"] = function(x, y, clicks) - local line, col = dv():resolve_screen_position(x, y) - doc():set_selection(line, col, line, col) - command.perform("doc:select-lines") - dv().mouse_selecting = { line, col } + set_cursor(x, y, "lines") end, ["doc:split-cursor"] = function(x, y, clicks) From 7a3e8ed86a5a3608cd0de0d3e9a4099d5cbbfdae Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 14:31:15 -0500 Subject: [PATCH 095/135] Added in mousewheel as part of this. --- data/core/init.lua | 4 +++- data/core/keymap.lua | 5 +++++ data/plugins/scale.lua | 13 ++----------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index a8aa0645..d07f1cbc 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -928,7 +928,9 @@ function core.on_event(type, ...) 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 diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 3ecdd589..ef8c03ee 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -85,6 +85,11 @@ function keymap.on_key_pressed(k, ...) return false end +function keymap.on_mouse_wheel(delta, ...) + return not keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...) + and keymap.on_key_pressed("wheel", delta, ...) +end + local click_prefixes = { "", "d", "t" } function keymap.on_mouse_pressed(button, x, y, clicks) return keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index b8384609..56eabbb0 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -67,17 +67,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 +90,8 @@ keymap.add { ["ctrl+0"] = "scale:reset", ["ctrl+-"] = "scale:decrease", ["ctrl+="] = "scale:increase", + ["ctrl+wheelup"] = "scale:increase", + ["ctrl+wheeldown"] = "scale:decrease" } return { From 7babed1e6b3b45d5c169a3178cd09256fd0e7c45 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 14:38:05 -0500 Subject: [PATCH 096/135] Added in more broad strokes for clicking to match wheel. 's' is single, 'd' is double, 't' is triple, and no prefix will always take any amount of clicks. --- data/core/keymap-macos.lua | 6 +++--- data/core/keymap.lua | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 8d23e050..d9abf876 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -93,9 +93,9 @@ local function keymap_macos(keymap) ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", - ["shift+lclick"] = "doc:select-to-cursor", - ["ctrl+lclick"] = "doc:split-cursor", - ["lclick"] = "doc:set-cursor", + ["shift+slclick"] = "doc:select-to-cursor", + ["ctrl+slclick"] = "doc:split-cursor", + ["slclick"] = "doc:set-cursor", ["dlclick"] = "doc:set-cursor-word", ["tlclick"] = "doc:set-cursor-line", ["shift+left"] = "doc:select-to-previous-char", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index ef8c03ee..e3fe15a4 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -90,9 +90,10 @@ function keymap.on_mouse_wheel(delta, ...) and keymap.on_key_pressed("wheel", delta, ...) end -local click_prefixes = { "", "d", "t" } +local click_prefixes = { "s", "d", "t" } function keymap.on_mouse_pressed(button, x, y, clicks) - return keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) + return not keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) + keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) end function keymap.on_key_released(k) @@ -202,9 +203,9 @@ keymap.add_direct { ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", - ["shift+lclick"] = "doc:select-to-cursor", - ["ctrl+lclick"] = "doc:split-cursor", - ["lclick"] = "doc:set-cursor", + ["shift+slclick"] = "doc:select-to-cursor", + ["ctrl+slclick"] = "doc:split-cursor", + ["slclick"] = "doc:set-cursor", ["dlclick"] = "doc:set-cursor-word", ["tlclick"] = "doc:set-cursor-line", ["shift+left"] = "doc:select-to-previous-char", From 50c06594455f5797ea876a83be31c4e3afbffaf2 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 15:42:03 -0500 Subject: [PATCH 097/135] Also changed docview mousewheel into a scroll command. --- data/core/commands/root.lua | 9 +++++++++ data/core/keymap.lua | 3 ++- data/core/view.lua | 6 +----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index e41c723d..7ebb0e60 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 = { @@ -77,6 +78,14 @@ local t = { local n = (parent.a == node) and 0.1 or -0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) end, + + ["root:scroll"] = function(delta) + if core.active_view and core.active_view.scrollable then + core.active_view.scroll.to.y = core.active_view.scroll.to.y + delta * -config.mouse_wheel_scroll + return true + end + return false + end } diff --git a/data/core/keymap.lua b/data/core/keymap.lua index e3fe15a4..2c60f8d9 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -92,7 +92,7 @@ end local click_prefixes = { "s", "d", "t" } function keymap.on_mouse_pressed(button, x, y, clicks) - return not keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) + return not keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) and keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) end @@ -143,6 +143,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", diff --git a/data/core/view.lua b/data/core/view.lua index d1374ee4..d6d1bcbc 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 From 2931bdeb68409c7d3110f63cc39db09da2ff15ee Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Nov 2021 15:43:40 -0500 Subject: [PATCH 098/135] Can't forget mac. --- data/core/keymap-macos.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index d9abf876..c6564310 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", From d8473a3e00cece41bc66c3bb75d1d63e897f2383 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 14 Nov 2021 15:44:54 -0500 Subject: [PATCH 099/135] Changed click prefixes to be numbers, as Takase suggested. --- data/core/config.lua | 1 + data/core/keymap-macos.lua | 10 +++++----- data/core/keymap.lua | 13 ++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/core/config.lua b/data/core/config.lua index 7cf23925..faffc27e 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -28,6 +28,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/keymap-macos.lua b/data/core/keymap-macos.lua index c6564310..b0bd41a5 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -95,11 +95,11 @@ local function keymap_macos(keymap) ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", - ["shift+slclick"] = "doc:select-to-cursor", - ["ctrl+slclick"] = "doc:split-cursor", - ["slclick"] = "doc:set-cursor", - ["dlclick"] = "doc:set-cursor-word", - ["tlclick"] = "doc:set-cursor-line", + ["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 2c60f8d9..d643242f 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -90,9 +90,8 @@ function keymap.on_mouse_wheel(delta, ...) and keymap.on_key_pressed("wheel", delta, ...) end -local click_prefixes = { "s", "d", "t" } function keymap.on_mouse_pressed(button, x, y, clicks) - return not keymap.on_key_pressed(click_prefixes[((clicks - 1) % 3) + 1] .. button:sub(1,1) .. "click", x, y, clicks) and + return not keymap.on_key_pressed((((clicks - 1) % config.max_clicks) + 1) .. button:sub(1,1) .. "click", x, y, clicks) and keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) end @@ -204,11 +203,11 @@ keymap.add_direct { ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", - ["shift+slclick"] = "doc:select-to-cursor", - ["ctrl+slclick"] = "doc:split-cursor", - ["slclick"] = "doc:set-cursor", - ["dlclick"] = "doc:set-cursor-word", - ["tlclick"] = "doc:set-cursor-line", + ["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", From acc6667f575d983c3b664e34637a99fa48bda954 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 14 Nov 2021 15:45:46 -0500 Subject: [PATCH 100/135] Bug. --- data/core/keymap.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index d643242f..da819709 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 = {} From 6750ddca2a07f0ff1abd3a08af97c6df94458ac6 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 14 Nov 2021 15:46:33 -0500 Subject: [PATCH 101/135] Changed name of x1 and x2 to x and y. --- src/api/system.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/system.c b/src/api/system.c index 4901404d..dc87b723 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -21,8 +21,8 @@ static const char* button_name(int button) { case SDL_BUTTON_LEFT : return "left"; case SDL_BUTTON_MIDDLE : return "middle"; case SDL_BUTTON_RIGHT : return "right"; - case SDL_BUTTON_X1 : return "x1"; - case SDL_BUTTON_X2 : return "x2"; + case SDL_BUTTON_X1 : return "x"; + case SDL_BUTTON_X2 : return "y"; default : return "?"; } } From 2463c5d209de483b9fb99bd8ea65f3cb2329743a Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 14 Nov 2021 15:51:27 -0500 Subject: [PATCH 102/135] Made keymap more flexible. --- data/core/keymap.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index da819709..257be283 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -92,8 +92,11 @@ function keymap.on_mouse_wheel(delta, ...) end function keymap.on_mouse_pressed(button, x, y, clicks) - return not keymap.on_key_pressed((((clicks - 1) % config.max_clicks) + 1) .. button:sub(1,1) .. "click", x, y, clicks) and - keymap.on_key_pressed(button:sub(1,1) .. "click", 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) From 46f81e0bad400633acd751f12acc1e4bb3f2c887 Mon Sep 17 00:00:00 2001 From: cukmekerb Date: Mon, 15 Nov 2021 09:20:34 -0800 Subject: [PATCH 103/135] add BigInt literal and numeric separators to js syntax highlighter --- data/plugins/language_js.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 7556b00b..f6486da0 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -12,7 +12,7 @@ syntax.add { { pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "`", "`", '\\' }, type = "string" }, { pattern = "0x[%da-fA-F]+", type = "number" }, - { pattern = "-?%d+[%d%.eE]*", type = "number" }, + { pattern = "-?%d+[%d%.eE_n]*", type = "number" }, { pattern = "-?%.?%d+", type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" }, From 18959aebefe22e7bcfff80a2affc8ad0fda76328 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 16 Nov 2021 19:12:39 -0500 Subject: [PATCH 104/135] Fixed predicate and minor propogation issue. --- data/core/commands/root.lua | 21 ++++++++++++--------- data/core/keymap.lua | 4 ++-- data/core/rootview.lua | 13 +++++++------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 7ebb0e60..8f2536b8 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -64,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) @@ -77,14 +77,6 @@ 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, - - ["root:scroll"] = function(delta) - if core.active_view and core.active_view.scrollable then - core.active_view.scroll.to.y = core.active_view.scroll.to.y + delta * -config.mouse_wheel_scroll - return true - end - return false end } @@ -131,3 +123,14 @@ command.add(function() local node = core.root_view:get_active_node() return not node:get_locked_size() 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/keymap.lua b/data/core/keymap.lua index 7ad04399..fd552f19 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -113,8 +113,8 @@ function keymap.on_key_pressed(k, ...) end function keymap.on_mouse_wheel(delta, ...) - return not keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...) - and keymap.on_key_pressed("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) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 6c3d1d2d..07f8b7bf 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -1002,17 +1002,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 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 @@ -1020,7 +1021,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 From 6d36f2684a376a4698a62f453cf46bba74d4b9bc Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 17 Nov 2021 08:37:37 +0800 Subject: [PATCH 105/135] add polyfill for table.pack and table.unpack --- data/core/start.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/core/start.lua b/data/core/start.lua index 71050057..2c47c902 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -19,3 +19,6 @@ package.path = DATADIR .. '/?.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path + +table.pack = table.pack or pack or function(...) return {...} end +table.unpack = table.unpack or unpack From 6a7a02542fa86dce410a0dee72fd1c8b5bcdb12b Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 17 Nov 2021 02:57:14 +0100 Subject: [PATCH 106/135] Draw only visible whitespaces --- data/plugins/drawwhitespace.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index da9d1b12..7b7fa011 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -9,24 +9,28 @@ 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 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 - - From 4f55555ca92290e699930dbd199e3042160a98c3 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Fri, 19 Nov 2021 01:05:26 -0800 Subject: [PATCH 107/135] Selection expands by word or line on double or triple click followed by drag. --- data/core/commands/doc.lua | 10 +++++----- data/core/docview.lua | 24 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index ba3d1f0c..fe1fa3b1 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -82,13 +82,13 @@ local function split_cursor(direction) core.blink_reset() end -local function set_cursor(x, y, type) +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 type == "word" or type == "lines" then - command.perform("doc:select-" .. type) + if snap_type == "word" or snap_type == "lines" then + command.perform("doc:select-" .. snap_type) end - dv().mouse_selecting = { line, col } + dv().mouse_selecting = { line, col, snap_type } core.blink_reset() end @@ -402,7 +402,7 @@ local commands = { ["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 } + dv().mouse_selecting = { line1, col1, nil } doc():set_selection(line2, col2, line1, col1) end, diff --git a/data/core/docview.lua b/data/core/docview.lua index 07da1cef..60ef62bc 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -224,6 +224,7 @@ function DocView:scroll_to_make_visible(line, col) end end + function DocView:on_mouse_moved(x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...) @@ -235,7 +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 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 = { } @@ -243,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 + 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 From f24aa64cd5af8ab2ae07bf567c4ee91d4d7095c6 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 Nov 2021 01:15:13 +0100 Subject: [PATCH 108/135] Add `soft_reset` to highlighter This allows clearing the `lines` table without removing entries. --- data/core/doc/highlighter.lua | 7 +++++++ data/core/doc/init.lua | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index c77e1138..68b66356 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -40,6 +40,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 diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 2e72907a..b0f07921 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 From d0a2c913f505a6e357bb4edff127d00cd3834930 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 Nov 2021 01:18:37 +0100 Subject: [PATCH 109/135] Pre-populate the highlighter This avoids problems with calls to `[insert,remove]_notify` on lines that the highlighter has not yet added. --- data/core/doc/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index b0f07921..06cde9f9 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -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") From b77b1c022125647d3b49b5ef6c56de29efe1d93f Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 Nov 2021 03:15:08 +0100 Subject: [PATCH 110/135] Add `Doc:get_indent_info` It returns the indentation type, size and confirmation status, used by the `Doc`. --- data/core/doc/init.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 640e9fd5..bdf6263a 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -115,6 +115,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 From 3d9901695bdec387bccbc7bc9bc9702b2d66fa13 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 Nov 2021 03:20:49 +0100 Subject: [PATCH 111/135] Use the new `Doc:get_indent_info` throughout `core` --- data/core/commands/doc.lua | 15 ++++----------- data/core/doc/init.lua | 18 ++++++++++-------- data/core/docview.lua | 4 ++-- data/core/statusview.lua | 6 +++--- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index fe1fa3b1..b2f55e26 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() @@ -157,11 +149,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 @@ -271,7 +264,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 diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index bdf6263a..af692a4d 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -494,19 +494,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 @@ -528,14 +530,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/docview.lua b/data/core/docview.lua index 60ef62bc..74fbb0bb 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -395,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() 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", From 2de48b6ac8541f90949c9ca03db3a3bcf5ecd36e Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 Nov 2021 03:22:53 +0100 Subject: [PATCH 112/135] Adapt `detectindent` to the new indentation architecture --- data/plugins/detectindent.lua | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) 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] From 3176b467ca26d904dc2cac06f02e2394da57f9ba Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 21 Nov 2021 03:46:43 +0100 Subject: [PATCH 113/135] Add names to language plugins --- data/plugins/language_c.lua | 1 + data/plugins/language_cpp.lua | 1 + data/plugins/language_css.lua | 1 + data/plugins/language_html.lua | 1 + data/plugins/language_js.lua | 1 + data/plugins/language_lua.lua | 1 + data/plugins/language_md.lua | 1 + data/plugins/language_python.lua | 1 + data/plugins/language_xml.lua | 1 + 9 files changed, 9 insertions(+) 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..291c1287 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 = { 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..62cb8a86 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -4,6 +4,7 @@ local syntax = require "core.syntax" syntax.add { + name = "Markdown", files = { "%.md$", "%.markdown$" }, patterns = { { pattern = "\\.", type = "normal" }, diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 60aa41a6..f1430fb1 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -2,6 +2,7 @@ local syntax = require "core.syntax" syntax.add { + name = "Python", files = { "%.py$", "%.pyw$" }, headers = "^#!.*[ /]python", comment = "#", 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 = { From bede0ff878c2720402c9fbd4152f9ad359cac134 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Sun, 21 Nov 2021 00:24:24 -0800 Subject: [PATCH 114/135] Allow for color overrides in the tree view --- data/plugins/treeview.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index fa3ab53a..8768f036 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -43,6 +43,7 @@ function TreeView:new() self.cache = {} self.last = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } + self.color_overrides = {} end @@ -295,6 +296,16 @@ function TreeView:draw_tooltip() end +function TreeView:set_color_override(abs_filename, color) + self.color_overrides[abs_filename] = color +end + + +function TreeView:clear_all_color_overrides() + self.color_overrides = {} +end + + function TreeView:draw() self:draw_background(style.background2) @@ -318,6 +329,9 @@ function TreeView:draw() color = style.accent end + -- allow for color overrides + local icon_color = self.color_overrides[item.abs_filename] or color + -- icons x = x + item.depth * style.padding.x + style.padding.x if item.type == "dir" then @@ -325,11 +339,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 From 373007a7675a0de7add8447c0b42eb5bd64f5f24 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Sun, 21 Nov 2021 15:24:45 -0800 Subject: [PATCH 115/135] Switched to TreeView:color_for_item(abs_path) --- data/plugins/treeview.lua | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 8768f036..70dca08f 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -43,7 +43,6 @@ function TreeView:new() self.cache = {} self.last = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } - self.color_overrides = {} end @@ -296,13 +295,9 @@ function TreeView:draw_tooltip() end -function TreeView:set_color_override(abs_filename, color) - self.color_overrides[abs_filename] = color -end - - -function TreeView:clear_all_color_overrides() - self.color_overrides = {} +function TreeView:color_for_item(abs_filename) + -- other plugins can override this to customize the color of each icon + return nil end @@ -330,7 +325,7 @@ function TreeView:draw() end -- allow for color overrides - local icon_color = self.color_overrides[item.abs_filename] or color + local icon_color = self:color_for_item(item.abs_filename) or color -- icons x = x + item.depth * style.padding.x + style.padding.x From 23a0f6ca796651e122f3e921aea35f6e495e3f65 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Mon, 22 Nov 2021 06:23:16 +0100 Subject: [PATCH 116/135] Speed up highlighter notify Avoid calling `table.{insert,remove}` multiple times, as this causes multiple shifts in the `self.lines` table. --- data/core/doc/highlighter.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index c77e1138..22ed12a4 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" @@ -51,16 +52,16 @@ end function Highlighter:insert_notify(line, n) self:invalidate(line) + local blanks = { } for i = 1, n do - table.insert(self.lines, line, false) + 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 From 65f125176717e1adf666d87c25481aa6a291701f Mon Sep 17 00:00:00 2001 From: cukmekerb Date: Mon, 22 Nov 2021 12:15:19 -0800 Subject: [PATCH 117/135] JS hex BigInts and hex numeric separators --- data/plugins/language_js.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index f6486da0..dd1151eb 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -11,7 +11,7 @@ syntax.add { { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "`", "`", '\\' }, type = "string" }, - { pattern = "0x[%da-fA-F]+", type = "number" }, + { pattern = "0x[%da-fA-F_]+n?", type = "number" }, { pattern = "-?%d+[%d%.eE_n]*", type = "number" }, { pattern = "-?%.?%d+", type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, From bc5be3c9b718d92b37576a10f7a77fb5b4496077 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 22 Nov 2021 18:13:43 -0500 Subject: [PATCH 118/135] Support no antialiasing. --- src/api/renderer.c | 12 +++++++----- src/renderer.c | 49 +++++++++++++++++++++++++++------------------- src/renderer.h | 3 ++- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/api/renderer.c b/src/api/renderer.c index 60256118..f2f3045f 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -6,16 +6,18 @@ static int f_font_load(lua_State *L) { const char *filename = luaL_checkstring(L, 1); float size = luaL_checknumber(L, 2); unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0; - bool subpixel = true; + ERenFontAntialiasing font_antialiasing = FONT_ANTIALIASING_SUBPIXEL; if (lua_gettop(L) > 2 && lua_istable(L, 3)) { lua_getfield(L, 3, "antialiasing"); if (lua_isstring(L, -1)) { const char *antialiasing = lua_tostring(L, -1); if (antialiasing) { - if (strcmp(antialiasing, "grayscale") == 0) { - subpixel = false; + if (strcmp(antialiasing, "none") == 0) { + font_antialiasing = FONT_ANTIALIASING_NONE; + } else if (strcmp(antialiasing, "grayscale") == 0) { + font_antialiasing = FONT_ANTIALIASING_GRAYSCALE; } else if (strcmp(antialiasing, "subpixel") == 0) { - subpixel = true; + font_antialiasing = FONT_ANTIALIASING_SUBPIXEL; } else { return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing); } @@ -48,7 +50,7 @@ static int f_font_load(lua_State *L) { lua_pop(L, 5); } RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); - *font = ren_font_load(filename, size, subpixel, font_hinting, font_style); + *font = ren_font_load(filename, size, font_antialiasing, font_hinting, font_style); if (!*font) return luaL_error(L, "failed to load font"); luaL_setmetatable(L, API_TYPE_FONT); diff --git a/src/renderer.c b/src/renderer.c index 5aa90ff1..b0e17f58 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -43,7 +43,7 @@ typedef struct RenFont { GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS]; float size, space_advance, tab_advance; short max_height; - bool subpixel; + ERenFontAntialiasing antialiasing; ERenFontHinting hinting; unsigned char style; char path[0]; @@ -66,16 +66,16 @@ static const char* utf8_to_codepoint(const char *p, unsigned *dst) { } static int font_set_load_options(RenFont* font) { - switch (font->hinting) { - case FONT_HINTING_SLIGHT: return FT_LOAD_TARGET_LIGHT | FT_LOAD_FORCE_AUTOHINT; - case FONT_HINTING_FULL: return FT_LOAD_TARGET_NORMAL | FT_LOAD_FORCE_AUTOHINT; - case FONT_HINTING_NONE: return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING; - } - return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING; + int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO + : (font->hinting == FONT_HINTING_SLIGHT ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_NORMAL); + int hinting = font->hinting == FONT_HINTING_NONE ? FT_LOAD_NO_HINTING : FT_LOAD_FORCE_AUTOHINT; + return load_target | hinting; } static int font_set_render_options(RenFont* font) { - if (font->subpixel) { + if (font->antialiasing == FONT_ANTIALIASING_NONE) + return FT_RENDER_MODE_MONO; + if (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL) { unsigned char weights[] = { 0x10, 0x40, 0x70, 0x40, 0x10 } ; switch (font->hinting) { case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break; @@ -106,8 +106,8 @@ static int font_set_style(FT_Outline* outline, int x_translation, unsigned char static void font_load_glyphset(RenFont* font, int idx) { unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font); - int bitmaps_cached = font->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1; - unsigned int byte_width = font->subpixel ? 3 : 1; + int bitmaps_cached = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1; + unsigned int byte_width = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1; for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) { GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet))); font->sets[j][idx] = set; @@ -117,13 +117,15 @@ static void font_load_glyphset(RenFont* font, int idx) { continue; FT_GlyphSlot slot = font->face->glyph; int glyph_width = slot->bitmap.width / byte_width; + if (font->antialiasing == FONT_ANTIALIASING_NONE) + glyph_width *= 8; set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f}; pen_x += glyph_width; font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height; } if (pen_x == 0) continue; - set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->subpixel ? 24 : 8, 0, 0, 0, 0)); + set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 24 : 8, 0, 0, 0, 0)); unsigned char* pixels = set->surface->pixels; for (int i = 0; i < MAX_GLYPHSET; ++i) { int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); @@ -136,7 +138,14 @@ static void font_load_glyphset(RenFont* font, int idx) { for (int line = 0; line < slot->bitmap.rows; ++line) { int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width; int source_offset = line * slot->bitmap.pitch; - memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); + if (font->antialiasing == FONT_ANTIALIASING_NONE) { + for (int column = 0; column < slot->bitmap.width; ++column) { + int current_source_offset = source_offset + (column / 8); + int source_pixel = slot->bitmap.buffer[current_source_offset]; + pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7; + } + } else + memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); } } } @@ -144,9 +153,9 @@ static void font_load_glyphset(RenFont* font, int idx) { static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) { int idx = (codepoint >> 8) % MAX_LOADABLE_GLYPHSETS; - if (!font->sets[font->subpixel ? subpixel_idx : 0][idx]) + if (!font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx]) font_load_glyphset(font, idx); - return font->sets[font->subpixel ? subpixel_idx : 0][idx]; + return font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx]; } static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) { @@ -163,7 +172,7 @@ static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFo return fonts[0]; } -RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned char hinting, unsigned char style) { +RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) { FT_Face face; if (FT_New_Face( library, path, 0, &face)) return NULL; @@ -175,7 +184,7 @@ RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned cha strcpy(font->path, path); font->face = face; font->size = size; - font->subpixel = subpixel; + font->antialiasing = antialiasing; font->hinting = hinting; font->style = style; font->space_advance = (int)font_get_glyphset(font, ' ', 0)->metrics[' '].xadvance; @@ -187,7 +196,7 @@ RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned cha } RenFont* ren_font_copy(RenFont* font, float size) { - return ren_font_load(font->path, size, font->subpixel, font->hinting, font->style); + return ren_font_load(font->path, size, font->antialiasing, font->hinting, font->style); } void ren_font_free(RenFont* font) { @@ -206,7 +215,7 @@ void ren_font_free(RenFont* font) { void ren_font_group_set_tab_size(RenFont **fonts, int n) { for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) { - for (int i = 0; i < (fonts[j]->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) + for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) font_get_glyphset(fonts[j], '\t', i)->metrics['\t'].xadvance = fonts[j]->space_advance * n; } } @@ -274,11 +283,11 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor glyph_start += offset; } unsigned int* destination_pixel = (unsigned int*)&destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]; - unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->subpixel ? 3 : 1)]; + unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1)]; for (int x = glyph_start; x < glyph_end; ++x) { unsigned int destination_color = *destination_pixel; SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift }; - SDL_Color src = { *(font->subpixel ? source_pixel++ : source_pixel), *(font->subpixel ? source_pixel++ : source_pixel), *source_pixel++ }; + SDL_Color src = { *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *source_pixel++ }; r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025; g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; diff --git a/src/renderer.h b/src/renderer.h index 6058583e..a97706ff 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -8,11 +8,12 @@ #define FONT_FALLBACK_MAX 4 typedef struct RenFont RenFont; typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting; +typedef enum { FONT_ANTIALIASING_NONE, FONT_ANTIALIASING_GRAYSCALE, FONT_ANTIALIASING_SUBPIXEL } ERenFontAntialiasing; typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4 } ERenFontStyle; typedef struct { uint8_t b, g, r, a; } RenColor; typedef struct { int x, y, width, height; } RenRect; -RenFont* ren_font_load(const char *filename, float size, bool subpixel, unsigned char hinting, unsigned char style); +RenFont* ren_font_load(const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style); RenFont* ren_font_copy(RenFont* font, float size); void ren_font_free(RenFont *font); int ren_font_group_get_tab_size(RenFont **font); From cd2adb4a30c4d139e8d2858482f799fde1aca87c Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 10 Sep 2021 21:58:11 +0200 Subject: [PATCH 119/135] Apply again 1976facaf1a2b9934f3cfdb4524712c1418000ef Use reverse search for `find-replace:previous-find` --- data/core/commands/findreplace.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 8fcb8dbc..0801b745 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -7,8 +7,7 @@ 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 @@ -54,9 +53,9 @@ end local function find(label, search_fn) - last_view, last_sel, last_finds = core.active_view, - { core.active_view.doc:get_selection() }, {} - local text = last_view.doc:get_text(table.unpack(last_sel)) + 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 core.command_view:set_text(text, true) From 00d555b016eb93bcab617479a413d5a3ee47df12 Mon Sep 17 00:00:00 2001 From: PIESEL <50019824+piotrek94692@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:30:15 +0100 Subject: [PATCH 120/135] Apply again cd10497b495f42f5cb102aac93af3e9280e65619 Use Python syntax highlighting for Ren'Py scripts. --- data/plugins/language_python.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index f1430fb1..8bc6fbd4 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -3,7 +3,7 @@ local syntax = require "core.syntax" syntax.add { name = "Python", - files = { "%.py$", "%.pyw$" }, + files = { "%.py$", "%.pyw$", "%.rpy$" }, headers = "^#!.*[ /]python", comment = "#", patterns = { From 3162f4ea4f63ccb00b093a646933314996166be1 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 18:42:01 -0500 Subject: [PATCH 121/135] Added in the ability to specify a color for whitespace. --- data/plugins/drawwhitespace.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 7b7fa011..0004c7ea 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -7,8 +7,8 @@ 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 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 From cc3fddd1e56b44a2bd98582b72080093bb5c9800 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 20:34:01 -0500 Subject: [PATCH 122/135] Added in check to make sure you can use a scrollbar on a split. --- data/core/rootview.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 49da2923..bedea557 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -877,11 +877,11 @@ 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 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 true @@ -1030,7 +1030,7 @@ function RootView:on_mouse_moved(x, y, dx, dy) 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") @@ -1125,10 +1125,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 From 64f66e5d1e22d8ea38fdc7b8830cfe4b4c008c06 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 21:03:38 -0500 Subject: [PATCH 123/135] Added in cut, copy and paste to the context menu. Also removed find pattern, as that's no longer a valid command. Also made it so commands only show up if their predicates are valid. --- data/core/command.lua | 3 +++ data/core/commands/doc.lua | 31 ++++++++++++++++++------------- data/core/contextmenu.lua | 8 ++++++-- data/core/doc/init.lua | 7 +++++++ data/plugins/contextmenu.lua | 16 ++++++++-------- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/data/core/command.lua b/data/core/command.lua index 2531fb96..2cf851da 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -41,6 +41,9 @@ 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 cmd = command.map[name] diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index fe1fa3b1..89a17be0 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -92,6 +92,21 @@ local function set_cursor(x, y, snap_type) core.blink_reset() end +local selection_commands = { + ["doc:cut"] = function() + cut_or_copy(true) + end, + + ["doc:copy"] = function() + 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() @@ -101,14 +116,6 @@ local commands = { doc():redo() end, - ["doc:cut"] = function() - cut_or_copy(true) - end, - - ["doc:copy"] = function() - cut_or_copy(false) - end, - ["doc:paste"] = function() local clipboard = system.get_clipboard() -- If the clipboard has changed since our last look, use that instead @@ -173,11 +180,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) @@ -481,3 +483,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/contextmenu.lua b/data/core/contextmenu.lua index d6131cdf..9db35bb3 100644 --- a/data/core/contextmenu.lua +++ b/data/core/contextmenu.lua @@ -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/init.lua b/data/core/doc/init.lua index 03dcc31e..a46e428c 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -149,6 +149,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) 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 From 463605ff41a844d3b301a5fe21f6f27ca5e510e8 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 21:56:07 -0500 Subject: [PATCH 124/135] Fixed event propogation. --- data/core/rootview.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 49da2923..e497919e 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -900,9 +900,7 @@ function RootView:on_mouse_pressed(button, x, y, clicks) 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 - return 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 From 7ee23da1870525b3dd4820f1add874f2437ccd13 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 22:30:35 -0500 Subject: [PATCH 125/135] Added in additional environment variables to scale off of. --- data/core/start.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/start.lua b/data/core/start.lua index 31fed147..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("^(.+)[/\\][^/\\]+$") From f7b3a2b0c222fd6b6e21fdc74bc8049caaafe374 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 23 Nov 2021 22:35:11 -0500 Subject: [PATCH 126/135] Added an exclusion for lineguide in the commandview. --- data/plugins/lineguide.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 9f2fca4a..96745659 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -2,18 +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 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) - + 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 From 5dca37b11aa882cd01d6fbd8b956de892b65717d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 24 Nov 2021 05:03:42 +0100 Subject: [PATCH 127/135] Don't search if there are no files --- data/core/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/core/init.lua b/data/core/init.lua index daec49a6..35609496 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -191,6 +191,7 @@ end local function file_search(files, info) local filename, type = info.filename, info.type local inf, sup = 1, #files + if sup <= 0 then return 1, false end 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 From 59f64088e1e88f2f2a29e4d5418ccdf781fdc12e Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 24 Nov 2021 06:16:54 +0100 Subject: [PATCH 128/135] Remove changed files/dirs from `TreeView` cache --- data/core/init.lua | 2 +- data/plugins/treeview.lua | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 35609496..b0aef1af 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1157,7 +1157,7 @@ end -- no-op but can be overrided by plugins -function core.on_dirmonitor_modify() +function core.on_dirmonitor_modify(dir, filepath) end diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 659393ec..2e66083a 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -42,6 +42,14 @@ function TreeView:new() self.target_size = default_treeview_size self.cache = {} 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 From 0c488c94920a1100986fa73e11ccc4a2c5cd5667 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 26 Nov 2021 13:45:13 +0100 Subject: [PATCH 129/135] Fix logic in project's file insertion The function "file_search" in core.init was sometimes giving a wrong index value, off by one. The problem happened for example when the entry to search was "less than" the first entry, the function returned a value of two instead of one as expected. The bug was easily observed creating a new directory with a name that comes as the first in alphabetical order within the project. --- data/core/init.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 35609496..8f2a8075 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -191,7 +191,6 @@ end local function file_search(files, info) local filename, type = info.filename, info.type local inf, sup = 1, #files - if sup <= 0 then return 1, false end 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 @@ -200,12 +199,12 @@ local function file_search(files, info) inf = curr end end - repeat + 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 - until inf > sup or system.path_compare(filename, type, files[inf].filename, files[inf].type) + end return inf, false end From 272ecd64bf8cf7f6b0cb7d92496a7e42ec0b7ab1 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Fri, 26 Nov 2021 23:25:34 -0800 Subject: [PATCH 130/135] Removed docs for get_width_subpixel and subpixel_scale which no longer exist. --- docs/api/renderer.lua | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index 6820a14d..7a9b636d 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -61,15 +61,6 @@ function renderer.font:set_tab_size(chars) end ---@return number function renderer.font:get_width(text) end ---- ----Get the width in subpixels of the given text when ----rendered with this font. ---- ----@param text string ---- ----@return number -function renderer.font:get_width_subpixel(text) end - --- ---Get the height in pixels that occupies a single character ---when rendered with this font. @@ -77,12 +68,6 @@ function renderer.font:get_width_subpixel(text) end ---@return number function renderer.font:get_height() end ---- ----Gets the font subpixel scale. ---- ----@return number -function renderer.font:subpixel_scale() end - --- ---Get the current size of the font. --- From 01e38f041ad01f82e3b66f0e678dc059bafe79f6 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 27 Nov 2021 13:16:49 -0500 Subject: [PATCH 131/135] Used basenames for ignore_files rather than full paths. --- data/core/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 1e343e6c..aadccc66 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -100,7 +100,7 @@ local function get_project_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(info.filename, config.ignore_files) + not common.match_pattern(common.basename(info.filename), config.ignore_files) and info) end end From ef4c02ab0ef647c5ca842199267da7541e5f65e3 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 28 Nov 2021 07:16:53 +0100 Subject: [PATCH 132/135] Check the entire path in `TreeView` predicate --- data/plugins/treeview.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 2e66083a..a5604722 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -406,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, { @@ -417,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" }, From d7afcb08b1a03ca2823070aaa12322d736d5b58d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Tue, 30 Nov 2021 01:11:35 +0100 Subject: [PATCH 133/135] Check the entire path in `TreeView` `new-file` and `new-folder` commands --- data/plugins/treeview.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index a5604722..4f5db701 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -467,9 +467,8 @@ command.add(function() return view.hovered_item ~= nil end, { 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 @@ -482,9 +481,8 @@ command.add(function() return view.hovered_item ~= nil end, { 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 From 5029e2ce29ff88fc6ec1aafd23cc0b43ee228a5d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 2 Dec 2021 22:34:49 +0100 Subject: [PATCH 134/135] Add name to plain text fallback syntax --- data/core/syntax.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/syntax.lua b/data/core/syntax.lua index de8ec9d0..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) From 0705c23c357a1416ad98eb9c3f00e6fe26e9dbb3 Mon Sep 17 00:00:00 2001 From: Nightwing Date: Fri, 3 Dec 2021 23:50:23 +0900 Subject: [PATCH 135/135] Improved Markdown syntax highlighter --- data/plugins/language_md.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index 62cb8a86..e7c870ec 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -9,7 +9,6 @@ syntax.add { 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" }, @@ -26,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" },