From bd50401687c924ecacb979e419c447c5316bfeda Mon Sep 17 00:00:00 2001 From: jgmdev Date: Fri, 25 Jun 2021 02:25:05 -0400 Subject: [PATCH 001/180] Documented with EmmyLua the C API using .lua interface files. --- docs/api/globals.lua | 21 ++++ docs/api/process.lua | 163 +++++++++++++++++++++++++++++ docs/api/regex.lua | 57 ++++++++++ docs/api/renderer.lua | 177 ++++++++++++++++++++++++++++++++ docs/api/system.lua | 234 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 652 insertions(+) create mode 100644 docs/api/globals.lua create mode 100644 docs/api/process.lua create mode 100644 docs/api/regex.lua create mode 100644 docs/api/renderer.lua create mode 100644 docs/api/system.lua diff --git a/docs/api/globals.lua b/docs/api/globals.lua new file mode 100644 index 00000000..98fe61b1 --- /dev/null +++ b/docs/api/globals.lua @@ -0,0 +1,21 @@ +---@meta + +---The command line arguments given to lite. +---@type table +ARGS = {} + +---The current operating system. +---@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'" +PLATFORM = "Operating System" + +---The current text or ui scale. +---@type number +SCALE = 1.0 + +---Full path of lite executable. +---@type string +EXEFILE = "/path/to/lite" + +---Path to the users home directory. +---@type string +HOME = "/path/to/user/dir" diff --git a/docs/api/process.lua b/docs/api/process.lua new file mode 100644 index 00000000..0374c0c6 --- /dev/null +++ b/docs/api/process.lua @@ -0,0 +1,163 @@ +---@meta + +--- +---Functionality that allows you to launch subprocesses and read +---or write to them in a non-blocking fashion. +---@class process +process = {} + +---Error triggered when the stdout, stderr or stdin fails while reading +---or writing, its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_PIPE = -1 + +---Error triggered when a read or write action is blocking, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_WOULDBLOCK = -2 + +---Error triggered when a process takes more time than that specified +---by the deadline parameter given on process:start(), +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_TIMEDOUT = -3 + +---Error triggered when trying to terminate or kill a non running process, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_INVALID = -4 + +---Used for the process:close_stream() method to close stdin. +---@type integer +process.STREAM_STDIN = 0 + +---Used for the process:close_stream() method to close stdout. +---@type integer +process.STREAM_STDOUT = 1 + +---Used for the process:close_stream() method to close stderr. +---@type integer +process.STREAM_STDERR = 2 + +---Instruct process:wait() to wait until the process ends. +---@type integer +process.WAIT_INFINITE = -1 + +---Instruct process:wait() to wait until the deadline given on process:start() +---@type integer +process.WAIT_DEADLINE = -2 + +--- +---Create a new process object +--- +---@return process +function process.new() end + +--- +---Translates an error code into a useful text message +--- +---@param code integer +--- +---@return string +function process.strerror(code) end + +--- +---Start a process +--- +---@param command_and_params table First index is the command to execute +---and subsequente elements are parameters for the command. +---@param working_directory? string Path where the command will be launched. +---@param deadline? integer Maximum time in milliseconds the +---process is allowed to run on a process:wait(process.WAIT_DEADLINE) call. +--- +---@return integer|boolean status Negative integer error code if could +---not start or true on success +function process:start(command_and_params, working_directory, deadline) end + +--- +---Get the process id. +--- +---@return integer id Process id or 0 if not running. +function process:pid() end + +--- +---Read from stdout, if the process fails with a ERROR_PIPE it is +---automatically destroyed, so checking process status with the +---process:running() method would be advised. +--- +---@param len? integer Amount of bytes to read. +---@param tries? integer Retry reading the given amount of times +---if nothing was read. +--- +---@return integer|nil bytes Amount of bytes read or nil if nothing was read. +function process:read(len, tries) end + +--- +---Read from stderr, if the process fails with a ERROR_PIPE it is +---automatically destroyed, so checking process status with the +---process:running() method would be advised. +--- +---@param len? integer Amount of bytes to read. +---@param tries? integer Retry reading the given amount of times +---if nothing was read. +--- +---@return integer|nil bytes Amount of bytes read or nil if nothing was read. +function process:read_errors(len, tries) end + +--- +---Write to the stdin, if the process fails with a ERROR_PIPE it is +---automatically destroyed, so checking process status with the +---process:running() method would be advised. +--- +---@param data string +--- +---@return integer bytes The amount of bytes written or negative integer +---error code: process.ERROR_PIPE, process.ERROR_WOULDBLOCK +function process:write(data) end + +--- +---Allows you to close a stream pipe that you will not be using. +--- +---@param stream integer Could be one of the following: +---process.STREAM_STDIN, process.STREAM_STDOUT, process.STREAM_STDERR +--- +---@return integer status Negative error code process.ERROR_INVALID if +---process is not running or stream is already closed. +function process:close_stream(stream) end + +--- +---Wait the specified amount of time for the process to exit. +--- +---@param timeout integer Time to wait in milliseconds, if 0, the function +---will only check if process is running without waiting, also the timeout +---can be set to: +--- * process.WAIT_INFINITE - will wait until the process ends +--- * process.WAIT_DEADLINE - will wait until the deadline declared on start() +--- +---@return integer exit_status The process exit status or negative integer +---error code like process.ERROR_TIMEDOUT +function process:wait(timeout) end + +--- +---Sends SIGTERM to the process +--- +---@return boolean|integer status Returns true on success or a +---negative integer error code like process.ERROR_INVALID +function process:terminate() end + +--- +---Sends SIGKILL to the process +--- +---@return boolean|integer status Returns true on success or a +---negative integer error code like process.ERROR_INVALID +function process:kill() end + +--- +---Check if the process is running +--- +---@return boolean +function process:running() end diff --git a/docs/api/regex.lua b/docs/api/regex.lua new file mode 100644 index 00000000..d1d7346c --- /dev/null +++ b/docs/api/regex.lua @@ -0,0 +1,57 @@ +---@meta + +--- +---Provides the base functionality for regular expressions matching. +---@class regex +regex = {} + +---Instruct regex:cmatch() to match only at the first position. +---@type integer +regex.ANCHORED = 0x80000000 + +---Tell regex:cmatch() that the pattern can match only at end of subject. +---@type integer +regex.ENDANCHORED = 0x20000000 + +---Tell regex:cmatch() that subject string is not the beginning of a line. +---@type integer +regex.NOTBOL = 0x00000001 + +---Tell regex:cmatch() that subject string is not the end of a line. +---@type integer +regex.NOTEOL = 0x00000002 + +---Tell regex:cmatch() that an empty string is not a valid match. +---@type integer +regex.NOTEMPTY = 0x00000004 + +---Tell regex:cmatch() that an empty string at the start of the +---subject is not a valid match. +---@type integer +regex.NOTEMPTY_ATSTART = 0x00000008 + +---@alias RegexModifiers +---|>'"i"' # Case insesitive matching +---| '"m"' # Multiline matching +---| '"s"' # Match all characters with dot (.) metacharacter even new lines + +--- +---Compiles a regular expression pattern that can be used to search in strings. +--- +---@param pattern string +---@param options? RegexModifiers A string of one or more pattern modifiers. +--- +---@return regex|string regex Ready to use regular expression object or error +---message if compiling the pattern failed. +function regex.compile(pattern, options) end + +--- +---Search a string for valid matches and returns a list of matching offsets. +--- +---@param subject string The string to search for valid matches. +---@param offset? integer The position on the subject to start searching. +---@param options? integer A bit field of matching options, eg: +---regex.NOTBOL | regex.NOTEMPTY +--- +---@return table list List of offsets where a match was found. +function regex:cmatch(subject, offset, options) end diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua new file mode 100644 index 00000000..3a1c6036 --- /dev/null +++ b/docs/api/renderer.lua @@ -0,0 +1,177 @@ +---@meta + +--- +---Core functionality to render or draw elements into the screen. +---@class renderer +renderer = {} + +--- +---Represents a color used by the rendering functions. +---@class RendererColor +---@field public r number Red +---@field public g number Green +---@field public b number Blue +---@field public a number Alpha +RendererColor = {} + +--- +---Represent options that affect a font's rendering. +---@class RendererFontOptions +---@field public antialiasing "'grayscale'" | "'subpixel'" +---@field public hinting "'slight'" | "'none'" | '"full"' +RendererFontOptions = {} + +--- +---@class renderer.font +renderer.font = {} + +--- +---Create a new font object. +--- +---@param path string +---@param size number +---@param options RendererFontOptions +--- +---@return renderer.font +function renderer.font.load(path, size, options) end + +--- +---Clones a font object into a new one. +--- +---@param size? number Optional new size for cloned font. +--- +---@return renderer.font +function renderer.font:copy(size) end + +--- +---Set the amount of characters that represent a tab. +--- +---@param chars integer Also known as tab width. +function renderer.font:set_tab_size(chars) end + +--- +---Get the width in pixels of the given text when +---rendered with this font. +--- +---@param text string +--- +---@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. +--- +---@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. +--- +---@return number +function renderer.font:get_size() end + +--- +---Set a new size for the font. +--- +---@param size number +function renderer.font:set_size(size) end + +--- +---Assistive functionality to replace characters in a +---rendered text with other characters. +---@class renderer.replacements +renderer.replacements = {} + +--- +---Create a new character replacements object. +--- +---@return renderer.replacements +function renderer.replacements.new() end + +--- +---Add to internal map a character to character replacement. +--- +---@param original_char string Should be a single character like '\t' +---@param replacement_char string Should be a single character like 'ยป' +function renderer.replacements:add(original_char, replacement_char) end + +--- +---Toggles drawing debugging rectangles on the currently rendered sections +---of the window to help troubleshoot the renderer. +--- +---@param enable boolean +function renderer.show_debug(enable) end + +--- +---Get the size of the screen area been rendered. +--- +---@return number width +---@return number height +function renderer.get_size() end + +--- +---Tell the rendering system that we want to build a new frame to render. +function renderer.begin_frame() end + +--- +---Tell the rendering system that we finished building the frame. +function renderer.end_frame() end + +--- +---Set the region of the screen where draw operations will take effect. +--- +---@param x number +---@param y number +---@param width number +---@param height number +function renderer.set_clip_rect(x, y, width, height) end + +--- +---Draw a rectangle. +--- +---@param x number +---@param y number +---@param width number +---@param height number +---@param color RendererColor +function renderer.draw_rect(x, y, width, height, color) end + +--- +---Draw text. +--- +---@param font renderer.font +---@param text string +---@param x number +---@param y number +---@param color RendererColor +---@param replace renderer.replacements +---@param color_replace RendererColor +function renderer.draw_text(font, text, x, y, color, replace, color_replace) end + +--- +---Draw text at subpixel level. +--- +---@param font renderer.font +---@param text string +---@param x number +---@param y number +---@param color RendererColor +---@param replace renderer.replacements +---@param color_replace RendererColor +function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end diff --git a/docs/api/system.lua b/docs/api/system.lua new file mode 100644 index 00000000..df0e711b --- /dev/null +++ b/docs/api/system.lua @@ -0,0 +1,234 @@ +---@meta + +--- +---Utilites for managing current window, files and more. +---@class system +system = {} + +---@alias FileInfoType +---|>'"file"' # It is a file. +---| '"dir"' # It is a directory. + +--- +---@class FileInfo +---@field public modified number A timestamp in seconds. +---@field public size number Size in bytes. +---@field public type FileInfoType Type of file +FileInfo = {} + +--- +---Core function used to retrieve the current event been triggered by SDL. +--- +---The following is a list of event types emitted by this function and +---the arguments for each of them if applicable. +--- +---Window events: +--- * "quit" +--- * "resized" -> width, height +--- * "exposed" +--- * "minimized" +--- * "maximized" +--- * "restored" +--- * "focuslost" +--- +---File events: +--- * "filedropped" -> filename, x, y +--- +---Keyboard events: +--- * "keypressed" -> key_name +--- * "keyreleased" -> key_name +--- * "textinput" -> text +--- +---Mouse events: +--- * "mousepressed" -> button_name, x, y, amount_of_clicks +--- * "mousereleased" -> button_name, x, y +--- * "mousemoved" -> x, y, relative_x, relative_y +--- * "mousewheel" -> y +--- +---@return string type +---@return any? arg1 +---@return any? arg2 +---@return any? arg3 +---@return any? arg4 +function system.poll_event() end + +--- +---Wait until an event is triggered. +--- +---@param timeout number Amount of seconds, also supports fractions +---of a second, eg: 0.01 +--- +---@return boolean status True on success or false if there was an error. +function system.wait_event(timeout) end + +--- +---Change the cursor type displayed on screen. +--- +---@param type string | "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'" +function system.set_cursor(type) end + +--- +---Change the window title. +--- +---@param title string +function system.set_window_title(title) end + +---@alias SystemWindowMode +---|>'"normal"' +---| '"minimized"' +---| '"maximized"' +---| '"fullscreen"' + +--- +---Change the window mode. +--- +---@param mode SystemWindowMode +function system.set_window_mode(mode) end + +--- +---Retrieve the current window mode. +--- +---@return SystemWindowMode mode +function system.get_window_mode() end + +--- +---Toggle between bordered and borderless. +--- +---@param bordered boolean +function system.set_window_bordered(bordered) end + +--- +---When then window is run borderless (without system decorations), this +---function allows to set the size of the different regions that allow +---for custom window management. +--- +---@param title_height number +---@param controls_width number This is for minimize, maximize, close, etc... +---@param resize_border number The amount of pixels reserved for resizing +function system.set_window_hit_test(title_height, controls_width, resize_border) end + +--- +---Get the size and coordinates of the window. +--- +---@return number width +---@return number height +---@return number x +---@return number y +function system.get_window_size() end + +--- +---Sets the size and coordinates of the window. +--- +---@param width number +---@param height number +---@param x number +---@param y number +function system.set_window_size(width, height, x, y) end + +--- +---Check if the window currently has focus. +--- +---@return boolean +function system.window_has_focus() end + +--- +---Opens a message box to display an error message. +--- +---@param title string +---@param message string +function system.show_fatal_error(title, message) end + +--- +---Change the current directory path which affects relative file operations. +---This function raises an error if the path doesn't exists. +--- +---@param path string +function system.chdir(path) end + +--- +---Create a new directory, note that this function doesn't recursively +---creates the directories on the given path. +--- +---@param directory_path string +--- +---@return boolean created True on success or false on failure. +function system.mkdir(directory_path) end + +--- +---Gets a list of files and directories for a given path. +--- +---@param path string +--- +---@return table|nil list List of directories or nil if empty or error. +---@return string? message Error message in case of error. +function system.list_dir(path) end + +--- +---Converts a relative path from current directory to the absolute one. +--- +---@param path string +--- +---@return string +function system.absolute_path(path) end + +--- +---Get details about a given file or path. +--- +---@param path string Can be a file or a directory path +--- +---@return FileInfo|nil info Path details or nil if empty or error. +---@return string? message Error message in case of error. +function system.get_file_info(path) end + +--- +---Retrieve the text currently stored on the clipboard. +--- +---@return string +function system.get_clipboard() end + +--- +---Set the content of the clipboard. +--- +---@param text string +function system.set_clipboard(text) end + +--- +---Get amount of iterations since the application was launched +---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency() +--- +---@return number +function system.get_time() end + +--- +---Sleep for the given amount of seconds. +--- +---@param seconds number Also supports fractions of a second, eg: 0.01 +function system.sleep(seconds) end + +--- +---Similar to os.execute() but does not return the exit status of the +---executed command and executes the process in a non blocking way by +---forking it to the background. +--- +---@param command string The command to execute. +function system.exec(command) end + +--- +---Generates a matching score depending on how well the value of the +---given needle compares to that of the value in the haystack. +--- +---@param haystack string +---@param needle string +---@param file boolean Reverse the algorithm to prioritize the end +---of the haystack, eg: with a haystack "/my/path/to/file" and a needle +---"file", will get better score than with this option not set to true. +--- +---@return integer score +function system.fuzzy_match(haystack, needle, file) end + +--- +---Change the opacity (also known as transparency) of the window. +--- +---@param opacity number A value from 0.0 to 1.0, the lower the value +---the less visible the window will be. +function system.set_window_opacity(opacity) end From 18eee34aa9f7aa5b3018d96c2bfdc843f263a635 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Fri, 25 Jun 2021 03:01:48 -0400 Subject: [PATCH 002/180] Added README to docs directory. --- docs/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..7191e3f5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# Interface Files + +This directory holds the documentation for the Lua C API that +is hidden in the C source files of Lite. The idea of these files +is to serve you as a quick reference about the functionality +that is not written in Lua it self. Please note that they +don't have any real code, just metadata or annotations. + +Also, these interfaces are using +[EmmyLua annotation syntax](https://emmylua.github.io/annotation.html) +which is supported by LSP servers like the +[Sumneko Lua LSP](https://github.com/sumneko/lua-language-server). +This means that you can get nice code autocompletion and descriptions +of Lite core libraries and symbols when developing plugins or adding +any options to your **User Module File** (init.lua). + +## The Base Core + +Most of the code that is written in Lua for Lite is powered by the exposed +C API in the four namespaces that follow: + +* [system](api/system.lua) +* [renderer](api/renderer.lua) +* [regex](api/regex.lua) +* [process](api/process.lua) + +Finally, all global variables are documented in the file named +[globals.lua](api/globals.lua). From 68459a9199f8fffec316bcd28d1c74d86d25269a Mon Sep 17 00:00:00 2001 From: jgmdev Date: Thu, 24 Jun 2021 14:07:50 -0400 Subject: [PATCH 003/180] Added context menu to treeview. --- data/core/contextmenu.lua | 218 ++++++++++++++++++++++++++++++++++ data/plugins/contextmenu.lua | 219 +---------------------------------- data/plugins/treeview.lua | 154 ++++++++++++++++++++++-- 3 files changed, 368 insertions(+), 223 deletions(-) create mode 100644 data/core/contextmenu.lua diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua new file mode 100644 index 00000000..36247597 --- /dev/null +++ b/data/core/contextmenu.lua @@ -0,0 +1,218 @@ +local core = require "core" +local common = require "core.common" +local command = require "core.command" +local config = require "core.config" +local keymap = require "core.keymap" +local style = require "core.style" +local Object = require "core.object" + +local border_width = 1 +local divider_width = 1 +local DIVIDER = {} + +local ContextMenu = Object:extend() + +ContextMenu.DIVIDER = DIVIDER + +function ContextMenu:new() + self.itemset = {} + self.show_context_menu = false + self.selected = -1 + self.height = 0 + self.position = { x = 0, y = 0 } +end + +local function get_item_size(item) + local lw, lh + if item == DIVIDER then + lw = 0 + lh = divider_width + else + lw = style.font:get_width(item.text) + if item.info then + lw = lw + style.padding.x + style.font:get_width(item.info) + end + lh = style.font:get_height() + style.padding.y + end + return lw, lh +end + +function ContextMenu:register(predicate, items) + if type(predicate) == "string" then + predicate = require(predicate) + end + if type(predicate) == "table" then + local class = predicate + predicate = function() return core.active_view:is(class) end + end + + 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] + end + local lw, lh = get_item_size(item) + width = math.max(width, lw) + height = height + lh + end + width = width + style.padding.x * 2 + items.width, items.height = width, height + table.insert(self.itemset, { predicate = predicate, items = items }) +end + +function ContextMenu:show(x, y) + self.items = nil + local items_list = { width = 0, height = 0 } + 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 + for _, subitems in ipairs(items.items) do + table.insert(items_list, subitems) + end + end + end + + if #items_list > 0 then + self.items = items_list + local w, h = self.items.width, self.items.height + + -- by default the box is opened on the right and below + if x + w >= core.root_view.size.x then + x = x - w + end + if y + h >= core.root_view.size.y then + y = y - h + end + + self.position.x, self.position.y = x, y + self.show_context_menu = true + return true + end + return false +end + +function ContextMenu:hide() + self.show_context_menu = false + self.items = nil + self.selected = -1 + self.height = 0 +end + +function ContextMenu:each_item() + local x, y, w = self.position.x, self.position.y, self.items.width + local oy = y + return coroutine.wrap(function() + for i, item in ipairs(self.items) do + local _, lh = get_item_size(item) + if y - oy > self.height then break end + coroutine.yield(i, item, x, y, w, lh) + y = y + lh + end + end) +end + +function ContextMenu:on_mouse_moved(px, py) + if not self.show_context_menu then return end + + self.selected = -1 + for i, item, x, y, w, h in self:each_item() do + if px > x and px <= x + w and py > y and py <= y + h then + self.selected = i + break + end + end + if self.selected >= 0 then + core.request_cursor("arrow") + end + return true +end + +function ContextMenu:on_selected(item) + if type(item.command) == "string" then + command.perform(item.command) + else + item.command() + end +end + +function ContextMenu:on_mouse_pressed(button, x, y, clicks) + local selected = (self.items or {})[self.selected] + local caught = false + + self:hide() + if button == "left" then + if selected then + self:on_selected(selected) + caught = true + end + end + + if button == "right" then + caught = self:show(x, y) + end + return caught +end + +-- copied from core.docview +function ContextMenu:move_towards(t, k, dest, rate) + if type(t) ~= "table" then + return self:move_towards(self, t, k, dest, rate) + end + local val = t[k] + if not config.transitions or math.abs(val - dest) < 0.5 then + t[k] = dest + else + rate = rate or 0.5 + if config.fps ~= 60 or config.animation_rate ~= 1 then + local dt = 60 / config.fps + rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) + end + t[k] = common.lerp(val, dest, rate) + end + if val ~= dest then + core.redraw = true + end +end + +function ContextMenu:update() + if self.show_context_menu then + self:move_towards("height", self.items.height) + end +end + +function ContextMenu:draw() + if not self.show_context_menu then return end + core.root_view:defer_draw(self.draw_context_menu, self) +end + +function ContextMenu:draw_context_menu() + if not self.items then return end + local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height + + renderer.draw_rect( + bx - border_width, + by - border_width, + bw + (border_width * 2), + bh + (border_width * 2), + style.divider + ) + renderer.draw_rect(bx, by, bw, bh, style.background3) + + for i, item, x, y, w, h in self:each_item() do + if item == DIVIDER then + renderer.draw_rect(x, y, w, h, style.caret) + else + if i == self.selected then + renderer.draw_rect(x, y, w, h, style.selection) + end + + common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h) + if item.info then + common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h) + end + end + end +end + +return ContextMenu diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 4de46080..c0fe49fd 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,223 +1,10 @@ -- mod-version:1 -- lite-xl 1.16 local core = require "core" -local common = require "core.common" -local config = require "core.config" local command = require "core.command" local keymap = require "core.keymap" -local style = require "core.style" -local Object = require "core.object" +local ContextMenu = require "core.contextmenu" local RootView = require "core.rootview" -local border_width = 1 -local divider_width = 1 -local DIVIDER = {} - -local ContextMenu = Object:extend() - -ContextMenu.DIVIDER = DIVIDER - -function ContextMenu:new() - self.itemset = {} - self.show_context_menu = false - self.selected = -1 - self.height = 0 - self.position = { x = 0, y = 0 } -end - -local function get_item_size(item) - local lw, lh - if item == DIVIDER then - lw = 0 - lh = divider_width - else - lw = style.font:get_width(item.text) - if item.info then - lw = lw + style.padding.x + style.font:get_width(item.info) - end - lh = style.font:get_height() + style.padding.y - end - return lw, lh -end - -function ContextMenu:register(predicate, items) - if type(predicate) == "string" then - predicate = require(predicate) - end - if type(predicate) == "table" then - local class = predicate - predicate = function() return core.active_view:is(class) end - end - - 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] - end - local lw, lh = get_item_size(item) - width = math.max(width, lw) - height = height + lh - end - width = width + style.padding.x * 2 - items.width, items.height = width, height - table.insert(self.itemset, { predicate = predicate, items = items }) -end - -function ContextMenu:show(x, y) - self.items = nil - local items_list = { width = 0, height = 0 } - 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 - for _, subitems in ipairs(items.items) do - table.insert(items_list, subitems) - end - end - end - - if #items_list > 0 then - self.items = items_list - local w, h = self.items.width, self.items.height - - -- by default the box is opened on the right and below - if x + w >= core.root_view.size.x then - x = x - w - end - if y + h >= core.root_view.size.y then - y = y - h - end - - self.position.x, self.position.y = x, y - self.show_context_menu = true - return true - end - return false -end - -function ContextMenu:hide() - self.show_context_menu = false - self.items = nil - self.selected = -1 - self.height = 0 -end - -function ContextMenu:each_item() - local x, y, w = self.position.x, self.position.y, self.items.width - local oy = y - return coroutine.wrap(function() - for i, item in ipairs(self.items) do - local _, lh = get_item_size(item) - if y - oy > self.height then break end - coroutine.yield(i, item, x, y, w, lh) - y = y + lh - end - end) -end - -function ContextMenu:on_mouse_moved(px, py) - if not self.show_context_menu then return end - - self.selected = -1 - for i, item, x, y, w, h in self:each_item() do - if px > x and px <= x + w and py > y and py <= y + h then - self.selected = i - break - end - end - if self.selected >= 0 then - core.request_cursor("arrow") - end - return true -end - -function ContextMenu:on_selected(item) - if type(item.command) == "string" then - command.perform(item.command) - else - item.command() - end -end - -function ContextMenu:on_mouse_pressed(button, x, y, clicks) - local selected = (self.items or {})[self.selected] - local caught = false - - self:hide() - if button == "left" then - if selected then - self:on_selected(selected) - caught = true - end - end - - if button == "right" then - caught = self:show(x, y) - end - return caught -end - --- copied from core.docview -function ContextMenu:move_towards(t, k, dest, rate) - if type(t) ~= "table" then - return self:move_towards(self, t, k, dest, rate) - end - local val = t[k] - if not config.transitions or math.abs(val - dest) < 0.5 then - t[k] = dest - else - rate = rate or 0.5 - if config.fps ~= 60 or config.animation_rate ~= 1 then - local dt = 60 / config.fps - rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) - end - t[k] = common.lerp(val, dest, rate) - end - if val ~= dest then - core.redraw = true - end -end - -function ContextMenu:update() - if self.show_context_menu then - self:move_towards("height", self.items.height) - end -end - -function ContextMenu:draw() - if not self.show_context_menu then return end - core.root_view:defer_draw(self.draw_context_menu, self) -end - -function ContextMenu:draw_context_menu() - if not self.items then return end - local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height - - renderer.draw_rect( - bx - border_width, - by - border_width, - bw + (border_width * 2), - bh + (border_width * 2), - style.divider - ) - renderer.draw_rect(bx, by, bw, bh, style.background3) - - for i, item, x, y, w, h in self:each_item() do - if item == DIVIDER then - renderer.draw_rect(x, y, w, h, style.caret) - else - if i == self.selected then - renderer.draw_rect(x, y, w, h, style.selection) - end - - common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h) - if item.info then - common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h) - end - end - end -end - - local menu = ContextMenu() local on_view_mouse_pressed = RootView.on_view_mouse_pressed local on_mouse_moved = RootView.on_mouse_moved @@ -260,10 +47,10 @@ if require("plugins.scale") then { text = "Font +", command = "scale:increase" }, { text = "Font -", command = "scale:decrease" }, { text = "Font Reset", command = "scale:reset" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find", command = "find-replace:find" }, { text = "Replace", command = "find-replace:replace" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find Pattern", command = "find-replace:find-pattern" }, { text = "Replace Pattern", command = "find-replace:replace-pattern" }, }) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 8214bda4..90145260 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -6,9 +6,12 @@ local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" local View = require "core.view" +local ContextMenu = require "core.contextmenu" +local RootView = require "core.rootview" + local default_treeview_size = 200 * SCALE -local tooltip_offset = style.font:get_height("A") +local tooltip_offset = style.font:get_height() local tooltip_border = 1 local tooltip_delay = 0.5 local tooltip_alpha = 255 @@ -173,13 +176,13 @@ end function TreeView:on_mouse_moved(px, py, ...) TreeView.super.on_mouse_moved(self, px, py, ...) if self.dragging_scrollbar then return end - + local item_changed, tooltip_changed for item, x,y,w,h in self:each_item() do if px > x and py > y and px <= x + w and py <= y + h then item_changed = true self.hovered_item = item - + x,y,w,h = self:get_text_bounding_box(item, x,y,w,h) if px > x and py > y and px <= x + w and py <= y + h then tooltip_changed = true @@ -210,7 +213,7 @@ end function TreeView:on_mouse_pressed(button, x, y, clicks) local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then + if caught or button ~= "left" then return end local hovered_item = self.hovered_item @@ -256,7 +259,7 @@ function TreeView:update() else self:move_towards(self.size, "x", dest) end - + local duration = system.get_time() - self.tooltip.begin if self.hovered_item and self.tooltip.x and duration > tooltip_delay then self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate) @@ -353,9 +356,10 @@ local treeview_node = node:split("left", view, {x = true}, true) -- plugin to be independent of each other. In addition it is not the -- plugin module that plug itself in the active node but it is plugged here -- in the treeview node. +local toolbar_view = nil local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview") if config.toolbarview ~= false and toolbar_plugin then - local toolbar_view = ToolbarView() + toolbar_view = ToolbarView() treeview_node:split("down", toolbar_view, {y = true}) local min_toolbar_width = toolbar_view:get_min_width() view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width)) @@ -366,12 +370,148 @@ if config.toolbarview ~= false and toolbar_plugin then }) end +-- Add a context menu to the treeview +local menu = ContextMenu() --- register commands and keymap +local on_view_mouse_pressed = RootView.on_view_mouse_pressed +local on_mouse_moved = RootView.on_mouse_moved +local root_view_update = RootView.update +local root_view_draw = RootView.draw + +function RootView:on_mouse_moved(...) + if menu:on_mouse_moved(...) then return end + on_mouse_moved(self, ...) +end + +function RootView.on_view_mouse_pressed(button, x, y, clicks) + -- We give the priority to the menu to process mouse pressed events. + if button == "right" then + view.tooltip.alpha = 0 + view.tooltip.x, view.tooltip.y = nil, nil + end + local handled = menu:on_mouse_pressed(button, x, y, clicks) + return handled or on_view_mouse_pressed(button, x, y, clicks) +end + +function RootView:update(...) + root_view_update(self, ...) + menu:update() +end + +function RootView:draw(...) + root_view_draw(self, ...) + menu:draw() +end + +local function is_project_folder(path) + return common.basename(core.project_dir) == path +end + +menu:register(function() return view.hovered_item end, { + { text = "Open in System", command = "treeview:open-in-system" }, + ContextMenu.DIVIDER +}) + +menu:register( + function() + return view.hovered_item + and not is_project_folder(view.hovered_item.filename) + end, + { + { text = "Rename", command = "treeview:rename" }, + { text = "Delete", command = "treeview:delete" }, + } +) + +menu:register( + function() + return view.hovered_item and view.hovered_item.type == "dir" + end, + { + { text = "New File", command = "treeview:new-file" }, + { text = "New Folder", command = "treeview:new-folder" }, + } +) + +-- Register the TreeView commands and keymap command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible end, + + ["treeview:rename"] = function() + local old_filename = view.hovered_item.filename + core.command_view:set_text(old_filename) + core.command_view:enter("Rename", function(filename) + os.rename(old_filename, filename) + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + end, common.path_suggest) + end, + + ["treeview:new-file"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + end + core.command_view:enter("Filename", function(filename) + local doc_filename = core.project_dir .. PATHSEP .. filename + local file = io.open(doc_filename, "a+") + file:write("") + file:close() + core.root_view:open_doc(core.open_doc(doc_filename)) + core.log("Created %s", doc_filename) + end, common.path_suggest) + end, + + ["treeview:new-folder"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + end + core.command_view:enter("Folder Name", function(filename) + local dir_path = core.project_dir .. PATHSEP .. filename + common.mkdirp(dir_path) + core.log("Created %s", dir_path) + end, common.path_suggest) + end, + + ["treeview:delete"] = function() + local filename = view.hovered_item.abs_filename + local file_info = system.get_file_info(filename) + if file_info.type == "dir" then + local deleted, error, path = common.rm(filename, true) + if not deleted then + core.error("Error: %s - \"%s\" ", error, path) + return + end + else + local removed, error = os.remove(filename) + if not removed then + core.error("Error: %s - \"%s\"", error, filename) + return + end + end + core.log("Deleted \"%s\"", filename) + end, + + ["treeview:open-in-system"] = function() + local hovered_item = view.hovered_item + + if PLATFORM == "Windows" then + system.exec("start " .. hovered_item.abs_filename) + elseif string.find(PLATFORM, "Mac") then + system.exec(string.format("open %q", hovered_item.abs_filename)) + elseif PLATFORM == "Linux" then + system.exec(string.format("xdg-open %q", hovered_item.abs_filename)) + end + end, }) keymap.add { ["ctrl+\\"] = "treeview:toggle" } + +-- Return the treeview with toolbar and contextmenu to allow +-- user or plugin modifications +view.toolbar = toolbar_view +view.contextmenu = menu + +return view From 900e9d1422b9b48dcdfdf54b126f1d85f794aad8 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Wed, 30 Jun 2021 03:02:01 -0400 Subject: [PATCH 004/180] Namespaced aliases, virtual classes and added missing returns. --- docs/api/process.lua | 171 +++++++++++++++++++++++++++++------------- docs/api/regex.lua | 4 +- docs/api/renderer.lua | 24 +++--- docs/api/system.lua | 16 ++-- 4 files changed, 144 insertions(+), 71 deletions(-) diff --git a/docs/api/process.lua b/docs/api/process.lua index 0374c0c6..abba67ae 100644 --- a/docs/api/process.lua +++ b/docs/api/process.lua @@ -29,7 +29,13 @@ process.ERROR_TIMEDOUT = -3 ---its value is platform dependent, so the value declared on this ---interface does not represents the real one. ---@type integer -process.ERROR_INVALID = -4 +process.ERROR_INVAL = -4 + +---Error triggered when no memory is available to allocate the process, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_NOMEM = -5 ---Used for the process:close_stream() method to close stdin. ---@type integer @@ -51,111 +57,174 @@ process.WAIT_INFINITE = -1 ---@type integer process.WAIT_DEADLINE = -2 +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_DEFAULT = 0 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_PIPE = 1 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_PARENT = 2 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_DISCARD = 3 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_STDOUT = 4 + +---@alias process.errortype +---|>'process.ERROR_PIPE' +---| 'process.ERROR_WOULDBLOCK' +---| 'process.ERROR_TIMEDOUT' +---| 'process.ERROR_INVAL' +---| 'process.ERROR_NOMEM' + +---@alias process.streamtype +---|>'process.STREAM_STDIN' +---| 'process.STREAM_STDOUT' +---| 'process.STREAM_STDERR' + +---@alias process.waittype +---|>'process.WAIT_INFINITE' +---| 'process.WAIT_DEADLINE' + +---@alias process.redirecttype +---|>'process.REDIRECT_DEFAULT' +---| 'process.REDIRECT_PIPE' +---| 'process.REDIRECT_PARENT' +---| 'process.REDIRECT_DISCARD' +---| 'process.REDIRECT_STDOUT' + --- ----Create a new process object +--- Options that can be passed to process.start() +---@class process.options +---@field public timeout number +---@field public cwd string +---@field public stdin process.redirecttype +---@field public stdout process.redirecttype +---@field public stderr process.redirecttype +---@field public env table +process.options = {} + --- ----@return process -function process.new() end +---Create and start a new process +--- +---@param command_and_params table First index is the command to execute +---and subsequente elements are parameters for the command. +---@param options process.options +--- +---@return process | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:start(command_and_params, options) end --- ---Translates an error code into a useful text message --- ---@param code integer --- ----@return string +---@return string | nil function process.strerror(code) end ---- ----Start a process ---- ----@param command_and_params table First index is the command to execute ----and subsequente elements are parameters for the command. ----@param working_directory? string Path where the command will be launched. ----@param deadline? integer Maximum time in milliseconds the ----process is allowed to run on a process:wait(process.WAIT_DEADLINE) call. ---- ----@return integer|boolean status Negative integer error code if could ----not start or true on success -function process:start(command_and_params, working_directory, deadline) end - --- ---Get the process id. --- ---@return integer id Process id or 0 if not running. function process:pid() end +--- +---Read from the given stream type, if the process fails with a ERROR_PIPE it is +---automatically destroyed returning nil along error message and code. +--- +---@param stream process.streamtype +---@param len? integer Amount of bytes to read, defaults to 2048. +--- +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read(stream, len) end + --- ---Read from stdout, if the process fails with a ERROR_PIPE it is ----automatically destroyed, so checking process status with the ----process:running() method would be advised. +---automatically destroyed returning nil along error message and code. --- ----@param len? integer Amount of bytes to read. ----@param tries? integer Retry reading the given amount of times ----if nothing was read. +---@param len? integer Amount of bytes to read, defaults to 2048. --- ----@return integer|nil bytes Amount of bytes read or nil if nothing was read. -function process:read(len, tries) end +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read_stdout(len) end --- ---Read from stderr, if the process fails with a ERROR_PIPE it is ----automatically destroyed, so checking process status with the ----process:running() method would be advised. +---automatically destroyed returning nil along error message and code. --- ----@param len? integer Amount of bytes to read. ----@param tries? integer Retry reading the given amount of times ----if nothing was read. +---@param len? integer Amount of bytes to read, defaults to 2048. --- ----@return integer|nil bytes Amount of bytes read or nil if nothing was read. -function process:read_errors(len, tries) end +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read_stderr(len) end --- ---Write to the stdin, if the process fails with a ERROR_PIPE it is ----automatically destroyed, so checking process status with the ----process:running() method would be advised. +---automatically destroyed returning nil along error message and code. --- ---@param data string --- ----@return integer bytes The amount of bytes written or negative integer ----error code: process.ERROR_PIPE, process.ERROR_WOULDBLOCK +---@return integer | nil bytes The amount of bytes written or nil if error +---@return string errmsg +---@return process.errortype | integer errcode function process:write(data) end --- ---Allows you to close a stream pipe that you will not be using. --- ----@param stream integer Could be one of the following: ----process.STREAM_STDIN, process.STREAM_STDOUT, process.STREAM_STDERR +---@param stream process.streamtype --- ----@return integer status Negative error code process.ERROR_INVALID if ----process is not running or stream is already closed. +---@return integer | nil +---@return string errmsg +---@return process.errortype | integer errcode function process:close_stream(stream) end --- ---Wait the specified amount of time for the process to exit. --- ----@param timeout integer Time to wait in milliseconds, if 0, the function ----will only check if process is running without waiting, also the timeout ----can be set to: ---- * process.WAIT_INFINITE - will wait until the process ends ---- * process.WAIT_DEADLINE - will wait until the deadline declared on start() +---@param timeout integer | process.waittype Time to wait in milliseconds, +---if 0, the function will only check if process is running without waiting. --- ----@return integer exit_status The process exit status or negative integer ----error code like process.ERROR_TIMEDOUT +---@return integer | nil exit_status The process exit status or nil on error +---@return string errmsg +---@return process.errortype | integer errcode function process:wait(timeout) end --- ---Sends SIGTERM to the process --- ----@return boolean|integer status Returns true on success or a ----negative integer error code like process.ERROR_INVALID +---@return boolean | nil +---@return string errmsg +---@return process.errortype | integer errcode function process:terminate() end --- ---Sends SIGKILL to the process --- ----@return boolean|integer status Returns true on success or a ----negative integer error code like process.ERROR_INVALID +---@return boolean | nil +---@return string errmsg +---@return process.errortype | integer errcode function process:kill() end +--- +---Get the exit code of the process or nil if still running. +--- +---@return number | nil +function process:returncode() end + --- ---Check if the process is running --- diff --git a/docs/api/regex.lua b/docs/api/regex.lua index d1d7346c..02d8c796 100644 --- a/docs/api/regex.lua +++ b/docs/api/regex.lua @@ -30,7 +30,7 @@ regex.NOTEMPTY = 0x00000004 ---@type integer regex.NOTEMPTY_ATSTART = 0x00000008 ----@alias RegexModifiers +---@alias regex.modifiers ---|>'"i"' # Case insesitive matching ---| '"m"' # Multiline matching ---| '"s"' # Match all characters with dot (.) metacharacter even new lines @@ -39,7 +39,7 @@ regex.NOTEMPTY_ATSTART = 0x00000008 ---Compiles a regular expression pattern that can be used to search in strings. --- ---@param pattern string ----@param options? RegexModifiers A string of one or more pattern modifiers. +---@param options? regex.modifiers A string of one or more pattern modifiers. --- ---@return regex|string regex Ready to use regular expression object or error ---message if compiling the pattern failed. diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index 3a1c6036..bb622131 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -7,19 +7,19 @@ renderer = {} --- ---Represents a color used by the rendering functions. ----@class RendererColor +---@class renderer.color ---@field public r number Red ---@field public g number Green ---@field public b number Blue ---@field public a number Alpha -RendererColor = {} +renderer.color = {} --- ---Represent options that affect a font's rendering. ----@class RendererFontOptions +---@class renderer.fontoptions ---@field public antialiasing "'grayscale'" | "'subpixel'" ---@field public hinting "'slight'" | "'none'" | '"full"' -RendererFontOptions = {} +renderer.fontoptions = {} --- ---@class renderer.font @@ -30,7 +30,7 @@ renderer.font = {} --- ---@param path string ---@param size number ----@param options RendererFontOptions +---@param options renderer.fontoptions --- ---@return renderer.font function renderer.font.load(path, size, options) end @@ -149,7 +149,7 @@ function renderer.set_clip_rect(x, y, width, height) end ---@param y number ---@param width number ---@param height number ----@param color RendererColor +---@param color renderer.color function renderer.draw_rect(x, y, width, height, color) end --- @@ -159,9 +159,11 @@ function renderer.draw_rect(x, y, width, height, color) end ---@param text string ---@param x number ---@param y number ----@param color RendererColor +---@param color renderer.color ---@param replace renderer.replacements ----@param color_replace RendererColor +---@param color_replace renderer.color +--- +---@return number x_subpixel function renderer.draw_text(font, text, x, y, color, replace, color_replace) end --- @@ -171,7 +173,9 @@ function renderer.draw_text(font, text, x, y, color, replace, color_replace) end ---@param text string ---@param x number ---@param y number ----@param color RendererColor +---@param color renderer.color ---@param replace renderer.replacements ----@param color_replace RendererColor +---@param color_replace renderer.color +--- +---@return number x_subpixel function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end diff --git a/docs/api/system.lua b/docs/api/system.lua index df0e711b..a655099b 100644 --- a/docs/api/system.lua +++ b/docs/api/system.lua @@ -5,16 +5,16 @@ ---@class system system = {} ----@alias FileInfoType +---@alias system.fileinfotype ---|>'"file"' # It is a file. ---| '"dir"' # It is a directory. --- ----@class FileInfo +---@class system.fileinfo ---@field public modified number A timestamp in seconds. ---@field public size number Size in bytes. ----@field public type FileInfoType Type of file -FileInfo = {} +---@field public type system.fileinfotype Type of file +system.fileinfo = {} --- ---Core function used to retrieve the current event been triggered by SDL. @@ -73,7 +73,7 @@ function system.set_cursor(type) end ---@param title string function system.set_window_title(title) end ----@alias SystemWindowMode +---@alias system.windowmode ---|>'"normal"' ---| '"minimized"' ---| '"maximized"' @@ -82,13 +82,13 @@ function system.set_window_title(title) end --- ---Change the window mode. --- ----@param mode SystemWindowMode +---@param mode system.windowmode function system.set_window_mode(mode) end --- ---Retrieve the current window mode. --- ----@return SystemWindowMode mode +---@return system.windowmode mode function system.get_window_mode() end --- @@ -176,7 +176,7 @@ function system.absolute_path(path) end --- ---@param path string Can be a file or a directory path --- ----@return FileInfo|nil info Path details or nil if empty or error. +---@return system.fileinfo|nil info Path details or nil if empty or error. ---@return string? message Error message in case of error. function system.get_file_info(path) end From a4d5622edae6c3689d3e2e62201b84751112fcd1 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Sun, 11 Jul 2021 23:03:33 -0400 Subject: [PATCH 005/180] Make use of core.reschedule_project_scan() --- data/plugins/treeview.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 90145260..87a9db1c 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -444,6 +444,7 @@ command.add(nil, { core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) os.rename(old_filename, filename) + core.reschedule_project_scan() core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) end, common.path_suggest) end, @@ -459,6 +460,7 @@ command.add(nil, { 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, @@ -471,6 +473,7 @@ command.add(nil, { 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, @@ -491,6 +494,7 @@ command.add(nil, { return end end + core.reschedule_project_scan() core.log("Deleted \"%s\"", filename) end, From afa0c175e85ae183c6537fd23cd9e91cd45a1789 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Mon, 12 Jul 2021 11:33:14 -0400 Subject: [PATCH 006/180] Added delete confirmation using NagView. --- data/plugins/treeview.lua | 47 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 87a9db1c..0b9304aa 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -480,22 +480,41 @@ command.add(nil, { ["treeview:delete"] = function() local filename = view.hovered_item.abs_filename + local relfilename = view.hovered_item.filename local file_info = system.get_file_info(filename) - if file_info.type == "dir" then - local deleted, error, path = common.rm(filename, true) - if not deleted then - core.error("Error: %s - \"%s\" ", error, path) - return + local file_type = file_info.type == "dir" and "Directory" or "File" + -- Ask before deleting + local opt = { + { font = style.font, text = "Yes", default_yes = true }, + { font = style.font, text = "No" , default_no = true } + } + core.nag_view:show( + string.format("Delete %s", file_type), + string.format( + "Are you sure you want to delete the %s?\n%s: %s", + file_type:lower(), file_type, relfilename + ), + opt, + function(item) + if item.text == "Yes" then + if file_info.type == "dir" then + local deleted, error, path = common.rm(filename, true) + if not deleted then + core.error("Error: %s - \"%s\" ", error, path) + return + end + else + local removed, error = os.remove(filename) + if not removed then + core.error("Error: %s - \"%s\"", error, filename) + return + end + end + core.reschedule_project_scan() + core.log("Deleted \"%s\"", filename) + end end - else - local removed, error = os.remove(filename) - if not removed then - core.error("Error: %s - \"%s\"", error, filename) - return - end - end - core.reschedule_project_scan() - core.log("Deleted \"%s\"", filename) + ) end, ["treeview:open-in-system"] = function() From 0ed707c68f48c8f4709c035ad44ec597c8851ed8 Mon Sep 17 00:00:00 2001 From: redtide Date: Wed, 23 Jun 2021 16:43:58 +0200 Subject: [PATCH 007/180] InnoSetup build scripts --- .github/workflows/build.yml | 7 +- meson.build | 7 ++ meson_options.txt | 3 +- scripts/innosetup/innosetup.iss.in | 83 ++++++++++++++++++++++ scripts/innosetup/litexl-55px.bmp | Bin 0 -> 12238 bytes scripts/innosetup/wizard-modern-image.bmp | Bin 0 -> 52574 bytes 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 scripts/innosetup/innosetup.iss.in create mode 100644 scripts/innosetup/litexl-55px.bmp create mode 100644 scripts/innosetup/wizard-modern-image.bmp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4bf3358..0ef07952 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,12 @@ name: CI on: - workflow_dispatch: + push: + branches: + - '*' + pull_request: + branches: + - '*' jobs: build-linux: diff --git a/meson.build b/meson.build index 1f1ff0d2..9240f68a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,9 @@ project('lite-xl', 'c', 'cpp', default_options : ['c_std=gnu11', 'cpp_std=c++03']) +version = get_option('version') +conf_data = configuration_data() +conf_data.set('PROJECT_VERSION', version) + if host_machine.system() == 'darwin' add_languages('objc') endif @@ -56,6 +60,9 @@ lite_rc = [] if host_machine.system() == 'windows' windows = import('windows') lite_rc += windows.compile_resources('resources/icons/icon.rc') + iss = configure_file(input : 'scripts/innosetup/innosetup.iss.in', + output : 'innosetup.iss', + configuration : conf_data) endif # On macos we need to use the SDL renderer to support retina displays diff --git a/meson_options.txt b/meson_options.txt index 73a542f2..a61bd359 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ +option('innosetup', type : 'boolean', value : false, description: 'Build Windows setup package') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') - +option('version', type : 'string', value : '0.0.0', description: 'Project version') diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in new file mode 100644 index 00000000..f085d271 --- /dev/null +++ b/scripts/innosetup/innosetup.iss.in @@ -0,0 +1,83 @@ +#define MyAppName "Lite XL" +#define MyAppVersion "@PROJECT_VERSION@" +#define MyAppPublisher "Lite XL Team" +#define MyAppURL "https://lite-xl.github.io" +#define MyAppExeName "lite-xl.exe" +#define BuildDir "@PROJECT_BUILD_DIR@" +#define SourceDir "." + +; Use /dArch option to create a setup for a different architecture, e.g.: +; iscc /dArch=x86 innosetup.iss +#ifndef Arch +#define Arch "x64" +#endif + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; To generate a new GUID, click Tools | Generate GUID inside the InnoSetup IDE. +AppId={{06761240-D97C-43DE-B9ED-C15F765A2D65} + +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} + +#if Arch=="x64" +ArchitecturesAllowed=x64 +ArchitecturesInstallIn64BitMode={#Arch} +#endif + +AllowNoIcons=yes +Compression=lzma +SolidCompression=yes +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppPublisher} +UninstallFilesDir={app} + +; Uncomment the following line to run in non administrative install mode +; (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog + +; The [Icons] "quicklaunchicon" entry uses {userappdata} +; but its [Tasks] entry has a proper IsAdminInstallMode Check. +UsedUserAreasWarning=no + +OutputDir=. +OutputBaseFilename=LiteXL-{#MyAppVersion}-{#Arch}-setup +;DisableDirPage=yes +;DisableProgramGroupPage=yes + +LicenseFile={#SourceDir}\LICENSE +SetupIconFile={#SourceDir}\icon.ico +WizardImageFile="wizard-modern-image.bmp" +WizardSmallImageFile="litexl-55px.bmp" + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode +Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked + +[Files] +Source: "{#BuildDir}\lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#BuildDir}\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not IsTaskSelected('portablemode') +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not IsTaskSelected('portablemode') +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not IsTaskSelected('portablemode') + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Setup] +Uninstallable=not IsTaskSelected('portablemode') diff --git a/scripts/innosetup/litexl-55px.bmp b/scripts/innosetup/litexl-55px.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d3424a9b23f4d0cfbb4b8454674dc249eee48110 GIT binary patch literal 12238 zcmeI2-z!657{?DT{2TrOv#p)Dky5sA{8+$j^~4tMU9C^s&I8$~YNj9QZ$NfG%` zY-*HirO2-y?{^%%Tj$KIb9VN=y{CP5e!ZPNpYuHL&ppBLaf6oKX4&^NQ{#`Sv^uSZ z9f#Qc$F}OM*y+zkn)dy{IR#sLCX749?yF4-`aGUmzsEDlbj|d^L;?zS-!M%v)dCZH zradMRd8}*(kKr$?G9q6`9Kw$xGg#S<`t>O(o2c!Em9NxwIqRVEtMaRg0Tlx(1{{ik zra*wYIy$JQv(q-|KA)@p8jHnfKarqJDn$>OjIF|Za%)VuFgHi{x3{+P@bAYhwsGb! zC%M4r78~M_f5%Q7yTR6^>vWe+JI>bC-d^_lhW5}2Kk?5zKRKa**IRZr#0qbR+40rc z866!Ql%&MY4h==4W#>Yh{1@BW^0F&-+0kDVV!LGivg0@0dPX5O#GxH_HOE#-Z|Sz> zS^u%eX}0FkH41Nhk{;}niVgk(S9p_zVjGTxXnSRZcGt&DHM6w!%Ow&UxSp=&=`Foz zs?&=k4J2Q){sNasZ17hhuEE1+>QBB2$%Z&IV}rlI6*+v7ijC)zi7lT?7B(|38Q842 zL}x3+B{CcOW%OTY=dIKme;2L~;Kxx$zg*0|(#DgCcsyr(Ym9q~VaYF3No-gL;OEPw z`paN+gAMDwnCI}q8cP292IgO|uT=TH{+t;2&pb?(l`=;|#e+lt3#s~|>Whj26$2F# l12X5~$(%ol{7@PD=t+Nly|nqtTs}7ZCvqMv+Krq%^DkTw(U$-K literal 0 HcmV?d00001 diff --git a/scripts/innosetup/wizard-modern-image.bmp b/scripts/innosetup/wizard-modern-image.bmp new file mode 100644 index 0000000000000000000000000000000000000000..cf844e093a4cc7aceee0be44353817397825e35b GIT binary patch literal 52574 zcmdtL37Aynwe^3zh#&|ugOEgxnkbT(M5BquIK`NcK?M^v62U3RU>uMcgviuQGfE2$ z-AF?NBA_79$P6;ig9ym1(v2tzn&IYp75jd_wfB3fst3TB{Ga=L|9z@YRS#6HT5GTU z?)N?C6t^~OwI#B+_UtISoJW)Sy8f&vIxDJZKfioK6xFEgU-pBr`=9^$pD6puC!=58 zdTaEbfB$1+E&71y8^jz02(K~N#k2bAY9ZelGDw>!% zB3eFgZZv*aMs#nB=F#dUi=r75UWyjI{#x|mk-}*ErVUZSp##yz6-%Qp{_*$dz@FXF zh9|FyUcCS6sA%BNqEAQM92JfIb+ob1t{8e@(RS<=diNlYbQ*e62~eYf6*o(H1|B-d*}!^wE~l z(I;M@6Ed>+B^HcsAy-m=;PJTMn!wmqk>hvqQbTPqYpL=i9VQC zC)%G|J6e}nhv)k?|Mt>o*Cd`l=Q8ep5N!tY!B?+|)@J`GI`HZ*qQmok6>Xh#Q?zTw zZ9Hxo9m;PS?S8FUw0BPP=-`4sMEl-&I6AbbO>}I{6Vdz2pNI}EeI`1*vU7A|%X86x zAIyrre1A&x<KbYR90qBS{JM2GXQ=Xrn1^Zp_#Saf@IXzuM%;o_!I z!Q$UWM_1ez6)yWjbZqq_(UBF8MaMV&Df(b_`{?-QPSIc9?i2lGS4Q;Z-ws4yeSR$3 zGp%m4HJ7i+S4O+QePHe{qCInOjr#rRis;aiyP{7v{xN!gUj1n2%uAxe|aDfpZ|IE!PXx}%U`=G`gqS>(fWmrqm^@h7#-VvQ?zl} z&!VED|Kh7C`r?Z(qM!ZjXVFi7@)L9aRcDM}{o?@O&%WyFum z_*uyxo$&?E{0C=xt4LZR7y(WfCH|Tm`2R8zuT}!m4^9&)fBHinC!XS8LV60OODgu` zpZvIZ{HRnE|2^N8e2(w`gzcL|nL_;VXKacTT9V}5dYVZ5Rm#8AQ}KjX{jAK_`Winp zC}r=$%lzO6KTEta=&rr>+S7&b$AA7a{$Y5MvR~nA{l#Cl#xwzUUbr z7iXL2Q092J#Co9=eAnH=7UG_JZn)tdHug8iKhtl8)c=Y8v6N3}9-h7UtDf<@lFTR7 z`EigeNEjE&IN45hAHU*N{_nz*q&#iOS3L9gfwW(R`#>RYUKBVR;>CEn=boD(?dF?r zZgF#q>#uKNaea&MphXLQLGTCn+~c$sne#p6i6qFo5a$n?ZQJ z#l6?x>ks0+BTe9Rn5nt>4Uol5r-+dvGKMwng0OL7UUw~9so;kp!k^neClgj)|-+4dQ_L_$R11X{weP9k}rSy z@BO_i3q21VAQDSsmsZfq0#^dAK~=(D--3U7kFqmJ?$7Q8SdwmP3 zXaqhg${zjQsp8ka|t_5Rr~jP~zigr>4y=uh2o`U;i#CaHror ze5&YlTc=J&tN288ZW2K*Z(%0avr{sHcW$K+pP5Ddg=7wic&kIiiZ}DTrm_fnRAiZ~ zx7~Kz!?&d(eCT0q$>0xedpIOOP#ut#mU8$#1~Oo@9`5U46w^WH@BIj&8bKl-_9Y|_uysW>9DsqES4`rdw~h|@st z&2xl`Z-S@zTlKuGF}!T;V;arTW@!4ZSZBS??=b7b4;zIsS&y`N>A5FHjA+H0Ylvq? zw0h#XCT;pY68^;g95RF8kGjE~NS7PH;iYlx8RQOsm{eM=p{1giYqN0@Vc#~-jd&(T zR@6|BzWRzgdsI~PVyowxJnlI4ZzQ=19;y&>YXJ97J*aJ^@3d0?z_tFKm+nxxt!&43 zCL0yEKYaV`O`d2K-~p*djT)6JXj9ZESL3UO4I375D=K>Ci6*z*&ZdY3m%-iMz)>~7 z>Z+fAZF7r9cz3&4S$xb5Z88Uy#Y~eYu5f_mDpamsz8X=xcBM+^o?EL{)v8s`uTE61 z3|2!~R5YSVU;d4A+39yG?5?-j8?|Megxa^>3Yn+b1o2k>0d?n&J8rlk7TPLK(3uzO z^u&t>utw$bH7~f}f|^7Eq@gsB0$QokRi%)cQlnAc(`zU&=oL_%miyQ8^!{uho zQG_AH9+$O~)+tI~eeKnZ&**H9cAe4K09L48t!@>I;wWp0$^=NmSwjihuvHTi9Dp*o z3i@@|@d638W3hkqqaUAk12;rFlN5QEq2QS5h>sBnkj0R#b5lhP`ht2p&eovT6kt4_IpCFRpZ?A zJRD|>()IS7ED5rDWkWfl@3VakWdfwBjoivF&BJ3Pr@sHu_kM)O7)Ir332jP4CC`a3 zo2by#s1y(;aRw@jfrP!{QLzSZ?~30i-0f;3x2tU;_=k%rToT* z>0(MN8NKMpE0wF9CvYhtoM*?$P%x7@)DnqEi6+j=qjuzuL(iK2$(syRdfOoVV6l`)eJvTCvVlSEmO zdf5shHFop#5Gk?m<3(ldTW(3w^b`;IBNcBT-ly@!JZ(agiw(asx$hua8 ze84%S+SMzTD{A;e0IA`|LB`I$<=Wq#N$t2HyH>NSxEqtwjjt&Bs+u4gusVsbZ)1=l zB~MRe_3Gt{itZ=LU2Fi2%XK+nW8@(Rm6%qUQ(weySx2N4G-cq`SC!-uKrTaeHsi>$zfanMt)zRh= zgOqO=ajd;KwY33Rp<&T;iHro}A9+Q@_B#IC-<~25;~xi0rzma3`(bFT=|T$@@Dic# zjml7hM6<(8Cle@$wQq|6S74Mw7$b8V4jqx-fPKL zo|dJ(c>j(W-R(_^iYmo43K;-Bcw85R>NBhZw=O(}TdqQaxDpR4G9;#mZG)iJC`{^~ zpCRbS%rL>ys@{E{9W|;=!00+B5Q4MOxk6N-maJ`=pm$KGO0CMs$+%RZVhMEN+oGc9 z`eE!&D&uBscXr_Mx3`>b3rh1ZDLYEWzJ{@@FnTr`jUusr{rc>y31O8+^}lRT7t^G0 zeRkc7g85bDvOvNk#tsPy$j2UYXMZiNHJOppHz$<7joH<+x08T7h4RaAFe>H1>v%NS#=I^ywy+O_E(0a_EBFlvN0s8^HGxfGMs$5ws& zrd09A9;1xkkFURu*w=D+$Qm+C?bTaKKl=#dOyyt+<)Yw$%XzF?uRh;YJGWBpO0{F4 zV$_wd_Ek~Ck~s;D&#@e!AzM|P{wx;zSJt7K;HTV$%J?Wk$DT&%S08zGL{U-gB##bJ zAkq5#24v{TYPD*et6W!xn+JcO%(Lf*_BkC$6<(cXi$WG^0XMw^Z z0F4QyIu%byO41Xz-yUm?*f-omH$m5^wWwdYvrj2Bzc5dGY3%1TK4LV&ByB~Fx}iFH zKC)NsIj0&1X;7J?D>f`j`ns~_g)IEs z&L|U%&AKyFSIz%f!}X;{^gfN`Mx&~CDHg@#2dc<5ol4kL2xIlC)SYww;~Y@(rAT-L zob)&`MjIVRy!PsA&ZCXkkERewiPR$6J-0yS?@zg(5li zDUqi3=?2Awrww9PErp7v%9Sg_MaAkBE1u6}suuT^EAq3jtn8>+2`@*qsu4JKr01Md zfpNy(WQ;Hd2}Y~po{$VRDnE14PqeY2z;-2!O@DTS<}&W=SZ*EiEOXO1-OsD{ zDCt~Tjg0ykVe}PH0~uo!$hgSVI`@1ug)+;57h^tK@ieVoN}4YrcFbc^Y$NjiAU1NB zx!qwHt?o$YugZh)^dOzcP_Qd$A-8J93YL68gU<@AhqroPxKhK6t)V8RjEBa88RGq# zYo9t2N}J9i(W08K$(O}s5@gj{m8wi&>SrSq%?r zTOp6-_KIF|uZ|2)&orG4UTqyVs92$5c}Pm6q;fKrI{~s57;9U1#vhU$RbxI!1UsiN znW&I*O~TuBXp+0d{VncEDxQ!&Jh&r_Ff1uc=PGQ5v2s!QFVAkkQc1O>ZwzD7HyWN} zjG@=9S+kOjEFAFqU{(r19>!vO8MpeQ>7jd@`PUuyoLXyI9v#=m&$SsxfLOD(mT)Q; zU8eZ}ak9~Qb!)o0OM{fwMq~nH%?qm8@PcicqzwH!kfSNokEs8KhrW3axCdj_rD z)LK&x5ir(uh+*HT&UrN}x!md&&4yPKweO#Hf&PxulOLvJH?3XWY)I1mla77 z8Q1cXQr~lrnhDBx3dU;ictPc&;usC;$vSl}p!*JTs{);?j8!L|=x@no%^0fG)l)6X&vKAUw zN=_DJoET)?3(8wX8WBo4V(a*V$?b=V<0I}tZcVq#4)4icgGXj{h7ycb!1%52eCIot zUedbb#!?{p!O41ct5pRg7^^7-EGOOEstsY*UH4oyK1Czo@}t-`YJOzxAoioUhBXU_ z#N4>M%y6k=jKV35RU9KQzjM(=mtJy7X2(Otp{n0KyIvKlQ;ZS09b}S8=G>SspiEjj zh@I-~KT7OVs_jQ&o~4}=i!QzVBE_YbWNwI&3gXH7bmeC^s#irs#wi)p z_MGI&&NGx>ef30Vig{35-nQd39H`+qjZX8r`f6LVJ%!x;`p#5mi`M7W)m94QMHgLu z`K6Zw^K!*CH?>~BvA*Wn0(rI;sm{jpRjQS&KtXPv^1KTw>-hTz?|F@!8N_bW>Crao z(H?!ws-ze5`{~v0FSUif5gs{sC5Z3{k(C=NA0@TKzVy;-9N{&JyY9N_!Sx&8WrYfW z>@;<(Q>OtqW4RN)2`elY(5^Ln@gwONARdV=@*H%*0)(T*4P7r7~qSLb&+ZUYGpJRyh#m9< zRNcA^R)ExVdGOsit)6+N)%{&wIE$#?{OQIzCBW3i*@&$v7$LojZ)nXRC9Q=~ae-dJ zY7c%frG@d*n=X0qCc_w$C}1zY0>)Qfj-z$Qj(`+z;8L0OvFm~=7c{r+vJua;s(Q8{ zS~P#U83kOnru}gmF?K40{@5zdd#M;XUVUEOitg)}MnK+mN$Ur15=zTN04juW!z)RU zfNh=GF#*!aG)8OIyi8FuiO^tfe7Y(4K=?;%B+Ej0|9xmpX*I1)rv?oSqvxa1XoV54 z-1Ojs4_@-5d&L1lqcE;}#pC6G_4$`aHg|oMm&9e&4Bus!H7|kCU^cqDnNCHxHa;gn z=XMp_ZXJcK-!mR{1YIzN@w|%o+7Skb*IaWG<-7Is=9L&?z!-xxs8Fe2uLY+d*1qhr zT88lMvJo20`py4DUf!>MoT)6nDIT7V(%Lc}n1-=((ZyDER2{uAs&cn(-TJ}jpMO#q zl!9Pdx8cyn4eQq_UeQMAH~b#fXUw>)cJ0e*-Ceb6_3D>ZD~m%z7+^l#gaPIbA7n_& zl&}guN?MyO*;P@}laF2+6ErFlA8frrARVIctV_XYKoclm;SZnu^B?~3^wTt)KRxHN zdPz8^eAN8uCb$0V7dk#uY2YBm&Kw( zR1Jqni*@`GIxSki^5BCj|NQ4aO#bDKHxB>l*FtE}>pk7<_xJx^?xrfmp;6|&SEY3w zgIK3tT~!bAG1V|Wy4>{SgRR$Zuv!xku4mj%U=&b+Z2d~>b(sg(P6gsGpStnJK2LQ# zC39u{0uM1qOjTQF@jUy;YxLn7C`2R}8-Vd*<58_dpiy=4`3KiQ+PauTm=ng0>o;uJ zpf3>$%JrG+S|6Oc^3P8_HSES4|4D>~`t0UEz4bR<(Y04&7I?JrtWKR`7?X}6AMy0_ zudG+qCWK`@-@Gfa;{YPt`t_Lyr~Y|25Ql#gga)(z-Lca5-RGsp@-YQt18J>D(im#@ z%YcM0N65pC?@DY7-GOg?$Mu;bFIltVsVT#r%B)|OnqT8*_3v&HkKgy*r^aklthJ$} zlWG9QivvVS?PH*5q^PU|*aoKyP=|yObQ?ElzhQmH4IMjXj-0-F#Zyl`m3igYKvUMA z)W7@wq|#_@B^=L`=TMB@&Ql=|Jf_g7;%!AoYJ*2Y0}YG<>A?>=8pIb?KmE-So_%+d z`xqIMn=romk+h}jU>ty^ZyZ#pZW6 z|HcSA{wefnB~Z%OkG!VwIzBT-r%^b@)XL*pdmo_+C~!bCjEy$d*$Bk*5PHVHg7A*t z*i`%Kl3_f)iq`PxC`(~f3nOw=h$!LRePLw^c0ZKG0cZ*sHJ zWa)R`#cI;|ynhMd(?0tO(}q%7k9wAKJjh3fcs3XT z8DoqqPf}_dVS=TpTrMKj6;fi`m!Je5Kr^wbjS+wk!frVXWh| zC(%!&6h}&rS8v3=^2#ggzuV|gosJDac(TmbKl2aIp5E!vco{98fL;ICA5(6owdJEQ zD!^DLmB)A>^g+hb5gL_{)NjKGjdi~9us6Mbr?B*sm};BiDLb9dV{=#IF~(>-rj#Dd zv{Eptmr&h-$@=x*ty6E~H*OtX=&Ds8PU=8xTgH{xB_nu7`53~^CM=PU#UWY)p$duE z_3Jk}RPUR*IWxbXudIaisTA%~9}43~nk#_uq@gT{QCi>REHYM;jX`KEjn-F!u};Tt zH1uV@0YbyrCb@`~iY34SfABdb!Zj8y4Wf>%O^M8bkl>+Gt09w;RP+}-SQhdBRf@Z<5Bgoye-T^Wj%F2<-fXEroL25 zD1@dcNk6M_3!#>uzd-Cd8ykEhBG20RX1`8daWznd_A#xO`qD~Fq1^|J;?dV;Pu5e5 z<~#y2L1W<2>>`km_=Ql)*>yU86CzcX>Q5h}1^keY5LyPr$Fz>?zIKc!Pr_rPI^`R> z)$vWyeiLRLxJ2ti^*S~v z&dopj9TUNDVuc)Sr_vM2qVP>e#3aVE&T9VB z)jElfI*!tJa506Hcdb8`)*(1>)u@qpq=@UljKErqX_iA!%PNJ_CNumbr_yR%e~ zXEkri%CoZ>*Ge!VwdG@sQO&rGGY-%e!ELgj@?%6Ajar)+j2dLy<5@Urre2?JXD zs+mA)sIg@;Yjah}JuE2=q82T{IOCi4y2Sw6H}y5gag5TM;W|}o9ZhYd@j8Gkxo#$m zUW9^k)bgUemh}E@M1r%?PfxA29g_xQypEfg8sZy>)Tqs*M&^#Kkw{Q# z-9oPMVJ5Zq7{-(}($wV?ceYRprZ5KcaL4NH$myA-Rr^zc|>dHN9 zQl9^>7#dXF%=gt&Y+aQvX;?kUBmGTviS_`8eyncQ!$1%@P$p85%q_sUC zr;gXc$Q39$zb7enqDZ>h$!czuDBc16oj>nhb$a${pHutve^BbTW#pr#hmJ87Wjx&Y zMAo8)mKPev1aQv;&e>_uTb|A!8b8Vt@WX|b?3z)3gaJ6 zKO#0Ncg3fleDuk|uZ_~%@YH-PhOtCS2H(Wa;ZVPz#uVxsVf^J&rzf)N6`y|m$tTAO zzV5NGvb9d+W9;ojzZ;mm>n6+tHiMP2!}jp_%hMBicDWv}zy8|T(OKzVuTLuN89Q+> z);dvu616-A8CFEDf8ymSpOff2P$a56GZ%DZe} zw9Z_mNME)h^d#gu#v4!HTAcm&zkTw_N5_sG+q7xUwlkwNt&8<$^k~W2k?|)B+Sc@pMDl2 zOWa@chikw%qf{kM{XzZD(E1Z2a?_?+vz|WvJ1Q$5Au<(XIGZ%?FJhE7sZAI?hR6i% znrnV}(Tx|Ler0z95WzV6@TNIAIsFIpc;R$~2(A4b&BXlL`f)JcZaTZzp#)u&oX^Gu zJ>(`34X#1F=JH?u^1|t{WtMX%GCPHVpDJNkeN8yC6mVhF4;6xVK!ZY zsEX}*8yEfZ!V8~@X(;>tgo8xvSzB@j2xQl%Pa(LUkIT8PCAEIy+u#27x5CNI#hCO; z>3kN$=HKWS7ya_03okqaksW8xnUkHB)xCe)E^WKEAsRPs(zI;B;ZciHspGX0vXWhE z@YPp*KF5d{>qQsFX&L9G0~Mv0`^nky4=xhMGZ4AQ#D>Q$Td<9I?Ao=>pUQH0>9rK| z%-NhLUaMB+$~F8dJT5pC){8H`&|AuqAY*J$C>jYcUU>StxA9oPv4X>g56_vCGiv~1 z_YllBZSF3e8amCMb^DT9gVBPMemU2_Jf8p;Usl#?#J9ieSOv_-afcWl`@0av3roLT z>Q9yn-R5Y0-xwfBAD>{J+a=C7aq%LA|iR@cRFWmUNmPki^g7vGqg zogfZ0LpKn=Q|i@HA)X~7#iOL2)qg-&nC#KDDVce9iPJfywV#(_8i(DGKD;GAXHd=nuKx`wiQa>Yzq!6{?BYD0 zj4?{;WY{HX4%4rbEDy0YYWQ`TUyT?6R$FZYs31}ePTwyaVvNZdFn}5keN82HV~MRO zMy0ea$)hHQoC7H;5xN-47@^Li*D1@@YU={OTDrD05DV6CfA?FQ&8xWZlG9f5X+~r| zV&}kP7l`c91{wu-aLPY?-$ADGs5uu0idhM0#MvJ@@jg&lGodzu+f|Y=NIs}S*Z5U( ztRnWShF-M3^_@#Po}x6Lb=JFJ%+JryQ4t?t=@Hz}=^lA&brd^vh zmDaJ?y1zI+PFjM;fKWX81w4Z2p+m+%JrCzNNY^?*w99wxB4bBct7>^}=R4LHuV`4K zQdx&@AaYComN#++_3zcAhnQ3uYT6aG@psd>&j6z`vILK37w0hvQs)Se01b2rr=LXc zJQ|H$`x>zGHb>MU=UB9<4(>J`s9r{X!) zE0tJDsXuto00;zAaCQ^q)}|ZLOn*PqiR7F(D~$=Qlle$Smf|r1Qs-$almOwHOHMIV{JWy>3b1`UdNBsGuL-cAwM)|*irI?i z)~woh71JmtZB$aRk85b6G|BVGkADni#dFFP{pEOJVZru-f<@Cho|S3&*sE87 zD03?es++tMC)gl2P)B7^%Z2s5W9oXh%rdZ)VdVgKb`RLf6>C+iDH080AhI|_75W53 zvwSG-xaj=qd|<#|atjV`+csg#8*dEi@brKl&0?)hZmZ?K!{ul%9wqmC?;m^b*hktv z`B?F{kN>6{@n8S->0kdkVuXPNr$bd5RMzGADN=j?k*am-2T0iXgW2tx=f58>JaT0F z_J!LPPM84BL4)xOuJID-JQ|l><#A~pQWE;05C%W_$U!j^xQJd(>XTLZ5z1QD+@AYHHK6)L*y20)zA0H3&Zu`{0f>oP7_S%9EnXX*D*10i} z39&sN$;flhS2O?OUym1VFMvZ)XaqJ(h}{@#qc=O#+V_o>iNvP&Hz7E3zjy4|dx9lQ zk~|0=*!*1gpMUPR&p-d~zZ+OSog1iZ0j8yfq~G?bQ;;B=5I#6X^40`wEh%>Ysm1Io`+;vWt+ z5Xx-QLm#|Sk&j)pPc-4vhPdSQAG=PRI4X#X1W{xTmeih*1~Ha9Kt=0E5gWO?>P$p~ zi{M$KH;sOOHk)gH4AD1#!m7e2?71n9b31Sedj)VNO&p-S8^8mJ?P-^Cz z1WQxdu-soxIEAvy1Oobuz-G;wSq(Rx5!&zdK$-_+sJ)5XruS#Rw@G?iIGn`(Z=d{4 zx6l3tDE2%uSVrf6e)gZm;~)RB1h|KeYHGk`Q67cE+}FmK_)-V5Qe!{EV#0@$Fp zES@xZOdmP6cY7ksk$c%cL*PJHJ30Qvawt!xB$iUnKeZ7rNPfByV}@e^}% zGctNkXy2|~yIyx1G?;7F3$2?qYuZc_C&E3B)J^*-6Z^BxoH=Wj?Ck-U#Rx4RA3yn| zfK60-HVOrSfS-M~%i#H*Z%fAs-U$O>VF9Ik`}W-23FKq?gu(6Go2xsB#$fGcQaM%X z9`cHtWfmxB&dLfPHvw|??4;<(USPTxd;)ug0vgAO&rW=H)V|mWvqgMwIjA%|j?hQA zp>%HEw%)Q#`}DzsTDBkDL4*>PjJ`*3gK0^rGhronSJiU*wSN7O8^MWLvu5SYnw6Ek zX;$_ei~RgKWAYClo~tOBTM(kKprEiouU;^3-uUtJ*cHy3r@SjHoX4a2Z1d(9&KG?d z85zAN^zPlhefyS3eP>J4SmNGkVoUE5VS;z<*U9BZZ7~Uzo3we%-n8lP?8B^Na$_qf z&=)raz>FBiHL*k#YGOHeapcuTZ+AAy?PBE z+%nc1Dvd|84@^2j3Esc|%>DuiN&^YbIde8?BW=&>!W_~v_weC@ZLWI(4eNGGOk))`<&GUomh32mK`7kL@C@L* zMR|F7OPBVB#tul`9_B=3%U*YS`QZ^f`c1A;t?bpqoDyfGS%q~r(6Nr*e^%#Nomq;> zVU;jjdfV#EwrvHuhjR<^52qcTi|D!B;4K%dz$`%V?POiyb_q-p0B*hw&Wje!U%F_K zAm$A>jP2X^9^8I#dm!G~@=oLqoZi_|f}7IX(VP$HWg5fTkpKQ-g)wKUWW-HZ$h6wfxCp- zNnP==f#oN7JlLGG+^IPb$=07+s5^1_$=NjL@TP$4Fu3?K!r(_NDUfm7;cdCZA~Mfo z(f0YcW6>mb+i7qh5DFJ98VcNnLx&IVy^sP#UaANYy;`<}%R6DS*_}O_HDmm7&0{u2 z6e&qzjE$0TN&i_nTa4B@g9N0ux*u?(Es0(ov;p_&-*8OFT4p(s7H zcV2Juu>DB(}t^r9n6k1Vq|I%Tq{VkNk#&bY`jw$ z=}BqaLgSsm-pm;JWI27*{-cc>KiZhZ(?{7FH#V$Yy4WXaA~p+|0}y)N4(zlR=o`>7;FR>7t7?>@cz44(p#NG;C@e|r~OJW?vX)#$}kv6)FNHrvD!8+W$r-#-VR zxY#*^w#>;1Vo$&;+)#319s@54XR-8%(Klt;lqrJuCbtzUxUE>e)DfE2_OK7=`cQgw zC#gd~Y6o!}=c7h%RY}!zB_$WF+$&d0!z=%^?Xb4{-z25tm!Y+%q71a|$X$ zrt$dZsyA1yMsrM(*Qd`=*SZfp!Xklb+&F@-Y81k8H*1UBcecgZgL{RtV-S^(2C+XD zAq;2smh?ANeQ54ORm{tiTL7sr?h+`)onnQ)4Cv|=t6|epQoz7EWhofJZAoeF)^2d9 zKmw`}#{w4BWJu-kh0ecbs19(u>X=TCwrzXJ)hwxFZ4@8$h6Q3%o?U6QUO0T=!llEP z>K4QfS_`vo!$amt^zhhI(B+om7~CE1wL6sW#u~m8WznG-jRWW+j7aNva&oY>jE!gV zA#))`nwmq!S-6O51BcSuKnV^)6QYtkSk$%}&4(kh^d3G1&+vc}36r+<@fy{VELF7( zOtz(8YkOz29xVr(tyO!Ji$Q7gwS!~?%uyy16I2+j_AqzP6GBOCVlTrfpnP)*2u*3h zT)kQtxl^oKjpW0H4U7mbt@{k^0L+BHWvwvgXqY4(fY}FdQ?PVn)$mlMInda)OP987 zC9iYRV%8(xi40(Ha&>JCSIfL23yMi>pN7 z8oTLk^@=Hdrc6O{d1k2Uq=IrDlPM-8H{BYzd)?XAn3Tex)bUw{X^&(w4$2;so$m-$ zc)aweBI;(tpRwoZ2zziAWcEBXo!Px$2-ySl&EX`aWJhzwP;W8J;VE*r6RqvgyIEVb zwy_#dH)fJ8$cip(vb1T<0iIZB77Hh|xArUOETt9lH04_xVP-Z?W2Ftc-oo@3z-GK5 zydt)VrJ&Lq6t+1k>n-a%&!V+9hoY?rI$7Z2J9p;{gr#@uWK zqOk~gt9T&ikHb&cZI3JJ1}OOm=*yI$??>?Ixr5rkc2>>zb0JcDv_ zwq$FF3CU`av=NDdBZMPSQku%otn56Vdc4dalcHu}D>X4dT`6fE!oOORqrrZF9ao8$N9(z!x&=-0cj$~ z01?z~;Cn$gVmcsS7@0Yp2l{T>>J>C^o{~yR6Wgj#AhumQ>HWZ+?N~C?jZ=VJYH=!| z+j1IMP1S|C7~Bb zMsjgyVTwy^jo?k2!k^7rv9?z9&^oS;_+?K179&yvHKm&ywc8qS%?qsC!7w;lxIFD_ z>|3*X+8U{gs>T`&DrWG-ClGmv;Azv`;u^=TRvKhs7F^w+vR6B~!)gx|kz53!+nSba z3+0=xYyxvtZydMTJc<_Fs<*WgDmzL>OGW6jk(nwZrAa#!b!dhQj59KuJH#2&rt2+C zTO*5G2&8f{&`OKMDrHL+rQ30Bww6wOoiunyxaq(-QB5N?Qfr`ogH>U5;EdAI{og<|uSXlxS5USd!teie_B-oK%Bcpje2(Hp7`a(tbg6>m>rk$`0GD9Y7 zd75e=wbr0==D;NLYFI?+3EO(3v}~>F2$2f&i+J=YOsL&fuh&dl14n_e8F7r9F=Hg3 zlQcto>UT4snRsE%Q@q_EcU;Y36TRE1PPP*^LPpVogd@xWpR&cbgl4KX zX&yRsi1Jc1gcYl-a#L*B5IMw*>0lZ;a)zMsmB}@TdT2l9PXv*7vw}LLlr(?iB3}-2 zlxS_sCz3mqaD&{Xt;sB@nKq=qfu-MQFQK(C!rKeczI{*9rYCc0mLKR0Juj_V%f*Nb zTop!zQ8zNv?kGN1l3Z{p!0gP$PU&+sbSqe*M{6F)B!(?oe(zdEJ zp=Wx}2@`tu?wP@wYzFIg{B%g(kRd~d3?DLN_^@SUBACE6O`yb>X!OV&IT9X;m{rHk zyC64I(v8bRHODlHmMxt&-2As~Y2BH_%tWncuS!pHHSFkK4U1i@}YO4Fj z)2_7@Z&pCt(~za7KhWy|!gMB^+qdh9)E#~+U0Vyh>W>3sN|y& zYro*vAoI@1YQdXK9%OFuM=e+LXwe0+S&g?)bz}@A=7>gf48_};+uS@Y&_m$_D4f6= zJxa4a+k0Z3lqR5XlJhuhvc>XM(*uWa7Y7$WBey9ouu2Crj?@M(R9cl(p%hqhy?wfC z9b>e47L|v(V@iAdW|4;jDq2uxMLDCV@L2$nFn~iQX`5tHFPl7U$>hm9b=y642GXWL z%ruOG=y_=<5gg8^sr^{9%7$>s(gA%yDZNs)Zewh=?ax4+sNU&=a@qr~9Rql~b`P+5 z$k4FbvwcQ;6S}wZa-!oMqNKD~3{co4LK2dX@gs*PZ1AUY&g4fUz*5m?Vz*QOD6v@; zWeaLs0Y`0mTVJD>-118XF!Ot27j77aaHWmMiGf9fH906fdFSq(yLa!|vuEYLm1JQi zAG_cYtL|W3Z!AXgLgbp&t5&TTuH`io+rYMO&(T5a$&kBq|L!>)jA1!1o5L}ljRp}v zE1=De90+1w@7%nJYD<#jZK-Kela@i|#glgu zP&swwzIRp{nVIWDJ_f-Jp380EG}vn#klu{kGIx8_4z`9&*_)$UEH9%pVMNZ&r+zcR z7LDoo89noZ)ERc{g$$I?i)5E2KwGi|w^%?TG_HJS-@dhL_Y-T0gF$R?>O&YpW`?2B zd?PAXDOPByr8hekgVWmwrD+|zba}iB%>)4X5q6pbl7vyvbw0@w_A}rSy_v zi^)j=Mdx=`?%zLks;Jz5VC~3*N=i>h)7#Q9HaE^Tsny=SImO!oSzGf}jno!h`m2Qd z!49J_Lk+lu&QDKESMg4hVK~|*yGZDX6Rj==Xi{2*Q9{4-&O7_z5S5|OBXiyQkq6hw zILgZaHN`iERmEsEM6OsaBH^KZ`}o)!7`t@t+?gZ3o{sq(A)Jt(#*vO(3Eh)cA|oxW zr!aC5n3|J|t(A;2wWp$IqkIC$cXrDsmWT&HxNaod`pos~GS?l1ONE0B<_>jSy@*nh zVhPG2QhP#AK4L5mt>U&pY&_i`p}P+n)E!@&)ZHIw+dWpAAEZ z7}O`{vhgVvFexrme1mC1nMCQ;!P;P5v0MunJv|XS`(6Le{X5IpR(C+TErSE9`M4UX z(=w!US}q6sWorXzmKMYvOE{js#0sLDS{Nxh6dVEq*8^|EhK+nWO#rzz=^piI%1R%# z*Q`QpgK3m<@_=h?D%*sMIRi@f=Y#}KNyw2~(y86uy9ZOJ<7^W;BUy!vP8)2LnTr?i zSI;bK_vM1N^#*+g|uNCTd%T2#MT_t(=oGUOFOh{ z7oR92A_s`a?keA2!sl!|Y?jI9tcf&~c2ny}b#AUnJ%7l2S9*t)Z?IWyd2q|n@3;Z$@N<5D(9_acv?fhlS{;YPRcK8NajlUWbfD~xwK;i05Yrf8@-q(S z=4NN(>7G~`wUHXFIm~a#IDg0_VcaoEN-tjGgP*jf?(8KS-7E}%f_R9~2Y5l{2Fu9< z{DHa+9~V_9Q5%dt)T~&wN&pp0-PJ*CTbxoAN9>$I+1*WRNgX0MTCP@5zVn2ZDj8?y z4ot0KdKaZqIxYWjewsW(yCbdP(IUa)#QCN)OyU;-+r8TivkMw=4C9I+G#c$g z#;*M0N&~qOwdL*u$i08%bfa=c=*Q7IIED%-s7IGi1|T9oj?U)o0hIC1UV1pD&Vkn4 zZD|yrG(rG97d;s#PMkl{q~1X+ad?c;@4T~b|(!`WJ7 zWEx|2no3VzZn}nPFX76?-0@y_IOkHPHl<}1HQsVGQmc^|DYwL)e)rukzBG(qejy$c zTJK+r)O&XCUcP&e&2i-%ms>D*D<3B>pRA(_38A%mZa* z81p$_HE73X5?8-l-$9zzDn5`%88@w|s*{%tTeboQ)jJx6G~<$6 zMSSTZTSW(ayQw=L z?%nIQPDw|B%9~o@m8CTC(xtEGBA}5oTZf^A#OcaIIv|et0RsqM>oRrtU`z)lU zMN3Q5Xgyb`at)$T=5lf#+>IZPr6*1TBPdO4AcndK%6Fhq7%>fU$G(QhFHYhaN;zfR zN{?!fv_@>o0=?%NE0dOy)Jp3jO#2bLT{_5v)M**&BIC5A z0s&R2DLzH#+i&j@P^;kP>tJks%BmOfcQ>l^tVL?Zbiv!XShJyF!-h-=zP5E%+A|Ia zptfnP(x_APG{2tJomM<6D?5!7^RSH0T{Ipw26B!bh8aJ8JHR}g#~llRwi9#`8{2&N z;fH%wd{S+r&#M(gl^$x*dVEcX4v*9-xlKGSS@M)OlB+a@mDVPZVS~B~Fd9VW z;|k_&D>TavZAC~(v$cfw>TNos_Tz2I$>8e=a(2&dY#thL2rEIho8);ZsBkGpm67{V zd#_b-tZj`C6skGXAVa1eI=Nx(RK_Jqttlm;Rih!pRKqpTwkj@bi^iFdsq-;A=SXmo z2#pX4le9RT%x$h2pEh^k-0`{N2?jxnLb+Nz?gSUFLVGcoKy$7K|L;a`Y<$gxd%wbkz5?SF_RS+@=-Gl z=rlfQ%mAsO##+d5EY5t5&`55+ejKqocjv%Ddj5p82^rZvyLEG^c0UBh|?s(NXaKPK!%tKe6b>+rP`fwWs4Vtvp`CvLL3}Y`H)|Udw z$+wJz?7MTz!q#w^)h)Xlwt>gI?Gv|8G#X`VX)P8f&ELUIAr>dU?Q?zxLs)rd4>TIQ z1e45Y;1aWlL@9l6jmXWXwaF|3XhI)2Ffv{Q$n*=e z4|VijH?BUaJOMc<^Rb*{ zzL3PK^5foS${?UPSxHFn1G0ccjCeJLBXoYz=An+6q@w2`u9nhhy>w>hnKR{Sl+Max z;+mC{B~NEV;+Sr8#-!=YYMnO9MI|E9q#j=|&x~D21HNO4%?(IIRwtDGd+5zp?xS_x zPaL356Zl4j(nSU>KVq!jzcytulesZ`bP{yZ^Qg^r7HNDWHPuHufHGQV`KrD0aZK76 z)Yf@~9DYM;7keJpO(ceDO|*OqTC;M?+U-jEGp$fC^%F#)Ja|w;3l9XE5r*IH4Injn zoiSs2^3#;za~criS(gqMO6Ww$)@Ti8JMX_|Ixf~yR2s&%LD76oYoYQjp*>hF(?GF2 zk(lz%hv*FygS%HyRn`^zr+&zIv}@NMP5oyin#LJ1ne>`h9?G1tW|$ z{^5t@>{N~)ibHPR-^P~{TSv@?c0=iOiOihZcUhg4j9CnVX=&u+m~JNb+yXlxe4UL$m#t4sw!;WI)B%Smjk8&2Ub$cEw}i^gUUBK! zNb5$>3e&Rh-nmaXsZjl99l)EOktwMOIQSB*baE1%Lttw_l8(S+d{Ox!CF26F5l}*F zl$pGk1puf+Y7a}tsr!Iv2`RmGJ4h}%MyRx8G&Xnb+-KP+scnoApVY|Rs}eAan_BKh z=`IY`9*EtoJ7YCtw$7%|)jV4C33Hl>lfoj3Rv$y4whcFyoW?6_rC9vKwP-GhJ!saE z+G9EFC71K8tz|;EZ29D6(>rR)o2}YN^`?U7sDwkKahWBp(?sOhv@vtXrWG)l963S{ zK7PA5J+u*%zzw&LtqZU9p>RnB+s+rP)OetQ?2 zg2^tiV6oM($q82*s>xaoU$$(?vSo`VEj`kCW|o|-t4BL$%?hrD$^?&dVG$}Rjs-`! z?rA^%|q#^gdA;3r==k_#vVI+EQ1Vc?>G{-W}2*u0+ZTc`Y;1^3adY4q!bs> zm1_^|1EW_)Db2isT*NtIlK`X&Pv|E0BpM3lVH9u<4W~xi$5C1fx_GI3R=3XGva%$# z>NkGguB){OlUj9h*b;6_G+MjZlWoM=ZSI!R_*=K#d-m?$w+~2fzhz2ukIqbbnGR{r zMuTVVqFfDkQjiZUYnUfs>!YBwHM4})f+Uzzb&s=&cfh!3-(Js0Kr-dg z99QcGF7*OdIVOAAQ8MnK?CcbyI#Q|&Aax>uj=DZ>cN~drs~MLEYBS=5U|dA)iS)an z3s+w@Oa~Q)@l?Ur;#I^3Zl$wg>OOWN4VB+|YXLV@md=Y&T3Qd8G*L(OmQ7x!{jgz+ zh7DP?XoybOki>lxmazh^0Xr+(JmWkDU)ywMp5)%ab-95;^J zxN$U(ObWS36p%Z16f({b;*FL~v8G^Hs)U|eaHh}Im8dzkH8GKLCiAX(hNI4ln? z0HFZ{Bw-7&y6!zU}A6;XCCYN_#9=u$9}S1w73V12uoVss*bw85Uvb zrf1KT(kL3OI->QMY%{hTL(H8Ek4P;d)rG?!Ocwf4LYvWe8*W7=s4XrDT;qhUB_H?r zAP7(s+N%(Wsl3z|Hz(0(1aa;}RvR)BsThPdm}o7GqeqYR(leLOEI?_AEw$$%^pZkr z&WyXA%W1{w^u$nG`l6BdPF1om0 zVw>P5cf2g1OtoA^Ym?fd+zJ*>&@7jPbFYiSO`Az^-V^(lYv?GVwU~$x!)5@!%&b;1Ziv3!JTP68}ux zuH-Ri>*O92+snAd9U$w=6t_39N1mpH3zNyrXKQTTT8Q(c^gK1sJm>iF1LbNh*U;}L zv@XS?xn@lEXlM*-&mKDt%fKZaE{;*yf)u!vi92^5m1p8k+ypnH?8e)oQ$%8!z+!?% zPfQ>aleWU3)kSos21#rzJs!jZGfe12V-d?9#Kzdf?6I=-+;QXQjT^Ul^Stq!CHH&^ zxmjh$QMHf)DQ7SDp$}XF7V3{OK(^k=O%0>8jsv0fVI{HFwvHs@C>;fn8FPlqEVVs4 zQ@}$Tj^Lp(jh5VuJoJ*LwT+4sb19ToF93_ymYj^)2(AH}wF@2LRM5lj+_QV}QJH$N zv<8-9-d6cf4y1Dqk%8K#wNBJwQ)G?G8pTI)g}~Cp=+W4Q#wYeQR4Skn7U#{|Y-00? z2@176_?qDmv4h@ls5LYXDb06N&C^1h!s1pl3_JpQ{(NrM;)mn}bnd``19Jza1#pQw zP>#+X4NMiEu?elmz0PpG`E|5LZAm?U2iXor+W?IaiX%1^h0T_(a{#T%vywJA`pZQ zGD9Q%?HpK?+U^*n9yjjwxv#%Ijw)Ut9_h^rgV@yZoku701(Wd%HYU)?6}Nj?s@~HJWmWX5>lE-T6I~xk)mdreTTsY0srt~}wiLg3x9ycR(-1xa9<9Ikl=@hxm z)hghzw}pIkR=3gQr!5Ddwx%{5Hq@r~K7Y~?l-381xUtpT>ua&?ID*Au2Gbr%z1W3b zPzZ6nQGF?(OkV@p($WSFv3oSHbYYhK=}j(k`wu1X_UDv<7ZcX*$7NP|wY9Uz3sO8*lJteOZ=J zjz#R*V_(;oa#A&%4fYtYNl2-!16@kUw^hYsWTYSuHG}q_?6dav?VWEez|@S;{y|eQ zNYW9R=hF;0rN%1ELhV3|{ZPMteII|kuX($#q_)-3#}n3$#RjjsNmhSslBkh@v4AJm zDiBJmM?-As?O%2@n}rNbtaj|!HQ5g=&R+q?7w9_`w< z+k}owW0F7>A<-CzrKaBhTwvoGbup?v+6>z0lz_Jwqfm~0eH{NK!sgAccKm@+OuV4) zttCf~E~fez%%j{HGNDrlEBVMc=m?Wzq2ps8M*ygnU*rN)l%lm&ZuAa;-nu0NS`|R< z2B%{OqO~1j7-wZX6mo_gq?X=^J9~_!6^nQ8n7@P74f<|st00lpR>(GM3Nnt&<4frW zDh{z|pbm3-?y^RWHXa4T&}hd@M&W|1?-@J$^|AV|C8V_thANP%k%G2h$JWL4UrTJ+#^1I+V9kcZ zLKPRfg|~JbU2x*)lD8Nc-#+m+oW8}5g&j=)*3JcS;~29z?~|=tH?xISPfBm5hvtS? zgd0o4;@r8cbf;*YqO;J2d=yN`BOjHKX#Khm*@*3O3n&$kRVJmJH4Io-->F^`Z-=@K zJWE8E8i|oy_AvudDCHjZ1X7P9A&pvqK7`V--ssGCe0A+2V2TKs59LCW$KLk?<)WwFjdWA0f48)erZvCQI&J|B@w+PZr;RdCFV z-lc>#W1FvK8whkJO%~Iemo4NIG7-BFa~Cd99>QN>6Y|u=B5#UQtYoWjt2*#Z3P{vu zX0g42A1fXQs)rdn$|FVMVh<%X2(_%@32DWLC)O(fQO*P<_hwq@YGL4f>TD20A}&w^ ZAv0SJCQyo6!nL-C18G<#HQU&+{}&UhRh<9; literal 0 HcmV?d00001 From e539310e6d5d61dddb8a6d15b2a853c1407a1f6c Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 13 Jul 2021 10:14:00 -0400 Subject: [PATCH 008/180] Namespace plugin-specific configuration settings. --- data/core/config.lua | 6 ++++-- data/core/init.lua | 2 +- data/plugins/autocomplete.lua | 4 ++-- data/plugins/scale.lua | 10 ++++++---- data/plugins/treeview.lua | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/data/core/config.lua b/data/core/config.lua index 777c190e..401ac0af 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -28,7 +28,9 @@ config.tab_close_button = true -- Disable plugin loading setting to false the config entry -- of the same name. -config.trimwhitespace = false -config.lineguide = false +config.plugins = {} + +config.plugins.trimwhitespace = false +config.plugins.lineguide = false return config diff --git a/data/core/init.lua b/data/core/init.lua index a23026b2..7a16ccae 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -712,7 +712,7 @@ function core.load_plugins() local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins ls[#ls + 1] = filename end - if version_match and config[basename] ~= false then + if version_match and config.plugins[basename] ~= false then local modname = "plugins." .. basename local ok = core.try(require, modname) if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index c76eed02..ce2d7ff1 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -9,7 +9,7 @@ local translate = require "core.doc.translate" local RootView = require "core.rootview" local DocView = require "core.docview" -config.autocomplete_max_suggestions = 6 +config.plugins.autocomplete = { max_suggestions = 6 } local autocomplete = {} autocomplete.map = {} @@ -129,7 +129,7 @@ local function update_suggestions() -- fuzzy match, remove duplicates and store items = common.fuzzy_match(items, partial) local j = 1 - for i = 1, config.autocomplete_max_suggestions do + for i = 1, config.autocomplete.max_suggestions do suggestions[i] = items[j] while items[j] and items[i].text == items[j].text do items[i].info = items[i].info or items[j].info diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index a5f5aaee..79152f8b 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -8,8 +8,10 @@ local style = require "core.style" local RootView = require "core.rootview" local CommandView = require "core.commandview" -config.scale_mode = "code" -config.scale_use_mousewheel = true +config.plugins.scale = { + mode = "code", + use_mousewheel = true +} local scale_level = 0 local scale_steps = 0.05 @@ -35,7 +37,7 @@ local function set_scale(scale) -- we set scale_level in case this was called by user scale_level = (scale - default_scale) / scale_steps - if config.scale_mode == "ui" then + if config.plugins.scale.mode == "ui" then SCALE = scale style.padding.x = style.padding.x * s @@ -68,7 +70,7 @@ end local on_mouse_wheel = RootView.on_mouse_wheel function RootView:on_mouse_wheel(d, ...) - if keymap.modkeys["ctrl"] and config.scale_use_mousewheel then + 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 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 0b9304aa..14ada70f 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -358,7 +358,7 @@ local treeview_node = node:split("left", view, {x = true}, true) -- in the treeview node. local toolbar_view = nil local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview") -if config.toolbarview ~= false and toolbar_plugin then +if config.plugins.toolbarview ~= false and toolbar_plugin then toolbar_view = ToolbarView() treeview_node:split("down", toolbar_view, {y = true}) local min_toolbar_width = toolbar_view:get_min_width() From 423cd338105f8d4acdda346ab89c4b95f6214b88 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 13 Jul 2021 10:16:17 -0400 Subject: [PATCH 009/180] Typo. --- data/plugins/autocomplete.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index ce2d7ff1..867d5360 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -129,7 +129,7 @@ local function update_suggestions() -- fuzzy match, remove duplicates and store items = common.fuzzy_match(items, partial) local j = 1 - for i = 1, config.autocomplete.max_suggestions do + for i = 1, config.plugins.autocomplete.max_suggestions do suggestions[i] = items[j] while items[j] and items[i].text == items[j].text do items[i].info = items[i].info or items[j].info From e1530c0951a85acc9084ccfc0a557ec8ae06a924 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 10 May 2021 13:08:39 +0200 Subject: [PATCH 010/180] Remove duplicate normalize_path function Use the function defined in the "common" module. Move the check for not-nil filename from common.normalize_path to core.open_doc. In this latter the filename can be nil if a new unnamed document is created. --- data/core/common.lua | 2 +- data/core/init.lua | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 3df3e8f1..aa428792 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -258,7 +258,7 @@ end function common.normalize_path(filename) - if filename and PATHSEP == '\\' then + if PATHSEP == '\\' then filename = filename:gsub('[/\\]', '\\') local drive, rem = filename:match('^([a-zA-Z])(:.*)') return drive and drive:upper() .. rem or filename diff --git a/data/core/init.lua b/data/core/init.lua index 7a16ccae..c13151ad 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -36,14 +36,8 @@ local function save_session() end -local function normalize_path(s) - local drive, path = s:match("^([a-z]):([/\\].*)") - return drive and drive:upper() .. ":" .. path or s -end - - local function update_recents_project(action, dir_path_abs) - local dirname = normalize_path(dir_path_abs) + local dirname = common.normalize_path(dir_path_abs) if not dirname then return end local recents = core.recent_projects local n = #recents @@ -70,7 +64,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 = normalize_path(new_dir) + core.project_dir = common.normalize_path(new_dir) core.project_directories = {} core.add_project_directory(new_dir) core.project_files = {} @@ -379,7 +373,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 = normalize_path(path) + path = common.normalize_path(path) table.insert(core.project_directories, { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, @@ -826,7 +820,7 @@ function core.open_doc(filename) end end -- no existing doc for filename; create new - filename = core.normalize_to_project_dir(filename) + filename = filename and core.normalize_to_project_dir(filename) local doc = Doc(filename) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) From 265501bb9e5f887472da32c09c2320f645cd986c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 10 May 2021 17:01:14 +0200 Subject: [PATCH 011/180] Fix problem with previous commit Desastrous problem where core.normalize_path was removing the leading /. --- data/core/common.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/core/common.lua b/data/core/common.lua index aa428792..6b9dff7d 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -269,6 +269,9 @@ end local function split_on_slash(s, sep_pattern) local t = {} + if s:match("^[/\\]") then + t[#t + 1] = "" + end for fragment in string.gmatch(s, "([^/\\]+)") do t[#t + 1] = fragment end From 6e460a20acac652585ebe63bb18d0806792bbd22 Mon Sep 17 00:00:00 2001 From: redtide Date: Thu, 27 May 2021 21:07:40 +0200 Subject: [PATCH 012/180] Added Editorconfig for the project (#228) --- .editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7278fb63 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true From f4f33bd36b50994565d76893f8801ef55b7d0705 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:09:36 +0800 Subject: [PATCH 013/180] remove deprecated code --- data/core/commands/doc.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 815d9511..ca39bfde 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -45,7 +45,6 @@ end local function save(filename) doc():save(filename and core.normalize_to_project_dir(filename)) local saved_filename = doc().filename - core.on_doc_save(saved_filename) core.log("Saved \"%s\"", saved_filename) end From c41747c8fbeb1b73d71a77fc841baabbc4e9fa20 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 23 Jun 2021 18:18:13 +0800 Subject: [PATCH 014/180] add .ccls-cache to .gitignore For that one user that uses ccls :) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 92d20437..c77f2fee 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ subprojects/libagg subprojects/reproc lite-xl error.txt +.ccls-cache From 8bbb26a4695352fe305d5202071176abbf4040eb Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 23 Jun 2021 18:21:36 +0800 Subject: [PATCH 015/180] refactor process.c - include api.h instead of individual #includes - moved metatable name to API_TYPE_PROCESS - moved read buffer size to READ_BUF_SIZE --- src/api/api.h | 1 + src/api/process.c | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/api/api.h b/src/api/api.h index 4b0e14f0..51ebb9a8 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -7,6 +7,7 @@ #define API_TYPE_FONT "Font" #define API_TYPE_REPLACE "Replace" +#define API_TYPE_PROCESS "Process" void api_load_libs(lua_State *L); diff --git a/src/api/process.c b/src/api/process.c index 111667a1..1de6064f 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -5,12 +5,12 @@ */ #include -#include -#include -#include -#include #include #include +#include +#include "api.h" + +#define READ_BUF_SIZE 4096 typedef struct { reproc_t * process; @@ -29,7 +29,7 @@ static int process_new(lua_State* L) self->process = NULL; self->L = L; - luaL_getmetatable(L, "PROCESS"); + luaL_getmetatable(L, API_TYPE_PROCESS); lua_setmetatable(L, -2); return 1; @@ -53,7 +53,7 @@ static int process_strerror(lua_State* L) static int process_gc(lua_State* L) { - process_t* self = (process_t*) luaL_checkudata(L, 1, "PROCESS"); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); if(self->process){ reproc_kill(self->process); @@ -151,7 +151,7 @@ static int process_read(lua_State* L) process_t* self = (process_t*) lua_touserdata(L, 1); if(self->process){ - int read_size = 4096; + int read_size = READ_BUF_SIZE; if (lua_type(L, 2) == LUA_TNUMBER){ read_size = (int) lua_tonumber(L, 2); } @@ -410,7 +410,7 @@ static const struct luaL_Reg process[] = { int luaopen_process(lua_State *L) { - luaL_newmetatable(L, "PROCESS"); + luaL_newmetatable(L, API_TYPE_PROCESS); luaL_setfuncs(L, process_methods, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); From e7b025203baaba2431825dc5c73be43ea8f3b0a7 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 23 Jun 2021 18:23:19 +0800 Subject: [PATCH 016/180] add generic read function process_read and process_read_errors no longer contain redundant code --- src/api/process.c | 58 ++++++----------------------------------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index 1de6064f..f20eaae9 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -146,7 +146,7 @@ static int process_pid(lua_State* L) return 1; } -static int process_read(lua_State* L) +static int g_read(lua_State* L, int stream) { process_t* self = (process_t*) lua_touserdata(L, 1); @@ -200,58 +200,14 @@ static int process_read(lua_State* L) return 1; } +static int process_read(lua_State *L) +{ + return g_read(L, REPROC_STREAM_OUT); +} + static int process_read_errors(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int read_size = 4096; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } - - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } - - int out = 0; - uint8_t buffer[read_size]; - - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_ERR, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - // if request for tries was set and nothing - // read kill the process - if(tries > 1 && out < 0) - out = REPROC_EPIPE; - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - - lua_pushnil(L); - } else if(out > 0) { - lua_pushlstring(L, (const char*) buffer, out); - } else { - lua_pushnil(L); - } - } else { - lua_pushnil(L); - } - - return 1; + return g_read(L, REPROC_STREAM_ERR); } static int process_write(lua_State* L) From de3013ce88890a891c66ad5604ad432a7b6ed9fd Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 23 Jun 2021 18:29:38 +0800 Subject: [PATCH 017/180] fix wrongly spaced variable name --- src/api/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/process.c b/src/api/process.c index f20eaae9..7bdfd00a 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -200,7 +200,7 @@ static int g_read(lua_State* L, int stream) return 1; } -static int process_read(lua_State *L) +static int process_read(lua_State* L) { return g_read(L, REPROC_STREAM_OUT); } From 818e21610c439ea3f95b494a940f087fd2e95278 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:01:45 +0800 Subject: [PATCH 018/180] do not terminate process when read fails --- src/api/process.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index 7bdfd00a..f279a6cc 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -177,11 +177,6 @@ static int g_read(lua_State* L, int stream) break; } - // if request for tries was set and nothing - // read kill the process - if(tries > 1 && out < 0) - out = REPROC_EPIPE; - if(out == REPROC_EPIPE){ reproc_kill(self->process); reproc_destroy(self->process); From 192a93014d077651760a8d8d20e4197aba70a421 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 27 Jun 2021 23:14:48 +0800 Subject: [PATCH 019/180] change double quotes to single quotes to reduce escaping --- src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.c b/src/main.c index f84cc3b8..53e8e364 100644 --- a/src/main.c +++ b/src/main.c @@ -172,8 +172,8 @@ init_lua: "local core\n" "xpcall(function()\n" " HOME = os.getenv('" LITE_OS_HOME "')\n" - " local exedir = EXEFILE:match(\"^(.*)" LITE_PATHSEP_PATTERN LITE_NONPATHSEP_PATTERN "$\")\n" - " local prefix = exedir:match(\"^(.*)" LITE_PATHSEP_PATTERN "bin$\")\n" + " 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.init()\n" From c7bbf221ee04345025e46d851157344ece18f8aa Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 7 Jul 2021 12:37:07 +0800 Subject: [PATCH 020/180] remove duplicated constants --- src/api/process.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/api/process.c b/src/api/process.c index f279a6cc..84d86b6e 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -389,11 +389,5 @@ int luaopen_process(lua_State *L) lua_pushnumber(L, REPROC_STREAM_ERR); lua_setfield(L, -2, "STREAM_STDERR"); - lua_pushnumber(L, REPROC_INFINITE); - lua_setfield(L, -2, "WAIT_INFINITE"); - - lua_pushnumber(L, REPROC_DEADLINE); - lua_setfield(L, -2, "WAIT_DEADLINE"); - return 1; } From 4ef707e94155d31f97bc921aef0633a6786d9b9e Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:22:55 +0800 Subject: [PATCH 021/180] add compile_commands.json to gitignore Apparently ccls needs it to work, and it's usually located in project root. I symlinked it from the build folder and now I should put it in gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c77f2fee..42b4ba06 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ subprojects/reproc lite-xl error.txt .ccls-cache +compile_commands.json From 169b8abae53da5e1c3c62133765c347e9885c213 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:28:38 +0800 Subject: [PATCH 022/180] fix number of parameters passed to self:move_towards self:move_towards(self) causes self to be passed twice, ignoring rate --- data/core/view.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/view.lua b/data/core/view.lua index 2fb431d6..24c84c8d 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -18,7 +18,7 @@ end function View:move_towards(t, k, dest, rate) if type(t) ~= "table" then - return self:move_towards(self, t, k, dest, rate) + return self:move_towards(t, k, dest, rate) end local val = t[k] if not config.transitions or math.abs(val - dest) < 0.5 then From 6bcdaa9d7a7e9f375dcb2d4a4871fd8c8bbda72c Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Fri, 9 Jul 2021 11:57:16 +0800 Subject: [PATCH 023/180] Revert "fix number of parameters passed to self:move_towards" Apparently the LSP intellisense is wrong on this one, this actually causes an infinite loop --- data/core/view.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/view.lua b/data/core/view.lua index 24c84c8d..2fb431d6 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -18,7 +18,7 @@ end function View:move_towards(t, k, dest, rate) if type(t) ~= "table" then - return self:move_towards(t, k, dest, rate) + return self:move_towards(self, t, k, dest, rate) end local val = t[k] if not config.transitions or math.abs(val - dest) < 0.5 then From 0dda252096da3c34b9f9b1eb0282c8a96a147459 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Thu, 15 Jul 2021 18:15:05 -0400 Subject: [PATCH 024/180] Reverted find fixes. --- data/core/commands/findreplace.lua | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index e5d7c70b..648c8d9e 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -10,8 +10,8 @@ local StatusView = require "core.statusview" local max_last_finds = 50 local last_finds, last_view, last_fn, last_text, last_sel -local case_insensitive = config.find_case_insensitive or false -local plain = config.find_plain or false +local case_sensitive = config.find_case_sensitive or false +local find_regex = config.find_regex or false local function doc() return last_view and last_view.doc or core.active_view.doc @@ -19,18 +19,18 @@ end local function get_find_tooltip() local rf = keymap.get_binding("find-replace:repeat-find") - local ti = keymap.get_binding("find-replace:toggle-insensitivity") - local tr = keymap.get_binding("find-replace:toggle-plain") - return (plain and "[Plain] " or "") .. - (case_insensitive and "[Insensitive] " or "") .. + local ti = keymap.get_binding("find-replace:toggle-sensitivity") + local tr = keymap.get_binding("find-replace:toggle-regex") + return (find_regex and "[Regex] " or "") .. + (case_sensitive and "[Sensitive] " or "") .. (rf and ("Press " .. rf .. " to select the next match.") or "") .. (ti and (" " .. ti .. " toggles case sensitivity.") or "") .. - (tr and (" " .. tr .. " toggles plain find.") or "") + (tr and (" " .. tr .. " toggles regex find.") or "") end local function update_preview(sel, search_fn, text) local ok, line1, col1, line2, col2 = - pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_insensitive, plain) + pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_sensitive, find_regex) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) last_view:scroll_to_line(line2, true) @@ -105,15 +105,15 @@ command.add(has_selection, { command.add("core.docview", { ["find-replace:find"] = function() - find("Find Text", function(doc, line, col, text, case_insensitive, plain) - local opt = { wrap = true, no_case = case_insensitive, regex = not plain } + find("Find Text", function(doc, line, col, text, case_sensitive, find_regex) + local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex } return search.find(doc, line, col, text, opt) end) end, ["find-replace:replace"] = function() replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new) - if plain then + if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end local result, matches = regex.gsub(regex.compile(old), text, new) @@ -150,7 +150,7 @@ 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_insensitive, plain) + local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex) if line1 then if last_view.doc ~= doc() then last_finds = {} @@ -177,14 +177,14 @@ command.add(valid_for_finding, { }) command.add("core.commandview", { - ["find-replace:toggle-insensitivity"] = function() - case_insensitive = not case_insensitive + ["find-replace:toggle-sensitivity"] = function() + case_sensitive = not case_sensitive core.status_view:show_tooltip(get_find_tooltip()) update_preview(last_sel, last_fn, last_text) end, - ["find-replace:toggle-plain"] = function() - plain = not plain + ["find-replace:toggle-regex"] = function() + find_regex = not find_regex core.status_view:show_tooltip(get_find_tooltip()) update_preview(last_sel, last_fn, last_text) end From a218a95c4535be0f2a507aa06e2564546e0d16dd Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Thu, 15 Jul 2021 18:21:54 -0400 Subject: [PATCH 025/180] Updated keys as well. --- data/core/keymap.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index c0d556b8..c402f37b 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -137,8 +137,8 @@ keymap.add_direct { ["ctrl+r"] = "find-replace:replace", ["f3"] = "find-replace:repeat-find", ["shift+f3"] = "find-replace:previous-find", - ["ctrl+i"] = "find-replace:toggle-insensitivity", - ["ctrl+shift+i"] = "find-replace:toggle-plain", + ["ctrl+i"] = "find-replace:toggle-sensitivity", + ["ctrl+shift+i"] = "find-replace:toggle-regex", ["ctrl+g"] = "doc:go-to-line", ["ctrl+s"] = "doc:save", ["ctrl+shift+s"] = "doc:save-as", From 6330f4d596d08314d0f89d4da47a4fe0bbb34b8d Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Thu, 15 Jul 2021 18:29:48 -0400 Subject: [PATCH 026/180] Allowed find to function across different views. --- data/core/commands/findreplace.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 648c8d9e..7904632a 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -14,7 +14,7 @@ local case_sensitive = config.find_case_sensitive or false local find_regex = config.find_regex or false local function doc() - return last_view and last_view.doc or core.active_view.doc + return core.active_view:is(DocView) and core.active_view.doc or last_view.doc end local function get_find_tooltip() From e144ad327150164fc9e483f89ca87f6be4c8552f Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Thu, 15 Jul 2021 20:47:44 -0400 Subject: [PATCH 027/180] Removed legacy build system. --- build.sh | 39 -------------------------------------- lib/font_renderer/build.sh | 27 -------------------------- 2 files changed, 66 deletions(-) delete mode 100755 build.sh delete mode 100755 lib/font_renderer/build.sh diff --git a/build.sh b/build.sh deleted file mode 100755 index b49dc1bf..00000000 --- a/build.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -cflags+="-Wall -O3 -g -std=gnu11 -fno-strict-aliasing -Isrc -Ilib/font_renderer" -cflags+=" $(pkg-config --cflags lua5.2) $(sdl2-config --cflags)" -lflags="-static-libgcc -static-libstdc++" -for package in libagg freetype2 lua5.2 x11 libpcre2-8 reproc; do - lflags+=" $(pkg-config --libs $package)" -done -lflags+=" $(sdl2-config --libs) -lm" - -if [[ $* == *windows* ]]; then - echo "cross compiling for windows is not yet supported" - exit 1 -else - outfile="lite-xl" - compiler="gcc" - cxxcompiler="g++" -fi - -lib/font_renderer/build.sh || exit 1 -libs=libfontrenderer.a - -echo "compiling lite-xl..." -for f in `find src -name "*.c"`; do - $compiler -c $cflags $f -o "${f//\//_}.o" - if [[ $? -ne 0 ]]; then - got_error=true - fi -done - -if [[ ! $got_error ]]; then - echo "linking..." - $cxxcompiler -o $outfile *.o $libs $lflags -fi - -echo "cleaning up..." -rm *.o *.a -echo "done" - diff --git a/lib/font_renderer/build.sh b/lib/font_renderer/build.sh deleted file mode 100755 index 364c4b6a..00000000 --- a/lib/font_renderer/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -cxxcompiler="g++" -cxxflags="-Wall -O3 -g -std=c++03 -fno-exceptions -fno-rtti -Isrc -Ilib/font_renderer" -cxxflags+=" -DFONT_RENDERER_HEIGHT_HACK" -for package in libagg freetype2; do - cxxflags+=" $(pkg-config --cflags $package)" -done - -echo "compiling font renderer library..." - -for f in `find lib -name "*.cpp"`; do - $cxxcompiler -c $cxxflags $f -o "${f//\//_}.o" - if [[ $? -ne 0 ]]; then - got_error=true - fi -done - -if [[ $got_error ]]; then - rm -f *.o - exit 1 -fi - -ar -rcs libfontrenderer.a *.o - -rm *.o -echo "font renderer library created" From d3f1a3a5b2e8a2128b550f9b7d63e21e9329220a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 17 Jul 2021 21:30:25 +0200 Subject: [PATCH 028/180] Bump 2.0-beta1 version --- 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 6090df4e..6288b877 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,5 +1,5 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "1.16.11" +VERSION = "2.0-beta1" MOD_VERSION = "1" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE From 7605b626e8f8908da7520421f3be7c0ffc6eb67f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 19 Jul 2021 08:18:38 +0200 Subject: [PATCH 029/180] Add language_cpp plugins Brought form the 1.16.12 release. It provides support for C++ using multi-part syntax patterns. Take the priority over C language plugins for header files. --- data/plugins/language_c.lua | 2 +- data/plugins/language_cpp.lua | 122 ++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 data/plugins/language_cpp.lua diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index b311884b..836e1692 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -2,7 +2,7 @@ local syntax = require "core.syntax" syntax.add { - files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, + files = { "%.c$", "%.h$", "%.inl$" }, comment = "//", patterns = { { pattern = "//.-\n", type = "comment" }, diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua new file mode 100644 index 00000000..cf3d7cd2 --- /dev/null +++ b/data/plugins/language_cpp.lua @@ -0,0 +1,122 @@ +-- mod-version:1 -- lite-xl 1.16 +pcall(require, "plugins.language_c") + +local syntax = require "core.syntax" + +syntax.add { + files = { + "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", + "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" + }, + comment = "//", + patterns = { + { pattern = "//.-\n", type = "comment" }, + { pattern = { "/%*", "%*/" }, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = "0x%x+", type = "number" }, + { pattern = "%d+[%d%.eE]*f?", type = "number" }, + { pattern = "%.?%d+f?", type = "number" }, + { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + { pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + { pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + { pattern = "[%a_][%w_]*::", type = "symbol" }, + { pattern = "::", type = "symbol" }, + { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "#include%s()<.->", type = {"keyword", "string"} }, + { pattern = "#[%a_][%w_]*", type = "keyword" }, + }, + symbols = { + ["alignof"] = "keyword", + ["alignas"] = "keyword", + ["private"] = "keyword", + ["protected"] = "keyword", + ["public"] = "keyword", + ["register"] = "keyword", + ["nullptr"] = "keyword", + ["operator"] = "keyword", + ["asm"] = "keyword", + ["catch"] = "keyword", + ["throw"] = "keyword", + ["try"] = "keyword", + ["compl"] = "keyword", + ["explicit"] = "keyword", + ["export"] = "keyword", + ["concept"] = "keyword", + ["consteval"] = "keyword", + ["constexpr"] = "keyword", + ["constinit"] = "keyword", + ["const_cast"] = "keyword", + ["dynamic_cast"] = "keyword", + ["reinterpret_cast"] = "keyword", + ["static_cast"] = "keyword", + ["static_assert"] = "keyword", + ["template"] = "keyword", + ["this"] = "keyword", + ["thread_local"] = "keyword", + ["requires"] = "keyword", + ["co_wait"] = "keyword", + ["co_return"] = "keyword", + ["co_yield"] = "keyword", + ["decltype"] = "keyword", + ["delete"] = "keyword", + ["export"] = "keyword", + ["friend"] = "keyword", + ["typeid"] = "keyword", + ["typename"] = "keyword", + ["mutable"] = "keyword", + ["override"] = "keyword", + ["virtual"] = "keyword", + ["using"] = "keyword", + ["new"] = "keyword", + ["noexcept"] = "keyword", + ["if"] = "keyword", + ["then"] = "keyword", + ["else"] = "keyword", + ["elseif"] = "keyword", + ["do"] = "keyword", + ["while"] = "keyword", + ["for"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["return"] = "keyword", + ["goto"] = "keyword", + ["typedef"] = "keyword", + ["enum"] = "keyword", + ["extern"] = "keyword", + ["static"] = "keyword", + ["volatile"] = "keyword", + ["const"] = "keyword", + ["inline"] = "keyword", + ["switch"] = "keyword", + ["case"] = "keyword", + ["default"] = "keyword", + ["auto"] = "keyword", + ["const"] = "keyword", + ["void"] = "keyword", + ["int"] = "keyword2", + ["short"] = "keyword2", + ["long"] = "keyword2", + ["float"] = "keyword2", + ["double"] = "keyword2", + ["char"] = "keyword2", + ["unsigned"] = "keyword2", + ["bool"] = "keyword2", + ["true"] = "keyword2", + ["false"] = "keyword2", + ["#include"] = "keyword", + ["#if"] = "keyword", + ["#ifdef"] = "keyword", + ["#ifndef"] = "keyword", + ["#else"] = "keyword", + ["#elseif"] = "keyword", + ["#endif"] = "keyword", + ["#define"] = "keyword", + ["#warning"] = "keyword", + ["#error"] = "keyword", + ["#pragma"] = "keyword", + }, +} + From 0777a6f0b8fd5d4392973d403b7ca6b54eee1ac4 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 20 Jul 2021 14:39:50 -0400 Subject: [PATCH 030/180] Merged dev to master. --- data/core/commands/core.lua | 15 +- data/core/commands/doc.lua | 64 +++- data/core/common.lua | 27 +- data/core/doc/init.lua | 48 ++- data/core/init.lua | 80 ++--- data/core/keymap.lua | 1 + data/core/regex.lua | 14 +- data/plugins/autocomplete.lua | 281 +++++++++++++++-- data/plugins/language_c.lua | 11 + src/api/process.c | 575 +++++++++++++++++----------------- src/api/regex.c | 8 +- 11 files changed, 747 insertions(+), 377 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 859fb066..ac30fe20 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -104,11 +104,20 @@ command.add(nil, { end, function (text) return common.home_encode_list(common.path_suggest(common.home_expand(text))) end, nil, function(text) - local path_stat, err = system.get_file_info(common.home_expand(text)) + local filename = common.home_expand(text) + local path_stat, err = system.get_file_info(filename) if err then - core.error("Cannot open file %q: %q", text, err) + if err:find("No such file", 1, true) then + -- check if the containing directory exists + local dirname = common.dirname(filename) + local dir_stat = dirname and system.get_file_info(dirname) + if not dirname or (dir_stat and dir_stat.type == 'dir') then + return true + end + end + core.error("Cannot open file %s: %s", text, err) elseif path_stat.type == 'dir' then - core.error("Cannot open %q, is a folder", text) + core.error("Cannot open %s, is a folder", text) else return true end diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index ca39bfde..5ddcd507 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -43,11 +43,67 @@ local function append_line_if_last_line(line) end local function save(filename) - doc():save(filename and core.normalize_to_project_dir(filename)) + local abs_filename + if filename then + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) + end + doc():save(filename, abs_filename) local saved_filename = doc().filename core.log("Saved \"%s\"", saved_filename) 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) + local _, e = line:find("^[ \t]+") + local soft_tab = string.rep(" ", config.indent_size) + if config.tab_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 + local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" + local number = #indent / #soft_tab + return e, indent:sub(1, + (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) + end +end + +-- un/indents text; behaviour varies based on selection and un/indent. +-- * if there's a selection, it will stay static around the +-- text for both indenting and unindenting. +-- * if you are in the beginning whitespace of a line, and are indenting, the +-- cursor will insert the exactly appropriate amount of spaces, and jump the +-- cursor to the beginning of first non whitespace characters +-- * if you are not in the beginning whitespace of a line, and you indent, it +-- inserts the appropriate whitespace, as if you typed them normally. +-- * if you are unindenting, the cursor will jump to the start of the line, +-- and remove the appropriate amount of spaces (or a tab). +local function indent_text(unindent) + local text = get_indent_string() + local line1, col1, line2, col2, swap = doc_multiline_selection(true) + local _, se = doc().lines[line1]:find("^[ \t]+") + local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) + if unindent or doc():has_selection() or in_beginning_whitespace then + local l1d, l2d = #doc().lines[line1], #doc().lines[line2] + for line = line1, line2 do + local e, rnded = get_line_indent(doc().lines[line], unindent) + doc():remove(line, 1, line, (e or 0) + 1) + doc():insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end + l1d, l2d = #doc().lines[line1] - l1d, #doc().lines[line2] - l2d + if (unindent or in_beginning_whitespace) and not doc():has_selection() then + local start_cursor = (se and se + 1 or 1) + l1d or #(doc().lines[line1]) + doc():set_selection(line1, start_cursor, line2, start_cursor, swap) + else + doc():set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) + end + else + doc():text_input(text) + end +end + local function cut_or_copy(delete) local full_text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do @@ -363,12 +419,14 @@ local commands = { end core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - doc():save(filename) + save(common.home_expand(filename)) core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) if filename ~= old_filename then os.remove(old_filename) end - end, common.path_suggest) + end, function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end) end, diff --git a/data/core/common.lua b/data/core/common.lua index 6b9dff7d..ce450570 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -230,6 +230,12 @@ function common.basename(path) end +-- can return nil if there is no directory part in the path +function common.dirname(path) + return path:match("(.+)[\\/][^\\/]+$") +end + + function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 @@ -279,8 +285,27 @@ local function split_on_slash(s, sep_pattern) end +function common.normalize_path(filename) + if PATHSEP == '\\' then + filename = filename:gsub('[/\\]', '\\') + local drive, rem = filename:match('^([a-zA-Z])(:.*)') + filename = drive and drive:upper() .. rem or filename + end + local parts = split_on_slash(filename, PATHSEP) + local accu = {} + for _, part in ipairs(parts) do + if part == '..' then + table.remove(accu) + elseif part ~= '.' then + table.insert(accu, part) + end + end + return table.concat(accu, PATHSEP) +end + + function common.path_belongs_to(filename, path) - return filename and string.find(filename, path .. PATHSEP, 1, true) == 1 + return string.find(filename, path .. PATHSEP, 1, true) == 1 end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index c33ade5a..652b5545 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -17,10 +17,34 @@ local function split_lines(text) return res end -function Doc:new(filename) + +local function splice(t, at, remove, insert) + insert = insert or {} + local offset = #insert - remove + local old_len = #t + if offset < 0 then + for i = at - offset, old_len - offset do + t[i + offset] = t[i] + end + elseif offset > 0 then + for i = old_len, at, -1 do + t[i + offset] = t[i] + end + end + for i, item in ipairs(insert) do + t[at + i - 1] = item + end +end + + +function Doc:new(filename, abs_filename, new_file) + self.new_file = new_file self:reset() if filename then - self:load(filename) + self:set_filename(filename, abs_filename) + if not new_file then + self:load(filename) + end end end @@ -47,16 +71,15 @@ function Doc:reset_syntax() end -function Doc:set_filename(filename) +function Doc:set_filename(filename, abs_filename) self.filename = filename - self.abs_filename = system.absolute_path(filename) + self.abs_filename = abs_filename end function Doc:load(filename) local fp = assert( io.open(filename, "rb") ) self:reset() - self:set_filename(filename) self.lines = {} for line in fp:lines() do if line:byte(-1) == 13 then @@ -73,17 +96,20 @@ function Doc:load(filename) end -function Doc:save(filename) - filename = filename or assert(self.filename, "no filename set to default to") +function Doc:save(filename, abs_filename) + if not filename then + assert(self.filename, "no filename set to default to") + filename = self.filename + abs_filename = self.abs_filename + end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) end fp:close() - if filename then - self:set_filename(filename) - end + self:set_filename(filename, abs_filename) + self.new_file = false self:reset_syntax() self:clean() end @@ -95,7 +121,7 @@ end function Doc:is_dirty() - return self.clean_change_id ~= self:get_change_id() + return self.clean_change_id ~= self:get_change_id() or self.new_file end diff --git a/data/core/init.lua b/data/core/init.lua index c13151ad..9bdbb36c 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -620,24 +620,6 @@ do end --- DEPRECATED function -core.doc_save_hooks = {} -function core.add_save_hook(fn) - core.error("The function core.add_save_hook is deprecated." .. - " Modules should now directly override the Doc:save function.") - core.doc_save_hooks[#core.doc_save_hooks + 1] = fn -end - - --- DEPRECATED function -function core.on_doc_save(filename) - -- for backward compatibility in modules. Hooks are deprecated, the function Doc:save - -- should be directly overidded. - for _, hook in ipairs(core.doc_save_hooks) do - hook(filename) - end -end - local function quit_with_function(quit_fn, force) if force then delete_temp_files() @@ -695,24 +677,27 @@ function core.load_plugins() userdir = {dir = USERDIR, plugins = {}}, datadir = {dir = DATADIR, plugins = {}}, } - for _, root_dir in ipairs {USERDIR, DATADIR} do + local files = {} + for _, root_dir in ipairs {DATADIR, USERDIR} do local plugin_dir = root_dir .. "/plugins" - local files = system.list_dir(plugin_dir) - for _, filename in ipairs(files or {}) do - local basename = filename:match("(.-)%.lua$") or filename - local version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if not version_match then - core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins - ls[#ls + 1] = filename - end - if version_match and config.plugins[basename] ~= false then - local modname = "plugins." .. basename - local ok = core.try(require, modname) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end - if not ok then - no_errors = false - end + for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do + files[filename] = plugin_dir -- user plugins will always replace system plugins + end + end + + for filename, plugin_dir in pairs(files) do + local basename = filename:match("(.-)%.lua$") or filename + local version_match = check_plugin_version(plugin_dir .. '/' .. filename) + if not version_match then + core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) + local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins + table.insert(list, filename) + end + if version_match and core.plugins[basename] ~= false then + local ok = core.try(require, "plugins." .. basename) + if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + if not ok then + no_errors = false end end end @@ -809,10 +794,30 @@ function core.normalize_to_project_dir(filename) end +-- The function below works like system.absolute_path except it +-- doesn't fail if the file does not exist. We consider that the +-- current dir is core.project_dir so relative filename are considered +-- to be in core.project_dir. +-- Please note that .. or . in the filename are not taken into account. +-- This function should get only filenames normalized using +-- common.normalize_path function. +function core.project_absolute_path(filename) + if filename:match('^%a:\\') or filename:find('/', 1, true) then + return filename + else + return core.project_dir .. PATHSEP .. filename + end +end + + function core.open_doc(filename) + local new_file = not filename or not system.get_file_info(filename) + local abs_filename if filename then + -- normalize filename and set absolute filename then -- try to find existing doc for filename - local abs_filename = system.absolute_path(filename) + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) for _, doc in ipairs(core.docs) do if doc.abs_filename and abs_filename == doc.abs_filename then return doc @@ -820,8 +825,7 @@ function core.open_doc(filename) end end -- no existing doc for filename; create new - filename = filename and core.normalize_to_project_dir(filename) - local doc = Doc(filename) + local doc = Doc(filename, abs_filename, new_file) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) return doc diff --git a/data/core/keymap.lua b/data/core/keymap.lua index c402f37b..86be82b1 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -108,6 +108,7 @@ keymap.add_direct { ["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", + ["f11"] = "core:toggle-fullscreen", ["alt+shift+j"] = "root:split-left", ["alt+shift+l"] = "root:split-right", diff --git a/data/core/regex.lua b/data/core/regex.lua index 19c59164..19306e04 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -1,5 +1,5 @@ --- So that in addition to regex.gsub(pattern, string), we can also do +-- 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 @@ -9,7 +9,7 @@ regex.match = function(pattern_string, string, offset, options) return regex.cmatch(pattern, string, offset or 1, options or 0) end --- Will iterate back through any UTF-8 bytes so that we don't replace bits +-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- mid character. local function previous_character(str, index) local byte @@ -32,7 +32,7 @@ end -- Build off matching. For now, only support basic replacements, but capture -- groupings should be doable. We can even have custom group replacements and --- transformations and stuff in lua. Currently, this takes group replacements +-- transformations and stuff in lua. Currently, this takes group replacements -- as \1 - \9. -- Should work on UTF-8 text. regex.gsub = function(pattern_string, str, replacement) @@ -48,8 +48,8 @@ regex.gsub = function(pattern_string, str, replacement) if #indices > 2 then for i = 1, (#indices/2 - 1) do currentReplacement = string.gsub( - currentReplacement, - "\\" .. i, + currentReplacement, + "\\" .. i, str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) ) end @@ -57,10 +57,10 @@ regex.gsub = function(pattern_string, str, replacement) currentReplacement = string.gsub(currentReplacement, "\\%d", "") table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) if indices[1] > 1 then - result = result .. + result = result .. str:sub(1, previous_character(str, indices[1])) .. currentReplacement else - result = result .. currentReplacement + result = result .. currentReplacement end str = str:sub(indices[2]) end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 867d5360..4703a63a 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -8,25 +8,65 @@ local keymap = require "core.keymap" local translate = require "core.doc.translate" local RootView = require "core.rootview" local DocView = require "core.docview" +local Doc = require "core.doc" -config.plugins.autocomplete = { max_suggestions = 6 } +config.plugins.autocomplete = { + -- Amount of characters that need to be written for autocomplete + min_len = 1 + -- The max amount of visible items + max_height = 6 + -- The max amount of scrollable items + max_suggestions = 100 +} local autocomplete = {} -autocomplete.map = {} +autocomplete.map = {} +autocomplete.map_manually = {} +autocomplete.on_close = nil + +-- Flag that indicates if the autocomplete box was manually triggered +-- with the autocomplete.complete() function to prevent the suggestions +-- from getting cluttered with arbitrary document symbols by using the +-- autocomplete.map_manually table. +local triggered_manually = false local mt = { __tostring = function(t) return t.text end } -function autocomplete.add(t) +function autocomplete.add(t, triggered_manually) local items = {} for text, info in pairs(t.items) do - info = (type(info) == "string") and info - table.insert(items, setmetatable({ text = text, info = info }, mt)) + if type(info) == "table" then + table.insert( + items, + setmetatable( + { + text = text, + info = info.info, + desc = info.desc, -- Description shown on item selected + cb = info.cb, -- A callback called once when item is selected + data = info.data -- Optional data that can be used on cb + }, + mt + ) + ) + else + info = (type(info) == "string") and info + table.insert(items, setmetatable({ text = text, info = info }, mt)) + end + end + + if not triggered_manually then + autocomplete.map[t.name] = { files = t.files or ".*", items = items } + else + autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items } end - autocomplete.map[t.name] = { files = t.files or ".*", items = items } end -local max_symbols = config.max_symbols or 2000 +-- +-- Thread that scans open document symbols and cache them +-- +local max_symbols = config.max_symbols core.add_thread(function() local cache = setmetatable({}, { __mode = "k" }) @@ -109,16 +149,39 @@ local last_line, last_col local function reset_suggestions() suggestions_idx = 1 suggestions = {} + + triggered_manually = false + + local doc = core.active_view.doc + if autocomplete.on_close then + autocomplete.on_close(doc, suggestions[suggestions_idx]) + autocomplete.on_close = nil + end end +local function in_table(value, table_array) + for i, element in pairs(table_array) do + if element == value then + return true + end + end + + return false +end local function update_suggestions() local doc = core.active_view.doc local filename = doc and doc.filename or "" + local map = autocomplete.map + + if triggered_manually then + map = autocomplete.map_manually + end + -- get all relevant suggestions for given filename local items = {} - for _, v in pairs(autocomplete.map) do + for _, v in pairs(map) do if common.match_pattern(filename, v.files) then for _, item in pairs(v.items) do table.insert(items, item) @@ -138,7 +201,6 @@ local function update_suggestions() end end - local function get_partial_symbol() local doc = core.active_view.doc local line2, col2 = doc:get_selection() @@ -146,14 +208,12 @@ local function get_partial_symbol() return doc:get_text(line1, col1, line2, col2) end - local function get_active_view() if getmetatable(core.active_view) == DocView then return core.active_view end end - local function get_suggestions_rect(av) if #suggestions == 0 then return 0, 0, 0, 0 @@ -175,15 +235,67 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) end + local ah = config.plugins.autocomplete.max_height + + local max_items = #suggestions + if max_items > ah then + max_items = ah + end + + -- additional line to display total items + max_items = max_items + 1 + + if max_width < 150 then + max_width = 150 + end + return x - style.padding.x, y - style.padding.y, max_width + style.padding.x * 2, - #suggestions * (th + style.padding.y) + style.padding.y + max_items * (th + style.padding.y) + style.padding.y end +local function draw_description_box(text, av, sx, sy, sw, sh) + local width = 0 + + local lines = {} + for line in string.gmatch(text.."\n", "(.-)\n") do + width = math.max(width, style.font:get_width(line)) + table.insert(lines, line) + end + + local height = #lines * style.font:get_height() + + -- draw background rect + renderer.draw_rect( + sx + sw + style.padding.x / 4, + sy, + width + style.padding.x * 2, + height + style.padding.y * 2, + style.background3 + ) + + -- draw text + local lh = style.font:get_height() + local y = sy + style.padding.y + local x = sx + sw + style.padding.x / 4 + + for _, line in pairs(lines) do + common.draw_text( + style.font, style.text, line, "left", x + style.padding.x, y, width, lh + ) + y = y + lh + end +end local function draw_suggestions_box(av) + if #suggestions <= 0 then + return + end + + local ah = config.plugins.autocomplete.max_height + -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) renderer.draw_rect(rx, ry, rw, rh, style.background3) @@ -192,7 +304,14 @@ local function draw_suggestions_box(av) local font = av:get_font() local lh = font:get_height() + style.padding.y local y = ry + style.padding.y / 2 - for i, s in ipairs(suggestions) do + local show_count = #suggestions <= ah and #suggestions or ah + local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1 + + for i=start_index, start_index+show_count-1, 1 do + if not suggestions[i] then + break + end + local s = suggestions[i] local color = (i == suggestions_idx) and style.accent or style.text common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) if s.info then @@ -200,26 +319,55 @@ local function draw_suggestions_box(av) common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) end y = y + lh + if suggestions_idx == i then + if s.cb then + s.cb(suggestions_idx, s) + s.cb = nil + s.data = nil + end + if s.desc and #s.desc > 0 then + draw_description_box(s.desc, av, rx, ry, rw, rh) + end + end end + + renderer.draw_rect(rx, y, rw, 2, style.caret) + renderer.draw_rect(rx, y+2, rw, lh, style.background) + common.draw_text( + style.font, + style.accent, + "Items", + "left", + rx + style.padding.x, y, rw, lh + ) + common.draw_text( + style.font, + style.accent, + tostring(suggestions_idx) .. "/" .. tostring(#suggestions), + "right", + rx, y, rw - style.padding.x, lh + ) end - --- patch event logic into RootView -local on_text_input = RootView.on_text_input -local update = RootView.update -local draw = RootView.draw - - -RootView.on_text_input = function(...) - on_text_input(...) - +local function show_autocomplete() local av = get_active_view() if av then -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= 3 then + + if #partial >= config.plugins.autocomplete.min_len or triggered_manually then update_suggestions() - last_line, last_col = av.doc:get_selection() + + if not triggered_manually then + last_line, last_col = av.doc:get_selection() + else + local line, col = av.doc:get_selection() + local char = av.doc:get_char(line, col-1, line, col-1) + + if char:match("%s") or (char:match("%p") and col ~= last_col) then + reset_suggestions() + end + end else reset_suggestions() end @@ -233,6 +381,30 @@ RootView.on_text_input = function(...) end end +-- +-- Patch event logic into RootView and Doc +-- +local on_text_input = RootView.on_text_input +local on_text_remove = Doc.remove +local update = RootView.update +local draw = RootView.draw + +RootView.on_text_input = function(...) + on_text_input(...) + show_autocomplete() +end + +Doc.remove = function(self, line1, col1, line2, col2) + on_text_remove(self, line1, col1, line2, col2) + + if triggered_manually and line1 == line2 then + if last_col >= col1 then + reset_suggestions() + else + show_autocomplete() + end + end +end RootView.update = function(...) update(...) @@ -241,13 +413,19 @@ RootView.update = function(...) if av then -- reset suggestions if caret was moved local line, col = av.doc:get_selection() - if line ~= last_line or col ~= last_col then - reset_suggestions() + + if not triggered_manually then + if line ~= last_line or col ~= last_col then + reset_suggestions() + end + else + if line ~= last_line or col < last_col then + reset_suggestions() + end end end end - RootView.draw = function(...) draw(...) @@ -258,12 +436,53 @@ RootView.draw = function(...) end end +-- +-- Public functions +-- +function autocomplete.open(on_close) + triggered_manually = true + if on_close then + autocomplete.on_close = on_close + end + + local av = get_active_view() + last_line, last_col = av.doc:get_selection() + update_suggestions() +end + +function autocomplete.close() + reset_suggestions() +end + +function autocomplete.is_open() + return #suggestions > 0 +end + +function autocomplete.complete(completions, on_close) + reset_suggestions() + + autocomplete.map_manually = {} + autocomplete.add(completions, true) + + autocomplete.open(on_close) +end + +function autocomplete.can_complete() + if #partial >= config.plugins.autocomplete.min_len then + return true + end + return false +end + + +-- +-- Commands +-- local function predicate() return get_active_view() and #suggestions > 0 end - command.add(predicate, { ["autocomplete:complete"] = function() local doc = core.active_view.doc @@ -288,7 +507,9 @@ command.add(predicate, { end, }) - +-- +-- Keymaps +-- keymap.add { ["tab"] = "autocomplete:complete", ["up"] = "autocomplete:previous", diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 836e1692..f55140c9 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -55,6 +55,17 @@ syntax.add { ["true"] = "literal", ["false"] = "literal", ["NULL"] = "literal", + ["#include"] = "keyword", + ["#if"] = "keyword", + ["#ifdef"] = "keyword", + ["#ifndef"] = "keyword", + ["#else"] = "keyword", + ["#elseif"] = "keyword", + ["#endif"] = "keyword", + ["#define"] = "keyword", + ["#warning"] = "keyword", + ["#error"] = "keyword", + ["#pragma"] = "keyword", }, } diff --git a/src/api/process.c b/src/api/process.c index 84d86b6e..4b018e4c 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -10,28 +10,166 @@ #include #include "api.h" -#define READ_BUF_SIZE 4096 +#define READ_BUF_SIZE 2048 + +#define L_GETTABLE(L, idx, key, conv, def) ( \ + lua_getfield(L, idx, key), \ + conv(L, -1, def) \ +) + +#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def) +#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def) + +#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) + +#define L_RETURN_REPROC_ERROR(L, code) { \ + lua_pushnil(L); \ + lua_pushstring(L, reproc_strerror(code)); \ + lua_pushnumber(L, code); \ + return 3; \ +} + +#define ASSERT_MALLOC(ptr) \ + if (ptr == NULL) \ + L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM) + +#define ASSERT_REPROC_ERRNO(L, code) { \ + if (code < 0) \ + L_RETURN_REPROC_ERROR(L, code) \ +} typedef struct { reproc_t * process; - lua_State* L; - + bool running; + int returncode; } process_t; -static int process_new(lua_State* L) +// this function should be called instead of reproc_wait +static int poll_process(process_t* proc, int timeout) { - process_t* self = (process_t*) lua_newuserdata( - L, sizeof(process_t) + int ret = reproc_wait(proc->process, timeout); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } + return ret; +} + +static int kill_process(process_t* proc) +{ + int ret = reproc_stop( + proc->process, + (reproc_stop_actions) { + {REPROC_STOP_KILL, 0}, + {REPROC_STOP_TERMINATE, 0}, + {REPROC_STOP_NOOP, 0} + } ); - memset(self, 0, sizeof(process_t)); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } - self->process = NULL; - self->L = L; + return ret; +} - luaL_getmetatable(L, API_TYPE_PROCESS); - lua_setmetatable(L, -2); +static int process_start(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + if (lua_isnoneornil(L, 2)) { + lua_settop(L, 1); // remove the nil if it's there + lua_newtable(L); + } + luaL_checktype(L, 2, LUA_TTABLE); + int cmd_len = lua_rawlen(L, 1); + const char** cmd = malloc(sizeof(char *) * (cmd_len + 1)); + ASSERT_MALLOC(cmd); + cmd[cmd_len] = NULL; + + for(int i = 0; i < cmd_len; i++) { + lua_rawgeti(L, 1, i + 1); + + cmd[i] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + + int deadline = L_GETNUM(L, 2, "timeout", 0); + const char* cwd =L_GETSTR(L, 2, "cwd", NULL); + int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT); + int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT); + int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT); + lua_pop(L, 5); // remove args we just read + + if ( + redirect_in > REPROC_REDIRECT_STDOUT + || redirect_out > REPROC_REDIRECT_STDOUT + || redirect_err > REPROC_REDIRECT_STDOUT) + { + lua_pushnil(L); + lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported"); + return 2; + } + + // env + luaL_getsubtable(L, 2, "env"); + const char **env = NULL; + int env_len = 0; + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + env_len++; + lua_pop(L, 1); + } + + if (env_len > 0) { + env = malloc(sizeof(char*) * (env_len + 1)); + env[env_len] = NULL; + + int i = 0; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + lua_pushliteral(L, "="); + lua_pushvalue(L, -3); // push the key to the top + lua_concat(L, 3); // key=value + + env[i++] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + + reproc_t* proc = reproc_new(); + int out = reproc_start( + proc, + (const char* const*) cmd, + (reproc_options) { + .working_directory = cwd, + .deadline = deadline, + .nonblocking = true, + .env = { + .behavior = REPROC_ENV_EXTEND, + .extra = env + }, + .redirect = { + .in.type = redirect_in, + .out.type = redirect_out, + .err.type = redirect_err + } + } + ); + + if (out < 0) { + reproc_destroy(proc); + L_RETURN_REPROC_ERROR(L, out); + } + + process_t* self = lua_newuserdata(L, sizeof(process_t)); + self->process = proc; + self->running = true; + + // this is equivalent to using lua_setmetatable() + luaL_setmetatable(L, API_TYPE_PROCESS); return 1; } @@ -39,24 +177,20 @@ static int process_strerror(lua_State* L) { int error_code = luaL_checknumber(L, 1); - if(error_code){ - lua_pushstring( - L, - reproc_strerror(error_code) - ); - } else { + if (error_code < 0) + lua_pushstring(L, reproc_strerror(error_code)); + else lua_pushnil(L); - } - + return 1; } -static int process_gc(lua_State* L) +static int f_gc(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - reproc_kill(self->process); + if(self->process) { + kill_process(self); reproc_destroy(self->process); self->process = NULL; } @@ -64,330 +198,211 @@ static int process_gc(lua_State* L) return 0; } -static int process_start(lua_State* L) +static int f_tostring(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - luaL_checktype(L, 2, LUA_TTABLE); - - char* path = NULL; - size_t path_len = 0; - - if(lua_type(L, 3) == LUA_TSTRING){ - path = (char*) lua_tolstring(L, 3, &path_len); - } - - size_t deadline = 0; - - if(lua_type(L, 4) == LUA_TNUMBER){ - deadline = lua_tonumber(L, 4); - } - - size_t table_len = luaL_len(L, 2); - char* command[table_len+1]; - command[table_len] = NULL; - - int i; - for(i=1; i<=table_len; i++){ - lua_pushnumber(L, i); - lua_gettable(L, 2); - - command[i-1] = (char*) lua_tostring(L, -1); - - lua_remove(L, -1); - } - - if(self->process){ - reproc_kill(self->process); - reproc_destroy(self->process); - } - - self->process = reproc_new(); - - int out = reproc_start( - self->process, - (const char* const*) command, - (reproc_options){ - .working_directory = path, - .deadline = deadline, - .nonblocking=true, - .redirect.err.type=REPROC_REDIRECT_PIPE - } - ); - - if(out > 0) { - lua_pushboolean(L, 1); - } - else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushnumber(L, out); - } + luaL_checkudata(L, 1, API_TYPE_PROCESS); + lua_pushliteral(L, API_TYPE_PROCESS); return 1; } -static int process_pid(lua_State* L) +static int f_pid(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - int id = reproc_pid(self->process); - - if(id > 0){ - lua_pushnumber(L, id); - } else { - lua_pushnumber(L, 0); - } - } else { - lua_pushnumber(L, 0); - } + lua_pushnumber(L, reproc_pid(self->process)); + return 1; +} + +static int f_returncode(lua_State *L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + int ret = poll_process(self, 0); + + if (self->running) + lua_pushnil(L); + else + lua_pushnumber(L, ret); return 1; } static int g_read(lua_State* L, int stream) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE); - if(self->process){ - int read_size = READ_BUF_SIZE; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } + luaL_Buffer b; + uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size); - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } + int out = reproc_read( + self->process, + stream, + buffer, + read_size + ); - int out = 0; - uint8_t buffer[read_size]; + if (out >= 0) + luaL_addsize(&b, out); + luaL_pushresult(&b); - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_OUT, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - - lua_pushnil(L); - } else if(out > 0) { - lua_pushlstring(L, (const char*) buffer, out); - } else { - lua_pushnil(L); - } - } else { - lua_pushnil(L); + if (out == REPROC_EPIPE) { + kill_process(self); + ASSERT_REPROC_ERRNO(L, out); } return 1; } -static int process_read(lua_State* L) +static int f_read_stdout(lua_State* L) { return g_read(L, REPROC_STREAM_OUT); } -static int process_read_errors(lua_State* L) +static int f_read_stderr(lua_State* L) { return g_read(L, REPROC_STREAM_ERR); } -static int process_write(lua_State* L) +static int f_read(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + int stream = luaL_checknumber(L, 2); + lua_remove(L, 2); + if (stream > REPROC_STREAM_ERR) + L_RETURN_REPROC_ERROR(L, REPROC_EINVAL); - if(self->process){ - size_t data_size = 0; - const char* data = luaL_checklstring(L, 2, &data_size); + return g_read(L, stream); +} - int out = 0; +static int f_write(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - out = reproc_write( - self->process, - (uint8_t*) data, - data_size - ); + size_t data_size = 0; + const char* data = luaL_checklstring(L, 2, &data_size); - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EPIPE); + int out = reproc_write( + self->process, + (uint8_t*) data, + data_size + ); + if (out == REPROC_EPIPE) { + kill_process(self); + L_RETURN_REPROC_ERROR(L, out); } + lua_pushnumber(L, out); + return 1; +} + +static int f_close_stream(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int stream = luaL_checknumber(L, 2); + int out = reproc_close(self->process, stream); + ASSERT_REPROC_ERRNO(L, out); + + lua_pushboolean(L, 1); + return 1; +} + +static int f_wait(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int timeout = luaL_optnumber(L, 2, 0); + + int ret = poll_process(self, timeout); + // negative returncode is also used for signals on POSIX + if (ret == REPROC_ETIMEDOUT) + L_RETURN_REPROC_ERROR(L, ret); + + lua_pushnumber(L, ret); + return 1; +} + +static int f_terminate(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int out = reproc_terminate(self->process); + ASSERT_REPROC_ERRNO(L, out); + + poll_process(self, 0); + lua_pushboolean(L, 1); + return 1; } -static int process_close_stream(lua_State* L) +static int f_kill(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - size_t stream = luaL_checknumber(L, 2); + int out = reproc_kill(self->process); + ASSERT_REPROC_ERRNO(L, out); - int out = reproc_close(self->process, stream); - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + poll_process(self, 0); + lua_pushboolean(L, 1); return 1; } -static int process_wait(lua_State* L) +static int f_running(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - size_t timeout = luaL_checknumber(L, 2); - - int out = reproc_wait(self->process, timeout); - - if(out >= 0){ - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + poll_process(self, 0); + lua_pushboolean(L, self->running); return 1; } -static int process_terminate(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_terminate(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_kill(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_kill(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_running(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - lua_pushboolean(L, 1); - } else { - lua_pushboolean(L, 0); - } - - return 1; -} - -static const struct luaL_Reg process_methods[] = { - { "__gc", process_gc}, +static const struct luaL_Reg lib[] = { {"start", process_start}, - {"pid", process_pid}, - {"read", process_read}, - {"read_errors", process_read_errors}, - {"write", process_write}, - {"close_stream", process_close_stream}, - {"wait", process_wait}, - {"terminate", process_terminate}, - {"kill", process_kill}, - {"running", process_running}, - {NULL, NULL} -}; - -static const struct luaL_Reg process[] = { - {"new", process_new}, {"strerror", process_strerror}, - {"ERROR_PIPE", NULL}, - {"ERROR_WOULDBLOCK", NULL}, - {"ERROR_TIMEDOUT", NULL}, - {"ERROR_INVALID", NULL}, - {"STREAM_STDIN", NULL}, - {"STREAM_STDOUT", NULL}, - {"STREAM_STDERR", NULL}, - {"WAIT_INFINITE", NULL}, - {"WAIT_DEADLINE", NULL}, + {"__gc", f_gc}, + {"__tostring", f_tostring}, + {"pid", f_pid}, + {"returncode", f_returncode}, + {"read", f_read}, + {"read_stdout", f_read_stdout}, + {"read_stderr", f_read_stderr}, + {"write", f_write}, + {"close_stream", f_close_stream}, + {"wait", f_wait}, + {"terminate", f_terminate}, + {"kill", f_kill}, + {"running", f_running}, {NULL, NULL} }; int luaopen_process(lua_State *L) { luaL_newmetatable(L, API_TYPE_PROCESS); - luaL_setfuncs(L, process_methods, 0); + luaL_setfuncs(L, lib, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); - luaL_newlib(L, process); + // constants + L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL); + L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT); + L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE); + L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM); + L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK); - lua_pushnumber(L, REPROC_EPIPE); - lua_setfield(L, -2, "ERROR_PIPE"); + L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE); + L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE); - lua_pushnumber(L, REPROC_EWOULDBLOCK); - lua_setfield(L, -2, "ERROR_WOULDBLOCK"); + L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN); + L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT); + L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR); - lua_pushnumber(L, REPROC_ETIMEDOUT); - lua_setfield(L, -2, "ERROR_TIMEDOUT"); - - lua_pushnumber(L, REPROC_EINVAL); - lua_setfield(L, -2, "ERROR_INVALID"); - - lua_pushnumber(L, REPROC_STREAM_IN); - lua_setfield(L, -2, "STREAM_STDIN"); - - lua_pushnumber(L, REPROC_STREAM_OUT); - lua_setfield(L, -2, "STREAM_STDOUT"); - - lua_pushnumber(L, REPROC_STREAM_ERR); - lua_setfield(L, -2, "STREAM_STDERR"); + L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT); + L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE); + L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT); + L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD); + L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT); return 1; } diff --git a/src/api/regex.c b/src/api/regex.c index a5d17604..1043b1c5 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -74,11 +74,11 @@ static int f_pcre_match(lua_State *L) { } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); if (ovector[0] > ovector[1]) { - /* We must guard against patterns such as /(?=.\K)/ that use \K in an + /* We must guard against patterns such as /(?=.\K)/ that use \K in an assertion to set the start of a match later than its end. In the editor, we just detect this case and give up. */ luaL_error(L, "regex matching error: \\K was used in an assertion to " - " set the match start after its end"); + " set the match start after its end"); pcre2_match_data_free(md); return 0; } @@ -103,8 +103,8 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, LUA_REGISTRYINDEX, "regex"); lua_pushnumber(L, PCRE2_ANCHORED); lua_setfield(L, -2, "ANCHORED"); - lua_pushnumber(L, PCRE2_ANCHORED) ; - lua_setfield(L, -2, "ENDANCHORED"); + lua_pushnumber(L, PCRE2_ANCHORED) ; + lua_setfield(L, -2, "ENDANCHORED"); lua_pushnumber(L, PCRE2_NOTBOL); lua_setfield(L, -2, "NOTBOL"); lua_pushnumber(L, PCRE2_NOTEOL); From c461cfae935004615d85c23f722128bc41fa5e52 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 20 Jul 2021 14:50:40 -0400 Subject: [PATCH 031/180] Removed unecessary duplicates. --- data/core/commands/doc.lua | 51 -------------------------------------- data/core/common.lua | 10 -------- data/core/doc/init.lua | 19 -------------- 3 files changed, 80 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 5ddcd507..60a913ce 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -53,57 +53,6 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) 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) - local _, e = line:find("^[ \t]+") - local soft_tab = string.rep(" ", config.indent_size) - if config.tab_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 - local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" - local number = #indent / #soft_tab - return e, indent:sub(1, - (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) - end -end - --- un/indents text; behaviour varies based on selection and un/indent. --- * if there's a selection, it will stay static around the --- text for both indenting and unindenting. --- * if you are in the beginning whitespace of a line, and are indenting, the --- cursor will insert the exactly appropriate amount of spaces, and jump the --- cursor to the beginning of first non whitespace characters --- * if you are not in the beginning whitespace of a line, and you indent, it --- inserts the appropriate whitespace, as if you typed them normally. --- * if you are unindenting, the cursor will jump to the start of the line, --- and remove the appropriate amount of spaces (or a tab). -local function indent_text(unindent) - local text = get_indent_string() - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - local _, se = doc().lines[line1]:find("^[ \t]+") - local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) - if unindent or doc():has_selection() or in_beginning_whitespace then - local l1d, l2d = #doc().lines[line1], #doc().lines[line2] - for line = line1, line2 do - local e, rnded = get_line_indent(doc().lines[line], unindent) - doc():remove(line, 1, line, (e or 0) + 1) - doc():insert(line, 1, - unindent and rnded:sub(1, #rnded - #text) or rnded .. text) - end - l1d, l2d = #doc().lines[line1] - l1d, #doc().lines[line2] - l2d - if (unindent or in_beginning_whitespace) and not doc():has_selection() then - local start_cursor = (se and se + 1 or 1) + l1d or #(doc().lines[line1]) - doc():set_selection(line1, start_cursor, line2, start_cursor, swap) - else - doc():set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) - end - else - doc():text_input(text) - end -end - local function cut_or_copy(delete) local full_text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do diff --git a/data/core/common.lua b/data/core/common.lua index ce450570..4b16ed1f 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -263,16 +263,6 @@ function common.home_expand(text) end -function common.normalize_path(filename) - if PATHSEP == '\\' then - filename = filename:gsub('[/\\]', '\\') - local drive, rem = filename:match('^([a-zA-Z])(:.*)') - return drive and drive:upper() .. rem or filename - end - return filename -end - - local function split_on_slash(s, sep_pattern) local t = {} if s:match("^[/\\]") then diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 652b5545..52f2113c 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -18,25 +18,6 @@ local function split_lines(text) end -local function splice(t, at, remove, insert) - insert = insert or {} - local offset = #insert - remove - local old_len = #t - if offset < 0 then - for i = at - offset, old_len - offset do - t[i + offset] = t[i] - end - elseif offset > 0 then - for i = old_len, at, -1 do - t[i + offset] = t[i] - end - end - for i, item in ipairs(insert) do - t[at + i - 1] = item - end -end - - function Doc:new(filename, abs_filename, new_file) self.new_file = new_file self:reset() From 152fd6c66c6b8c9d8ac0e6deba3138f328237c06 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 20 Jul 2021 15:09:14 -0400 Subject: [PATCH 032/180] Fixed doc, and fixed plugin load. --- data/core/init.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 9bdbb36c..c68c487b 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -342,11 +342,11 @@ local style = require "core.style" -- enable or disable plugin loading setting config entries: --- enable trimwhitespace, otherwise it is disable by default: --- config.trimwhitespace = true +-- enable plugins.trimwhitespace, otherwise it is disable by default: +-- config.plugins.trimwhitespace = true -- -- disable detectindent, otherwise it is enabled by default --- config.detectindent = false +-- config.plugins.detectindent = false ]]) init_file:close() end @@ -693,7 +693,7 @@ function core.load_plugins() local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins table.insert(list, filename) end - if version_match and core.plugins[basename] ~= false then + if version_match and config.plugins[basename] ~= false then local ok = core.try(require, "plugins." .. basename) if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end if not ok then From 2df363747bd13e1889026c3e5a6d6c9e1ab955a5 Mon Sep 17 00:00:00 2001 From: ep Date: Thu, 22 Jul 2021 15:32:22 +0700 Subject: [PATCH 033/180] fix workspace folders on different drives in Windows --- data/core/common.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/core/common.lua b/data/core/common.lua index 6b9dff7d..0c1cf0e3 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -285,6 +285,12 @@ end function common.relative_path(ref_dir, dir) + if #ref_dir>2 and ref_dir:sub(2,2)==':' + and #dir>2 and dir:sub(2,2)==':' + and ref_dir:sub(1,1)~=dir:sub(1,1) then + -- Windows, different drives, system.absolute_path fails for C:\..\D:\ + return dir + end local ref_ls = split_on_slash(ref_dir) local dir_ls = split_on_slash(dir) local i = 1 From af22a6a824c9e3dfefe64328853b4e9d87cbf60e Mon Sep 17 00:00:00 2001 From: ep Date: Mon, 26 Jul 2021 10:22:56 +0700 Subject: [PATCH 034/180] +readability, hopefully --- data/core/common.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 0c1cf0e3..bd6d52e7 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -285,9 +285,9 @@ end function common.relative_path(ref_dir, dir) - if #ref_dir>2 and ref_dir:sub(2,2)==':' - and #dir>2 and dir:sub(2,2)==':' - and ref_dir:sub(1,1)~=dir:sub(1,1) then + local drive_pattern = "^(%a):\\" + local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern) + if drive and ref_drive and drive ~= ref_drive then -- Windows, different drives, system.absolute_path fails for C:\..\D:\ return dir end From 8103f219918c3b72499e241dffe0ce510e8c3287 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 27 Jul 2021 23:16:33 +0200 Subject: [PATCH 035/180] Only load plugins that are lua files Before trying to load a plugin or checking its version verify if it looks like a lua file. Close issue #349. --- data/core/init.lua | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index c13151ad..caac1737 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -664,8 +664,8 @@ local function check_plugin_version(filename) if info ~= nil and info.type == "dir" then filename = filename .. "/init.lua" info = system.get_file_info(filename) - if not info then return true end end + if not info or not filename:match("%.lua$") then return false end local f = io.open(filename, "r") if not f then return false end local version_match = false @@ -685,7 +685,7 @@ local function check_plugin_version(filename) end end f:close() - return version_match + return true, version_match end @@ -700,18 +700,19 @@ function core.load_plugins() local files = system.list_dir(plugin_dir) for _, filename in ipairs(files or {}) do local basename = filename:match("(.-)%.lua$") or filename - local version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if not version_match then - core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins - ls[#ls + 1] = filename - end - if version_match and config.plugins[basename] ~= false then - local modname = "plugins." .. basename - local ok = core.try(require, modname) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end - if not ok then - no_errors = false + local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) + if is_lua_file then + if not version_match then + core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) + local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins + ls[#ls + 1] = filename + elseif config.plugins[basename] ~= false then + local modname = "plugins." .. basename + local ok = core.try(require, modname) + if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + if not ok then + no_errors = false + end end end end From 63f406773b21e7f214daabcaa5c03f3a2320361f Mon Sep 17 00:00:00 2001 From: cukmekerb Date: Thu, 22 Jul 2021 22:02:49 -0700 Subject: [PATCH 036/180] align line numbers to right --- data/core/docview.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index ceed8636..139333cc 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -391,7 +391,8 @@ function DocView:draw_line_gutter(idx, x, y) end local yoffset = self:get_line_text_y_offset() x = x + style.padding.x - renderer.draw_text(self:get_font(), idx, x, y + yoffset, color) + local width = string.len(tostring(#self.doc.lines)) * style.padding.x / 2 + style.padding.x / 2 + common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) end From 4ad353eb4b106ceea46b1d179b23ec481798c6b6 Mon Sep 17 00:00:00 2001 From: cukmekerb Date: Thu, 22 Jul 2021 22:20:04 -0700 Subject: [PATCH 037/180] fix line number align bug --- 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 139333cc..3ecb699f 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -391,7 +391,7 @@ function DocView:draw_line_gutter(idx, x, y) end local yoffset = self:get_line_text_y_offset() x = x + style.padding.x - local width = string.len(tostring(#self.doc.lines)) * style.padding.x / 2 + style.padding.x / 2 + local width = local width = self:get_font():get_width(#self.doc.lines) common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) end From 135ad072bdc7fe776c9543f3e84470f60e4bc6c9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 28 Jul 2021 20:12:46 +0200 Subject: [PATCH 038/180] Move gutter width calculation out of loop --- data/core/docview.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 3ecb699f..89da8190 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -112,7 +112,8 @@ end function DocView:get_gutter_width() - return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2 + local padding = style.padding.x * 2 + return self:get_font():get_width(#self.doc.lines) + padding, padding end @@ -381,7 +382,7 @@ function DocView:draw_line_body(idx, x, y) end -function DocView:draw_line_gutter(idx, x, y) +function DocView:draw_line_gutter(idx, x, y, width) local color = style.line_number for _, line1, _, line2 in self.doc:get_selections(true) do if idx >= line1 and idx <= line2 then @@ -391,7 +392,6 @@ function DocView:draw_line_gutter(idx, x, y) end local yoffset = self:get_line_text_y_offset() x = x + style.padding.x - local width = local width = self:get_font():get_width(#self.doc.lines) common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) end @@ -421,12 +421,12 @@ function DocView:draw() local lh = self:get_line_height() local x, y = self:get_line_screen_position(minline) + local gw, gpad = self:get_gutter_width() for i = minline, maxline do - self:draw_line_gutter(i, self.position.x, y) + self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) y = y + lh end - local gw = self:get_gutter_width() 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) From 62bcc6abc20632c1fe428f306858ee3064e861fd Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 2 Aug 2021 10:07:43 +0200 Subject: [PATCH 039/180] Fix missing commas in autocomplete module --- data/plugins/autocomplete.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 4703a63a..e5265b7b 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -12,11 +12,11 @@ local Doc = require "core.doc" config.plugins.autocomplete = { -- Amount of characters that need to be written for autocomplete - min_len = 1 + min_len = 1, -- The max amount of visible items - max_height = 6 + max_height = 6, -- The max amount of scrollable items - max_suggestions = 100 + max_suggestions = 100, } local autocomplete = {} From f1f3eb1185d8a460517b0a35895138a31465754a Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 2 Aug 2021 18:43:59 +0200 Subject: [PATCH 040/180] Updated InnoSetup configuration file and added related build script --- scripts/innosetup/innosetup.iss.in | 30 +++++++++------- scripts/innosetup/innosetup.sh | 55 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 scripts/innosetup/innosetup.sh diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in index f085d271..83a730d8 100644 --- a/scripts/innosetup/innosetup.iss.in +++ b/scripts/innosetup/innosetup.iss.in @@ -4,7 +4,7 @@ #define MyAppURL "https://lite-xl.github.io" #define MyAppExeName "lite-xl.exe" #define BuildDir "@PROJECT_BUILD_DIR@" -#define SourceDir "." +#define SourceDir "@PROJECT_SOURCE_DIR@" ; Use /dArch option to create a setup for a different architecture, e.g.: ; iscc /dArch=x86 innosetup.iss @@ -34,7 +34,7 @@ ArchitecturesInstallIn64BitMode={#Arch} AllowNoIcons=yes Compression=lzma SolidCompression=yes -DefaultDirName={autopf}\{#MyAppName} +DefaultDirName={autopf}/{#MyAppName} DefaultGroupName={#MyAppPublisher} UninstallFilesDir={app} @@ -52,10 +52,10 @@ OutputBaseFilename=LiteXL-{#MyAppVersion}-{#Arch}-setup ;DisableDirPage=yes ;DisableProgramGroupPage=yes -LicenseFile={#SourceDir}\LICENSE -SetupIconFile={#SourceDir}\icon.ico -WizardImageFile="wizard-modern-image.bmp" -WizardSmallImageFile="litexl-55px.bmp" +LicenseFile={#SourceDir}/LICENSE +SetupIconFile={#SourceDir}/resources/icons/icon.ico +WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp" +WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp" [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -66,18 +66,22 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked [Files] -Source: "{#BuildDir}\lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#BuildDir}\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs +Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion +; MSYS2 produces no external dlls on 32 bit builds when using lhelper +#if Arch=="x64" +Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion +#endif +Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not IsTaskSelected('portablemode') -Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not IsTaskSelected('portablemode') +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not WizardIsTaskSelected('portablemode') +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not WizardIsTaskSelected('portablemode') Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not IsTaskSelected('portablemode') +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') [Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent [Setup] -Uninstallable=not IsTaskSelected('portablemode') +Uninstallable=not WizardIsTaskSelected('portablemode') diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh new file mode 100644 index 00000000..b2d0e86c --- /dev/null +++ b/scripts/innosetup/innosetup.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: 'build'." + echo +} + +BUILD_DIR=build + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--BUILD_DIR) + BUILD_DIR="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac +done + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +# TODO: Required MinGW dlls are built only (?) when using lhelper on 64 bit +if [[ $MSYSTEM == "MINGW64" ]]; then + ARCH=x64; + mingwLibsDir=$BUILD_DIR/mingwLibs$ARCH + mkdir -p "$mingwLibsDir" + ldd "$BUILD_DIR/src/lite-xl.exe" | grep mingw | awk '{print $3}' | xargs -I '{}' cp -v '{}' $mingwLibsDir +else + ARCH=Win32 +fi + +/c/Program\ Files\ \(x86\)/Inno\ Setup\ 6/ISCC.exe -dARCH=$ARCH $BUILD_DIR/innosetup.iss +mv $BUILD_DIR/LiteXL*.exe $(pwd) From cee1639d34e5b104f9c8b6a1d4c8e00be6bafe64 Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 2 Aug 2021 17:54:14 +0200 Subject: [PATCH 041/180] Updated .gitignore - Makes a clear distinction between files and directories, don't ignore build files - Added some file types and directories - ignore also 'lite-xl' prefixed install directories other than the executable --- .gitignore | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 42b4ba06..aa8f4ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,19 @@ -build* -.build* -.run* -*.zip -*.tar.gz -.lite-debug.log -subprojects/lua -subprojects/libagg -subprojects/reproc -lite-xl -error.txt +build*/ +.build*/ +lhelper/ +submodules/ +subprojects/lua/ +subprojects/libagg/ +subprojects/reproc/ .ccls-cache +.lite-debug.log +.run* +*.diff +*.tar.gz +*.zip +*.DS_Store +*App* +appimage* compile_commands.json +error.txt +lite-xl* From 0b2bf227a8c8911e64a51855febd05973dfd5c4b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 4 Aug 2021 22:13:56 +0200 Subject: [PATCH 042/180] Fix inactive divider intercepting mouse clicks In the function Node:get_divider_overlapping_point() we check if we hit a divider (separator between two nodes). If yes the event is intercepted and used to set the cursor and drag the separator if appropriate. In reality, on mouse move events, when one of the node is a split and one of its child is not resizable we don't set the cursor to and we don't intercept the event. However on a mouse pressed event the event was intercepted regardless of the fact that the child nodes are resizable or not. This latter behavior was unwanted as it prevents mouse clicks to be processed because of a divided that is inactive. In addition it prevented processing of mouse clicks when the child node was invisible leading to issue #363. For this latter the issue was the invisible NagView in the upper part of the window. To fix the problem we provide a divider with Node:get_divider_overlapping_point() only if its child node are resizable. In this way the mouse clicks or movements are intercepted only if the divider is actually active. --- data/core/rootview.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 36ab148d..3ef22173 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -240,12 +240,15 @@ end function Node:get_divider_overlapping_point(px, py) if self.type ~= "leaf" then - local p = 6 - local x, y, w, h = self:get_divider_rect() - x, y = x - p, y - p - w, h = w + p * 2, h + p * 2 - if px > x and py > y and px < x + w and py < y + h then - return self + local axis = self.type == "hsplit" and "x" or "y" + if self.a:is_resizable(axis) and self.b:is_resizable(axis) then + local p = 6 + local x, y, w, h = self:get_divider_rect() + x, y = x - p, y - p + w, h = w + p * 2, h + p * 2 + if px > x and py > y and px < x + w and py < y + h then + return self + end end return self.a:get_divider_overlapping_point(px, py) or self.b:get_divider_overlapping_point(px, py) @@ -823,10 +826,7 @@ function RootView:on_mouse_moved(x, y, dx, dy) if node and node:get_scroll_button_index(x, y) then core.request_cursor("arrow") elseif div then - local axis = (div.type == "hsplit" and "x" or "y") - if div.a:is_resizable(axis) and div.b:is_resizable(axis) then - core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") - end + core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") elseif tab_index then core.request_cursor("arrow") elseif node then From 2bf56e67c5c660c79b96a3f71f5646f998462d81 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Fri, 6 Aug 2021 18:08:08 -0400 Subject: [PATCH 043/180] Changed behaviour of home. --- data/core/commands/doc.lua | 1 + data/core/doc/translate.lua | 4 ++++ data/core/keymap-macos.lua | 2 +- data/core/keymap.lua | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 60a913ce..95919a70 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -418,6 +418,7 @@ local translations = { ["start-of-line"] = translate.start_of_line, ["end-of-line"] = translate.end_of_line, ["start-of-word"] = translate.start_of_word, + ["start-of-indentation"] = translate.start_of_indentation, ["end-of-word"] = translate.end_of_word, ["previous-line"] = DocView.translate.previous_line, ["next-line"] = DocView.translate.next_line, diff --git a/data/core/doc/translate.lua b/data/core/doc/translate.lua index b084e89a..d1bde5f0 100644 --- a/data/core/doc/translate.lua +++ b/data/core/doc/translate.lua @@ -117,6 +117,10 @@ function translate.start_of_line(doc, line, col) return line, 1 end +function translate.start_of_indentation(doc, line, col) + local s, e = doc.lines[line]:find("^%s*") + return line, col > e + 1 and e + 1 or 1 +end function translate.end_of_line(doc, line, col) return line, math.huge diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index e233bb2e..757b25da 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -80,7 +80,7 @@ local function keymap_macos(keymap) ["cmd+right"] = "doc:move-to-next-word-end", ["cmd+["] = "doc:move-to-previous-block-start", ["cmd+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", ["cmd+home"] = "doc:move-to-start-of-doc", ["cmd+end"] = "doc:move-to-end-of-doc", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 86be82b1..a757cbc9 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -184,7 +184,7 @@ keymap.add_direct { ["ctrl+right"] = "doc:move-to-next-word-end", ["ctrl+["] = "doc:move-to-previous-block-start", ["ctrl+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", ["ctrl+home"] = "doc:move-to-start-of-doc", ["ctrl+end"] = "doc:move-to-end-of-doc", From 3c8da0fc3fce7400327301d5edcf73f6d8490eb6 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Fri, 6 Aug 2021 18:09:36 -0400 Subject: [PATCH 044/180] Added in selection as well. --- data/core/keymap-macos.lua | 2 +- data/core/keymap.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 757b25da..647cb132 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -95,7 +95,7 @@ local function keymap_macos(keymap) ["cmd+shift+right"] = "doc:select-to-next-word-end", ["cmd+shift+["] = "doc:select-to-previous-block-start", ["cmd+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", ["cmd+shift+home"] = "doc:select-to-start-of-doc", ["cmd+shift+end"] = "doc:select-to-end-of-doc", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index a757cbc9..93ca39a3 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -199,7 +199,7 @@ keymap.add_direct { ["ctrl+shift+right"] = "doc:select-to-next-word-end", ["ctrl+shift+["] = "doc:select-to-previous-block-start", ["ctrl+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", ["ctrl+shift+home"] = "doc:select-to-start-of-doc", ["ctrl+shift+end"] = "doc:select-to-end-of-doc", From 346816451854a3b0d6fee95ce820715e7a7c1219 Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 9 Aug 2021 19:32:53 +0200 Subject: [PATCH 045/180] Updated Meson configuration - Added version and license metadata - Configuration data to be used in configured files to set metadata - Portable binary and directories in the main install directory - Binary file installed in correct places for all supported platforms - Freedesktop AppStream support - Added missing files install rules --- .editorconfig | 3 + meson.build | 127 +++++++++++++----- meson_options.txt | 2 - .../linux/org.lite-xl.lite-xl.appdata.xml | 28 ++++ ...xl.desktop => org.lite-xl.lite-xl.desktop} | 0 data/core/start.lua => scripts/start.lua.in | 3 +- src/meson.build | 9 +- 7 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 resources/linux/org.lite-xl.lite-xl.appdata.xml rename resources/linux/{lite-xl.desktop => org.lite-xl.lite-xl.desktop} (100%) rename data/core/start.lua => scripts/start.lua.in (96%) diff --git a/.editorconfig b/.editorconfig index 7278fb63..f942842a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,6 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[meson.build] +indent_size = 4 diff --git a/meson.build b/meson.build index 9240f68a..0031c9ae 100644 --- a/meson.build +++ b/meson.build @@ -1,14 +1,61 @@ -project('lite-xl', 'c', 'cpp', default_options : ['c_std=gnu11', 'cpp_std=c++03']) +project('lite-xl', + ['c', 'cpp'], + version : '2.0.0', + license : 'MIT', + meson_version : '>= 0.54', + default_options : ['c_std=gnu11', 'cpp_std=c++03'] +) +# TODO: the project version could be automatically generated from git with: +# version : run_command('bash', 'scripts/version.sh').stdout(), -version = get_option('version') +#=============================================================================== +# Configuration +#=============================================================================== conf_data = configuration_data() -conf_data.set('PROJECT_VERSION', version) +conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) +conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) +conf_data.set('PROJECT_VERSION', meson.project_version()) +configure_file( + input : 'scripts/start.lua.in', + output : 'start.lua', + configuration : conf_data +) +if host_machine.system() == 'windows' + configure_file( + input : 'scripts/innosetup/innosetup.iss.in', + output : 'innosetup.iss', + configuration : conf_data + ) +endif +#=============================================================================== +# Compiler Settings +#=============================================================================== if host_machine.system() == 'darwin' add_languages('objc') endif cc = meson.get_compiler('c') + +lite_cargs = [] +# On macos we need to use the SDL renderer to support retina displays +if get_option('renderer') or host_machine.system() == 'darwin' + lite_cargs += '-DLITE_USE_SDL_RENDERER' +endif +#=============================================================================== +# Linker Settings +#=============================================================================== +lite_link_args = [] +if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' + lite_link_args += ['-static-libgcc', '-static-libstdc++'] +endif + +if host_machine.system() == 'darwin' + lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] +endif +#=============================================================================== +# Dependencies +#=============================================================================== libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) libx11 = dependency('x11', required : false) @@ -17,11 +64,18 @@ pcre2_dep = dependency('libpcre2-8') sdl_dep = dependency('sdl2', method: 'config-tool') if not lua_dep.found() - lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false']) + lua_subproject = subproject('lua', + default_options: ['shared=false', 'use_readline=false', 'app=false'] + ) lua_dep = lua_subproject.get_variable('lua_dep') endif -reproc_subproject = subproject('reproc', default_options: ['default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false']) +reproc_subproject = subproject('reproc', + default_options: [ + 'default_library=static', 'multithreaded=false', + 'reproc-cpp=false', 'examples=false' + ] +) reproc_dep = reproc_subproject.get_variable('reproc_dep') lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, libx11] @@ -31,44 +85,47 @@ if host_machine.system() == 'windows' # the pkg-config file from reproc does not include it. lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) endif - -lite_cargs = [] -if get_option('portable') - lite_docdir = 'doc' - lite_datadir = 'data' +#=============================================================================== +# Install Configuration +#=============================================================================== +if get_option('portable') or host_machine.system() == 'windows' + lite_bindir = '/' + lite_docdir = '/doc' + lite_datadir = '/data' +elif host_machine.system() == 'darwin' + lite_bindir = 'Contents/MacOS' + lite_docdir = 'Contents/Resources' + lite_datadir = 'Contents/Resources' + install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources') + install_data('resources/macos/Info.plist', install_dir : 'Contents') else + lite_bindir = 'bin' lite_docdir = 'share/doc/lite-xl' lite_datadir = 'share/lite-xl' + if host_machine.system() == 'linux' + install_data('resources/icons/lite-xl.svg', + install_dir : 'share/icons/hicolor/scalable/apps' + ) + install_data('resources/linux/org.lite-xl.lite-xl.desktop', + install_dir : 'share/applications' + ) + install_data('resources/linux/org.lite-xl.lite-xl.appdata.xml', + install_dir : 'share/metainfo' + ) + endif endif -lite_include = include_directories('src') +install_data('licenses/licenses.md', install_dir : lite_docdir) + foreach data_module : ['core', 'fonts', 'plugins', 'colors'] install_subdir('data' / data_module , install_dir : lite_datadir) endforeach -install_data('licenses/licenses.md', install_dir : lite_docdir) - -lite_link_args = [] -if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' - lite_link_args += ['-static-libgcc', '-static-libstdc++'] -endif -if host_machine.system() == 'darwin' - lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] -endif - -lite_rc = [] -if host_machine.system() == 'windows' - windows = import('windows') - lite_rc += windows.compile_resources('resources/icons/icon.rc') - iss = configure_file(input : 'scripts/innosetup/innosetup.iss.in', - output : 'innosetup.iss', - configuration : conf_data) -endif - -# On macos we need to use the SDL renderer to support retina displays -if get_option('renderer') or host_machine.system() == 'darwin' - lite_cargs += '-DLITE_USE_SDL_RENDERER' -endif - +install_data( + meson.current_build_dir() / 'start.lua', install_dir : lite_datadir / 'core' +) +#=============================================================================== +# Targets +#=============================================================================== subdir('lib/font_renderer') subdir('src') diff --git a/meson_options.txt b/meson_options.txt index a61bd359..9fcd0da0 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,2 @@ -option('innosetup', type : 'boolean', value : false, description: 'Build Windows setup package') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') -option('version', type : 'string', value : '0.0.0', description: 'Project version') diff --git a/resources/linux/org.lite-xl.lite-xl.appdata.xml b/resources/linux/org.lite-xl.lite-xl.appdata.xml new file mode 100644 index 00000000..d2d0a73a --- /dev/null +++ b/resources/linux/org.lite-xl.lite-xl.appdata.xml @@ -0,0 +1,28 @@ + + + org.lite-xl.lite-xl + MIT + MIT + Lite XL + A lightweight text editor written in Lua + + +

+ Lite XL is a text editor and development tool written mainly in Lua, + on top of a minimalistic C core using the SDL2 graphics library. +

+
+ + + + The editor window + https://lite-xl.github.io/assets/img/screenshots/editor.png + + + + https://lite-xl.github.io + + + lite-xl + +
diff --git a/resources/linux/lite-xl.desktop b/resources/linux/org.lite-xl.lite-xl.desktop similarity index 100% rename from resources/linux/lite-xl.desktop rename to resources/linux/org.lite-xl.lite-xl.desktop diff --git a/data/core/start.lua b/scripts/start.lua.in similarity index 96% rename from data/core/start.lua rename to scripts/start.lua.in index 6288b877..3dcca2b3 100644 --- a/data/core/start.lua +++ b/scripts/start.lua.in @@ -1,5 +1,5 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "2.0-beta1" +VERSION = "@PROJECT_VERSION@" MOD_VERSION = "1" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE @@ -19,4 +19,3 @@ 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/meson.build b/src/meson.build index 03cc826f..2f3943fb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -13,10 +13,16 @@ lite_sources = [ 'main.c', ] -if host_machine.system() == 'darwin' +lite_rc = [] +if host_machine.system() == 'windows' + windows = import('windows') + lite_rc += windows.compile_resources('../resources/icons/icon.rc') +elif host_machine.system() == 'darwin' lite_sources += 'bundle_open.m' endif +lite_include = include_directories('.') + executable('lite-xl', lite_sources + lite_rc, include_directories: [lite_include, font_renderer_include], @@ -24,6 +30,7 @@ executable('lite-xl', c_args: lite_cargs, link_with: libfontrenderer, link_args: lite_link_args, + install_dir: lite_bindir, install: true, gui_app: true, ) From c552d373ca146c2cdd2263777f7c6913875b4602 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 10 Aug 2021 11:12:47 +0200 Subject: [PATCH 046/180] Fix run-local script to copy generated start.lua Now the file data/core/start.lua no longer exists but it is automatically generated by meson. Ensure the file is copied when running locally. --- scripts/run-local | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/run-local b/scripts/run-local index d5de23aa..7f8d7034 100755 --- a/scripts/run-local +++ b/scripts/run-local @@ -75,6 +75,8 @@ copy_lite_build () { else cp "$builddir/src/lite-xl" "$bindir" fi + mkdir -p "$datadir/core" + cp "$builddir/start.lua" "$datadir/core" for module_name in core plugins colors fonts; do cp -r "data/$module_name" "$datadir" done From 71e62ce84f96d4078168fe9d02cee93d8b092120 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 21:25:40 -0400 Subject: [PATCH 047/180] Added in a hash check to the system clipboard. --- data/core/commands/doc.lua | 33 +++++++++++++++++++++------------ data/core/doc/init.lua | 1 + src/api/system.c | 9 +++++++++ src/rencache.c | 13 +++++-------- src/rencache.h | 4 ++++ 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 95919a70..0a751e77 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -53,6 +53,18 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) end + +local function split_cursor(direction) + local new_cursors = {} + for _, line1, col1 in doc():get_selections() do + if line1 > 1 and line1 < #doc().lines then + table.insert(new_cursors, { line1 + direction, col1 }) + end + end + for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end + core.blink_reset() +end + local function cut_or_copy(delete) local full_text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do @@ -67,18 +79,10 @@ local function cut_or_copy(delete) doc().cursor_clipboard[idx] = "" end end - system.set_clipboard(full_text) -end - -local function split_cursor(direction) - local new_cursors = {} - for _, line1, col1 in doc():get_selections() do - if line1 > 1 and line1 < #doc().lines then - table.insert(new_cursors, { line1 + direction, col1 }) - end + if #doc().cursor_clipboard > 1 then + doc().cursor_clipboard["hash"] = system.hash(system.get_clipboard()) end - for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end - core.blink_reset() + system.set_clipboard(full_text) end local commands = { @@ -99,8 +103,13 @@ local commands = { end, ["doc:paste"] = function() + local clipboard = system.get_clipboard() + -- If the clipboard has changed since our last look, use that instead + if doc().cursor_clipboard["hash"] ~= system.hash(clipboard) then + doc().cursor_clipboard = {} + end for idx, line1, col1, line2, col2 in doc():get_selections() do - local value = doc().cursor_clipboard[idx] or system.get_clipboard() + local value = doc().cursor_clipboard[idx] or clipboard doc():text_input(value:gsub("\r", ""), idx) end end, diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 52f2113c..1baf7182 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -173,6 +173,7 @@ function Doc:merge_cursors(idx) if self.selections[i] == self.selections[j] and self.selections[i+1] == self.selections[j+1] then common.splice(self.selections, i, 4) + common.splice(self.cursor_clipboard, i, 1) break end end diff --git a/src/api/system.c b/src/api/system.c index 2f1bf763..85238e34 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -637,6 +637,14 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +static int f_hash(lua_State* L) { + unsigned i = RENCACHE_HASH_INITIAL; + size_t strLen; + const char *str = luaL_checklstring(L, 1, &strLen); + rencache_hash(&i, str, strLen) ; + lua_pushnumber(L, i); + return 1; +} static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -664,6 +672,7 @@ static const luaL_Reg lib[] = { { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, + { "hash", f_hash }, { NULL, NULL } }; diff --git a/src/rencache.c b/src/rencache.c index 31165e90..530615f4 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -83,10 +83,7 @@ static void font_refs_clear(lua_State *L) { font_refs_len = 0; } -/* 32bit fnv-1a hash */ -#define HASH_INITIAL 2166136261 - -static void hash(unsigned *h, const void *data, int size) { +void rencache_hash(unsigned *h, const void *data, int size) { const unsigned char *p = data; while (size--) { *h = (*h ^ *p++) * 16777619; @@ -227,7 +224,7 @@ static void update_overlapping_cells(RenRect r, unsigned h) { for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { int idx = cell_idx(x, y); - hash(&cells[idx], &h, sizeof(h)); + rencache_hash(&cells[idx], &h, sizeof(h)); } } } @@ -255,8 +252,8 @@ void rencache_end_frame(lua_State *L) { if (cmd->type == SET_CLIP) { cr = cmd->rect; } RenRect r = intersect_rects(cmd->rect, cr); if (r.width == 0 || r.height == 0) { continue; } - unsigned h = HASH_INITIAL; - hash(&h, cmd, cmd->size); + unsigned h = RENCACHE_HASH_INITIAL; + rencache_hash(&h, cmd, cmd->size); update_overlapping_cells(r, h); } @@ -271,7 +268,7 @@ void rencache_end_frame(lua_State *L) { if (cells[idx] != cells_prev[idx]) { push_rect((RenRect) { x, y, 1, 1 }, &rect_count); } - cells_prev[idx] = HASH_INITIAL; + cells_prev[idx] = RENCACHE_HASH_INITIAL; } } diff --git a/src/rencache.h b/src/rencache.h index 1d0f45a6..95a088d6 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -5,8 +5,12 @@ #include #include "renderer.h" +/* 32bit fnv-1a hash */ +#define RENCACHE_HASH_INITIAL 2166136261 + void rencache_show_debug(bool enable); void rencache_set_clip_rect(RenRect rect); +void rencache_hash(unsigned *h, const void *data, int size); void rencache_draw_rect(RenRect rect, RenColor color); int rencache_draw_text(lua_State *L, FontDesc *font_desc, int font_index, const char *text, int x, int y, RenColor color, bool draw_subpixel, CPReplaceTable *replacements, RenColor replace_color); From 255c45b30b641deb0fffabd8db31675fb106e0f8 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 21:29:33 -0400 Subject: [PATCH 048/180] Since we're modifying the clipboard, actually makes way more sense to use this hash. --- data/core/commands/doc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 0a751e77..a571b1bb 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -80,7 +80,7 @@ local function cut_or_copy(delete) end end if #doc().cursor_clipboard > 1 then - doc().cursor_clipboard["hash"] = system.hash(system.get_clipboard()) + doc().cursor_clipboard["hash"] = system.hash(full_text) end system.set_clipboard(full_text) end From 26a77542e31f77f476043bcb24307a403589714b Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 22:01:28 -0400 Subject: [PATCH 049/180] Fixed small bug on bootup, and added in a multiline qualifier to replacements. --- data/core/commands/findreplace.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 7904632a..1b539b8a 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -29,8 +29,8 @@ local function get_find_tooltip() end local function update_preview(sel, search_fn, text) - local ok, line1, col1, line2, col2 = - pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_sensitive, find_regex) + local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc, + sel[1], sel[2], text, case_sensitive, find_regex) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) last_view:scroll_to_line(line2, true) @@ -116,7 +116,7 @@ command.add("core.docview", { if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end - local result, matches = regex.gsub(regex.compile(old), text, new) + local result, matches = regex.gsub(regex.compile(old, "m"), text, new) return result, #matches end) end, @@ -180,12 +180,12 @@ command.add("core.commandview", { ["find-replace:toggle-sensitivity"] = function() case_sensitive = not case_sensitive core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end, ["find-replace:toggle-regex"] = function() find_regex = not find_regex core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end }) From 851dc0740812c60ec74d7f098d17bf463d948515 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 23:14:40 -0400 Subject: [PATCH 050/180] Added in close others, and refactored close all. --- data/core/commands/core.lua | 2 +- data/core/commands/root.lua | 8 +++++++- data/core/init.lua | 6 +++--- data/core/rootview.lua | 13 +++++++------ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index ac30fe20..d7d0f1a3 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -155,7 +155,7 @@ command.add(nil, { core.error("Cannot open folder %q", text) return end - core.confirm_close_all(core.open_folder_project, text) + core.confirm_close_docs(core.docs, core.open_folder_project, text) end, suggest_directory) end, diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 7bc13283..1375fe89 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -21,9 +21,15 @@ local t = { end, ["root:close-all"] = function() - core.confirm_close_all(core.root_view.close_all_docviews, core.root_view) + core.confirm_close_docs(core.docs, core.root_view.close_all_docviews, core.root_view) end, + ["root:close-all-others"] = function() + local active_doc, docs = core.active_view and core.active_view.doc, {} + for k, v in pairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end + core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true) + end, + ["root:switch-to-previous-tab"] = function() local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) diff --git a/data/core/init.lua b/data/core/init.lua index 6fbdde48..5c893d88 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -564,10 +564,10 @@ function core.init() end -function core.confirm_close_all(close_fn, ...) +function core.confirm_close_docs(docs, close_fn, ...) local dirty_count = 0 local dirty_name - for _, doc in ipairs(core.docs) do + for _, doc in ipairs(docs or core.docs) do if doc:is_dirty() then dirty_count = dirty_count + 1 dirty_name = doc:get_name() @@ -627,7 +627,7 @@ local function quit_with_function(quit_fn, force) save_session() quit_fn() else - core.confirm_close_all(quit_with_function, quit_fn, true) + core.confirm_close_docs(core.docs, quit_with_function, quit_fn, true) end end diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 3ef22173..c53d40de 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -594,12 +594,13 @@ function Node:is_empty() end -function Node:close_all_docviews() +function Node:close_all_docviews(keep_inactive) if self.type == "leaf" then local i = 1 while i <= #self.views do local view = self.views[i] - if view:is(DocView) and not view:is(CommandView) then + if view:is(DocView) and not view:is(CommandView) and + (not keep_inactive or view ~= self.active_view) then table.remove(self.views, i) else i = i + 1 @@ -609,8 +610,8 @@ function Node:close_all_docviews() self:add_view(EmptyView()) end else - self.a:close_all_docviews() - self.b:close_all_docviews() + self.a:close_all_docviews(keep_inactive) + self.b:close_all_docviews(keep_inactive) if self.a:is_empty() and not self.a.is_primary_node then self:consume(self.b) elseif self.b:is_empty() and not self.b.is_primary_node then @@ -736,8 +737,8 @@ function RootView:open_doc(doc) end -function RootView:close_all_docviews() - self.root_node:close_all_docviews() +function RootView:close_all_docviews(keep_inactive) + self.root_node:close_all_docviews(keep_inactive) end From 40c68ffcc6a42a3e13862096fadbfa004bd41a9c Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 23:18:30 -0400 Subject: [PATCH 051/180] Pairs -> IPairs --- data/core/commands/root.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 1375fe89..e41c723d 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -26,7 +26,7 @@ local t = { ["root:close-all-others"] = function() local active_doc, docs = core.active_view and core.active_view.doc, {} - for k, v in pairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end + for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true) end, From c644ca7df6dcd7a25c5b3caa58eb33a7121a9c73 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 10 Aug 2021 23:29:39 -0400 Subject: [PATCH 052/180] keep_inactive -> keep_active --- data/core/rootview.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index c53d40de..7b13487b 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -594,13 +594,13 @@ function Node:is_empty() end -function Node:close_all_docviews(keep_inactive) +function Node:close_all_docviews(keep_active) if self.type == "leaf" then local i = 1 while i <= #self.views do local view = self.views[i] if view:is(DocView) and not view:is(CommandView) and - (not keep_inactive or view ~= self.active_view) then + (not keep_active or view ~= self.active_view) then table.remove(self.views, i) else i = i + 1 @@ -610,8 +610,8 @@ function Node:close_all_docviews(keep_inactive) self:add_view(EmptyView()) end else - self.a:close_all_docviews(keep_inactive) - self.b:close_all_docviews(keep_inactive) + self.a:close_all_docviews(keep_active) + self.b:close_all_docviews(keep_active) if self.a:is_empty() and not self.a.is_primary_node then self:consume(self.b) elseif self.b:is_empty() and not self.b.is_primary_node then @@ -737,8 +737,8 @@ function RootView:open_doc(doc) end -function RootView:close_all_docviews(keep_inactive) - self.root_node:close_all_docviews(keep_inactive) +function RootView:close_all_docviews(keep_active) + self.root_node:close_all_docviews(keep_active) end From 0bafece6a6e6742754b189c0edd2e85eaaa86faf Mon Sep 17 00:00:00 2001 From: luarocks Date: Wed, 11 Aug 2021 13:22:11 +0300 Subject: [PATCH 053/180] Add textadept theme and correct language_lua just a bit --- data/colors/textadept.lua | 51 +++++++++++++++++++++++++++++++++++ data/plugins/language_lua.lua | 4 +-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 data/colors/textadept.lua diff --git a/data/colors/textadept.lua b/data/colors/textadept.lua new file mode 100644 index 00000000..276406d9 --- /dev/null +++ b/data/colors/textadept.lua @@ -0,0 +1,51 @@ +local b05 = 'rgba(0,0,0,0.5)' local red = '#994D4D' +local b80 = '#333333' local orange = '#B3661A' +local b60 = '#808080' local green = '#52994D' +local b40 = '#ADADAD' local teal = '#4D9999' +local b20 = '#CECECE' local blue = '#1A66B3' +local b00 = '#E6E6E6' local magenta = '#994D99' +--------------------------=-------------------------- +local style = require 'core.style' +local common = require 'core.common' +--------------------------=-------------------------- +style.line_highlight = { common.color(b20) } +style.background = { common.color(b00) } +style.background2 = { common.color(b20) } +style.background3 = { common.color(b20) } +style.text = { common.color(b60) } +style.caret = { common.color(b80) } +style.accent = { common.color(b80) } +style.dim = { common.color(b60) } +style.divider = { common.color(b40) } +style.selection = { common.color(b40) } +style.line_number = { common.color(b60) } +style.line_number2 = { common.color(b80) } +style.scrollbar = { common.color(b40) } +style.scrollbar2 = { common.color(b60) } +style.nagbar = { common.color(red) } +style.nagbar_text = { common.color(b00) } +style.nagbar_dim = { common.color(b05) } +--------------------------=-------------------------- +style.syntax = {} +style.syntax['normal'] = { common.color(b80) } +style.syntax['symbol'] = { common.color(b80) } +style.syntax['comment'] = { common.color(b60) } +style.syntax['keyword'] = { common.color(blue) } +style.syntax['keyword2'] = { common.color(red) } +style.syntax['number'] = { common.color(teal) } +style.syntax['literal'] = { common.color(blue) } +style.syntax['string'] = { common.color(green) } +style.syntax['operator'] = { common.color(magenta) } +style.syntax['function'] = { common.color(blue) } +--------------------------=-------------------------- +style.syntax.paren1 = { common.color(magenta) } +style.syntax.paren2 = { common.color(orange) } +style.syntax.paren3 = { common.color(teal) } +style.syntax.paren4 = { common.color(blue) } +style.syntax.paren5 = { common.color(red) } +--------------------------=-------------------------- +style.lint = {} +style.lint.info = { common.color(blue) } +style.lint.hint = { common.color(green) } +style.lint.warning = { common.color(red) } +style.lint.error = { common.color(orange) } diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5df3d29f..5c570200 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -41,8 +41,8 @@ syntax.add { ["until"] = "keyword", ["while"] = "keyword", ["for"] = "keyword", - ["break"] = "keyword", - ["return"] = "keyword", + ["break"] = "keyword2", + ["return"] = "keyword2", ["local"] = "keyword", ["in"] = "keyword", ["not"] = "keyword", From 205b52b08a641f3f91bfbd368cc0127738eeb890 Mon Sep 17 00:00:00 2001 From: luarocks Date: Wed, 11 Aug 2021 18:05:22 +0300 Subject: [PATCH 054/180] Revert language_lua --- data/plugins/language_lua.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5c570200..5df3d29f 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -41,8 +41,8 @@ syntax.add { ["until"] = "keyword", ["while"] = "keyword", ["for"] = "keyword", - ["break"] = "keyword2", - ["return"] = "keyword2", + ["break"] = "keyword", + ["return"] = "keyword", ["local"] = "keyword", ["in"] = "keyword", ["not"] = "keyword", From aa4d91a03f54a3b9b1075ac608a5fb3e546c2ee8 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 11 Aug 2021 18:14:46 -0400 Subject: [PATCH 055/180] Reverted hash changes; simply copy entire clipboard. --- data/core/commands/doc.lua | 29 +++++++++++++---------------- src/api/system.c | 9 --------- src/rencache.c | 13 ++++++++----- src/rencache.h | 4 ---- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index a571b1bb..7787054c 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -53,18 +53,6 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) end - -local function split_cursor(direction) - local new_cursors = {} - for _, line1, col1 in doc():get_selections() do - if line1 > 1 and line1 < #doc().lines then - table.insert(new_cursors, { line1 + direction, col1 }) - end - end - for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end - core.blink_reset() -end - local function cut_or_copy(delete) local full_text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do @@ -79,12 +67,21 @@ local function cut_or_copy(delete) doc().cursor_clipboard[idx] = "" end end - if #doc().cursor_clipboard > 1 then - doc().cursor_clipboard["hash"] = system.hash(full_text) - end + doc().cursor_clipboard["full"] = full_text system.set_clipboard(full_text) end +local function split_cursor(direction) + local new_cursors = {} + for _, line1, col1 in doc():get_selections() do + if line1 > 1 and line1 < #doc().lines then + table.insert(new_cursors, { line1 + direction, col1 }) + end + end + for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end + core.blink_reset() +end + local commands = { ["doc:undo"] = function() doc():undo() @@ -105,7 +102,7 @@ local commands = { ["doc:paste"] = function() local clipboard = system.get_clipboard() -- If the clipboard has changed since our last look, use that instead - if doc().cursor_clipboard["hash"] ~= system.hash(clipboard) then + if doc().cursor_clipboard["full"] ~= clipboard then doc().cursor_clipboard = {} end for idx, line1, col1, line2, col2 in doc():get_selections() do diff --git a/src/api/system.c b/src/api/system.c index 85238e34..2f1bf763 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -637,14 +637,6 @@ static int f_set_window_opacity(lua_State *L) { return 1; } -static int f_hash(lua_State* L) { - unsigned i = RENCACHE_HASH_INITIAL; - size_t strLen; - const char *str = luaL_checklstring(L, 1, &strLen); - rencache_hash(&i, str, strLen) ; - lua_pushnumber(L, i); - return 1; -} static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -672,7 +664,6 @@ static const luaL_Reg lib[] = { { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, - { "hash", f_hash }, { NULL, NULL } }; diff --git a/src/rencache.c b/src/rencache.c index 530615f4..31165e90 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -83,7 +83,10 @@ static void font_refs_clear(lua_State *L) { font_refs_len = 0; } -void rencache_hash(unsigned *h, const void *data, int size) { +/* 32bit fnv-1a hash */ +#define HASH_INITIAL 2166136261 + +static void hash(unsigned *h, const void *data, int size) { const unsigned char *p = data; while (size--) { *h = (*h ^ *p++) * 16777619; @@ -224,7 +227,7 @@ static void update_overlapping_cells(RenRect r, unsigned h) { for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { int idx = cell_idx(x, y); - rencache_hash(&cells[idx], &h, sizeof(h)); + hash(&cells[idx], &h, sizeof(h)); } } } @@ -252,8 +255,8 @@ void rencache_end_frame(lua_State *L) { if (cmd->type == SET_CLIP) { cr = cmd->rect; } RenRect r = intersect_rects(cmd->rect, cr); if (r.width == 0 || r.height == 0) { continue; } - unsigned h = RENCACHE_HASH_INITIAL; - rencache_hash(&h, cmd, cmd->size); + unsigned h = HASH_INITIAL; + hash(&h, cmd, cmd->size); update_overlapping_cells(r, h); } @@ -268,7 +271,7 @@ void rencache_end_frame(lua_State *L) { if (cells[idx] != cells_prev[idx]) { push_rect((RenRect) { x, y, 1, 1 }, &rect_count); } - cells_prev[idx] = RENCACHE_HASH_INITIAL; + cells_prev[idx] = HASH_INITIAL; } } diff --git a/src/rencache.h b/src/rencache.h index 95a088d6..1d0f45a6 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -5,12 +5,8 @@ #include #include "renderer.h" -/* 32bit fnv-1a hash */ -#define RENCACHE_HASH_INITIAL 2166136261 - void rencache_show_debug(bool enable); void rencache_set_clip_rect(RenRect rect); -void rencache_hash(unsigned *h, const void *data, int size); void rencache_draw_rect(RenRect rect, RenColor color); int rencache_draw_text(lua_State *L, FontDesc *font_desc, int font_index, const char *text, int x, int y, RenColor color, bool draw_subpixel, CPReplaceTable *replacements, RenColor replace_color); From acd122bc345674fa93e4440ee335dc6625444da6 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 11 Aug 2021 18:38:36 -0400 Subject: [PATCH 056/180] Small fix. --- data/core/doc/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 1baf7182..d05d9d45 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -178,6 +178,7 @@ function Doc:merge_cursors(idx) end end end + if #self.selections <= 4 then self.cursor_clipboard = {} end end local function selection_iterator(invariant, idx) From 5e66f74f3897a1beceeccf126f6e1f0ddb1804bd Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 11 Aug 2021 19:19:58 -0400 Subject: [PATCH 057/180] Handle proper path normalization if we begin with '..'. --- 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 5077f851..42801f00 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -284,7 +284,7 @@ function common.normalize_path(filename) local parts = split_on_slash(filename, PATHSEP) local accu = {} for _, part in ipairs(parts) do - if part == '..' then + if part == '..' and #accu > 0 then table.remove(accu) elseif part ~= '.' then table.insert(accu, part) From e2a75785534a8424f47925e246c28854bbe270bf Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 11 Aug 2021 20:54:03 -0400 Subject: [PATCH 058/180] If multiple '..' handle correctly. --- 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 42801f00..3093a36d 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -284,7 +284,7 @@ function common.normalize_path(filename) local parts = split_on_slash(filename, PATHSEP) local accu = {} for _, part in ipairs(parts) do - if part == '..' and #accu > 0 then + if part == '..' and #accu > 0 and accu[#accu] ~= ".." then table.remove(accu) elseif part ~= '.' then table.insert(accu, part) From 2fdde9cc997e609700c577c2b6100bc7dd518b5f Mon Sep 17 00:00:00 2001 From: redtide Date: Wed, 11 Aug 2021 20:33:25 +0200 Subject: [PATCH 059/180] Provide a quick offline build guide in README.md --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b96c9dca..78ef6930 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A lightweight text editor written in Lua, adapted from [lite]. * **[Get color themes]** โ€” Add additional colors themes. Please refer to our [website] for the user and developer documentation, -including [build] instructions. +including [build] instructions details. A quick build guide is described below. Lite XL has support for high DPI display on Windows and Linux and, since 1.16.7 release, it supports **retina displays** on macOS. @@ -42,6 +42,32 @@ the [plugins repository] or in the [Lite XL plugins repository]. Additional color themes can be found in the [colors repository]. These color themes are bundled with all releases of Lite XL by default. +## Quick Build Guide + +If you compile Lite XL yourself, it is recommended to use the script +`build-packages.sh`: + +```sh +bash build-packages.sh +``` + +The script will run Meson and create a zip file with the application or, +for Linux, a tar compressed archive. Lite XL can be easily installed +by unpacking the archive in any directory of your choice. + +Otherwise the following is an example of basic commands if you want to customize +the build: + +```sh +meson setup --buildtype=release --prefix build +meson compile -C build +DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build +``` + +where `` might be one of `/`, `/usr` or `/opt`, the default is `/usr/local`. +Please note that the package is relocatable to any prefix and the option prefix +affects only the place where the application is actually installed. + ## Contributing Any additional functionality that can be added through a plugin should be done From 3396a6c802de6edddf2132c2b29e0f0fd9f8e842 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 12 Aug 2021 09:03:57 +0200 Subject: [PATCH 060/180] Use the start.lua file in data/core as a template Instead of having a separate start.lua.in file in the scripts directory and no start.lua file in data/core we use the file data/core/start.lua as a template for Meson to generate the final start.lua file for release. In this way people naturally trying to run lite-xl from the source folder will have a start.lua file albeit without a resolved version number. Otherwise, when using run-local script or the meson install command the meson-generated start.lua file will be used as it should be. --- scripts/start.lua.in => data/core/start.lua | 0 meson.build | 17 +++++++++-------- scripts/run-local | 5 ++++- 3 files changed, 13 insertions(+), 9 deletions(-) rename scripts/start.lua.in => data/core/start.lua (100%) diff --git a/scripts/start.lua.in b/data/core/start.lua similarity index 100% rename from scripts/start.lua.in rename to data/core/start.lua diff --git a/meson.build b/meson.build index 0031c9ae..eaecc303 100644 --- a/meson.build +++ b/meson.build @@ -16,11 +16,6 @@ conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) conf_data.set('PROJECT_VERSION', meson.project_version()) -configure_file( - input : 'scripts/start.lua.in', - output : 'start.lua', - configuration : conf_data -) if host_machine.system() == 'windows' configure_file( input : 'scripts/innosetup/innosetup.iss.in', @@ -117,13 +112,19 @@ endif install_data('licenses/licenses.md', install_dir : lite_docdir) -foreach data_module : ['core', 'fonts', 'plugins', 'colors'] +install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua') +foreach data_module : ['fonts', 'plugins', 'colors'] install_subdir('data' / data_module , install_dir : lite_datadir) endforeach -install_data( - meson.current_build_dir() / 'start.lua', install_dir : lite_datadir / 'core' +configure_file( + input : 'data/core/start.lua', + output : 'start.lua', + configuration : conf_data, + install : true, + install_dir : lite_datadir / 'core', ) + #=============================================================================== # Targets #=============================================================================== diff --git a/scripts/run-local b/scripts/run-local index 7f8d7034..cef53ab1 100755 --- a/scripts/run-local +++ b/scripts/run-local @@ -76,10 +76,13 @@ copy_lite_build () { cp "$builddir/src/lite-xl" "$bindir" fi mkdir -p "$datadir/core" - cp "$builddir/start.lua" "$datadir/core" for module_name in core plugins colors fonts; do cp -r "data/$module_name" "$datadir" done + # The start.lua file is generated by meson in $builddir but + # there is already a start.lua file in data/core so the command below + # should be executed after we copy the data/core directory. + cp "$builddir/start.lua" "$datadir/core" } run_lite () { From 8c86cc51b0769f04ac271646bd4acd30de5a7d47 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 12 Aug 2021 09:20:55 +0200 Subject: [PATCH 061/180] Fix copying of start file in build-packages script --- build-packages.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build-packages.sh b/build-packages.sh index 3701ac53..ff851910 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -71,6 +71,8 @@ lite_build_package_windows () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + # copy the meson generated start.lua file + cp "$build/start.lua" "$datadir/core" for module_name in plugins colors; do cp -r "$build/third/data/$module_name" "$datadir" done @@ -97,6 +99,8 @@ lite_build_package_macos () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + # copy the meson generated start.lua file + cp "$build/start.lua" "$datadir/core" for module_name in plugins colors; do cp -r "$build/third/data/$module_name" "$datadir" done @@ -135,6 +139,8 @@ lite_build_package_linux () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + # copy the meson generated start.lua file + cp "$build/start.lua" "$datadir/core" for module_name in plugins colors; do cp -r "$build/third/data/$module_name" "$datadir" done From 904214378d00b9f1e111ca38bba3aa72ee95ad78 Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 9 Aug 2021 12:19:27 +0200 Subject: [PATCH 062/180] CI and custom build script utilities - macOS DMG image using AppDMG with custom background - Linux AppImage - Windows MSYS2 packaging - Source code tarball including subprojects source code - LHelper compatible build script --- .gitignore | 2 +- resources/macos/appdmg.png | Bin 0 -> 6167 bytes scripts/appdmg.sh | 30 +++++++ scripts/appimage.sh | 160 +++++++++++++++++++++++++++++++++++++ scripts/build.sh | 103 ++++++++++++++++++++++++ scripts/msys2-package.sh | 56 +++++++++++++ scripts/source-package.sh | 69 ++++++++++++++++ 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 resources/macos/appdmg.png create mode 100644 scripts/appdmg.sh create mode 100644 scripts/appimage.sh create mode 100644 scripts/build.sh create mode 100644 scripts/msys2-package.sh create mode 100644 scripts/source-package.sh diff --git a/.gitignore b/.gitignore index aa8f4ba0..7dc39e42 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ submodules/ subprojects/lua/ subprojects/libagg/ subprojects/reproc/ +/appimage* .ccls-cache .lite-debug.log .run* @@ -13,7 +14,6 @@ subprojects/reproc/ *.zip *.DS_Store *App* -appimage* compile_commands.json error.txt lite-xl* diff --git a/resources/macos/appdmg.png b/resources/macos/appdmg.png new file mode 100644 index 0000000000000000000000000000000000000000..1df7b60d2a4a30c26f3a861ad2f93e89a2cbdee3 GIT binary patch literal 6167 zcmeHL`BziRvfc?1f`Sr20TBtJ3@VeLAQ;&yh>8jdiZTd-$0&-3Od5tHGRP!&6c8CA zTn>UFpjTy38AOKXz+eQK2NOgBGUN~v(2%^!Id{E(;I8}AeLw6~tGlbJtGl|tu5ZUX z9=4NTx_T*sAoBb7*&Ia>EDb?0!+2@v+4`w}1cdD6eJ2AEWcgdkhiUdNIs=_*LAED? zoczxQg`U3h8xk5CYIy$Ag+QOvmwz+#ze3KM+OZlzRQ}j+v&;Ev_T)g=t(>`tA@20a z^TA7YMc+cP>YKIH_TzWSzlhm#``{Y<-GjYX0&eTy>TbAn(Z+}FcX^Fcu2ZP{od9=J zlkS^O3w^!zSv~c}Zm>w+@MOKV@wyFZh)+T(EhM<7b9G5b1=TBD--{p8I=%h-FUsOW zHBuY-tp9WbBck=8fosqxiXgZ4x^XZFVwR5JkbQd*EV5o3!63H@Gz58vM-kc?91%gc zU=BfNLERwLEgMyLB%8J zh)1#!7PI#-g3T{IUfkH}BHoo9{G}&&dkr3^3MuWdb#rrb4;^@9F;&SPzY`k;(oG%oru!D5@@K0fyYa;(UvgsfW%=eI1D~k+{vn6W>hq>-G*4#jz@E+ z2Ioqj`FzYU2`1eD_dCG-!0&cz355*H+NtjZIufOg&|@=9$%pr}YGW~jnFwyRcT*Ba z-s&zMH~$SsY*$dyIe2kw0sLht(a=3fYM(IT3z}M}bVu7A?(P|9-4}KkQ!rM)JooR}!K7e331J)5~c-&FA30hdXJC0cA z5Qh?%Vem77HpE+W1UD~^LvuT2QJOgsOFyoIUhtT1?ExpfNX&SDk3GQjXL7~;XR8h9os($Qz&ARonH zTWk<)k|G#D2*wGZCdQ2k0aHT{_#_=dkS~4z?l5vTYj7ubp zpT{cXA_RQudIYl+hV`LIgeDQ1Fq#iumV&i5$OMr2U?ox02AKe|S~3paNRn}o)sk_r zBS9)k=yW<$NgDE!e+a>zpVOce@S=Aq(d*7B!v4z$LQ)yV>(?o$8bI=jXBWu!4ys!S z_#|ma#vIk0bd*y6Bo6iZ14q2|Wma8mBK`B1eVghk(1=EG%<-N+Lf`%F6O^VW@u*iN zDD^h^hd(@DhTRKBS0!!6cSbU~<`>FQ(_V+A=fR|u6+_&rv2*ph*QpFVE*5gZnd7fX zk`c4#xNNB~dj~ZF0WSww7#FwrDzw4en)_xf{Ht9-VNx6(C%p&33L9#2`dC@!?i$v; zGXqSgy*4 z#cUs!2A@#shEtJzv5n_+9`jl32%S^R>XGs{B1skHJ}rAG%bO(MgW8`5I(mD( z>r?adBZSe+XVQ~Xar0fzUa?b-uQ@_l>Zk=L=N4>EKH1-nm%4U~jc1?6r=c#D;>gJ& zrV}mU@-kH-z07|sp-sG4!!(67(vbae=IV6JKq@FkGsnOGa*b3+Wi zE=!T#o(XS9Z7L{$`BnRM3a*I=&dU)L0Ho@2`#i&hT?rQ!kHMR@_mJ-& zqhJv&gf8v+&yj4V(%;?nG0WJz&knG&L=vcVq`90@d*NEblcRM2;5Zg$Z!wRYs1P0F zs38vlWxkfa9Dign@%Wj28?w(Hrs?|fWmT6a)gwEgiN$n6j>0h_DCr(f$h!1$yJ6MH zZRleH3Yqx%`2~DSb7?fF=wJPe+-69UA>Qo$hb?(|>ROCkTjo}(8>PO8Gd-O$H&Lr{ zp{w-fp{#AT4h{~S{fNN@cxMno*(rH<2>b+mW0R9N4cy8b%KeONTwFqfLg7*AVp77w zeA*Z!?psZfambJEBb4Rs2!M4qpU*#0dHM8a_sS={)Sz6AAN|?Z>beN8dj_X=?BAw@qL9A>?S&$UD3Af0^)q zJZnp5phwwFFaFPqU*ED%Jv}EW(#WB`Fps*bYFtZ}`CSGEy+fe2k3`NyX{IuezZ+|> zjr?vAH0B5=$BY2MP7S}AnimPo(?MGaw6Qgdlm6iO@5b5)-ly)TZ(m(Uj4iMVt|*mC z70!7Mxk;wljqFamI+o|(Y;RP3Lec?iOdOlAT2t&&W=N8v=@5~;m>s^9Ml*}qn~6j`|*$+Ha(32B03yM{y}LE;=uAiVC+4%QkKgOm$AK0lp-MPu;j zxqvKlYeL09NS1CC000Ir``E2w+s@g0ZIv2wD8R@*cDybl&Vl+f#YZe!lz7TOqHW4A z_9%9pdbNc?8Gi_OMGum_#i&+zrT;2hK47|9uopIhvmH?;B3gMhH8qtJrDctej%g;A zxK*Sxq|hi?bl;DAWJdf%S*;<|C?5ibQ5VoRi$kJEU@<-|nOnK^6~a^f*S>3f`t<1t zMG*-;?)H4_Bb}*D8&@^Id!#7rs2s*g6qyA)3Eu^4t60 z3kr`w6r&puIw8yK{FQ^*7H?M8p9z`l1spOyC;_+3p*K+j0W)5qa7N^A7t;Q}wEp)< zM3MoC=IeXmLgJETqs@Tq-s2q)O+p?!q@4|%8Gh4U73gxY`-#aZ3a5Y>cy0^8aB|PH zAwu(7D}$yKO;5`5J(uTazK)HJCHeK0H{Nj&Iq~M8BD7hu@UYeBoWFm{2cuducmNVC z_}xDCZKsvxj2N_KLY4Vg^lYp)8G>wBi;Ci5EZ z3kwM6fJ0t#tA$U1B#8MN)cM04edqt_uPSWt2#wg9Y;|cbN5A7;WdNJ?dUjCdRmfyA zJSnM+JxlPYcYkF-!1o-gmgshdRp_Ol;Na%bmP;~90Ubx2SjC>z+7Aw89j7b}K3?4E z&0z+5vzw-yOFdq}@{JuZN_Ra%;`?ge3BPJa>ODAQ^?)b0ad^DgTGf=?x|tLueI7Pa z-Cua-`HKba{hT)1NN;a1>-{FNA4kos`;Yy1SpWQBXh4dpe{k@g;$6?FSz^|;+0iCu z!S__guG3iKs+eUe4Lr_g`?g$~peEFoBdg3Wu6=0+tjUEUy(Bwq3JSGcV@yWBaG?+)#t-*Cn(sM&8|VNV4{QEu^LsS%A_AFIX=sExb!oqZ0=rK3JLrJJxXPu zyWkPuOrl>87tUsyoj>}Q$$#p4(ssF8hVACaBKeZ?pV?`j5@&saat1&_8;C>fu=a-s zQ1Wx2+R}m=`k4|9c{}s_Z%!;H&YBC;|nF74w57&-U0{^bfDs*%f z=z(@n%b)sO`UYSTYoQiYe(Vm6WfaP!z3<+ybYcnaLYXLC=cWimVyt(iUE}mCMZ9x1 z`1!4?Gy^K*12A8dbJ*gZI)C49;K_=7sB}nGQg|W3j>Wd&!U(%WH1{@+h~~UrMXy~n zToH?OLJ>G#bUbmsp-gjSZA3aAC#lIgPL{0V#tY)nTuacF0_|z)-C9}Pdv!Mo(w;rI zE%>6aV#unTmlT`13m|wmN$o@^B#4s z6V00^C!6>YLj7TD)M~<{%ubZ73V3_XuKAm|e$p#f4uf~ByFj>l506_45yy1+63iK) zLGF|Xc-(40aAjv$!Z;9H$v+OgA20B)N93Rpi{OfAU5w?c{>5dAI@j%-Q@3mMH_UXV zENy2&{4viveYu4y8X7A1b+GiGu+u9)1BY%=5hvhrUs3iollJAN;Rp|)i#||{zLG#OyGQDmL+#DJv6D*$clyl)G(sa1KxWg;yb#ah= zoj=EiaEEUSayDY=Um+`p9u!tshfGdDIdsk&h*^A4HB(dbRJAUnKcEQg<0Up)BHAa& zMkZ}uYVRFbl?7KOX&Y?HY1WD&8IVUns({=DQWvBN$O;L^m@)Mz<2BznUb{y8Jih|m z#Z+POjs(_u?i6|hOb?~(Ny!*R*@mp!`_b7~1JMFsaI}+&6ps6Oa%G(3VU0>4 zfoc~?fxIU<$)IH^A;t}we-mQ@6@50dyB*1|vkFIA8dd_C{&`ZUv5~aI%Mpk(v2$?P zkJhN!o&*hJSi?!!$h1u&wp#!{Rbv z4}`!az`J0+jsJ;XWX4s0k!QVgUb(eA+|obiR$P;<8?m&q0_e*ng;^c|Q5_~o1NhU> zs1cBLkvn})eaL`f%z*(7ndNKrNKw~ArUyL$5q}I;7lRKa+Pf0Z+MPfE4gj*=(2Q&_ zs2llGHslB@;nQI|`8_ZLf3i;#$jK&B6!y#Gh2!9_{QPGhE-TR}ov|xwP9SJr6j(b_ zq+?`JR-&iyC{GZkO1#s&Hf%@DuYnf@WXJBWZ*KsHsCB1E%kx920(8eB&s5T%p=O#~ z`;do!KMv2Yd7jH=Cz-Cg%h;_E92C^>K8-q3L5=kF@*;jI^-xb#F-pE? znqow{VM*aOs<5>+Ppb7*OzV+i03(DQvGVHds_&7?h4*XpbQ~D&l;!ys2VUM__g=7G z)wc83TgjU|I5=22UGvL_)e!&G>y7Z-h1srMC1)jC`I?J@r+$5W(2A2VJc`^cPvg_| z(UoKWSmxcB@GW5cVPTU=^Faer{Ame4_|f~qYTLzb1;IclbqBwjU7-63H^?tnV#7Ol z^V^5JjZii_QkH8!37xJvQE|yb*fR7u{-$~@)GI5sk!$volas)S8 lite-xl-dmg.json << EOF +{ + "title": "Lite XL", + "icon": "$(pwd)/resources/icons/icon.icns", + "background": "$(pwd)/resources/macos/appdmg.png", + "window": { + "position": { + "x": 360, + "y": 360 + }, + "size": { + "width": 480, + "height": 360 + } + }, + "contents": [ + { "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" }, + { "x": 336, "y": 248, "type": "link", "path": "/Applications" } + ] +} +EOF +~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg" diff --git a/scripts/appimage.sh b/scripts/appimage.sh new file mode 100644 index 00000000..b2cc5cd7 --- /dev/null +++ b/scripts/appimage.sh @@ -0,0 +1,160 @@ +#!/bin/env bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +show_help(){ + echo + echo $0 + echo + echo "Available options:" + echo + echo "-h --help Show this help and exits." + echo "-b --builddir DIRNAME Sets the name of the build dir (no path)." + echo " Default: 'build'." + echo "-n --nobuild Skips the build step, use existing files." + echo "-s --static Specify if building using static libraries" + echo " by using lhelper tool." + echo "-v --version VERSION Specify a version, non whitespace separated string." + echo +} + +ARCH="$(uname -m)" +BUILD_DIR=build +RUN_BUILD=true +STATIC_BUILD=false + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -n|--nobuild) + RUN_BUILD=false + shift + ;; + -s|--static) + STATIC_BUILD=true + shift + ;; + -v|--version) + VERSION="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac +done + +# TODO: Versioning using git +#if [[ -z $VERSION && -d .git ]]; then +# VERSION=$(git describe --tags --long | sed 's/^v//; s/\([^-]*-g\)/r\1/; s/-/./g') +#fi + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +setup_appimagetool(){ + if ! which appimagetool > /dev/null ; then + if [ ! -e appimagetool ]; then + if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then + echo "Could not download the appimagetool for the arch '${ARCH}'." + exit 1 + else + chmod 0755 appimagetool + fi + fi + fi +} + +download_appimage_apprun(){ + if [ ! -e AppRun ]; then + if ! wget -O AppRun "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH}" ; then + echo "Could not download AppRun for the arch '${ARCH}'." + exit 1 + else + chmod 0755 AppRun + fi + fi +} + +build_litexl(){ + if [ -e build ]; then + rm -rf build + fi + + if [ -e ${BUILD_DIR} ]; then + rm -rf ${BUILD_DIR} + fi + + echo "Build lite-xl..." + sleep 1 + meson setup --buildtype=release --prefix /usr ${BUILD_DIR} + meson compile -C ${BUILD_DIR} +} + +generate_appimage(){ + if [ -e LiteXL.AppDir ]; then + rm -rf LiteXL.AppDir + fi + + echo "Creating LiteXL.AppDir..." + + DESTDIR="$(realpath LiteXL.AppDir)" meson install --skip-subprojects -C ${BUILD_DIR} + mv AppRun LiteXL.AppDir/ + # These could be symlinks but it seems they doesn't work with AppimageLauncher + cp resources/icons/lite-xl.svg LiteXL.AppDir/ + cp resources/linux/org.lite-xl.lite-xl.desktop LiteXL.AppDir/ + + if [[ $STATIC_BUILD == false ]]; then + echo "Copying libraries..." + + mkdir -p LiteXL.AppDir/usr/lib/ + + local allowed_libs=( + libfreetype + libpcre2 + libSDL2 + libsndio + liblua + ) + + while read line; do + local libname="$(echo $line | cut -d' ' -f1)" + local libpath="$(echo $line | cut -d' ' -f2)" + for lib in "${allowed_libs[@]}" ; do + if echo "$libname" | grep "$lib" > /dev/null ; then + cp "$libpath" LiteXL.AppDir/usr/lib/ + continue 2 + fi + done + echo " Ignoring: $libname" + done < <(ldd build/src/lite-xl | awk '{print $1 " " $3}') + fi + + echo "Generating AppImage..." + local version="" + if [ -n "$VERSION" ]; then + version="-$VERSION" + fi + + ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage +} + +setup_appimagetool +download_appimage_apprun +if [[ $RUN_BUILD == true ]]; then build_litexl; fi +generate_appimage $1 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 00000000..b0e013b1 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,103 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: 'build'." + echo "-p --prefix Install directory prefix. Mandatory." + echo "-s --static Specify if building using static libraries" + echo " by using lhelper tool." + echo +} + +install_lhelper() { + if [[ ! -d lhelper ]]; then + git clone https://github.com/franko/lhelper.git + pushd lhelper; bash install-github; popd + + if [[ "$OSTYPE" == "darwin"* ]]; then + CC=clang CXX=clang++ lhelper create lite-xl -n + else + lhelper create lite-xl -n + fi + fi + + # Not using `lhelper activate lite-xl` + source "$(lhelper env-source lite-xl)" + + lhelper install freetype2 + lhelper install sdl2 2.0.14-wait-event-timeout-1 + lhelper install pcre2 + + # Help MSYS2 to find the SDL2 include and lib directories to avoid errors + # during build and linking when using lhelper. + if [[ "$OSTYPE" == "msys" ]]; then + CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 + LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib + fi +} + +build() { + CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ + --buildtype=release \ + --prefix "$PREFIX" \ + --wrap-mode=forcefallback \ + "${BUILD_DIR}" + + meson compile -C build +} + +BUILD_DIR=build +STATIC_BUILD=false + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -p|--prefix) + PREFIX="$2" + shift + shift + ;; + -s|--static) + STATIC_BUILD=true + shift + ;; + *) + # unknown option + ;; + esac +done + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +if [[ -z $PREFIX ]]; then + echo "ERROR: prefix argument is missing." + exit 1 +fi + +if [[ $STATIC_BUILD == true ]]; then + install_lhelper +fi + +build diff --git a/scripts/msys2-package.sh b/scripts/msys2-package.sh new file mode 100644 index 00000000..5138d8a3 --- /dev/null +++ b/scripts/msys2-package.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +# FIXME: For some strange reason sometimes an error occurs randomly on the +# MINGW32 build of GitHub Actions; the environment variable $INSTALL_NAME +# is correct, but it expands with a drive letter at the end (all builds). + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: 'build'." + echo "-d --destdir DIRNAME Sets the name of the install directory (not path)." + echo +} + +BUILD_DIR=build + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -d|--destdir) + DEST_DIR="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac +done + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +DESTDIR="$(pwd)/${DEST_DIR}" meson install --skip-subprojects -C ${BUILD_DIR} + +zip -9rv ${DEST_DIR}.zip ${DEST_DIR}/* diff --git a/scripts/source-package.sh b/scripts/source-package.sh new file mode 100644 index 00000000..aff80c9d --- /dev/null +++ b/scripts/source-package.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (no path)." + echo " Default: 'build'." + echo "-d --destdir DIRNAME Sets the name of the install directory (no path)." + echo +} + +BUILD_DIR=build +DEST_DIR=lite-xl-src + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -d|--destdir) + DEST_DIR="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac +done + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +if test -d ${BUILD_DIR}; then rm -rf ${BUILD_DIR}; fi +if test -d ${DEST_DIR}; then rm -rf ${DEST_DIR}; fi +if test -f ${DEST_DIR}.tar.gz; then rm ${DEST_DIR}.tar.gz; fi + +meson subprojects download +meson setup ${BUILD_DIR} + +rsync -arv \ + --exclude /*build*/ \ + --exclude *.git* \ + --exclude lhelper \ + --exclude lite-xl* \ + --exclude submodules \ + . ${DEST_DIR} + +cp "${BUILD_DIR}/start.lua" "${DEST_DIR}/data/core" + +tar rf ${DEST_DIR}.tar ${DEST_DIR} +gzip -9 ${DEST_DIR}.tar From 48ab8c9836517d320ac43a3fc7b7ef921c413fa0 Mon Sep 17 00:00:00 2001 From: redtide Date: Mon, 9 Aug 2021 12:39:03 +0200 Subject: [PATCH 063/180] GitHub Actions builds and deployment --- .github/workflows/build.yml | 297 ++++++++++++++++++++++++++++++------ 1 file changed, 253 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ef07952..6eec7a63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,14 +3,45 @@ name: CI on: push: branches: - - '*' + - '*' + # Temporarily disabled + #tags: + #- 'v[0-9]*' pull_request: branches: - - '*' + - '*' jobs: - build-linux: - name: Build Linux + # Note: not using git-archive(-all) because it can't include subprojects ignored by git + archive_source_code: + name: Source Code Tarball + runs-on: ubuntu-18.04 + # Only on tags/releases + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Set Environment Variables + run: echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-src" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.6 + - name: Install Dependencies + run: | + sudo apt-get install -qq ninja-build + pip3 install meson + - name: Archive source code + shell: bash + run: bash scripts/source-package.sh --destdir "${INSTALL_NAME}" + - uses: actions/upload-artifact@v2 + with: + name: Source Code Tarball + path: ${{ env.INSTALL_NAME }}.tar.gz + + # All builds use lhelper only for releases, using --static build argument, + # otherwise for normal builds dependencies are dynamically linked. + build_linux: + name: Linux runs-on: ubuntu-18.04 strategy: matrix: @@ -21,48 +52,226 @@ jobs: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install dependencies - run: | - sudo apt-get install -qq libsdl2-dev libfreetype6 ninja-build - pip3 install meson - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Ubuntu Package - path: lite-xl-linux-*.tar.gz + - name: Set Archive Name + if: ${{ matrix.config.cc == 'gcc' }} + run: | + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.6 + - name: Update Packages + run: sudo apt-get update + - name: Install Meson + run: | + sudo apt-get install -qq ninja-build + pip3 install meson + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: sudo apt-get install -qq libsdl2-dev libfreetype6 + - name: Build + if: ${{ matrix.config.cc == 'gcc' && !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/build.sh --prefix / + - name: Release Build + if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/build.sh --prefix / --static + - name: Package + if: ${{ matrix.config.cc == 'gcc' }} + run: | + DESTDIR="$(pwd)/$INSTALL_NAME" meson install --skip-subprojects -C build + tar czvf "${INSTALL_NAME}".tar.gz "${INSTALL_NAME}" + - name: AppImage + if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/appimage.sh --nobuild --static --version ${{ env.INSTALL_REF }} + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + if: ${{ matrix.config.cc == 'gcc' }} + with: + name: Linux Artifacts + path: | + ${{ env.INSTALL_NAME }}.tar.gz + LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage - build-macox: - name: Build Mac OS X + build_macos: + name: macOS (x86_64) runs-on: macos-10.15 + env: + CC: clang + CXX: clang++ + steps: + - name: System Information + run: | + system_profiler SPSoftwareDataType + gcc -v + xcodebuild -version + - name: Set Archive Name + run: | + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos" >> "$GITHUB_ENV" + bash --version + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Build Tools + run: | + brew install ninja + pip3 install meson + cd ~; npm install appdmg; cd - + ~/node_modules/appdmg/bin/appdmg.js --version + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: brew install sdl2 + - name: Install LHelper Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: brew install bash md5sha1sum + - name: Build + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/build.sh --prefix "${GITHUB_WORKSPACE}/Lite XL.app" + - name: Release Build + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash --version + bash scripts/build.sh --prefix "${GITHUB_WORKSPACE}/Lite XL.app" --static + - name: Error Logs + if: failure() + run: | + mkdir ${INSTALL_NAME} + cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} + tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} + - name: Install + run: meson install --skip-subprojects -C build + - name: Create DMG Image + run: bash scripts/appdmg.sh ${{ env.INSTALL_NAME }} + - name: Upload DMG Image + uses: actions/upload-artifact@v2 + with: + name: macOS DMG Image + path: ${{ env.INSTALL_NAME }}.dmg + - name: Upload Error Logs + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Error Logs + path: ${{ env.INSTALL_NAME }}.tar.gz + + build_windows_msys2: + name: Windows + runs-on: windows-2019 strategy: matrix: - config: - # - { name: "GCC", cc: gcc-10, cxx: g++-10 } - - { name: "clang", cc: clang, cxx: clang++ } - env: - CC: ${{ matrix.config.cc }} - CXX: ${{ matrix.config.cxx }} + msystem: [MINGW32, MINGW64] + defaults: + run: + shell: msys2 {0} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - pip3 install meson - brew install ninja sdl2 - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Mac OS X Package - path: lite-xl-macosx-*.zip + - uses: actions/checkout@v2 + - uses: msys2/setup-msys2@v2 + with: + #msystem: MINGW64 + msystem: ${{ matrix.msystem }} + update: true + install: >- + base-devel + git + zip + - name: Set Environment Variables + run: echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-$(echo $MSYSTEM | awk '{print tolower($0)}')" >> "$GITHUB_ENV" + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: | + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-gcc \ + ${MINGW_PACKAGE_PREFIX}-meson \ + ${MINGW_PACKAGE_PREFIX}-ninja \ + ${MINGW_PACKAGE_PREFIX}-pkg-config \ + ${MINGW_PACKAGE_PREFIX}-freetype \ + ${MINGW_PACKAGE_PREFIX}-pcre2 \ + ${MINGW_PACKAGE_PREFIX}-SDL2 + - name: Install Release Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-gcc \ + ${MINGW_PACKAGE_PREFIX}-meson \ + ${MINGW_PACKAGE_PREFIX}-ninja \ + ${MINGW_PACKAGE_PREFIX}-pkg-config + - name: Build + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/build.sh --prefix / + - name: Release Build + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash --version + bash scripts/build.sh --prefix / --static + - name: Error Logs + if: failure() + run: | + mkdir ${INSTALL_NAME} + cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} + tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} + - name: Package + run: bash scripts/msys2-package.sh --destdir ${INSTALL_NAME} + - name: Build Installer + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/innosetup/innosetup.sh + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: Windows Artifacts + path: | + LiteXL*.exe + ${{ env.INSTALL_NAME }}.zip + - name: Upload Error Logs + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Error Logs + path: ${{ env.INSTALL_NAME }}.tar.gz + + deploy: + name: Deployment + runs-on: ubuntu-18.04 + # Temporarily disabled + if: false + #if: startsWith(github.ref, 'refs/tags/') + needs: + - archive_source_code + - build_linux + - build_macos + - build_windows_msys2 + steps: + - name: Set Environment Variables + run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + - uses: actions/download-artifact@v2 + with: + name: Linux Artifacts + - uses: actions/download-artifact@v2 + with: + name: macOS DMG Image + - uses: actions/download-artifact@v2 + with: + name: Source Code Tarball + - uses: actions/download-artifact@v2 + with: + name: Windows Artifacts + - name: Display File Information + shell: bash + run: ls -lR + # Note: not using `actions/create-release@v1` + # because it cannot update an existing release + # see https://github.com/actions/create-release/issues/29 + - uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.INSTALL_REF }} + name: Release ${{ env.INSTALL_REF }} + draft: false + prerelease: false + files: | + lite-xl-${{ env.INSTALL_REF }}-* + LiteXL*.AppImage + LiteXL*.exe From 8e9c410d275cafd2435a92360a383149c43d3eb5 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 14 Aug 2021 08:59:37 -0400 Subject: [PATCH 064/180] Fixed multilne cursors at the edges of docuemnts. --- data/core/commands/doc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 60a913ce..f64fd416 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -73,7 +73,7 @@ end local function split_cursor(direction) local new_cursors = {} for _, line1, col1 in doc():get_selections() do - if line1 > 1 and line1 < #doc().lines then + if line1 + direction >= 1 and line1 + direction <= #doc().lines then table.insert(new_cursors, { line1 + direction, col1 }) end end From 5f1e68b824da3aab638aac68d52bf071a63bd6df Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 14 Aug 2021 09:11:07 -0400 Subject: [PATCH 065/180] Added in additional function to remove tooltip after cancelling replace. --- data/core/commands/findreplace.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 7904632a..910e6c2e 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -87,6 +87,8 @@ local function replace(kind, default, fn) end, function() end, function() core.status_view:remove_tooltip() end) + end, function() end, function() + core.status_view:remove_tooltip() end) end From 7ffe1b49d77154aa649fa89179601c71aefa66a5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 16 Aug 2021 11:54:25 +0200 Subject: [PATCH 066/180] Reference latest reproc subproject version --- subprojects/reproc.wrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subprojects/reproc.wrap b/subprojects/reproc.wrap index f1afb4fa..9ff98b7e 100644 --- a/subprojects/reproc.wrap +++ b/subprojects/reproc.wrap @@ -1,4 +1,4 @@ [wrap-git] directory = reproc url = https://github.com/franko/reproc -revision = v14.2.2-meson-1 +revision = v14.2.3-meson-1 From 02d59c8ec2666a715c9965b908a369e135a8b194 Mon Sep 17 00:00:00 2001 From: redtide Date: Sat, 14 Aug 2021 11:18:16 +0200 Subject: [PATCH 067/180] Added GH Actions CI badge on README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 78ef6930..339747fd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Lite XL +[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml) [![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) ![screenshot-dark] @@ -86,6 +87,7 @@ the terms of the MIT license. See [LICENSE] for details. See the [licenses] file for details on licenses used by the required dependencies. +[CI]: https://github.com/lite-xl/lite-xl/actions/workflows/build.yml/badge.svg [Discord Badge Image]: https://img.shields.io/discord/847122429742809208?label=discord&logo=discord [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [lite]: https://github.com/rxi/lite From 35fd29fc399ae124d8947cba67c1f73e6cfd7d31 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 15 Aug 2021 08:09:06 +0800 Subject: [PATCH 068/180] remove extraneous DPI code since 5 months ago (ttps://github.com/libsdl-org/SDL/commit/c289bad9007cb672c994f726d967f6e5682f200d) SDL2 now reads Xft.dpi. There is no need to link to X11 anymore. --- src/main.c | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main.c b/src/main.c index 53e8e364..576b7fcf 100644 --- a/src/main.c +++ b/src/main.c @@ -9,10 +9,6 @@ #include #elif __linux__ #include - #include - #include - #include - #include #include #elif __APPLE__ #include @@ -22,35 +18,9 @@ SDL_Window *window; static double get_scale(void) { -#ifdef _WIN32 - float dpi; + float dpi = 96.0; SDL_GetDisplayDPI(0, NULL, &dpi, NULL); return dpi / 96.0; -#elif __linux__ - SDL_SysWMinfo info; - XrmDatabase db; - XrmValue value; - char *type = NULL; - - SDL_VERSION(&info.version); - if (!SDL_GetWindowWMInfo(window, &info) - || info.subsystem != SDL_SYSWM_X11) - return 1.0; - - char *resource = XResourceManagerString(info.info.x11.display); - if (resource == NULL) - return 1.0; - - XrmInitialize(); - db = XrmGetStringDatabase(resource); - if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) == False - || value.addr == NULL) - return 1.0; - - return atof(value.addr) / 96.0; -#else - return 1.0; -#endif } From 419cd58c8fc2063a72c42463c7efd82fe8c41dec Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 15 Aug 2021 08:14:12 +0800 Subject: [PATCH 069/180] remove x11 dependency in meson.build --- meson.build | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meson.build b/meson.build index eaecc303..442c8d5f 100644 --- a/meson.build +++ b/meson.build @@ -53,7 +53,6 @@ endif #=============================================================================== libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) -libx11 = dependency('x11', required : false) lua_dep = dependency('lua5.2', required : false) pcre2_dep = dependency('libpcre2-8') sdl_dep = dependency('sdl2', method: 'config-tool') @@ -73,7 +72,7 @@ reproc_subproject = subproject('reproc', ) reproc_dep = reproc_subproject.get_variable('reproc_dep') -lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, libx11] +lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] if host_machine.system() == 'windows' # Note that we need to explicitly add the windows socket DLL because From b6af395fc77328af2f11e8b38d131867643d3f7f Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 17 Aug 2021 09:46:21 -0400 Subject: [PATCH 070/180] 2.0 changelog and modversion updates. --- changelog.md | 36 ++++++++++++++++++++++++++++++++ data/core/init.lua | 4 ++-- data/plugins/autocomplete.lua | 2 +- data/plugins/autoreload.lua | 2 +- data/plugins/contextmenu.lua | 2 +- data/plugins/detectindent.lua | 2 +- data/plugins/language_c.lua | 2 +- data/plugins/language_cpp.lua | 2 +- data/plugins/language_css.lua | 2 +- data/plugins/language_html.lua | 2 +- data/plugins/language_js.lua | 2 +- data/plugins/language_lua.lua | 2 +- data/plugins/language_md.lua | 2 +- data/plugins/language_python.lua | 2 +- data/plugins/language_xml.lua | 2 +- data/plugins/lineguide.lua | 2 +- data/plugins/macro.lua | 2 +- data/plugins/projectsearch.lua | 2 +- data/plugins/quote.lua | 2 +- data/plugins/reflow.lua | 2 +- data/plugins/scale.lua | 2 +- data/plugins/tabularize.lua | 2 +- data/plugins/toolbarview.lua | 2 +- data/plugins/treeview.lua | 2 +- data/plugins/trimwhitespace.lua | 2 +- data/plugins/workspace.lua | 2 +- resources/macos/Info.plist | 2 +- 27 files changed, 63 insertions(+), 27 deletions(-) diff --git a/changelog.md b/changelog.md index eb203747..d6e30519 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,41 @@ This files document the changes done in Lite XL for each release. +### 2.0 + +The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; +any custom plugins may need to be adjusted accordingly (see note below about plugin namespacing). + +Contains the following new features: + +Full PCRE (regex) support for find and replace, as well as in language syntax definitions. Can be accessed +programatically via the lua `regex` module. + +A full, finalized subprocess API, using libreproc. Subprocess can be started and interacted with using +`Process.new`. + +Support for multi-cursor editing. Cursors can be created by either ctrl+clicking on the screen, or by using +the keyboard shortcuts ctrl+shift+up/down to create an additional cursor on the previous/next line. + +All build systems other than meson removed. + +A more organized directory structure has been implemented; in particular a docs folder which contains C api +documentation, and a resource folder which houses all build resources. + +Plugin config namespacing has been implemented. This means that instead of using `config.myplugin.a`, +to read settings, and `config.myplugin = false` to disable plugins, this has been changed to +`config.plugins.myplugin.a`, and `config.plugins.myplugin = false` repsectively. This may require changes to +your user plugin, or to any custom plugins you have. + +A context menu on right click has been added. + +Changes to how we deal with indentation have been implemented; in particular, hitting home no longer brings you +to the start of a line, it'll bring you to the start of indentation, which is more in line with other editors. + +Lineguide, and scale plugins moved into the core, and removed from `lite-plugins`. This may also require you to +adjust your personal plugin folder to remove these if they're present. + +In addition, there have been many other small fixes and improvements, too numerous to list here. + ### 1.16.11 When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. diff --git a/data/core/init.lua b/data/core/init.lua index 5c893d88..4af35921 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -661,8 +661,8 @@ local function check_plugin_version(filename) -- Future versions will look only at the mod-version tag. local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$') if version then - -- we consider the version tag 1.16 equivalent to mod-version:1 - version_match = (version == '1.16' and MOD_VERSION == "1") + -- we consider the version tag 2.0 equivalent to mod-version:2 + version_match = (version == '2.0' and MOD_VERSION == "2") break end end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index e5265b7b..c41f233d 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local config = require "core.config" diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index f84d87d6..e772666f 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local Doc = require "core.doc" diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index c0fe49fd..b84d114d 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9e7ed93c..45ebaee6 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local common = require "core.common" diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index f55140c9..44c3b895 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index cf3d7cd2..499a09db 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 pcall(require, "plugins.language_c") local syntax = require "core.syntax" diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index 08a256f9..222e2f94 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index c45b43a3..cebb3f1a 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 671e1bd8..7556b00b 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5df3d29f..165633b6 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index ab2a7d8b..6e6e4255 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 849bafc1..e19caa63 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index d97fa9a8..95e310bb 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 8ef3ee68..61debbff 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 15d8a75e..2678363a 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 45399ed0..3873da3b 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index 85a5874c..c714cbf8 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index f0051c12..cbaa31ef 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 79152f8b..3e3e1a41 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 2fa06d69..4cdae6ea 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index 93102df2..bfd71138 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 14ada70f..f9a67aaf 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index a6d3d140..79886c67 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local Doc = require "core.doc" diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 9c1e20c8..1edfbe1e 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local DocView = require "core.docview" diff --git a/resources/macos/Info.plist b/resources/macos/Info.plist index cc369cd0..70d49a02 100644 --- a/resources/macos/Info.plist +++ b/resources/macos/Info.plist @@ -19,7 +19,7 @@ NSDesktopFolderUsageDescriptionTo access, edit and index your projects. NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. CFBundleShortVersionString - 1.16.10 + 2.0 NSHumanReadableCopyright ยฉ 2019-2021 Francesco Abbate From dd7c345fd9035a464d76e914a38265ced5e69100 Mon Sep 17 00:00:00 2001 From: redtide Date: Tue, 17 Aug 2021 23:58:19 +0200 Subject: [PATCH 071/180] Added missing resource files in build-packages.sh --- build-packages.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index ff851910..2c735320 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -130,12 +130,15 @@ lite_build_package_linux () { if [ "$portable" == "-portable" ]; then local bindir="$pdir" local datadir="$pdir/data" + local docdir="$pdir/doc" else local bindir="$pdir/bin" local datadir="$pdir/share/lite-xl" + local docdir="$pdir/share/doc/lite-xl" fi mkdir -p "$bindir" mkdir -p "$datadir" + mkdir -p "$docdir" for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done @@ -145,10 +148,12 @@ lite_build_package_linux () { cp -r "$build/third/data/$module_name" "$datadir" done cp "$build/src/lite-xl" "$bindir" + cp "licenses/licenses.md" "$docdir" strip "$bindir/lite-xl" if [ -z "$portable" ]; then - mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" - cp "resources/linux/lite-xl.desktop" "$pdir/share/applications" + mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" "$pdir/share/metainfo" + cp "resources/linux/org.lite-xl.lite-xl.desktop" "$pdir/share/applications" + cp "resources/linux/org.lite-xl.lite-xl.appdata.xml" "$pdir/share/metainfo" cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg" fi pushd ".package-build" From 2d088256b1981c068f528d87e9be57f7a0485004 Mon Sep 17 00:00:00 2001 From: harens Date: Wed, 18 Aug 2021 13:26:51 +0100 Subject: [PATCH 072/180] Add unix-like behaviour on macOS Closes https://github.com/lite-xl/lite-xl/issues/398 --- data/core/keymap.lua | 2 +- meson.build | 3 ++- meson_options.txt | 1 + src/main.c | 10 ++++++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 93ca39a3..0b08259d 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_RESOURCES") +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")) diff --git a/meson.build b/meson.build index 442c8d5f..709288d7 100644 --- a/meson.build +++ b/meson.build @@ -86,7 +86,8 @@ if get_option('portable') or host_machine.system() == 'windows' lite_bindir = '/' lite_docdir = '/doc' lite_datadir = '/data' -elif host_machine.system() == 'darwin' +elif get_option('bundle') and host_machine.system() == 'darwin' + lite_cargs += '-DMACOS_USE_BUNDLE' lite_bindir = 'Contents/MacOS' lite_docdir = 'Contents/Resources' lite_datadir = 'Contents/Resources' diff --git a/meson_options.txt b/meson_options.txt index 9fcd0da0..9621ff64 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,2 +1,3 @@ +option('bundle', type : 'boolean', value : true, description: 'Build a macOS bundle') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') diff --git a/src/main.c b/src/main.c index 576b7fcf..6537ba36 100644 --- a/src/main.c +++ b/src/main.c @@ -74,8 +74,10 @@ static void init_window_icon(void) { #endif #ifdef __APPLE__ -void set_macos_bundle_resources(lua_State *L); void enable_momentum_scroll(); +#ifdef MACOS_USE_BUNDLE +void set_macos_bundle_resources(lua_State *L); +#endif #endif int main(int argc, char **argv) { @@ -134,8 +136,12 @@ init_lua: lua_setglobal(L, "EXEFILE"); #ifdef __APPLE__ - set_macos_bundle_resources(L); + lua_pushboolean(L, true); + lua_setglobal(L, "MACOS"); enable_momentum_scroll(); + #ifdef MACOS_USE_BUNDLE + set_macos_bundle_resources(L); + #endif #endif const char *init_lite_code = \ From 1687bbd92d31bd4f4c2d07c9e31fe03d2fc7b827 Mon Sep 17 00:00:00 2001 From: Nikolai Sinyov Date: Thu, 19 Aug 2021 10:07:46 +0300 Subject: [PATCH 073/180] Update main.c Fixed interface Scale in MacOS in get_scale function --- src/main.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.c b/src/main.c index 576b7fcf..9dd7b9f7 100644 --- a/src/main.c +++ b/src/main.c @@ -18,9 +18,13 @@ 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; +#endif } From 717f4eb7825b4016c5a8b3d804e1a27f7a201328 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 19 Aug 2021 11:32:18 +0200 Subject: [PATCH 074/180] Conditionally disable macos bundle function --- src/bundle_open.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bundle_open.m b/src/bundle_open.m index f4f0b94c..08685d08 100644 --- a/src/bundle_open.m +++ b/src/bundle_open.m @@ -1,6 +1,7 @@ #import #include "lua.h" +#ifdef MACOS_USE_BUNDLE void set_macos_bundle_resources(lua_State *L) { @autoreleasepool { @@ -25,7 +26,7 @@ void set_macos_bundle_resources(lua_State *L) lua_pushstring(L, resource_path); lua_setglobal(L, "MACOS_RESOURCES"); }} - +#endif /* Thanks to mathewmariani, taken from his lite-macos github repository. */ void enable_momentum_scroll() { From c9669410adc52c2e21b95d7312910e3c6f9ee6d0 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 19 Aug 2021 14:30:44 -0700 Subject: [PATCH 075/180] Fix macOS build issue with recent commit Add objc_args in meson when compiling to pass C defines also to bundle_open.m. Default "bundle" option to false to have by default a unix-like build and install. In the run-local script always expect that "bundle" option is to false to have a unix-like install. In the build-package script pass the -Dbundle=true option when building on macos. When setting the resouce path revert to original method using [[NSBundle mainBundle] resourcePath] to have the real resource path when the bundle option will be activated. With the recent commit the function set_macos_bundle_resources will be called only if the "bundle" option is activate and is not used in unix-like mode. --- build-packages.sh | 6 +++++- meson_options.txt | 2 +- scripts/run-local | 5 +---- src/bundle_open.m | 21 ++------------------- src/meson.build | 1 + 5 files changed, 10 insertions(+), 25 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index 2c735320..f30b1112 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -32,7 +32,11 @@ lite_build () { local build="$1" build_dir_is_usable "$build" || exit 1 rm -fr "$build" - meson setup --buildtype=release "$build" || exit 1 + setup_options=() + if [[ "$OSTYPE" == "darwin"* ]]; then + setup_options+=(-Dbundle=true) + fi + meson setup "${setup_options[@]}" "$build" || exit 1 ninja -C "$build" || exit 1 } diff --git a/meson_options.txt b/meson_options.txt index 9621ff64..ce7eca98 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,3 @@ -option('bundle', type : 'boolean', value : true, description: 'Build a macOS bundle') +option('bundle', type : 'boolean', value : false, description: 'Build a macOS bundle') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') diff --git a/scripts/run-local b/scripts/run-local index cef53ab1..8a32e7fa 100755 --- a/scripts/run-local +++ b/scripts/run-local @@ -47,10 +47,7 @@ if [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "mingw"* ]]; then fi rundir=".run" -if [[ "$OSTYPE" == "darwin"* ]]; then - bindir="$rundir" - datadir="$rundir" -elif [ "$option_portable" == on ]; then +if [ "$option_portable" == on ]; then bindir="$rundir" datadir="$rundir/data" else diff --git a/src/bundle_open.m b/src/bundle_open.m index 08685d08..2ba10da7 100644 --- a/src/bundle_open.m +++ b/src/bundle_open.m @@ -5,25 +5,8 @@ void set_macos_bundle_resources(lua_State *L) { @autoreleasepool { - /* Use resolved executablePath instead of resourcePath to allow lanching - the lite-xl binary via a symlink, like typically done by Homebrew: - - /usr/local/bin/lite-xl -> /Applications/lite-xl.app/Contents/MacOS/lite-xl - - The resourcePath returns /usr/local in this case instead of - /Applications/lite-xl.app/Contents/Resources, which makes later - access to the resource files fail. Resolving the symlink to the - executable and then the relative path to the expected directory - Resources is a workaround for starting the application from both - the launcher directly and the command line via the symlink. - */ - NSString* executable_path = [[NSBundle mainBundle] executablePath]; - char resolved_path[PATH_MAX + 16 + 1]; - realpath([executable_path UTF8String], resolved_path); - strcat(resolved_path, "/../../Resources"); - char resource_path[PATH_MAX + 1]; - realpath(resolved_path, resource_path); - lua_pushstring(L, resource_path); + NSString* resource_path = [[NSBundle mainBundle] resourcePath]; + lua_pushstring(L, [resource_path UTF8String]); lua_setglobal(L, "MACOS_RESOURCES"); }} #endif diff --git a/src/meson.build b/src/meson.build index 2f3943fb..707e04e9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,6 +28,7 @@ executable('lite-xl', include_directories: [lite_include, font_renderer_include], dependencies: lite_deps, c_args: lite_cargs, + objc_args: lite_cargs, link_with: libfontrenderer, link_args: lite_link_args, install_dir: lite_bindir, From b76917ef9e2035c54a6afac442e4ae91f323f50d Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 19 Aug 2021 14:37:03 -0700 Subject: [PATCH 076/180] Require modversion 2 --- 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 3dcca2b3..71050057 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,6 @@ -- this file is used by lite-xl to setup the Lua environment when starting VERSION = "@PROJECT_VERSION@" -MOD_VERSION = "1" +MOD_VERSION = "2" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE PATHSEP = package.config:sub(1, 1) From dc501cb41a80fc61f4cc08f51bcbe8dd581e5792 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 25 Aug 2021 23:45:18 +0200 Subject: [PATCH 077/180] Fix plugin version check --- data/core/init.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 4af35921..92156c93 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -687,17 +687,19 @@ function core.load_plugins() for filename, plugin_dir in pairs(files) do local basename = filename:match("(.-)%.lua$") or filename - local version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if not version_match then - core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins - table.insert(list, filename) - end - if version_match and config.plugins[basename] ~= false then - local ok = core.try(require, "plugins." .. basename) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end - if not ok then - no_errors = false + local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) + if is_lua_file then + if not version_match then + core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) + local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins + table.insert(list, filename) + end + if version_match and config.plugins[basename] ~= false then + local ok = core.try(require, "plugins." .. basename) + if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + if not ok then + no_errors = false + end end end end From daf916769f5fdb85c412e9098d4893a5ff00ac01 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 00:13:40 +0200 Subject: [PATCH 078/180] Fix bug with close-all command There are really multiple things here in the close_all_docviews function: 1. we reset the Node's tab_offset 2. we ensure the core's active_view is properly set 3. we close LogViews as well as DocViews Some conditions seems to never happen but we stay safe and try to cover all possible cases. --- data/core/rootview.lua | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 7b13487b..9cedcf52 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -8,6 +8,7 @@ local View = require "core.view" local CommandView = require "core.commandview" local NagView = require "core.nagview" local DocView = require "core.docview" +local LogView = require "core.logview" local EmptyView = View:extend() @@ -595,19 +596,35 @@ end function Node:close_all_docviews(keep_active) + local node_active_view = self.active_view + local lost_active_view = false if self.type == "leaf" then local i = 1 while i <= #self.views do local view = self.views[i] - if view:is(DocView) and not view:is(CommandView) and + if (view:is(DocView) or view:is(LogView)) and not view:is(CommandView) and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) + if view == node_active_view then + lost_active_view = true + end else i = i + 1 end end - if #self.views == 0 and self.is_primary_node then - self:add_view(EmptyView()) + self.tab_offset = 1 + if #self.views == 0 then + if self.is_primary_node then + self:add_view(EmptyView()) + elseif node_active_view == core.active_view then + -- Apparently we never gets here. In practice the primary node + -- is cleared first and get the active view on the empty view it sets. + local default_view = core.root_view:get_primary_node().views[1] + core.set_active_view(default_view) + end + elseif #self.views > 0 and lost_active_view then + -- We never get there either + self:set_active_view(self.views[1]) end else self.a:close_all_docviews(keep_active) From 456f6eda657fd2bc67fa375491032bab8c7dea32 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 00:17:43 +0200 Subject: [PATCH 079/180] Do not use os.exit to exit the application Properly quit the application by terminating the core.run() function. Otherwise a BadWindow event was happening when closing the window. --- data/core/init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 92156c93..bd87385a 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -494,6 +494,7 @@ function core.init() core.redraw = true core.visited_files = {} core.restart_request = false + core.quit_request = false core.replacements = whitespace_replacements() core.root_view = RootView() @@ -632,7 +633,7 @@ local function quit_with_function(quit_fn, force) end function core.quit(force) - quit_with_function(os.exit, force) + quit_with_function(function() core.quit_request = true end, force) end @@ -1032,7 +1033,7 @@ function core.run() core.frame_start = system.get_time() local did_redraw = core.step() local need_more_work = run_threads() - if core.restart_request then break end + 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 -- do not wait of events at idle_iterations = 1 to give a chance at core.step to run From 7f4d9789d605be378487361d731ecf02420cecf6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 13:02:28 +0200 Subject: [PATCH 080/180] Simplify commit daf91676 about active view setting --- data/core/rootview.lua | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 9cedcf52..8bc402f9 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -613,17 +613,15 @@ function Node:close_all_docviews(keep_active) end end self.tab_offset = 1 - if #self.views == 0 then - if self.is_primary_node then - self:add_view(EmptyView()) - elseif node_active_view == core.active_view then - -- Apparently we never gets here. In practice the primary node - -- is cleared first and get the active view on the empty view it sets. - local default_view = core.root_view:get_primary_node().views[1] - core.set_active_view(default_view) - end + if #self.views == 0 and self.is_primary_node then + -- if we are not the primary view and we had the active view it doesn't + -- matter to reattribute the active view because, within the close_all_docviews + -- top call, the primary node will take the active view anyway. + -- Set the empty view and takes the active view. + self:add_view(EmptyView()) elseif #self.views > 0 and lost_active_view then - -- We never get there either + -- In practice we never get there but if a view remain we need + -- to reset the Node's active view. self:set_active_view(self.views[1]) end else From bb6b99b16777f0b34c1ea28db78525b4041d0f31 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 14:42:57 +0200 Subject: [PATCH 081/180] Further simplifies logic for active view in close-all command --- data/core/rootview.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8bc402f9..47f028cb 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -596,8 +596,6 @@ end function Node:close_all_docviews(keep_active) - local node_active_view = self.active_view - local lost_active_view = false if self.type == "leaf" then local i = 1 while i <= #self.views do @@ -605,9 +603,6 @@ function Node:close_all_docviews(keep_active) if (view:is(DocView) or view:is(LogView)) and not view:is(CommandView) and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) - if view == node_active_view then - lost_active_view = true - end else i = i + 1 end @@ -619,9 +614,10 @@ function Node:close_all_docviews(keep_active) -- top call, the primary node will take the active view anyway. -- Set the empty view and takes the active view. self:add_view(EmptyView()) - elseif #self.views > 0 and lost_active_view then - -- In practice we never get there but if a view remain we need - -- to reset the Node's active view. + elseif #self.views > 0 then + -- In practice we never get there but if it ever happen we need to + -- reset the Node's active view. We do this irrespectively of what the + -- previous active view was. self:set_active_view(self.views[1]) end else From a8f4c0c4e514b76836bf0b78af62fc22624254bb Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 15:22:09 +0200 Subject: [PATCH 082/180] Set initial text for core:change-project-folder --- data/core/commands/core.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index d7d0f1a3..cacd37c0 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -147,6 +147,8 @@ command.add(nil, { end, ["core:change-project-folder"] = function() + local dirname = common.dirname(core.project_dir) + core.command_view:set_text(dirname .. PATHSEP) core.command_view:enter("Change Project Folder", function(text, item) text = system.absolute_path(common.home_expand(item and item.text or text)) if text == core.project_dir then return end From 9592ce85f5fd029b3d55f8f0c579fc3c5181503f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 23:20:08 +0200 Subject: [PATCH 083/180] Revert "Further simplifies logic for active view in close-all command" This reverts commit bb6b99b16777f0b34c1ea28db78525b4041d0f31. --- data/core/rootview.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 47f028cb..8bc402f9 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -596,6 +596,8 @@ end function Node:close_all_docviews(keep_active) + local node_active_view = self.active_view + local lost_active_view = false if self.type == "leaf" then local i = 1 while i <= #self.views do @@ -603,6 +605,9 @@ function Node:close_all_docviews(keep_active) if (view:is(DocView) or view:is(LogView)) and not view:is(CommandView) and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) + if view == node_active_view then + lost_active_view = true + end else i = i + 1 end @@ -614,10 +619,9 @@ function Node:close_all_docviews(keep_active) -- top call, the primary node will take the active view anyway. -- Set the empty view and takes the active view. self:add_view(EmptyView()) - elseif #self.views > 0 then - -- In practice we never get there but if it ever happen we need to - -- reset the Node's active view. We do this irrespectively of what the - -- previous active view was. + elseif #self.views > 0 and lost_active_view then + -- In practice we never get there but if a view remain we need + -- to reset the Node's active view. self:set_active_view(self.views[1]) end else From d46475532f95a1371dd9187382c407eec7d52a98 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 27 Aug 2021 23:55:17 +0200 Subject: [PATCH 084/180] Introduce View objects context property Used to determine if an instance of the given class should be closed or not when a project session is terminated. --- data/core/commandview.lua | 2 ++ data/core/docview.lua | 1 + data/core/logview.lua | 1 + data/core/rootview.lua | 5 +---- data/core/view.lua | 4 ++++ data/plugins/projectsearch.lua | 1 + 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index 4d518d02..eb7febc7 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -15,6 +15,8 @@ end local CommandView = DocView:extend() +CommandView.context = "application" + local max_suggestions = 10 local noop = function() end diff --git a/data/core/docview.lua b/data/core/docview.lua index 89da8190..17fb534a 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -9,6 +9,7 @@ local View = require "core.view" local DocView = View:extend() +DocView.context = "session" local function move_to_line_offset(dv, line, col, offset) local xo = dv.last_x_offset diff --git a/data/core/logview.lua b/data/core/logview.lua index d7142fb5..b731df3c 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -5,6 +5,7 @@ local View = require "core.view" local LogView = View:extend() +LogView.context = "session" function LogView:new() LogView.super.new(self) diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8bc402f9..f2499e68 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -5,10 +5,8 @@ local style = require "core.style" local keymap = require "core.keymap" local Object = require "core.object" local View = require "core.view" -local CommandView = require "core.commandview" local NagView = require "core.nagview" local DocView = require "core.docview" -local LogView = require "core.logview" local EmptyView = View:extend() @@ -602,8 +600,7 @@ function Node:close_all_docviews(keep_active) local i = 1 while i <= #self.views do local view = self.views[i] - if (view:is(DocView) or view:is(LogView)) and not view:is(CommandView) and - (not keep_active or view ~= self.active_view) then + if view.context == "session" and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) if view == node_active_view then lost_active_view = true diff --git a/data/core/view.lua b/data/core/view.lua index 2fb431d6..d1374ee4 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -7,6 +7,10 @@ local Object = require "core.object" local View = Object:extend() +-- context can be "application" or "session". The instance of objects +-- with context "session" will be closed when a project session is +-- terminated. The context "application" is for functional UI elements. +View.context = "application" function View:new() self.position = { x = 0, y = 0 } diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 3873da3b..45967697 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -9,6 +9,7 @@ local View = require "core.view" local ResultsView = View:extend() +ResultsView.context = "session" function ResultsView:new(text, fn) ResultsView.super.new(self) From 4f8de02bcf2fb19b0d64bc03f6b3b7bc93b606ee Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 28 Aug 2021 00:08:25 +0200 Subject: [PATCH 085/180] Remove unused Object's method "implement" Not used in the code base. --- data/core/object.lua | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/data/core/object.lua b/data/core/object.lua index af41b7e9..0941ce5d 100644 --- a/data/core/object.lua +++ b/data/core/object.lua @@ -20,17 +20,6 @@ function Object:extend() end -function Object:implement(...) - for _, cls in pairs({...}) do - for k, v in pairs(cls) do - if self[k] == nil and type(v) == "function" then - self[k] = v - end - end - end -end - - function Object:is(T) local mt = getmetatable(self) while mt do From 06252382ec60d15ff2ff367bfab450d64908de7a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 28 Aug 2021 00:21:29 +0200 Subject: [PATCH 086/180] Fix focus problem with NagView with root:close-all Fix provided by @Guldoman in PR: https://github.com/lite-xl/lite-xl/pull/419 --- data/core/init.lua | 6 +++++- data/core/nagview.lua | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index bd87385a..f6034a06 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -746,8 +746,12 @@ end function core.set_active_view(view) assert(view, "Tried to set active view to nil") - if core.active_view and core.active_view.force_focus then return end if view ~= core.active_view then + if core.active_view and core.active_view.force_focus then + core.next_active_view = view + return + end + core.next_active_view = nil if view.doc and view.doc.filename then core.set_visited(view.doc.filename) end diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 6d6f89f4..3d448cd4 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -193,7 +193,8 @@ function NagView:next() self:change_hovered(common.find_index(self.options, "default_yes")) end self.force_focus = self.message ~= nil - core.set_active_view(self.message ~= nil and self or core.last_active_view) + core.set_active_view(self.message ~= nil and self or + core.next_active_view or core.last_active_view) end function NagView:show(title, message, options, on_select) From 5e801492951dc7737cb28a3ad22f9f33707578b7 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 28 Aug 2021 06:02:12 +0200 Subject: [PATCH 087/180] Avoid having no `pixel_width` On small scales `pixel_width` could become `0`. This caused the creation of buffers of size `0` with consequent overflows. --- lib/font_renderer/font_renderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/font_renderer/font_renderer.cpp b/lib/font_renderer/font_renderer.cpp index 8026a89d..14110107 100644 --- a/lib/font_renderer/font_renderer.cpp +++ b/lib/font_renderer/font_renderer.cpp @@ -245,7 +245,7 @@ FR_Bitmap *FR_Bake_Font_Bitmap(FR_Renderer *font_renderer, int font_height, } const int glyph_avg_width = glyph_count > 0 ? x_size_sum / (glyph_count * subpixel_scale) : font_height; - const int pixels_width = glyph_avg_width * 28; + const int pixels_width = glyph_avg_width > 0 ? glyph_avg_width * 28 : 28; // dry run simulating pixel position to estimate required image's height int x = x_start, y = 0, y_bottom = y; From dac3a9cba52862a7d58afdcb76cb693005c72e70 Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Fri, 27 Aug 2021 20:02:04 -0300 Subject: [PATCH 088/180] Fix minimal scale possible --- data/plugins/scale.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 3e3e1a41..68577285 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -13,6 +13,8 @@ config.plugins.scale = { use_mousewheel = true } +local MIN_SCALE = 0.25; + local scale_level = 0 local scale_steps = 0.05 @@ -89,6 +91,13 @@ local function inc_scale() end local function dec_scale() + local new_scale_level = default_scale + (scale_level - 1) * scale_steps + + if new_scale_level < MIN_SCALE then + set_scale(MIN_SCALE) + return + end + scale_level = scale_level - 1 set_scale(default_scale + scale_level * scale_steps) end From 49ec7c88e85a4081906c7a575975caf20d015382 Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Fri, 27 Aug 2021 20:12:09 -0300 Subject: [PATCH 089/180] Fix the additional four spaces to two spaces in the indent --- data/plugins/scale.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 68577285..2b340e5c 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -81,25 +81,25 @@ function RootView:on_mouse_wheel(d, ...) end local function res_scale() - scale_level = 0 - set_scale(default_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) + scale_level = scale_level + 1 + set_scale(default_scale + scale_level * scale_steps) end local function dec_scale() - local new_scale_level = default_scale + (scale_level - 1) * scale_steps + local new_scale_level = default_scale + (scale_level - 1) * scale_steps - if new_scale_level < MIN_SCALE then - set_scale(MIN_SCALE) - return - end + if new_scale_level < MIN_SCALE then + set_scale(MIN_SCALE) + return + end - scale_level = scale_level - 1 - set_scale(default_scale + scale_level * scale_steps) + scale_level = scale_level - 1 + set_scale(default_scale + scale_level * scale_steps) end From 2b1c157a36998db53cfe2ab11ba3242491d5cd85 Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Fri, 27 Aug 2021 23:57:51 -0300 Subject: [PATCH 090/180] Refactored minimum scale bug fix code --- data/plugins/scale.lua | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 2b340e5c..8d16304b 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -13,7 +13,7 @@ config.plugins.scale = { use_mousewheel = true } -local MIN_SCALE = 0.25; +local MINIMUM_SCALE = 0.25; local scale_level = 0 local scale_steps = 0.05 @@ -90,16 +90,9 @@ local function inc_scale() set_scale(default_scale + scale_level * scale_steps) end -local function dec_scale() - local new_scale_level = default_scale + (scale_level - 1) * scale_steps - - if new_scale_level < MIN_SCALE then - set_scale(MIN_SCALE) - return - end - +local function dec_scale() scale_level = scale_level - 1 - set_scale(default_scale + scale_level * scale_steps) + set_scale(math.max(default_scale + scale_level * scale_steps), MINIMUM_SCALE) end From eeac85d4b4ba0324c3a5dd0a51ca73e0a925aacb Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 28 Aug 2021 16:44:25 +0200 Subject: [PATCH 091/180] Bump new version number --- meson.build | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 709288d7..5dee12ec 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,10 @@ project('lite-xl', ['c', 'cpp'], - version : '2.0.0', + version : '2.0.1', license : 'MIT', meson_version : '>= 0.54', default_options : ['c_std=gnu11', 'cpp_std=c++03'] ) -# TODO: the project version could be automatically generated from git with: -# version : run_command('bash', 'scripts/version.sh').stdout(), #=============================================================================== # Configuration From f1c004411ceec9a4af3199c3125ba26533b769fe Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 28 Aug 2021 08:08:53 -0700 Subject: [PATCH 092/180] Add missing home_encode for change-project-folder --- 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 cacd37c0..7ac36976 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -148,7 +148,7 @@ command.add(nil, { ["core:change-project-folder"] = function() local dirname = common.dirname(core.project_dir) - core.command_view:set_text(dirname .. PATHSEP) + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) core.command_view:enter("Change Project Folder", function(text, item) text = system.absolute_path(common.home_expand(item and item.text or text)) if text == core.project_dir then return end From 3cc4cd1adab62411f5666f71c1df13d45c4c1557 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 05:09:26 -0700 Subject: [PATCH 093/180] Fix error when opening root directory --- data/core/common.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core/common.lua b/data/core/common.lua index 3093a36d..9f3102bb 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -276,6 +276,7 @@ end function common.normalize_path(filename) + if not filename then return end if PATHSEP == '\\' then filename = filename:gsub('[/\\]', '\\') local drive, rem = filename:match('^([a-zA-Z])(:.*)') @@ -290,7 +291,8 @@ function common.normalize_path(filename) table.insert(accu, part) end end - return table.concat(accu, PATHSEP) + local npath = table.concat(accu, PATHSEP) + return npath == "" and PATHSEP or npath end From 368ffca40ab2746f7eb4896bbfa63a428e8cce1f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 05:10:00 -0700 Subject: [PATCH 094/180] Fix macOS minimum system version in info.plist --- resources/macos/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/macos/Info.plist b/resources/macos/Info.plist index 70d49a02..d465479c 100644 --- a/resources/macos/Info.plist +++ b/resources/macos/Info.plist @@ -14,7 +14,7 @@ APPL NSHighResolutionCapable - MinimumOSVersion10.13 + LSMinimumSystemVersion10.11 NSDocumentsFolderUsageDescriptionTo access, edit and index your projects. NSDesktopFolderUsageDescriptionTo access, edit and index your projects. NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. From 28e8a98ffc27d0f0c7342e5250ea19ba02d575ee Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 05:21:45 -0700 Subject: [PATCH 095/180] Fix error in change-project-folder command --- data/core/commands/core.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 7ac36976..215ff654 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -148,7 +148,9 @@ command.add(nil, { ["core:change-project-folder"] = function() local dirname = common.dirname(core.project_dir) - core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) + if dirname then + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) + end core.command_view:enter("Change Project Folder", function(text, item) text = system.absolute_path(common.home_expand(item and item.text or text)) if text == core.project_dir then return end From afaf0a718d634fb786285370fa4b2f005f17c9f6 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 22 Aug 2021 19:29:21 +0800 Subject: [PATCH 096/180] improve logview The logview is now less cluttered. The filename and stack trace (if any) is hidden by default. The user can click on the log entry to expand it. --- data/core/logview.lua | 146 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/data/core/logview.lua b/data/core/logview.lua index b731df3c..b81e8efe 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -1,8 +1,37 @@ local core = require "core" +local common = require "core.common" local style = require "core.style" local View = require "core.view" +local function lines(text) + if text == "" then return 0 end + local l = 1 + for _ in string.gmatch(text, "\n") do + l = l + 1 + end + return l +end + + +local item_height_result = {} + + +local function get_item_height(item) + local h = item_height_result[item] + if not h then + h = {} + local l = 1 + lines(item.text) + lines(item.info or "") + h.normal = style.font:get_height() + style.padding.y + h.expanded = l * style.font:get_height() + style.padding.y + h.current = h.normal + h.target = h.current + item_height_result[item] = h + end + return h +end + + local LogView = View:extend() LogView.context = "session" @@ -10,6 +39,7 @@ LogView.context = "session" function LogView:new() LogView.super.new(self) self.last_item = core.log_items[#core.log_items] + self.expanding = {} self.scrollable = true self.yoffset = 0 end @@ -20,6 +50,55 @@ function LogView:get_name() end +local function is_expanded(item) + local item_height = get_item_height(item) + return item_height.target == item_height.expanded +end + + +function LogView:expand_item(item) + item = get_item_height(item) + item.target = item.target == item.expanded and item.normal or item.expanded + table.insert(self.expanding, item) +end + + +function LogView:each_item() + local x, y = self:get_content_offset() + y = y + style.padding.y + self.yoffset + return coroutine.wrap(function() + for i = #core.log_items, 1, -1 do + local item = core.log_items[i] + local h = get_item_height(item).current + coroutine.yield(i, item, x, y, self.size.x, h) + y = y + h + end + end) +end + + +function LogView:on_mouse_moved(px, py, ...) + LogView.super.on_mouse_moved(self, px, py, ...) + local hovered = false + for _, item, x, y, w, h in self:each_item() do + if px >= x and py >= y and px < x + w and py < y + h then + hovered = true + self.hovered_item = item + break + end + end + if not hovered then self.hovered_item = nil end +end + + +function LogView:on_mouse_pressed(button, mx, my, clicks) + if LogView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end + if self.hovered_item then + self:expand_item(self.hovered_item) + end +end + + function LogView:update() local item = core.log_items[#core.log_items] if self.last_item ~= item then @@ -28,6 +107,14 @@ function LogView:update() self.yoffset = -(style.font:get_height() + style.padding.y) end + local expanding = self.expanding[1] + if expanding then + self:move_towards(expanding, "current", expanding.target) + if expanding.current == expanding.target then + table.remove(self.expanding, 1) + end + end + self:move_towards("yoffset", 0) LogView.super.update(self) @@ -46,28 +133,57 @@ local function draw_text_multiline(font, text, x, y, color) end +local function draw_text_elipsis(font, color, text, x, y, w, h, elipsis_style) + elipsis_style = elipsis_style or "end" + local c = font:get_width("_") + local approxc = math.floor(w / c) + if #text > approxc then + if elipsis_style == "end" then + text = string.sub(text, 1, approxc - 3) .. "..." + elseif elipsis_style == "middle" then + local mid = math.floor(#text / 2) + text = string.sub(text, 1, mid - 3) .. "..." .. string.sub(text, mid) + end + end + return common.draw_text(font, color, text, "left", x, y, w, h) +end + + function LogView:draw() self:draw_background(style.background) - local ox, oy = self:get_content_offset() local th = style.font:get_height() - local y = oy + style.padding.y + self.yoffset - - for i = #core.log_items, 1, -1 do - local x = ox + style.padding.x - local item = core.log_items[i] - local time = os.date(nil, item.time) - x = renderer.draw_text(style.font, time, x, y, style.dim) + local lh = th + style.padding.y -- for one line + for _, item, x, y, w, h in self:each_item() do + core.push_clip_rect(x, y, w, h) x = x + style.padding.x - local subx = x - x, y = draw_text_multiline(style.font, item.text, x, y, style.text) - renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) - y = y + th - if item.info then - subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) + + local time = os.date(nil, item.time) + x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh) + x = x + style.padding.x + + x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh) + x = x + style.padding.x + w = w - (x - self:get_content_offset()) + + if is_expanded(item) then + y = y + common.round(style.padding.y / 2) + draw_text_multiline(style.font, item.text, x, y, style.text) + y = y + th + + local at = "at " .. common.home_encode(item.at) + draw_text_elipsis(style.font, style.dim, at, x, y, w, lh, "middle") + y = y + th + + if item.info then + draw_text_multiline(style.font, item.info, x, y, style.dim) + end + else + draw_text_elipsis(style.font, style.text, item.text, x, y, w, lh) y = y + th end - y = y + style.padding.y + + core.pop_clip_rect() end end From 622b162225689424740a92c6390fff494eabd08c Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 22 Aug 2021 19:32:45 +0800 Subject: [PATCH 097/180] add core.get_log() --- data/core/init.lua | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/core/init.lua b/data/core/init.lua index f6034a06..702e31fd 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -881,6 +881,23 @@ function core.error(...) end +function core.get_log(i) + if i == nil then + local r = {} + for _, item in ipairs(core.log_items) do + table.insert(r, core.get_log(item)) + end + return table.concat(r, "\n") + end + local item = type(i) == "number" and core.log_items[i] or i + local text = string.format("[%s] %s at %s", os.date(nil, item.time), item.text, item.at) + if item.info then + text = string.format("%s\n%s\n", text, item.info) + end + return text +end + + function core.try(fn, ...) local err local ok, res = xpcall(fn, function(msg) From e25ea1196a539f56b88b84bc3fd442001251bb65 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 22 Aug 2021 19:33:28 +0800 Subject: [PATCH 098/180] add context menu options for logview --- data/plugins/contextmenu.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index b84d114d..dc95567f 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -42,6 +42,24 @@ keymap.add { ["menu"] = "context:show" } +local function copy_log() + local item = core.active_view.hovered_item + if item then + system.set_clipboard(core.get_log(item)) + end +end + +local function open_as_doc() + local doc = core.open_doc("logs.txt") + core.root_view:open_doc(doc) + doc:insert(1, 1, core.get_log()) +end + +menu:register("core.logview", { + { text = "Copy entry", command = copy_log }, + { text = "Open as file", command = open_as_doc } +}) + if require("plugins.scale") then menu:register("core.docview", { { text = "Font +", command = "scale:increase" }, From 14565b5226523614719f5a6e77fbf0c1e6ad7e23 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:32:20 +0800 Subject: [PATCH 099/180] more changes to logview - remove draw_text_elipsis - remove clip rect operations - fix text drawing when expanded - simplify code --- data/core/logview.lua | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/data/core/logview.lua b/data/core/logview.lua index b81e8efe..f79ff3e3 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -123,29 +123,12 @@ end local function draw_text_multiline(font, text, x, y, color) local th = font:get_height() - local resx, resy = x, y + local resx = x for line in text:gmatch("[^\n]+") do - resy = y resx = renderer.draw_text(style.font, line, x, y, color) y = y + th end - return resx, resy -end - - -local function draw_text_elipsis(font, color, text, x, y, w, h, elipsis_style) - elipsis_style = elipsis_style or "end" - local c = font:get_width("_") - local approxc = math.floor(w / c) - if #text > approxc then - if elipsis_style == "end" then - text = string.sub(text, 1, approxc - 3) .. "..." - elseif elipsis_style == "middle" then - local mid = math.floor(#text / 2) - text = string.sub(text, 1, mid - 3) .. "..." .. string.sub(text, mid) - end - end - return common.draw_text(font, color, text, "left", x, y, w, h) + return resx, y end @@ -155,7 +138,6 @@ function LogView:draw() local th = style.font:get_height() local lh = th + style.padding.y -- for one line for _, item, x, y, w, h in self:each_item() do - core.push_clip_rect(x, y, w, h) x = x + style.padding.x local time = os.date(nil, item.time) @@ -168,22 +150,21 @@ function LogView:draw() if is_expanded(item) then y = y + common.round(style.padding.y / 2) - draw_text_multiline(style.font, item.text, x, y, style.text) - y = y + th + _, y = draw_text_multiline(style.font, item.text, x, y, style.text) local at = "at " .. common.home_encode(item.at) - draw_text_elipsis(style.font, style.dim, at, x, y, w, lh, "middle") - y = y + th + _, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh) if item.info then - draw_text_multiline(style.font, item.info, x, y, style.dim) + _, y = draw_text_multiline(style.font, item.info, x, y, style.dim) end else - draw_text_elipsis(style.font, style.text, item.text, x, y, w, lh) - y = y + th + local line, has_newline = string.match(item.text, "([^\n]+)(\n?)") + if has_newline ~= "" then + line = line .. " ..." + end + _, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh) end - - core.pop_clip_rect() end end From 30d3751632cfa237e81d5f7b4c1ddffe2d630f84 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 24 Aug 2021 12:02:22 +0800 Subject: [PATCH 100/180] remove unused variable --- data/core/logview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/logview.lua b/data/core/logview.lua index f79ff3e3..1ea0e43e 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -137,7 +137,7 @@ function LogView:draw() local th = style.font:get_height() local lh = th + style.padding.y -- for one line - for _, item, x, y, w, h in self:each_item() do + for _, item, x, y, w in self:each_item() do x = x + style.padding.x local time = os.date(nil, item.time) From 97493a1a4eca756e81404bc053de90a4591fb4d9 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Mon, 2 Aug 2021 11:37:00 +0800 Subject: [PATCH 101/180] add doc:get_selection_text() --- data/core/doc/init.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index d05d9d45..ae948479 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -124,9 +124,21 @@ function Doc:get_selection(sort) return line1, col1, line2, col2, sort end +function Doc:get_selection_text(limit) + limit = limit or math.huge + local result = {} + for idx, line1, col1, line2, col2 in self:get_selections() do + if idx > limit then break end + if line1 ~= line2 or col1 ~= col2 then + local text = self:get_text(line1, col1, line2, col2) + if text ~= "" then result[#result + 1] = text end + end + end + return table.concat(result, "\n") +end + function Doc:has_selection() - local line1, col1, line2, col2 = self:get_selection(false) - return line1 ~= line2 or col1 ~= col2 + local line1, col1, line2, col2 = self:get_selection(false) return line1 ~= line2 or col1 ~= col2 end function Doc:sanitize_selection() From fb907c9bf4ffbcb84f9b7b1b1e010cf66ac88fd8 Mon Sep 17 00:00:00 2001 From: Takase <20792268+takase1121@users.noreply.github.com> Date: Tue, 17 Aug 2021 20:16:56 +0800 Subject: [PATCH 102/180] increase code readibility --- 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 ae948479..4a231295 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -138,7 +138,8 @@ function Doc:get_selection_text(limit) end function Doc:has_selection() - local line1, col1, line2, col2 = self:get_selection(false) return line1 ~= line2 or col1 ~= col2 + local line1, col1, line2, col2 = self:get_selection(false) + return line1 ~= line2 or col1 ~= col2 end function Doc:sanitize_selection() From fc4c7a29eee4cb0c775454ce700f32f49fd936f4 Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Mon, 23 Aug 2021 19:10:13 +0200 Subject: [PATCH 103/180] use dependency fallbacks, use system reproc if available --- lib/font_renderer/meson.build | 6 +----- meson.build | 13 ++----------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/lib/font_renderer/meson.build b/lib/font_renderer/meson.build index 7724d584..d596e152 100644 --- a/lib/font_renderer/meson.build +++ b/lib/font_renderer/meson.build @@ -1,10 +1,6 @@ freetype_dep = dependency('freetype2') -libagg_dep = dependency('libagg', required: false) -if not libagg_dep.found() - libagg_subproject = subproject('libagg') - libagg_dep = libagg_subproject.get_variable('libagg_dep') -endif +libagg_dep = dependency('libagg', fallback: ['libagg', 'libagg_dep']) font_renderer_sources = [ 'agg_font_freetype.cpp', diff --git a/meson.build b/meson.build index 5dee12ec..20067d43 100644 --- a/meson.build +++ b/meson.build @@ -51,24 +51,15 @@ endif #=============================================================================== libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) -lua_dep = dependency('lua5.2', required : false) +lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep']) pcre2_dep = dependency('libpcre2-8') sdl_dep = dependency('sdl2', method: 'config-tool') - -if not lua_dep.found() - lua_subproject = subproject('lua', - default_options: ['shared=false', 'use_readline=false', 'app=false'] - ) - lua_dep = lua_subproject.get_variable('lua_dep') -endif - -reproc_subproject = subproject('reproc', +reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], default_options: [ 'default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false' ] ) -reproc_dep = reproc_subproject.get_variable('reproc_dep') lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] From e23b6176f4921a9f086cb94ee81b79a73ed414f2 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 24 Aug 2021 19:04:48 +0200 Subject: [PATCH 104/180] Fix lua subproject options removed by error --- meson.build | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 20067d43..0c5bc865 100644 --- a/meson.build +++ b/meson.build @@ -51,7 +51,9 @@ endif #=============================================================================== libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) -lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep']) +lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], + default_options: ['shared=false', 'use_readline=false', 'app=false'] +) pcre2_dep = dependency('libpcre2-8') sdl_dep = dependency('sdl2', method: 'config-tool') reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], From 7f338fc993b2edc940268e884e638e5d2072ace8 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 27 Aug 2021 19:03:41 +0200 Subject: [PATCH 105/180] Allow tabs to always be visible --- data/core/config.lua | 1 + data/core/rootview.lua | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/data/core/config.lua b/data/core/config.lua index 401ac0af..caecdfcd 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -12,6 +12,7 @@ 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.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 diff --git a/data/core/rootview.lua b/data/core/rootview.lua index f2499e68..9d017268 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -261,7 +261,7 @@ end function Node:get_tab_overlapping_point(px, py) - if #self.views == 1 then return nil end + if not self:should_show_tabs() then return nil end local tabs_number = self:get_visible_tabs_number() local x1, y1, w, h = self:get_tab_rect(self.tab_offset) local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number) @@ -271,6 +271,16 @@ function Node:get_tab_overlapping_point(px, py) end +function Node:should_show_tabs() + if self.locked then return false end + if #self.views > 1 then return true + elseif config.always_show_tabs then + return not self.views[1]:is(EmptyView) + end + return false +end + + local function close_button_location(x, w) local cw = style.icon_font:get_width("C") local pad = style.padding.y @@ -414,7 +424,7 @@ end function Node:update_layout() if self.type == "leaf" then local av = self.active_view - if #self.views > 1 then + if self:should_show_tabs() then local _, _, _, th = self:get_tab_rect(1) av.position.x, av.position.y = self.position.x, self.position.y + th av.size.x, av.size.y = self.size.x, self.size.y - th @@ -569,7 +579,7 @@ end function Node:draw() if self.type == "leaf" then - if #self.views > 1 then + if self:should_show_tabs() then self:draw_tabs() end local pos, size = self.active_view.position, self.active_view.size From d3bd35b577b3d65f5a0704566219e4cecf1c9cc3 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 29 Aug 2021 02:23:47 +0200 Subject: [PATCH 106/180] Use plain `string:find` when matching plugin directories --- 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 702e31fd..5b1cca26 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -692,7 +692,7 @@ function core.load_plugins() if is_lua_file then if not version_match then core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local list = refused_list[plugin_dir:find(USERDIR) == 1 and 'userdir' or 'datadir'].plugins + local list = refused_list[plugin_dir:find(USERDIR, 1, true) == 1 and 'userdir' or 'datadir'].plugins table.insert(list, filename) end if version_match and config.plugins[basename] ~= false then From e93bbc559c5dda25a2aab870a49b76f64124e681 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sun, 29 Aug 2021 04:02:53 +0200 Subject: [PATCH 107/180] Fix crash in project search when project has no files --- data/plugins/projectsearch.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 45967697..dda3a2d0 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -171,7 +171,7 @@ function ResultsView:draw() local ox, oy = self:get_content_offset() local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() - local per = files_number and self.last_file_idx / files_number or 1 + local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1) local text if self.searching then if files_number then From ea795aa411ef814c5db3fb0d70619184f0e520cc Mon Sep 17 00:00:00 2001 From: boppyt <71049646+boppyt@users.noreply.github.com> Date: Sat, 28 Aug 2021 22:37:02 -0700 Subject: [PATCH 108/180] build script: add .git fallback --- build-packages.sh | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index f30b1112..039e56f8 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -10,7 +10,16 @@ copy_directory_from_repo () { fi local dirname="$1" local destdir="$2" - git archive "$use_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" + _archive "$use_branch" "$dirname" "$destdir" "${tar_options[@]}" +} + +_archive () { + if [[ -d ".git" ]]; then + git archive "$1" "$2" --format=tar | tar xf - -C "$3" "$4" + else + echo ".git not found, falling back to UNIX operation" + cp -r "$2" "$3" + fi } # Check if build directory is ok to be used to build. @@ -20,10 +29,14 @@ build_dir_is_usable () { echo "invalid build directory, no path allowed: \"$build\"" return 1 fi - git ls-files --error-unmatch "$build" &> /dev/null - if [ $? == 0 ]; then - echo "invalid path, \"$build\" is under revision control" - return 1 + if [[ -d ".git" ]]; then + git ls-files --error-unmatch "$build" &> /dev/null + if [ $? == 0 ]; then + echo "invalid path, \"$build\" is under revision control" + return 1 + fi + else + echo ".git not found, skipping check for revision control" fi } @@ -214,7 +227,12 @@ if [ -z ${arch+set} ]; then fi if [ -z ${use_branch+set} ]; then - use_branch="$(git rev-parse --abbrev-ref HEAD)" + if [[ -d ".git" ]]; then + use_branch="$(git rev-parse --abbrev-ref HEAD)" + else + # it really doesn't matter if git isn't present + use_branch="master" + fi fi build_dir=".build-$arch" From e34ec195b42960a1e7ab49285bc46d1ee55cc74a Mon Sep 17 00:00:00 2001 From: boppyt <71049646+boppyt@users.noreply.github.com> Date: Sat, 28 Aug 2021 23:31:49 -0700 Subject: [PATCH 109/180] build script: check for BUILD_PROHIBIT_GIT --- build-packages.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index 039e56f8..366882e6 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -14,7 +14,7 @@ copy_directory_from_repo () { } _archive () { - if [[ -d ".git" ]]; then + if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then git archive "$1" "$2" --format=tar | tar xf - -C "$3" "$4" else echo ".git not found, falling back to UNIX operation" @@ -29,7 +29,7 @@ build_dir_is_usable () { echo "invalid build directory, no path allowed: \"$build\"" return 1 fi - if [[ -d ".git" ]]; then + if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then git ls-files --error-unmatch "$build" &> /dev/null if [ $? == 0 ]; then echo "invalid path, \"$build\" is under revision control" @@ -227,7 +227,7 @@ if [ -z ${arch+set} ]; then fi if [ -z ${use_branch+set} ]; then - if [[ -d ".git" ]]; then + if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then use_branch="$(git rev-parse --abbrev-ref HEAD)" else # it really doesn't matter if git isn't present From 97ca19c73fa062600293c994c2488b413bc38957 Mon Sep 17 00:00:00 2001 From: Zack A Date: Sun, 29 Aug 2021 16:16:14 -0700 Subject: [PATCH 110/180] build script: check if .git is present --- build-packages.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index 366882e6..ed85e175 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -14,10 +14,9 @@ copy_directory_from_repo () { } _archive () { - if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then + if [[ $git_available -eq 1 ]]; then git archive "$1" "$2" --format=tar | tar xf - -C "$3" "$4" else - echo ".git not found, falling back to UNIX operation" cp -r "$2" "$3" fi } @@ -29,14 +28,12 @@ build_dir_is_usable () { echo "invalid build directory, no path allowed: \"$build\"" return 1 fi - if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then + if [[ $git_available -eq 1 ]]; then git ls-files --error-unmatch "$build" &> /dev/null if [ $? == 0 ]; then echo "invalid path, \"$build\" is under revision control" return 1 fi - else - echo ".git not found, skipping check for revision control" fi } @@ -203,7 +200,6 @@ lite_copy_third_party_modules () { mv "$build/lite-colors-master/colors" "$build/third/data" rm -fr "$build/lite-colors-master" } - unset arch while [ ! -z {$1+x} ]; do case $1 in @@ -226,8 +222,15 @@ if [ -z ${arch+set} ]; then exit 1 fi + if [[ -d ".git" || $BUILD_PROHIBIT_GIT -ne 0 ]]; then + git_available=1; + else + echo "Use of git prohibited. Either '.git' wasn't found or BUILD_PROHIBIT_GIT was set." + git_available=0; + fi + if [ -z ${use_branch+set} ]; then - if [[ -d ".git" || $BUILD_PROHIBIT_GIT -eq 1 ]]; then + if [[ $git_available -eq 1 ]]; then use_branch="$(git rev-parse --abbrev-ref HEAD)" else # it really doesn't matter if git isn't present From 8dde1dbb8649f33e08e5d44e4931c3ede0ce0407 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 30 Aug 2021 14:27:31 +0200 Subject: [PATCH 111/180] Add renderer build options in build-packages.sh --- build-packages.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/build-packages.sh b/build-packages.sh index ed85e175..ffe3537b 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -42,7 +42,7 @@ lite_build () { local build="$1" build_dir_is_usable "$build" || exit 1 rm -fr "$build" - setup_options=() + setup_options=("${@:2}") if [[ "$OSTYPE" == "darwin"* ]]; then setup_options+=(-Dbundle=true) fi @@ -55,7 +55,8 @@ lite_build_pgo () { local build="$1" build_dir_is_usable "$build" || exit 1 rm -fr "$build" - meson setup --buildtype=release -Db_pgo=generate "$build" || exit 1 + echo meson setup --buildtype=release "${@:2}" -Db_pgo=generate "$build" + meson setup --buildtype=release "${@:2}" -Db_pgo=generate "$build" || exit 1 ninja -C "$build" || exit 1 copy_directory_from_repo data "$build/src" "$build/src/lite-xl" @@ -200,9 +201,15 @@ lite_copy_third_party_modules () { mv "$build/lite-colors-master/colors" "$build/third/data" rm -fr "$build/lite-colors-master" } + +build_opts=() unset arch while [ ! -z {$1+x} ]; do case $1 in + -renderer) + build_opts+=("-Drenderer=true") + shift + ;; -pgo) pgo=true shift @@ -241,9 +248,9 @@ fi build_dir=".build-$arch" if [ -z ${pgo+set} ]; then - lite_build "$build_dir" + lite_build "$build_dir" "${build_opts[@]}" else - lite_build_pgo "$build_dir" + lite_build_pgo "$build_dir" "${build_opts[@]}" fi lite_copy_third_party_modules "$build_dir" lite_build_package "$build_dir" "$arch" From 68c1cc606fda70769db200d79421db2311d4313f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 30 Aug 2021 14:35:42 +0200 Subject: [PATCH 112/180] Bring back min len autocomplete default to 3 --- data/plugins/autocomplete.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index c41f233d..484199a9 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -12,7 +12,7 @@ local Doc = require "core.doc" config.plugins.autocomplete = { -- Amount of characters that need to be written for autocomplete - min_len = 1, + min_len = 3, -- The max amount of visible items max_height = 6, -- The max amount of scrollable items From 9d4e944549b48ae2c311860f913bc1f217fa6748 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 28 Aug 2021 13:42:06 -0400 Subject: [PATCH 113/180] Added in two new VSC-style multicursor shortcuts. --- data/core/commands/findreplace.lua | 32 ++++++++++++++++++++++++------ data/core/keymap-macos.lua | 1 + data/core/keymap.lua | 1 + 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 6dd7ddae..b9a424fa 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -96,13 +96,33 @@ local function has_selection() return core.active_view:is(DocView) and core.active_view.doc:has_selection() end -command.add(has_selection, { - ["find-replace:select-next"] = function() - local l1, c1, l2, c2 = doc():get_selection(true) - local text = doc():get_text(l1, c1, l2, c2) - l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) - if l2 then doc():set_selection(l2, c2, l1, c1) end +local function has_unique_selection() + 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 + local selection = doc():get_text(line1, col1, line2, col2) + if text ~= nil and text ~= selection then return false end + text = selection end + return text ~= nil +end + +local function select_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) + repeat + l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) + if l1 == il1 and c1 == ic1 then break end + if l2 then doc():add_selection(l2, c2, l1, c1) end + until not all or not l2 + break + end +end + +command.add(has_unique_selection, { + ["find-replace:select-next"] = function() select_next(false) end, + ["find-replace:select-all"] = function() select_next(true) end }) command.add("core.docview", { diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 647cb132..89f68949 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -66,6 +66,7 @@ local function keymap_macos(keymap) ["cmd+a"] = "doc:select-all", ["cmd+d"] = { "find-replace:select-next", "doc:select-word" }, ["cmd+l"] = "doc:select-lines", + ["cmd+shift+l"] = { "find-replace:select-all", "doc:select-word" }, ["cmd+/"] = "doc:toggle-line-comments", ["cmd+up"] = "doc:move-lines-up", ["cmd+down"] = "doc:move-lines-down", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 0b08259d..2be0dfc7 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-next", "doc:select-word" }, ["ctrl+l"] = "doc:select-lines", + ["ctrl+shift+l"] = { "find-replace:select-all", "doc:select-word" }, ["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+up"] = "doc:move-lines-up", ["ctrl+down"] = "doc:move-lines-down", From d352eb1cb9169576f2ce8972b8bcab3b208bf204 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 29 Aug 2021 17:54:57 -0400 Subject: [PATCH 114/180] Fixed cursors moving around with removal and inserts with cursors. Also fixed drawing line highlights with multicursors. --- data/core/doc/init.lua | 19 +++++++++++++++++++ data/core/docview.lua | 10 +++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 4a231295..269d6d12 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -322,6 +322,7 @@ end function Doc:raw_insert(line, col, text, undo_stack, time) -- split text into lines and merge with line at insertion point local lines = split_lines(text) + local len = #lines[#lines] local before = self.lines[line]:sub(1, col - 1) local after = self.lines[line]:sub(col) for i = 1, #lines - 1 do @@ -338,6 +339,15 @@ function Doc:raw_insert(line, col, text, undo_stack, time) push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "remove", line, col, line2, col2) + -- keep cursors where they should be + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true) do + if cline1 >= line then + local line_addition = line > cline1 or ccol1 > col and #lines - 1 or 0 + local column_addition = line == cline1 and ccol1 > col and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + end + end + -- update highlighter and assure selection is in bounds self.highlighter:invalidate(line) self:sanitize_selection() @@ -356,6 +366,15 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- splice line into line array common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) + + -- move all cursors back if they share a line with the removed text + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true) do + if cline1 >= line2 then + local line_removal = line2 - line1 + local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0 + self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal) + end + end -- update highlighter and assure selection is in bounds self.highlighter:invalidate(line1) diff --git a/data/core/docview.lua b/data/core/docview.lua index 17fb534a..161eac47 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -367,16 +367,20 @@ function DocView:draw_line_body(idx, x, y) local x1 = x + self:get_col_x_offset(idx, col1) local x2 = x + self:get_col_x_offset(idx, col2) local lh = self:get_line_height() - renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + if x1 ~= x2 then + renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + 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 config.highlight_current_line and (line1 == line2 and col1 == col2) + if draw_highlight ~= false and config.highlight_current_line and line1 == idx and core.active_view == self then - self:draw_line_highlight(x + self.scroll.x, y) + 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 1d61cf989fd6d7ef023763acf7e1fd5459f49893 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 29 Aug 2021 20:05:58 -0400 Subject: [PATCH 115/180] Fixed cursor movement. --- data/core/doc/init.lua | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 269d6d12..ea219a8a 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -333,21 +333,20 @@ function Doc:raw_insert(line, col, text, undo_stack, time) -- splice lines into line array common.splice(self.lines, line, 1, lines) + + -- keep cursors where they should be + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line then break end + local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 + local column_addition = line == cline1 and ccol1 > col and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + end -- 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, "remove", line, col, line2, col2) - -- keep cursors where they should be - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true) do - if cline1 >= line then - local line_addition = line > cline1 or ccol1 > col and #lines - 1 or 0 - local column_addition = line == cline1 and ccol1 > col and len or 0 - self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) - end - end - -- update highlighter and assure selection is in bounds self.highlighter:invalidate(line) self:sanitize_selection() @@ -368,12 +367,11 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) -- move all cursors back if they share a line with the removed text - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true) do - if cline1 >= line2 then - local line_removal = line2 - line1 - local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0 - self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal) - end + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line2 then break end + local line_removal = line2 - line1 + local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0 + self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal) end -- update highlighter and assure selection is in bounds @@ -411,7 +409,7 @@ end function Doc:text_input(text, idx) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do if line1 ~= line2 or col1 ~= col2 then self:delete_to_cursor(sidx) end From 604626fa32380880bb73a210a4366337c6ab9d9b Mon Sep 17 00:00:00 2001 From: Timofffee Date: Mon, 30 Aug 2021 16:21:16 +0400 Subject: [PATCH 116/180] Fix macOS keymap --- data/core/keymap-macos.lua | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 89f68949..7f54ddbb 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -52,12 +52,14 @@ local function keymap_macos(keymap) ["shift+tab"] = "doc:unindent", ["backspace"] = "doc:backspace", ["shift+backspace"] = "doc:backspace", - ["cmd+backspace"] = "doc:delete-to-previous-word-start", + ["option+backspace"] = "doc:delete-to-previous-word-start", ["cmd+shift+backspace"] = "doc:delete-to-previous-word-start", + ["cmd+backspace"] = "doc:delete-to-start-of-indentation", ["delete"] = "doc:delete", ["shift+delete"] = "doc:delete", - ["cmd+delete"] = "doc:delete-to-next-word-end", + ["option+delete"] = "doc:delete-to-next-word-end", ["cmd+shift+delete"] = "doc:delete-to-next-word-end", + ["cmd+delete"] = "doc:delete-to-end-of-line", ["return"] = { "command:submit", "doc:newline", "dialog:select" }, ["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" }, ["cmd+return"] = "doc:newline-below", @@ -68,8 +70,8 @@ local function keymap_macos(keymap) ["cmd+l"] = "doc:select-lines", ["cmd+shift+l"] = { "find-replace:select-all", "doc:select-word" }, ["cmd+/"] = "doc:toggle-line-comments", - ["cmd+up"] = "doc:move-lines-up", - ["cmd+down"] = "doc:move-lines-down", + ["option+up"] = "doc:move-lines-up", + ["option+down"] = "doc:move-lines-down", ["cmd+shift+d"] = "doc:duplicate-lines", ["cmd+shift+k"] = "doc:delete-lines", @@ -77,14 +79,16 @@ local function keymap_macos(keymap) ["right"] = { "doc:move-to-next-char", "dialog:next-entry"}, ["up"] = { "command:select-previous", "doc:move-to-previous-line" }, ["down"] = { "command:select-next", "doc:move-to-next-line" }, - ["cmd+left"] = "doc:move-to-previous-word-start", - ["cmd+right"] = "doc:move-to-next-word-end", + ["option+left"] = "doc:move-to-previous-word-start", + ["option+right"] = "doc:move-to-next-word-end", + ["cmd+left"] = "doc:move-to-start-of-indentation", + ["cmd+right"] = "doc:move-to-end-of-line", ["cmd+["] = "doc:move-to-previous-block-start", ["cmd+]"] = "doc:move-to-next-block-end", ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", - ["cmd+home"] = "doc:move-to-start-of-doc", - ["cmd+end"] = "doc:move-to-end-of-doc", + ["cmd+up"] = "doc:move-to-start-of-doc", + ["cmd+down"] = "doc:move-to-end-of-doc", ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", @@ -92,18 +96,20 @@ local function keymap_macos(keymap) ["shift+right"] = "doc:select-to-next-char", ["shift+up"] = "doc:select-to-previous-line", ["shift+down"] = "doc:select-to-next-line", - ["cmd+shift+left"] = "doc:select-to-previous-word-start", - ["cmd+shift+right"] = "doc:select-to-next-word-end", + ["option+shift+left"] = "doc:select-to-previous-word-start", + ["option+shift+right"] = "doc:select-to-next-word-end", + ["cmd+shift+left"] = "doc:select-to-start-of-indentation", + ["cmd+shift+right"] = "doc:select-to-end-of-line", ["cmd+shift+["] = "doc:select-to-previous-block-start", ["cmd+shift+]"] = "doc:select-to-next-block-end", ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", - ["cmd+shift+home"] = "doc:select-to-start-of-doc", - ["cmd+shift+end"] = "doc:select-to-end-of-doc", + ["cmd+shift+up"] = "doc:select-to-start-of-doc", + ["cmd+shift+down"] = "doc:select-to-end-of-doc", ["shift+pageup"] = "doc:select-to-previous-page", ["shift+pagedown"] = "doc:select-to-next-page", - ["cmd+shift+up"] = "doc:create-cursor-previous-line", - ["cmd+shift+down"] = "doc:create-cursor-next-line" + ["cmd+option+up"] = "doc:create-cursor-previous-line", + ["cmd+option+down"] = "doc:create-cursor-next-line" } end From a75aca25380b404a2ccda61277071b7b86e1914e Mon Sep 17 00:00:00 2001 From: redtide Date: Tue, 31 Aug 2021 14:45:00 +0200 Subject: [PATCH 117/180] Renamed freedesktop resources --- build-packages.sh | 4 ++-- meson.build | 4 ++-- ...lite-xl.appdata.xml => org.lite_xl.lite_xl.appdata.xml} | 7 ++++++- ...lite-xl.lite-xl.desktop => org.lite_xl.lite_xl.desktop} | 2 +- scripts/appimage.sh | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) rename resources/linux/{org.lite-xl.lite-xl.appdata.xml => org.lite_xl.lite_xl.appdata.xml} (83%) rename resources/linux/{org.lite-xl.lite-xl.desktop => org.lite_xl.lite_xl.desktop} (80%) diff --git a/build-packages.sh b/build-packages.sh index ffe3537b..424d60ba 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -167,8 +167,8 @@ lite_build_package_linux () { strip "$bindir/lite-xl" if [ -z "$portable" ]; then mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" "$pdir/share/metainfo" - cp "resources/linux/org.lite-xl.lite-xl.desktop" "$pdir/share/applications" - cp "resources/linux/org.lite-xl.lite-xl.appdata.xml" "$pdir/share/metainfo" + cp "resources/linux/org.lite_xl.lite_xl.desktop" "$pdir/share/applications" + cp "resources/linux/org.lite_xl.lite_xl.appdata.xml" "$pdir/share/metainfo" cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg" fi pushd ".package-build" diff --git a/meson.build b/meson.build index 0c5bc865..3d5f9da9 100644 --- a/meson.build +++ b/meson.build @@ -92,10 +92,10 @@ else install_data('resources/icons/lite-xl.svg', install_dir : 'share/icons/hicolor/scalable/apps' ) - install_data('resources/linux/org.lite-xl.lite-xl.desktop', + install_data('resources/linux/org.lite_xl.lite_xl.desktop', install_dir : 'share/applications' ) - install_data('resources/linux/org.lite-xl.lite-xl.appdata.xml', + install_data('resources/linux/org.lite_xl.lite_xl.appdata.xml', install_dir : 'share/metainfo' ) endif diff --git a/resources/linux/org.lite-xl.lite-xl.appdata.xml b/resources/linux/org.lite_xl.lite_xl.appdata.xml similarity index 83% rename from resources/linux/org.lite-xl.lite-xl.appdata.xml rename to resources/linux/org.lite_xl.lite_xl.appdata.xml index d2d0a73a..c5895178 100644 --- a/resources/linux/org.lite-xl.lite-xl.appdata.xml +++ b/resources/linux/org.lite_xl.lite_xl.appdata.xml @@ -1,10 +1,11 @@ - org.lite-xl.lite-xl + org.lite_xl.lite_xl MIT MIT Lite XL A lightweight text editor written in Lua +

@@ -25,4 +26,8 @@ lite-xl + + + + diff --git a/resources/linux/org.lite-xl.lite-xl.desktop b/resources/linux/org.lite_xl.lite_xl.desktop similarity index 80% rename from resources/linux/org.lite-xl.lite-xl.desktop rename to resources/linux/org.lite_xl.lite_xl.desktop index f2fa9610..c09fb5db 100644 --- a/resources/linux/org.lite-xl.lite-xl.desktop +++ b/resources/linux/org.lite_xl.lite_xl.desktop @@ -6,5 +6,5 @@ Exec=lite-xl %F Icon=lite-xl Terminal=false StartupNotify=false -Categories=Utility;TextEditor;Development; +Categories=Development;IDE; MimeType=text/plain; diff --git a/scripts/appimage.sh b/scripts/appimage.sh index b2cc5cd7..65dce3c4 100644 --- a/scripts/appimage.sh +++ b/scripts/appimage.sh @@ -117,7 +117,7 @@ generate_appimage(){ mv AppRun LiteXL.AppDir/ # These could be symlinks but it seems they doesn't work with AppimageLauncher cp resources/icons/lite-xl.svg LiteXL.AppDir/ - cp resources/linux/org.lite-xl.lite-xl.desktop LiteXL.AppDir/ + cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ if [[ $STATIC_BUILD == false ]]; then echo "Copying libraries..." From 7811660cafd01a977814378d57973b36f4cf15aa Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 30 Aug 2021 22:56:33 -0400 Subject: [PATCH 118/180] Fixed replace to make it multicursor-aware. --- data/core/doc/init.lua | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index ea219a8a..47869d5a 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -418,12 +418,7 @@ function Doc:text_input(text, idx) end end - -function Doc:replace(fn) - local line1, col1, line2, col2 = self:get_selection(true) - if line1 == line2 and col1 == col2 then - line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] - end +function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) local old_text = self:get_text(line1, col1, line2, col2) local new_text, n = fn(old_text) if old_text ~= new_text then @@ -431,12 +426,26 @@ function Doc:replace(fn) self:remove(line1, col1, line2, col2) if line1 == line2 and col1 == col2 then line2, col2 = self:position_offset(line1, col1, #new_text) - self:set_selection(line1, col1, line2, col2) + self:set_selections(idx, line1, col1, line2, col2) end end return n end +function Doc:replace(fn) + local has_selection = false + for idx, line1, col1, line2, col2 in self:get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + self:replace_cursor(idx, line1, col1, line2, col2, fn) + has_selection = true + end + end + if not has_selection then + self:set_selection(table.unpack(self.selections)) + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) + end +end + function Doc:delete_to_cursor(idx, ...) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do From b7f2d1ad03bc39df8fbe227d77934c10b2b64ee8 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 31 Aug 2021 16:21:40 -0400 Subject: [PATCH 119/180] Forgot to return an 'n'. --- data/core/doc/init.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 47869d5a..aff31e94 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -433,17 +433,18 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) end function Doc:replace(fn) - local has_selection = false + local has_selection, n = false, 0 for idx, line1, col1, line2, col2 in self:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then - self:replace_cursor(idx, line1, col1, line2, col2, fn) + n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn) has_selection = true end end if not has_selection then self:set_selection(table.unpack(self.selections)) - self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) + n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) end + return n end From de4072e207094f872be0e924c38755e02fc50624 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 1 Sep 2021 16:29:47 +0200 Subject: [PATCH 120/180] Avoid checking for unique selections on non-`DocView`s --- 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 b9a424fa..48cbecaf 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -96,7 +96,8 @@ local function has_selection() return core.active_view:is(DocView) and core.active_view.doc:has_selection() end -local function has_unique_selection() +local function has_unique_selection() + if not core.active_view:is(DocView) 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 11521ff883aa28446f1af551d880d6df7df9c069 Mon Sep 17 00:00:00 2001 From: redtide Date: Wed, 1 Sep 2021 19:42:09 +0200 Subject: [PATCH 121/180] Specify the WM_CLASS of the application, used by some Linux DEs --- resources/linux/org.lite_xl.lite_xl.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/linux/org.lite_xl.lite_xl.desktop b/resources/linux/org.lite_xl.lite_xl.desktop index c09fb5db..d251c4dc 100644 --- a/resources/linux/org.lite_xl.lite_xl.desktop +++ b/resources/linux/org.lite_xl.lite_xl.desktop @@ -5,6 +5,6 @@ Comment=A lightweight text editor written in Lua Exec=lite-xl %F Icon=lite-xl Terminal=false -StartupNotify=false +StartupWMClass=lite-xl Categories=Development;IDE; MimeType=text/plain; From 59aa7f0090ecbd0049d15fdc2b9154ee857c1d8d Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 2 Sep 2021 18:58:24 +0200 Subject: [PATCH 122/180] Fix absolute path detection in `core.project_absolute_path` --- 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 5b1cca26..3bf10211 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -809,7 +809,7 @@ end -- This function should get only filenames normalized using -- common.normalize_path function. function core.project_absolute_path(filename) - if filename:match('^%a:\\') or filename:find('/', 1, true) then + if filename:match('^%a:\\') or filename:find('/', 1, true) == 1 then return filename else return core.project_dir .. PATHSEP .. filename From 9887dd47469f83ceb5c10c546a5df62361bf4fa5 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 2 Sep 2021 19:09:29 +0200 Subject: [PATCH 123/180] Make open files follow renames --- data/plugins/treeview.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index f9a67aaf..9bb597f2 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -441,11 +441,24 @@ command.add(nil, { ["treeview:rename"] = function() local old_filename = view.hovered_item.filename + local old_abs_filename = view.hovered_item.abs_filename core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - os.rename(old_filename, filename) + filename = core.normalize_to_project_dir(filename) + local abs_filename = core.project_absolute_path(filename) + local res, err = os.rename(old_abs_filename, abs_filename) + if res then -- successfully renamed + 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 + break -- only first needed + end + end + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + else + core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) + end core.reschedule_project_scan() - core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) end, common.path_suggest) end, From 3278eebdad8c30c337e2e7a77087f3ba30cb00af Mon Sep 17 00:00:00 2001 From: Guldoman Date: Thu, 2 Sep 2021 19:12:26 +0200 Subject: [PATCH 124/180] Avoid exposing `treeview` commands when not usable --- data/plugins/treeview.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 9bb597f2..48d6f5a7 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -437,8 +437,10 @@ menu:register( command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible - end, + end}) + +command.add(function() return view.hovered_item ~= nil end, { ["treeview:rename"] = function() local old_filename = view.hovered_item.filename local old_abs_filename = view.hovered_item.abs_filename From 8d355bd3a058028306d1c3c004c069aa717435aa Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 3 Sep 2021 23:33:36 +0200 Subject: [PATCH 125/180] Add missing release flag in build-packages.sh --- build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-packages.sh b/build-packages.sh index 424d60ba..666fb9e2 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -46,7 +46,7 @@ lite_build () { if [[ "$OSTYPE" == "darwin"* ]]; then setup_options+=(-Dbundle=true) fi - meson setup "${setup_options[@]}" "$build" || exit 1 + meson setup --buildtype=release "${setup_options[@]}" "$build" || exit 1 ninja -C "$build" || exit 1 } From f9c7eeeeb8265b6f741cde5cabdb060c2a57c630 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 4 Sep 2021 00:32:01 +0200 Subject: [PATCH 126/180] Check if session file returned anything --- data/core/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 3bf10211..6b770fd6 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -17,7 +17,7 @@ local core = {} local function load_session() local ok, t = pcall(dofile, USERDIR .. "/session.lua") - if ok then + if ok and t then return t.recents, t.window, t.window_mode end return {} @@ -441,7 +441,7 @@ function core.init() elseif window_mode == "maximized" then system.set_window_mode("maximized") end - core.recent_projects = recent_projects + core.recent_projects = recent_projects or {} end local project_dir = core.recent_projects[1] or "." From de038a633d140a4fa3be89cd1f3de7fac2d85146 Mon Sep 17 00:00:00 2001 From: Francesco Date: Sat, 4 Sep 2021 17:24:08 +0200 Subject: [PATCH 127/180] Update README to remove reference to notarization We assume we don't need to mention the macOS package is notarized. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 339747fd..32f63d70 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A lightweight text editor written in Lua, adapted from [lite]. -* **[Get Lite XL]** โ€” Download for Windows, Linux and Mac OS (notarized app). +* **[Get Lite XL]** โ€” Download for Windows, Linux and Mac OS. * **[Get plugins]** โ€” Add additional functionality, adapted for Lite XL. * **[Get color themes]** โ€” Add additional colors themes. From 9246a16e67501c62593018d14d3eae91e8d09589 Mon Sep 17 00:00:00 2001 From: Timofffee Date: Fri, 3 Sep 2021 15:50:44 +0400 Subject: [PATCH 128/180] Bring back info.plist with meson configuration --- meson.build | 8 +++++++- resources/macos/{Info.plist => Info.plist.in} | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) rename resources/macos/{Info.plist => Info.plist.in} (96%) diff --git a/meson.build b/meson.build index 3d5f9da9..7e9b4ab1 100644 --- a/meson.build +++ b/meson.build @@ -83,7 +83,13 @@ elif get_option('bundle') and host_machine.system() == 'darwin' lite_docdir = 'Contents/Resources' lite_datadir = 'Contents/Resources' install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources') - install_data('resources/macos/Info.plist', install_dir : 'Contents') + configure_file( + input : 'resources/macos/Info.plist.in', + output : 'Info.plist', + configuration : conf_data, + install : true, + install_dir : 'Contents' + ) else lite_bindir = 'bin' lite_docdir = 'share/doc/lite-xl' diff --git a/resources/macos/Info.plist b/resources/macos/Info.plist.in similarity index 96% rename from resources/macos/Info.plist rename to resources/macos/Info.plist.in index d465479c..c15fd566 100644 --- a/resources/macos/Info.plist +++ b/resources/macos/Info.plist.in @@ -19,8 +19,9 @@ NSDesktopFolderUsageDescriptionTo access, edit and index your projects. NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. CFBundleShortVersionString - 2.0 + @PROJECT_VERSION@ NSHumanReadableCopyright ยฉ 2019-2021 Francesco Abbate + From cc20849afdde351bcf5bd96a43b4586b212feca7 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 4 Sep 2021 18:08:04 +0200 Subject: [PATCH 129/180] Fix build-packages to use generated Info.plist --- build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-packages.sh b/build-packages.sh index 666fb9e2..6b874cd9 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -120,7 +120,7 @@ lite_build_package_macos () { cp -r "$build/third/data/$module_name" "$datadir" done cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns" - cp resources/macos/Info.plist "$appdir/Contents/Info.plist" + cp "$build/Info.plist" "$appdir/Contents/Info.plist" cp "$build/src/lite-xl" "$bindir/lite-xl" strip "$bindir/lite-xl" pushd ".package-build" From 91da2ddfdd8fb0a63ed30d2ef94c88e13f907f27 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 4 Sep 2021 18:15:07 +0200 Subject: [PATCH 130/180] Move innosetup meson config into scripts directory The purpose is to keep the main meson build file as simple as possible while keeping the innosetup config. --- meson.build | 8 +------- scripts/innosetup/innosetup.sh | 2 +- scripts/meson.build | 8 ++++++++ 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 scripts/meson.build diff --git a/meson.build b/meson.build index 7e9b4ab1..611d154a 100644 --- a/meson.build +++ b/meson.build @@ -14,13 +14,6 @@ conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) conf_data.set('PROJECT_VERSION', meson.project_version()) -if host_machine.system() == 'windows' - configure_file( - input : 'scripts/innosetup/innosetup.iss.in', - output : 'innosetup.iss', - configuration : conf_data - ) -endif #=============================================================================== # Compiler Settings #=============================================================================== @@ -127,3 +120,4 @@ configure_file( #=============================================================================== subdir('lib/font_renderer') subdir('src') +subdir('scripts') diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh index b2d0e86c..44a6c787 100644 --- a/scripts/innosetup/innosetup.sh +++ b/scripts/innosetup/innosetup.sh @@ -51,5 +51,5 @@ else ARCH=Win32 fi -/c/Program\ Files\ \(x86\)/Inno\ Setup\ 6/ISCC.exe -dARCH=$ARCH $BUILD_DIR/innosetup.iss +/c/Program\ Files\ \(x86\)/Inno\ Setup\ 6/ISCC.exe -dARCH=$ARCH $BUILD_DIR/scripts/innosetup.iss mv $BUILD_DIR/LiteXL*.exe $(pwd) diff --git a/scripts/meson.build b/scripts/meson.build new file mode 100644 index 00000000..8b45814d --- /dev/null +++ b/scripts/meson.build @@ -0,0 +1,8 @@ +if host_machine.system() == 'windows' + configure_file( + input : 'innosetup/innosetup.iss.in', + output : 'innosetup.iss', + configuration : conf_data + ) +endif + From 261ab5daf226a44271f45219bb3c6ec1fe8d6ff9 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 29 Aug 2021 10:15:55 +0200 Subject: [PATCH 131/180] Adapt all scripts to work together with build-packages.sh --- .github/workflows/build.yml | 121 ++++----- .gitignore | 2 + README.md | 16 +- build-packages.sh | 393 +++++++++++------------------ meson.build | 46 ++-- meson_options.txt | 1 + scripts/README.md | 28 ++ scripts/appimage.sh | 14 +- scripts/build.sh | 157 ++++++------ scripts/common.sh | 25 ++ scripts/innosetup/innosetup.iss.in | 19 +- scripts/innosetup/innosetup.sh | 82 +++--- scripts/install-dependencies.sh | 74 ++++++ scripts/lhelper.sh | 75 ++++++ scripts/msys2-package.sh | 56 ---- scripts/package.sh | 252 ++++++++++++++++++ scripts/source-package.sh | 69 ----- 17 files changed, 829 insertions(+), 601 deletions(-) create mode 100644 scripts/README.md create mode 100644 scripts/common.sh create mode 100644 scripts/install-dependencies.sh create mode 100644 scripts/lhelper.sh delete mode 100644 scripts/msys2-package.sh create mode 100644 scripts/package.sh delete mode 100644 scripts/source-package.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6eec7a63..2778de79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,26 +1,25 @@ name: CI +# All builds use lhelper only for releases, +# otherwise for normal builds dependencies are dynamically linked. + on: push: branches: - '*' - # Temporarily disabled - #tags: - #- 'v[0-9]*' +# tags: +# - 'v[0-9]*' pull_request: branches: - '*' jobs: - # Note: not using git-archive(-all) because it can't include subprojects ignored by git archive_source_code: name: Source Code Tarball runs-on: ubuntu-18.04 # Only on tags/releases if: startsWith(github.ref, 'refs/tags/') steps: - - name: Set Environment Variables - run: echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-src" >> "$GITHUB_ENV" - uses: actions/checkout@v2 - name: Python Setup uses: actions/setup-python@v2 @@ -30,16 +29,14 @@ jobs: run: | sudo apt-get install -qq ninja-build pip3 install meson - - name: Archive source code + - name: Package shell: bash - run: bash scripts/source-package.sh --destdir "${INSTALL_NAME}" + run: bash scripts/package.sh --version ${GITHUB_REF##*/} --debug --source - uses: actions/upload-artifact@v2 with: name: Source Code Tarball - path: ${{ env.INSTALL_NAME }}.tar.gz + path: "lite-xl-*-src.tar.gz" - # All builds use lhelper only for releases, using --static build argument, - # otherwise for normal builds dependencies are dynamically linked. build_linux: name: Linux runs-on: ubuntu-18.04 @@ -52,11 +49,12 @@ jobs: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} steps: - - name: Set Archive Name + - name: Set Environment Variables if: ${{ matrix.config.cc == 'gcc' }} run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)" >> "$GITHUB_ENV" - uses: actions/checkout@v2 - name: Python Setup uses: actions/setup-python@v2 @@ -64,27 +62,24 @@ jobs: python-version: 3.6 - name: Update Packages run: sudo apt-get update - - name: Install Meson - run: | - sudo apt-get install -qq ninja-build - pip3 install meson - name: Install Dependencies if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: sudo apt-get install -qq libsdl2-dev libfreetype6 + run: bash scripts/install-dependencies.sh --debug + - name: Install Release Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash scripts/install-dependencies.sh --debug --lhelper + bash scripts/lhelper.sh --debug - name: Build - if: ${{ matrix.config.cc == 'gcc' && !startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/build.sh --prefix / - - name: Release Build - if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/build.sh --prefix / --static + run: | + bash --version + bash scripts/build.sh --debug --forcefallback - name: Package if: ${{ matrix.config.cc == 'gcc' }} - run: | - DESTDIR="$(pwd)/$INSTALL_NAME" meson install --skip-subprojects -C build - tar czvf "${INSTALL_NAME}".tar.gz "${INSTALL_NAME}" + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary - name: AppImage if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/appimage.sh --nobuild --static --version ${{ env.INSTALL_REF }} + run: bash scripts/appimage.sh --nobuild --version ${INSTALL_REF} - name: Upload Artifacts uses: actions/upload-artifact@v2 if: ${{ matrix.config.cc == 'gcc' }} @@ -104,47 +99,42 @@ jobs: - name: System Information run: | system_profiler SPSoftwareDataType + bash --version gcc -v xcodebuild -version - - name: Set Archive Name + - name: Set Environment Variables run: | - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos" >> "$GITHUB_ENV" - bash --version + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV" - uses: actions/checkout@v2 - name: Python Setup uses: actions/setup-python@v2 with: python-version: 3.9 - - name: Install Build Tools - run: | - brew install ninja - pip3 install meson - cd ~; npm install appdmg; cd - - ~/node_modules/appdmg/bin/appdmg.js --version - name: Install Dependencies if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: brew install sdl2 - - name: Install LHelper Dependencies - if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: brew install bash md5sha1sum - - name: Build - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/build.sh --prefix "${GITHUB_WORKSPACE}/Lite XL.app" - - name: Release Build + run: bash scripts/install-dependencies.sh --debug + - name: Install Release Dependencies if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash scripts/install-dependencies.sh --debug --lhelper + bash scripts/lhelper.sh --debug + - name: Build run: | bash --version - bash scripts/build.sh --prefix "${GITHUB_WORKSPACE}/Lite XL.app" --static + bash scripts/build.sh --bundle --debug --forcefallback - name: Error Logs if: failure() run: | mkdir ${INSTALL_NAME} cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} - - name: Install - run: meson install --skip-subprojects -C build +# - name: Package +# if: ${{ !startsWith(github.ref, 'refs/tags/') }} +# run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons - name: Create DMG Image - run: bash scripts/appdmg.sh ${{ env.INSTALL_NAME }} + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg - name: Upload DMG Image uses: actions/upload-artifact@v2 with: @@ -178,34 +168,20 @@ jobs: git zip - name: Set Environment Variables - run: echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-$(echo $MSYSTEM | awk '{print tolower($0)}')" >> "$GITHUB_ENV" + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-$(uname -m)" >> "$GITHUB_ENV" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - name: Install Dependencies if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: | - pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-gcc \ - ${MINGW_PACKAGE_PREFIX}-meson \ - ${MINGW_PACKAGE_PREFIX}-ninja \ - ${MINGW_PACKAGE_PREFIX}-pkg-config \ - ${MINGW_PACKAGE_PREFIX}-freetype \ - ${MINGW_PACKAGE_PREFIX}-pcre2 \ - ${MINGW_PACKAGE_PREFIX}-SDL2 + run: bash scripts/install-dependencies.sh --debug - name: Install Release Dependencies if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: | - pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-gcc \ - ${MINGW_PACKAGE_PREFIX}-meson \ - ${MINGW_PACKAGE_PREFIX}-ninja \ - ${MINGW_PACKAGE_PREFIX}-pkg-config + run: bash scripts/install-dependencies.sh --debug --lhelper - name: Build - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/build.sh --prefix / - - name: Release Build - if: ${{ startsWith(github.ref, 'refs/tags/') }} run: | bash --version - bash scripts/build.sh --prefix / --static + bash scripts/build.sh --debug --forcefallback - name: Error Logs if: failure() run: | @@ -213,10 +189,10 @@ jobs: cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} - name: Package - run: bash scripts/msys2-package.sh --destdir ${INSTALL_NAME} + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary - name: Build Installer if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/innosetup/innosetup.sh + run: bash scripts/innosetup/innosetup.sh --debug - name: Upload Artifacts uses: actions/upload-artifact@v2 with: @@ -234,9 +210,8 @@ jobs: deploy: name: Deployment runs-on: ubuntu-18.04 - # Temporarily disabled +# if: startsWith(github.ref, 'refs/tags/') if: false - #if: startsWith(github.ref, 'refs/tags/') needs: - archive_source_code - build_linux diff --git a/.gitignore b/.gitignore index 7dc39e42..16974405 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ subprojects/reproc/ .lite-debug.log .run* *.diff +*.exe *.tar.gz *.zip *.DS_Store @@ -17,3 +18,4 @@ subprojects/reproc/ compile_commands.json error.txt lite-xl* +LiteXL* diff --git a/README.md b/README.md index 32f63d70..73204c57 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ If you compile Lite XL yourself, it is recommended to use the script `build-packages.sh`: ```sh -bash build-packages.sh +bash build-packages.sh -h ``` -The script will run Meson and create a zip file with the application or, -for Linux, a tar compressed archive. Lite XL can be easily installed +The script will run Meson and create a tar compressed archive with the application or, +for Windows, a zip file. Lite XL can be easily installed by unpacking the archive in any directory of your choice. Otherwise the following is an example of basic commands if you want to customize @@ -65,7 +65,15 @@ meson compile -C build DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build ``` -where `` might be one of `/`, `/usr` or `/opt`, the default is `/usr/local`. +where `` might be one of `/`, `/usr` or `/opt`, the default is `/`. +To build a bundle application on macOS: + +```sh +meson setup --buildtype=release --Dbundle=true --prefix / build +meson compile -C build +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. diff --git a/build-packages.sh b/build-packages.sh index 6b874cd9..6014be96 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -1,259 +1,156 @@ #!/bin/bash +set -e -# strip-components is normally set to 1 to strip the initial "data" from the -# directory path. -copy_directory_from_repo () { - local tar_options=() - if [[ $1 == --strip-components=* ]]; then - tar_options+=($1) - shift - fi - local dirname="$1" - local destdir="$2" - _archive "$use_branch" "$dirname" "$destdir" "${tar_options[@]}" +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Common options:" + echo + echo "-h --help Show this help and exit." + echo "-b --builddir DIRNAME Set the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '/'." + echo " --debug Debug this script." + echo + echo "Build options:" + echo + echo "-f --forcefallback Force to build subprojects dependencies statically." + echo "-B --bundle Create an App bundle (macOS only)" + echo "-P --portable Create a portable package." + echo + echo "Package options:" + echo + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + echo "-v --version VERSION Sets the version on the package name." + echo "-A --appimage Create an AppImage (Linux only)." + echo "-D --dmg Create a DMG disk image (macOS only)." + echo " Requires NPM and AppDMG." + echo "-I --innosetup Create an InnoSetup installer (Windows only)." + echo "-S --source Create a source code package," + echo " including subprojects dependencies." + echo } -_archive () { - if [[ $git_available -eq 1 ]]; then - git archive "$1" "$2" --format=tar | tar xf - -C "$3" "$4" - else - cp -r "$2" "$3" - fi -} +main() { + local build_dir + local build_dir_option=() + local dest_dir + local dest_dir_option=() + local prefix + local prefix_option=() + local version + local version_option=() + local debug + local force_fallback + local appimage + local bundle + local innosetup + local portable -# Check if build directory is ok to be used to build. -build_dir_is_usable () { - local build="$1" - if [[ $build == */* || -z "$build" ]]; then - echo "invalid build directory, no path allowed: \"$build\"" - return 1 - fi - if [[ $git_available -eq 1 ]]; then - git ls-files --error-unmatch "$build" &> /dev/null - if [ $? == 0 ]; then - echo "invalid path, \"$build\" is under revision control" - return 1 - fi - fi -} - -# Ordinary release build -lite_build () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - setup_options=("${@:2}") - if [[ "$OSTYPE" == "darwin"* ]]; then - setup_options+=(-Dbundle=true) - fi - meson setup --buildtype=release "${setup_options[@]}" "$build" || exit 1 - ninja -C "$build" || exit 1 -} - -# Build using Profile Guided Optimizations (PGO) -lite_build_pgo () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - echo meson setup --buildtype=release "${@:2}" -Db_pgo=generate "$build" - meson setup --buildtype=release "${@:2}" -Db_pgo=generate "$build" || exit 1 - ninja -C "$build" || exit 1 - copy_directory_from_repo data "$build/src" - "$build/src/lite-xl" - meson configure -Db_pgo=use "$build" - ninja -C "$build" || exit 1 -} - -lite_build_package_windows () { - local portable="-msys" - if [ "$1" == "-portable" ]; then - portable="" - shift - fi - local build="$1" - local arch="$2" - local os="win" - local pdir=".package-build/lite-xl" - if [ -z "$portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -f|--forcefallback) + force_fallback="--forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + version="$2" + shift + shift + ;; + -A|--appimage) + appimage="--appimage" + shift + ;; + -B|--bundle) + bundle="--bundle" + shift + ;; + -D|--dmg) + dmg="--dmg" + shift + ;; + -I|--innosetup) + innosetup="--innosetup" + shift + ;; + -P|--portable) + portable="--portable" + shift + ;; + -S|--source) + source="--source" + shift + ;; + --debug) + debug="--debug" + set -x + shift + ;; + *) + # unknown option + ;; + esac done - # copy the meson generated start.lua file - cp "$build/start.lua" "$datadir/core" - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl.exe" "$bindir" - strip --strip-all "$bindir/lite-xl.exe" - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.zip" - zip "$package_name" -r "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} -lite_build_package_macos () { - local build="$1" - local arch="$2" - local os="macos" - - local appdir=".package-build/lite-xl.app" - local bindir="$appdir/Contents/MacOS" - local datadir="$appdir/Contents/Resources" - mkdir -p "$bindir" "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - # copy the meson generated start.lua file - cp "$build/start.lua" "$datadir/core" - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns" - cp "$build/Info.plist" "$appdir/Contents/Info.plist" - cp "$build/src/lite-xl" "$bindir/lite-xl" - strip "$bindir/lite-xl" - pushd ".package-build" - local package_name="lite-xl-$os-$arch.zip" - zip "$package_name" -r "lite-xl.app" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package_linux () { - local portable="" - if [ "$1" == "-portable" ]; then - portable="-portable" - shift - fi - local build="$1" - local arch="$2" - local os="linux" - local pdir=".package-build/lite-xl" - if [ "$portable" == "-portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - local docdir="$pdir/doc" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - local docdir="$pdir/share/doc/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - mkdir -p "$docdir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - # copy the meson generated start.lua file - cp "$build/start.lua" "$datadir/core" - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl" "$bindir" - cp "licenses/licenses.md" "$docdir" - strip "$bindir/lite-xl" - if [ -z "$portable" ]; then - mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" "$pdir/share/metainfo" - cp "resources/linux/org.lite_xl.lite_xl.desktop" "$pdir/share/applications" - cp "resources/linux/org.lite_xl.lite_xl.appdata.xml" "$pdir/share/metainfo" - cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg" - fi - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.tar.gz" - tar czf "$package_name" "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package () { - if [[ "$OSTYPE" == msys || "$OSTYPE" == win32 ]]; then - lite_build_package_windows "$@" - elif [[ "$OSTYPE" == "darwin"* ]]; then - lite_build_package_macos "$@" - elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then - lite_build_package_linux "$@" - else - echo "Unknown OS type \"$OSTYPE\"" + if [[ -n $1 ]]; then + show_help exit 1 fi + + if [[ -n $build_dir ]]; then build_dir_option=("--builddir" "${build_dir}"); fi + if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_dir}"); fi + if [[ -n $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi + if [[ -n $version ]]; then version_option=("--version" "${version}"); fi + + source scripts/build.sh \ + ${build_dir_option[@]} \ + ${prefix_option[@]} \ + $debug \ + $force_fallback \ + $bundle \ + $portable + + source scripts/package.sh \ + ${build_dir_option[@]} \ + ${dest_dir_option[@]} \ + ${prefix_option[@]} \ + ${version_option[@]} \ + --binary \ + --addons \ + $debug \ + $appimage \ + $dmg \ + $innosetup \ + $source } -lite_copy_third_party_modules () { - local build="$1" - curl --insecure -L "https://github.com/rxi/lite-colors/archive/master.zip" -o "$build/rxi-lite-colors.zip" - mkdir -p "$build/third/data/colors" "$build/third/data/plugins" - unzip "$build/rxi-lite-colors.zip" -d "$build" - mv "$build/lite-colors-master/colors" "$build/third/data" - rm -fr "$build/lite-colors-master" -} - -build_opts=() -unset arch -while [ ! -z {$1+x} ]; do - case $1 in - -renderer) - build_opts+=("-Drenderer=true") - shift - ;; - -pgo) - pgo=true - shift - ;; - -branch=*) - use_branch="${1#-branch=}" - shift - ;; - *) - arch="$1" - break - esac -done - -if [ -z ${arch+set} ]; then - echo "usage: $0 [options] " - exit 1 -fi - - if [[ -d ".git" || $BUILD_PROHIBIT_GIT -ne 0 ]]; then - git_available=1; - else - echo "Use of git prohibited. Either '.git' wasn't found or BUILD_PROHIBIT_GIT was set." - git_available=0; - fi - -if [ -z ${use_branch+set} ]; then - if [[ $git_available -eq 1 ]]; then - use_branch="$(git rev-parse --abbrev-ref HEAD)" - else - # it really doesn't matter if git isn't present - use_branch="master" - fi -fi - -build_dir=".build-$arch" - -if [ -z ${pgo+set} ]; then - lite_build "$build_dir" "${build_opts[@]}" -else - lite_build_pgo "$build_dir" "${build_opts[@]}" -fi -lite_copy_third_party_modules "$build_dir" -lite_build_package "$build_dir" "$arch" -if [[ ! ( "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* || "$OSTYPE" == "darwin"* ) ]]; then - lite_build_package -portable "$build_dir" "$arch" -fi +main "$@" diff --git a/meson.build b/meson.build index 611d154a..9d3eac31 100644 --- a/meson.build +++ b/meson.build @@ -42,26 +42,28 @@ endif #=============================================================================== # Dependencies #=============================================================================== -libm = cc.find_library('m', required : false) -libdl = cc.find_library('dl', required : false) -lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], - default_options: ['shared=false', 'use_readline=false', 'app=false'] -) -pcre2_dep = dependency('libpcre2-8') -sdl_dep = dependency('sdl2', method: 'config-tool') -reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], - default_options: [ - 'default_library=static', 'multithreaded=false', - 'reproc-cpp=false', 'examples=false' - ] -) +if not get_option('source-only') + libm = cc.find_library('m', required : false) + libdl = cc.find_library('dl', required : false) + lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], + default_options: ['shared=false', 'use_readline=false', 'app=false'] + ) + pcre2_dep = dependency('libpcre2-8') + sdl_dep = dependency('sdl2', method: 'config-tool') + reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], + default_options: [ + 'default_library=static', 'multithreaded=false', + 'reproc-cpp=false', 'examples=false' + ] + ) -lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] + lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] -if host_machine.system() == 'windows' - # Note that we need to explicitly add the windows socket DLL because - # the pkg-config file from reproc does not include it. - lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) + if host_machine.system() == 'windows' + # Note that we need to explicitly add the windows socket DLL because + # the pkg-config file from reproc does not include it. + lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) + endif endif #=============================================================================== # Install Configuration @@ -118,6 +120,8 @@ configure_file( #=============================================================================== # Targets #=============================================================================== -subdir('lib/font_renderer') -subdir('src') -subdir('scripts') +if not get_option('source-only') + subdir('lib/font_renderer') + subdir('src') + subdir('scripts') +endif diff --git a/meson_options.txt b/meson_options.txt index ce7eca98..1cf3e22f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option('bundle', type : 'boolean', value : false, description: 'Build a macOS bundle') +option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..a236599c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,28 @@ +# Scripts + +Various scripts and configurations used to configure, build, and package Lite XL. + +### Build + +- **build.sh** +- **build-packages.sh**: In root directory, as all in one script; relies to the + ones in this directory. + +### Package + +- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1]. +- **appimage.sh**: [AppImage][2] builder. +- **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package. +- **package.sh**: Creates all binary / DMG image / installer / source packages. + +### Utility + +- **common.sh**: Common functions used by other scripts. +- **install-dependencies.sh**: Installs required applications to build, package + 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. + +[1]: https://github.com/LinusU/node-appdmg +[2]: https://docs.appimage.org/ +[3]: https://jrsoftware.org/isinfo.php diff --git a/scripts/appimage.sh b/scripts/appimage.sh index 65dce3c4..8844fafe 100644 --- a/scripts/appimage.sh +++ b/scripts/appimage.sh @@ -6,9 +6,11 @@ if [ ! -e "src/api/api.h" ]; then exit 1 fi +source scripts/common.sh + show_help(){ echo - echo $0 + echo "Usage: $0 " echo echo "Available options:" echo @@ -23,7 +25,7 @@ show_help(){ } ARCH="$(uname -m)" -BUILD_DIR=build +BUILD_DIR="$(get_default_build_dir)" RUN_BUILD=true STATIC_BUILD=false @@ -67,7 +69,7 @@ if [[ -n $1 ]]; then exit 1 fi -setup_appimagetool(){ +setup_appimagetool() { if ! which appimagetool > /dev/null ; then if [ ! -e appimagetool ]; then if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then @@ -80,7 +82,7 @@ setup_appimagetool(){ fi } -download_appimage_apprun(){ +download_appimage_apprun() { if [ ! -e AppRun ]; then if ! wget -O AppRun "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH}" ; then echo "Could not download AppRun for the arch '${ARCH}'." @@ -91,7 +93,7 @@ download_appimage_apprun(){ fi } -build_litexl(){ +build_litexl() { if [ -e build ]; then rm -rf build fi @@ -106,7 +108,7 @@ build_litexl(){ meson compile -C ${BUILD_DIR} } -generate_appimage(){ +generate_appimage() { if [ -e LiteXL.AppDir ]; then rm -rf LiteXL.AppDir fi diff --git a/scripts/build.sh b/scripts/build.sh index b0e013b1..7ceb3ce4 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,103 +1,102 @@ #!/bin/bash -set -ex +set -e if [ ! -e "src/api/api.h" ]; then - echo "Please run this script from the root directory of Lite XL." - exit 1 + echo "Please run this script from the root directory of Lite XL."; exit 1 fi -show_help(){ +source scripts/common.sh + +show_help() { echo echo "Usage: $0 " echo echo "Available options:" echo echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." - echo " Default: 'build'." - echo "-p --prefix Install directory prefix. Mandatory." - echo "-s --static Specify if building using static libraries" - echo " by using lhelper tool." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." + echo "-f --forcefallback Force to build dependencies statically." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix. Default: '/'." + echo "-B --bundle Create an App bundle (macOS only)" + echo "-P --portable Create a portable binary package." + echo " macOS: disabled when used with --bundle," + echo " Windows: Implicit being the only option." echo } -install_lhelper() { - if [[ ! -d lhelper ]]; then - git clone https://github.com/franko/lhelper.git - pushd lhelper; bash install-github; popd +main() { + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local prefix=/ + local force_fallback + local bundle + local portable - if [[ "$OSTYPE" == "darwin"* ]]; then - CC=clang CXX=clang++ lhelper create lite-xl -n - else - lhelper create lite-xl -n - fi + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + -f|--forcefallback) + force_fallback="--wrap-mode=forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -B|--bundle) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --bundle option, works only under macOS." + else + bundle="-Dbundle=true" + fi + shift + ;; + -P|--portable) + portable="-Dportable=true" + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 fi - # Not using `lhelper activate lite-xl` - source "$(lhelper env-source lite-xl)" - - lhelper install freetype2 - lhelper install sdl2 2.0.14-wait-event-timeout-1 - lhelper install pcre2 - - # Help MSYS2 to find the SDL2 include and lib directories to avoid errors - # during build and linking when using lhelper. - if [[ "$OSTYPE" == "msys" ]]; then - CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 - LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib + if [[ $platform == "macos" && -n $bundle && -n $portable ]]; then + echo "Warning: \"bundle\" and \"portable\" specified; excluding portable package." + portable="" fi -} -build() { + rm -rf "${build_dir}" + CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ --buildtype=release \ - --prefix "$PREFIX" \ - --wrap-mode=forcefallback \ - "${BUILD_DIR}" + --prefix "$prefix" \ + $force_fallback \ + $bundle \ + $portable \ + "${build_dir}" - meson compile -C build + meson compile -C "${build_dir}" } -BUILD_DIR=build -STATIC_BUILD=false - -for i in "$@"; do - case $i in - -h|--belp) - show_help - exit 0 - ;; - -b|--builddir) - BUILD_DIR="$2" - shift - shift - ;; - -p|--prefix) - PREFIX="$2" - shift - shift - ;; - -s|--static) - STATIC_BUILD=true - shift - ;; - *) - # unknown option - ;; - esac -done - -if [[ -n $1 ]]; then - show_help - exit 1 -fi - -if [[ -z $PREFIX ]]; then - echo "ERROR: prefix argument is missing." - exit 1 -fi - -if [[ $STATIC_BUILD == true ]]; then - install_lhelper -fi - -build +main "$@" diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 00000000..2b49d362 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +get_platform_name() { + if [[ "$OSTYPE" == "msys" ]]; then + echo "windows" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macos" + elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then + echo "linux" + else + echo "UNSUPPORTED-OS" + fi +} + +get_default_build_dir() { + platform=$(get_platform_name) + echo "build-$platform-$(uname -m)" +} + +if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then + echo "Error: unknown OS type: \"$OSTYPE\"" + exit 1 +fi diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in index 83a730d8..2b669fc0 100644 --- a/scripts/innosetup/innosetup.iss.in +++ b/scripts/innosetup/innosetup.iss.in @@ -9,7 +9,7 @@ ; Use /dArch option to create a setup for a different architecture, e.g.: ; iscc /dArch=x86 innosetup.iss #ifndef Arch -#define Arch "x64" + #define Arch "x64" #endif [Setup] @@ -27,8 +27,11 @@ AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} #if Arch=="x64" -ArchitecturesAllowed=x64 -ArchitecturesInstallIn64BitMode={#Arch} + ArchitecturesAllowed=x64 + ArchitecturesInstallIn64BitMode=x64 + #define ArchInternal "x86_64" +#else + #define ArchInternal "i686" #endif AllowNoIcons=yes @@ -48,7 +51,7 @@ PrivilegesRequiredOverridesAllowed=dialog UsedUserAreasWarning=no OutputDir=. -OutputBaseFilename=LiteXL-{#MyAppVersion}-{#Arch}-setup +OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup ;DisableDirPage=yes ;DisableProgramGroupPage=yes @@ -67,18 +70,16 @@ Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked [Files] Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion -; MSYS2 produces no external dlls on 32 bit builds when using lhelper -#if Arch=="x64" -Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion -#endif +Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}')) Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not WizardIsTaskSelected('portablemode') Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not WizardIsTaskSelected('portablemode') -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') +; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" [Run] Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh index 44a6c787..4384d13c 100644 --- a/scripts/innosetup/innosetup.sh +++ b/scripts/innosetup/innosetup.sh @@ -1,55 +1,65 @@ #!/bin/bash -set -ex +set -e if [ ! -e "src/api/api.h" ]; then - echo "Please run this script from the root directory of Lite XL." - exit 1 + echo "Please run this script from the root directory of Lite XL."; exit 1 fi -show_help(){ +source scripts/common.sh + +show_help() { echo echo "Usage: $0 " echo echo "Available options:" echo echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." - echo " Default: 'build'." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." echo } -BUILD_DIR=build +main() { + local build_dir=$(get_default_build_dir) + local arch -for i in "$@"; do - case $i in - -h|--belp) - show_help - exit 0 - ;; - -b|--BUILD_DIR) - BUILD_DIR="$2" - shift - shift - ;; - *) - # unknown option - ;; - esac -done + if [[ $MSYSTEM == "MINGW64" ]]; then arch=x64; else arch=Win32; fi -if [[ -n $1 ]]; then - show_help - exit 1 -fi + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done -# TODO: Required MinGW dlls are built only (?) when using lhelper on 64 bit -if [[ $MSYSTEM == "MINGW64" ]]; then - ARCH=x64; - mingwLibsDir=$BUILD_DIR/mingwLibs$ARCH + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + # Copy MinGW libraries dependencies. + # MSYS2 ldd command seems to be only 64bit, so use ntldd + # see https://github.com/msys2/MINGW-packages/issues/4164 + local mingwLibsDir="${build_dir}/mingwLibs$arch" mkdir -p "$mingwLibsDir" - ldd "$BUILD_DIR/src/lite-xl.exe" | grep mingw | awk '{print $3}' | xargs -I '{}' cp -v '{}' $mingwLibsDir -else - ARCH=Win32 -fi + ntldd -R "${build_dir}/src/lite-xl.exe" | grep mingw | awk '{print $3}' | sed 's#\\#/#g' | xargs -I '{}' cp -v '{}' $mingwLibsDir -/c/Program\ Files\ \(x86\)/Inno\ Setup\ 6/ISCC.exe -dARCH=$ARCH $BUILD_DIR/scripts/innosetup.iss -mv $BUILD_DIR/LiteXL*.exe $(pwd) + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch "${build_dir}/scripts/innosetup.iss" + pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd +} + +main "$@" diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh new file mode 100644 index 00000000..bad3b358 --- /dev/null +++ b/scripts/install-dependencies.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +show_help() { + echo + echo "Lite XL dependecies installer. Mainly used for CI but can also work on users systems." + echo "USE IT AT YOUR OWN RISK!" + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-l --lhelper Install tools required by LHelper and doesn't" + echo " install external libraries." + echo " --debug Debug this script." + echo +} + +main() { + local lhelper=false + + for i in "$@"; do + case $i in + -s|--lhelper) + lhelper=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + if [[ "$OSTYPE" == "linux"* ]]; then + if [[ $lhelper == true ]]; then + sudo apt-get install -qq ninja-build + else + sudo apt-get install -qq ninja-build libsdl2-dev libfreetype6 + fi + pip3 install meson + elif [[ "$OSTYPE" == "darwin"* ]]; then + if [[ $lhelper == true ]]; then + brew install bash md5sha1sum ninja + else + brew install ninja sdl2 + fi + pip3 install meson + cd ~; npm install appdmg; cd - + ~/node_modules/appdmg/bin/appdmg.js --version + elif [[ "$OSTYPE" == "msys" ]]; then + if [[ $lhelper == true ]]; then + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config} unzip + else + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,freetype,pcre2,SDL2} unzip + fi + fi +} + +main "$@" diff --git a/scripts/lhelper.sh b/scripts/lhelper.sh new file mode 100644 index 00000000..af6ae158 --- /dev/null +++ b/scripts/lhelper.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo " --debug Debug this script." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '$HOME/.local'." + echo +} + +main() { + local lhelper_prefix="$HOME/.local" + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + lhelper_prefix="$2" + echo "LHelper prefix set to: \"${lhelper_prefix}\"" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + if [[ ! -f ${lhelper_prefix}/bin/lhelper ]]; then + + git clone https://github.com/franko/lhelper.git + + # FIXME: This should be set in ~/.bash_profile if not using CI + # export PATH="${HOME}/.local/bin:${PATH}" + mkdir -p "${lhelper_prefix}/bin" + pushd lhelper; bash install "${lhelper_prefix}"; popd + + if [[ "$OSTYPE" == "darwin"* ]]; then + CC=clang CXX=clang++ lhelper create lite-xl -n + else + lhelper create lite-xl -n + fi + fi + + # Not using $(lhelper activate lite-xl) to support CI + source "$(lhelper env-source lite-xl)" + + lhelper install freetype2 + lhelper install sdl2 2.0.14-wait-event-timeout-1 + lhelper install pcre2 + + # Help MSYS2 to find the SDL2 include and lib directories to avoid errors + # during build and linking when using lhelper. + if [[ "$OSTYPE" == "msys" ]]; then + CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 + LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib + fi +} + +main diff --git a/scripts/msys2-package.sh b/scripts/msys2-package.sh deleted file mode 100644 index 5138d8a3..00000000 --- a/scripts/msys2-package.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -set -ex - -if [ ! -e "src/api/api.h" ]; then - echo "Please run this script from the root directory of Lite XL." - exit 1 -fi - -# FIXME: For some strange reason sometimes an error occurs randomly on the -# MINGW32 build of GitHub Actions; the environment variable $INSTALL_NAME -# is correct, but it expands with a drive letter at the end (all builds). - -show_help(){ - echo - echo "Usage: $0 " - echo - echo "Available options:" - echo - echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." - echo " Default: 'build'." - echo "-d --destdir DIRNAME Sets the name of the install directory (not path)." - echo -} - -BUILD_DIR=build - -for i in "$@"; do - case $i in - -h|--belp) - show_help - exit 0 - ;; - -b|--builddir) - BUILD_DIR="$2" - shift - shift - ;; - -d|--destdir) - DEST_DIR="$2" - shift - shift - ;; - *) - # unknown option - ;; - esac -done - -if [[ -n $1 ]]; then - show_help - exit 1 -fi - -DESTDIR="$(pwd)/${DEST_DIR}" meson install --skip-subprojects -C ${BUILD_DIR} - -zip -9rv ${DEST_DIR}.zip ${DEST_DIR}/* diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100644 index 00000000..b014c41c --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,252 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix. Default: '/'." + echo "-v --version VERSION Sets the version on the package name." + echo " --addons Install 3rd party addons (currently RXI colors)." + echo " --debug Debug this script." + echo "-A --appimage Create an AppImage (Linux only)." + echo "-B --binary Create a normal / portable package or macOS bundle," + echo " depending on how the build was configured. (Default.)" + echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." + echo "-I --innosetup Create a InnoSetup package (Windows only)." + echo "-S --source Create a source code package," + echo " including subprojects dependencies." + echo +} + +# Addons installation: some distributions forbid external downloads +# so make it as optional module. +install_addons() { + local build_dir="$1" + local data_dir="$2" + + if [[ -d "${build_dir}/third/data/colors" ]]; then + echo "Warning: found previous colors addons installation, skipping." + return 0 + fi + + # Copy third party color themes + curl --insecure \ + -L "https://github.com/rxi/lite-colors/archive/master.zip" \ + -o "${build_dir}/rxi-lite-colors.zip" + + mkdir -p "${build_dir}/third/data/colors" + unzip "${build_dir}/rxi-lite-colors.zip" -d "${build_dir}" + mv "${build_dir}/lite-colors-master/colors" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-colors-master" + + for module_name in colors; do + cp -r "${build_dir}/third/data/$module_name" "${data_dir}" + done +} + +source_package() { + local build_dir=build-src + local package_name=$1 + + rm -rf ${build_dir} + rm -rf ${package_name} + rm -f ${package_name}.tar.gz + + meson subprojects download + meson setup ${build_dir} -Dsource-only=true + + # Note: not using git-archive(-all) because it can't include subprojects ignored by git + rsync -arv \ + --exclude /*build*/ \ + --exclude *.git* \ + --exclude lhelper \ + --exclude lite-xl* \ + --exclude submodules \ + . ${package_name} + + cp "${build_dir}/start.lua" "${package_name}/data/core" + + tar rf ${package_name}.tar ${package_name} + gzip -9 ${package_name}.tar +} + +main() { + local arch="$(uname -m)" + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local dest_dir=lite-xl + local prefix=/ + local version + local addons=false + local appimage=false + local binary=false + local dmg=false + local innosetup=false + local source=false + + for i in "$@"; do + case $i in + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + if [[ -n $2 ]]; then version="-$2"; fi + shift + shift + ;; + -A|--appimage) + if [[ "$platform" != "linux" ]]; then + echo "Warning: ignoring --appimage option, works only under Linux." + else + appimage=true + fi + shift + ;; + -B|--binary) + binary=true + shift + ;; + -D|--dmg) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --dmg option, works only under macOS." + else + dmg=true + fi + shift + ;; + -I|--innosetup) + if [[ "$platform" != "windows" ]]; then + echo "Warning: ignoring --innosetup option, works only under Windows." + else + innosetup=true + fi + shift + ;; + -S|--source) + source=true + shift + ;; + --addons) + addons=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + # The source package doesn't require a previous build, + # nor the following install step, so run it now. + if [[ $source == true ]]; then source_package "lite-xl$version-src"; fi + + # No packages request + if [[ $appimage == false && $binary == false && $dmg == false && $innosetup == false ]]; then + # Source only, return. + if [[ $source == true ]]; then return 0; fi + # Build the binary package as default instead doing nothing. + binary=true + fi + + rm -rf "${dest_dir}" + + DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}" + + local data_dir="$(pwd)/${dest_dir}/data" + local exe_file="$(pwd)/${dest_dir}/lite-xl" + local package_name=lite-xl$version-$platform-$arch + local bundle=false + local stripcmd="strip" + + if [[ -d "${data_dir}" ]]; then + # Portable archive + exe_file="$(pwd)/${dest_dir}/lite-xl" + if [[ $platform == "windows" ]]; then + exe_file="${exe_file}.exe" + stripcmd="strip --strip-all" + else + # Windows archive is always portable + package_name+="-portable" + fi + elif [[ $platform == "macos" && ! -d "${data_dir}" ]]; then + # macOS bundle app + bundle=true + # Specify "bundle" on compressed archive only, implicit on images + if [[ $dmg == false ]]; then package_name+="-bundle"; fi + rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" + dest_dir="Lite XL.app" + data_dir="$(pwd)/${dest_dir}/Contents/Resources" + exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" + else + data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl" + exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl" + fi + + mkdir -p "${data_dir}" + + if [[ $addons == true ]]; then install_addons "${build_dir}" "${data_dir}"; fi + + # TODO: use --skip-subprojects when 0.58.0 will be available on supported + # distributions to avoid subprojects' include and lib directories to be copied. + # Install Meson with PIP to get the latest version is not always possible. + pushd "${dest_dir}" + find . -type d -name 'include' -prune -exec rm -rf {} \; + find . -type d -name 'lib' -prune -exec rm -rf {} \; + find . -type d -empty -delete + popd + + $stripcmd "${exe_file}" + + if [[ $binary == true ]]; then + rm -f "${package_name}".tar.gz + rm -f "${package_name}".zip + + if [[ $platform == "windows" ]]; then + zip -9rv ${package_name}.zip ${dest_dir}/* + else + tar czvf "${package_name}".tar.gz "${dest_dir}" + fi + fi + + if [[ $appimage == true ]]; then source scripts/appimage.sh; fi + if [[ $bundle == true && $dmg == true ]]; then source scripts/appdmg.sh "${package_name}"; fi + if [[ $innosetup == true ]]; then source scripts/innosetup/innosetup.sh -b "${build_dir}"; fi +} + +main "$@" diff --git a/scripts/source-package.sh b/scripts/source-package.sh deleted file mode 100644 index aff80c9d..00000000 --- a/scripts/source-package.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -set -ex - -if [ ! -e "src/api/api.h" ]; then - echo "Please run this script from the root directory of Lite XL." - exit 1 -fi - -show_help(){ - echo - echo "Usage: $0 " - echo - echo "Available options:" - echo - echo "-b --builddir DIRNAME Sets the name of the build directory (no path)." - echo " Default: 'build'." - echo "-d --destdir DIRNAME Sets the name of the install directory (no path)." - echo -} - -BUILD_DIR=build -DEST_DIR=lite-xl-src - -for i in "$@"; do - case $i in - -h|--belp) - show_help - exit 0 - ;; - -b|--builddir) - BUILD_DIR="$2" - shift - shift - ;; - -d|--destdir) - DEST_DIR="$2" - shift - shift - ;; - *) - # unknown option - ;; - esac -done - -if [[ -n $1 ]]; then - show_help - exit 1 -fi - -if test -d ${BUILD_DIR}; then rm -rf ${BUILD_DIR}; fi -if test -d ${DEST_DIR}; then rm -rf ${DEST_DIR}; fi -if test -f ${DEST_DIR}.tar.gz; then rm ${DEST_DIR}.tar.gz; fi - -meson subprojects download -meson setup ${BUILD_DIR} - -rsync -arv \ - --exclude /*build*/ \ - --exclude *.git* \ - --exclude lhelper \ - --exclude lite-xl* \ - --exclude submodules \ - . ${DEST_DIR} - -cp "${BUILD_DIR}/start.lua" "${DEST_DIR}/data/core" - -tar rf ${DEST_DIR}.tar ${DEST_DIR} -gzip -9 ${DEST_DIR}.tar From b4080ba14831d3a1bfa4ef198811a2cfb1e7ce22 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 5 Sep 2021 15:43:22 +0200 Subject: [PATCH 132/180] Bring back pgo option in new build package script --- build-packages.sh | 10 +++++++++- scripts/build.sh | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/build-packages.sh b/build-packages.sh index 6014be96..4ecda0a0 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -25,6 +25,8 @@ show_help() { echo "-f --forcefallback Force to build subprojects dependencies statically." echo "-B --bundle Create an App bundle (macOS only)" echo "-P --portable Create a portable package." + echo "-O --pgo Use profile guided optimizations (pgo)." + echo " Requires running the application iteractively." echo echo "Package options:" echo @@ -55,6 +57,7 @@ main() { local bundle local innosetup local portable + local pgo for i in "$@"; do case $i in @@ -110,6 +113,10 @@ main() { source="--source" shift ;; + -O|--pgo) + pgo="--pgo" + shift + ;; --debug) debug="--debug" set -x @@ -137,7 +144,8 @@ main() { $debug \ $force_fallback \ $bundle \ - $portable + $portable \ + $pgo source scripts/package.sh \ ${build_dir_option[@]} \ diff --git a/scripts/build.sh b/scripts/build.sh index 7ceb3ce4..75212468 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -21,6 +21,7 @@ show_help() { echo "-p --prefix PREFIX Install directory prefix. Default: '/'." echo "-B --bundle Create an App bundle (macOS only)" echo "-P --portable Create a portable binary package." + echo "-O --pgo Use profile guided optimizations (pgo)." echo " macOS: disabled when used with --bundle," echo " Windows: Implicit being the only option." echo @@ -33,6 +34,7 @@ main() { local force_fallback local bundle local portable + local pgo for i in "$@"; do case $i in @@ -70,6 +72,10 @@ main() { portable="-Dportable=true" shift ;; + -O|--pgo) + pgo="-Db_pgo=generate" + shift + ;; *) # unknown option ;; @@ -94,9 +100,18 @@ main() { $force_fallback \ $bundle \ $portable \ + $pgo \ "${build_dir}" meson compile -C "${build_dir}" + + if [ ! -z ${pgo+x} ]; then + cp -r data "${build_dir}/src" + "${build_dir}/src/lite-xl" + meson configure -Db_pgo=use "${build_dir}" + meson compile -C "${build_dir}" + rm -fr "${build_dir}/data" + fi } main "$@" From aa9e2e2df516bf2204c3c8dce380c3a3c9bee656 Mon Sep 17 00:00:00 2001 From: redtide Date: Sun, 5 Sep 2021 19:29:17 +0200 Subject: [PATCH 133/180] Fixed some build scripts issues, keep bash always updated on macOS --- scripts/install-dependencies.sh | 2 +- scripts/package.sh | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index bad3b358..2f9519b1 100644 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -55,7 +55,7 @@ main() { if [[ $lhelper == true ]]; then brew install bash md5sha1sum ninja else - brew install ninja sdl2 + brew install bash ninja sdl2 fi pip3 install meson cd ~; npm install appdmg; cd - diff --git a/scripts/package.sh b/scripts/package.sh index b014c41c..1370aee8 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -192,10 +192,12 @@ main() { local exe_file="$(pwd)/${dest_dir}/lite-xl" local package_name=lite-xl$version-$platform-$arch local bundle=false + local portable=false local stripcmd="strip" if [[ -d "${data_dir}" ]]; then - # Portable archive + echo "Creating a portable, compressed archive..." + portable=true exe_file="$(pwd)/${dest_dir}/lite-xl" if [[ $platform == "windows" ]]; then exe_file="${exe_file}.exe" @@ -205,15 +207,20 @@ main() { package_name+="-portable" fi elif [[ $platform == "macos" && ! -d "${data_dir}" ]]; then - # macOS bundle app - bundle=true - # Specify "bundle" on compressed archive only, implicit on images - if [[ $dmg == false ]]; then package_name+="-bundle"; fi - rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" - dest_dir="Lite XL.app" data_dir="$(pwd)/${dest_dir}/Contents/Resources" - exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" - else + if [[ -d "${data_dir}" ]]; then + echo "Creating a macOS bundle application..." + bundle=true + # Specify "bundle" on compressed archive only, implicit on images + if [[ $dmg == false ]]; then package_name+="-bundle"; fi + rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" + dest_dir="Lite XL.app" + exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" + fi + fi + + if [[ $bundle == false && $portable == false ]]; then + echo "Creating a compressed archive..." data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl" exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl" fi From 90c721b823eec21930ddb91d486f1858d4cbf93c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 15:11:20 +0200 Subject: [PATCH 134/180] Adopt bigger fonts by default --- data/core/init.lua | 4 ++-- data/core/style.lua | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 6b770fd6..018beadb 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -320,8 +320,8 @@ local style = require "core.style" ------------------------------- Fonts ---------------------------------------- -- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) --- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +-- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE) +-- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE) -- -- font names used by lite: -- style.font : user interface diff --git a/data/core/style.lua b/data/core/style.lua index 7cf16eb1..faca166e 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -21,11 +21,11 @@ style.tab_width = common.round(170 * SCALE) -- -- On High DPI monitor or non RGB monitor you may consider using antialiasing grayscale instead. -- The antialiasing grayscale with full hinting is interesting for crisp font rendering. -style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) +style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE) style.big_font = style.font:copy(40 * SCALE) -style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) -style.icon_big_font = style.icon_font:copy(20 * SCALE) -style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, {antialiasing="grayscale", hinting="full"}) +style.icon_big_font = style.icon_font:copy(24 * SCALE) +style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE) style.background = { common.color "#2e2e32" } style.background2 = { common.color "#252529" } From 67d7b894ae90e04413a2075d5e2f7cf4a8976bef Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 15:23:41 +0200 Subject: [PATCH 135/180] Add initial suggestion in open-project-folder --- data/core/commands/core.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 215ff654..432ded89 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -164,6 +164,10 @@ command.add(nil, { end, ["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) + end core.command_view:enter("Open Project", function(text, item) text = common.home_expand(item and item.text or text) local path_stat = system.get_file_info(text) From 2b277bb50224fdc52aa76c838b66640010dfa4bb Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 06:44:15 -0700 Subject: [PATCH 136/180] Fix problem with -psn argument on macOS --- data/core/init.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/core/init.lua b/data/core/init.lua index 018beadb..e84792ac 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -461,7 +461,10 @@ function core.init() project_dir = arg_filename project_dir_explicit = true else - delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + -- on macOS we can get an argument like "-psn_0_52353" that we just ignore. + if not ARGS[i]:match("^-psn") then + delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + end end end From 0a36e66abaa908fe84306938db178431c81ce25b Mon Sep 17 00:00:00 2001 From: Guldoman Date: Tue, 7 Sep 2021 22:14:40 +0200 Subject: [PATCH 137/180] Fix `treeview:open-in-system` command on Windows The first argument is the title for the `CMD` window. --- 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 48d6f5a7..84d5dd28 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -536,7 +536,7 @@ command.add(function() return view.hovered_item ~= nil end, { local hovered_item = view.hovered_item if PLATFORM == "Windows" then - system.exec("start " .. hovered_item.abs_filename) + system.exec(string.format("start \"\" %q", hovered_item.abs_filename)) elseif string.find(PLATFORM, "Mac") then system.exec(string.format("open %q", hovered_item.abs_filename)) elseif PLATFORM == "Linux" then From 48c709a95fc2e84e12f667c20349e7de0d6022a2 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 22:46:58 +0200 Subject: [PATCH 138/180] Prepare 2.0.2 version and changelog --- changelog.md | 35 +++++++++++++++++++++++++++++++++++ meson.build | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d6e30519..2cf29061 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,40 @@ This files document the changes done in Lite XL for each release. +### 2.0.2 + +Fix problem project directory when starting the application from Launcher on macOS. + +Improved LogView by @takase1121. + +Fix problem when trying to close an unsaved new document. +Fix `treeview:open-in-system` command on Windows. +Fix rename command to update name of document if opened. +Contributed by @Guldoman. + +Improve the find and replace dialog so that previously used expressions can be recalled +using "up" and "down" keys. + +Multi-cursors fixes and improvement by @adamharrison. + +Build package script rewrite and enhancement by @redtide. + +Use bigger fonts by default. + +Other minor improvements and fixes. + +### 2.0.1 + +Fix a few bugs and we mandate the mod-version 2 for plugins. +This means that users should ensure they have up-to-date plugins for Lite XL 2.0. + +Here some details about the bug fixes: + +- fix a bug that created a fatal error when using the command to change project folder or when closing all the active documents +- add a limit to avoid scaling fonts too much and fix a related invalid memory access for very small fonts +- fix focus problem with NagView when switching project directory +- fix error that prevented the verification of plugins versions +- fix error on X11 that caused a bug window event on exit + ### 2.0 The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; diff --git a/meson.build b/meson.build index 9d3eac31..047ce90e 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c', 'cpp'], - version : '2.0.1', + version : '2.0.2', license : 'MIT', meson_version : '>= 0.54', default_options : ['c_std=gnu11', 'cpp_std=c++03'] From 16170e8db9628d83b80fa24f077ea2b9fdef574f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 8 Sep 2021 10:27:45 +0200 Subject: [PATCH 139/180] Improve info.plist for macOS package config As suggested by @redtide and @Timofffee. --- resources/macos/Info.plist.in | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/resources/macos/Info.plist.in b/resources/macos/Info.plist.in index c15fd566..4d715f2f 100644 --- a/resources/macos/Info.plist.in +++ b/resources/macos/Info.plist.in @@ -2,24 +2,28 @@ -CFBundleExecutable + CFBundleExecutable lite-xl CFBundleGetInfoString lite-xl CFBundleIconFile - icon + icon.icns CFBundleName - lite-xl + Lite XL CFBundlePackageType APPL NSHighResolutionCapable - LSMinimumSystemVersion10.11 - NSDocumentsFolderUsageDescriptionTo access, edit and index your projects. - NSDesktopFolderUsageDescriptionTo access, edit and index your projects. - NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. + LSMinimumSystemVersion + 10.11 + NSDocumentsFolderUsageDescription + To access, edit and index your projects. + NSDesktopFolderUsageDescription + To access, edit and index your projects. + NSDownloadsFolderUsageDescription + To access, edit and index your projects. CFBundleShortVersionString - @PROJECT_VERSION@ + @PROJECT_VERSION@ NSHumanReadableCopyright ยฉ 2019-2021 Francesco Abbate From dfb64fbdf196aa85ef8b41ec279d372b15badec9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 9 Sep 2021 15:39:41 +0200 Subject: [PATCH 140/180] Do not add selection with newlines in replace If the selected text containes newlines it doesn't make sense to use it as the initial text in the "replace text" command view. Do not use the selected text if a newline is found in the selection. Fix #511. --- data/core/commands/findreplace.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 48cbecaf..03aa7737 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -135,7 +135,9 @@ command.add("core.docview", { end, ["find-replace:replace"] = function() - replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new) + local selected_text = doc():get_text(doc():get_selection()) + local has_newlines = selected_text:find("\n", 1, true) + replace("Text", has_newlines and "" or selected_text, function(text, old, new) if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end From f85fe102d99aa6af042ac1f6f7855bfaf7c07f92 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 17:06:19 +0200 Subject: [PATCH 141/180] Implement hidden suggestions for find dialog --- data/core/commands/findreplace.lua | 5 ++++- data/core/commandview.lua | 23 +++++++++++++++++++---- data/core/init.lua | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 03aa7737..73a5ae07 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -49,7 +49,9 @@ local function find(label, search_fn) core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) - core.command_view:enter(label, function(text) + core.command_view:set_hidden_suggestions() + core.command_view:enter(label, function(text, item) + table.insert(core.previous_find, text) core.status_view:remove_tooltip() if found then last_fn, last_text = search_fn, text @@ -61,6 +63,7 @@ local function find(label, search_fn) end, function(text) found = update_preview(last_sel, search_fn, text) last_fn, last_text = search_fn, text + return core.previous_find end, function(explicit) core.status_view:remove_tooltip() if explicit then diff --git a/data/core/commandview.lua b/data/core/commandview.lua index eb7febc7..d41db0d5 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -34,6 +34,7 @@ function CommandView:new() self.suggestion_idx = 1 self.suggestions = {} self.suggestions_height = 0 + self.show_suggestions = true self.last_change_id = 0 self.gutter_width = 0 self.gutter_text_brightness = 0 @@ -45,6 +46,11 @@ function CommandView:new() end +function CommandView:set_hidden_suggestions() + self.show_suggestions = false +end + + function CommandView:get_name() return View.get_name(self) end @@ -83,10 +89,16 @@ end function CommandView:move_suggestion_idx(dir) - local n = self.suggestion_idx + dir - self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + local current_suggestion = self.suggestions[self.suggestion_idx].text + if self.show_suggestions or self:get_text() == current_suggestion then + local n = self.suggestion_idx + dir + self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + end self:complete() self.last_change_id = self.doc:get_change_id() + if not self.show_suggestions then + self.state.suggest(self:get_text()) + end end @@ -134,6 +146,7 @@ function CommandView:exit(submitted, inexplicit) self.doc:reset() self.suggestions = {} if not submitted then cancel(not inexplicit) end + self.show_suggestions = true end @@ -187,7 +200,7 @@ function CommandView:update() -- update suggestions box height local lh = self:get_suggestion_line_height() - local dest = math.min(#self.suggestions, max_suggestions) * lh + local dest = self.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 self:move_towards("suggestions_height", dest) -- update suggestion cursor offset @@ -256,7 +269,9 @@ end function CommandView:draw() CommandView.super.draw(self) - core.root_view:defer_draw(draw_suggestions_box, self) + if self.show_suggestions then + core.root_view:defer_draw(draw_suggestions_box, self) + end end diff --git a/data/core/init.lua b/data/core/init.lua index e84792ac..b1072b38 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -496,6 +496,7 @@ function core.init() core.redraw = true core.visited_files = {} + core.previous_find = {} core.restart_request = false core.quit_request = false core.replacements = whitespace_replacements() From 4bcc1cc07c76cfdac92aba13322e0d5687dd8f1a Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 18:07:26 +0200 Subject: [PATCH 142/180] Fix error with hidden suggestions Avoid indexing a nil if there are no suggestions. --- data/core/commandview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index d41db0d5..5ccb8d3a 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -89,7 +89,7 @@ end function CommandView:move_suggestion_idx(dir) - local current_suggestion = self.suggestions[self.suggestion_idx].text + local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text if self.show_suggestions or self:get_text() == current_suggestion then local n = self.suggestion_idx + dir self.suggestion_idx = common.clamp(n, 1, #self.suggestions) From fa8b3b33b180d265f472877a23fb8df9a9eca191 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 18:08:54 +0200 Subject: [PATCH 143/180] Use hidden suggestions also for replace dialog --- data/core/commands/findreplace.lua | 7 +++++-- data/core/init.lua | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 73a5ae07..7027f293 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -78,19 +78,22 @@ local function replace(kind, default, fn) core.command_view:set_text(default, true) core.status_view:show_tooltip(get_find_tooltip()) + core.command_view:set_hidden_suggestions() core.command_view:enter("Find To Replace " .. kind, function(old) core.command_view:set_text(old, true) local s = string.format("Replace %s %q With", kind, old) + core.command_view:set_hidden_suggestions() core.command_view:enter(s, function(new) + table.insert(core.previous_replace, new) local n = doc():replace(function(text) return fn(text, old, new) end) core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) - end, function() end, function() + end, function() return core.previous_replace end, function() core.status_view:remove_tooltip() end) - end, function() end, function() + end, function() return core.previous_find end, function() core.status_view:remove_tooltip() end) end diff --git a/data/core/init.lua b/data/core/init.lua index b1072b38..1978ce38 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -497,6 +497,7 @@ function core.init() core.redraw = true core.visited_files = {} core.previous_find = {} + core.previous_replace = {} core.restart_request = false core.quit_request = false core.replacements = whitespace_replacements() From b440a2258118c916dfec657c676befca53e108ac Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 18:33:58 +0200 Subject: [PATCH 144/180] Remeber initial user text for hidden suggestions When using hidden suggestions remember the text user was typing when navigating suggestions. Ensure also that in the previously searched expressiosn we have no duplicate entries. --- data/core/commands/findreplace.lua | 15 +++++++++++++-- data/core/commandview.lua | 26 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 7027f293..56761690 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -41,6 +41,16 @@ local function update_preview(sel, search_fn, text) end end + +local function insert_unique(t, v) + local n = #t + for i = 1, n do + if t[i] == v then return end + end + t[n + 1] = v +end + + local function find(label, search_fn) last_view, last_sel, last_finds = core.active_view, { core.active_view.doc:get_selection() }, {} @@ -51,7 +61,7 @@ local function find(label, search_fn) core.command_view:set_hidden_suggestions() core.command_view:enter(label, function(text, item) - table.insert(core.previous_find, text) + insert_unique(core.previous_find, text) core.status_view:remove_tooltip() if found then last_fn, last_text = search_fn, text @@ -80,12 +90,13 @@ local function replace(kind, default, fn) core.status_view:show_tooltip(get_find_tooltip()) core.command_view:set_hidden_suggestions() core.command_view:enter("Find To Replace " .. kind, function(old) + insert_unique(core.previous_find, old) core.command_view:set_text(old, true) local s = string.format("Replace %s %q With", kind, old) core.command_view:set_hidden_suggestions() core.command_view:enter(s, function(new) - table.insert(core.previous_replace, new) + insert_unique(core.previous_replace, new) local n = doc():replace(function(text) return fn(text, old, new) end) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index 5ccb8d3a..b91f1394 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -89,14 +89,27 @@ end function CommandView:move_suggestion_idx(dir) - local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text - if self.show_suggestions or self:get_text() == current_suggestion then + if self.show_suggestions then local n = self.suggestion_idx + dir self.suggestion_idx = common.clamp(n, 1, #self.suggestions) - end - self:complete() - self.last_change_id = self.doc:get_change_id() - if not self.show_suggestions then + self:complete() + self.last_change_id = self.doc:get_change_id() + else + local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text + local text = self:get_text() + if text == current_suggestion then + local n = self.suggestion_idx + dir + if n == 0 and self.save_suggestion then + self:set_text(self.save_suggestion) + else + self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self:complete() + end + else + self.save_suggestion = text + self:complete() + end + self.last_change_id = self.doc:get_change_id() self.state.suggest(self:get_text()) end end @@ -147,6 +160,7 @@ function CommandView:exit(submitted, inexplicit) self.suggestions = {} if not submitted then cancel(not inexplicit) end self.show_suggestions = true + self.save_suggestion = nil end From 403b7f6fb6d619f920e350ed9bb468dbb8300eb3 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 18:42:36 +0200 Subject: [PATCH 145/180] Add missing remove tooltip call --- data/core/commands/findreplace.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 56761690..f41a748a 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -96,6 +96,7 @@ local function replace(kind, default, fn) local s = string.format("Replace %s %q With", kind, old) core.command_view:set_hidden_suggestions() core.command_view:enter(s, function(new) + core.status_view:remove_tooltip() insert_unique(core.previous_replace, new) local n = doc():replace(function(text) return fn(text, old, new) From 04250a206a64bec1e207d2dfa74c36862a52daea Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 7 Sep 2021 22:31:36 +0200 Subject: [PATCH 146/180] Add previous find and replace in session --- data/core/init.lua | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 1978ce38..af291767 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -17,10 +17,7 @@ local core = {} local function load_session() local ok, t = pcall(dofile, USERDIR .. "/session.lua") - if ok and t then - return t.recents, t.window, t.window_mode - end - return {} + return ok and t or {} end @@ -30,6 +27,8 @@ local function save_session() fp:write("return {recents=", common.serialize(core.recent_projects), ", window=", common.serialize(table.pack(system.get_window_size())), ", window_mode=", common.serialize(system.get_window_mode()), + ", previous_find=", common.serialize(core.previous_find), + ", previous_replace=", common.serialize(core.previous_replace), "}\n") fp:close() end @@ -435,13 +434,15 @@ function core.init() end do - local recent_projects, window_position, window_mode = load_session() - if window_mode == "normal" then - system.set_window_size(table.unpack(window_position)) - elseif window_mode == "maximized" then + local session = load_session() + if session.window_mode == "normal" then + system.set_window_size(table.unpack(session.window)) + elseif session.window_mode == "maximized" then system.set_window_mode("maximized") end - core.recent_projects = recent_projects or {} + core.recent_projects = session.recents or {} + core.previous_find = session.previous_find or {} + core.previous_replace = session.previous_replace or {} end local project_dir = core.recent_projects[1] or "." @@ -496,8 +497,6 @@ function core.init() core.redraw = true core.visited_files = {} - core.previous_find = {} - core.previous_replace = {} core.restart_request = false core.quit_request = false core.replacements = whitespace_replacements() From aa0e083cb9d0399cb3aafdfd4735ffbe5394e6c7 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Wed, 8 Sep 2021 22:47:37 +0200 Subject: [PATCH 147/180] Allow `find-replace:select-next` to select more occurrences after wrap The initial position for the search is defined by the last selection towards the end of the file. After reaching the end of the file, it would always select the same selection to start the search from. Now, we start the search from each selection, until a new occurrence is found. --- data/core/commands/findreplace.lua | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index f41a748a..902623be 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -126,6 +126,20 @@ local function has_unique_selection() return text ~= nil end +local function is_in_selection(line, col, l1, c1, l2, c2) + if line < l1 or line > l2 then return false end + if line == l1 and col <= c1 then return false end + if line == l2 and col > c2 then return false end + return true +end + +local function is_in_any_selection(line, col) + for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do + if is_in_selection(line, col, l1, c1, l2, c2) then return true end + end + return false +end + local function select_next(all) local il1, ic1 = doc():get_selection(true) for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do @@ -133,9 +147,15 @@ local function select_next(all) repeat l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) if l1 == il1 and c1 == ic1 then break end - if l2 then doc():add_selection(l2, c2, l1, c1) end + if l2 and (all or not is_in_any_selection(l2, c2)) then + doc():add_selection(l2, c2, l1, c1) + if not all then + core.active_view:scroll_to_make_visible(l2, c2) + return + end + end until not all or not l2 - break + if all then break end end end From d9afc40a174122485dec73d2025fd8079fb58c88 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 9 Sep 2021 19:12:56 +0200 Subject: [PATCH 148/180] Bring back command find-replace:select-next Bring back the command like before to keep single selection but with ctrl+f3 keybindings. Change the name of the new multi-cursor command but keep the ctrl+d keybinding. --- changelog.md | 3 +++ data/core/commands/findreplace.lua | 10 ++++++++-- data/core/keymap-macos.lua | 5 +++-- data/core/keymap.lua | 5 +++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 2cf29061..5ea7b87c 100644 --- a/changelog.md +++ b/changelog.md @@ -35,6 +35,9 @@ Here some details about the bug fixes: - fix error that prevented the verification of plugins versions - fix error on X11 that caused a bug window event on exit +Change behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. +The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. + ### 2.0 The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 902623be..c5f2bf07 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -160,8 +160,14 @@ local function select_next(all) end command.add(has_unique_selection, { - ["find-replace:select-next"] = function() select_next(false) end, - ["find-replace:select-all"] = function() select_next(true) end + ["find-replace:select-next"] = function() + local l1, c1, l2, c2 = doc():get_selection(true) + local text = doc():get_text(l1, c1, l2, c2) + 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 }) command.add("core.docview", { diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 7f54ddbb..53a20468 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -66,9 +66,10 @@ local function keymap_macos(keymap) ["cmd+shift+return"] = "doc:newline-above", ["cmd+j"] = "doc:join-lines", ["cmd+a"] = "doc:select-all", - ["cmd+d"] = { "find-replace:select-next", "doc:select-word" }, + ["cmd+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["cmd+f3"] = "find-replace:select-next", ["cmd+l"] = "doc:select-lines", - ["cmd+shift+l"] = { "find-replace:select-all", "doc:select-word" }, + ["cmd+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["cmd+/"] = "doc:toggle-line-comments", ["option+up"] = "doc:move-lines-up", ["option+down"] = "doc:move-lines-down", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 2be0dfc7..50eadec6 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -168,9 +168,10 @@ keymap.add_direct { ["ctrl+shift+return"] = "doc:newline-above", ["ctrl+j"] = "doc:join-lines", ["ctrl+a"] = "doc:select-all", - ["ctrl+d"] = { "find-replace:select-next", "doc:select-word" }, + ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["ctrl+f3"] = "find-replace:select-next", ["ctrl+l"] = "doc:select-lines", - ["ctrl+shift+l"] = { "find-replace:select-all", "doc:select-word" }, + ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+up"] = "doc:move-lines-up", ["ctrl+down"] = "doc:move-lines-down", From 83607aec4a5553a2a6b7a6d448996df2212be2db Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 9 Sep 2021 22:37:26 +0200 Subject: [PATCH 149/180] Reword changelog --- changelog.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 5ea7b87c..57ab9646 100644 --- a/changelog.md +++ b/changelog.md @@ -4,24 +4,33 @@ This files document the changes done in Lite XL for each release. Fix problem project directory when starting the application from Launcher on macOS. -Improved LogView by @takase1121. +Improved LogView. Entries can now be expanded and there is a context menu to copy the item's content. + +Change the behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. +The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. + +Added a command to create a multi-cursor with all the occurrences of the current selection. +Activated with the shortcut `ctrl+shift+l`. Fix problem when trying to close an unsaved new document. + +No longer shows an error for the `-psn` argument passed to the application on macOS. + Fix `treeview:open-in-system` command on Windows. + Fix rename command to update name of document if opened. -Contributed by @Guldoman. Improve the find and replace dialog so that previously used expressions can be recalled using "up" and "down" keys. -Multi-cursors fixes and improvement by @adamharrison. - -Build package script rewrite and enhancement by @redtide. +Build package script rewrite with many improvements. Use bigger fonts by default. Other minor improvements and fixes. +With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, @redtide, @Timofffee, @boppyt, @Jan200101. + ### 2.0.1 Fix a few bugs and we mandate the mod-version 2 for plugins. @@ -35,9 +44,6 @@ Here some details about the bug fixes: - fix error that prevented the verification of plugins versions - fix error on X11 that caused a bug window event on exit -Change behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. -The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. - ### 2.0 The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; From cec1e4efb9cffeb759165d6e4521721ae2ef2eb5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 9 Sep 2021 23:30:18 +0200 Subject: [PATCH 150/180] Do not fail search if there was an option change --- data/core/commands/findreplace.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index c5f2bf07..2412c4e1 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -12,6 +12,7 @@ local last_finds, last_view, last_fn, last_text, last_sel local case_sensitive = config.find_case_sensitive or false local find_regex = config.find_regex or false +local found_expression local function doc() return core.active_view:is(DocView) and core.active_view.doc or last_view.doc @@ -34,10 +35,10 @@ local function update_preview(sel, search_fn, text) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) last_view:scroll_to_line(line2, true) - return true + found_expression = true else last_view.doc:set_selection(unpack(sel)) - return false + found_expression = false end end @@ -54,7 +55,8 @@ 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 = last_view.doc:get_text(unpack(last_sel)) + found_expression = false core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) @@ -63,7 +65,7 @@ local function find(label, search_fn) core.command_view:enter(label, function(text, item) insert_unique(core.previous_find, text) core.status_view:remove_tooltip() - if found then + if found_expression then last_fn, last_text = search_fn, text else core.error("Couldn't find %q", text) @@ -71,7 +73,7 @@ local function find(label, search_fn) last_view:scroll_to_make_visible(unpack(last_sel)) end end, function(text) - found = update_preview(last_sel, search_fn, text) + update_preview(last_sel, search_fn, text) last_fn, last_text = search_fn, text return core.previous_find end, function(explicit) From afd067219769686076d74a8e3c31290e3e4ec566 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 10 Sep 2021 14:54:55 +0200 Subject: [PATCH 151/180] Use line/col to identify selection in replace command --- data/core/commands/findreplace.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 2412c4e1..f8e8e45a 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -181,9 +181,9 @@ command.add("core.docview", { end, ["find-replace:replace"] = function() - local selected_text = doc():get_text(doc():get_selection()) - local has_newlines = selected_text:find("\n", 1, true) - replace("Text", has_newlines and "" or selected_text, function(text, old, new) + local l1, c1, l2, c2 = doc():get_selection() + local selected_text = doc():get_text(l1, c1, l2, c2) + replace("Text", l1 == l2 and selected_text or "", function(text, old, new) if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end From 218999dff899872fb38d3c8889d5bc27cc600ed1 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 10 Sep 2021 14:55:04 +0200 Subject: [PATCH 152/180] Avoid bug when replacement stop at end of string Detect when we are past the end of the string to avoid by checking if byte is not nil. Fix #510. --- data/core/regex.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/regex.lua b/data/core/regex.lua index 19306e04..69203cbd 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -23,7 +23,7 @@ end -- Moves to the end of the identified character. local function end_character(str, index) local byte = string.byte(str, index + 1) - while byte >= 128 and byte < 192 do + while byte and byte >= 128 and byte < 192 do index = index + 1 byte = string.byte(str, index + 1) end From 8dd530e5cf4c1be080fcd387fc38aa6afc44be83 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 10 Sep 2021 15:47:33 +0200 Subject: [PATCH 153/180] Add -branch option in repackage script --- scripts/repackage.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/repackage.sh b/scripts/repackage.sh index 99368582..3239ea5e 100644 --- a/scripts/repackage.sh +++ b/scripts/repackage.sh @@ -10,7 +10,7 @@ copy_directory_from_repo () { fi local dirname="$1" local destdir="$2" - git archive master "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" + git archive "$lite_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" } lite_copy_third_party_modules () { @@ -23,12 +23,17 @@ lite_copy_third_party_modules () { rm "$build/rxi-lite-colors.zip" } +lite_branch=master while [ ! -z ${1+x} ]; do case "$1" in -dir) use_dir="$(realpath $2)" shift 2 ;; + -branch) + lite_branch="$2" + shift 2 + ;; *) echo "unknown option: $1" exit 1 From 18189e63b603040bb534d2aabcc2029f3f964421 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 10 Sep 2021 15:48:43 +0200 Subject: [PATCH 154/180] Fix repackage script to restore project version --- scripts/repackage.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/repackage.sh b/scripts/repackage.sh index 3239ea5e..f8da579f 100644 --- a/scripts/repackage.sh +++ b/scripts/repackage.sh @@ -78,6 +78,8 @@ for filename in $(ls -1 *.zip *.tar.*); do fi rm "$filename" find lite-xl -name lite -exec chmod a+x '{}' \; + start_file=$(find lite-xl -name start.lua) + lite_version=$(cat "$start_file" | awk 'match($0, /^\s*VERSION\s*=\s*"(.+)"/, a) { print(a[1]) }') xcoredir="$(find lite-xl -type d -name 'core')" coredir="$(dirname $xcoredir)" echo "coredir: $coredir" @@ -86,6 +88,7 @@ for filename in $(ls -1 *.zip *.tar.*); do rm -fr "$coredir/$module_name" (cd .. && copy_directory_from_repo --strip-components=1 "data/$module_name" "$workdir/$coredir") done + sed -i "s/@PROJECT_VERSION@/$lite_version/g" "$start_file" for module_name in plugins colors; do cp -r "third/data/$module_name" "$coredir" done From 1ba385eb5e414649f86977616bc66387e4f50d5c Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 12 Jul 2021 18:21:27 +0200 Subject: [PATCH 155/180] First integration of dmon for directory monitoring --- data/core/init.lua | 98 ++- data/plugins/treeview.lua | 3 +- licenses/licenses.md | 27 + src/api/system.c | 61 ++ src/dirmonitor.c | 56 ++ src/dirmonitor.h | 14 + src/dmon.h | 1514 +++++++++++++++++++++++++++++++++++++ src/main.c | 5 + src/meson.build | 1 + 9 files changed, 1773 insertions(+), 6 deletions(-) create mode 100644 src/dirmonitor.c create mode 100644 src/dirmonitor.h create mode 100644 src/dmon.h diff --git a/data/core/init.lua b/data/core/init.lua index af291767..4e2a4e37 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -99,6 +99,18 @@ 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, size_limit) + local info = system.get_file_info(root .. file) + if info then + info.filename = strip_leading_path(file) + end + return info and info.size < size_limit and info +end + + -- "root" will by an absolute path without trailing '/' -- "path" will be a path starting with '/' and without trailing '/' -- or the empty string. @@ -117,10 +129,8 @@ local function get_directory_files(root, path, t, recursive, begin_hook) 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) + local info = get_project_file_info(root, path .. PATHSEP .. file, size_limit) + if info then 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 @@ -276,6 +286,69 @@ function core.project_files_number() 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_remove_file(watch_id, filepath) + local project_dir_entry + for i = 1, #core.project_directories do + if core.project_directories[i].watch_id == watch_id then + project_dir_entry = core.project_directories[i] + end + end + if not project_dir_entry then return end + print("LOOKING for", filepath, " in", project_dir_entry and project_dir_entry.name) + local fileinfo = { filename = filepath } + for _, filetype in ipairs {"dir", "file"} do + fileinfo.type = filetype + local index, match = file_search(project_dir_entry.files, fileinfo) + if match then + print("FOUND", filepath, " at index", index) + table.remove(project_dir_entry.files, index) + project_dir_entry.is_dirty = true + return + end + end +end + + +local function project_scan_add_file(watch_id, filepath) + local project_dir_entry + for i = 1, #core.project_directories do + if core.project_directories[i].watch_id == watch_id then + project_dir_entry = core.project_directories[i] + end + end + if not project_dir_entry then return end + local size_limit = config.file_size_limit * 10e5 + local fileinfo = get_project_file_info(project_dir_entry.name, PATHSEP .. filepath, size_limit) + local index, match = file_search(project_dir_entry.files, fileinfo) + if not match then + table.insert(project_dir_entry.files, index, fileinfo) + project_dir_entry.is_dirty = true + return + end +end + + -- create a directory using mkdir but may need to create the parent -- directories as well. local function create_user_directory() @@ -373,10 +446,13 @@ function core.add_project_directory(path) -- 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 watch_id = system.watch_dir(path); table.insert(core.project_directories, { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, - files = {} + files = {}, + is_dirty = true, + watch_id = watch_id, }) end @@ -916,6 +992,15 @@ function core.try(fn, ...) end +function core.on_dir_change(watch_id, action, filepath) + if action == "delete" then + project_scan_remove_file(watch_id, filepath) + elseif action == "create" then + project_scan_add_file(watch_id, filepath) + end +end + + function core.on_event(type, ...) local did_keymap = false if type == "textinput" then @@ -951,6 +1036,9 @@ function core.on_event(type, ...) end elseif type == "focuslost" then core.root_view:on_focus_lost(...) + elseif type == "dirchange" then + print("DEBUG: dirchange", select(1, ...), select(2, ...), select(3, ...)) + core.on_dir_change(...) elseif type == "quit" then core.quit() end diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 84d5dd28..22ba4921 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -111,11 +111,12 @@ function TreeView:check_cache() if not last_files then self.last[dir.name] = dir.files else - if dir.files ~= last_files then + if dir.is_dirty or dir.files ~= last_files then self:invalidate_cache(dir.name) self.last[dir.name] = dir.files end end + dir.is_dirty = false 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/src/api/system.c b/src/api/system.c index 2f1bf763..9c8f8faa 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 @@ -222,6 +223,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; } @@ -637,6 +646,56 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +static void watch_callback(dmon_watch_id watch_id, dmon_action action, const char* rootdir, + const char* filepath, const char* oldfilepath, void* user) +{ + (void)(user); + (void)(rootdir); + dirmonitor_push_event(watch_id, action, filepath, oldfilepath); +} + +static int f_watch_dir(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + fprintf(stderr, "DEBUG: watching dir: %s\n", path); fflush(stderr); + dmon_watch_id watch_id = dmon_watch(path, watch_callback, DMON_WATCHFLAGS_RECURSIVE, NULL); + lua_pushnumber(L, watch_id.id); + // FIXME: we ignore if there is an error. + return 1; +} + +#ifdef _WIN32 +#define PATHSEP '\\' +#else +#define PATHSEP '/' +#endif + + +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; + int i; + for (i = 0; i < len1 && i < len2; i++) { + if (path1[i] != path2[i]) break; + } + if (strchr(path1 + i, PATHSEP)) { + type1 = 0; + } + if (strchr(path2 + i, PATHSEP)) { + type2 = 0; + } + if (type1 != type2) { + lua_pushboolean(L, type1 < type2); + return 1; + } + lua_pushboolean(L, strcmp(path1 + i, path2 + i) < 0); + return 1; +} + static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -664,6 +723,8 @@ 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 }, { NULL, NULL } }; diff --git a/src/dirmonitor.c b/src/dirmonitor.c new file mode 100644 index 00000000..e94c7687 --- /dev/null +++ b/src/dirmonitor.c @@ -0,0 +1,56 @@ +#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; + 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; + fprintf(stderr, "DEBUG: send watch_id: %d action; %d\n", watch_id.id, action); fflush(stderr); + ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff); + ev.user.data1 = new_filepath; + SDL_PushEvent(&ev); +} + +void dirmonitor_init() { + dmon_init(); + /* FIXME: not needed ? */ + /* sdl_dmon_event_type = SDL_RegisterEvents(1); */ +} + +void dirmonitor_deinit() { + dmon_deinit(); +} + +void dirmonitor_push_event(dmon_watch_id watch_id, dmon_action action, const char *filepath, + const char *oldfilepath) +{ + 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..1fae4635 --- /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_push_event(dmon_watch_id watch_id, dmon_action action, const char *filepath, + const char *oldfilepath); + +#endif + diff --git a/src/dmon.h b/src/dmon.h new file mode 100644 index 00000000..b4f0d592 --- /dev/null +++ b/src/dmon.h @@ -0,0 +1,1514 @@ +// +// 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. +// +// clang-format off +// 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); + +#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; + +// clang-format on + +_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); +} + +// clang-format off +#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; + volatile int modify_watches; + pthread_t thread_handle; + pthread_mutex_t mutex; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; +// clang-format on + +_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 = 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); + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recurse + dmon__watch_recursive(watchdir, fd, mask, followlinks, watch); + } + } + closedir(dir); +} + +_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; + } + } + + DMON_ASSERT(0); + return NULL; +} + +_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_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; + } + } + } + + // trigger user callbacks + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; 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; + } + + switch (ev->mask) { + case IN_CREATE: + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case IN_MODIFY: + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case IN_MOVED_FROM: { + 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) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } break; + case IN_DELETE: + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + } + } + + + 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) { + + 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]; + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(watch->fd, &rfds); + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { + 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]; + + char filepath[DMON_MAX_PATH]; + dmon__strcpy(filepath, sizeof(filepath), dmon__find_subdir(watch, iev->wd)); + 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]); + + __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 { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = 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); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + 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("watch failed: %s", watch->rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watch->rootdir); + 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); + __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); +} +// clang-format off +#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]; + 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; +// clang-format on + +_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, ev->filepath, NULL, + watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, 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, + 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, 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]; + + 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); + + // normalize path (TODO: have to recheck this to be consistent with other platforms) + dmon__tolower(abs_filepath, sizeof(abs_filepath), + dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath)); + + // strip the root dir + DMON_ASSERT(strstr(abs_filepath, watch->rootdir) == abs_filepath); + 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 { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + // create FS objects + CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir, 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); +} + +// clang-format off +#endif + +#endif // DMON_IMPL +#endif // __DMON_H__ +// clang-format on diff --git a/src/main.c b/src/main.c index 470f0b5e..3918cf0d 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,8 @@ #include #endif +#include "dirmonitor.h" + SDL_Window *window; @@ -108,6 +110,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); @@ -191,6 +195,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 09d2c0a325f9edf5c97a19bf8675e6f6191e11c9 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 14 Jul 2021 12:36:17 +0200 Subject: [PATCH 156/180] Update dmon from septag/dmon with fix for linux Update from https://github.com/septag/dmon, commit: 48234fc2 to include a fix for linux. --- src/dmon.h | 84 +++++++++++++++++++++++------------------------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/dmon.h b/src/dmon.h index b4f0d592..54928bcc 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -763,19 +763,6 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m closedir(dir); } -_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; - } - } - - DMON_ASSERT(0); - return NULL; -} - _DMON_PRIVATE void dmon__inotify_process_events(void) { for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { @@ -811,10 +798,9 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) break; } } - } else if (check_ev->mask == IN_MODIFY && - strcmp(ev->filepath, check_ev->filepath) == 0) { + } 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 + // so we ignore MODIFY event check_ev->skip = true; } } @@ -867,12 +853,10 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) switch (ev->mask) { case IN_CREATE: - watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, - watch->user_data); + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); break; case IN_MODIFY: - watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, - watch->user_data); + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); break; case IN_MOVED_FROM: { for (int j = i + 1; j < c; j++) { @@ -885,8 +869,7 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) } } break; case IN_DELETE: - watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, - watch->user_data); + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); break; } } @@ -921,38 +904,43 @@ static void* dmon__thread(void* arg) 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 rfds; - FD_ZERO(&rfds); FD_SET(watch->fd, &rfds); - timeout.tv_sec = 0; - timeout.tv_usec = 100000; + } - if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { - 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]; - - char filepath[DMON_MAX_PATH]; - dmon__strcpy(filepath, sizeof(filepath), dmon__find_subdir(watch, iev->wd)); - dmon__strcat(filepath, sizeof(filepath), iev->name); - - // TODO: ignore directories if flag is set - - if (stb_sb_count(_dmon.events) == 0) { - usecs_elapsed = 0; + 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; } - 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; + while (offset < len) { + struct inotify_event* iev = (struct inotify_event*)&buff[offset]; + + char filepath[DMON_MAX_PATH]; + dmon__strcpy(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; + } } } } From 7ee00c3317fc8d5ec7df8bfb4f1b3b63d53e2b3e Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 14 Jul 2021 22:57:37 +0200 Subject: [PATCH 157/180] Remove the project scan thread Since the directory monitoring is now basically working we remove the project scan thread periodically scanning the project directory. Each project's directory is scanned only once at the beginning when calling the function `core.add_project_directory` and is updated incrementally when directory change events are treated. The config variable `project_scan_rate` is removed as well as the function `core.reschedule_project_scan`. --- data/core/commands/core.lua | 2 - data/core/config.lua | 1 - data/core/init.lua | 151 ++++++++++++---------------- data/plugins/autoreload.lua | 5 +- data/plugins/treeview.lua | 1 - resources/notes-dmon-integration.md | 31 ++++++ 6 files changed, 99 insertions(+), 92 deletions(-) create mode 100644 resources/notes-dmon-integration.md diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 432ded89..ab021368 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -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 caecdfcd..689968d5 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 4e2a4e37..d5b14864 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 @@ -106,8 +96,8 @@ local function get_project_file_info(root, file, size_limit) local info = system.get_file_info(root .. file) if info then info.filename = strip_leading_path(file) + return info.size < size_limit and info end - return info and info.size < size_limit and info end @@ -156,48 +146,47 @@ 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 + +-- FIXME: change the name with core.scan_project_folder +function core.project_scan_topdir(index) + local dir = core.project_directories[index] + local t, entries_count = get_directory_files(dir.name, "", {}, true) + if entries_count > config.max_project_files then + dir.files_limit = true + if core.status_view then -- FIXME + 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 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 - end - - -- wait for next scan - coroutine.yield(config.project_scan_rate) + dir.files = t + if dir.name == core.project_dir then + core.project_files = dir.files 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_path(path) + local watch_id = system.watch_dir(path) + print("DEBUG watch_id:", watch_id) + table.insert(core.project_directories, { + name = path, + item = {filename = common.basename(path), type = "dir", topdir = true}, + files_limit = false, + is_dirty = true, + watch_id = watch_id, + }) + core.project_scan_topdir(#core.project_directories) + core.redraw = true +end + + function core.is_project_folder(dirname) for _, dir in ipairs(core.project_directories) do if dir.name == dirname then @@ -227,7 +216,7 @@ function core.scan_project_folder(dirname, filename) end end - +-- FIXME: rename this function local function find_project_files_co(root, path) local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} @@ -250,39 +239,46 @@ end 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 + local ok, name, file = coroutine.resume(state.co, dir.name, "") + if ok and name then + return name, file + else + state.co = false + state.file_index = 1 + state.dir_index = state.dir_index + 1 + dir = core.project_directories[state.dir_index] + end + else + 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 + state.co = coroutine.create(find_project_files_co) + 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 - end - return n + 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 @@ -441,22 +437,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) - local watch_id = system.watch_dir(path); - table.insert(core.project_directories, { - name = path, - item = {filename = common.basename(path), type = "dir", topdir = true}, - files = {}, - is_dirty = true, - watch_id = watch_id, - }) -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 @@ -592,7 +572,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() 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 22ba4921..f18378d0 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -207,7 +207,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 diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md new file mode 100644 index 00000000..6bc2e920 --- /dev/null +++ b/resources/notes-dmon-integration.md @@ -0,0 +1,31 @@ +`core.scan_project_folder`: + scan a single folder, without recursion. Used when too many files. + + +`core.add_project_directory`: + Add a new top-level folder to the project. + +`core.set_project_dir`: + Set the initial project directory. + + + +`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. + From 0d1bec8e353e60a754b475178f08cfe093ed0c90 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 14 Jul 2021 23:15:05 +0200 Subject: [PATCH 158/180] Remove the treeview check for modified files In the treeview the implementation was checking the files list to detect if it changed because of a project scan. Since we removed the project scan we no longer need the check. Removed the TreeView's self.last table that stores previous files object by top-level directories. --- data/plugins/treeview.lua | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index f18378d0..5edc9cdd 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 @@ -107,14 +106,8 @@ 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.is_dirty or dir.files ~= last_files then - self:invalidate_cache(dir.name) - self.last[dir.name] = dir.files - end + if dir.is_dirty and self.cache[dir.name] then + self:invalidate_cache(dir.name) end dir.is_dirty = false end From e01f6fefe653bcaa9250a10e99ce12efccf8d6b8 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 15 Jul 2021 09:56:49 +0200 Subject: [PATCH 159/180] Treat watch dir errors and fix various things Verity if dmon_watch returns an error. Add a check if an added file for which we received a create event is ignored based on the user's config. Add some explanatory comments in the code. --- data/core/init.lua | 2 +- src/api/system.c | 21 +++++++++------------ src/dirmonitor.c | 12 ++++++++---- src/dirmonitor.h | 4 ++-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index d5b14864..07f97ff3 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -333,7 +333,7 @@ local function project_scan_add_file(watch_id, filepath) project_dir_entry = core.project_directories[i] end end - if not project_dir_entry then return end + if not project_dir_entry or common.match_pattern(filepath, config.ignore_files) then return end local size_limit = config.file_size_limit * 10e5 local fileinfo = get_project_file_info(project_dir_entry.name, PATHSEP .. filepath, size_limit) local index, match = file_search(project_dir_entry.files, fileinfo) diff --git a/src/api/system.c b/src/api/system.c index 9c8f8faa..7eceb692 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -646,20 +646,11 @@ static int f_set_window_opacity(lua_State *L) { return 1; } -static void watch_callback(dmon_watch_id watch_id, dmon_action action, const char* rootdir, - const char* filepath, const char* oldfilepath, void* user) -{ - (void)(user); - (void)(rootdir); - dirmonitor_push_event(watch_id, action, filepath, oldfilepath); -} - static int f_watch_dir(lua_State *L) { const char *path = luaL_checkstring(L, 1); - fprintf(stderr, "DEBUG: watching dir: %s\n", path); fflush(stderr); - dmon_watch_id watch_id = dmon_watch(path, watch_callback, DMON_WATCHFLAGS_RECURSIVE, NULL); + dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, DMON_WATCHFLAGS_RECURSIVE, NULL); + if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } lua_pushnumber(L, watch_id.id); - // FIXME: we ignore if there is an error. return 1; } @@ -669,7 +660,8 @@ static int f_watch_dir(lua_State *L) { #define PATHSEP '/' #endif - +/* Special purpose filepath compare function. Corresponds to the + order used in the TreeView view of the project's files. */ static int f_path_compare(lua_State *L) { const char *path1 = luaL_checkstring(L, 1); const char *type1_s = luaL_checkstring(L, 2); @@ -678,20 +670,25 @@ static int f_path_compare(lua_State *L) { 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 i; for (i = 0; i < len1 && i < len2; i++) { if (path1[i] != path2[i]) break; } + /* If a path separator is present in the name after the common part we consider + the entry like a directory. */ if (strchr(path1 + i, PATHSEP)) { type1 = 0; } if (strchr(path2 + i, 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. */ lua_pushboolean(L, strcmp(path1 + i, path2 + i) < 0); return 1; } diff --git a/src/dirmonitor.c b/src/dirmonitor.c index e94c7687..eb3b185f 100644 --- a/src/dirmonitor.c +++ b/src/dirmonitor.c @@ -11,6 +11,8 @@ 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); @@ -23,7 +25,6 @@ static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const cha #endif SDL_zero(ev); ev.type = SDL_USEREVENT; - fprintf(stderr, "DEBUG: send watch_id: %d action; %d\n", watch_id.id, action); fflush(stderr); ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff); ev.user.data1 = new_filepath; SDL_PushEvent(&ev); @@ -31,7 +32,8 @@ static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const cha void dirmonitor_init() { dmon_init(); - /* FIXME: not needed ? */ + /* 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); */ } @@ -39,9 +41,11 @@ void dirmonitor_deinit() { dmon_deinit(); } -void dirmonitor_push_event(dmon_watch_id watch_id, dmon_action action, const char *filepath, - const char *oldfilepath) +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); diff --git a/src/dirmonitor.h b/src/dirmonitor.h index 1fae4635..ab9376c0 100644 --- a/src/dirmonitor.h +++ b/src/dirmonitor.h @@ -7,8 +7,8 @@ void dirmonitor_init(); void dirmonitor_deinit(); -void dirmonitor_push_event(dmon_watch_id watch_id, dmon_action action, const char *filepath, - const char *oldfilepath); +void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, + const char *filepath, const char *oldfilepath, void *user); #endif From ad7bdeb1294284cbbb46dbf123529b2cd94a61e6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 15 Jul 2021 12:17:18 +0200 Subject: [PATCH 160/180] Fix bug with expanding directory when file limited Introduce a new field in items generated by TreeView:each_item() to point "dir" to the toplevel directory entry. In this was we can simplify the code and know if the toplevel directory is files limited. --- data/core/commands/core.lua | 1 + data/core/init.lua | 38 ++++++++++++++--------------- data/plugins/treeview.lua | 23 ++++++----------- resources/notes-dmon-integration.md | 5 +++- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index ab021368..b6604591 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,6 +66,7 @@ command.add(nil, { end, ["core:find-file"] = function() + -- FIXME: core.project_files_limit was removed! if core.project_files_limit then return command.perform "core:open-file" end diff --git a/data/core/init.lua b/data/core/init.lua index 07f97ff3..003bc410 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -147,8 +147,8 @@ local function get_directory_files(root, path, t, recursive, begin_hook) end --- FIXME: change the name with core.scan_project_folder -function core.project_scan_topdir(index) +-- Populate a project folder top directory by scanning the filesystem. +function core.scan_project_folder(index) local dir = core.project_directories[index] local t, entries_count = get_directory_files(dir.name, "", {}, true) if entries_count > config.max_project_files then @@ -182,22 +182,12 @@ function core.add_project_directory(path) is_dirty = true, watch_id = watch_id, }) - core.project_scan_topdir(#core.project_directories) + core.scan_project_folder(#core.project_directories) core.redraw = true 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 -end - - -function core.scan_project_folder(dirname, filename) +function core.scan_project_subdir(dirname, filename) for _, dir in ipairs(core.project_directories) do if dir.name == dirname then for i, file in ipairs(dir.files) do @@ -209,15 +199,17 @@ function core.scan_project_folder(dirname, filename) table.insert(dir.files, i + j, new_file) end file.scanned = true - return + return true end end end end end --- FIXME: rename this function -local function find_project_files_co(root, path) +-- 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 size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} for _, file in ipairs(all) do @@ -229,7 +221,7 @@ local function find_project_files_co(root, path) if info.type == "file" then coroutine.yield(root, info) else - find_project_files_co(root, PATHSEP .. info.filename) + find_files_rec(root, PATHSEP .. info.filename) end end end @@ -237,19 +229,25 @@ local function find_project_files_co(root, path) end +-- Iterator function to list all project files local function project_files_iter(state) local 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 @@ -259,7 +257,9 @@ local function project_files_iter(state) end if not dir then return end if dir.files_limit then - state.co = coroutine.create(find_project_files_co) + -- 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] diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 5edc9cdd..426990f2 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -53,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 = {} @@ -79,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 @@ -125,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 @@ -216,19 +217,9 @@ 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) + if hovered_item.dir.files_limit and not hovered_item.expanded then + local dirname = hovered_item.dir.name + if core.scan_project_subdir(dirname, hovered_item.filename) then self:invalidate_cache(dirname) end end diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md index 6bc2e920..c4485537 100644 --- a/resources/notes-dmon-integration.md +++ b/resources/notes-dmon-integration.md @@ -1,6 +1,9 @@ -`core.scan_project_folder`: +`core.scan_project_folder`: (renamed to `core.scan_project_subdir`) scan a single folder, without recursion. Used when too many files. +New `core.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. From aa37f2b149522dbfd58996edc532a449668ed944 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 15 Jul 2021 18:49:18 +0200 Subject: [PATCH 161/180] Fix several problem with directory update When scanning a subdirectory on-demand ensure files aready present are not added twice. Files or directory can be already present due to dir monitoring create message. Fix check for ignore files when adding a file to respond to a dir monitor event to use each part of the file's path. Fix C function to compare files for treeview placement. --- data/core/init.lua | 69 +++++++++++++++++++++++---------------- data/plugins/treeview.lua | 6 ++-- src/api/system.c | 16 ++++++++- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 003bc410..25661205 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -187,6 +187,36 @@ function core.add_project_directory(path) 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 + + function core.scan_project_subdir(dirname, filename) for _, dir in ipairs(core.project_directories) do if dir.name == dirname then @@ -195,8 +225,8 @@ function core.scan_project_subdir(dirname, filename) 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) + for _, new_file in ipairs(new_files) do + project_scan_add_entry(dir, new_file) end file.scanned = true return true @@ -282,27 +312,6 @@ function core.project_files_number() 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_remove_file(watch_id, filepath) local project_dir_entry for i = 1, #core.project_directories do @@ -333,14 +342,16 @@ local function project_scan_add_file(watch_id, filepath) project_dir_entry = core.project_directories[i] end end - if not project_dir_entry or common.match_pattern(filepath, config.ignore_files) then return end + if not project_dir_entry then return end + for fragment in string.gmatch(filepath, "([^/\\]+)") do + if common.match_pattern(fragment, config.ignore_files) then + return + end + end local size_limit = config.file_size_limit * 10e5 local fileinfo = get_project_file_info(project_dir_entry.name, PATHSEP .. filepath, size_limit) - local index, match = file_search(project_dir_entry.files, fileinfo) - if not match then - table.insert(project_dir_entry.files, index, fileinfo) - project_dir_entry.is_dirty = true - return + if fileinfo then + project_scan_add_entry(project_dir_entry, fileinfo) end end diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 426990f2..63f98dc5 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -104,9 +104,9 @@ 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] + -- 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 @@ -219,9 +219,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) else if hovered_item.dir.files_limit and not hovered_item.expanded then local dirname = hovered_item.dir.name - if core.scan_project_subdir(dirname, hovered_item.filename) then - self:invalidate_cache(dirname) - end + core.scan_project_subdir(dirname, hovered_item.filename) end hovered_item.expanded = not hovered_item.expanded end diff --git a/src/api/system.c b/src/api/system.c index 7eceb692..2fa1fb60 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -689,7 +689,21 @@ static int f_path_compare(lua_State *L) { return 1; } /* If types are the same compare the files' path alphabetically. */ - lua_pushboolean(L, strcmp(path1 + i, path2 + i) < 0); + int cfr = 0; + int len_min = (len1 < len2 ? len1 : len2); + for (int j = i; 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; } From 5f4cf6f2507b3f3bc64fddbe8beaac7b3f11c778 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 19 Jul 2021 10:47:31 +0200 Subject: [PATCH 162/180] Show max files warning message for initial project If the max number of files limit is achieved when the application is starting the StatusView is not yet configured so we cannot show the warning. We show the warning in the function scanning the directory only if the StatusView is up. On the other side, when the application starts it will check if the initial project dir hit the max files limit and show the warning if needed. --- data/core/init.lua | 41 +++++++++++++++++------------ resources/notes-dmon-integration.md | 13 +++++++++ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 25661205..21e23df1 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -147,24 +147,25 @@ local function get_directory_files(root, path, t, recursive, begin_hook) end +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. -function core.scan_project_folder(index) +local function scan_project_folder(index) local dir = core.project_directories[index] local t, entries_count = get_directory_files(dir.name, "", {}, true) if entries_count > config.max_project_files then dir.files_limit = true - if core.status_view then -- FIXME - 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." - ) + if core.status_view then -- May be not yet initialized. + show_max_files_warning() end end dir.files = t - if dir.name == core.project_dir then - core.project_files = dir.files - end end @@ -174,15 +175,18 @@ function core.add_project_directory(path) -- The field item.topdir will identify it as a top level directory. path = common.normalize_path(path) local watch_id = system.watch_dir(path) - print("DEBUG watch_id:", watch_id) - table.insert(core.project_directories, { + local dir = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, files_limit = false, is_dirty = true, watch_id = watch_id, - }) - core.scan_project_folder(#core.project_directories) + } + 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 @@ -320,13 +324,11 @@ local function project_scan_remove_file(watch_id, filepath) end end if not project_dir_entry then return end - print("LOOKING for", filepath, " in", project_dir_entry and project_dir_entry.name) local fileinfo = { filename = filepath } for _, filetype in ipairs {"dir", "file"} do fileinfo.type = filetype local index, match = file_search(project_dir_entry.files, fileinfo) if match then - print("FOUND", filepath, " at index", index) table.remove(project_dir_entry.files, index) project_dir_entry.is_dirty = true return @@ -593,6 +595,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 @@ -1027,7 +1035,6 @@ function core.on_event(type, ...) elseif type == "focuslost" then core.root_view:on_focus_lost(...) elseif type == "dirchange" then - print("DEBUG: dirchange", select(1, ...), select(2, ...), select(3, ...)) core.on_dir_change(...) elseif type == "quit" then core.quit() diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md index c4485537..9248b115 100644 --- a/resources/notes-dmon-integration.md +++ b/resources/notes-dmon-integration.md @@ -1,3 +1,16 @@ + +`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. + `core.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_folder`: (renamed to `core.scan_project_subdir`) scan a single folder, without recursion. Used when too many files. From e57d63e3a30bc5da08ed88a4c4eb753d30e22245 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 20 Jul 2021 11:47:55 +0200 Subject: [PATCH 163/180] Update dmon from septag/dmon commit 74bbd93b The new version includes fixes from jgmdev, github PR: https://github.com/septag/dmon/pull/11 to solve incorrect behavior on linux not reporting directory creation. Includes also a further revision from septag. --- src/dmon.h | 130 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/src/dmon.h b/src/dmon.h index 54928bcc..92fba034 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -6,23 +6,23 @@ // watches directories for file or directory changes. // // clang-format off -// Usage: +// 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. +// dmon_init(): +// Call this once at the start of your program. // This will start a low-priority monitoring thread -// dmon_deinit(): +// 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 +// 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 @@ -35,12 +35,12 @@ // // Configuration: // You can customize some low-level functionality like malloc and logging by overriding macros: -// -// DMON_MALLOC, DMON_FREE, DMON_REALLOC: +// +// 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 +// DMON_ASSERT: +// define this to provide your own assert // default is 'assert' // DMON_LOG_ERROR: // define this to provide your own logging mechanism @@ -60,9 +60,9 @@ // // TODO: // - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files -// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS +// - 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 @@ -86,9 +86,9 @@ typedef struct { uint32_t id; } dmon_watch_id; -// Pass these flags to `dmon_watch` +// Pass these flags to `dmon_watch` typedef enum dmon_watch_flags_t { - DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories + 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 @@ -197,7 +197,7 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # include # define DMON_LOG_DEBUG(s) do { puts(s); } while(0) # else -# define DMON_LOG_DEBUG(s) +# define DMON_LOG_DEBUG(s) # endif #endif @@ -230,7 +230,7 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); #endif #ifndef dmon__min -# define dmon__min(a, b) ((a) < (b) ? (a) : (b)) +# define dmon__min(a, b) ((a) < (b) ? (a) : (b)) #endif #ifndef dmon__max @@ -364,7 +364,7 @@ typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const cha typedef struct dmon__win32_event { char filepath[DMON_MAX_PATH]; DWORD action; - dmon_watch_id watch_id; + dmon_watch_id watch_id; bool skip; } dmon__win32_event; @@ -753,6 +753,10 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m 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); @@ -763,6 +767,19 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m closedir(dir); } +_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; + } + } + + DMON_ASSERT(0); + return NULL; +} + _DMON_PRIVATE void dmon__inotify_process_events(void) { for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { @@ -772,43 +789,54 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) } // remove redundant modify events on a single file - if (ev->mask == IN_MODIFY) { + 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) { + 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 = strlen(ev->filepath); + int l2 = 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) { + } 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) { + 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) { + 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) { + } 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) { + } 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) { + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { move_valid = true; break; } @@ -820,11 +848,11 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) if (!move_valid) { ev->mask = IN_DELETE; } - } else if (ev->mask == IN_MOVED_TO) { + } 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) { + if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { move_valid = true; break; } @@ -851,26 +879,45 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) continue; } - switch (ev->mask) { - case IN_CREATE: + 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); + } + } watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); - break; - case IN_MODIFY: + } + else if (ev->mask & IN_MODIFY) { watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); - break; - case IN_MOVED_FROM: { + } + else if (ev->mask & IN_MOVED_FROM) { 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) { + 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; } } - } break; - case IN_DELETE: + } + else if (ev->mask & IN_DELETE) { watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); - break; } } @@ -928,7 +975,8 @@ static void* dmon__thread(void* arg) struct inotify_event* iev = (struct inotify_event*)&buff[offset]; char filepath[DMON_MAX_PATH]; - dmon__strcpy(filepath, sizeof(filepath), iev->name); + dmon__strcpy(filepath, sizeof(filepath), dmon__find_subdir(watch, iev->wd)); + dmon__strcat(filepath, sizeof(filepath), iev->name); // TODO: ignore directories if flag is set @@ -1068,7 +1116,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, return dmon__make_id(0); } dmon__watch_subdir subdir; - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watch->rootdir); + 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); @@ -1379,7 +1427,7 @@ _DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void dmon__strcpy(abs_filepath, sizeof(abs_filepath), filepath); // normalize path (TODO: have to recheck this to be consistent with other platforms) - dmon__tolower(abs_filepath, sizeof(abs_filepath), + dmon__tolower(abs_filepath, sizeof(abs_filepath), dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath)); // strip the root dir From 494587954fd95add8c74e3b523b59205b3834a56 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 20 Jul 2021 15:28:36 +0200 Subject: [PATCH 164/180] More accurate path compare function --- resources/notes-dmon-integration.md | 2 +- src/api/system.c | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md index 9248b115..6b3a316d 100644 --- a/resources/notes-dmon-integration.md +++ b/resources/notes-dmon-integration.md @@ -14,7 +14,7 @@ `core.scan_project_folder`: (renamed to `core.scan_project_subdir`) scan a single folder, without recursion. Used when too many files. -New `core.scan_project_folder`: +New local function `scan_project_folder`: Populate the project folder top directory. Done only once when the directory is added to the project. diff --git a/src/api/system.c b/src/api/system.c index 2fa1fb60..0eb09108 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -661,7 +661,8 @@ static int f_watch_dir(lua_State *L) { #endif /* Special purpose filepath compare function. Corresponds to the - order used in the TreeView view of the project's files. */ + 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); @@ -671,16 +672,19 @@ static int f_path_compare(lua_State *L) { 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 i; + 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 + i, PATHSEP)) { + if (strchr(path1 + offset, PATHSEP)) { type1 = 0; } - if (strchr(path2 + i, PATHSEP)) { + if (strchr(path2 + offset, PATHSEP)) { type2 = 0; } /* If types are different "dir" types comes before "file" types. */ @@ -691,7 +695,7 @@ static int f_path_compare(lua_State *L) { /* If types are the same compare the files' path alphabetically. */ int cfr = 0; int len_min = (len1 < len2 ? len1 : len2); - for (int j = i; j <= len_min; j++) { + 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); From b214471a1b6b88fe61526d484d4ae8ea913d3763 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 23 Jul 2021 19:36:31 +0200 Subject: [PATCH 165/180] Implement project files rescan on dir changes In theory the dmon based directory monitoring is enough to ensure that the list of project files is always correct. In reality some events may be missing and the project files list may get disaligned with the real list of files. To avoid the problem we add an additional rescan to be done later in a thread on any project subdirectory affected by an event of directory of file change. In the rescan found the same files already present the thread terminates. If a difference is found the files list is modified and a new rescan is scheduled. --- data/core/init.lua | 135 +++++++++++++++++++++++----- resources/notes-dmon-integration.md | 4 +- 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 21e23df1..38bb5c0f 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -109,8 +109,7 @@ 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) - if begin_hook then begin_hook() end +local function get_directory_files(root, path, t, recursive) local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} local dirs, files = {}, {} @@ -230,6 +229,7 @@ function core.scan_project_subdir(dirname, filename) if file.scanned then return end local new_files = get_directory_files(dirname, PATHSEP .. filename, {}) for _, new_file in ipairs(new_files) do + -- FIXME: add index bounds to limit the scope of the search. project_scan_add_entry(dir, new_file) end file.scanned = true @@ -240,6 +240,58 @@ function core.scan_project_subdir(dirname, filename) end end +-- for "a" inclusive from i1 + 1 and i2 +local function files_list_match(a, i1, i2, b) + if i2 - i1 ~= #b then return false end + for i = 1, #b do + if a[i1 + i].filename ~= b[i].filename or a[i1 + i].type ~= b[i].type then + return false + end + end + return true +end + +-- arguments like for files_list_match +local function files_list_replace(a, i1, i2, b) + local nmin = math.min(i2 - i1, #b) + for i = 1, nmin do + a[i1 + i] = b[i] + end + for j = 1, i2 - i1 - nmin do + table.remove(a, i1 + nmin + 1) + end + for j = 1, #b - nmin do + table.insert(a, i1 + nmin + 1, b[nmin + j]) + end +end + +local function rescan_project_subdir(dir, filename_rooted) + local new_files = get_directory_files(dir.name, filename_rooted, {}, true) + local index, n = 0, #dir.files + if filename_rooted ~= "" then + local filename = strip_leading_path(filename_rooted) + 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 + break + end + end + end + + if not files_list_match(dir.files, index, index + n, new_files) then + files_list_replace(dir.files, index, 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. @@ -316,44 +368,39 @@ function core.project_files_number() end -local function project_scan_remove_file(watch_id, filepath) - local project_dir_entry +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 - project_dir_entry = core.project_directories[i] + return core.project_directories[i] end end - if not project_dir_entry then return 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(project_dir_entry.files, fileinfo) + local index, match = file_search(dir.files, fileinfo) if match then - table.remove(project_dir_entry.files, index) - project_dir_entry.is_dirty = true + table.remove(dir.files, index) + dir.is_dirty = true return end end end -local function project_scan_add_file(watch_id, filepath) - local project_dir_entry - for i = 1, #core.project_directories do - if core.project_directories[i].watch_id == watch_id then - project_dir_entry = core.project_directories[i] - end - end - if not project_dir_entry then return 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 size_limit = config.file_size_limit * 10e5 - local fileinfo = get_project_file_info(project_dir_entry.name, PATHSEP .. filepath, size_limit) + local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath, size_limit) if fileinfo then - project_scan_add_entry(project_dir_entry, fileinfo) + project_scan_add_entry(dir, fileinfo) end end @@ -989,12 +1036,58 @@ function core.try(fn, ...) return false, err end +local scheduled_rescan = {} + +local function 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 + local _, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"}) + if not dir_match then return end + end + local new_time = system.get_time() + 1 + 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 + 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 + rescan.time_limit = new_time + else + scheduled_rescan[rescan.abs_path] = nil + return + end + end + coroutine.yield(0.2) + end + end, dir) +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 + dir_rescan_add_job(dir, filepath) if action == "delete" then - project_scan_remove_file(watch_id, filepath) + project_scan_remove_file(dir, filepath) elseif action == "create" then - project_scan_add_file(watch_id, filepath) + project_scan_add_file(dir, filepath) end end diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md index 6b3a316d..6dfc997e 100644 --- a/resources/notes-dmon-integration.md +++ b/resources/notes-dmon-integration.md @@ -6,12 +6,12 @@ `core.add_project_directory`: Add a new top-level directory to the project. Also called from modules and commands outside core.init. - `core.scan_project_folder`: + 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_folder`: (renamed to `core.scan_project_subdir`) +`core.scan_project_subdir`: (before was named `core.scan_project_folder`) scan a single folder, without recursion. Used when too many files. New local function `scan_project_folder`: From 3af4d9fd5999cb3764f6e5f030909db2793a0126 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 23 Jul 2021 21:23:37 +0200 Subject: [PATCH 166/180] Ensure directory is rescanned after the first read --- data/core/init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 38bb5c0f..59c8e2d2 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -165,6 +165,7 @@ local function scan_project_folder(index) end end dir.files = t + core.dir_rescan_add_job(dir, ".") end @@ -1038,7 +1039,7 @@ end local scheduled_rescan = {} -local function dir_rescan_add_job(dir, filepath) +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 @@ -1083,7 +1084,7 @@ 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 - dir_rescan_add_job(dir, filepath) + core.dir_rescan_add_job(dir, filepath) if action == "delete" then project_scan_remove_file(dir, filepath) elseif action == "create" then From 9d9adccec89ded03abe5238ac8491ad96f9d9e37 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 23 Jul 2021 23:04:57 +0200 Subject: [PATCH 167/180] Fix a few things about dmon Ensure that we call coroutine.yield when scanning recursively. Do not use a weak-key based on project dir when adding the job for rescan. Since "dir" was not unique many threads were missing. Ensure we do not block waiting for events if there are pending rescan. --- data/core/init.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 59c8e2d2..3cf92eca 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -109,7 +109,8 @@ 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) +local function get_directory_files(root, path, t, recursive, begin_hook) + if begin_hook then begin_hook() end local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} local dirs, files = {}, {} @@ -131,7 +132,7 @@ local function get_directory_files(root, path, t, recursive) 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) + local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive, begin_hook) entries_count = entries_count + subdir_count f.scanned = true end @@ -267,7 +268,7 @@ local function files_list_replace(a, i1, i2, b) end local function rescan_project_subdir(dir, filename_rooted) - local new_files = get_directory_files(dir.name, filename_rooted, {}, true) + local new_files = get_directory_files(dir.name, filename_rooted, {}, true, coroutine.yield) local index, n = 0, #dir.files if filename_rooted ~= "" then local filename = strip_leading_path(filename_rooted) @@ -1039,6 +1040,12 @@ 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 "" @@ -1077,7 +1084,7 @@ function core.dir_rescan_add_job(dir, filepath) end coroutine.yield(0.2) end - end, dir) + end) end @@ -1236,7 +1243,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 From 44cbe55baade57125b4a770a9e88ba8063695e37 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sat, 24 Jul 2021 14:42:31 +0200 Subject: [PATCH 168/180] Ensure all project files are correctly filtered --- data/core/init.lua | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 3cf92eca..8bc71bb7 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -92,11 +92,13 @@ 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, size_limit) +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 < size_limit and info + return (info.size < config.file_size_limit * 1e6 and + not common.match_pattern(info.filename, config.ignore_files) + and info) end end @@ -111,20 +113,17 @@ end -- complete file path relative to "root" *without* the trailing '/'. local function get_directory_files(root, path, t, recursive, begin_hook) if begin_hook then begin_hook() end - local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} local dirs, files = {}, {} local entries_count = 0 local max_entries = config.max_project_files for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local info = get_project_file_info(root, path .. PATHSEP .. file, size_limit) - if info then - 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 + if recursive and entries_count > max_entries then return nil, entries_count end end end @@ -298,19 +297,16 @@ end -- 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 size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - if info.type == "file" then - coroutine.yield(root, info) - else - find_files_rec(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 @@ -399,8 +395,7 @@ local function project_scan_add_file(dir, filepath) return end end - local size_limit = config.file_size_limit * 10e5 - local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath, size_limit) + local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) if fileinfo then project_scan_add_entry(dir, fileinfo) end From 4cfe0c924585e311fcc6a63490eea14159bf6b21 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Fri, 23 Jul 2021 23:30:06 +0200 Subject: [PATCH 169/180] Fix error in rescan list replace --- 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 8bc71bb7..8ab5c61c 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -262,7 +262,7 @@ local function files_list_replace(a, i1, i2, b) table.remove(a, i1 + nmin + 1) end for j = 1, #b - nmin do - table.insert(a, i1 + nmin + 1, b[nmin + j]) + table.insert(a, i1 + nmin + j, b[nmin + j]) end end From 264bda273d46a81a3e31484c5867304f4428c015 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 25 Jul 2021 00:14:21 +0200 Subject: [PATCH 170/180] Smarter algorithm to patch files list New algorithm use the fact that files list are always sorted to optimize the table's insertions and removals. --- data/core/init.lua | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 8ab5c61c..ea6a2883 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -241,11 +241,16 @@ function core.scan_project_subdir(dirname, filename) end end --- for "a" inclusive from i1 + 1 and i2 -local function files_list_match(a, i1, i2, b) - if i2 - i1 ~= #b then return false end - for i = 1, #b do - if a[i1 + i].filename ~= b[i].filename or a[i1 + i].type ~= b[i].type then + +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 @@ -253,16 +258,22 @@ local function files_list_match(a, i1, i2, b) end -- arguments like for files_list_match -local function files_list_replace(a, i1, i2, b) - local nmin = math.min(i2 - i1, #b) - for i = 1, nmin do - a[i1 + i] = b[i] - end - for j = 1, i2 - i1 - nmin do - table.remove(a, i1 + nmin + 1) - end - for j = 1, #b - nmin do - table.insert(a, i1 + nmin + j, b[nmin + j]) +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 @@ -286,8 +297,8 @@ local function rescan_project_subdir(dir, filename_rooted) end end - if not files_list_match(dir.files, index, index + n, new_files) then - files_list_replace(dir.files, index, index + n, new_files) + 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 From 5a5fc04da417b9be14ba61c0c53c8af205e510fa Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 25 Jul 2021 15:16:01 +0200 Subject: [PATCH 171/180] Fix a new things about project rescan Add a flag core.redraw to force redraw when rescan is done. Inhibit recursion when files_limit is reached. Still doesn't work correctly for files limited directories. --- data/core/init.lua | 76 +++++++++++++++-------------- data/plugins/treeview.lua | 3 +- resources/notes-dmon-integration.md | 9 +++- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index ea6a2883..754b6e5a 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -221,27 +221,6 @@ local function project_scan_add_entry(dir, fileinfo) end -function core.scan_project_subdir(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 _, new_file in ipairs(new_files) do - -- FIXME: add index bounds to limit the scope of the search. - project_scan_add_entry(dir, new_file) - end - file.scanned = true - return true - end - end - end - end -end - - local function files_info_equal(a, b) return a.filename == b.filename and a.type == b.type end @@ -277,24 +256,30 @@ local function files_list_replace(as, i1, n, bs) 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.name, filename_rooted, {}, true, coroutine.yield) + local recursive = not dir.files_limit + local new_files = get_directory_files(dir.name, filename_rooted, {}, recursive, coroutine.yield) local index, n = 0, #dir.files if filename_rooted ~= "" then local filename = strip_leading_path(filename_rooted) - 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 - break - end - end + index, n = project_subdir_bounds(dir, filename) end if not files_list_match(dir.files, index, n, new_files) then @@ -304,6 +289,17 @@ local function rescan_project_subdir(dir, filename_rooted) end end + +function core.scan_project_subdir(dir, filename) + local index, n, file = project_subdir_bounds(dir, filename) + if index then + local new_files = get_directory_files(dir.name, PATHSEP .. filename, {}) + files_list_replace(dir.files, index, n, new_files) + file.scanned = 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. @@ -1057,10 +1053,13 @@ function core.dir_rescan_add_job(dir, filepath) local dirpath_rooted = dirpath and PATHSEP .. dirpath or "" local abs_dirpath = dir.name .. dirpath_rooted if dirpath then - local _, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"}) - if not dir_match then return end + -- 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"}) + if not dir_match or (dir.files_limit and not dir.files[dir_index].scanned) 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 @@ -1068,12 +1067,14 @@ function core.dir_rescan_add_job(dir, filepath) 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 @@ -1082,6 +1083,7 @@ function core.dir_rescan_add_job(dir, filepath) 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 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 63f98dc5..c0d812aa 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -218,8 +218,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) create_directory_in(hovered_item) else if hovered_item.dir.files_limit and not hovered_item.expanded then - local dirname = hovered_item.dir.name - core.scan_project_subdir(dirname, hovered_item.filename) + core.scan_project_subdir(hovered_item.dir, hovered_item.filename) end hovered_item.expanded = not hovered_item.expanded end diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md index 6dfc997e..5179df40 100644 --- a/resources/notes-dmon-integration.md +++ b/resources/notes-dmon-integration.md @@ -14,7 +14,7 @@ `core.scan_project_subdir`: (before was named `core.scan_project_folder`) scan a single folder, without recursion. Used when too many files. -New local function `scan_project_folder`: +Local function `scan_project_folder`: Populate the project folder top directory. Done only once when the directory is added to the project. @@ -24,6 +24,13 @@ New local function `scan_project_folder`: `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`: From 2e59aaad3e37e5153a3d947b0fbe97318a2395a6 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Sun, 25 Jul 2021 20:49:35 +0200 Subject: [PATCH 172/180] Fix files limited project with dir monintoring Changed approach to files limited project. Now we keep into the top-level dir a list of subdirectories to be shown. When in file limited mode we will not scan subdirectories unless they are in the list of shown subdirectories. With the new mechanism the function get_subdirectory_files always recurse into subdirectories by default but is able to figure out to stop recursing into subdirectories for files limited project. The new mechanism is more robust of the previous one. Now the rescan of subdirectories is compatible with files limited project. --- data/core/init.lua | 36 +++++++++++++++++++++++------------- data/plugins/treeview.lua | 9 ++++++--- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 754b6e5a..2d98ff15 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -111,29 +111,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 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 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 - if recursive and entries_count > max_entries then return nil, entries_count end end end table.sort(dirs, compare_file) for _, f in ipairs(dirs) do table.insert(t, f) - if recursive and entries_count <= max_entries then - local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive, begin_hook) - 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 @@ -146,6 +144,16 @@ local function get_directory_files(root, path, t, recursive, begin_hook) end +function core.project_subdir_set_show(dir, filename, show) + dir.shown_subdir[filename] = show +end + + +function core.project_subdir_is_shown(dir, filename) + return not dir.files_limit or dir.shown_subdir[filename] +end + + local function show_max_files_warning() core.status_view:show_message("!", style.accent, "Too many files in project directory: stopped reading at ".. @@ -157,7 +165,7 @@ 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.name, "", {}, true) + 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 if core.status_view then -- May be not yet initialized. @@ -181,6 +189,7 @@ function core.add_project_directory(path) files_limit = false, is_dirty = true, watch_id = watch_id, + shown_subdir = {}, } table.insert(core.project_directories, dir) scan_project_folder(#core.project_directories) @@ -274,8 +283,7 @@ local function project_subdir_bounds(dir, filename) end local function rescan_project_subdir(dir, filename_rooted) - local recursive = not dir.files_limit - local new_files = get_directory_files(dir.name, filename_rooted, {}, recursive, coroutine.yield) + 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) @@ -293,9 +301,9 @@ end function core.scan_project_subdir(dir, filename) local index, n, file = project_subdir_bounds(dir, filename) if index then - local new_files = get_directory_files(dir.name, PATHSEP .. filename, {}) + local new_files = get_directory_files(dir, dir.name, PATHSEP .. filename, {}) files_list_replace(dir.files, index, n, new_files) - file.scanned = true + dir.is_dirty = true return true end end @@ -1048,6 +1056,7 @@ function core.has_pending_rescan() end end + function core.dir_rescan_add_job(dir, filepath) local dirpath = filepath:match("^(.+)[/\\].+$") local dirpath_rooted = dirpath and PATHSEP .. dirpath or "" @@ -1055,7 +1064,8 @@ function core.dir_rescan_add_job(dir, filepath) 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"}) - if not dir_match or (dir.files_limit and not dir.files[dir_index].scanned) then return end + local dir_filename = dir.files[dir_index].filename + if not dir_match or not core.project_subdir_is_shown(dir, dir_filename) then return end end local new_time = system.get_time() + 1 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index c0d812aa..987ed039 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -217,10 +217,13 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) if keymap.modkeys["ctrl"] and button == "left" then create_directory_in(hovered_item) else - if hovered_item.dir.files_limit and not hovered_item.expanded then - core.scan_project_subdir(hovered_item.dir, hovered_item.filename) - end hovered_item.expanded = not hovered_item.expanded + if hovered_item.dir.files_limit then + if hovered_item.expanded then + core.scan_project_subdir(hovered_item.dir, hovered_item.filename) + end + core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded) + end end else core.try(function() From e8b9d2c44ad17cfbf899abc606b65a677d371b4d Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Tue, 21 Sep 2021 16:41:27 +0200 Subject: [PATCH 173/180] Add missing pthread dependency --- meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 047ce90e..209677ba 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 From 4e58e8be28a80707b4ad255c16f01f4c405816e1 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Mon, 20 Sep 2021 17:18:03 +0200 Subject: [PATCH 174/180] Use new dmon version win watch_add/rm Include changes in dmon not yet merged into master. --- data/core/init.lua | 21 ++- data/plugins/treeview.lua | 4 +- resources/notes-dmon-integration-2.md | 10 ++ src/api/system.c | 26 ++- src/dmon.h | 237 +++++++++++++++++++++----- 5 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 resources/notes-dmon-integration-2.md diff --git a/data/core/init.lua b/data/core/init.lua index 2d98ff15..50cbd5a4 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -146,6 +146,14 @@ end function core.project_subdir_set_show(dir, filename, show) dir.shown_subdir[filename] = show + if dir.files_limit and PLATFORM == "Linux" then + local fullpath = dir.name .. PATHSEP .. filename + local watch_fn = show and system.watch_dir_add or system.watch_dir_rm + local success = watch_fn(dir.watch_id, fullpath) + if not success then + core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") + end + end end @@ -168,9 +176,15 @@ local function scan_project_folder(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, ".") @@ -182,13 +196,11 @@ function core.add_project_directory(path) -- 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 watch_id = system.watch_dir(path) local dir = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, files_limit = false, is_dirty = true, - watch_id = watch_id, shown_subdir = {}, } table.insert(core.project_directories, dir) @@ -298,16 +310,17 @@ local function rescan_project_subdir(dir, filename_rooted) end -function core.scan_project_subdir(dir, filename) +function core.update_project_subdir(dir, filename, expanded) local index, n, file = project_subdir_bounds(dir, filename) if index then - local new_files = get_directory_files(dir, dir.name, PATHSEP .. filename, {}) + 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. diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 987ed039..4d567384 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -219,9 +219,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) else hovered_item.expanded = not hovered_item.expanded if hovered_item.dir.files_limit then - if hovered_item.expanded then - core.scan_project_subdir(hovered_item.dir, hovered_item.filename) - end + 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 diff --git a/resources/notes-dmon-integration-2.md b/resources/notes-dmon-integration-2.md new file mode 100644 index 00000000..f0e0803c --- /dev/null +++ b/resources/notes-dmon-integration-2.md @@ -0,0 +1,10 @@ +## from core/init.lua + +- `scan_project_folder` set the `watch_id` recursively or not + * called from `core.add_project_directory` + +## from treeview.lua + +`TreeView:on_mouse_pressed` + * calls `core.scan_project_subdir` only in `files_limit` mode + * calls `core.project_subdir_set_show` diff --git a/src/api/system.c b/src/api/system.c index 0eb09108..fc8a4589 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -648,12 +648,32 @@ static int f_set_window_opacity(lua_State *L) { static int f_watch_dir(lua_State *L) { const char *path = luaL_checkstring(L, 1); - dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, DMON_WATCHFLAGS_RECURSIVE, NULL); + 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 @@ -740,6 +760,10 @@ static const luaL_Reg lib[] = { { "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/dmon.h b/src/dmon.h index 92fba034..db39a6c1 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -5,7 +5,6 @@ // Portable directory monitoring library // watches directories for file or directory changes. // -// clang-format off // Usage: // define DMON_IMPL and include this file to use it: // #define DMON_IMPL @@ -115,6 +114,8 @@ 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 } @@ -394,8 +395,6 @@ typedef struct dmon__state { static bool _dmon_init; static dmon__state _dmon; -// clang-format on - _DMON_PRIVATE bool dmon__refresh_watch(dmon__watch_state* watch) { return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), @@ -670,7 +669,6 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) _InterlockedExchange(&_dmon.modify_watches, 0); } -// clang-format off #elif DMON_OS_LINUX // inotify linux backend #define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) @@ -702,7 +700,6 @@ typedef struct dmon__state { dmon__watch_state watches[DMON_MAX_WATCHES]; dmon__inotify_event* events; int num_watches; - volatile int modify_watches; pthread_t thread_handle; pthread_mutex_t mutex; bool quit; @@ -710,7 +707,6 @@ typedef struct dmon__state { static bool _dmon_init; static dmon__state _dmon; -// clang-format on _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask, bool followlinks, dmon__watch_state* watch) @@ -742,7 +738,7 @@ _DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t m // add sub-directory to watch dirs if (entry_valid) { - int watchdir_len = strlen(watchdir); + int watchdir_len = (int)strlen(watchdir); if (watchdir[watchdir_len - 1] != '/') { watchdir[watchdir_len] = '/'; watchdir[watchdir_len + 1] = '\0'; @@ -767,6 +763,126 @@ _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; @@ -776,10 +892,42 @@ _DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int } } - DMON_ASSERT(0); 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++) { @@ -799,8 +947,8 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) // 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 = strlen(ev->filepath); - int l2 = strlen(check_ev->filepath); + 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) { @@ -864,11 +1012,20 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) 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, c = stb_sb_count(_dmon.events); i < c; i++) { + for (int i = 0; i < stb_sb_count(_dmon.events); i++) { dmon__inotify_event* ev = &_dmon.events[i]; if (ev->skip) { continue; @@ -899,6 +1056,11 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) 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); @@ -907,7 +1069,7 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) 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 < c; j++) { + 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, @@ -921,7 +1083,6 @@ _DMON_PRIVATE void dmon__inotify_process_events(void) } } - stb_sb_reset(_dmon.events); } @@ -939,15 +1100,8 @@ static void* dmon__thread(void* arg) gettimeofday(&starttm, 0); 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); + nanosleep(&req, &rem); + if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { continue; } @@ -974,18 +1128,21 @@ static void* dmon__thread(void* arg) while (offset < len) { struct inotify_event* iev = (struct inotify_event*)&buff[offset]; - char filepath[DMON_MAX_PATH]; - dmon__strcpy(filepath, sizeof(filepath), dmon__find_subdir(watch, iev->wd)); - dmon__strcat(filepath, sizeof(filepath), iev->name); + 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 + // TODO: ignore directories if flag is set - if (stb_sb_count(_dmon.events) == 0) { - usecs_elapsed = 0; + 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); } - 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; } @@ -1051,7 +1208,6 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, 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); @@ -1068,7 +1224,6 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, (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); } @@ -1085,7 +1240,6 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, _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 { @@ -1093,7 +1247,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, } // add trailing slash - int rootdir_len = strlen(watch->rootdir); + int rootdir_len = (int)strlen(watch->rootdir); if (watch->rootdir[rootdir_len - 1] != '/') { watch->rootdir[rootdir_len] = '/'; watch->rootdir[rootdir_len + 1] = '\0'; @@ -1103,16 +1257,14 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, if (watch->fd < -1) { DMON_LOG_ERROR("could not create inotify instance"); pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); 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("watch failed: %s", watch->rootdir); + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); return dmon__make_id(0); } dmon__watch_subdir subdir; @@ -1128,7 +1280,6 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); return dmon__make_id(id); } @@ -1136,7 +1287,6 @@ 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; @@ -1149,9 +1299,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) --_dmon.num_watches; pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); } -// clang-format off #elif DMON_OS_MACOS // FSEvents MacOS backend typedef struct dmon__fsevent_event { @@ -1193,7 +1341,6 @@ union dmon__cast_userdata { static bool _dmon_init; static dmon__state _dmon; -// clang-format on _DMON_PRIVATE void* dmon__cf_malloc(CFIndex size, CFOptionFlags hints, void* info) { @@ -1491,7 +1638,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, } // add trailing slash - int rootdir_len = strlen(watch->rootdir); + int rootdir_len = (int)strlen(watch->rootdir); if (watch->rootdir[rootdir_len - 1] != '/') { watch->rootdir[rootdir_len] = '/'; watch->rootdir[rootdir_len + 1] = '\0'; @@ -1542,9 +1689,7 @@ DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) __sync_lock_test_and_set(&_dmon.modify_watches, 0); } -// clang-format off #endif #endif // DMON_IMPL #endif // __DMON_H__ -// clang-format on From 201dd295f979ea05ad0148bc33e35abecfa5242b Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Sep 2021 11:38:55 +0200 Subject: [PATCH 175/180] Use PLATFORM to detect macOS for key bindings --- 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 50eadec6..cb2aa876 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:match("^[Mm]ac") -- Thanks to mathewmariani, taken from his lite-macos github repository. local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic")) From 5421a1d69c6d785932006c8c0899bee2d98fab34 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Sep 2021 13:41:27 +0200 Subject: [PATCH 176/180] remove dev note --- resources/notes-dmon-integration-2.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 resources/notes-dmon-integration-2.md diff --git a/resources/notes-dmon-integration-2.md b/resources/notes-dmon-integration-2.md deleted file mode 100644 index f0e0803c..00000000 --- a/resources/notes-dmon-integration-2.md +++ /dev/null @@ -1,10 +0,0 @@ -## from core/init.lua - -- `scan_project_folder` set the `watch_id` recursively or not - * called from `core.add_project_directory` - -## from treeview.lua - -`TreeView:on_mouse_pressed` - * calls `core.scan_project_subdir` only in `files_limit` mode - * calls `core.project_subdir_set_show` From 01f6767d98f8d92b678782a26e02b6d0ee5f7a24 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Sep 2021 22:16:20 +0200 Subject: [PATCH 177/180] Fix call to missing project_files_limit --- data/core/commands/core.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index b6604591..e836ea2f 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,9 +66,8 @@ command.add(nil, { end, ["core:find-file"] = function() - -- FIXME: core.project_files_limit was removed! - 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 From 991b014a5f171fb28d5e750d23006cbeb2136c3f Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Wed, 29 Sep 2021 22:16:44 +0200 Subject: [PATCH 178/180] Remove calls to reschedule_project_scan --- data/plugins/treeview.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 4d567384..aac129f8 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -442,7 +442,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, @@ -457,7 +456,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, @@ -470,7 +468,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, @@ -507,7 +504,6 @@ command.add(function() return view.hovered_item ~= nil end, { return end end - core.reschedule_project_scan() core.log("Deleted \"%s\"", filename) end end From f25272298953fc91a29eb23d970087874e61b6fd Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Sep 2021 14:05:48 -0700 Subject: [PATCH 179/180] Fix error in dir_rescan_add_job --- data/core/init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/core/init.lua b/data/core/init.lua index 50cbd5a4..9c6f9b4e 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1077,8 +1077,9 @@ function core.dir_rescan_add_job(dir, filepath) 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"}) - local dir_filename = dir.files[dir_index].filename - if not dir_match or not core.project_subdir_is_shown(dir, dir_filename) then return end + -- 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 From 66196d612c94abd2997686e70d28eaecb5b4baff Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 30 Sep 2021 14:11:37 -0700 Subject: [PATCH 180/180] Fix in dmon.h for macOS path case sensitivity --- src/dmon.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dmon.h b/src/dmon.h index db39a6c1..fdbac3f4 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -1578,8 +1578,11 @@ _DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath)); // strip the root dir - DMON_ASSERT(strstr(abs_filepath, watch->rootdir) == abs_filepath); - dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + size_t len = strlen(watch->rootdir); + // FIXME: filesystems on macOS can be case sensitive or not. The check below + // ignore case but we should check if the filesystem is case sensitive. + DMON_ASSERT(strncasecmp(abs_filepath, watch->rootdir, len) == 0); + dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + len); ev.event_flags = flags; ev.event_id = event_id;