diff --git a/LICENSE b/LICENSE index 39ddd05c..20ca7d69 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020 rxi +Copyright (c) 2020-2021 Francesco Abbate Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index ca9558f2..b96c9dca 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,76 @@ # Lite XL -![screenshot-dark](https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png) +[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) -A lightweight text editor written in Lua, adapted from [lite](https://github.com/rxi/lite) +![screenshot-dark] -* **[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 +A lightweight text editor written in Lua, adapted from [lite]. -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. +* **[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. + +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. -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 +## Licenses + 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. +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/build.sh b/build.sh index bfb679b0..6f605797 100755 --- a/build.sh +++ b/build.sh @@ -3,7 +3,7 @@ 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; do +for package in libagg freetype2 lua5.2 x11 libpcre2-8; do lflags+=" $(pkg-config --libs $package)" done lflags+=" $(sdl2-config --libs) -lm" diff --git a/changelog.md b/changelog.md index 62ccdf21..eb203747 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,47 @@ -Lite XL is following closely [rxi/lite](https://github.com/rxi/lite) but with some enhancements. - This files document the changes done in Lite XL for each release. +### 1.16.11 + +When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. +The application remains functional and the directories can be explored without using too much memory. +In this operating mode the files of the project are not indexed so the command "Core: Find File" will act as the "Core: Open File" command. +The "Project Search: Find" will work by searching all the files present in the project directory even if they are not indexed. + +Implemented changing fonts per syntax group by @liquidev. + +Example user module snippet that makes all comments italic: + +```lua +local style = require "core.style" + +-- italic.ttf must be provided by the user +local italic = renderer.font.load("italic.ttf", 14) +style.syntax_fonts["comment"] = italic +``` + +Improved indentation behavior by @adamharrison. + +Fix bug with close button not working in borderless window mode. + +Fix problem with normalization of filename for opened documents. + +### 1.16.10 + +Improved syntax highlight system thanks to @liquidev and @adamharrison. +Thanks to the new system we provide more a accurate syntax highlighting for Lua, C and C++. +Other syntax improvements contributed by @vincens2005. + +Move to JetBrains Mono and Fira Sans fonts for code and UI respectively. +Thet are provided under the SIL Open Font License, Version 1.1. +See `doc/licenses.md` for license details. + +Fixed bug with fonts and rencache module. +Under very specific situations the application was crashing due to invalid memory access. + +Add documentation for keymap binding, thanks to @Janis-Leuenberger. + +Added a contributors page in `doc/contributors.md`. + ### 1.16.9 Fix a bug related to nested panes resizing. diff --git a/data/core/command.lua b/data/core/command.lua index 95743061..7915e16d 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -59,7 +59,10 @@ end function command.add_defaults() - local reg = { "core", "root", "command", "doc", "findreplace", "files", "drawwhitespace" } + local reg = { + "core", "root", "command", "doc", "findreplace", + "files", "drawwhitespace", "dialog" + } for _, name in ipairs(reg) do require("core.commands." .. name) end diff --git a/data/core/commands/command.lua b/data/core/commands/command.lua index f0b80077..1a635a86 100644 --- a/data/core/commands/command.lua +++ b/data/core/commands/command.lua @@ -1,13 +1,7 @@ local core = require "core" local command = require "core.command" -local CommandView = require "core.commandview" -local function has_commandview() - return core.active_view:is(CommandView) -end - - -command.add(has_commandview, { +command.add("core.commandview", { ["command:submit"] = function() core.active_view:submit() end, diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index c8233062..859fb066 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,6 +66,9 @@ command.add(nil, { end, ["core:find-file"] = function() + if core.project_files_limit then + return command.perform "core:open-file" + end local files = {} for dir, item in core.get_project_files() do if item.type == "file" then @@ -88,17 +91,23 @@ command.add(nil, { ["core:open-file"] = function() local view = core.active_view if view.doc and view.doc.abs_filename then - core.command_view:set_text(common.home_encode(view.doc.abs_filename)) + local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") + if dirname then + dirname = core.normalize_to_project_dir(dirname) + local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP + core.command_view:set_text(text) + end end core.command_view:enter("Open File", function(text) - core.root_view:open_doc(core.open_doc(common.home_expand(text))) + local filename = system.absolute_path(common.home_expand(text)) + core.root_view:open_doc(core.open_doc(filename)) 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)) if err then core.error("Cannot open file %q: %q", text, err) - elseif path_stat.type == 'dir' then + elseif path_stat.type == 'dir' then core.error("Cannot open %q, is a folder", text) else return true diff --git a/data/core/commands/dialog.lua b/data/core/commands/dialog.lua new file mode 100644 index 00000000..90606abb --- /dev/null +++ b/data/core/commands/dialog.lua @@ -0,0 +1,35 @@ +local core = require "core" +local command = require "core.command" +local common = require "core.common" + +command.add("core.nagview", { + ["dialog:previous-entry"] = function() + local v = core.active_view + local hover = v.hovered_item or 1 + v:change_hovered(hover == 1 and #v.options or hover - 1) + end, + ["dialog:next-entry"] = function() + local v = core.active_view + local hover = v.hovered_item or 1 + v:change_hovered(hover == #v.options and 1 or hover + 1) + end, + ["dialog:select-yes"] = function() + local v = core.active_view + if v ~= core.nag_view then return end + v:change_hovered(common.find_index(v.options, "default_yes")) + command.perform "dialog:select" + end, + ["dialog:select-no"] = function() + local v = core.active_view + if v ~= core.nag_view then return end + v:change_hovered(common.find_index(v.options, "default_no")) + command.perform "dialog:select" + end, + ["dialog:select"] = function() + local v = core.active_view + if v.hovered_item then + v.on_selected(v.options[v.hovered_item]) + v:next() + end + end +}) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 965f3451..c8c27509 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -33,33 +33,6 @@ local function doc_multiline_selection(sort) return line1, col1, line2, col2, swap end - -local function insert_at_start_of_selected_lines(text, skip_empty) - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - for line = line1, line2 do - local line_text = doc().lines[line] - if (not skip_empty or line_text:find("%S")) then - doc():insert(line, 1, text) - end - end - doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap) -end - - -local function remove_from_start_of_selected_lines(text, skip_empty) - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - for line = line1, line2 do - local line_text = doc().lines[line] - if line_text:sub(1, #text) == text - and (not skip_empty or line_text:find("%S")) - then - doc():remove(line, 1, line, #text + 1) - end - end - doc():set_selection(line1, col1 - #text, line2, col2 - #text, swap) -end - - local function append_line_if_last_line(line) if line >= #doc().lines then doc():insert(line, math.huge, "\n") @@ -74,7 +47,6 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) end - local commands = { ["doc:undo"] = function() doc():undo() @@ -183,17 +155,11 @@ local commands = { end, ["doc:indent"] = function() - local text = get_indent_string() - if doc():has_selection() then - insert_at_start_of_selected_lines(text) - else - doc():text_input(text) - end + doc():indent_text(false, doc_multiline_selection(true)) end, ["doc:unindent"] = function() - local text = get_indent_string() - remove_from_start_of_selected_lines(text) + doc():indent_text(true, doc_multiline_selection(true)) end, ["doc:duplicate-lines"] = function() @@ -237,19 +203,31 @@ local commands = { ["doc:toggle-line-comments"] = function() local comment = doc().syntax.comment if not comment then return end + local indentation = get_indent_string() local comment_text = comment .. " " - local line1, _, line2 = doc():get_selection(true) + local line1, _, line2 = doc_multiline_selection(true) local uncomment = true + local start_offset = math.huge for line = line1, line2 do local text = doc().lines[line] - if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then + local s = text:find("%S") + local cs, ce = text:find(comment_text, s, true) + if s and cs ~= s then uncomment = false + start_offset = math.min(start_offset, s) end end - if uncomment then - remove_from_start_of_selected_lines(comment_text, true) - else - insert_at_start_of_selected_lines(comment_text, true) + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") + if uncomment then + local cs, ce = text:find(comment_text, s, true) + if ce then + doc():remove(line, cs, line, ce + 1) + end + elseif s then + doc():insert(line, start_offset, comment_text) + end end end, @@ -260,7 +238,7 @@ local commands = { ["doc:lower-case"] = function() doc():replace(string.lower) end, - + ["doc:go-to-line"] = function() local dv = dv() @@ -297,8 +275,12 @@ local commands = { end, ["doc:save-as"] = function() + local last_doc = core.last_active_view and core.last_active_view.doc if doc().filename then core.command_view:set_text(doc().filename) + elseif last_doc and last_doc.filename then + local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$") + core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP) end core.command_view:enter("Save As", function(filename) save(common.home_expand(filename)) @@ -315,7 +297,7 @@ local commands = { end end, - ["doc:rename"] = function() + ["file:rename"] = function() local old_filename = doc().filename if not old_filename then core.error("Cannot rename unsaved doc") @@ -330,6 +312,21 @@ local commands = { end end, common.path_suggest) end, + + + ["file:delete"] = function() + local filename = doc().abs_filename + if not filename then + core.error("Cannot remove unsaved doc") + return + end + for i,docview in ipairs(core.get_views_referencing_doc(doc())) do + local node = core.root_view.root_node:get_node_for_view(docview) + node:close_view(core.root_view, docview) + end + os.remove(filename) + core.log("Removed \"%s\"", filename) + end } diff --git a/data/core/commands/files.lua b/data/core/commands/files.lua index a0106f47..b2fdb336 100644 --- a/data/core/commands/files.lua +++ b/data/core/commands/files.lua @@ -1,12 +1,13 @@ local core = require "core" local command = require "core.command" +local common = require "core.common" command.add(nil, { ["files:create-directory"] = function() core.command_view:enter("New directory name", function(text) - local success, err = system.mkdir(text) + local success, err, path = common.mkdirp(text) if not success then - core.error("cannot create directory %q: %s", text, err) + core.error("cannot create directory %q: %s", path, err) end end) end, diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 937c410a..af60f33f 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -90,6 +90,7 @@ local function has_selection() 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) @@ -107,9 +108,9 @@ command.add("core.docview", { end) end, - ["find-replace:find-pattern"] = function() - find("Find Text Pattern", function(doc, line, col, text) - local opt = { wrap = true, no_case = true, pattern = true } + ["find-replace:find-regex"] = function() + find("Find Text Regex", function(doc, line, col, text) + local opt = { wrap = true, no_case = true, regex = true } return search.find(doc, line, col, text, opt) end) end, @@ -144,9 +145,10 @@ command.add("core.docview", { end) end, - ["find-replace:replace-pattern"] = function() - replace("Pattern", "", function(text, old, new) - return text:gsub(old, new) + ["find-replace:replace-regex"] = function() + replace("Regex", "", function(text, old, new) + local re = regex.compile(old) + return regex.gsub(re, text, new) end) end, diff --git a/data/core/common.lua b/data/core/common.lua index 05f91111..b7dd61db 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -22,6 +22,13 @@ function common.round(n) end +function common.find_index(tbl, prop) + for i, o in ipairs(tbl) do + if o[prop] then return i end + end +end + + function common.lerp(a, b, t) if type(a) ~= "table" then return a + (b - a) * t @@ -206,7 +213,9 @@ end function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 - if string.find(text, PATHSEP, dir_pos, true) == dir_pos then + -- ensure we don't replace if the text is just "$HOME" or "$HOME/" so + -- it must have a "/" following the $HOME and some characters following. + if string.find(text, PATHSEP, dir_pos, true) == dir_pos and #text > dir_pos then return "~" .. text:sub(dir_pos) end end @@ -271,4 +280,26 @@ function common.relative_path(ref_dir, dir) end +function common.mkdirp(path) + local stat = system.get_file_info(path) + if stat and stat.type then + return false, "path exists", path + end + local subdirs = {} + while path and path ~= "" do + local success_mkdir = system.mkdir(path) + if success_mkdir then break end + local updir, basedir = path:match("(.*)[/\\](.+)$") + table.insert(subdirs, 1, basedir or path) + path = updir + end + for _, dirname in ipairs(subdirs) do + path = path and path .. PATHSEP .. dirname or dirname + if not system.mkdir(path) then + return false, "cannot create directory", path + end + end + return true +end + return common diff --git a/data/core/config.lua b/data/core/config.lua index f9b4e87f..36a67add 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -3,7 +3,7 @@ local config = {} config.project_scan_rate = 5 config.fps = 60 config.max_log_items = 80 -config.message_timeout = 3 +config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE config.file_size_limit = 10 config.ignore_files = "^%." @@ -11,6 +11,7 @@ config.symbol_pattern = "[%a_][%w_]*" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.undo_merge_timeout = 0.3 config.max_undos = 10000 +config.max_tabs = 10 config.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 39fae9ca..88f63a68 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -405,6 +405,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 fe57523e..04090673 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -15,12 +15,8 @@ local function init_args(doc, line, col, text, opt) opt = opt or default_opt line, col = doc:sanitize_position(line, col) - if opt.no_case then - if opt.pattern then - text = text:gsub("%%?.", pattern_lower) - else - text = text:lower() - end + if opt.no_case and not opt.regex then + text = text:lower() end return doc, line, col, text, opt @@ -30,20 +26,32 @@ end function search.find(doc, line, col, text, opt) doc, line, col, text, opt = init_args(doc, line, col, text, opt) + local re + if opt.regex then + re = regex.compile(text, opt.no_case and "i" or "") + end for line = line, #doc.lines do local line_text = doc.lines[line] - if opt.no_case then - line_text = line_text:lower() + if opt.regex then + local s, e = re:cmatch(line_text, col) + if s then + return line, s, line, e + end + col = 1 + else + if opt.no_case then + line_text = line_text:lower() + end + local s, e = line_text:find(text, col, true) + if s then + return line, s, line, e + 1 + end + col = 1 end - local s, e = line_text:find(text, col, not opt.pattern) - if s then - return line, s, line, e + 1 - end - col = 1 end if opt.wrap then - opt = { no_case = opt.no_case, pattern = opt.pattern } + opt = { no_case = opt.no_case, regex = opt.regex } return search.find(doc, 1, 1, text, opt) end end diff --git a/data/core/docview.lua b/data/core/docview.lua index 68b242f3..070ee0c4 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -141,29 +141,45 @@ end function DocView:get_col_x_offset(line, col) - local text = self.doc.lines[line] - if not text then return 0 end - return self:get_font():get_width(text:sub(1, col - 1)) + local default_font = self:get_font() + local column = 1 + local xoffset = 0 + for _, type, text in self.doc.highlighter:each_token(line) do + local font = style.syntax_fonts[type] or default_font + for char in common.utf8_chars(text) do + if column == col then + return xoffset / font:subpixel_scale() + end + xoffset = xoffset + font:get_width_subpixel(char) + column = column + #char + end + end + + return xoffset / default_font:subpixel_scale() end function DocView:get_x_offset_col(line, x) - local text = self.doc.lines[line] + local line_text = self.doc.lines[line] local xoffset, last_i, i = 0, 1, 1 - local subpixel_scale = self:get_font():subpixel_scale(); + local default_font = self:get_font() + local subpixel_scale = default_font:subpixel_scale() local x_subpixel = subpixel_scale * x + subpixel_scale / 2 - for char in common.utf8_chars(text) do - local w = self:get_font():get_width_subpixel(char) - if xoffset >= subpixel_scale * x then - return (xoffset - x_subpixel > w / 2) and last_i or i + for _, type, text in self.doc.highlighter:each_token(line) do + local font = style.syntax_fonts[type] or default_font + for char in common.utf8_chars(text) do + local w = font:get_width_subpixel(char) + if xoffset >= subpixel_scale * x then + return (xoffset - x_subpixel > w / 2) and last_i or i + end + xoffset = xoffset + w + last_i = i + i = i + #char end - xoffset = xoffset + w - last_i = i - i = i + #char end - return #text + return #line_text end @@ -308,11 +324,12 @@ end function DocView:draw_line_text(idx, x, y) - local font = self:get_font() - local subpixel_scale = font:subpixel_scale() + local default_font = self:get_font() + local subpixel_scale = default_font:subpixel_scale() local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset() for _, type, text in self.doc.highlighter:each_token(idx) do local color = style.syntax[type] + local font = style.syntax_fonts[type] or default_font if config.draw_whitespace then tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment) else diff --git a/data/core/init.lua b/data/core/init.lua index 8d4d84c3..e571e6b4 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1,4 +1,5 @@ require "core.strict" +require "core.regex" local common = require "core.common" local config = require "core.config" local style = require "core.style" @@ -17,7 +18,7 @@ local core = {} local function load_session() local ok, t = pcall(dofile, USERDIR .. "/session.lua") if ok then - return t.recents, t.window + return t.recents, t.window, t.window_mode end return {} end @@ -28,6 +29,7 @@ local function save_session() if fp then 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()), "}\n") fp:close() end @@ -72,6 +74,7 @@ function core.set_project_dir(new_dir, change_project_fn) core.project_directories = {} core.add_project_directory(new_dir) core.project_files = {} + core.project_files_limit = false core.reschedule_project_scan() return true end @@ -99,6 +102,57 @@ local function strip_trailing_slash(filename) return filename end +local function compare_file(a, b) + return a.filename < b.filename +end + +-- "root" will by an absolute path without trailing '/' +-- "path" will be a path starting with '/' and without trailing '/' +-- or the empty string. +-- It will identifies a sub-path within "root. +-- The current path location will therefore always be: root .. path. +-- When recursing "root" will always be the same, only "path" will change. +-- Returns a list of file "items". In eash item the "filename" will be the +-- complete file path relative to "root" *without* the trailing '/'. +local function get_directory_files(root, path, t, recursive, begin_hook) + if begin_hook then begin_hook() end + local size_limit = config.file_size_limit * 10e5 + local all = system.list_dir(root .. path) or {} + local dirs, files = {}, {} + + local entries_count = 0 + local max_entries = config.max_project_files + for _, file in ipairs(all) do + if not common.match_pattern(file, config.ignore_files) then + local file = path .. PATHSEP .. file + local info = system.get_file_info(root .. file) + if info and info.size < size_limit then + info.filename = strip_leading_path(file) + table.insert(info.type == "dir" and dirs or files, info) + entries_count = entries_count + 1 + if recursive and entries_count > max_entries then return nil, entries_count end + end + end + end + + table.sort(dirs, compare_file) + for _, f in ipairs(dirs) do + table.insert(t, f) + if recursive and entries_count <= max_entries then + local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive) + entries_count = entries_count + subdir_count + f.scanned = true + end + end + + table.sort(files, compare_file) + for _, f in ipairs(files) do + table.insert(t, f) + end + + return t, entries_count +end + local function project_scan_thread() local function diff_files(a, b) if #a ~= #b then return true end @@ -110,71 +164,21 @@ local function project_scan_thread() end end - local function compare_file(a, b) - return a.filename < b.filename - end - - -- "root" will by an absolute path without trailing '/' - -- "path" will be a path starting with '/' and without trailing '/' - -- or the empty string. - -- It will identifies a sub-path within "root. - -- The current path location will therefore always be: root .. path. - -- When recursing "root" will always be the same, only "path" will change. - -- Returns a list of file "items". In eash item the "filename" will be the - -- complete file path relative to "root" *without* the trailing '/'. - local function get_files(root, path, t) - coroutine.yield() - t = t or {} - local size_limit = config.file_size_limit * 10e5 - local all = system.list_dir(root .. path) or {} - local dirs, files = {}, {} - - local entries_count = 0 - local max_entries = config.max_project_files - for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 - if entries_count > max_entries then break end - end - end - end - - table.sort(dirs, compare_file) - for _, f in ipairs(dirs) do - table.insert(t, f) - if entries_count <= max_entries then - local subdir_t, subdir_count = get_files(root, PATHSEP .. f.filename, t) - entries_count = entries_count + subdir_count - end - end - - table.sort(files, compare_file) - for _, f in ipairs(files) do - table.insert(t, f) - end - - return t, entries_count - end - while true do -- get project files and replace previous table if the new table is -- different - for i = 1, #core.project_directories do + local i = 1 + while not core.project_files_limit and i <= #core.project_directories do local dir = core.project_directories[i] - local t, entries_count = get_files(dir.name, "") + local t, entries_count = get_directory_files(dir.name, "", {}, true) if diff_files(dir.files, t) then if entries_count > config.max_project_files then + core.project_files_limit = true core.status_view:show_message("!", style.accent, - "Too many files in project directory: stopping reading at ".. - config.max_project_files.." files according to config.max_project_files. ".. - "Either tweak this variable, or ignore certain files/directories by ".. - "using the config.ignore_files variable in your user plugin or ".. - "project config.") + "Too many files in project directory: stopped reading at ".. + config.max_project_files.." files. For more information see ".. + "usage.md at github.com/franko/lite-xl." + ) end dir.files = t core.redraw = true @@ -182,6 +186,7 @@ local function project_scan_thread() if dir.name == core.project_dir then core.project_files = dir.files end + i = i + 1 end -- wait for next scan @@ -190,6 +195,56 @@ local function project_scan_thread() 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) + for _, dir in ipairs(core.project_directories) do + if dir.name == dirname then + for i, file in ipairs(dir.files) do + local file = dir.files[i] + if file.filename == filename then + if file.scanned then return end + local new_files = get_directory_files(dirname, PATHSEP .. filename, {}) + for j, new_file in ipairs(new_files) do + table.insert(dir.files, i + j, new_file) + end + file.scanned = true + return + end + end + end + end +end + + +local function find_project_files_co(root, path) + local size_limit = config.file_size_limit * 10e5 + local all = system.list_dir(root .. path) or {} + for _, file in ipairs(all) do + if not common.match_pattern(file, config.ignore_files) then + local file = path .. PATHSEP .. file + local info = system.get_file_info(root .. file) + if info and info.size < size_limit then + info.filename = strip_leading_path(file) + if info.type == "file" then + coroutine.yield(root, info) + else + find_project_files_co(root, PATHSEP .. info.filename) + end + end + end + end +end + + local function project_files_iter(state) local dir = core.project_directories[state.dir_index] state.file_index = state.file_index + 1 @@ -204,42 +259,39 @@ end function core.get_project_files() - local state = { dir_index = 1, file_index = 0 } - return project_files_iter, state + if core.project_files_limit then + return coroutine.wrap(function() + for _, dir in ipairs(core.project_directories) do + find_project_files_co(dir.name, "") + end + end) + else + local state = { dir_index = 1, file_index = 0 } + return project_files_iter, state + end end function core.project_files_number() - local n = 0 - for i = 1, #core.project_directories do - n = n + #core.project_directories[i].files + if not core.project_files_limit then + local n = 0 + for i = 1, #core.project_directories do + n = n + #core.project_directories[i].files + end + return n end - return n end -- create a directory using mkdir but may need to create the parent -- directories as well. local function create_user_directory() - local dirname_create = USERDIR - local basedir - local subdirs = {} - while dirname_create and dirname_create ~= "" do - local success_mkdir = system.mkdir(dirname_create) - if success_mkdir then break end - dirname_create, basedir = dirname_create:match("(.*)[/\\](.+)$") - if basedir then - subdirs[#subdirs + 1] = basedir - end - end - for _, dirname in ipairs(subdirs) do - dirname_create = dirname_create .. '/' .. dirname - if not system.mkdir(dirname_create) then - error("cannot create directory: \"" .. dirname_create .. "\"") - end + local success, err = common.mkdirp(USERDIR) + if not success then + error("cannot create directory \"" .. USERDIR .. "\": " .. err) end for _, modname in ipairs {'plugins', 'colors', 'fonts'} do - local subdirname = dirname_create .. '/' .. modname + local subdirname = USERDIR .. PATHSEP .. modname if not system.mkdir(subdirname) then error("cannot create directory: \"" .. subdirname .. "\"") end @@ -274,8 +326,8 @@ local style = require "core.style" ------------------------------- Fonts ---------------------------------------- -- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 13 * SCALE) --- style.code_font = renderer.font.load(DATADIR .. "/fonts/monospace.ttf", 12 * SCALE) +-- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) +-- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) -- -- font names used by lite: -- style.font : user interface @@ -357,6 +409,20 @@ local function whitespace_replacements() end +local function reload_on_user_module_save() + -- auto-realod style when user's module is saved by overriding Doc:Save() + local doc_save = Doc.save + local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") + function Doc:save(filename, abs_filename) + doc_save(self, filename, abs_filename) + if self.abs_filename == user_filename then + core.reload_module("core.style") + core.load_user_directory() + end + end +end + + function core.init() command = require "core.command" keymap = require "core.keymap" @@ -375,9 +441,11 @@ function core.init() end do - local recent_projects, window_position = load_session() - if window_position then + 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 + system.set_window_mode("maximized") end core.recent_projects = recent_projects end @@ -497,6 +565,8 @@ function core.init() if item.text == "Exit" then os.exit(1) end end) end + + reload_on_user_module_save() end @@ -556,13 +626,19 @@ do end +-- DEPRECATED function core.doc_save_hooks = {} function core.add_save_hook(fn) + core.error("The function core.add_save_hook is deprecated." .. + " Modules should now directly override the Doc:save function.") core.doc_save_hooks[#core.doc_save_hooks + 1] = fn end +-- DEPRECATED function function core.on_doc_save(filename) + -- for backward compatibility in modules. Hooks are deprecated, the function Doc:save + -- should be directly overidded. for _, hook in ipairs(core.doc_save_hooks) do hook(filename) end @@ -870,26 +946,18 @@ end function core.step() -- handle events local did_keymap = false - local mouse_moved = false - local mouse = { x = 0, y = 0, dx = 0, dy = 0 } - for type, a,b,c,d in system.poll_event do - if type == "mousemoved" then - mouse_moved = true - mouse.x, mouse.y = a, b - mouse.dx, mouse.dy = mouse.dx + c, mouse.dy + d - elseif type == "textinput" and did_keymap then + if type == "textinput" and did_keymap then did_keymap = false + elseif type == "mousemoved" then + core.try(core.on_event, type, a, b, c, d) else local _, res = core.try(core.on_event, type, a, b, c, d) did_keymap = res or did_keymap end core.redraw = true end - if mouse_moved then - core.try(core.on_event, "mousemoved", mouse.x, mouse.y, mouse.dx, mouse.dy) - end local width, height = renderer.get_size() @@ -994,6 +1062,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") @@ -1009,14 +1082,4 @@ function core.on_error(err) end -core.add_save_hook(function(filename) - local doc = core.active_view.doc - if doc and doc:is(Doc) and doc.abs_filename == USERDIR .. PATHSEP .. "init.lua" then - core.reload_module("core.style") - core.load_user_directory() - end -end) - - - return core diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 0809615f..ccbb102a 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -6,31 +6,31 @@ local function keymap_macos(keymap) ["cmd+n"] = "core:new-doc", ["cmd+shift+c"] = "core:change-project-folder", ["cmd+shift+o"] = "core:open-project-folder", - ["alt+return"] = "core:toggle-fullscreen", + ["cmd+ctrl+return"] = "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", + ["cmd+ctrl+shift+j"] = "root:split-left", + ["cmd+ctrl+shift+l"] = "root:split-right", + ["cmd+ctrl+shift+i"] = "root:split-up", + ["cmd+ctrl+shift+k"] = "root:split-down", + ["cmd+ctrl+j"] = "root:switch-to-left", + ["cmd+ctrl+l"] = "root:switch-to-right", + ["cmd+ctrl+i"] = "root:switch-to-up", + ["cmd+ctrl+k"] = "root:switch-to-down", ["ctrl+w"] = "root:close", - ["cmd+tab"] = "root:switch-to-next-tab", - ["cmd+shift+tab"] = "root:switch-to-previous-tab", + ["ctrl+tab"] = "root:switch-to-next-tab", + ["ctrl+shift+tab"] = "root:switch-to-previous-tab", ["cmd+pageup"] = "root:move-tab-left", ["cmd+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", + ["cmd+1"] = "root:switch-to-tab-1", + ["cmd+2"] = "root:switch-to-tab-2", + ["cmd+3"] = "root:switch-to-tab-3", + ["cmd+4"] = "root:switch-to-tab-4", + ["cmd+5"] = "root:switch-to-tab-5", + ["cmd+6"] = "root:switch-to-tab-6", + ["cmd+7"] = "root:switch-to-tab-7", + ["cmd+8"] = "root:switch-to-tab-8", + ["cmd+9"] = "root:switch-to-tab-9", ["cmd+f"] = "find-replace:find", ["cmd+r"] = "find-replace:replace", diff --git a/data/core/nagview.lua b/data/core/nagview.lua index accb1a45..6d6f89f4 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -170,12 +170,6 @@ function NagView:draw() end end -local function findindex(tbl, prop) - for i, o in ipairs(tbl) do - if o[prop] then return i end - end -end - function NagView:get_message_height() local h = 0 for str in string.gmatch(self.message, "(.-)\n") do @@ -196,7 +190,7 @@ function NagView:next() -- self.target_height is the nagview height needed to display the message and -- the buttons, excluding the top and bottom padding space. self.target_height = math.max(message_height, self:get_buttons_height()) - self:change_hovered(findindex(self.options, "default_yes")) + 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) @@ -212,36 +206,4 @@ function NagView:show(title, message, options, on_select) if #self.queue > 0 and not self.title then self:next() end end -command.add(NagView, { - ["dialog:previous-entry"] = function() - local v = core.active_view - local hover = v.hovered_item or 1 - v:change_hovered(hover == 1 and #v.options or hover - 1) - end, - ["dialog:next-entry"] = function() - local v = core.active_view - local hover = v.hovered_item or 1 - v:change_hovered(hover == #v.options and 1 or hover + 1) - end, - ["dialog:select-yes"] = function() - local v = core.active_view - if v ~= core.nag_view then return end - v:change_hovered(findindex(v.options, "default_yes")) - command.perform "dialog:select" - end, - ["dialog:select-no"] = function() - local v = core.active_view - if v ~= core.nag_view then return end - v:change_hovered(findindex(v.options, "default_no")) - command.perform "dialog:select" - end, - ["dialog:select"] = function() - local v = core.active_view - if v.hovered_item then - v.on_selected(v.options[v.hovered_item]) - v:next() - end - end, -}) - -return NagView \ No newline at end of file +return NagView diff --git a/data/core/regex.lua b/data/core/regex.lua new file mode 100644 index 00000000..19c59164 --- /dev/null +++ b/data/core/regex.lua @@ -0,0 +1,70 @@ + +-- 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, options) + local pattern = type(pattern_string) == "table" and + pattern_string or regex.compile(pattern_string) + return regex.cmatch(pattern, string, offset or 1, options or 0) +end + +-- 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 + repeat + index = index - 1 + byte = string.byte(str, index) + until byte < 128 or byte >= 192 + return index +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 + index = index + 1 + byte = string.byte(str, index + 1) + end + return index +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 +-- 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 matches, replacements = {}, {} + repeat + indices = { regex.cmatch(pattern, str) } + if #indices > 0 then + table.insert(matches, indices) + local currentReplacement = replacement + if #indices > 2 then + for i = 1, (#indices/2 - 1) do + currentReplacement = string.gsub( + 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 .. + str:sub(1, previous_character(str, indices[1])) .. currentReplacement + else + result = result .. currentReplacement + end + str = str:sub(indices[2]) + end + until #indices == 0 or indices[1] == indices[2] + return result .. str, matches, replacements +end + diff --git a/data/core/rootview.lua b/data/core/rootview.lua index da628c7e..aa9a40cb 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -1,5 +1,6 @@ local core = require "core" local common = require "core.common" +local config = require "core.config" local style = require "core.style" local keymap = require "core.keymap" local Object = require "core.object" @@ -59,7 +60,9 @@ function Node:new(type) end self.hovered = {x = -1, y = -1 } self.hovered_close = 0 - self.tab_margin = style.padding.x + self.tab_shift = 0 + self.tab_offset = 1 + self.tab_width = style.tab_width self.move_towards = View.move_towards end @@ -126,36 +129,41 @@ function Node:split(dir, view, locked, resizable) return self.b end - -function Node:close_view(root, view) - local new_active_view = view == self.active_view - local do_close = function() - if #self.views > 1 then - local idx = self:get_view_idx(view) - table.remove(self.views, idx) - if new_active_view then - self:set_active_view(self.views[idx] or self.views[#self.views]) - end +function Node:remove_view(root, view) + if #self.views > 1 then + local idx = self:get_view_idx(view) + if idx < self.tab_offset then + self.tab_offset = self.tab_offset - 1 + end + table.remove(self.views, idx) + if self.active_view == view then + self:set_active_view(self.views[idx] or self.views[#self.views]) + end + else + local parent = self:get_parent_node(root) + local is_a = (parent.a == self) + local other = parent[is_a and "b" or "a"] + if other:get_locked_size() then + self.views = {} + self:add_view(EmptyView()) else - local parent = self:get_parent_node(root) - local is_a = (parent.a == self) - local other = parent[is_a and "b" or "a"] - if other:get_locked_size() then - self.views = {} - self:add_view(EmptyView()) - else - parent:consume(other) - local p = parent - while p.type ~= "leaf" do - p = p[is_a and "a" or "b"] - end - p:set_active_view(p.active_view) - if self.is_primary_node then - p.is_primary_node = true - end + parent:consume(other) + local p = parent + while p.type ~= "leaf" do + p = p[is_a and "a" or "b"] + end + p:set_active_view(p.active_view) + if self.is_primary_node then + p.is_primary_node = true end end - core.last_active_view = nil + end + core.last_active_view = nil +end + +function Node:close_view(root, view) + local do_close = function() + self:remove_view(root, view) end view:try_close(do_close) end @@ -166,13 +174,13 @@ function Node:close_active_view(root) end -function Node:add_view(view) +function Node:add_view(view, idx) assert(self.type == "leaf", "Tried to add view to non-leaf node") assert(not self.locked, "Tried to add view to locked node") if self.views[1] and self.views[1]:is(EmptyView) then table.remove(self.views) end - table.insert(self.views, view) + table.insert(self.views, idx or (#self.views + 1), view) self:set_active_view(view) end @@ -221,6 +229,15 @@ function Node:get_children(t) end +-- return the width including the padding space and separately +-- the padding space itself +local function get_scroll_button_width() + local w = style.icon_font:get_width(">") + local pad = w + return w + 2 * pad, pad +end + + function Node:get_divider_overlapping_point(px, py) if self.type ~= "leaf" then local p = 6 @@ -236,11 +253,18 @@ function Node:get_divider_overlapping_point(px, py) end +function Node:get_visible_tabs_number() + return math.min(#self.views - self.tab_offset + 1, config.max_tabs) +end + + function Node:get_tab_overlapping_point(px, py) if #self.views == 1 then return nil end - local x, y, w, h = self:get_tab_rect(1) - if px >= x and py >= y and px < x + w * #self.views and py < y + h then - return math.floor((px - x) / w) + 1 + 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) + if px >= x1 and py >= y1 and px < x2 and py < y1 + h then + return math.floor((px - x1) / w) + self.tab_offset end end @@ -252,18 +276,31 @@ local function close_button_location(x, w) end +function Node:get_scroll_button_index(px, py) + if #self.views == 1 then return end + for i = 1, 2 do + local x, y, w, h = self:get_scroll_button_rect(i) + if px >= x and px < x + w and py >= y and py < y + h then + return i + end + end +end + + function Node:tab_hovered_update(px, py) local tab_index = self:get_tab_overlapping_point(px, py) self.hovered_tab = tab_index + self.hovered_close = 0 + self.hovered_scroll_button = 0 if tab_index then local x, y, w, h = self:get_tab_rect(tab_index) local cx, cw = close_button_location(x, w) if px >= cx and px < cx + cw and py >= y and py < y + h then self.hovered_close = tab_index - return end + else + self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0 end - self.hovered_close = 0 end @@ -280,10 +317,20 @@ function Node:get_child_overlapping_point(x, y) end +function Node:get_scroll_button_rect(index) + local w, pad = get_scroll_button_width() + local h = style.font:get_height() + style.padding.y * 2 + local x = self.position.x + (index == 1 and 0 or self.size.x - w) + return x, self.position.y, w, h, pad +end + + function Node:get_tab_rect(idx) - local tw = math.min(style.tab_width, (self.size.x - self.tab_margin) / #self.views) - local x_left = self.position.x + tw * (idx - 1) - local x1, x2 = math.floor(x_left), math.floor(x_left + tw) + local sbw = get_scroll_button_width() + local maxw = self.size.x - 2 * sbw + local x0 = self.position.x + sbw + local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw) + local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw) local h = style.font:get_height() + style.padding.y * 2 return x1, self.position.y, x2 - x1, h end @@ -386,13 +433,61 @@ function Node:update_layout() end +function Node:scroll_tabs_to_visible() + local index = self:get_view_idx(self.active_view) + if index then + local tabs_number = self:get_visible_tabs_number() + if self.tab_offset > index then + self.tab_offset = index + elseif self.tab_offset + tabs_number - 1 < index then + self.tab_offset = index - tabs_number + 1 + elseif tabs_number < config.max_tabs and self.tab_offset > 1 then + self.tab_offset = #self.views - config.max_tabs + 1 + end + end +end + + +function Node:scroll_tabs(dir) + local view_index = self:get_view_idx(self.active_view) + if dir == 1 then + if self.tab_offset > 1 then + self.tab_offset = self.tab_offset - 1 + local last_index = self.tab_offset + self:get_visible_tabs_number() - 1 + if view_index > last_index then + self:set_active_view(self.views[last_index]) + end + end + elseif dir == 2 then + local tabs_number = self:get_visible_tabs_number() + if self.tab_offset + tabs_number - 1 < #self.views then + self.tab_offset = self.tab_offset + 1 + local view_index = self:get_view_idx(self.active_view) + if view_index < self.tab_offset then + self:set_active_view(self.views[self.tab_offset]) + end + end + end +end + + +function Node:target_tab_width() + local n = self:get_visible_tabs_number() + local w = self.size.x - get_scroll_button_width() * 2 + return common.clamp(style.tab_width, w / config.max_tabs, w / n) +end + + function Node:update() if self.type == "leaf" then + self:scroll_tabs_to_visible() for _, view in ipairs(self.views) do view:update() end self:tab_hovered_update(self.hovered.x, self.hovered.y) - self:move_towards("tab_margin", style.padding.x) + local tab_width = self:target_tab_width() + self:move_towards("tab_shift", tab_width * (self.tab_offset - 1)) + self:move_towards("tab_width", tab_width) else self.a:update() self.b:update() @@ -401,14 +496,27 @@ end function Node:draw_tabs() - local x, y, _, h = self:get_tab_rect(1) + local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1) local ds = style.divider_size local dots_width = style.font:get_width("…") core.push_clip_rect(x, y, self.size.x, h) renderer.draw_rect(x, y, self.size.x, h, style.background2) renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider) - for i, view in ipairs(self.views) do + if self.tab_offset > 1 then + local button_style = self.hovered_scroll_button == 1 and style.text or style.dim + common.draw_text(style.icon_font, button_style, "<", nil, x + scroll_padding, y, 0, h) + end + + local tabs_number = self:get_visible_tabs_number() + if #self.views > self.tab_offset + tabs_number - 1 then + local xrb, yrb, wrb = self:get_scroll_button_rect(2) + local button_style = self.hovered_scroll_button == 2 and style.text or style.dim + common.draw_text(style.icon_font, button_style, ">", nil, xrb + scroll_padding, yrb, 0, h) + end + + for i = self.tab_offset, self.tab_offset + tabs_number - 1 do + local view = self.views[i] local x, y, w, h = self:get_tab_rect(i) local text = view:get_name() local color = style.dim @@ -630,6 +738,12 @@ function RootView:close_all_docviews() end +-- Function to intercept mouse pressed events on the active view. +-- Do nothing by default. +function RootView.on_view_mouse_pressed(button, x, y, clicks) +end + + function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) if div then @@ -637,19 +751,23 @@ function RootView:on_mouse_pressed(button, x, y, clicks) 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 - local _, _, tw = node:get_tab_rect(idx) - node.tab_margin = node.tab_margin + tw node:close_view(self.root_node, node.views[idx]) else - self.dragged_node = idx + self.dragged_node = { node, idx } node:set_active_view(node.views[idx]) end else core.set_active_view(node.active_view) - node.active_view:on_mouse_pressed(button, x, y, clicks) + if not self.on_view_mouse_pressed(button, x, y, clicks) then + node.active_view:on_mouse_pressed(button, x, y, clicks) + end end end @@ -678,7 +796,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 @@ -702,21 +820,35 @@ function RootView:on_mouse_moved(x, y, dx, dy) local node = self.root_node:get_child_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y) local tab_index = node and node:get_tab_overlapping_point(x, y) - if div then + 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 - 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") - 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) - table.insert(node.views, tab_index, tab) - self.dragged_node = tab_index - end - else - system.set_cursor(node.active_view.cursor) + core.request_cursor("arrow") + elseif node then + core.request_cursor(node.active_view.cursor) + end + if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index)) + and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then + local tab = self.dragged_node[1].views[self.dragged_node[2]] + if self.dragged_node[1] ~= node then + for i, v in ipairs(node.views) do if v.doc == tab.doc then tab = nil break end end + if tab then + self.dragged_node[1]:remove_view(self.root_node, tab) + node:add_view(tab, tab_index) + self.root_node:update_layout() + self.dragged_node = { node, tab_index or #node.views } + core.redraw = true + end + else + table.remove(self.dragged_node[1].views, self.dragged_node[2]) + table.insert(node.views, tab_index, tab) + self.dragged_node = { node, tab_index } + end end end @@ -751,6 +883,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/start.lua b/data/core/start.lua index 7d4ad9a8..3ef806cc 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,5 @@ --- this file is used by lite-xl to setup the Lua environment --- when starting -VERSION = "1.16.9" +-- this file is used by lite-xl to setup the Lua environment when starting +VERSION = "1.16.11" MOD_VERSION = "1" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE @@ -13,7 +12,7 @@ else local prefix = EXEDIR:match("^(.+)[/\\]bin$") DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data') end -USERDIR = HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user') +USERDIR = os.getenv("XDG_CONFIG_HOME") or (HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user')) package.path = DATADIR .. '/?.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path diff --git a/data/core/style.lua b/data/core/style.lua index ab72bde5..60df7c73 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/font.ttf", 13 * SCALE) -style.big_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 34 * SCALE) +style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) +style.big_font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 40 * SCALE) style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) style.icon_big_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 20 * SCALE, {antialiasing="grayscale", hinting="full"}) -style.code_font = renderer.font.load(DATADIR .. "/fonts/monospace.ttf", 12 * SCALE) +style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) style.background = { common.color "#2e2e32" } style.background2 = { common.color "#252529" } @@ -57,4 +57,11 @@ style.syntax["string"] = { common.color "#f7c95c" } style.syntax["operator"] = { common.color "#93DDFA" } style.syntax["function"] = { common.color "#93DDFA" } +-- This can be used to override fonts per syntax group. +-- The syntax highlighter will take existing values from this table and +-- override style.code_font on a per-token basis, so you can choose to eg. +-- render comments in an italic font if you want to. +style.syntax_fonts = {} +-- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options) + return style diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index f3070995..a20dba5e 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -15,59 +15,80 @@ local function push_token(t, type, text) 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 +local function push_tokens(t, syn, pattern, full_text, find_results) + if #find_results > 2 then + -- We do some manipulation with find_results so that it's arranged + -- like this: + -- { start, end, i_1, i_2, i_3, …, i_last } + -- Each position spans characters from i_n to ((i_n+1) - 1), to form + -- consecutive spans of text. + -- + -- If i_1 is not equal to start, start is automatically inserted at + -- that index. + if find_results[3] ~= find_results[1] then + table.insert(find_results, 3, find_results[1]) end + -- Copy the ending index to the end of the table, so that an ending index + -- always follows a starting index after position 3 in the table. + table.insert(find_results, find_results[2] + 1) + -- Then, we just iterate over our modified table. + for i = 3, #find_results - 1 do + local start = find_results[i] + local fin = find_results[i + 1] - 1 + local type = pattern.type[i - 2] + -- ↑ (i - 2) to convert from [3; n] to [1; n] + local text = full_text:sub(start, fin) + push_token(t, syn.symbols[text] or type, text) + end + else + local start, fin = find_results[1], find_results[2] + local text = full_text:sub(start, fin) + push_token(t, syn.symbols[text] or pattern.type, text) 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, --- does not support further highlighting. + +-- 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, +-- does not support further highlighting. + +-- You can think of it as a maximum 4 integer (0-255) stack. It always has +-- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling +-- `pop_subsyntax` decreases it. The integers represent the index of a pattern +-- that we're following in the syntax. The top of the stack can be any valid +-- pattern index, any integer lower in the stack must represent a pattern that +-- specifies a subsyntax. + +-- If you do not have subsyntaxes in your syntax, the three most +-- singificant numbers will always be 0, the stack will only ever be length 1 +-- and the state variable will only ever range from 0-255. local function retrieve_syntax_state(incoming_syntax, state) - local current_syntax, subsyntax_info, current_state, current_level = + local current_syntax, subsyntax_info, current_pattern_idx, current_level = incoming_syntax, nil, state, 0 if state > 0 and (state > 255 or current_syntax.patterns[state].syntax) then - -- If we have higher bits, then decode them one at a time, and find which - -- syntax we're using. Rather than walking the bytes, and calling into + -- If we have higher bits, then decode them one at a time, and find which + -- syntax we're using. Rather than walking the bytes, and calling into -- `syntax` each time, we could probably cache this in a single table. - for i=0,2 do + for i = 0, 2 do local target = bit32.extract(state, i*8, 8) if target ~= 0 then if current_syntax.patterns[target].syntax then subsyntax_info = current_syntax.patterns[target] - current_syntax = type(subsyntax_info.syntax) == "table" and + current_syntax = type(subsyntax_info.syntax) == "table" and subsyntax_info.syntax or syntax.get(subsyntax_info.syntax) - current_state = 0 + current_pattern_idx = 0 current_level = i+1 else - current_state = target + current_pattern_idx = target break end - else + else break end end end - return current_syntax, subsyntax_info, current_state, current_level + return current_syntax, subsyntax_info, current_pattern_idx, current_level end function tokenizer.tokenize(incoming_syntax, text, state) @@ -77,37 +98,94 @@ function tokenizer.tokenize(incoming_syntax, text, state) if #incoming_syntax.patterns == 0 then return { "normal", text } end - + state = state or 0 - local current_syntax, subsyntax_info, current_state, current_level = + -- incoming_syntax : the parent syntax of the file. + -- state : a 32-bit number representing syntax state (see above) + + -- current_syntax : the syntax we're currently in. + -- subsyntax_info : info about the delimiters of this subsyntax. + -- current_pattern_idx: the index of the pattern we're on for this syntax. + -- current_level : how many subsyntaxes deep we are. + local current_syntax, subsyntax_info, current_pattern_idx, current_level = retrieve_syntax_state(incoming_syntax, state) + + -- Should be used to set the state variable. Don't modify it directly. + local function set_subsyntax_pattern_idx(pattern_idx) + current_pattern_idx = pattern_idx + state = bit32.replace(state, pattern_idx, current_level*8, 8) + end + + + local function push_subsyntax(entering_syntax, pattern_idx) + set_subsyntax_pattern_idx(pattern_idx) + current_level = current_level + 1 + subsyntax_info = entering_syntax + current_syntax = type(entering_syntax.syntax) == "table" and + entering_syntax.syntax or syntax.get(entering_syntax.syntax) + current_pattern_idx = 0 + end + + local function pop_subsyntax() + set_subsyntax_pattern_idx(0) + current_level = current_level - 1 + 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_state > 0 then - local p = current_syntax.patterns[current_state] - local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3]) - + if current_pattern_idx > 0 then + local p = current_syntax.patterns[current_pattern_idx] + 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. + -- 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 + -- (as it's the syntax delimiter). if ss and (s == nil or ss < s) then - push_token(res, p.type, text:sub(i, ss - 1)) + push_token(res, p.type, text:sub(i, ss - 1)) i = ss cont = false end end + -- If we don't have any concerns about syntax delimiters, + -- continue on as normal. if cont then if s then push_token(res, p.type, text:sub(i, e)) - current_state = 0 - state = bit32.replace(state, 0, current_level*8, 8) + set_subsyntax_pattern_idx(0) i = e + 1 else push_token(res, p.type, text:sub(i)) @@ -115,21 +193,15 @@ function tokenizer.tokenize(incoming_syntax, text, state) end end end - -- Check for end of syntax. + -- General end of syntax check. Applies in the case where + -- 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)) - current_level = current_level - 1 - -- Zero out the state above us, as well as our new current state. - state = bit32.replace(state, 0, current_level*8, 16) - current_syntax, subsyntax_info, current_state, current_level = - retrieve_syntax_state(incoming_syntax, state) + -- On finding unescaped delimiter, pop it. + pop_subsyntax() i = e + 1 end end @@ -137,32 +209,21 @@ 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 s, e = text:find("^" .. pattern, i) - - if s then - -- matched pattern; make and add token - local t = text:sub(s, e) - - push_token(res, current_syntax.symbols[t] or p.type, t) + 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 - state = bit32.replace(state, n, current_level*8, 8) - -- If we've found a new subsyntax, bump our level, and set the - -- appropriate variables. + if type(p.pattern or p.regex) == "table" then + -- If we have a subsyntax, push that onto the subsyntax stack. if p.syntax then - current_level = current_level + 1 - subsyntax_info = p - current_syntax = type(p.syntax) == "table" and - p.syntax or syntax.get(p.syntax) - current_state = 0 - else - current_state = n + push_subsyntax(p, n) + else + set_subsyntax_pattern_idx(n) end end - -- move cursor past this token - i = e + 1 + i = find_results[2] + 1 matched = true break end diff --git a/data/fonts/FiraSans-Regular.ttf b/data/fonts/FiraSans-Regular.ttf new file mode 100644 index 00000000..6b288649 Binary files /dev/null and b/data/fonts/FiraSans-Regular.ttf differ diff --git a/data/fonts/JetBrainsMono-Regular.ttf b/data/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..5e3aa31d Binary files /dev/null and b/data/fonts/JetBrainsMono-Regular.ttf differ diff --git a/data/fonts/font.ttf b/data/fonts/font.ttf deleted file mode 100644 index 2b6392ff..00000000 Binary files a/data/fonts/font.ttf and /dev/null differ diff --git a/data/fonts/icons.ttf b/data/fonts/icons.ttf index 00b4cc3b..43ab3767 100644 Binary files a/data/fonts/icons.ttf and b/data/fonts/icons.ttf differ diff --git a/data/fonts/monospace.ttf b/data/fonts/monospace.ttf deleted file mode 100644 index 5919b5d1..00000000 Binary files a/data/fonts/monospace.ttf and /dev/null differ diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua new file mode 100644 index 00000000..4de46080 --- /dev/null +++ b/data/plugins/contextmenu.lua @@ -0,0 +1,272 @@ +-- 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 + 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 +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. + 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 + +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/detectindent.lua b/data/plugins/detectindent.lua index 681dce26..9e7ed93c 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -76,7 +76,7 @@ local function get_non_empty_lines(syntax, lines) end -local auto_detect_max_lines = 200 +local auto_detect_max_lines = 100 local function detect_indent_stat(doc) local stat = {} @@ -99,22 +99,11 @@ local function detect_indent_stat(doc) end -local doc_on_text_change = Doc.on_text_change -local adjust_threshold = 4 - local function update_cache(doc) local type, size, score = detect_indent_stat(doc) - cache[doc] = { type = type, size = size, confirmed = (score >= adjust_threshold) } + local score_threshold = 4 + cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) } doc.indent_info = cache[doc] - if score < adjust_threshold and doc_on_text_change then - Doc.on_text_change = function(self, ...) - doc_on_text_change(self, ...) - update_cache(self) - end - elseif score >= adjust_threshold and doc_on_text_change then - Doc.on_text_change = doc_on_text_change - doc_on_text_change = nil - end end @@ -122,6 +111,14 @@ local new = Doc.new function Doc:new(...) new(self, ...) update_cache(self) + if not cache[self].confirmed then + core.add_thread(function () + while not cache[self].confirmed do + update_cache(self) + coroutine.yield(1) + end + end, self) + end end local clean = Doc.clean diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 1445d067..b311884b 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -5,17 +5,20 @@ syntax.add { files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, comment = "//", patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { "#", "[^\\]\n" }, 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 = "[%a_][%w_]*%f[(]", type = "function" }, - { pattern = "[%a_][%w_]*", type = "symbol" }, + { 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 = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "#include%s()<.->", type = {"keyword", "string"} }, + { pattern = "#[%a_][%w_]*", type = "keyword" }, }, symbols = { ["if"] = "keyword", @@ -29,8 +32,6 @@ syntax.add { ["continue"] = "keyword", ["return"] = "keyword", ["goto"] = "keyword", - ["struct"] = "keyword", - ["union"] = "keyword", ["typedef"] = "keyword", ["enum"] = "keyword", ["extern"] = "keyword", @@ -42,7 +43,6 @@ syntax.add { ["case"] = "keyword", ["default"] = "keyword", ["auto"] = "keyword", - ["const"] = "keyword", ["void"] = "keyword", ["int"] = "keyword2", ["short"] = "keyword2", diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 168ea0ce..5df3d29f 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -6,21 +6,28 @@ syntax.add { headers = "^#!.*[ /]lua", comment = "--", patterns = { - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = { "%[%[", "%]%]" }, type = "string" }, - { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, - { pattern = "%-%-.-\n", type = "comment" }, - { pattern = "-?0x%x+", type = "number" }, - { pattern = "-?%d+[%d%.eE]*", type = "number" }, - { pattern = "-?%.?%d+", type = "number" }, - { pattern = "<%a+>", type = "keyword2" }, - { pattern = "%.%.%.?", type = "operator" }, - { pattern = "[<>~=]=", type = "operator" }, - { pattern = "[%+%-=/%*%^%%#<>]", type = "operator" }, - { pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" }, - { pattern = "[%a_][%w_]*", type = "symbol" }, - { pattern = "::[%a_][%w_]*::", type = "function" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = { "%[%[", "%]%]" }, type = "string" }, + { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, + { pattern = "%-%-.-\n", type = "comment" }, + { pattern = "0x%x+%.%x*[pP][-+]?%d+", type = "number" }, + { pattern = "0x%x+%.%x*", type = "number" }, + { pattern = "0x%.%x+[pP][-+]?%d+", type = "number" }, + { pattern = "0x%.%x+", type = "number" }, + { pattern = "0x%x+[pP][-+]?%d+", type = "number" }, + { pattern = "0x%x+", type = "number" }, + { pattern = "%d%.%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%d%.%d*", type = "number" }, + { pattern = "%.?%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%.?%d+", type = "number" }, + { pattern = "<%a+>", type = "keyword2" }, + { pattern = "%.%.%.?", type = "operator" }, + { pattern = "[<>~=]=", type = "operator" }, + { pattern = "[%+%-=/%*%^%%#<>]", type = "operator" }, + { pattern = "[%a_][%w_]*()%s*%f[(\"'{]", type = {"function", "normal"} }, + { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "::[%a_][%w_]*::", type = "function" }, }, symbols = { ["if"] = "keyword", diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 4c1fca6d..45399ed0 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -30,7 +30,10 @@ local function find_all_matches_in_file(t, filename, fn) for line in fp:lines() do local s = fn(line) if s then - table.insert(t, { file = filename, text = line, line = n, col = s }) + -- Insert maximum 256 characters. If we insert more, for compiled files, which can have very long lines + -- things tend to get sluggish. If our line is longer than 80 characters, begin to truncate the thing. + local start_index = math.max(s - 80, 1) + table.insert(t, { file = filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s }) core.redraw = true end if n % 100 == 0 then coroutine.yield() end @@ -167,12 +170,17 @@ function ResultsView:draw() local ox, oy = self:get_content_offset() local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() - local per = self.last_file_idx / files_number + local per = files_number and self.last_file_idx / files_number or 1 local text if self.searching then - text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", - per * 100, self.last_file_idx, files_number, - #self.results, self.query) + if files_number then + text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", + per * 100, self.last_file_idx, files_number, + #self.results, self.query) + else + text = string.format("Searching (%d files, %d matches) for %q...", + self.last_file_idx, #self.results, self.query) + end else text = string.format("Found %d matches for %q", #self.results, self.query) @@ -229,9 +237,12 @@ command.add(nil, { end) end, - ["project-search:find-pattern"] = function() - core.command_view:enter("Find Pattern In Project", function(text) - begin_search(text, function(line_text) return line_text:find(text) end) + ["project-search:find-regex"] = function() + core.command_view:enter("Find Regex In Project", function(text) + local re = regex.compile(text, "i") + begin_search(text, function(line_text) + return regex.cmatch(re, line_text) + end) end) end, @@ -265,12 +276,38 @@ command.add(ResultsView, { ["project-search:refresh"] = function() core.active_view:refresh() end, + + ["project-search:move-to-previous-page"] = function() + local view = core.active_view + view.scroll.to.y = view.scroll.to.y - view.size.y + end, + + ["project-search:move-to-next-page"] = function() + local view = core.active_view + view.scroll.to.y = view.scroll.to.y + view.size.y + end, + + ["project-search:move-to-start-of-doc"] = function() + local view = core.active_view + view.scroll.to.y = 0 + end, + + ["project-search:move-to-end-of-doc"] = function() + local view = core.active_view + view.scroll.to.y = view:get_scrollable_size() + end }) keymap.add { - ["f5"] = "project-search:refresh", - ["ctrl+shift+f"] = "project-search:find", - ["up"] = "project-search:select-previous", - ["down"] = "project-search:select-next", - ["return"] = "project-search:open-selected", + ["f5"] = "project-search:refresh", + ["ctrl+shift+f"] = "project-search:find", + ["up"] = "project-search:select-previous", + ["down"] = "project-search:select-next", + ["return"] = "project-search:open-selected", + ["pageup"] = "project-search:move-to-previous-page", + ["pagedown"] = "project-search:move-to-next-page", + ["ctrl+home"] = "project-search:move-to-start-of-doc", + ["ctrl+end"] = "project-search:move-to-end-of-doc", + ["home"] = "project-search:move-to-start-of-doc", + ["end"] = "project-search:move-to-end-of-doc" } diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua new file mode 100644 index 00000000..a5f5aaee --- /dev/null +++ b/data/plugins/scale.lua @@ -0,0 +1,113 @@ +-- 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 + + -- we set scale_level in case this was called by user + scale_level = (scale - default_scale) / scale_steps + + 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 function get_scale() + return current_scale +end + +local on_mouse_wheel = RootView.on_mouse_wheel + +function RootView:on_mouse_wheel(d, ...) + if keymap.modkeys["ctrl"] and config.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", +} + +return { + ["set"] = set_scale, + ["get"] = get_scale, + ["increase"] = inc_scale, + ["decrease"] = dec_scale, + ["reset"] = res_scale +} diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 73f4708c..8214bda4 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -93,6 +93,13 @@ function TreeView:get_item_height() end +function TreeView:invalidate_cache(dirname) + for _, v in pairs(self.cache[dirname]) do + v.skip = nil + end +end + + function TreeView:check_cache() -- invalidate cache's skip values if project_files has changed for i = 1, #core.project_directories do @@ -102,9 +109,7 @@ function TreeView:check_cache() self.last[dir.name] = dir.files else if dir.files ~= last_files then - for _, v in pairs(self.cache[dir.name]) do - v.skip = nil - end + self:invalidate_cache(dir.name) self.last[dir.name] = dir.files end end @@ -208,17 +213,34 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) if caught then return end - if not self.hovered_item then + local hovered_item = self.hovered_item + if not hovered_item then return - elseif self.hovered_item.type == "dir" then + elseif hovered_item.type == "dir" then if keymap.modkeys["ctrl"] and button == "left" then - create_directory_in(self.hovered_item) + create_directory_in(hovered_item) else - self.hovered_item.expanded = not self.hovered_item.expanded + if core.project_files_limit and not hovered_item.expanded then + local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename + local index = 0 + -- The loop below is used to find the first match starting from the end + -- in case there are multiple matches. + while index and index + #filename < #abs_filename do + index = string.find(abs_filename, filename, index + 1, true) + end + -- we assume here index is not nil because the abs_filename must contain the + -- relative filename + local dirname = string.sub(abs_filename, 1, index - 2) + if core.is_project_folder(dirname) then + core.scan_project_folder(dirname, filename) + self:invalidate_cache(dirname) + end + end + hovered_item.expanded = not hovered_item.expanded end else core.try(function() - local doc_filename = common.relative_path(core.project_dir, self.hovered_item.abs_filename) + local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) core.root_view:open_doc(core.open_doc(doc_filename)) end) end diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 25c7f672..9c1e20c8 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) @@ -11,7 +12,7 @@ local function workspace_files_for(project_dir) if not info_wsdir then local ok, err = system.mkdir(workspace_dir) if not ok then - error("cannot create workspace directory: %s", err) + error("cannot create workspace directory: \"" .. err .. "\"") end end return coroutine.wrap(function() @@ -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/data/user/init.lua b/data/user/init.lua deleted file mode 100644 index a1c6ddd3..00000000 --- a/data/user/init.lua +++ /dev/null @@ -1,50 +0,0 @@ --- put user settings here --- this module will be loaded after everything else when the application starts --- it will be automatically reloaded when saved - -local core = require "core" -local keymap = require "core.keymap" -local config = require "core.config" -local style = require "core.style" - ------------------------------- Themes ---------------------------------------- - --- light theme: --- core.reload_module("colors.summer") - ---------------------------- Key bindings ------------------------------------- - --- key binding: --- keymap.add { ["ctrl+escape"] = "core:quit" } - - -------------------------------- Fonts ---------------------------------------- - --- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 13 * SCALE) --- style.code_font = renderer.font.load(DATADIR .. "/fonts/monospace.ttf", 12 * SCALE) --- --- font names used by lite: --- style.font : user interface --- style.big_font : big text in welcome screen --- style.icon_font : icons --- style.icon_big_font : toolbar icons --- style.code_font : code --- --- the function to load the font accept a 3rd optional argument like: --- --- {antialiasing="grayscale", hinting="full"} --- --- possible values are: --- antialiasing: grayscale, subpixel --- hinting: none, slight, full - ------------------------------- Plugins ---------------------------------------- - --- enable or disable plugin loading setting config entries: - --- enable trimwhitespace, otherwise it is disable by default: --- config.trimwhitespace = true --- --- disable detectindent, otherwise it is enabled by default --- config.detectindent = false diff --git a/dev-utils/Info.plist b/dev-utils/Info.plist index 75591560..cc369cd0 100644 --- a/dev-utils/Info.plist +++ b/dev-utils/Info.plist @@ -15,9 +15,12 @@ NSHighResolutionCapable MinimumOSVersion10.13 + NSDocumentsFolderUsageDescriptionTo access, edit and index your projects. + NSDesktopFolderUsageDescriptionTo access, edit and index your projects. + NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. CFBundleShortVersionString - 1.16.5 + 1.16.10 NSHumanReadableCopyright - © 2019-2021 rxi franko + © 2019-2021 Francesco Abbate diff --git a/dev-utils/fontello-config-small.json b/dev-utils/fontello-config-small.json deleted file mode 100644 index 895de303..00000000 --- a/dev-utils/fontello-config-small.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "name": "icons", - "css_prefix_text": "icon-", - "css_use_suffix": false, - "hinting": true, - "units_per_em": 1000, - "ascent": 850, - "glyphs": [ - { - "uid": "9dd9e835aebe1060ba7190ad2b2ed951", - "css": "search-1", - "code": 76, - "src": "fontawesome" - }, - { - "uid": "c76b7947c957c9b78b11741173c8349b", - "css": "attention-1", - "code": 33, - "src": "fontawesome" - }, - { - "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", - "css": "doc-1", - "code": 102, - "src": "fontawesome" - }, - { - "uid": "f8aa663c489bcbd6e68ec8147dca841e", - "css": "folder-1", - "code": 100, - "src": "fontawesome" - }, - { - "uid": "c95735c17a10af81448c7fed98a04546", - "css": "folder-open-1", - "code": 68, - "src": "fontawesome" - }, - { - "uid": "e99461abfef3923546da8d745372c995", - "css": "cog", - "code": 80, - "src": "fontawesome" - }, - { - "uid": "7bf14281af5633a597f85b061ef1cfb9", - "css": "angle-right", - "code": 43, - "src": "fontawesome" - }, - { - "uid": "e4dde1992f787163e2e2b534b8c8067d", - "css": "angle-down", - "code": 45, - "src": "fontawesome" - }, - { - "uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7", - "css": "chart-line", - "code": 103, - "src": "fontawesome" - }, - { - "uid": "f4445feb55521283572ee88bc304f928", - "css": "floppy", - "code": 83, - "src": "fontawesome" - }, - { - "uid": "9755f76110ae4d12ac5f9466c9152031", - "css": "book", - "code": 66, - "src": "fontawesome" - }, - { - "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", - "css": "info-circled-1", - "code": 105, - "src": "fontawesome" - }, - { - "uid": "5211af474d3a9848f67f945e2ccaf143", - "css": "cancel-1", - "code": 67, - "src": "fontawesome" - }, - { - "uid": "04f022b8bd044d4ccfffd3887ff72088", - "css": "window-minimize", - "code": 95, - "src": "fontawesome" - }, - { - "uid": "d0e62145dbf40f30e47b3819b8b43a8f", - "css": "window-restore", - "code": 119, - "src": "fontawesome" - }, - { - "uid": "7394501fc0b17cb7bda99538f92e26d6", - "css": "window-close", - "code": 88, - "src": "fontawesome" - }, - { - "uid": "559647a6f430b3aeadbecd67194451dd", - "css": "menu-1", - "code": 77, - "src": "fontawesome" - }, - { - "uid": "07f0832c07f3d9713fffb06c8bffa027", - "css": "window-maximize", - "code": 87, - "src": "fontawesome" - } - ] -} \ No newline at end of file diff --git a/dev-utils/fontello-config.json b/dev-utils/fontello-config.json index 9dd4681d..42334c2f 100644 --- a/dev-utils/fontello-config.json +++ b/dev-utils/fontello-config.json @@ -1,5 +1,5 @@ { - "name": "", + "name": "icons", "css_prefix_text": "icon-", "css_use_suffix": false, "hinting": true, @@ -12,234 +12,18 @@ "code": 76, "src": "fontawesome" }, - { - "uid": "12f4ece88e46abd864e40b35e05b11cd", - "css": "ok-1", - "code": 59402, - "src": "fontawesome" - }, - { - "uid": "43ab845088317bd348dee1d975700c48", - "css": "ok-circled-1", - "code": 59403, - "src": "fontawesome" - }, - { - "uid": "ad33e708f4d2e25c5056c931da1528d6", - "css": "ok-circled2", - "code": 59405, - "src": "fontawesome" - }, - { - "uid": "1400d5103edd2fa6d2d61688fee79a5a", - "css": "ok-squared", - "code": 61770, - "src": "fontawesome" - }, - { - "uid": "5211af474d3a9848f67f945e2ccaf143", - "css": "cancel-1", - "code": 67, - "src": "fontawesome" - }, - { - "uid": "0f4cae16f34ae243a6144c18a003f2d8", - "css": "cancel-circled-1", - "code": 99, - "src": "fontawesome" - }, - { - "uid": "d7271d490b71df4311e32cdacae8b331", - "css": "home-1", - "code": 59407, - "src": "fontawesome" - }, - { - "uid": "3d4ea8a78dc34efe891f3a0f3d961274", - "css": "info", - "code": 61737, - "src": "fontawesome" - }, - { - "uid": "ce3cf091d6ebd004dd0b52d24074e6e3", - "css": "help", - "code": 61736, - "src": "fontawesome" - }, - { - "uid": "17ebadd1e3f274ff0205601eef7b9cc4", - "css": "help-circled-1", - "code": 59408, - "src": "fontawesome" - }, - { - "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", - "css": "info-circled-1", - "code": 59409, - "src": "fontawesome" - }, - { - "uid": "c1f1975c885aa9f3dad7810c53b82074", - "css": "lock", - "code": 59410, - "src": "fontawesome" - }, - { - "uid": "657ab647f6248a6b57a5b893beaf35a9", - "css": "lock-open-1", - "code": 59411, - "src": "fontawesome" - }, - { - "uid": "05376be04a27d5a46e855a233d6e8508", - "css": "lock-open-alt-1", - "code": 61758, - "src": "fontawesome" - }, - { - "uid": "3db5347bd219f3bce6025780f5d9ef45", - "css": "tag", - "code": 59412, - "src": "fontawesome" - }, - { - "uid": "a3f89e106175a5c5c4e9738870b12e55", - "css": "tags", - "code": 59413, - "src": "fontawesome" - }, - { - "uid": "7034e4d22866af82bef811f52fb1ba46", - "css": "code", - "code": 61729, - "src": "fontawesome" - }, - { - "uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6", - "css": "pencil-1", - "code": 59414, - "src": "fontawesome" - }, - { - "uid": "44fae3bfdd54754dc68ec50d37efea37", - "css": "pencil-squared", - "code": 61771, - "src": "fontawesome" - }, - { - "uid": "41087bc74d4b20b55059c60a33bf4008", - "css": "edit", - "code": 59415, - "src": "fontawesome" - }, - { - "uid": "ecb97add13804c190456025e43ec003b", - "css": "keyboard", - "code": 61724, - "src": "fontawesome" - }, { "uid": "c76b7947c957c9b78b11741173c8349b", "css": "attention-1", "code": 33, "src": "fontawesome" }, - { - "uid": "00391fac5d419345ffcccd95b6f76263", - "css": "attention-alt-1", - "code": 61738, - "src": "fontawesome" - }, - { - "uid": "b035c28eba2b35c6ffe92aee8b0df507", - "css": "attention-circled", - "code": 59417, - "src": "fontawesome" - }, - { - "uid": "f48ae54adfb27d8ada53d0fd9e34ee10", - "css": "trash-empty", - "code": 59418, - "src": "fontawesome" - }, { "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", "css": "doc-1", "code": 102, "src": "fontawesome" }, - { - "uid": "c8585e1e5b0467f28b70bce765d5840c", - "css": "docs", - "code": 61637, - "src": "fontawesome" - }, - { - "uid": "5408be43f7c42bccee419c6be53fdef5", - "css": "doc-text", - "code": 61686, - "src": "fontawesome" - }, - { - "uid": "178053298e3e5b03551d754d4b9acd8b", - "css": "doc-inv", - "code": 61787, - "src": "fontawesome" - }, - { - "uid": "c08a1cde48d96cba21d8c05fa7d7feb1", - "css": "doc-text-inv", - "code": 61788, - "src": "fontawesome" - }, - { - "uid": "9daa1fdf0838118518a7e22715e83abc", - "css": "file-pdf", - "code": 61889, - "src": "fontawesome" - }, - { - "uid": "310ffd629da85142bc8669f010556f2d", - "css": "file-word", - "code": 61890, - "src": "fontawesome" - }, - { - "uid": "edcd4022de8d8df266ef7c42d2658ca5", - "css": "file-powerpoint", - "code": 61892, - "src": "fontawesome" - }, - { - "uid": "3c961c1a8d874815856fc6637dc5a13c", - "css": "file-image", - "code": 61893, - "src": "fontawesome" - }, - { - "uid": "e80ae555c1413a4ec18b33fb348b4049", - "css": "file-archive", - "code": 61894, - "src": "fontawesome" - }, - { - "uid": "81db033e704eb7c586a365559d7c0f36", - "css": "file-audio", - "code": 61895, - "src": "fontawesome" - }, - { - "uid": "dd69d9aa589ea7bc0a82a3fe67039f4b", - "css": "file-video", - "code": 61896, - "src": "fontawesome" - }, - { - "uid": "26613a2e6bc41593c54bead46f8c8ee3", - "css": "file-code", - "code": 61897, - "src": "fontawesome" - }, { "uid": "f8aa663c489bcbd6e68ec8147dca841e", "css": "folder-1", @@ -252,58 +36,10 @@ "code": 68, "src": "fontawesome" }, - { - "uid": "b091a8bd0fdade174951f17d936f51e4", - "css": "folder-empty-1", - "code": 61716, - "src": "fontawesome" - }, - { - "uid": "6533bdc16ab201eb3f3b27ce989cab33", - "css": "folder-open-empty-1", - "code": 61717, - "src": "fontawesome" - }, - { - "uid": "559647a6f430b3aeadbecd67194451dd", - "css": "menu-1", - "code": 61641, - "src": "fontawesome" - }, { "uid": "e99461abfef3923546da8d745372c995", "css": "cog", - "code": 59422, - "src": "fontawesome" - }, - { - "uid": "98687378abd1faf8f6af97c254eb6cd6", - "css": "cog-alt", - "code": 59423, - "src": "fontawesome" - }, - { - "uid": "5bb103cd29de77e0e06a52638527b575", - "css": "wrench", - "code": 59424, - "src": "fontawesome" - }, - { - "uid": "0b2b66e526028a6972d51a6f10281b4b", - "css": "zoom-in", - "code": 59425, - "src": "fontawesome" - }, - { - "uid": "d25d10efa900f529ad1d275657cfd30e", - "css": "zoom-out", - "code": 59426, - "src": "fontawesome" - }, - { - "uid": "f3f90c8c89795da30f7444634476ea4f", - "css": "angle-left", - "code": 61700, + "code": 80, "src": "fontawesome" }, { @@ -312,24 +48,12 @@ "code": 43, "src": "fontawesome" }, - { - "uid": "5de9370846a26947e03f63142a3f1c07", - "css": "angle-up", - "code": 61701, - "src": "fontawesome" - }, { "uid": "e4dde1992f787163e2e2b534b8c8067d", "css": "angle-down", "code": 45, "src": "fontawesome" }, - { - "uid": "bbfb51903f40597f0b70fd75bc7b5cac", - "css": "trash", - "code": 61944, - "src": "fontawesome" - }, { "uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7", "css": "chart-line", @@ -342,106 +66,64 @@ "code": 83, "src": "fontawesome" }, - { - "uid": "b429436ec5a518c78479d44ef18dbd60", - "css": "paste", - "code": 61674, - "src": "fontawesome" - }, - { - "uid": "8772331a9fec983cdb5d72902a6f9e0e", - "css": "scissors", - "code": 59428, - "src": "fontawesome" - }, { "uid": "9755f76110ae4d12ac5f9466c9152031", "css": "book", - "code": 59429, + "code": 66, "src": "fontawesome" }, { - "uid": "f9cbf7508cd04145ade2800169959eef", - "css": "font", - "code": 59430, + "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", + "css": "info-circled-1", + "code": 105, "src": "fontawesome" }, { - "uid": "d3b3f17bc3eb7cd809a07bbd4d178bee", - "css": "resize-vertical", - "code": 59431, + "uid": "5211af474d3a9848f67f945e2ccaf143", + "css": "cancel-1", + "code": 67, "src": "fontawesome" }, { - "uid": "3c73d058e4589b65a8d959c0fc8f153d", - "css": "resize-horizontal", - "code": 59432, + "uid": "04f022b8bd044d4ccfffd3887ff72088", + "css": "window-minimize", + "code": 95, "src": "fontawesome" }, { - "uid": "e594fc6e5870b4ab7e49f52571d52577", - "css": "resize-full", - "code": 59433, + "uid": "d0e62145dbf40f30e47b3819b8b43a8f", + "css": "window-restore", + "code": 119, "src": "fontawesome" }, { - "uid": "5278ef7773e948d56c4d442c8c8c98cf", - "css": "lightbulb", - "code": 61675, + "uid": "7394501fc0b17cb7bda99538f92e26d6", + "css": "window-close", + "code": 88, "src": "fontawesome" }, { - "uid": "598a5f2bcf3521d1615de8e1881ccd17", - "css": "clock", - "code": 59434, + "uid": "559647a6f430b3aeadbecd67194451dd", + "css": "menu-1", + "code": 77, "src": "fontawesome" }, { - "uid": "1c4068ed75209e21af36017df8871802", - "css": "down-big", - "code": 59435, + "uid": "07f0832c07f3d9713fffb06c8bffa027", + "css": "window-maximize", + "code": 87, "src": "fontawesome" }, { - "uid": "555ef8c86832e686fef85f7af2eb7cde", - "css": "left-big", - "code": 59436, + "uid": "d870630ff8f81e6de3958ecaeac532f2", + "css": "left-open", + "code": 60, "src": "fontawesome" }, { - "uid": "ad6b3fbb5324abe71a9c0b6609cbb9f1", - "css": "right-big", - "code": 59437, - "src": "fontawesome" - }, - { - "uid": "95376bf082bfec6ce06ea1cda7bd7ead", - "css": "up-big", - "code": 59438, - "src": "fontawesome" - }, - { - "uid": "107ce08c7231097c7447d8f4d059b55f", - "css": "ellipsis", - "code": 61761, - "src": "fontawesome" - }, - { - "uid": "750058837a91edae64b03d60fc7e81a7", - "css": "ellipsis-vert", - "code": 61762, - "src": "fontawesome" - }, - { - "uid": "8fb55fd696d9a0f58f3b27c1d8633750", - "css": "table", - "code": 61646, - "src": "fontawesome" - }, - { - "uid": "53dd31a6cc6438192b2d7b09b1c1dd45", - "css": "columns", - "code": 61659, + "uid": "399ef63b1e23ab1b761dfbb5591fa4da", + "css": "right-open", + "code": 62, "src": "fontawesome" } ] diff --git a/doc/usage.md b/doc/usage.md deleted file mode 100644 index b42917d8..00000000 --- a/doc/usage.md +++ /dev/null @@ -1,200 +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. - -## 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" } -``` - - -## 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/licenses/licenses.md b/licenses/licenses.md new file mode 100644 index 00000000..8005c4a7 --- /dev/null +++ b/licenses/licenses.md @@ -0,0 +1,130 @@ +# Licenses + +## rxi/lite + +Copyright (c) 2020 rxi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Fira Sans + +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +## Fira Code + +Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +## JetBrains Mono + +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +# SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/meson.build b/meson.build index 5b9576d4..b1feef22 100644 --- a/meson.build +++ b/meson.build @@ -9,6 +9,7 @@ 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') if not lua_dep.found() lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false']) @@ -19,8 +20,10 @@ sdl_dep = dependency('sdl2', method: 'config-tool') lite_cargs = [] if get_option('portable') + lite_docdir = 'doc' lite_datadir = 'data' else + lite_docdir = 'share/doc/lite-xl' lite_datadir = 'share/lite-xl' endif @@ -29,10 +32,15 @@ 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' diff --git a/src/api/api.c b/src/api/api.c index 34067a9c..5ea2e782 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -3,11 +3,13 @@ int luaopen_system(lua_State *L); int luaopen_renderer(lua_State *L); +int luaopen_regex(lua_State *L); static const luaL_Reg libs[] = { { "system", luaopen_system }, { "renderer", luaopen_renderer }, + { "regex", luaopen_regex }, { NULL, NULL } }; diff --git a/src/api/regex.c b/src/api/regex.c new file mode 100644 index 00000000..a5d17604 --- /dev/null +++ b/src/api/regex.c @@ -0,0 +1,117 @@ +#include "api.h" + +#define PCRE2_CODE_UNIT_WIDTH 8 + +#include +#include + +static int f_pcre_gc(lua_State* L) { + lua_rawgeti(L, -1, 1); + pcre2_code* re = (pcre2_code*)lua_touserdata(L, -1); + if (re) + pcre2_code_free(re); + return 0; +} + +static int f_pcre_compile(lua_State *L) { + size_t len; + PCRE2_SIZE errorOffset; + int errorNumber; + int pattern = PCRE2_UTF; + const char* str = luaL_checklstring(L, 1, &len); + if (lua_gettop(L) > 1) { + const char* options = luaL_checkstring(L, 2); + if (strstr(options,"i")) + pattern |= PCRE2_CASELESS; + if (strstr(options,"m")) + pattern |= PCRE2_MULTILINE; + if (strstr(options,"s")) + pattern |= PCRE2_DOTALL; + } + pcre2_code* re = pcre2_compile( + (PCRE2_SPTR)str, + len, + pattern, + &errorNumber, + &errorOffset, + NULL + ); + if (re) { + lua_newtable(L); + lua_pushlightuserdata(L, re); + lua_rawseti(L, -2, 1); + luaL_setmetatable(L, "regex"); + return 1; + } + PCRE2_UCHAR buffer[256]; + pcre2_get_error_message(errorNumber, buffer, sizeof(buffer)); + 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 = 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 - 1, opts, md, NULL); + if (rc < 0) { + pcre2_match_data_free(md); + if (rc != PCRE2_ERROR_NOMATCH) + luaL_error(L, "regex matching error %d", rc); + return 0; + } + 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 + 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"); + pcre2_match_data_free(md); + return 0; + } + for (int i = 0; i < rc*2; i++) + lua_pushnumber(L, ovector[i]+1); + pcre2_match_data_free(md); + return rc*2; +} + +static const luaL_Reg lib[] = { + { "compile", f_pcre_compile }, + { "cmatch", f_pcre_match }, + { "__gc", f_pcre_gc }, + { NULL, NULL } +}; + +int luaopen_regex(lua_State *L) { + luaL_newlib(L, lib); + lua_pushliteral(L, "regex"); + 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.c b/src/api/renderer.c index c6883a95..8dc13ada 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -38,13 +38,13 @@ static int f_get_size(lua_State *L) { static int f_begin_frame(lua_State *L) { - rencache_begin_frame(); + rencache_begin_frame(L); return 0; } static int f_end_frame(lua_State *L) { - rencache_end_frame(); + rencache_end_frame(L); return 0; } @@ -90,7 +90,7 @@ static int draw_text_subpixel_impl(lua_State *L, bool draw_subpixel) { replace_color = (RenColor) {0}; } - x_subpixel = rencache_draw_text(font_desc, text, x_subpixel, y, color, draw_subpixel, rep_table, replace_color); + x_subpixel = rencache_draw_text(L, font_desc, 1, text, x_subpixel, y, color, draw_subpixel, rep_table, replace_color); lua_pushnumber(L, x_subpixel); return 1; } diff --git a/src/api/renderer_font.c b/src/api/renderer_font.c index 3d85597b..f510da70 100644 --- a/src/api/renderer_font.c +++ b/src/api/renderer_font.c @@ -1,3 +1,6 @@ +#include +#include + #include "api.h" #include "fontdesc.h" #include "renderer.h" @@ -62,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); - rencache_free_font(self); + font_desc_clear(self); return 0; } @@ -101,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 }, @@ -109,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..4b98509f 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 @@ -194,6 +201,14 @@ top: return 4; case SDL_MOUSEMOTION: + SDL_PumpEvents(); + SDL_Event event_plus; + while (SDL_PeepEvents(&event_plus, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) { + e.motion.x = event_plus.motion.x; + e.motion.y = event_plus.motion.y; + e.motion.xrel += event_plus.motion.xrel; + e.motion.yrel += event_plus.motion.yrel; + } lua_pushstring(L, "mousemoved"); lua_pushnumber(L, e.motion.x); lua_pushnumber(L, e.motion.y); @@ -523,11 +538,11 @@ static int f_fuzzy_match(lua_State *L) { bool files = false; if (lua_gettop(L) > 2 && lua_isboolean(L,3)) files = lua_toboolean(L, 3); - + int score = 0; int run = 0; - - // Match things *backwards*. This allows for better matching on filenames than the above + + // Match things *backwards*. This allows for better matching on filenames than the above // function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh // as the first result, rather than src/renderer.c. Clearly that's wrong. if (files) { 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 ca332dd6..341f820b 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,5 @@ #include +#include #include #include "api/api.h" #include "rencache.h" @@ -62,8 +63,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 @@ -159,11 +165,6 @@ init_lua: enable_momentum_scroll(); #endif - /* We need to clear the rencache commands because we may restarting the application - and it could be non-empty. It is important to clear the command buffer because it - stores pointers to font_desc objects and these are Lua managed. */ - rencache_clear(); - const char *init_lite_code = \ "local core\n" "xpcall(function()\n" diff --git a/src/meson.build b/src/meson.build index 881014be..faa1a8ea 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,6 +3,7 @@ lite_sources = [ 'api/cp_replace.c', 'api/renderer.c', 'api/renderer_font.c', + 'api/regex.c', 'api/system.c', 'renderer.c', 'renwindow.c', @@ -18,7 +19,7 @@ endif executable('lite', lite_sources + lite_rc, include_directories: [lite_include, font_renderer_include], - dependencies: [lua_dep, sdl_dep, libm, libdl, libx11], + dependencies: [lua_dep, sdl_dep, pcre2_dep, libm, libdl, libx11], c_args: lite_cargs, link_with: libfontrenderer, link_args: lite_link_args, diff --git a/src/rencache.c b/src/rencache.c index 1f3e42c2..31165e90 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -1,6 +1,8 @@ #include #include #include + +#include #include "rencache.h" /* a cache over the software renderer -- all drawing operations are stored as @@ -14,7 +16,7 @@ #define COMMAND_BUF_SIZE (1024 * 512) #define COMMAND_BARE_SIZE offsetof(Command, text) -enum { FREE_FONT, SET_CLIP, DRAW_TEXT, DRAW_RECT, DRAW_TEXT_SUBPIXEL }; +enum { SET_CLIP, DRAW_TEXT, DRAW_RECT, DRAW_TEXT_SUBPIXEL }; typedef struct { int8_t type; @@ -30,6 +32,15 @@ typedef struct { char text[0]; } Command; +#define FONT_REFS_MAX 12 +struct FontRef { + FontDesc *font_desc; + int index; +}; +typedef struct FontRef FontRef; +FontRef font_refs[FONT_REFS_MAX]; +int font_refs_len = 0; + static unsigned cells_buf1[CELLS_X * CELLS_Y]; static unsigned cells_buf2[CELLS_X * CELLS_Y]; @@ -45,6 +56,33 @@ static bool show_debug; static inline int min(int a, int b) { return a < b ? a : b; } static inline int max(int a, int b) { return a > b ? a : b; } +static int font_refs_add(lua_State *L, FontDesc *font_desc, int index) { + for (int i = 0; i < font_refs_len; i++) { + if (font_refs[i].font_desc == font_desc) { + return font_refs[i].index; + } + } + + if (font_refs_len >= FONT_REFS_MAX) { + fprintf(stderr, "Warning: (" __FILE__ "): exhausted font reference buffer\n"); + return LUA_NOREF; + } + + lua_pushvalue(L, index); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + font_refs[font_refs_len++] = (FontRef) { font_desc, ref }; + return ref; +} + + +static void font_refs_clear(lua_State *L) { + for (int i = 0; i < font_refs_len; i++) { + luaL_unref(L, LUA_REGISTRYINDEX, font_refs[i].index); + } + font_refs_len = 0; +} + /* 32bit fnv-1a hash */ #define HASH_INITIAL 2166136261 @@ -115,12 +153,6 @@ void rencache_show_debug(bool enable) { } -void rencache_free_font(FontDesc *font_desc) { - Command *cmd = push_command(FREE_FONT, COMMAND_BARE_SIZE); - if (cmd) { cmd->font_desc = font_desc; } -} - - void rencache_set_clip_rect(RenRect rect) { Command *cmd = push_command(SET_CLIP, COMMAND_BARE_SIZE); if (cmd) { cmd->rect = intersect_rects(rect, screen_rect); } @@ -136,7 +168,7 @@ void rencache_draw_rect(RenRect rect, RenColor color) { } } -int rencache_draw_text(FontDesc *font_desc, +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) { @@ -148,7 +180,7 @@ int rencache_draw_text(FontDesc *font_desc, rect.width = ren_font_subpixel_round(w_subpixel, subpixel_scale, 0); rect.height = ren_get_font_height(font_desc); - if (rects_overlap(screen_rect, rect)) { + if (rects_overlap(screen_rect, rect) && font_refs_add(L, font_desc, font_index) >= 0) { int sz = strlen(text) + 1; Command *cmd = push_command(draw_subpixel ? DRAW_TEXT_SUBPIXEL : DRAW_TEXT, COMMAND_BARE_SIZE + sz); if (cmd) { @@ -173,7 +205,7 @@ void rencache_invalidate(void) { } -void rencache_begin_frame(void) { +void rencache_begin_frame(lua_State *L) { /* reset all cells if the screen width/height has changed */ int w, h; ren_get_size(&w, &h); @@ -182,6 +214,7 @@ void rencache_begin_frame(void) { screen_rect.height = h; rencache_invalidate(); } + font_refs_clear(L); } @@ -214,7 +247,7 @@ static void push_rect(RenRect r, int *count) { } -void rencache_end_frame(void) { +void rencache_end_frame(lua_State *L) { /* update cells from commands */ Command *cmd = NULL; RenRect cr = screen_rect; @@ -253,7 +286,6 @@ void rencache_end_frame(void) { } /* redraw updated regions */ - bool has_free_commands = false; for (int i = 0; i < rect_count; i++) { /* draw */ RenRect r = rect_buf[i]; @@ -262,9 +294,6 @@ void rencache_end_frame(void) { cmd = NULL; while (next_command(&cmd)) { switch (cmd->type) { - case FREE_FONT: - has_free_commands = true; - break; case SET_CLIP: ren_set_clip_rect(intersect_rects(cmd->rect, r)); break; @@ -296,16 +325,6 @@ void rencache_end_frame(void) { ren_update_rects(rect_buf, rect_count); } - /* free fonts */ - if (has_free_commands) { - cmd = NULL; - while (next_command(&cmd)) { - if (cmd->type == FREE_FONT) { - font_desc_free(cmd->font_desc); - } - } - } - /* swap cell buffer and reset */ unsigned *tmp = cells; cells = cells_prev; @@ -313,6 +332,3 @@ void rencache_end_frame(void) { command_buf_idx = 0; } -void rencache_clear() { - command_buf_idx = 0; -} diff --git a/src/rencache.h b/src/rencache.h index 987b6855..1d0f45a6 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -2,17 +2,16 @@ #define RENCACHE_H #include +#include #include "renderer.h" void rencache_show_debug(bool enable); -void rencache_free_font(FontDesc *font_desc); void rencache_set_clip_rect(RenRect rect); void rencache_draw_rect(RenRect rect, RenColor color); -int rencache_draw_text(FontDesc *font_desc, const char *text, int x, int y, 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); void rencache_invalidate(void); -void rencache_begin_frame(void); -void rencache_end_frame(void); -void rencache_clear(); +void rencache_begin_frame(lua_State *L); +void rencache_end_frame(lua_State *L); #endif diff --git a/src/renderer.c b/src/renderer.c index b08c25d9..cd269687 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -309,7 +309,8 @@ void ren_draw_rect(RenRect rect, RenColor color) { int dr = surface->w - (x2 - x1); if (color.a == 0xff) { - rect_draw_loop(color); + SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 }; + SDL_FillRect(surface, &rect, SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a)); } else { rect_draw_loop(blend_pixel(*d, color)); }