diff --git a/README.md b/README.md index 06636d46..b96c9dca 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,76 @@ # Lite XL -[![Discord Badge Image]](https://discord.gg/SR4ArdYr) +[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) -![screenshot-dark](https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png) +![screenshot-dark] -A lightweight text editor written in Lua, adapted from [lite](https://github.com/rxi/lite) +A lightweight text editor written in Lua, adapted from [lite]. -* **[Get Lite XL](https://github.com/franko/lite-xl/releases/latest)** — Download - for Windows, Linux and Mac OS (notarized app). -* **[Get started](doc/usage.md)** — A quick overview on how to get started -* **[Get plugins](https://github.com/franko/lite-plugins)** — Add additional - functionality, adapted for Lite XL -* **[Get color themes](https://github.com/rxi/lite-colors)** — Add additional colors - themes +* **[Get Lite XL]** — Download for Windows, Linux and Mac OS (notarized app). +* **[Get plugins]** — Add additional functionality, adapted for Lite XL. +* **[Get color themes]** — Add additional colors themes. -Lite XL has support for high DPI display on Windows and Linux and, since 1.16.7 release, it supports **retina displays** on Mac OS. +Please refer to our [website] for the user and developer documentation, +including [build] instructions. + +Lite XL has support for high DPI display on Windows and Linux and, +since 1.16.7 release, it supports **retina displays** on macOS. Please note that Lite XL is compatible with lite for most plugins and all color themes. -We provide a separate lite-plugins repository for Lite XL, because in some cases some adaptations may be needed to make them work better with Lite XL. -The repository with modified plugins is http://github.com/franko/lite-plugins. +We provide a separate lite-plugins repository for Lite XL, because in some cases +some adaptations may be needed to make them work better with Lite XL. +The repository with modified plugins is https://github.com/franko/lite-plugins. -The changes and differences between Lite XL and rxi/lite are listed in the [changelog](https://github.com/franko/lite-xl/blob/master/changelog.md). +The changes and differences between Lite XL and rxi/lite are listed in the +[changelog]. ## Overview -Lite XL is derived from lite. It is a lightweight text editor written mostly in Lua — it aims to provide -something practical, pretty, *small* and fast easy to modify and extend, or to use without doing either. +Lite XL is derived from lite. +It is a lightweight text editor written mostly in Lua — it aims to provide +something practical, pretty, *small* and fast easy to modify and extend, +or to use without doing either. -The aim of Lite XL compared to lite is to be more user friendly, improve the quality of font rendering, and reduce CPU usage. +The aim of Lite XL compared to lite is to be more user friendly, +improve the quality of font rendering, and reduce CPU usage. ## Customization -Additional functionality can be added through plugins which are available in -the [plugins repository](https://github.com/rxi/lite-plugins) or in the [Lite XL-specific plugins repository](https://github.com/franko/lite-plugins). -Additional color themes can be found in the [colors repository](https://github.com/rxi/lite-colors). +Additional functionality can be added through plugins which are available in +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. -The editor can be customized by making changes to the [user module](data/user/init.lua). - -## Building - -You can build Lite XL yourself using Meson. - -In addition, the `build-packages.sh` script can be used to compile Lite XL and create an OS-specific package for Linux, Windows or Mac OS. - -The following libraries are required: - -- freetype2 -- SDL2 - -The following libraries are **optional**: - -- libagg -- Lua 5.2 - -If they are not found, they will be downloaded and compiled by Meson. -Otherwise, if they are present, they will be used to compile Lite XL. - -On Debian-based systems the required libraries and Meson can be installed using the following commands: - -```sh -# To install the required libraries: -sudo apt install libfreetype6-dev libsdl2-dev - -# To install Meson: -sudo apt install meson -# or pip3 install --user meson -``` - -To build Lite XL with Meson the commands below can be used: -```sh -meson setup --buildtype=release build -meson compile -C build -meson install -C build -``` - -If you are using a version of Meson below 0.54 you need to use diffent commands to compile and install: - -```sh -meson setup --buildtype=release build -ninja -C build -ninja -C build install -``` - -When performing the `meson setup` command you may enable the `-Dportable=true` option to specify whether files should be installed as in a portable application. - -If `portable` is enabled, Lite XL is built to use a `data` directory placed next to the executable. -Otherwise, Lite XL will use unix-like directory locations. -In this case, the `data` directory will be `$prefix/share/lite-xl` and the executable will be located in `$prefix/bin`. -`$prefix` is determined when the application starts as a directory such that `$prefix/bin` corresponds to the location of the executable. - -The `user` directory does not depend on the `portable` option and will always be `$HOME/.config/lite-xl`. -`$HOME` is determined from the corresponding environment variable. -As a special case on Windows the variable `$USERPROFILE` will be used instead. - -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. - -On Windows two packages will be created, one called "portable" using the "data" folder next to the executable and -the other one using a unix-like file layout. Both packages works correctly. The one with unix-like file layout -is meant for people using a unix-like shell and the command line. - -Please note that there aren't any hard-coded directories in the executable, so that the -package can be extracted and used in any directory. - -Mac OS X is fully supported and a notarized app disk image is provided in the [release page](https://github.com/franko/lite-xl/releases). -In addition the application can be compiled using the generic instructions given above. - ## Contributing -Any additional functionality that can be added through a plugin should be done -as a plugin, after which a pull request to the -[plugins repository](https://github.com/rxi/lite-plugins) can be made. -If the plugin uses any Lite XL-specific functionality, please open a pull request to the -[Lite XL plugins repository](https://github.com/franko/lite-plugins). +Any additional functionality that can be added through a plugin should be done +as a plugin, after which a pull request to the [plugins repository] can be made. + +If the plugin uses any Lite XL-specific functionality, +please open a pull request to the [Lite XL plugins repository]. Pull requests to improve or modify the editor itself are welcome. -## License -This project is free software; you can redistribute it and/or modify it under -the terms of the MIT license. See [LICENSE](LICENSE) for details. +## Licenses -[Discord Badge Image]: https://img.shields.io/discord/847122429742809208?label=discord&logo=discord +This project is free software; you can redistribute it and/or modify it under +the terms of the MIT license. See [LICENSE] for details. + +See the [licenses] file for details on licenses used by the required dependencies. + + +[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 +[website]: https://lite-xl.github.io +[build]: https://lite-xl.github.io/en/build +[Get Lite XL]: https://github.com/franko/lite-xl/releases/latest +[Get plugins]: https://github.com/franko/lite-plugins +[Get color themes]: https://github.com/rxi/lite-colors +[changelog]: https://github.com/franko/lite-xl/blob/master/changelog.md +[Lite XL plugins repository]: https://github.com/franko/lite-plugins +[plugins repository]: https://github.com/rxi/lite-plugins +[colors repository]: https://github.com/rxi/lite-colors +[LICENSE]: LICENSE +[licenses]: licenses/licenses.md diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index a9b4554e..d7e6be2f 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -211,11 +211,11 @@ local commands = { end, ["doc:indent"] = function() - indent_text() + doc():indent_text(false, doc_multiline_selection(true)) end, ["doc:unindent"] = function() - indent_text(true) + doc():indent_text(true, doc_multiline_selection(true)) end, ["doc:duplicate-lines"] = function() diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index ca41cdad..5c34b7b1 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -411,6 +411,65 @@ function Doc:select_to(...) self:set_selection(line, col, line2, col2) end + +local function get_indent_string() + if config.tab_type == "hard" then + return "\t" + end + return string.rep(" ", config.indent_size) +end + +-- returns the size of the original indent, and the indent +-- in your config format, rounded either up or down +local function get_line_indent(line, rnd_up) + 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). +function Doc:indent_text(unindent, line1, col1, line2, col2, swap) + local text = get_indent_string() + local _, se = self.lines[line1]:find("^[ \t]+") + local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) + local has_selection = line1 ~= line2 or col1 ~= col2 + if unindent or has_selection or in_beginning_whitespace then + local l1d, l2d = #self.lines[line1], #self.lines[line2] + for line = line1, line2 do + local e, rnded = get_line_indent(self.lines[line], unindent) + self:remove(line, 1, line, (e or 0) + 1) + self:insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end + l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d + if (unindent or in_beginning_whitespace) and not self:has_selection() then + local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) + self:set_selection(line1, start_cursor, line2, start_cursor, swap) + else + self:set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) + end + else + self:text_input(text) + end +end + -- For plugins to add custom actions of document change function Doc:on_text_change(type) end diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index 7fc85c41..04090673 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -33,7 +33,7 @@ function search.find(doc, line, col, text, opt) for line = line, #doc.lines do local line_text = doc.lines[line] if opt.regex then - local s, e = re:cmatch(line_text, col, true) + local s, e = re:cmatch(line_text, col) if s then return line, s, line, e end diff --git a/data/core/init.lua b/data/core/init.lua index 6a2767d9..f8000498 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1106,6 +1106,11 @@ function core.blink_reset() end +function core.request_cursor(value) + core.cursor_change_req = value +end + + function core.on_error(err) -- write error to file local fp = io.open(USERDIR .. "/error.txt", "wb") diff --git a/data/core/regex.lua b/data/core/regex.lua index a6ee0d5a..19306e04 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -1,15 +1,15 @@ --- 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 -regex.match = function(pattern_string, string, offset) +regex.match = function(pattern_string, string, offset, options) local pattern = type(pattern_string) == "table" and pattern_string or regex.compile(pattern_string) - return regex.cmatch(pattern, string, offset) + 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,38 +32,39 @@ 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) local pattern = type(pattern_string) == "table" and pattern_string or regex.compile(pattern_string) local result, indices = "" - local n = 0 + local matches, replacements = {}, {} repeat indices = { regex.cmatch(pattern, str) } if #indices > 0 then - n = n + 1 + table.insert(matches, indices) local currentReplacement = 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 end 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 until #indices == 0 or indices[1] == indices[2] - return result .. str, n + return result .. str, matches, replacements end diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8b3a91ae..b01303c5 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -783,7 +783,7 @@ end function RootView:on_mouse_moved(x, y, dx, dy) if core.active_view == core.nag_view then - system.set_cursor("arrow") + core.request_cursor("arrow") core.active_view:on_mouse_moved(x, y, dx, dy) return end @@ -808,14 +808,14 @@ function RootView:on_mouse_moved(x, y, dx, dy) local div = self.root_node:get_divider_overlapping_point(x, y) local tab_index = node and node:get_tab_overlapping_point(x, y) if node and node:get_scroll_button_index(x, y) then - system.set_cursor("arrow") + 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 - system.set_cursor(div.type == "hsplit" and "sizeh" or "sizev") + core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") end elseif tab_index then - system.set_cursor("arrow") + core.request_cursor("arrow") if self.dragged_node and self.dragged_node ~= tab_index then local tab = node.views[self.dragged_node] table.remove(node.views, self.dragged_node) @@ -823,7 +823,7 @@ function RootView:on_mouse_moved(x, y, dx, dy) self.dragged_node = tab_index end else - system.set_cursor(node.active_view.cursor) + core.request_cursor(node.active_view.cursor) end end @@ -858,6 +858,10 @@ function RootView:draw() local t = table.remove(self.deferred_draws) t.fn(table.unpack(t)) end + if core.cursor_change_req then + system.set_cursor(core.cursor_change_req) + core.cursor_change_req = nil + end end diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 83e0e665..a20dba5e 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -48,29 +48,6 @@ local function push_tokens(t, syn, pattern, full_text, find_results) end -local function is_escaped(text, idx, esc) - local byte = esc:byte() - local count = 0 - for i = idx - 1, 1, -1 do - if text:byte(i) ~= byte then break end - count = count + 1 - end - return count % 2 == 1 -end - - -local function find_non_escaped(text, pattern, offset, esc) - while true do - local s, e = text:find(pattern, offset) - if not s then break end - if esc and is_escaped(text, s, esc) then - offset = e + 1 - else - return s, e - end - end -end - -- State is a 32-bit number that is four separate bytes, illustrating how many -- differnet delimiters we have open, and which subsyntaxes we have active. -- At most, there are 3 subsyntaxes active at the same time. Beyond that, @@ -155,26 +132,44 @@ function tokenizer.tokenize(incoming_syntax, text, state) set_subsyntax_pattern_idx(0) current_syntax, subsyntax_info, current_pattern_idx, current_level = retrieve_syntax_state(incoming_syntax, state) + end + local function find_text(text, p, offset, at_start, close) + local target, res = p.pattern or p.regex, { 1, offset - 1 }, p.regex + local code = type(target) == "table" and target[close and 2 or 1] or target + if p.regex and type(p.regex) ~= "table" then + p._regex = p._regex or regex.compile(p.regex) + code = p._regex + end + repeat + res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) } + or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) } + if res[1] and close and target[3] then + local count = 0 + for i = res[1] - 1, 1, -1 do + if text:byte(i) ~= target[3]:byte() then break end + count = count + 1 + end + -- Check to see if the escaped character is there, + -- and if it is not itself escaped. + if count % 2 == 0 then break end + end + until not res[1] or not close or not target[3] + return unpack(res) end while i <= #text do -- continue trying to match the end pattern of a pair if we have a state set if current_pattern_idx > 0 then local p = current_syntax.patterns[current_pattern_idx] - local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3]) + local s, e = find_text(text, p, i, false, true) local cont = true -- If we're in subsyntax mode, always check to see if we end our syntax -- first, before the found delimeter, as ending the subsyntax takes -- precedence over ending the delimiter in the subsyntax. if subsyntax_info then - local ss, se = find_non_escaped( - text, - subsyntax_info.pattern[2], - i, - subsyntax_info.pattern[3] - ) + local ss, se = find_text(text, subsyntax_info, i, false, true) -- If we find that we end the subsyntax before the -- delimiter, push the token, and signal we shouldn't -- treat the bit after as a token to be normally parsed @@ -202,12 +197,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- we're ending early in the middle of a delimiter, or -- just normally, upon finding a token. if subsyntax_info then - local s, e = find_non_escaped( - text, - "^" .. subsyntax_info.pattern[2], - i, - nil - ) + local s, e = find_text(text, subsyntax_info, i, true, true) if s then push_token(res, subsyntax_info.type, text:sub(i, e)) -- On finding unescaped delimiter, pop it. @@ -219,16 +209,12 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- find matching pattern local matched = false for n, p in ipairs(current_syntax.patterns) do - local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern - local find_results = { text:find("^" .. pattern, i) } - local start, fin = find_results[1], find_results[2] - - if start then + local find_results = { find_text(text, p, i, true, false) } + if find_results[1] then -- matched pattern; make and add tokens push_tokens(res, current_syntax, p, text, find_results) - -- update state if this was a start|end pattern pair - if type(p.pattern) == "table" then + if type(p.pattern or p.regex) == "table" then -- If we have a subsyntax, push that onto the subsyntax stack. if p.syntax then push_subsyntax(p, n) @@ -236,9 +222,8 @@ function tokenizer.tokenize(incoming_syntax, text, state) set_subsyntax_pattern_idx(n) end end - -- move cursor past this token - i = fin + 1 + i = find_results[2] + 1 matched = true break end diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua new file mode 100644 index 00000000..6a14d049 --- /dev/null +++ b/data/plugins/contextmenu.lua @@ -0,0 +1,290 @@ +-- 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 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 + for _, items in ipairs(self.itemset) do + if items.predicate(x, y) then + self.items = items.items + break + end + end + + if self.items then + 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 root_view_on_mouse_pressed = RootView.on_mouse_pressed +local root_view_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 + root_view_on_mouse_moved(self, ...) +end + +-- this function is mostly copied from lite-xl's source +function RootView:on_mouse_pressed(button, x,y, clicks) + local div = self.root_node:get_divider_overlapping_point(x, y) + if div then + self.dragged_divider = div + return + end + local node = self.root_node:get_child_overlapping_point(x, y) + if node.hovered_scroll_button > 0 then + node:scroll_tabs(node.hovered_scroll_button) + return + end + local idx = node:get_tab_overlapping_point(x, y) + if idx then + if button == "middle" or node.hovered_close == idx then + node:close_view(self.root_node, node.views[idx]) + else + self.dragged_node = idx + node:set_active_view(node.views[idx]) + end + else + core.set_active_view(node.active_view) + -- send to context menu first + if not menu:on_mouse_pressed(button, x, y, clicks) then + node.active_view:on_mouse_pressed(button, x, y, clicks) + end + end +end + +function RootView:update(...) + root_view_update(self, ...) + menu:update() +end + +function RootView:draw(...) + root_view_draw(self, ...) + menu:draw() +end + +command.add(nil, { + ["context:show"] = function() + menu:show(core.active_view.position.x, core.active_view.position.y) + end +}) + +keymap.add { + ["menu"] = "context:show" +} + +if require("plugins.scale") then + menu:register("core.docview", { + { text = "Font +", command = "scale:increase" }, + { text = "Font -", command = "scale:decrease" }, + { text = "Font Reset", command = "scale:reset" }, + DIVIDER, + { text = "Find", command = "find-replace:find" }, + { text = "Replace", command = "find-replace:replace" }, + DIVIDER, + { text = "Find Pattern", command = "find-replace:find-pattern" }, + { text = "Replace Pattern", command = "find-replace:replace-pattern" }, + }) +end + +return menu diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua new file mode 100644 index 00000000..11064235 --- /dev/null +++ b/data/plugins/scale.lua @@ -0,0 +1,100 @@ +-- mod-version:1 -- lite-xl 1.16 +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 RootView = require "core.rootview" +local CommandView = require "core.commandview" + +config.scale_mode = "code" +config.scale_use_mousewheel = true + +local scale_level = 0 +local scale_steps = 0.05 + +local current_scale = SCALE +local default_scale = SCALE + +local function set_scale(scale) + scale = common.clamp(scale, 0.2, 6) + + -- save scroll positions + local scrolls = {} + for _, view in ipairs(core.root_view.root_node:get_children()) do + local n = view:get_scrollable_size() + if n ~= math.huge and not view:is(CommandView) and n > view.size.y then + scrolls[view] = view.scroll.y / (n - view.size.y) + end + end + + local s = scale / current_scale + current_scale = scale + + if config.scale_mode == "ui" then + SCALE = scale + + style.padding.x = style.padding.x * s + style.padding.y = style.padding.y * s + style.divider_size = style.divider_size * s + style.scrollbar_size = style.scrollbar_size * s + style.caret_width = style.caret_width * s + style.tab_width = style.tab_width * s + + for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do + renderer.font.set_size(style[name], s * style[name]:get_size()) + end + else + renderer.font.set_size(style.code_font, s * style.code_font:get_size()) + end + + -- restore scroll positions + for view, n in pairs(scrolls) do + view.scroll.y = n * (view:get_scrollable_size() - view.size.y) + view.scroll.to.y = view.scroll.y + end + + core.redraw = true +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 d < 0 then command.perform "scale:decrease" end + if d > 0 then command.perform "scale:increase" end + else + return on_mouse_wheel(self, d, ...) + end +end + +local function res_scale() + 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) +end + +local function dec_scale() + scale_level = scale_level - 1 + set_scale(default_scale + scale_level * scale_steps) +end + + +command.add(nil, { + ["scale:reset" ] = function() res_scale() end, + ["scale:decrease"] = function() dec_scale() end, + ["scale:increase"] = function() inc_scale() end, +}) + +keymap.add { + ["ctrl+0"] = "scale:reset", + ["ctrl+-"] = "scale:decrease", + ["ctrl+="] = "scale:increase", +} + diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 25c7f672..77efd7c9 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -2,6 +2,7 @@ local core = require "core" local common = require "core.common" local DocView = require "core.docview" +local LogView = require "core.logview" local function workspace_files_for(project_dir) @@ -29,7 +30,7 @@ local function workspace_files_for(project_dir) end -local function load_workspace_file(project_dir) +local function consume_workspace_file(project_dir) for filename, id in workspace_files_for(project_dir) do local load_f = loadfile(filename) local workspace = load_f and load_f() @@ -85,6 +86,7 @@ local function save_view(view) text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge) } end + if mt == LogView then return end for name, mod in pairs(package.loaded) do if mod == mt then return { @@ -99,16 +101,26 @@ end local function load_view(t) if t.type == "doc" then - local ok, doc = pcall(core.open_doc, t.filename) - if not ok then - return DocView(core.open_doc()) + local dv + if not t.filename then + -- document not associated to a file + dv = DocView(core.open_doc()) + if t.text then dv.doc:insert(1, 1, t.text) end + else + -- we have a filename, try to read the file + local ok, doc = pcall(core.open_doc, t.filename) + if ok then + dv = DocView(doc) + end + end + -- doc view "dv" can be nil here if the filename associated to the document + -- cannot be read. + if dv and dv.doc then + dv.doc:set_selection(table.unpack(t.selection)) + dv.last_line, dv.last_col = dv.doc:get_selection() + dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x + dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y end - local dv = DocView(doc) - if t.text then doc:insert(1, 1, t.text) end - doc:set_selection(table.unpack(t.selection)) - dv.last_line, dv.last_col = doc:get_selection() - dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x - dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y return dv end return require(t.module)() @@ -141,13 +153,19 @@ end local function load_node(node, t) if t.type == "leaf" then local res - for _, v in ipairs(t.views) do + local active_view + for i, v in ipairs(t.views) do local view = load_view(v) - if v.active then res = view end - node:add_view(view) + if view then + if v.active then res = view end + node:add_view(view) + if t.active_view == i then + active_view = view + end + end end - if t.active_view then - node:set_active_view(node.views[t.active_view]) + if active_view then + node:set_active_view(active_view) end return res else @@ -184,7 +202,7 @@ end local function load_workspace() - local workspace = load_workspace_file(core.project_dir) + local workspace = consume_workspace_file(core.project_dir) if workspace then local root = get_unlocked_root(core.root_view.root_node) local active_view = load_node(root, workspace.documents) diff --git a/doc/contributors.md b/doc/contributors.md deleted file mode 100644 index 4313e465..00000000 --- a/doc/contributors.md +++ /dev/null @@ -1,43 +0,0 @@ -## rxi - -Original development of lite editor. - -## Francesco Abbate (franko) - -Creator of lite-xl fork from rxi/lite. - -## Takase (takase1121) - -NagView and X Window database resource query for Xft.dpi setting. - -## Nils Kvist (budRich) - -Popup window replacement with CommandView dialog. - -## liquidev - -Tab style and animations improvements. - -## adamharrison - -Multi-language syntax highlighting and many other improvements. - -## vincens2005 - -Syntax highlighting improvements. - -## Janis-Leuenberger - -Add keymap bindings help file and macOS testing. - -## Mat Mariani (mathewmariani) - -Help for Mac OS port. Some resources taken from mathewmariani/lite-macos. - -## daubaris - -Initial implementation of Xft.dpi query using xrdb command. - -## Robert Štojs (netrobert) - -Continuos integration configuration diff --git a/doc/default-keymap.md b/doc/default-keymap.md deleted file mode 100644 index 6ecdfb85..00000000 --- a/doc/default-keymap.md +++ /dev/null @@ -1,114 +0,0 @@ -# Default keymap - -*Note: When using macOS `ctrl` refers to the command key.* - -| Key combination | Actions | -| --------------------- | ----------------------------------- | -| ctrl+shift+p | core:find-command | -| ctrl+p | core:find-file | -| ctrl+o | core:open-file | -| ctrl+n | core:new-doc | -| 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 | -| alt+shift+i | root:split-up | -| alt+shift+k | root:split-down | -| alt+j | root:switch-to-left | -| alt+l | root:switch-to-right | -| alt+i | root:switch-to-up | -| alt+k | root:switch-to-down | -| ctrl+w | root:close | -| ctrl+tab | root:switch-to-next-tab | -| ctrl+shift+tab | root:switch-to-previous-tab | -| ctrl+pageup | root:move-tab-left | -| ctrl+pagedown | root:move-tab-right | -| alt+1 | root:switch-to-tab-1 | -| alt+2 | root:switch-to-tab-2 | -| alt+3 | root:switch-to-tab-3 | -| alt+4 | root:switch-to-tab-4 | -| alt+5 | root:switch-to-tab-5 | -| alt+6 | root:switch-to-tab-6 | -| alt+7 | root:switch-to-tab-7 | -| alt+8 | root:switch-to-tab-8 | -| alt+9 | root:switch-to-tab-9 | -| ctrl+f | find-replace:find | -| ctrl+r | find-replace:replace | -| f3 | find-replace:repeat-find | -| shift+f3 | find-replace:previous-find | -| ctrl+g | doc:go-to-line | -| ctrl+s | doc:save | -| ctrl+shift+s | doc:save-as | -| ctrl+z | doc:undo | -| ctrl+y | doc:redo | -| ctrl+x | doc:cut | -| ctrl+c | doc:copy | -| ctrl+v | doc:paste | -| ctrl+insert | doc:copy | -| shift+insert | doc:paste | -| escape | command:escape | -| escape | doc:select-none | -| escape | dialog:select-no | -| tab | command:complete | -| tab | doc:indent | -| shift+tab | doc:unindent | -| backspace | doc:backspace | -| shift+backspace | doc:backspace | -| ctrl+backspace | doc:delete-to-previous-word-start | -| ctrl+shift+backspace | doc:delete-to-previous-word-start | -| delete | doc:delete | -| shift+delete | doc:delete | -| ctrl+delete | doc:delete-to-next-word-end | -| ctrl+shift+delete | doc:delete-to-next-word-end | -| return | command:submit | -| return | doc:newline | -| return | dialog:select | -| keypad enter | command:submit | -| keypad enter | doc:newline | -| keypad enter | dialog:select | -| ctrl+return | doc:newline-below | -| ctrl+shift+return | doc:newline-above | -| ctrl+j | doc:join-lines | -| ctrl+a | doc:select-all | -| ctrl+d | find-replace:select-next | -| ctrl+d | doc:select-word | -| ctrl+l | doc:select-lines | -| ctrl+/ | doc:toggle-line-comments | -| ctrl+up | doc:move-lines-up | -| ctrl+down | doc:move-lines-down | -| ctrl+shift+d | doc:duplicate-lines | -| ctrl+shift+k | doc:delete-lines | -| left | doc:move-to-previous-char | -| left | dialog:previous-entry | -| right | doc:move-to-next-char | -| right | dialog:next-entry | -| up | command:select-previous | -| up | doc:move-to-previous-line | -| down | command:select-next | -| down | doc:move-to-next-line | -| ctrl+left | doc:move-to-previous-word-start | -| 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 | -| end | doc:move-to-end-of-line | -| ctrl+home | doc:move-to-start-of-doc | -| ctrl+end | doc:move-to-end-of-doc | -| pageup | doc:move-to-previous-page | -| pagedown | doc:move-to-next-page | -| shift+left | doc:select-to-previous-char | -| shift+right | doc:select-to-next-char | -| shift+up | doc:select-to-previous-line | -| shift+down | doc:select-to-next-line | -| ctrl+shift+left | doc:select-to-previous-word-start | -| 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+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 | -| shift+pageup | doc:select-to-previous-page | -| shift+pagedown | doc:select-to-next-page | diff --git a/doc/usage.md b/doc/usage.md deleted file mode 100644 index 90f6dc8d..00000000 --- a/doc/usage.md +++ /dev/null @@ -1,209 +0,0 @@ -# lite - -![screenshot](https://user-images.githubusercontent.com/3920290/81471642-6c165880-91ea-11ea-8cd1-fae7ae8f0bc4.png) - -## Overview -Lite is a lightweight text editor written mostly in Lua — it aims to provide -something practical, pretty, *small* and fast, implemented as simply as -possible; easy to modify and extend, or to use without doing either. - -Lite XL is based on the Lite editor itself and provide some enhancements -while remaining generally compatible with Lite. - - -## Getting Started -Lite works using a *project directory* — this is the directory where your -project's code and other data resides. - -To open lite with a specific project directory the directory name can be passed -as a command-line argument *(`.` can be passed to use the current directory)* or -the directory can be dragged onto either the lite executable or a running -instance of lite. - -Once started the project directory can be changed using the command -`core:change-project-folder`. The command will close all the documents -currently opened and switch to the new project directory. - -If you want to open a project directory in a new window the command -`core:open-project-folder` will open a new editor window with the selected -project directory. - -The main way of opening files in lite is through the `core:find-file` command -— this provides a fuzzy finder over all of the project's files and can be -opened using the **`ctrl+p`** shortcut by default. - -Commands can be run using keyboard shortcuts, or by using the `core:find-command` -command bound to **`ctrl+shift+p`** by default. For example, pressing -`ctrl+shift+p` and typing `newdoc` then pressing `return` would open a new -document. The current keyboard shortcut for a command can be seen to the right -of the command name on the command finder, thus to find the shortcut for a command -`ctrl+shift+p` can be pressed and the command name typed. - - -## User Module -lite can be configured through use of the user module. The user module can be -used for changing options in the config module, adding additional key bindings, -loading custom color themes, modifying the style or changing any other part of -lite to your personal preference. - -The user module is loaded by lite when the application starts, after the plugins -have been loaded. - -The user module can be modified by running the `core:open-user-module` command -or otherwise directly opening the `$HOME/.config/lite-xl/init.lua` file. - -On Windows, the variable `$USERPROFILE` will be used instead of -`$HOME`. - -Please note that Lite XL differs from the standard Lite editor for the location -of the user's module. - -## Project Module -The project module is an optional module which is loaded from the current -project's directory when lite is started. Project modules can be useful for -things like adding custom commands for project-specific build systems, or -loading project-specific plugins. - -The project module is loaded by lite when the application starts, after both the -plugins and user module have been loaded. - -The project module can be edited by running the `core:open-project-module` -command — if the module does not exist for the current project when the -command is run it will be created. - -## Big directories -Often projects contain compiled, bundled or downloaded files which you don't want to edit. These files can be excluded from projects by configuring `config.ignore_files`. Such a configuration might look like `config.ignore_files = { "^%.", "node_modules" }`. This will exclude the `node_modules` folder and any file starting with `.`. You can add this to a user or project module. - -If a project has more files than the maximum (configured with `config.max_project_files`) lite-xl will switch to a different mode where files are lazily loaded. - -_Note: Because of lazy loading `core:find-file` will open `core:open-file` instead._ - -## Add directories to a project - -In addition to the project directories it is possible to add other directories -using the command `core:add-directory`. -Once added a directory it will be shown in the tree-view on the left side and -the additional files will be reachable using the `ctrl+p` command (find file). -The additonal files will be also visible when searching across the project. - -The additional directories can be removed using the command `core:remove-directory`. - -When you will open again Lite XL on the same project folder the application will -remember your workspace including the additonal project directories. - -Since version 1.15 Lite XL does not need a workspace plugin as it is now -bundled with the editor. - - -## Create new empty directory - -Using the command `files:create-directory` or control-click in a directory in the -tree-view to create a new empty subdirectory. - - -## Commands -Commands in lite are used both through the command finder (`ctrl+shift+p`) and -by lite's keyboard shortcut system. Commands consist of 3 components: -* **Name** — The command name in the form of `namespace:action-name`, for - example: `doc:select-all` -* **Predicate** — A function that returns true if the command can be ran, for - example, for any document commands the predicate checks whether the active - view is a document -* **Function** — The function which performs the command itself - -Commands can be added using the `command.add` function provided by the -`core.command` module: -```lua -local core = require "core" -local command = require "core.command" - -command.add("core.docview", { - ["doc:save"] = function() - core.active_view.doc:save() - core.log("Saved '%s', core.active_view.doc.filename) - end -}) -``` - -Commands can be performed programatically (eg. from another command or by your -user module) by calling the `command.perform` function after requiring the -`command` module: -```lua -local command = require "core.command" -command.perform "core:quit" -``` - - -## Keymap -All keyboard shortcuts in lite are handled by the `core.keymap` module. A key -binding in lite maps a "stroke" (eg. `ctrl+q`) to one or more commands (eg. -`core:quit`). When the shortcut is pressed lite will iterate each command -assigned to that key and run the *predicate function* for that command — if the -predicate passes it stops iterating and runs the command. - -An example of where this used is the default binding of the `tab` key: -``` lua - ["tab"] = { "command:complete", "doc:indent" }, -``` -When tab is pressed the `command:complete` command is attempted which will only -succeed if the command-input at the bottom of the window is active. Otherwise -the `doc:indent` command is attempted which will only succeed if we have a -document as our active view. - -A new mapping can be added by your user module as follows: -```lua -local keymap = require "core.keymap" -keymap.add { ["ctrl+q"] = "core:quit" } -``` - -A list of default mappings can be viewed [here](./default-keymap.md). - - -## Plugins -Plugins in lite are normal lua modules and are treated as such — no -complicated plugin manager is provided, and, once a plugin is loaded, it is never -expected be to have to unload itself. - -To install a plugin simply drop it in the `plugins` directory in the user -module directory. -When Lite XL starts it will first load the plugins included in the data directory -and will then loads the plugins located in the user module directory. - -To uninstall a plugin the -plugin file can be deleted — any plugin (including those included with lite's -default installation) can be deleted to remove its functionality. - -If you want to load a plugin only under a certain circumstance (for example, -only on a given project) the plugin can be placed somewhere other than the -`plugins` directory so that it is not automatically loaded. The plugin can -then be loaded manually as needed by using the `require` function. - -Plugins can be downloaded from the [plugins repository](https://github.com/rxi/lite-plugins). - - -## Restarting the editor - -If you modifies the user configuration file or some of the Lua implementation files you may -restart the editor using the command `core:restart`. -All the application will be restarting by keeping the window that is already in use. - - -## Color Themes -Colors themes in lite are lua modules which overwrite the color fields of lite's -`core.style` module. -Pre-defined color methods are located in the `colors` folder in the data directory. -Additional color themes can be installed in the user's directory in a folder named -`colors`. - -A color theme can be set by requiring it in your user module: -```lua -core.reload_module "colors.winter" -``` - -In the Lite editor the function `require` is used instead of `core.reload_module`. -In Lite XL `core.reload_module` should be used to ensure that the color module -is actually reloaded when saving the user's configuration file. - -Color themes can be downloaded from the [color themes repository](https://github.com/rxi/lite-colors). -They are included with Lite XL release packages. - diff --git a/doc/licenses.md b/licenses/licenses.md similarity index 100% rename from doc/licenses.md rename to licenses/licenses.md diff --git a/meson.build b/meson.build index 4b73a158..fbf907b9 100644 --- a/meson.build +++ b/meson.build @@ -42,9 +42,7 @@ foreach data_module : ['core', 'fonts', 'plugins', 'colors'] install_subdir('data' / data_module , install_dir : lite_datadir) endforeach -foreach file : ['usage.md', 'licenses.md', 'contributors.md', 'default-keymap.md'] - install_data('doc' / file, install_dir : lite_docdir) -endforeach +install_data('licenses/licenses.md', install_dir : lite_docdir) lite_link_args = [] if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' diff --git a/src/api/regex.c b/src/api/regex.c index 05df8eee..1043b1c5 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -45,23 +45,27 @@ static int f_pcre_compile(lua_State *L) { } PCRE2_UCHAR buffer[256]; pcre2_get_error_message(errorNumber, buffer, sizeof(buffer)); - luaL_error(L, "regex compilation failed at offset %d: %s", - (int)errorOffset, buffer); - return 0; + lua_pushnil(L); + char message[1024]; + len = snprintf(message, sizeof(message), "regex compilation failed at offset %d: %s", (int)errorOffset, buffer); + lua_pushlstring(L, message, len); + return 2; } // Takes string, compiled regex, returns list of indices of matched groups // (including the whole match), if a match was found. static int f_pcre_match(lua_State *L) { - size_t len, offset = 0; + size_t len, offset = 1, opts = 0; luaL_checktype(L, 1, LUA_TTABLE); const char* str = luaL_checklstring(L, 2, &len); if (lua_gettop(L) > 2) offset = luaL_checknumber(L, 3); + if (lua_gettop(L) > 3) + opts = luaL_checknumber(L, 4); lua_rawgeti(L, 1, 1); pcre2_code* re = (pcre2_code*)lua_touserdata(L, -1); pcre2_match_data* md = pcre2_match_data_create_from_pattern(re, NULL); - int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset, 0, md, NULL); + int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL); if (rc < 0) { pcre2_match_data_free(md); if (rc != PCRE2_ERROR_NOMATCH) @@ -70,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; } @@ -97,5 +101,17 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, -2, "__name"); lua_pushvalue(L, -1); 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_NOTBOL); + lua_setfield(L, -2, "NOTBOL"); + lua_pushnumber(L, PCRE2_NOTEOL); + lua_setfield(L, -2, "NOTEOL"); + lua_pushnumber(L, PCRE2_NOTEMPTY); + lua_setfield(L, -2, "NOTEMPTY"); + lua_pushnumber(L, PCRE2_NOTEMPTY_ATSTART); + lua_setfield(L, -2, "NOTEMPTY_ATSTART"); return 1; } diff --git a/src/api/renderer_font.c b/src/api/renderer_font.c index 47ea97a3..f510da70 100644 --- a/src/api/renderer_font.c +++ b/src/api/renderer_font.c @@ -65,7 +65,7 @@ static int f_set_tab_size(lua_State *L) { static int f_gc(lua_State *L) { FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - font_desc_free(self); + font_desc_clear(self); return 0; } @@ -104,6 +104,22 @@ static int f_get_height(lua_State *L) { } +static int f_get_size(lua_State *L) { + FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); + lua_pushnumber(L, self->size); + return 1; +} + + +static int f_set_size(lua_State *L) { + FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); + float new_size = luaL_checknumber(L, 2); + font_desc_clear(self); + self->size = new_size; + return 0; +} + + static const luaL_Reg lib[] = { { "__gc", f_gc }, { "load", f_load }, @@ -112,6 +128,8 @@ static const luaL_Reg lib[] = { { "get_width_subpixel", f_get_width_subpixel }, { "get_height", f_get_height }, { "subpixel_scale", f_subpixel_scale }, + { "get_size", f_get_size }, + { "set_size", f_set_size }, { NULL, NULL } }; diff --git a/src/api/system.c b/src/api/system.c index 89c037c2..bbe0801b 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -152,6 +152,14 @@ top: return 4; case SDL_KEYDOWN: +#ifdef __APPLE__ + /* on macos 11.2.3 with sdl 2.0.14 the keyup handler for cmd+w below + ** was not enough. Maybe the quit event started to be triggered from the + ** keydown handler? In any case, flushing the quit event here too helped. */ + if ((e.key.keysym.sym == SDLK_w) && (e.key.keysym.mod & KMOD_GUI)) { + SDL_FlushEvent(SDL_QUIT); + } +#endif lua_pushstring(L, "keypressed"); lua_pushstring(L, key_name(buf, e.key.keysym.sym)); return 2; @@ -162,8 +170,7 @@ top: ** we want to flush this event and let the keymapper ** handle this key combination. ** Thanks to mathewmariani, taken from his lite-macos github repository. */ - if ((e.key.keysym.sym == SDLK_w) && (e.key.keysym.mod & KMOD_GUI)) - { + if ((e.key.keysym.sym == SDLK_w) && (e.key.keysym.mod & KMOD_GUI)) { SDL_FlushEvent(SDL_QUIT); } #endif diff --git a/src/bundle_open.m b/src/bundle_open.m index 58811862..f4f0b94c 100644 --- a/src/bundle_open.m +++ b/src/bundle_open.m @@ -4,8 +4,25 @@ void set_macos_bundle_resources(lua_State *L) { @autoreleasepool { - NSString* resource_path = [[NSBundle mainBundle] resourcePath]; - lua_pushstring(L, [resource_path UTF8String]); + /* 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); lua_setglobal(L, "MACOS_RESOURCES"); }} diff --git a/src/fontdesc.c b/src/fontdesc.c index d1d0825f..44460a6d 100644 --- a/src/fontdesc.c +++ b/src/fontdesc.c @@ -18,11 +18,12 @@ void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsig font_desc->cache_last_index = 0; /* Normally no need to initialize. */ } -void font_desc_free(FontDesc *font_desc) { +void font_desc_clear(FontDesc *font_desc) { for (int i = 0; i < font_desc->cache_length; i++) { ren_free_font(font_desc->cache[i].font); } font_desc->cache_length = 0; + font_desc->cache_last_index = 0; } void font_desc_set_tab_size(FontDesc *font_desc, int tab_size) { diff --git a/src/fontdesc.h b/src/fontdesc.h index 2f4702ab..bf591801 100644 --- a/src/fontdesc.h +++ b/src/fontdesc.h @@ -26,7 +26,7 @@ void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsig int font_desc_alloc_size(const char *filename); int font_desc_get_tab_size(FontDesc *font_desc); void font_desc_set_tab_size(FontDesc *font_desc, int tab_size); -void font_desc_free(FontDesc *font_desc); +void font_desc_clear(FontDesc *font_desc); RenFont *font_desc_get_font_at_scale(FontDesc *font_desc, int scale); #endif diff --git a/src/main.c b/src/main.c index 435c15fa..46befe51 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,5 @@ #include +#include #include #include "api/api.h" #include "rencache.h" @@ -63,8 +64,13 @@ static void get_exe_filename(char *buf, int sz) { int len = readlink(path, buf, sz - 1); buf[len] = '\0'; #elif __APPLE__ + /* use realpath to resolve a symlink if the process was launched from one. + ** This happens when Homebrew installs a cack and creates a symlink in + ** /usr/loca/bin for launching the executable from the command line. */ unsigned size = sz; - _NSGetExecutablePath(buf, &size); + char exepath[size]; + _NSGetExecutablePath(exepath, &size); + realpath(exepath, buf); #else strcpy(buf, "./lite"); #endif