Merge branch 'lite-xl:master' into master

This commit is contained in:
Cukmekerb 2021-06-17 18:17:01 -07:00 committed by GitHub
commit eed5b79030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1923 additions and 1366 deletions

View File

@ -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 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 this software and associated documentation files (the "Software"), to deal in

156
README.md
View File

@ -1,126 +1,76 @@
# Lite XL # 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 A lightweight text editor written in Lua, adapted from [lite].
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
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. 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. We provide a separate lite-plugins repository for Lite XL, because in some cases
The repository with modified plugins is http://github.com/franko/lite-plugins. 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 ## Overview
Lite XL is derived from lite. It is a lightweight text editor written mostly in Lua — it aims to provide Lite XL is derived from lite.
something practical, pretty, *small* and fast easy to modify and extend, or to use without doing either. 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 ## 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. 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 <arch>
```
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 ## 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 Any additional functionality that can be added through a plugin should be done
[Lite XL plugins repository](https://github.com/franko/lite-plugins). 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. 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 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

View File

@ -3,7 +3,7 @@
cflags+="-Wall -O3 -g -std=gnu11 -fno-strict-aliasing -Isrc -Ilib/font_renderer" cflags+="-Wall -O3 -g -std=gnu11 -fno-strict-aliasing -Isrc -Ilib/font_renderer"
cflags+=" $(pkg-config --cflags lua5.2) $(sdl2-config --cflags)" cflags+=" $(pkg-config --cflags lua5.2) $(sdl2-config --cflags)"
lflags="-static-libgcc -static-libstdc++" 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)" lflags+=" $(pkg-config --libs $package)"
done done
lflags+=" $(sdl2-config --libs) -lm" lflags+=" $(sdl2-config --libs) -lm"

View File

@ -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. 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 ### 1.16.9
Fix a bug related to nested panes resizing. Fix a bug related to nested panes resizing.

View File

@ -59,7 +59,10 @@ end
function command.add_defaults() 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 for _, name in ipairs(reg) do
require("core.commands." .. name) require("core.commands." .. name)
end end

View File

@ -1,13 +1,7 @@
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local CommandView = require "core.commandview"
local function has_commandview() command.add("core.commandview", {
return core.active_view:is(CommandView)
end
command.add(has_commandview, {
["command:submit"] = function() ["command:submit"] = function()
core.active_view:submit() core.active_view:submit()
end, end,

View File

@ -66,6 +66,9 @@ command.add(nil, {
end, end,
["core:find-file"] = function() ["core:find-file"] = function()
if core.project_files_limit then
return command.perform "core:open-file"
end
local files = {} local files = {}
for dir, item in core.get_project_files() do for dir, item in core.get_project_files() do
if item.type == "file" then if item.type == "file" then
@ -88,10 +91,16 @@ command.add(nil, {
["core:open-file"] = function() ["core:open-file"] = function()
local view = core.active_view local view = core.active_view
if view.doc and view.doc.abs_filename then 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 end
core.command_view:enter("Open File", function(text) 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) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text))) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, nil, function(text) end, nil, function(text)

View File

@ -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
})

View File

@ -33,33 +33,6 @@ local function doc_multiline_selection(sort)
return line1, col1, line2, col2, swap return line1, col1, line2, col2, swap
end 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) local function append_line_if_last_line(line)
if line >= #doc().lines then if line >= #doc().lines then
doc():insert(line, math.huge, "\n") doc():insert(line, math.huge, "\n")
@ -74,7 +47,6 @@ local function save(filename)
core.log("Saved \"%s\"", saved_filename) core.log("Saved \"%s\"", saved_filename)
end end
local commands = { local commands = {
["doc:undo"] = function() ["doc:undo"] = function()
doc():undo() doc():undo()
@ -183,17 +155,11 @@ local commands = {
end, end,
["doc:indent"] = function() ["doc:indent"] = function()
local text = get_indent_string() doc():indent_text(false, doc_multiline_selection(true))
if doc():has_selection() then
insert_at_start_of_selected_lines(text)
else
doc():text_input(text)
end
end, end,
["doc:unindent"] = function() ["doc:unindent"] = function()
local text = get_indent_string() doc():indent_text(true, doc_multiline_selection(true))
remove_from_start_of_selected_lines(text)
end, end,
["doc:duplicate-lines"] = function() ["doc:duplicate-lines"] = function()
@ -237,19 +203,31 @@ local commands = {
["doc:toggle-line-comments"] = function() ["doc:toggle-line-comments"] = function()
local comment = doc().syntax.comment local comment = doc().syntax.comment
if not comment then return end if not comment then return end
local indentation = get_indent_string()
local comment_text = comment .. " " local comment_text = comment .. " "
local line1, _, line2 = doc():get_selection(true) local line1, _, line2 = doc_multiline_selection(true)
local uncomment = true local uncomment = true
local start_offset = math.huge
for line = line1, line2 do for line = line1, line2 do
local text = doc().lines[line] 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 uncomment = false
start_offset = math.min(start_offset, s)
end end
end end
if uncomment then for line = line1, line2 do
remove_from_start_of_selected_lines(comment_text, true) local text = doc().lines[line]
else local s = text:find("%S")
insert_at_start_of_selected_lines(comment_text, true) 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
end, end,
@ -297,8 +275,12 @@ local commands = {
end, end,
["doc:save-as"] = function() ["doc:save-as"] = function()
local last_doc = core.last_active_view and core.last_active_view.doc
if doc().filename then if doc().filename then
core.command_view:set_text(doc().filename) 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 end
core.command_view:enter("Save As", function(filename) core.command_view:enter("Save As", function(filename)
save(common.home_expand(filename)) save(common.home_expand(filename))
@ -315,7 +297,7 @@ local commands = {
end end
end, end,
["doc:rename"] = function() ["file:rename"] = function()
local old_filename = doc().filename local old_filename = doc().filename
if not old_filename then if not old_filename then
core.error("Cannot rename unsaved doc") core.error("Cannot rename unsaved doc")
@ -330,6 +312,21 @@ local commands = {
end end
end, common.path_suggest) end, common.path_suggest)
end, 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
} }

View File

@ -1,12 +1,13 @@
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local common = require "core.common"
command.add(nil, { command.add(nil, {
["files:create-directory"] = function() ["files:create-directory"] = function()
core.command_view:enter("New directory name", function(text) 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 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) end)
end, end,

View File

@ -90,6 +90,7 @@ local function has_selection()
and core.active_view.doc:has_selection() and core.active_view.doc:has_selection()
end end
command.add(has_selection, { command.add(has_selection, {
["find-replace:select-next"] = function() ["find-replace:select-next"] = function()
local l1, c1, l2, c2 = doc():get_selection(true) local l1, c1, l2, c2 = doc():get_selection(true)
@ -107,9 +108,9 @@ command.add("core.docview", {
end) end)
end, end,
["find-replace:find-pattern"] = function() ["find-replace:find-regex"] = function()
find("Find Text Pattern", function(doc, line, col, text) find("Find Text Regex", function(doc, line, col, text)
local opt = { wrap = true, no_case = true, pattern = true } local opt = { wrap = true, no_case = true, regex = true }
return search.find(doc, line, col, text, opt) return search.find(doc, line, col, text, opt)
end) end)
end, end,
@ -144,9 +145,10 @@ command.add("core.docview", {
end) end)
end, end,
["find-replace:replace-pattern"] = function() ["find-replace:replace-regex"] = function()
replace("Pattern", "", function(text, old, new) replace("Regex", "", function(text, old, new)
return text:gsub(old, new) local re = regex.compile(old)
return regex.gsub(re, text, new)
end) end)
end, end,

View File

@ -22,6 +22,13 @@ function common.round(n)
end 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) function common.lerp(a, b, t)
if type(a) ~= "table" then if type(a) ~= "table" then
return a + (b - a) * t return a + (b - a) * t
@ -206,7 +213,9 @@ end
function common.home_encode(text) function common.home_encode(text)
if HOME and string.find(text, HOME, 1, true) == 1 then if HOME and string.find(text, HOME, 1, true) == 1 then
local dir_pos = #HOME + 1 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) return "~" .. text:sub(dir_pos)
end end
end end
@ -271,4 +280,26 @@ function common.relative_path(ref_dir, dir)
end 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 return common

View File

@ -3,7 +3,7 @@ local config = {}
config.project_scan_rate = 5 config.project_scan_rate = 5
config.fps = 60 config.fps = 60
config.max_log_items = 80 config.max_log_items = 80
config.message_timeout = 3 config.message_timeout = 5
config.mouse_wheel_scroll = 50 * SCALE config.mouse_wheel_scroll = 50 * SCALE
config.file_size_limit = 10 config.file_size_limit = 10
config.ignore_files = "^%." config.ignore_files = "^%."
@ -11,6 +11,7 @@ config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3 config.undo_merge_timeout = 0.3
config.max_undos = 10000 config.max_undos = 10000
config.max_tabs = 10
config.highlight_current_line = true config.highlight_current_line = true
config.line_height = 1.2 config.line_height = 1.2
config.indent_size = 2 config.indent_size = 2

View File

@ -405,6 +405,65 @@ function Doc:select_to(...)
self:set_selection(line, col, line2, col2) self:set_selection(line, col, line2, col2)
end 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 -- For plugins to add custom actions of document change
function Doc:on_text_change(type) function Doc:on_text_change(type)
end end

View File

@ -15,12 +15,8 @@ local function init_args(doc, line, col, text, opt)
opt = opt or default_opt opt = opt or default_opt
line, col = doc:sanitize_position(line, col) line, col = doc:sanitize_position(line, col)
if opt.no_case then if opt.no_case and not opt.regex then
if opt.pattern then text = text:lower()
text = text:gsub("%%?.", pattern_lower)
else
text = text:lower()
end
end end
return doc, line, col, text, opt return doc, line, col, text, opt
@ -30,20 +26,32 @@ end
function search.find(doc, line, col, text, opt) function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(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 for line = line, #doc.lines do
local line_text = doc.lines[line] local line_text = doc.lines[line]
if opt.no_case then if opt.regex then
line_text = line_text:lower() 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 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 end
if opt.wrap then 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) return search.find(doc, 1, 1, text, opt)
end end
end end

View File

@ -141,29 +141,45 @@ end
function DocView:get_col_x_offset(line, col) function DocView:get_col_x_offset(line, col)
local text = self.doc.lines[line] local default_font = self:get_font()
if not text then return 0 end local column = 1
return self:get_font():get_width(text:sub(1, col - 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 end
function DocView:get_x_offset_col(line, x) 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 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 local x_subpixel = subpixel_scale * x + subpixel_scale / 2
for char in common.utf8_chars(text) do for _, type, text in self.doc.highlighter:each_token(line) do
local w = self:get_font():get_width_subpixel(char) local font = style.syntax_fonts[type] or default_font
if xoffset >= subpixel_scale * x then for char in common.utf8_chars(text) do
return (xoffset - x_subpixel > w / 2) and last_i or i 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 end
xoffset = xoffset + w
last_i = i
i = i + #char
end end
return #text return #line_text
end end
@ -308,11 +324,12 @@ end
function DocView:draw_line_text(idx, x, y) function DocView:draw_line_text(idx, x, y)
local font = self:get_font() local default_font = self:get_font()
local subpixel_scale = font:subpixel_scale() local subpixel_scale = default_font:subpixel_scale()
local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset() local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(idx) do for _, type, text in self.doc.highlighter:each_token(idx) do
local color = style.syntax[type] local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font
if config.draw_whitespace then if config.draw_whitespace then
tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment) tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment)
else else

View File

@ -1,4 +1,5 @@
require "core.strict" require "core.strict"
require "core.regex"
local common = require "core.common" local common = require "core.common"
local config = require "core.config" local config = require "core.config"
local style = require "core.style" local style = require "core.style"
@ -17,7 +18,7 @@ local core = {}
local function load_session() local function load_session()
local ok, t = pcall(dofile, USERDIR .. "/session.lua") local ok, t = pcall(dofile, USERDIR .. "/session.lua")
if ok then if ok then
return t.recents, t.window return t.recents, t.window, t.window_mode
end end
return {} return {}
end end
@ -28,6 +29,7 @@ local function save_session()
if fp then if fp then
fp:write("return {recents=", common.serialize(core.recent_projects), fp:write("return {recents=", common.serialize(core.recent_projects),
", window=", common.serialize(table.pack(system.get_window_size())), ", window=", common.serialize(table.pack(system.get_window_size())),
", window_mode=", common.serialize(system.get_window_mode()),
"}\n") "}\n")
fp:close() fp:close()
end end
@ -72,6 +74,7 @@ function core.set_project_dir(new_dir, change_project_fn)
core.project_directories = {} core.project_directories = {}
core.add_project_directory(new_dir) core.add_project_directory(new_dir)
core.project_files = {} core.project_files = {}
core.project_files_limit = false
core.reschedule_project_scan() core.reschedule_project_scan()
return true return true
end end
@ -99,6 +102,57 @@ local function strip_trailing_slash(filename)
return filename return filename
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_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 project_scan_thread()
local function diff_files(a, b) local function diff_files(a, b)
if #a ~= #b then return true end if #a ~= #b then return true end
@ -110,71 +164,21 @@ local function project_scan_thread()
end end
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 while true do
-- get project files and replace previous table if the new table is -- get project files and replace previous table if the new table is
-- different -- 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 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 diff_files(dir.files, t) then
if entries_count > config.max_project_files then if entries_count > config.max_project_files then
core.project_files_limit = true
core.status_view:show_message("!", style.accent, core.status_view:show_message("!", style.accent,
"Too many files in project directory: stopping reading at ".. "Too many files in project directory: stopped reading at "..
config.max_project_files.." files according to config.max_project_files. ".. config.max_project_files.." files. For more information see "..
"Either tweak this variable, or ignore certain files/directories by ".. "usage.md at github.com/franko/lite-xl."
"using the config.ignore_files variable in your user plugin or ".. )
"project config.")
end end
dir.files = t dir.files = t
core.redraw = true core.redraw = true
@ -182,6 +186,7 @@ local function project_scan_thread()
if dir.name == core.project_dir then if dir.name == core.project_dir then
core.project_files = dir.files core.project_files = dir.files
end end
i = i + 1
end end
-- wait for next scan -- wait for next scan
@ -190,6 +195,56 @@ local function project_scan_thread()
end end
function core.is_project_folder(dirname)
for _, dir in ipairs(core.project_directories) do
if dir.name == dirname then
return true
end
end
return false
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 function project_files_iter(state)
local dir = core.project_directories[state.dir_index] local dir = core.project_directories[state.dir_index]
state.file_index = state.file_index + 1 state.file_index = state.file_index + 1
@ -204,42 +259,39 @@ end
function core.get_project_files() function core.get_project_files()
local state = { dir_index = 1, file_index = 0 } if core.project_files_limit then
return project_files_iter, state 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 end
function core.project_files_number() function core.project_files_number()
local n = 0 if not core.project_files_limit then
for i = 1, #core.project_directories do local n = 0
n = n + #core.project_directories[i].files for i = 1, #core.project_directories do
n = n + #core.project_directories[i].files
end
return n
end end
return n
end end
-- create a directory using mkdir but may need to create the parent -- create a directory using mkdir but may need to create the parent
-- directories as well. -- directories as well.
local function create_user_directory() local function create_user_directory()
local dirname_create = USERDIR local success, err = common.mkdirp(USERDIR)
local basedir if not success then
local subdirs = {} error("cannot create directory \"" .. USERDIR .. "\": " .. err)
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
end end
for _, modname in ipairs {'plugins', 'colors', 'fonts'} do for _, modname in ipairs {'plugins', 'colors', 'fonts'} do
local subdirname = dirname_create .. '/' .. modname local subdirname = USERDIR .. PATHSEP .. modname
if not system.mkdir(subdirname) then if not system.mkdir(subdirname) then
error("cannot create directory: \"" .. subdirname .. "\"") error("cannot create directory: \"" .. subdirname .. "\"")
end end
@ -274,8 +326,8 @@ local style = require "core.style"
------------------------------- Fonts ---------------------------------------- ------------------------------- Fonts ----------------------------------------
-- customize fonts: -- customize fonts:
-- style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 13 * SCALE) -- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE)
-- 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)
-- --
-- font names used by lite: -- font names used by lite:
-- style.font : user interface -- style.font : user interface
@ -357,6 +409,20 @@ local function whitespace_replacements()
end 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() function core.init()
command = require "core.command" command = require "core.command"
keymap = require "core.keymap" keymap = require "core.keymap"
@ -375,9 +441,11 @@ function core.init()
end end
do do
local recent_projects, window_position = load_session() local recent_projects, window_position, window_mode = load_session()
if window_position then if window_mode == "normal" then
system.set_window_size(table.unpack(window_position)) system.set_window_size(table.unpack(window_position))
elseif window_mode == "maximized" then
system.set_window_mode("maximized")
end end
core.recent_projects = recent_projects core.recent_projects = recent_projects
end end
@ -497,6 +565,8 @@ function core.init()
if item.text == "Exit" then os.exit(1) end if item.text == "Exit" then os.exit(1) end
end) end)
end end
reload_on_user_module_save()
end end
@ -556,13 +626,19 @@ do
end end
-- DEPRECATED function
core.doc_save_hooks = {} core.doc_save_hooks = {}
function core.add_save_hook(fn) 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 core.doc_save_hooks[#core.doc_save_hooks + 1] = fn
end end
-- DEPRECATED function
function core.on_doc_save(filename) 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 for _, hook in ipairs(core.doc_save_hooks) do
hook(filename) hook(filename)
end end
@ -870,26 +946,18 @@ end
function core.step() function core.step()
-- handle events -- handle events
local did_keymap = false 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 for type, a,b,c,d in system.poll_event do
if type == "mousemoved" then if type == "textinput" and did_keymap 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
did_keymap = false did_keymap = false
elseif type == "mousemoved" then
core.try(core.on_event, type, a, b, c, d)
else else
local _, res = core.try(core.on_event, type, a, b, c, d) local _, res = core.try(core.on_event, type, a, b, c, d)
did_keymap = res or did_keymap did_keymap = res or did_keymap
end end
core.redraw = true core.redraw = true
end 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() local width, height = renderer.get_size()
@ -994,6 +1062,11 @@ function core.blink_reset()
end end
function core.request_cursor(value)
core.cursor_change_req = value
end
function core.on_error(err) function core.on_error(err)
-- write error to file -- write error to file
local fp = io.open(USERDIR .. "/error.txt", "wb") local fp = io.open(USERDIR .. "/error.txt", "wb")
@ -1009,14 +1082,4 @@ function core.on_error(err)
end 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 return core

View File

@ -6,31 +6,31 @@ local function keymap_macos(keymap)
["cmd+n"] = "core:new-doc", ["cmd+n"] = "core:new-doc",
["cmd+shift+c"] = "core:change-project-folder", ["cmd+shift+c"] = "core:change-project-folder",
["cmd+shift+o"] = "core:open-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", ["cmd+ctrl+shift+j"] = "root:split-left",
["alt+shift+l"] = "root:split-right", ["cmd+ctrl+shift+l"] = "root:split-right",
["alt+shift+i"] = "root:split-up", ["cmd+ctrl+shift+i"] = "root:split-up",
["alt+shift+k"] = "root:split-down", ["cmd+ctrl+shift+k"] = "root:split-down",
["alt+j"] = "root:switch-to-left", ["cmd+ctrl+j"] = "root:switch-to-left",
["alt+l"] = "root:switch-to-right", ["cmd+ctrl+l"] = "root:switch-to-right",
["alt+i"] = "root:switch-to-up", ["cmd+ctrl+i"] = "root:switch-to-up",
["alt+k"] = "root:switch-to-down", ["cmd+ctrl+k"] = "root:switch-to-down",
["ctrl+w"] = "root:close", ["ctrl+w"] = "root:close",
["cmd+tab"] = "root:switch-to-next-tab", ["ctrl+tab"] = "root:switch-to-next-tab",
["cmd+shift+tab"] = "root:switch-to-previous-tab", ["ctrl+shift+tab"] = "root:switch-to-previous-tab",
["cmd+pageup"] = "root:move-tab-left", ["cmd+pageup"] = "root:move-tab-left",
["cmd+pagedown"] = "root:move-tab-right", ["cmd+pagedown"] = "root:move-tab-right",
["alt+1"] = "root:switch-to-tab-1", ["cmd+1"] = "root:switch-to-tab-1",
["alt+2"] = "root:switch-to-tab-2", ["cmd+2"] = "root:switch-to-tab-2",
["alt+3"] = "root:switch-to-tab-3", ["cmd+3"] = "root:switch-to-tab-3",
["alt+4"] = "root:switch-to-tab-4", ["cmd+4"] = "root:switch-to-tab-4",
["alt+5"] = "root:switch-to-tab-5", ["cmd+5"] = "root:switch-to-tab-5",
["alt+6"] = "root:switch-to-tab-6", ["cmd+6"] = "root:switch-to-tab-6",
["alt+7"] = "root:switch-to-tab-7", ["cmd+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8", ["cmd+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9", ["cmd+9"] = "root:switch-to-tab-9",
["cmd+f"] = "find-replace:find", ["cmd+f"] = "find-replace:find",
["cmd+r"] = "find-replace:replace", ["cmd+r"] = "find-replace:replace",

View File

@ -170,12 +170,6 @@ function NagView:draw()
end end
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() function NagView:get_message_height()
local h = 0 local h = 0
for str in string.gmatch(self.message, "(.-)\n") do 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 -- self.target_height is the nagview height needed to display the message and
-- the buttons, excluding the top and bottom padding space. -- the buttons, excluding the top and bottom padding space.
self.target_height = math.max(message_height, self:get_buttons_height()) 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 end
self.force_focus = self.message ~= nil self.force_focus = self.message ~= nil
core.set_active_view(self.message ~= nil and self or core.last_active_view) core.set_active_view(self.message ~= nil and self or core.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 if #self.queue > 0 and not self.title then self:next() end
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 return NagView

70
data/core/regex.lua Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local Object = require "core.object" local Object = require "core.object"
@ -59,7 +60,9 @@ function Node:new(type)
end end
self.hovered = {x = -1, y = -1 } self.hovered = {x = -1, y = -1 }
self.hovered_close = 0 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 self.move_towards = View.move_towards
end end
@ -126,36 +129,41 @@ function Node:split(dir, view, locked, resizable)
return self.b return self.b
end end
function Node:remove_view(root, view)
function Node:close_view(root, view) if #self.views > 1 then
local new_active_view = view == self.active_view local idx = self:get_view_idx(view)
local do_close = function() if idx < self.tab_offset then
if #self.views > 1 then self.tab_offset = self.tab_offset - 1
local idx = self:get_view_idx(view) end
table.remove(self.views, idx) table.remove(self.views, idx)
if new_active_view then if self.active_view == view then
self:set_active_view(self.views[idx] or self.views[#self.views]) self:set_active_view(self.views[idx] or self.views[#self.views])
end 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 else
local parent = self:get_parent_node(root) parent:consume(other)
local is_a = (parent.a == self) local p = parent
local other = parent[is_a and "b" or "a"] while p.type ~= "leaf" do
if other:get_locked_size() then p = p[is_a and "a" or "b"]
self.views = {} end
self:add_view(EmptyView()) p:set_active_view(p.active_view)
else if self.is_primary_node then
parent:consume(other) p.is_primary_node = true
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 end
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 end
view:try_close(do_close) view:try_close(do_close)
end end
@ -166,13 +174,13 @@ function Node:close_active_view(root)
end 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(self.type == "leaf", "Tried to add view to non-leaf node")
assert(not self.locked, "Tried to add view to locked node") assert(not self.locked, "Tried to add view to locked node")
if self.views[1] and self.views[1]:is(EmptyView) then if self.views[1] and self.views[1]:is(EmptyView) then
table.remove(self.views) table.remove(self.views)
end end
table.insert(self.views, view) table.insert(self.views, idx or (#self.views + 1), view)
self:set_active_view(view) self:set_active_view(view)
end end
@ -221,6 +229,15 @@ function Node:get_children(t)
end 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) function Node:get_divider_overlapping_point(px, py)
if self.type ~= "leaf" then if self.type ~= "leaf" then
local p = 6 local p = 6
@ -236,11 +253,18 @@ function Node:get_divider_overlapping_point(px, py)
end 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) function Node:get_tab_overlapping_point(px, py)
if #self.views == 1 then return nil end if #self.views == 1 then return nil end
local x, y, w, h = self:get_tab_rect(1) local tabs_number = self:get_visible_tabs_number()
if px >= x and py >= y and px < x + w * #self.views and py < y + h then local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
return math.floor((px - x) / w) + 1 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
end end
@ -252,18 +276,31 @@ local function close_button_location(x, w)
end 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) function Node:tab_hovered_update(px, py)
local tab_index = self:get_tab_overlapping_point(px, py) local tab_index = self:get_tab_overlapping_point(px, py)
self.hovered_tab = tab_index self.hovered_tab = tab_index
self.hovered_close = 0
self.hovered_scroll_button = 0
if tab_index then if tab_index then
local x, y, w, h = self:get_tab_rect(tab_index) local x, y, w, h = self:get_tab_rect(tab_index)
local cx, cw = close_button_location(x, w) local cx, cw = close_button_location(x, w)
if px >= cx and px < cx + cw and py >= y and py < y + h then if px >= cx and px < cx + cw and py >= y and py < y + h then
self.hovered_close = tab_index self.hovered_close = tab_index
return
end end
else
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
end end
self.hovered_close = 0
end end
@ -280,10 +317,20 @@ function Node:get_child_overlapping_point(x, y)
end 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) function Node:get_tab_rect(idx)
local tw = math.min(style.tab_width, (self.size.x - self.tab_margin) / #self.views) local sbw = get_scroll_button_width()
local x_left = self.position.x + tw * (idx - 1) local maxw = self.size.x - 2 * sbw
local x1, x2 = math.floor(x_left), math.floor(x_left + tw) 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 local h = style.font:get_height() + style.padding.y * 2
return x1, self.position.y, x2 - x1, h return x1, self.position.y, x2 - x1, h
end end
@ -386,13 +433,61 @@ function Node:update_layout()
end 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() function Node:update()
if self.type == "leaf" then if self.type == "leaf" then
self:scroll_tabs_to_visible()
for _, view in ipairs(self.views) do for _, view in ipairs(self.views) do
view:update() view:update()
end end
self:tab_hovered_update(self.hovered.x, self.hovered.y) 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 else
self.a:update() self.a:update()
self.b:update() self.b:update()
@ -401,14 +496,27 @@ end
function Node:draw_tabs() 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 ds = style.divider_size
local dots_width = style.font:get_width("") local dots_width = style.font:get_width("")
core.push_clip_rect(x, y, self.size.x, h) 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, self.size.x, h, style.background2)
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider) 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 x, y, w, h = self:get_tab_rect(i)
local text = view:get_name() local text = view:get_name()
local color = style.dim local color = style.dim
@ -630,6 +738,12 @@ function RootView:close_all_docviews()
end 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) function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y)
if div then if div then
@ -637,19 +751,23 @@ function RootView:on_mouse_pressed(button, x, y, clicks)
return return
end end
local node = self.root_node:get_child_overlapping_point(x, y) 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) local idx = node:get_tab_overlapping_point(x, y)
if idx then if idx then
if button == "middle" or node.hovered_close == 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]) node:close_view(self.root_node, node.views[idx])
else else
self.dragged_node = idx self.dragged_node = { node, idx }
node:set_active_view(node.views[idx]) node:set_active_view(node.views[idx])
end end
else else
core.set_active_view(node.active_view) 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
end end
@ -678,7 +796,7 @@ end
function RootView:on_mouse_moved(x, y, dx, dy) function RootView:on_mouse_moved(x, y, dx, dy)
if core.active_view == core.nag_view then 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) core.active_view:on_mouse_moved(x, y, dx, dy)
return return
end 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 node = self.root_node:get_child_overlapping_point(x, y)
local div = self.root_node:get_divider_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) 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") local axis = (div.type == "hsplit" and "x" or "y")
if div.a:is_resizable(axis) and div.b:is_resizable(axis) then 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 end
elseif tab_index then elseif tab_index then
system.set_cursor("arrow") core.request_cursor("arrow")
if self.dragged_node and self.dragged_node ~= tab_index then elseif node then
local tab = node.views[self.dragged_node] core.request_cursor(node.active_view.cursor)
table.remove(node.views, self.dragged_node) end
table.insert(node.views, tab_index, tab) if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index))
self.dragged_node = tab_index and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then
end local tab = self.dragged_node[1].views[self.dragged_node[2]]
else if self.dragged_node[1] ~= node then
system.set_cursor(node.active_view.cursor) 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
end end
@ -751,6 +883,10 @@ function RootView:draw()
local t = table.remove(self.deferred_draws) local t = table.remove(self.deferred_draws)
t.fn(table.unpack(t)) t.fn(table.unpack(t))
end end
if core.cursor_change_req then
system.set_cursor(core.cursor_change_req)
core.cursor_change_req = nil
end
end end

View File

@ -1,6 +1,5 @@
-- this file is used by lite-xl to setup the Lua environment -- this file is used by lite-xl to setup the Lua environment when starting
-- when starting VERSION = "1.16.11"
VERSION = "1.16.9"
MOD_VERSION = "1" MOD_VERSION = "1"
SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE
@ -13,7 +12,7 @@ else
local prefix = EXEDIR:match("^(.+)[/\\]bin$") local prefix = EXEDIR:match("^(.+)[/\\]bin$")
DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data') DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data')
end 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 .. '/?.lua;' .. package.path
package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path

View File

@ -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. -- 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. -- The antialiasing grayscale with full hinting is interesting for crisp font rendering.
style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 13 * SCALE) style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE)
style.big_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 34 * 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_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.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.background = { common.color "#2e2e32" }
style.background2 = { common.color "#252529" } style.background2 = { common.color "#252529" }
@ -57,4 +57,11 @@ style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" } style.syntax["operator"] = { common.color "#93DDFA" }
style.syntax["function"] = { 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 return style

View File

@ -15,51 +15,72 @@ local function push_token(t, type, text)
end end
local function is_escaped(text, idx, esc) local function push_tokens(t, syn, pattern, full_text, find_results)
local byte = esc:byte() if #find_results > 2 then
local count = 0 -- We do some manipulation with find_results so that it's arranged
for i = idx - 1, 1, -1 do -- like this:
if text:byte(i) ~= byte then break end -- { start, end, i_1, i_2, i_3, …, i_last }
count = count + 1 -- Each position spans characters from i_n to ((i_n+1) - 1), to form
end -- consecutive spans of text.
return count % 2 == 1 --
end -- If i_1 is not equal to start, start is automatically inserted at
-- that index.
if find_results[3] ~= find_results[1] then
local function find_non_escaped(text, pattern, offset, esc) table.insert(find_results, 3, find_results[1])
while true do
local s, e = text:find(pattern, offset)
if not s then break end
if esc and is_escaped(text, s, esc) then
offset = e + 1
else
return s, e
end end
-- 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
end end
-- State is a 32-bit number that is four separate bytes, illustrating how many -- 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. -- differnet delimiters we have open, and which subsyntaxes we have active.
-- At most, there are 3 subsyntaxes active at the same time. Beyond that, -- At most, there are 3 subsyntaxes active at the same time. Beyond that,
-- does not support further highlighting. -- 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 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 incoming_syntax, nil, state, 0
if state > 0 and (state > 255 or current_syntax.patterns[state].syntax) then 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 -- 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 we're using. Rather than walking the bytes, and calling into
-- `syntax` each time, we could probably cache this in a single table. -- `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) local target = bit32.extract(state, i*8, 8)
if target ~= 0 then if target ~= 0 then
if current_syntax.patterns[target].syntax then if current_syntax.patterns[target].syntax then
subsyntax_info = current_syntax.patterns[target] 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) subsyntax_info.syntax or syntax.get(subsyntax_info.syntax)
current_state = 0 current_pattern_idx = 0
current_level = i+1 current_level = i+1
else else
current_state = target current_pattern_idx = target
break break
end end
else else
@ -67,7 +88,7 @@ local function retrieve_syntax_state(incoming_syntax, state)
end end
end end
end end
return current_syntax, subsyntax_info, current_state, current_level return current_syntax, subsyntax_info, current_pattern_idx, current_level
end end
function tokenizer.tokenize(incoming_syntax, text, state) function tokenizer.tokenize(incoming_syntax, text, state)
@ -79,35 +100,92 @@ function tokenizer.tokenize(incoming_syntax, text, state)
end end
state = state or 0 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) 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 while i <= #text do
-- continue trying to match the end pattern of a pair if we have a state set -- continue trying to match the end pattern of a pair if we have a state set
if current_state > 0 then if current_pattern_idx > 0 then
local p = current_syntax.patterns[current_state] local p = current_syntax.patterns[current_pattern_idx]
local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3]) local s, e = find_text(text, p, i, false, true)
local cont = true local cont = true
-- If we're in subsyntax mode, always check to see if we end our syntax -- 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 if subsyntax_info then
local ss, se = find_non_escaped( local ss, se = find_text(text, subsyntax_info, i, false, true)
text, -- If we find that we end the subsyntax before the
subsyntax_info.pattern[2], -- delimiter, push the token, and signal we shouldn't
i, -- treat the bit after as a token to be normally parsed
subsyntax_info.pattern[3] -- (as it's the syntax delimiter).
)
if ss and (s == nil or ss < s) then 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 i = ss
cont = false cont = false
end end
end end
-- If we don't have any concerns about syntax delimiters,
-- continue on as normal.
if cont then if cont then
if s then if s then
push_token(res, p.type, text:sub(i, e)) push_token(res, p.type, text:sub(i, e))
current_state = 0 set_subsyntax_pattern_idx(0)
state = bit32.replace(state, 0, current_level*8, 8)
i = e + 1 i = e + 1
else else
push_token(res, p.type, text:sub(i)) push_token(res, p.type, text:sub(i))
@ -115,21 +193,15 @@ function tokenizer.tokenize(incoming_syntax, text, state)
end end
end 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 if subsyntax_info then
local s, e = find_non_escaped( local s, e = find_text(text, subsyntax_info, i, true, true)
text,
"^" .. subsyntax_info.pattern[2],
i,
nil
)
if s then if s then
push_token(res, subsyntax_info.type, text:sub(i, e)) push_token(res, subsyntax_info.type, text:sub(i, e))
current_level = current_level - 1 -- On finding unescaped delimiter, pop it.
-- Zero out the state above us, as well as our new current state. pop_subsyntax()
state = bit32.replace(state, 0, current_level*8, 16)
current_syntax, subsyntax_info, current_state, current_level =
retrieve_syntax_state(incoming_syntax, state)
i = e + 1 i = e + 1
end end
end end
@ -137,32 +209,21 @@ function tokenizer.tokenize(incoming_syntax, text, state)
-- find matching pattern -- find matching pattern
local matched = false local matched = false
for n, p in ipairs(current_syntax.patterns) do for n, p in ipairs(current_syntax.patterns) do
local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern local find_results = { find_text(text, p, i, true, false) }
local s, e = text:find("^" .. pattern, i) if find_results[1] then
-- matched pattern; make and add tokens
if s then push_tokens(res, current_syntax, p, text, find_results)
-- matched pattern; make and add token
local t = text:sub(s, e)
push_token(res, current_syntax.symbols[t] or p.type, t)
-- update state if this was a start|end pattern pair -- update state if this was a start|end pattern pair
if type(p.pattern) == "table" then if type(p.pattern or p.regex) == "table" then
state = bit32.replace(state, n, current_level*8, 8) -- If we have a subsyntax, push that onto the subsyntax stack.
-- If we've found a new subsyntax, bump our level, and set the
-- appropriate variables.
if p.syntax then if p.syntax then
current_level = current_level + 1 push_subsyntax(p, n)
subsyntax_info = p
current_syntax = type(p.syntax) == "table" and
p.syntax or syntax.get(p.syntax)
current_state = 0
else else
current_state = n set_subsyntax_pattern_idx(n)
end end
end end
-- move cursor past this token -- move cursor past this token
i = e + 1 i = find_results[2] + 1
matched = true matched = true
break break
end end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -76,7 +76,7 @@ local function get_non_empty_lines(syntax, lines)
end end
local auto_detect_max_lines = 200 local auto_detect_max_lines = 100
local function detect_indent_stat(doc) local function detect_indent_stat(doc)
local stat = {} local stat = {}
@ -99,22 +99,11 @@ local function detect_indent_stat(doc)
end end
local doc_on_text_change = Doc.on_text_change
local adjust_threshold = 4
local function update_cache(doc) local function update_cache(doc)
local type, size, score = detect_indent_stat(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] 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 end
@ -122,6 +111,14 @@ local new = Doc.new
function Doc:new(...) function Doc:new(...)
new(self, ...) new(self, ...)
update_cache(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 end
local clean = Doc.clean local clean = Doc.clean

View File

@ -5,17 +5,20 @@ syntax.add {
files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" },
comment = "//", comment = "//",
patterns = { patterns = {
{ pattern = "//.-\n", type = "comment" }, { pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { "#", "[^\\]\n" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = "0x%x+", type = "number" },
{ pattern = "-?0x%x+", type = "number" }, { pattern = "%d+[%d%.eE]*f?", type = "number" },
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" }, { pattern = "%.?%d+f?", type = "number" },
{ pattern = "-?%.?%d+f?", type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "[%a_][%w_]*", type = "symbol" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "#include%s()<.->", type = {"keyword", "string"} },
{ pattern = "#[%a_][%w_]*", type = "keyword" },
}, },
symbols = { symbols = {
["if"] = "keyword", ["if"] = "keyword",
@ -29,8 +32,6 @@ syntax.add {
["continue"] = "keyword", ["continue"] = "keyword",
["return"] = "keyword", ["return"] = "keyword",
["goto"] = "keyword", ["goto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["typedef"] = "keyword", ["typedef"] = "keyword",
["enum"] = "keyword", ["enum"] = "keyword",
["extern"] = "keyword", ["extern"] = "keyword",
@ -42,7 +43,6 @@ syntax.add {
["case"] = "keyword", ["case"] = "keyword",
["default"] = "keyword", ["default"] = "keyword",
["auto"] = "keyword", ["auto"] = "keyword",
["const"] = "keyword",
["void"] = "keyword", ["void"] = "keyword",
["int"] = "keyword2", ["int"] = "keyword2",
["short"] = "keyword2", ["short"] = "keyword2",

View File

@ -6,21 +6,28 @@ syntax.add {
headers = "^#!.*[ /]lua", headers = "^#!.*[ /]lua",
comment = "--", comment = "--",
patterns = { patterns = {
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "%[%[", "%]%]" }, type = "string" }, { pattern = { "%[%[", "%]%]" }, type = "string" },
{ pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" },
{ pattern = "%-%-.-\n", type = "comment" }, { pattern = "%-%-.-\n", type = "comment" },
{ pattern = "-?0x%x+", type = "number" }, { pattern = "0x%x+%.%x*[pP][-+]?%d+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" }, { pattern = "0x%x+%.%x*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" }, { pattern = "0x%.%x+[pP][-+]?%d+", type = "number" },
{ pattern = "<%a+>", type = "keyword2" }, { pattern = "0x%.%x+", type = "number" },
{ pattern = "%.%.%.?", type = "operator" }, { pattern = "0x%x+[pP][-+]?%d+", type = "number" },
{ pattern = "[<>~=]=", type = "operator" }, { pattern = "0x%x+", type = "number" },
{ pattern = "[%+%-=/%*%^%%#<>]", type = "operator" }, { pattern = "%d%.%d*[eE][-+]?%d+", type = "number" },
{ pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" }, { pattern = "%d%.%d*", type = "number" },
{ pattern = "[%a_][%w_]*", type = "symbol" }, { pattern = "%.?%d*[eE][-+]?%d+", type = "number" },
{ pattern = "::[%a_][%w_]*::", type = "function" }, { 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 = { symbols = {
["if"] = "keyword", ["if"] = "keyword",

View File

@ -30,7 +30,10 @@ local function find_all_matches_in_file(t, filename, fn)
for line in fp:lines() do for line in fp:lines() do
local s = fn(line) local s = fn(line)
if s then 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 core.redraw = true
end end
if n % 100 == 0 then coroutine.yield() end if n % 100 == 0 then coroutine.yield() end
@ -167,12 +170,17 @@ function ResultsView:draw()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
local x, y = ox + style.padding.x, oy + style.padding.y local x, y = ox + style.padding.x, oy + style.padding.y
local files_number = core.project_files_number() 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 local text
if self.searching then if self.searching then
text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", if files_number then
per * 100, self.last_file_idx, files_number, text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
#self.results, self.query) 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 else
text = string.format("Found %d matches for %q", text = string.format("Found %d matches for %q",
#self.results, self.query) #self.results, self.query)
@ -229,9 +237,12 @@ command.add(nil, {
end) end)
end, end,
["project-search:find-pattern"] = function() ["project-search:find-regex"] = function()
core.command_view:enter("Find Pattern In Project", function(text) core.command_view:enter("Find Regex In Project", function(text)
begin_search(text, function(line_text) return line_text:find(text) end) local re = regex.compile(text, "i")
begin_search(text, function(line_text)
return regex.cmatch(re, line_text)
end)
end) end)
end, end,
@ -265,12 +276,38 @@ command.add(ResultsView, {
["project-search:refresh"] = function() ["project-search:refresh"] = function()
core.active_view:refresh() core.active_view:refresh()
end, 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 { keymap.add {
["f5"] = "project-search:refresh", ["f5"] = "project-search:refresh",
["ctrl+shift+f"] = "project-search:find", ["ctrl+shift+f"] = "project-search:find",
["up"] = "project-search:select-previous", ["up"] = "project-search:select-previous",
["down"] = "project-search:select-next", ["down"] = "project-search:select-next",
["return"] = "project-search:open-selected", ["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"
} }

113
data/plugins/scale.lua Normal file
View File

@ -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
}

View File

@ -93,6 +93,13 @@ function TreeView:get_item_height()
end end
function TreeView:invalidate_cache(dirname)
for _, v in pairs(self.cache[dirname]) do
v.skip = nil
end
end
function TreeView:check_cache() function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed -- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do for i = 1, #core.project_directories do
@ -102,9 +109,7 @@ function TreeView:check_cache()
self.last[dir.name] = dir.files self.last[dir.name] = dir.files
else else
if dir.files ~= last_files then if dir.files ~= last_files then
for _, v in pairs(self.cache[dir.name]) do self:invalidate_cache(dir.name)
v.skip = nil
end
self.last[dir.name] = dir.files self.last[dir.name] = dir.files
end end
end end
@ -208,17 +213,34 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
if caught then if caught then
return return
end end
if not self.hovered_item then local hovered_item = self.hovered_item
if not hovered_item then
return return
elseif self.hovered_item.type == "dir" then elseif hovered_item.type == "dir" then
if keymap.modkeys["ctrl"] and button == "left" then if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(self.hovered_item) create_directory_in(hovered_item)
else 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 end
else else
core.try(function() 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)) core.root_view:open_doc(core.open_doc(doc_filename))
end) end)
end end

View File

@ -2,6 +2,7 @@
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local DocView = require "core.docview" local DocView = require "core.docview"
local LogView = require "core.logview"
local function workspace_files_for(project_dir) local function workspace_files_for(project_dir)
@ -11,7 +12,7 @@ local function workspace_files_for(project_dir)
if not info_wsdir then if not info_wsdir then
local ok, err = system.mkdir(workspace_dir) local ok, err = system.mkdir(workspace_dir)
if not ok then if not ok then
error("cannot create workspace directory: %s", err) error("cannot create workspace directory: \"" .. err .. "\"")
end end
end end
return coroutine.wrap(function() return coroutine.wrap(function()
@ -29,7 +30,7 @@ local function workspace_files_for(project_dir)
end 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 for filename, id in workspace_files_for(project_dir) do
local load_f = loadfile(filename) local load_f = loadfile(filename)
local workspace = load_f and load_f() 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) text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge)
} }
end end
if mt == LogView then return end
for name, mod in pairs(package.loaded) do for name, mod in pairs(package.loaded) do
if mod == mt then if mod == mt then
return { return {
@ -99,16 +101,26 @@ end
local function load_view(t) local function load_view(t)
if t.type == "doc" then if t.type == "doc" then
local ok, doc = pcall(core.open_doc, t.filename) local dv
if not ok then if not t.filename then
return DocView(core.open_doc()) -- 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 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 return dv
end end
return require(t.module)() return require(t.module)()
@ -141,13 +153,19 @@ end
local function load_node(node, t) local function load_node(node, t)
if t.type == "leaf" then if t.type == "leaf" then
local res 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) local view = load_view(v)
if v.active then res = view end if view then
node:add_view(view) if v.active then res = view end
node:add_view(view)
if t.active_view == i then
active_view = view
end
end
end end
if t.active_view then if active_view then
node:set_active_view(node.views[t.active_view]) node:set_active_view(active_view)
end end
return res return res
else else
@ -184,7 +202,7 @@ end
local function load_workspace() local function load_workspace()
local workspace = load_workspace_file(core.project_dir) local workspace = consume_workspace_file(core.project_dir)
if workspace then if workspace then
local root = get_unlocked_root(core.root_view.root_node) local root = get_unlocked_root(core.root_view.root_node)
local active_view = load_node(root, workspace.documents) local active_view = load_node(root, workspace.documents)

View File

@ -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

View File

@ -15,9 +15,12 @@
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>MinimumOSVersion</key><string>10.13</string> <key>MinimumOSVersion</key><string>10.13</string>
<key>NSDocumentsFolderUsageDescription</key><string>To access, edit and index your projects.</string>
<key>NSDesktopFolderUsageDescription</key><string>To access, edit and index your projects.</string>
<key>NSDownloadsFolderUsageDescription</key><string>To access, edit and index your projects.</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.16.5</string> <string>1.16.10</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© 2019-2021 rxi franko</string> <string>© 2019-2021 Francesco Abbate</string>
</dict> </dict>
</plist> </plist>

View File

@ -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"
}
]
}

View File

@ -1,5 +1,5 @@
{ {
"name": "", "name": "icons",
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"css_use_suffix": false, "css_use_suffix": false,
"hinting": true, "hinting": true,
@ -12,234 +12,18 @@
"code": 76, "code": 76,
"src": "fontawesome" "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", "uid": "c76b7947c957c9b78b11741173c8349b",
"css": "attention-1", "css": "attention-1",
"code": 33, "code": 33,
"src": "fontawesome" "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", "uid": "1b5a5d7b7e3c71437f5a26befdd045ed",
"css": "doc-1", "css": "doc-1",
"code": 102, "code": 102,
"src": "fontawesome" "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", "uid": "f8aa663c489bcbd6e68ec8147dca841e",
"css": "folder-1", "css": "folder-1",
@ -252,58 +36,10 @@
"code": 68, "code": 68,
"src": "fontawesome" "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", "uid": "e99461abfef3923546da8d745372c995",
"css": "cog", "css": "cog",
"code": 59422, "code": 80,
"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,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
@ -312,24 +48,12 @@
"code": 43, "code": 43,
"src": "fontawesome" "src": "fontawesome"
}, },
{
"uid": "5de9370846a26947e03f63142a3f1c07",
"css": "angle-up",
"code": 61701,
"src": "fontawesome"
},
{ {
"uid": "e4dde1992f787163e2e2b534b8c8067d", "uid": "e4dde1992f787163e2e2b534b8c8067d",
"css": "angle-down", "css": "angle-down",
"code": 45, "code": 45,
"src": "fontawesome" "src": "fontawesome"
}, },
{
"uid": "bbfb51903f40597f0b70fd75bc7b5cac",
"css": "trash",
"code": 61944,
"src": "fontawesome"
},
{ {
"uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7", "uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7",
"css": "chart-line", "css": "chart-line",
@ -342,106 +66,64 @@
"code": 83, "code": 83,
"src": "fontawesome" "src": "fontawesome"
}, },
{
"uid": "b429436ec5a518c78479d44ef18dbd60",
"css": "paste",
"code": 61674,
"src": "fontawesome"
},
{
"uid": "8772331a9fec983cdb5d72902a6f9e0e",
"css": "scissors",
"code": 59428,
"src": "fontawesome"
},
{ {
"uid": "9755f76110ae4d12ac5f9466c9152031", "uid": "9755f76110ae4d12ac5f9466c9152031",
"css": "book", "css": "book",
"code": 59429, "code": 66,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "f9cbf7508cd04145ade2800169959eef", "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2",
"css": "font", "css": "info-circled-1",
"code": 59430, "code": 105,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "d3b3f17bc3eb7cd809a07bbd4d178bee", "uid": "5211af474d3a9848f67f945e2ccaf143",
"css": "resize-vertical", "css": "cancel-1",
"code": 59431, "code": 67,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "3c73d058e4589b65a8d959c0fc8f153d", "uid": "04f022b8bd044d4ccfffd3887ff72088",
"css": "resize-horizontal", "css": "window-minimize",
"code": 59432, "code": 95,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "e594fc6e5870b4ab7e49f52571d52577", "uid": "d0e62145dbf40f30e47b3819b8b43a8f",
"css": "resize-full", "css": "window-restore",
"code": 59433, "code": 119,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "5278ef7773e948d56c4d442c8c8c98cf", "uid": "7394501fc0b17cb7bda99538f92e26d6",
"css": "lightbulb", "css": "window-close",
"code": 61675, "code": 88,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "598a5f2bcf3521d1615de8e1881ccd17", "uid": "559647a6f430b3aeadbecd67194451dd",
"css": "clock", "css": "menu-1",
"code": 59434, "code": 77,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "1c4068ed75209e21af36017df8871802", "uid": "07f0832c07f3d9713fffb06c8bffa027",
"css": "down-big", "css": "window-maximize",
"code": 59435, "code": 87,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "555ef8c86832e686fef85f7af2eb7cde", "uid": "d870630ff8f81e6de3958ecaeac532f2",
"css": "left-big", "css": "left-open",
"code": 59436, "code": 60,
"src": "fontawesome" "src": "fontawesome"
}, },
{ {
"uid": "ad6b3fbb5324abe71a9c0b6609cbb9f1", "uid": "399ef63b1e23ab1b761dfbb5591fa4da",
"css": "right-big", "css": "right-open",
"code": 59437, "code": 62,
"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,
"src": "fontawesome" "src": "fontawesome"
} }
] ]

View File

@ -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.

130
licenses/licenses.md Normal file
View File

@ -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.

View File

@ -9,6 +9,7 @@ libm = cc.find_library('m', required : false)
libdl = cc.find_library('dl', required : false) libdl = cc.find_library('dl', required : false)
libx11 = dependency('x11', required : false) libx11 = dependency('x11', required : false)
lua_dep = dependency('lua5.2', required : false) lua_dep = dependency('lua5.2', required : false)
pcre2_dep = dependency('libpcre2-8')
if not lua_dep.found() if not lua_dep.found()
lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false']) lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false'])
@ -19,8 +20,10 @@ sdl_dep = dependency('sdl2', method: 'config-tool')
lite_cargs = [] lite_cargs = []
if get_option('portable') if get_option('portable')
lite_docdir = 'doc'
lite_datadir = 'data' lite_datadir = 'data'
else else
lite_docdir = 'share/doc/lite-xl'
lite_datadir = 'share/lite-xl' lite_datadir = 'share/lite-xl'
endif endif
@ -29,10 +32,15 @@ foreach data_module : ['core', 'fonts', 'plugins', 'colors']
install_subdir('data' / data_module , install_dir : lite_datadir) install_subdir('data' / data_module , install_dir : lite_datadir)
endforeach endforeach
install_data('licenses/licenses.md', install_dir : lite_docdir)
lite_link_args = [] lite_link_args = []
if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' if cc.get_id() == 'gcc' and get_option('buildtype') == 'release'
lite_link_args += ['-static-libgcc', '-static-libstdc++'] lite_link_args += ['-static-libgcc', '-static-libstdc++']
endif endif
if host_machine.system() == 'darwin'
lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation']
endif
lite_rc = [] lite_rc = []
if host_machine.system() == 'windows' if host_machine.system() == 'windows'

View File

@ -3,11 +3,13 @@
int luaopen_system(lua_State *L); int luaopen_system(lua_State *L);
int luaopen_renderer(lua_State *L); int luaopen_renderer(lua_State *L);
int luaopen_regex(lua_State *L);
static const luaL_Reg libs[] = { static const luaL_Reg libs[] = {
{ "system", luaopen_system }, { "system", luaopen_system },
{ "renderer", luaopen_renderer }, { "renderer", luaopen_renderer },
{ "regex", luaopen_regex },
{ NULL, NULL } { NULL, NULL }
}; };

117
src/api/regex.c Normal file
View File

@ -0,0 +1,117 @@
#include "api.h"
#define PCRE2_CODE_UNIT_WIDTH 8
#include <string.h>
#include <pcre2.h>
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;
}

View File

@ -38,13 +38,13 @@ static int f_get_size(lua_State *L) {
static int f_begin_frame(lua_State *L) { static int f_begin_frame(lua_State *L) {
rencache_begin_frame(); rencache_begin_frame(L);
return 0; return 0;
} }
static int f_end_frame(lua_State *L) { static int f_end_frame(lua_State *L) {
rencache_end_frame(); rencache_end_frame(L);
return 0; return 0;
} }
@ -90,7 +90,7 @@ static int draw_text_subpixel_impl(lua_State *L, bool draw_subpixel) {
replace_color = (RenColor) {0}; 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); lua_pushnumber(L, x_subpixel);
return 1; return 1;
} }

View File

@ -1,3 +1,6 @@
#include <lua.h>
#include <lauxlib.h>
#include "api.h" #include "api.h"
#include "fontdesc.h" #include "fontdesc.h"
#include "renderer.h" #include "renderer.h"
@ -62,7 +65,7 @@ static int f_set_tab_size(lua_State *L) {
static int f_gc(lua_State *L) { static int f_gc(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
rencache_free_font(self); font_desc_clear(self);
return 0; 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[] = { static const luaL_Reg lib[] = {
{ "__gc", f_gc }, { "__gc", f_gc },
{ "load", f_load }, { "load", f_load },
@ -109,6 +128,8 @@ static const luaL_Reg lib[] = {
{ "get_width_subpixel", f_get_width_subpixel }, { "get_width_subpixel", f_get_width_subpixel },
{ "get_height", f_get_height }, { "get_height", f_get_height },
{ "subpixel_scale", f_subpixel_scale }, { "subpixel_scale", f_subpixel_scale },
{ "get_size", f_get_size },
{ "set_size", f_set_size },
{ NULL, NULL } { NULL, NULL }
}; };

View File

@ -152,6 +152,14 @@ top:
return 4; return 4;
case SDL_KEYDOWN: 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, "keypressed");
lua_pushstring(L, key_name(buf, e.key.keysym.sym)); lua_pushstring(L, key_name(buf, e.key.keysym.sym));
return 2; return 2;
@ -162,8 +170,7 @@ top:
** we want to flush this event and let the keymapper ** we want to flush this event and let the keymapper
** handle this key combination. ** handle this key combination.
** Thanks to mathewmariani, taken from his lite-macos github repository. */ ** 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); SDL_FlushEvent(SDL_QUIT);
} }
#endif #endif
@ -194,6 +201,14 @@ top:
return 4; return 4;
case SDL_MOUSEMOTION: 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_pushstring(L, "mousemoved");
lua_pushnumber(L, e.motion.x); lua_pushnumber(L, e.motion.x);
lua_pushnumber(L, e.motion.y); lua_pushnumber(L, e.motion.y);

View File

@ -4,8 +4,25 @@
void set_macos_bundle_resources(lua_State *L) void set_macos_bundle_resources(lua_State *L)
{ @autoreleasepool { @autoreleasepool
{ {
NSString* resource_path = [[NSBundle mainBundle] resourcePath]; /* Use resolved executablePath instead of resourcePath to allow lanching
lua_pushstring(L, [resource_path UTF8String]); 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"); lua_setglobal(L, "MACOS_RESOURCES");
}} }}

View File

@ -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. */ 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++) { for (int i = 0; i < font_desc->cache_length; i++) {
ren_free_font(font_desc->cache[i].font); ren_free_font(font_desc->cache[i].font);
} }
font_desc->cache_length = 0; font_desc->cache_length = 0;
font_desc->cache_last_index = 0;
} }
void font_desc_set_tab_size(FontDesc *font_desc, int tab_size) { void font_desc_set_tab_size(FontDesc *font_desc, int tab_size) {

View File

@ -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_alloc_size(const char *filename);
int font_desc_get_tab_size(FontDesc *font_desc); int font_desc_get_tab_size(FontDesc *font_desc);
void font_desc_set_tab_size(FontDesc *font_desc, int tab_size); 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); RenFont *font_desc_get_font_at_scale(FontDesc *font_desc, int scale);
#endif #endif

View File

@ -1,4 +1,5 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <SDL.h> #include <SDL.h>
#include "api/api.h" #include "api/api.h"
#include "rencache.h" #include "rencache.h"
@ -62,8 +63,13 @@ static void get_exe_filename(char *buf, int sz) {
int len = readlink(path, buf, sz - 1); int len = readlink(path, buf, sz - 1);
buf[len] = '\0'; buf[len] = '\0';
#elif __APPLE__ #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; unsigned size = sz;
_NSGetExecutablePath(buf, &size); char exepath[size];
_NSGetExecutablePath(exepath, &size);
realpath(exepath, buf);
#else #else
strcpy(buf, "./lite"); strcpy(buf, "./lite");
#endif #endif
@ -159,11 +165,6 @@ init_lua:
enable_momentum_scroll(); enable_momentum_scroll();
#endif #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 = \ const char *init_lite_code = \
"local core\n" "local core\n"
"xpcall(function()\n" "xpcall(function()\n"

View File

@ -3,6 +3,7 @@ lite_sources = [
'api/cp_replace.c', 'api/cp_replace.c',
'api/renderer.c', 'api/renderer.c',
'api/renderer_font.c', 'api/renderer_font.c',
'api/regex.c',
'api/system.c', 'api/system.c',
'renderer.c', 'renderer.c',
'renwindow.c', 'renwindow.c',
@ -18,7 +19,7 @@ endif
executable('lite', executable('lite',
lite_sources + lite_rc, lite_sources + lite_rc,
include_directories: [lite_include, font_renderer_include], 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, c_args: lite_cargs,
link_with: libfontrenderer, link_with: libfontrenderer,
link_args: lite_link_args, link_args: lite_link_args,

View File

@ -1,6 +1,8 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <lauxlib.h>
#include "rencache.h" #include "rencache.h"
/* a cache over the software renderer -- all drawing operations are stored as /* a cache over the software renderer -- all drawing operations are stored as
@ -14,7 +16,7 @@
#define COMMAND_BUF_SIZE (1024 * 512) #define COMMAND_BUF_SIZE (1024 * 512)
#define COMMAND_BARE_SIZE offsetof(Command, text) #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 { typedef struct {
int8_t type; int8_t type;
@ -30,6 +32,15 @@ typedef struct {
char text[0]; char text[0];
} Command; } 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_buf1[CELLS_X * CELLS_Y];
static unsigned cells_buf2[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 min(int a, int b) { return a < b ? a : b; }
static inline int max(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 */ /* 32bit fnv-1a hash */
#define HASH_INITIAL 2166136261 #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) { void rencache_set_clip_rect(RenRect rect) {
Command *cmd = push_command(SET_CLIP, COMMAND_BARE_SIZE); Command *cmd = push_command(SET_CLIP, COMMAND_BARE_SIZE);
if (cmd) { cmd->rect = intersect_rects(rect, screen_rect); } 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, const char *text, int x, int y, RenColor color, bool draw_subpixel,
CPReplaceTable *replacements, RenColor replace_color) 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.width = ren_font_subpixel_round(w_subpixel, subpixel_scale, 0);
rect.height = ren_get_font_height(font_desc); 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; int sz = strlen(text) + 1;
Command *cmd = push_command(draw_subpixel ? DRAW_TEXT_SUBPIXEL : DRAW_TEXT, COMMAND_BARE_SIZE + sz); Command *cmd = push_command(draw_subpixel ? DRAW_TEXT_SUBPIXEL : DRAW_TEXT, COMMAND_BARE_SIZE + sz);
if (cmd) { 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 */ /* reset all cells if the screen width/height has changed */
int w, h; int w, h;
ren_get_size(&w, &h); ren_get_size(&w, &h);
@ -182,6 +214,7 @@ void rencache_begin_frame(void) {
screen_rect.height = h; screen_rect.height = h;
rencache_invalidate(); 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 */ /* update cells from commands */
Command *cmd = NULL; Command *cmd = NULL;
RenRect cr = screen_rect; RenRect cr = screen_rect;
@ -253,7 +286,6 @@ void rencache_end_frame(void) {
} }
/* redraw updated regions */ /* redraw updated regions */
bool has_free_commands = false;
for (int i = 0; i < rect_count; i++) { for (int i = 0; i < rect_count; i++) {
/* draw */ /* draw */
RenRect r = rect_buf[i]; RenRect r = rect_buf[i];
@ -262,9 +294,6 @@ void rencache_end_frame(void) {
cmd = NULL; cmd = NULL;
while (next_command(&cmd)) { while (next_command(&cmd)) {
switch (cmd->type) { switch (cmd->type) {
case FREE_FONT:
has_free_commands = true;
break;
case SET_CLIP: case SET_CLIP:
ren_set_clip_rect(intersect_rects(cmd->rect, r)); ren_set_clip_rect(intersect_rects(cmd->rect, r));
break; break;
@ -296,16 +325,6 @@ void rencache_end_frame(void) {
ren_update_rects(rect_buf, rect_count); 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 */ /* swap cell buffer and reset */
unsigned *tmp = cells; unsigned *tmp = cells;
cells = cells_prev; cells = cells_prev;
@ -313,6 +332,3 @@ void rencache_end_frame(void) {
command_buf_idx = 0; command_buf_idx = 0;
} }
void rencache_clear() {
command_buf_idx = 0;
}

View File

@ -2,17 +2,16 @@
#define RENCACHE_H #define RENCACHE_H
#include <stdbool.h> #include <stdbool.h>
#include <lua.h>
#include "renderer.h" #include "renderer.h"
void rencache_show_debug(bool enable); void rencache_show_debug(bool enable);
void rencache_free_font(FontDesc *font_desc);
void rencache_set_clip_rect(RenRect rect); void rencache_set_clip_rect(RenRect rect);
void rencache_draw_rect(RenRect rect, RenColor color); 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); bool draw_subpixel, CPReplaceTable *replacements, RenColor replace_color);
void rencache_invalidate(void); void rencache_invalidate(void);
void rencache_begin_frame(void); void rencache_begin_frame(lua_State *L);
void rencache_end_frame(void); void rencache_end_frame(lua_State *L);
void rencache_clear();
#endif #endif

View File

@ -309,7 +309,8 @@ void ren_draw_rect(RenRect rect, RenColor color) {
int dr = surface->w - (x2 - x1); int dr = surface->w - (x2 - x1);
if (color.a == 0xff) { 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 { } else {
rect_draw_loop(blend_pixel(*d, color)); rect_draw_loop(blend_pixel(*d, color));
} }