Merge branch 'master' into plugin_api_h_fix

This commit is contained in:
Adam 2021-12-05 22:11:47 -05:00 committed by GitHub
commit 8120654c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 4092 additions and 634 deletions

35
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,35 @@
"Category: CI":
- .github/workflows/*
"Category: Meta":
- ./*
- .github/*
- .github/ISSUE_TEMPLATE/*
- .github/PULL_REQUEST_TEMPLATE/*
- .gitignore
"Category: Build System":
- meson.build
- meson_options.txt
- subprojects/*
"Category: Documentation":
- docs/**/*
"Category: Resources":
- resources/**/*
"Category: Themes":
- data/colors/*
"Category: Lua Core":
- data/core/**/*
"Category: Fonts":
- data/fonts/*
"Category: Plugins":
- data/plugins/*
"Category: C Core":
- src/**/*

16
.github/workflows/auto_labeler_pr.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: "Pull Request Labeler"
on:
- pull_request_target
permissions:
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Apply Type Label
uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: "" # works around actions/labeler#104

View File

@ -77,6 +77,57 @@ DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build
Please note that the package is relocatable to any prefix and the option prefix Please note that the package is relocatable to any prefix and the option prefix
affects only the place where the application is actually installed. affects only the place where the application is actually installed.
## Installing Prebuilt
Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system.
### Linux
Unzip the file and `cd` into the `lite-xl` directory:
```sh
tar -xzf <file>
cd lite-xl
```
To run lite-xl without installing:
```sh
cd bin
./lite-xl
```
To install lite-xl copy files over into appropriate directories:
```sh
mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin
cp -r share $HOME/.local
```
If `$HOME/.local/bin` is not in PATH:
```sh
echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc
```
To get the icon to show up in app launcher:
```sh
xdg-desktop-menu forceupdate
```
You may need to logout and login again to see icon in app launcher.
To uninstall just run:
```sh
rm -f $HOME/.local/bin/lite-xl
rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
$HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \
$HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \
$HOME/.local/share/lite-xl
```
## Contributing ## Contributing
Any additional functionality that can be added through a plugin should be done Any additional functionality that can be added through a plugin should be done

View File

@ -1,5 +1,31 @@
This files document the changes done in Lite XL for each release. This files document the changes done in Lite XL for each release.
### 2.0.3
Replace periodic rescan of project folder with a notification based system using the
[dmon library](https://github.com/septag/dmon). Improves performance especially for
large project folders since the application no longer needs to rescan.
The application also reports immediatly any change in the project directory even
when the application is unfocused.
Improved find-replace reverse and forward search.
Fixed a bug in incremental syntax highlighting affecting documents with multiple-lines
comments or strings.
The application now always shows the tabs in the documents' view even when a single
document is opened. Can be changed with the option `config.always_show_tabs`.
Fix problem with numeric keypad function keys not properly working.
Fix problem with pixel not correctly drawn at the window's right edge.
Treat correctly and open network paths on Windows.
Add some improvements for very slow network filesystems.
Fix problem with python syntax highliting, contributed by @dflock.
### 2.0.2 ### 2.0.2
Fix problem project directory when starting the application from Launcher on macOS. Fix problem project directory when starting the application from Launcher on macOS.

View File

@ -41,11 +41,14 @@ function command.get_all_valid()
return res return res
end end
function command.is_valid(name, ...)
return command.map[name] and command.map[name].predicate(...)
end
local function perform(name) local function perform(name, ...)
local cmd = command.map[name] local cmd = command.map[name]
if cmd and cmd.predicate() then if cmd and cmd.predicate(...) then
cmd.perform() cmd.perform(...)
return true return true
end end
return false return false

View File

@ -6,10 +6,12 @@ local LogView = require "core.logview"
local fullscreen = false local fullscreen = false
local restore_title_view = false
local function suggest_directory(text) local function suggest_directory(text)
text = common.home_expand(text) text = common.home_expand(text)
return common.home_encode_list(text == "" and core.recent_projects or common.dir_path_suggest(text)) return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir)))
and core.recent_projects or common.dir_path_suggest(text))
end end
command.add(nil, { command.add(nil, {
@ -27,9 +29,12 @@ command.add(nil, {
["core:toggle-fullscreen"] = function() ["core:toggle-fullscreen"] = function()
fullscreen = not fullscreen fullscreen = not fullscreen
if fullscreen then
restore_title_view = core.title_view.visible
end
system.set_window_mode(fullscreen and "fullscreen" or "normal") system.set_window_mode(fullscreen and "fullscreen" or "normal")
core.show_title_bar(not fullscreen) core.show_title_bar(not fullscreen and restore_title_view)
core.title_view:configure_hit_test(not fullscreen) core.title_view:configure_hit_test(not fullscreen and restore_title_view)
end, end,
["core:reload-module"] = function() ["core:reload-module"] = function()
@ -66,7 +71,7 @@ command.add(nil, {
end, end,
["core:find-file"] = function() ["core:find-file"] = function()
if core.project_files_limit then if not core.project_files_number() then
return command.perform "core:open-file" return command.perform "core:open-file"
end end
local files = {} local files = {}
@ -149,7 +154,7 @@ command.add(nil, {
["core:change-project-folder"] = function() ["core:change-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
if dirname then if dirname then
core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) core.command_view:set_text(common.home_encode(dirname))
end end
core.command_view:enter("Change Project Folder", function(text, item) core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text)) text = system.absolute_path(common.home_expand(item and item.text or text))
@ -166,7 +171,7 @@ command.add(nil, {
["core:open-project-folder"] = function() ["core:open-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
if dirname then if dirname then
core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) core.command_view:set_text(common.home_encode(dirname))
end end
core.command_view:enter("Open Project", function(text, item) core.command_view:enter("Open Project", function(text, item)
text = common.home_expand(item and item.text or text) text = common.home_expand(item and item.text or text)
@ -191,8 +196,6 @@ command.add(nil, {
return return
end end
core.add_project_directory(system.absolute_path(text)) core.add_project_directory(system.absolute_path(text))
-- TODO: add the name of directory to prioritize
core.reschedule_project_scan()
end, suggest_directory) end, suggest_directory)
end, end,

View File

@ -16,14 +16,6 @@ local function doc()
end end
local function get_indent_string()
if config.tab_type == "hard" then
return "\t"
end
return string.rep(" ", config.indent_size)
end
local function doc_multiline_selections(sort) local function doc_multiline_selections(sort)
local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort) local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
return function() return function()
@ -82,6 +74,31 @@ local function split_cursor(direction)
core.blink_reset() core.blink_reset()
end end
local function set_cursor(x, y, snap_type)
local line, col = dv():resolve_screen_position(x, y)
doc():set_selection(line, col, line, col)
if snap_type == "word" or snap_type == "lines" then
command.perform("doc:select-" .. snap_type)
end
dv().mouse_selecting = { line, col, snap_type }
core.blink_reset()
end
local selection_commands = {
["doc:cut"] = function()
cut_or_copy(true)
end,
["doc:copy"] = function()
cut_or_copy(false)
end,
["doc:select-none"] = function()
local line, col = doc():get_selection()
doc():set_selection(line, col)
end
}
local commands = { local commands = {
["doc:undo"] = function() ["doc:undo"] = function()
doc():undo() doc():undo()
@ -91,14 +108,6 @@ local commands = {
doc():redo() doc():redo()
end, end,
["doc:cut"] = function()
cut_or_copy(true)
end,
["doc:copy"] = function()
cut_or_copy(false)
end,
["doc:paste"] = function() ["doc:paste"] = function()
local clipboard = system.get_clipboard() local clipboard = system.get_clipboard()
-- If the clipboard has changed since our last look, use that instead -- If the clipboard has changed since our last look, use that instead
@ -147,11 +156,12 @@ local commands = {
end, end,
["doc:backspace"] = function() ["doc:backspace"] = function()
local _, indent_size = doc():get_indent_info()
for idx, line1, col1, line2, col2 in doc():get_selections() do for idx, line1, col1, line2, col2 in doc():get_selections() do
if line1 == line2 and col1 == col2 then if line1 == line2 and col1 == col2 then
local text = doc():get_text(line1, 1, line1, col1) local text = doc():get_text(line1, 1, line1, col1)
if #text >= config.indent_size and text:find("^ *$") then if #text >= indent_size and text:find("^ *$") then
doc():delete_to_cursor(idx, 0, -config.indent_size) doc():delete_to_cursor(idx, 0, -indent_size)
return return
end end
end end
@ -163,11 +173,6 @@ local commands = {
doc():set_selection(1, 1, math.huge, math.huge) doc():set_selection(1, 1, math.huge, math.huge)
end, end,
["doc:select-none"] = function()
local line, col = doc():get_selection()
doc():set_selection(line, col)
end,
["doc:select-lines"] = function() ["doc:select-lines"] = function()
for idx, line1, _, line2 in doc():get_selections(true) do for idx, line1, _, line2 in doc():get_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
@ -261,7 +266,7 @@ 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 indentation = doc():get_indent_string()
local comment_text = comment .. " " local comment_text = comment .. " "
for idx, line1, _, line2 in doc_multiline_selections(true) do for idx, line1, _, line2 in doc_multiline_selections(true) do
local uncomment = true local uncomment = true
@ -389,6 +394,30 @@ local commands = {
core.log("Removed \"%s\"", filename) core.log("Removed \"%s\"", filename)
end, end,
["doc:select-to-cursor"] = function(x, y, clicks)
local line1, col1 = select(3, doc():get_selection())
local line2, col2 = dv():resolve_screen_position(x, y)
dv().mouse_selecting = { line1, col1, nil }
doc():set_selection(line2, col2, line1, col1)
end,
["doc:set-cursor"] = function(x, y)
set_cursor(x, y, "set")
end,
["doc:set-cursor-word"] = function(x, y)
set_cursor(x, y, "word")
end,
["doc:set-cursor-line"] = function(x, y, clicks)
set_cursor(x, y, "lines")
end,
["doc:split-cursor"] = function(x, y, clicks)
local line, col = dv():resolve_screen_position(x, y)
doc():add_selection(line, col, line, col)
end,
["doc:create-cursor-previous-line"] = function() ["doc:create-cursor-previous-line"] = function()
split_cursor(-1) split_cursor(-1)
doc():merge_cursors() doc():merge_cursors()
@ -447,3 +476,6 @@ commands["doc:move-to-next-char"] = function()
end end
command.add("core.docview", commands) command.add("core.docview", commands)
command.add(function()
return core.active_view:is(DocView) and core.active_view.doc:has_any_selection()
end ,selection_commands)

View File

@ -7,15 +7,15 @@ local DocView = require "core.docview"
local CommandView = require "core.commandview" local CommandView = require "core.commandview"
local StatusView = require "core.statusview" local StatusView = require "core.statusview"
local max_last_finds = 50 local last_view, last_fn, last_text, last_sel
local last_finds, last_view, last_fn, last_text, last_sel
local case_sensitive = config.find_case_sensitive or false local case_sensitive = config.find_case_sensitive or false
local find_regex = config.find_regex or false local find_regex = config.find_regex or false
local found_expression local found_expression
local function doc() local function doc()
return core.active_view:is(DocView) and core.active_view.doc or last_view.doc local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView)
return is_DocView and core.active_view.doc or (last_view and last_view.doc)
end end
local function get_find_tooltip() local function get_find_tooltip()
@ -37,7 +37,7 @@ local function update_preview(sel, search_fn, text)
last_view:scroll_to_line(line2, true) last_view:scroll_to_line(line2, true)
found_expression = true found_expression = true
else else
last_view.doc:set_selection(unpack(sel)) last_view.doc:set_selection(table.unpack(sel))
found_expression = false found_expression = false
end end
end end
@ -53,8 +53,8 @@ end
local function find(label, search_fn) local function find(label, search_fn)
last_view, last_sel, last_finds = core.active_view, last_view, last_sel = core.active_view,
{ core.active_view.doc:get_selection() }, {} { core.active_view.doc:get_selection() }
local text = last_view.doc:get_text(unpack(last_sel)) local text = last_view.doc:get_text(unpack(last_sel))
found_expression = false found_expression = false
@ -69,8 +69,8 @@ local function find(label, search_fn)
last_fn, last_text = search_fn, text last_fn, last_text = search_fn, text
else else
core.error("Couldn't find %q", text) core.error("Couldn't find %q", text)
last_view.doc:set_selection(unpack(last_sel)) last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(unpack(last_sel)) last_view:scroll_to_make_visible(table.unpack(last_sel))
end end
end, function(text) end, function(text)
update_preview(last_sel, search_fn, text) update_preview(last_sel, search_fn, text)
@ -79,8 +79,8 @@ local function find(label, search_fn)
end, function(explicit) end, function(explicit)
core.status_view:remove_tooltip() core.status_view:remove_tooltip()
if explicit then if explicit then
last_view.doc:set_selection(unpack(last_sel)) last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(unpack(last_sel)) last_view:scroll_to_make_visible(table.unpack(last_sel))
end end
end) end)
end end
@ -117,7 +117,7 @@ local function has_selection()
end end
local function has_unique_selection() local function has_unique_selection()
if not core.active_view:is(DocView) then return false end if not doc() then return false end
local text = nil local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end if line1 == line2 and col1 == col2 then return false end
@ -142,7 +142,7 @@ local function is_in_any_selection(line, col)
return false return false
end end
local function select_next(all) local function select_add_next(all)
local il1, ic1 = doc():get_selection(true) local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
local text = doc():get_text(l1, c1, l2, c2) local text = doc():get_text(l1, c1, l2, c2)
@ -161,21 +161,28 @@ local function select_next(all)
end end
end end
command.add(has_unique_selection, { local function select_next(reverse)
["find-replace:select-next"] = function()
local l1, c1, l2, c2 = doc():get_selection(true) local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2) local text = doc():get_text(l1, c1, l2, c2)
if reverse then
l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
end
if l2 then doc():set_selection(l2, c2, l1, c1) end if l2 then doc():set_selection(l2, c2, l1, c1) end
end, end
["find-replace:select-add-next"] = function() select_next(false) end,
["find-replace:select-add-all"] = function() select_next(true) end command.add(has_unique_selection, {
["find-replace:select-next"] = select_next,
["find-replace:select-previous"] = function() select_next(true) end,
["find-replace:select-add-next"] = select_add_next,
["find-replace:select-add-all"] = function() select_add_next(true) end
}) })
command.add("core.docview", { command.add("core.docview", {
["find-replace:find"] = function() ["find-replace:find"] = function()
find("Find Text", function(doc, line, col, text, case_sensitive, find_regex) find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse)
local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex } local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse }
return search.find(doc, line, col, text, opt) return search.find(doc, line, col, text, opt)
end) end)
end, end,
@ -221,29 +228,29 @@ command.add(valid_for_finding, {
core.error("No find to continue from") core.error("No find to continue from")
else else
local sl1, sc1, sl2, sc2 = doc():get_selection(true) local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex) local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
if line1 then if line1 then
if last_view.doc ~= doc() then
last_finds = {}
end
if #last_finds >= max_last_finds then
table.remove(last_finds, 1)
end
table.insert(last_finds, { sl1, sc1, sl2, sc2 })
doc():set_selection(line2, col2, line1, col1) doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true) last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end end
end end
end, end,
["find-replace:previous-find"] = function() ["find-replace:previous-find"] = function()
local sel = table.remove(last_finds) if not last_fn then
if not sel or doc() ~= last_view.doc then core.error("No find to continue from")
core.error("No previous finds") else
return local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc1, last_text, case_sensitive, find_regex, true)
if line1 then
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end end
doc():set_selection(table.unpack(sel))
last_view:scroll_to_line(sel[3], true)
end, end,
}) })

View File

@ -3,6 +3,7 @@ local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
local command = require "core.command" local command = require "core.command"
local common = require "core.common" local common = require "core.common"
local config = require "core.config"
local t = { local t = {
@ -76,7 +77,7 @@ local t = {
local parent = node:get_parent_node(core.root_view.root_node) local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and 0.1 or -0.1 local n = (parent.a == node) and 0.1 or -0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end, end
} }
@ -112,7 +113,8 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size) y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end end
local node = core.root_view.root_node:get_child_overlapping_point(x, y) local node = core.root_view.root_node:get_child_overlapping_point(x, y)
if not node:get_locked_size() then local sx, sy = node:get_locked_size()
if not sx and not sy then
core.set_active_view(node.active_view) core.set_active_view(node.active_view)
end end
end end
@ -120,5 +122,17 @@ end
command.add(function() command.add(function()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
return not node:get_locked_size() local sx, sy = node:get_locked_size()
return not sx and not sy
end, t) end, t)
command.add(nil, {
["root:scroll"] = function(delta)
local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view
if view and view.scrollable then
view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll
return true
end
return false
end
})

View File

@ -1,8 +1,8 @@
local common = {} local common = {}
function common.is_utf8_cont(char) function common.is_utf8_cont(s, offset)
local byte = char:byte() local byte = s:byte(offset or 1)
return byte >= 0x80 and byte < 0xc0 return byte >= 0x80 and byte < 0xc0
end end
@ -280,24 +280,61 @@ local function split_on_slash(s, sep_pattern)
end end
function common.normalize_path(filename) -- The filename argument given to the function is supposed to
-- come from system.absolute_path and as such should be an
-- absolute path without . or .. elements.
-- This function exists because on Windows the drive letter returned
-- by system.absolute_path is sometimes with a lower case and sometimes
-- with an upper case to we normalize to upper case.
function common.normalize_volume(filename)
if not filename then return end if not filename then return end
if PATHSEP == '\\' then
local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
if drive then
return drive:upper() .. rem
end
end
return filename
end
function common.normalize_path(filename)
if not filename then return end
local volume
if PATHSEP == '\\' then if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\') filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z])(:.*)') local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
filename = drive and drive:upper() .. rem or filename if drive then
volume, filename = drive:upper(), rem
else
drive, rem = filename:match('^(\\\\[^\\]+\\[^\\]+\\)(.*)')
if drive then
volume, filename = drive, rem
end
end
else
local relpath = filename:match('^/(.+)')
if relpath then
volume, filename = "/", relpath
end
end end
local parts = split_on_slash(filename, PATHSEP) local parts = split_on_slash(filename, PATHSEP)
local accu = {} local accu = {}
for _, part in ipairs(parts) do for _, part in ipairs(parts) do
if part == '..' and #accu > 0 and accu[#accu] ~= ".." then if part == '..' then
if #accu > 0 and accu[#accu] ~= ".." then
table.remove(accu) table.remove(accu)
elseif volume then
error("invalid path " .. volume .. filename)
else
table.insert(accu, part)
end
elseif part ~= '.' then elseif part ~= '.' then
table.insert(accu, part) table.insert(accu, part)
end end
end end
local npath = table.concat(accu, PATHSEP) local npath = table.concat(accu, PATHSEP)
return npath == "" and PATHSEP or npath return (volume or "") .. (npath == "" and PATHSEP or npath)
end end

View File

@ -1,6 +1,5 @@
local config = {} local config = {}
config.project_scan_rate = 5
config.fps = 60 config.fps = 60
config.max_log_items = 80 config.max_log_items = 80
config.message_timeout = 5 config.message_timeout = 5
@ -12,8 +11,8 @@ 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.max_tabs = 8
config.always_show_tabs = false config.always_show_tabs = true
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
@ -28,6 +27,7 @@ config.disable_blink = false
config.draw_whitespace = false config.draw_whitespace = false
config.borderless = false config.borderless = false
config.tab_close_button = true config.tab_close_button = true
config.max_clicks = 3
-- Disable plugin loading setting to false the config entry -- Disable plugin loading setting to false the config entry
-- of the same name. -- of the same name.

View File

@ -49,7 +49,7 @@ function ContextMenu:register(predicate, items)
local width, height = 0, 0 --precalculate the size of context menu local width, height = 0, 0 --precalculate the size of context menu
for i, item in ipairs(items) do for i, item in ipairs(items) do
if item ~= DIVIDER then if item ~= DIVIDER then
item.info = keymap.reverse_map[item.command] item.info = keymap.get_binding(item.command)
end end
local lw, lh = get_item_size(item) local lw, lh = get_item_size(item)
width = math.max(width, lw) width = math.max(width, lw)
@ -66,12 +66,16 @@ function ContextMenu:show(x, y)
for _, items in ipairs(self.itemset) do for _, items in ipairs(self.itemset) do
if items.predicate(x, y) then if items.predicate(x, y) then
items_list.width = math.max(items_list.width, items.items.width) items_list.width = math.max(items_list.width, items.items.width)
items_list.height = items_list.height + items.items.height items_list.height = items_list.height
for _, subitems in ipairs(items.items) do for _, subitems in ipairs(items.items) do
if not subitems.command or command.is_valid(subitems.command) then
local lw, lh = get_item_size(subitems)
items_list.height = items_list.height + lh
table.insert(items_list, subitems) table.insert(items_list, subitems)
end end
end end
end end
end
if #items_list > 0 then if #items_list > 0 then
self.items = items_list self.items = items_list

View File

@ -1,4 +1,5 @@
local core = require "core" local core = require "core"
local common = require "core.common"
local config = require "core.config" local config = require "core.config"
local tokenizer = require "core.tokenizer" local tokenizer = require "core.tokenizer"
local Object = require "core.object" local Object = require "core.object"
@ -40,6 +41,13 @@ end
function Highlighter:reset() function Highlighter:reset()
self.lines = {} self.lines = {}
self:soft_reset()
end
function Highlighter:soft_reset()
for i=1,#self.lines do
self.lines[i] = false
end
self.first_invalid_line = 1 self.first_invalid_line = 1
self.max_wanted_line = 0 self.max_wanted_line = 0
end end
@ -51,16 +59,16 @@ end
function Highlighter:insert_notify(line, n) function Highlighter:insert_notify(line, n)
self:invalidate(line) self:invalidate(line)
local blanks = { }
for i = 1, n do for i = 1, n do
table.insert(self.lines, line, nil) blanks[i] = false
end end
common.splice(self.lines, line, 0, blanks)
end end
function Highlighter:remove_notify(line, n) function Highlighter:remove_notify(line, n)
self:invalidate(line) self:invalidate(line)
for i = 1, n do common.splice(self.lines, line, n)
table.remove(self.lines, line)
end
end end

View File

@ -47,7 +47,7 @@ function Doc:reset_syntax()
local syn = syntax.get(self.filename or "", header) local syn = syntax.get(self.filename or "", header)
if self.syntax ~= syn then if self.syntax ~= syn then
self.syntax = syn self.syntax = syn
self.highlighter:reset() self.highlighter:soft_reset()
end end
end end
@ -62,12 +62,15 @@ function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert( io.open(filename, "rb") )
self:reset() self:reset()
self.lines = {} self.lines = {}
local i = 1
for line in fp:lines() do for line in fp:lines() do
if line:byte(-1) == 13 then if line:byte(-1) == 13 then
line = line:sub(1, -2) line = line:sub(1, -2)
self.crlf = true self.crlf = true
end end
table.insert(self.lines, line .. "\n") table.insert(self.lines, line .. "\n")
self.highlighter.lines[i] = false
i = i + 1
end end
if #self.lines == 0 then if #self.lines == 0 then
table.insert(self.lines, "\n") table.insert(self.lines, "\n")
@ -115,6 +118,14 @@ function Doc:clean()
end end
function Doc:get_indent_info()
if not self.indent_info then return config.tab_type, config.indent_size, false end
return self.indent_info.type or config.tab_type,
self.indent_info.size or config.indent_size,
self.indent_info.confirmed
end
function Doc:get_change_id() function Doc:get_change_id()
return self.undo_stack.idx return self.undo_stack.idx
end end
@ -146,6 +157,13 @@ function Doc:has_selection()
return line1 ~= line2 or col1 ~= col2 return line1 ~= line2 or col1 ~= col2
end end
function Doc:has_any_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do
if line1 ~= line2 or col1 ~= col2 then return true end
end
return false
end
function Doc:sanitize_selection() function Doc:sanitize_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do for idx, line1, col1, line2, col2 in self:get_selections() do
self:set_selections(idx, line1, col1, line2, col2) self:set_selections(idx, line1, col1, line2, col2)
@ -202,9 +220,9 @@ local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then if invariant[2] then
return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4)) return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4))
else else
return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4) return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4)
end end
end end
@ -305,7 +323,8 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
local line1, col1, line2, col2 = table.unpack(cmd) local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then elseif cmd.type == "selection" then
self.selections = { unpack(cmd) } self.selections = { table.unpack(cmd) }
self:sanitize_selection()
end end
modified = modified or (cmd.type ~= "selection") modified = modified or (cmd.type ~= "selection")
@ -348,7 +367,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
-- push undo -- push undo
local line2, col2 = self:position_offset(line, col, #text) local line2, col2 = self:position_offset(line, col, #text)
push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "remove", line, col, line2, col2) push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds -- update highlighter and assure selection is in bounds
@ -360,7 +379,7 @@ end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo -- push undo
local text = self:get_text(line1, col1, line2, col2) local text = self:get_text(line1, col1, line2, col2)
push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "insert", line1, col1, text) push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text -- get line content before/after removed text
@ -486,19 +505,21 @@ end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
local function get_indent_string() function Doc:get_indent_string()
if config.tab_type == "hard" then local indent_type, indent_size = self:get_indent_info()
if indent_type == "hard" then
return "\t" return "\t"
end end
return string.rep(" ", config.indent_size) return string.rep(" ", indent_size)
end end
-- returns the size of the original indent, and the indent -- returns the size of the original indent, and the indent
-- in your config format, rounded either up or down -- in your config format, rounded either up or down
local function get_line_indent(line, rnd_up) function Doc:get_line_indent(line, rnd_up)
local _, e = line:find("^[ \t]+") local _, e = line:find("^[ \t]+")
local soft_tab = string.rep(" ", config.indent_size) local indent_type, indent_size = self:get_indent_info()
if config.tab_type == "hard" then local soft_tab = string.rep(" ", indent_size)
if indent_type == "hard" then
local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or ""
return e, indent:gsub(" +", rnd_up and "\t" or "") return e, indent:gsub(" +", rnd_up and "\t" or "")
else else
@ -520,14 +541,14 @@ end
-- * if you are unindenting, the cursor will jump to the start of the line, -- * if you are unindenting, the cursor will jump to the start of the line,
-- and remove the appropriate amount of spaces (or a tab). -- and remove the appropriate amount of spaces (or a tab).
function Doc:indent_text(unindent, line1, col1, line2, col2) function Doc:indent_text(unindent, line1, col1, line2, col2)
local text = get_indent_string() local text = self:get_indent_string()
local _, se = self.lines[line1]:find("^[ \t]+") local _, se = self.lines[line1]:find("^[ \t]+")
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
local has_selection = line1 ~= line2 or col1 ~= col2 local has_selection = line1 ~= line2 or col1 ~= col2
if unindent or has_selection or in_beginning_whitespace then if unindent or has_selection or in_beginning_whitespace then
local l1d, l2d = #self.lines[line1], #self.lines[line2] local l1d, l2d = #self.lines[line1], #self.lines[line2]
for line = line1, line2 do for line = line1, line2 do
local e, rnded = get_line_indent(self.lines[line], unindent) local e, rnded = self:get_line_indent(self.lines[line], unindent)
self:remove(line, 1, line, (e or 0) + 1) self:remove(line, 1, line, (e or 0) + 1)
self:insert(line, 1, self:insert(line, 1,
unindent and rnded:sub(1, #rnded - #text) or rnded .. text) unindent and rnded:sub(1, #rnded - #text) or rnded .. text)

View File

@ -22,39 +22,64 @@ local function init_args(doc, line, col, text, opt)
return doc, line, col, text, opt return doc, line, col, text, opt
end end
-- This function is needed to uniform the behavior of
-- `regex:cmatch` and `string.find`.
local function regex_func(text, re, index, _)
local s, e = re:cmatch(text, index)
return s, e and e - 1
end
local function rfind(func, text, pattern, index, plain)
local s, e = func(text, pattern, 1, plain)
local last_s, last_e
if index < 0 then index = #text - index + 1 end
while e and e <= index do
last_s, last_e = s, e
s, e = func(text, pattern, s + 1, plain)
end
return last_s, last_e
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 plain = not opt.pattern
local re local pattern = text
local search_func = string.find
if opt.regex then if opt.regex then
re = regex.compile(text, opt.no_case and "i" or "") pattern = regex.compile(text, opt.no_case and "i" or "")
search_func = regex_func
end end
for line = line, #doc.lines do local start, finish, step = line, #doc.lines, 1
if opt.reverse then
start, finish, step = line, 1, -1
end
for line = start, finish, step do
local line_text = doc.lines[line] local line_text = doc.lines[line]
if opt.regex then if opt.no_case and not opt.regex then
local s, e = re:cmatch(line_text, col)
if s then
return line, s, line, e
end
col = 1
else
if opt.no_case then
line_text = line_text:lower() line_text = line_text:lower()
end end
local s, e = line_text:find(text, col, true) local s, e
if opt.reverse then
s, e = rfind(search_func, line_text, pattern, col - 1, plain)
else
s, e = search_func(line_text, pattern, col, plain)
end
if s then if s then
return line, s, line, e + 1 return line, s, line, e + 1
end end
col = 1 col = opt.reverse and -1 or 1
end
end end
if opt.wrap then if opt.wrap then
opt = { no_case = opt.no_case, regex = opt.regex } opt = { no_case = opt.no_case, regex = opt.regex, reverse = opt.reverse }
if opt.reverse then
return search.find(doc, #doc.lines, #doc.lines[#doc.lines], text, opt)
else
return search.find(doc, 1, 1, text, opt) return search.find(doc, 1, 1, text, opt)
end end
end end
end
return search return search

View File

@ -225,51 +225,6 @@ function DocView:scroll_to_make_visible(line, col)
end end
local function mouse_selection(doc, clicks, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then
line1, col1, line2, col2 = line2, col2, line1, col1
end
if clicks % 4 == 2 then
line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2)
elseif clicks % 4 == 3 then
if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then
doc:insert(math.huge, math.huge, "\n")
end
line1, col1, line2, col2 = line1, 1, line2 + 1, 1
end
if swap then
return line2, col2, line1, col1
end
return line1, col1, line2, col2
end
function DocView:on_mouse_pressed(button, x, y, clicks)
local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then
return
end
if keymap.modkeys["shift"] then
if clicks % 2 == 1 then
local line1, col1 = select(3, self.doc:get_selection())
local line2, col2 = self:resolve_screen_position(x, y)
self.doc:set_selection(line2, col2, line1, col1)
end
else
local line, col = self:resolve_screen_position(x, y)
if keymap.modkeys["ctrl"] then
self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col))
else
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
end
self.mouse_selecting = { line, col, clicks = clicks }
end
core.blink_reset()
end
function DocView:on_mouse_moved(x, y, ...) function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...)
@ -281,8 +236,7 @@ function DocView:on_mouse_moved(x, y, ...)
if self.mouse_selecting then if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y) local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2 = table.unpack(self.mouse_selecting) local l2, c2, snap_type = table.unpack(self.mouse_selecting)
local clicks = self.mouse_selecting.clicks
if keymap.modkeys["ctrl"] then if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { } self.doc.selections = { }
@ -290,12 +244,33 @@ function DocView:on_mouse_moved(x, y, ...)
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end end
else else
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) if snap_type then
l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2)
end
self.doc:set_selection(l1, c1, l2, c2)
end end
end end
end end
function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then
line1, col1, line2, col2 = line2, col2, line1, col1
end
if snap_type == "word" then
line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2)
elseif snap_type == "lines" then
col1, col2 = 1, math.huge
end
if swap then
return line2, col2, line1, col1
end
return line1, col1, line2, col2
end
function DocView:on_mouse_released(button) function DocView:on_mouse_released(button)
DocView.super.on_mouse_released(self, button) DocView.super.on_mouse_released(self, button)
self.mouse_selecting = nil self.mouse_selecting = nil
@ -354,6 +329,18 @@ function DocView:draw_caret(x, y)
end end
function DocView:draw_line_body(idx, x, y) function DocView:draw_line_body(idx, x, y)
-- draw highlight if any selection ends on this line
local draw_highlight = false
for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
if line1 == idx then
draw_highlight = true
break
end
end
if draw_highlight and config.highlight_current_line and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y)
end
-- draw selection if it overlaps this line -- draw selection if it overlaps this line
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
if idx >= line1 and idx <= line2 then if idx >= line1 and idx <= line2 then
@ -368,15 +355,6 @@ function DocView:draw_line_body(idx, x, y)
end end
end end
end end
local draw_highlight = nil
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
-- draw line highlight if caret is on this line
if draw_highlight ~= false and config.highlight_current_line
and line1 == idx and core.active_view == self then
draw_highlight = (line1 == line2 and col1 == col2)
end
end
if draw_highlight then self:draw_line_highlight(x + self.scroll.x, y) end
-- draw line's text -- draw line's text
self:draw_line_text(idx, x, y) self:draw_line_text(idx, x, y)
@ -417,8 +395,8 @@ end
function DocView:draw() function DocView:draw()
self:draw_background(style.background) self:draw_background(style.background)
local _, indent_size = self.doc:get_indent_info()
self:get_font():set_tab_size(config.indent_size) self:get_font():set_tab_size(indent_size)
local minline, maxline = self:get_visible_line_range() local minline, maxline = self:get_visible_line_range()
local lh = self:get_line_height() local lh = self:get_line_height()
@ -432,7 +410,9 @@ function DocView:draw()
local pos = self.position local pos = self.position
x, y = self:get_line_screen_position(minline) x, y = self:get_line_screen_position(minline)
core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y) -- the clip below ensure we don't write on the gutter region. On the
-- right side it is redundant with the Node's clip.
core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y)
for i = minline, maxline do for i = minline, maxline do
self:draw_line_body(i, x, y) self:draw_line_body(i, x, y)
y = y + lh y = y + lh

View File

@ -36,7 +36,7 @@ end
local function update_recents_project(action, dir_path_abs) local function update_recents_project(action, dir_path_abs)
local dirname = common.normalize_path(dir_path_abs) local dirname = common.normalize_volume(dir_path_abs)
if not dirname then return end if not dirname then return end
local recents = core.recent_projects local recents = core.recent_projects
local n = #recents local n = #recents
@ -52,23 +52,13 @@ local function update_recents_project(action, dir_path_abs)
end end
function core.reschedule_project_scan()
if core.project_scan_thread_id then
core.threads[core.project_scan_thread_id].wake = 0
end
end
function core.set_project_dir(new_dir, change_project_fn) function core.set_project_dir(new_dir, change_project_fn)
local chdir_ok = pcall(system.chdir, new_dir) local chdir_ok = pcall(system.chdir, new_dir)
if chdir_ok then if chdir_ok then
if change_project_fn then change_project_fn() end if change_project_fn then change_project_fn() end
core.project_dir = common.normalize_path(new_dir) core.project_dir = common.normalize_volume(new_dir)
core.project_directories = {} core.project_directories = {}
core.add_project_directory(new_dir) core.add_project_directory(new_dir)
core.project_files = {}
core.project_files_limit = false
core.reschedule_project_scan()
return true return true
end end
return false return false
@ -102,6 +92,29 @@ local function compare_file(a, b)
return a.filename < b.filename return a.filename < b.filename
end end
-- compute a file's info entry completed with "filename" to be used
-- in project scan or falsy if it shouldn't appear in the list.
local function get_project_file_info(root, file)
local info = system.get_file_info(root .. file)
if info then
info.filename = strip_leading_path(file)
return (info.size < config.file_size_limit * 1e6 and
not common.match_pattern(common.basename(info.filename), config.ignore_files)
and info)
end
end
-- Predicate function to inhibit directory recursion in get_directory_files
-- based on a time limit and the number of files.
local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
local n_limit = entries_count <= config.max_project_files
local t_limit = t_elapsed < 20 / config.fps
return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
end
-- "root" will by an absolute path without trailing '/' -- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/' -- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string. -- or the empty string.
@ -110,34 +123,31 @@ end
-- When recursing "root" will always be the same, only "path" will change. -- 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 -- Returns a list of file "items". In eash item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/'. -- complete file path relative to "root" *without* the trailing '/'.
local function get_directory_files(root, path, t, recursive, begin_hook) local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end if begin_hook then begin_hook() end
local size_limit = config.file_size_limit * 10e5 local t0 = system.get_time()
local all = system.list_dir(root .. path) or {} local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {} local dirs, files = {}, {}
local entries_count = 0
local max_entries = config.max_project_files
for _, file in ipairs(all) do for _, file in ipairs(all) do
if not common.match_pattern(file, config.ignore_files) then local info = get_project_file_info(root, path .. PATHSEP .. file)
local file = path .. PATHSEP .. file if info then
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) table.insert(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1 entries_count = entries_count + 1
if recursive and entries_count > max_entries then return nil, entries_count end
end
end end
end end
local recurse_complete = true
table.sort(dirs, compare_file) table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do for _, f in ipairs(dirs) do
table.insert(t, f) table.insert(t, f)
if recursive and entries_count <= max_entries then if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive) local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook)
entries_count = entries_count + subdir_count recurse_complete = recurse_complete and complete
f.scanned = true entries_count = n
else
recurse_complete = false
end end
end end
@ -146,136 +156,320 @@ local function get_directory_files(root, path, t, recursive, begin_hook)
table.insert(t, f) table.insert(t, f)
end end
return t, entries_count return t, recurse_complete, entries_count
end end
local function project_scan_thread()
local function diff_files(a, b) function core.project_subdir_set_show(dir, filename, show)
if #a ~= #b then return true end dir.shown_subdir[filename] = show
for i, v in ipairs(a) do if dir.files_limit and PLATFORM == "Linux" then
if b[i].filename ~= v.filename local fullpath = dir.name .. PATHSEP .. filename
or b[i].modified ~= v.modified then local watch_fn = show and system.watch_dir_add or system.watch_dir_rm
return true local success = watch_fn(dir.watch_id, fullpath)
if not success then
core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
end end
end end
end end
while true do
-- get project files and replace previous table if the new table is function core.project_subdir_is_shown(dir, filename)
-- different return not dir.files_limit or dir.shown_subdir[filename]
local i = 1 end
while not core.project_files_limit and i <= #core.project_directories do
local dir = core.project_directories[i]
local t, entries_count = get_directory_files(dir.name, "", {}, true) local function show_max_files_warning(dir)
if diff_files(dir.files, t) then local message = dir.slow_filesystem and
if entries_count > config.max_project_files then "Filesystem is too slow: project files will not be indexed." or
core.project_files_limit = true
core.status_view:show_message("!", style.accent,
"Too many files in project directory: stopped reading at ".. "Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see ".. config.max_project_files.." files. For more information see "..
"usage.md at github.com/franko/lite-xl." "usage.md at github.com/franko/lite-xl."
) core.status_view:show_message("!", style.accent, message)
end
dir.files = t
core.redraw = true
end
if dir.name == core.project_dir then
core.project_files = dir.files
end
i = i + 1
end end
-- wait for next scan
coroutine.yield(config.project_scan_rate) local function file_search(files, info)
local filename, type = info.filename, info.type
local inf, sup = 1, #files
while sup - inf > 8 do
local curr = math.floor((inf + sup) / 2)
if system.path_compare(filename, type, files[curr].filename, files[curr].type) then
sup = curr - 1
else
inf = curr
end
end
while inf <= sup and not system.path_compare(filename, type, files[inf].filename, files[inf].type) do
if files[inf].filename == filename then
return inf, true
end
inf = inf + 1
end
return inf, false
end
local function project_scan_add_entry(dir, fileinfo)
local index, match = file_search(dir.files, fileinfo)
if not match then
table.insert(dir.files, index, fileinfo)
dir.is_dirty = true
end end
end end
function core.is_project_folder(dirname) local function files_info_equal(a, b)
for _, dir in ipairs(core.project_directories) do return a.filename == b.filename and a.type == b.type
if dir.name == dirname then
return true
end
end end
-- for "a" inclusive from i1 + 1 and i1 + n
local function files_list_match(a, i1, n, b)
if n ~= #b then return false end
for i = 1, n do
if not files_info_equal(a[i1 + i], b[i]) then
return false return false
end end
end
return true
end
-- arguments like for files_list_match
local function files_list_replace(as, i1, n, bs)
local m = #bs
local i, j = 1, 1
while i <= m or i <= n do
local a, b = as[i1 + i], bs[j]
if i > n or (j <= m and not files_info_equal(a, b) and
not system.path_compare(a.filename, a.type, b.filename, b.type))
then
table.insert(as, i1 + i, b)
i, j, n = i + 1, j + 1, n + 1
elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then
table.remove(as, i1 + i)
n = n - 1
else
i, j = i + 1, j + 1
end
end
end
function core.scan_project_folder(dirname, filename) local function project_subdir_bounds(dir, filename)
for _, dir in ipairs(core.project_directories) do local index, n = 0, #dir.files
if dir.name == dirname then
for i, file in ipairs(dir.files) do for i, file in ipairs(dir.files) do
local file = dir.files[i] local file = dir.files[i]
if file.filename == filename then if file.filename == filename then
if file.scanned then return end index, n = i, #dir.files - i
local new_files = get_directory_files(dirname, PATHSEP .. filename, {}) for j = 1, #dir.files - i do
for j, new_file in ipairs(new_files) do if not common.path_belongs_to(dir.files[i + j].filename, filename) then
table.insert(dir.files, i + j, new_file) n = j - 1
end break
file.scanned = true
return
end end
end end
return index, n, file
end end
end end
end end
local function rescan_project_subdir(dir, filename_rooted)
local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield)
local index, n = 0, #dir.files
if filename_rooted ~= "" then
local filename = strip_leading_path(filename_rooted)
index, n = project_subdir_bounds(dir, filename)
end
local function find_project_files_co(root, path) if not files_list_match(dir.files, index, n, new_files) then
local size_limit = config.file_size_limit * 10e5 files_list_replace(dir.files, index, n, new_files)
dir.is_dirty = true
return true
end
end
local function add_dir_scan_thread(dir)
core.add_thread(function()
while true do
local has_changes = rescan_project_subdir(dir, "")
if has_changes then
core.redraw = true -- we run without an event, from a thread
end
coroutine.yield(5)
end
end)
end
-- Populate a project folder top directory by scanning the filesystem.
local function scan_project_folder(index)
local dir = core.project_directories[index]
if PLATFORM == "Linux" then
local fstype = system.get_fs_type(dir.name)
dir.force_rescan = (fstype == "nfs" or fstype == "fuse")
end
local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred)
if not complete then
dir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
dir.files_limit = true
if not dir.force_rescan then
-- Watch non-recursively on Linux only.
-- The reason is recursively watching with dmon on linux
-- doesn't work on very large directories.
dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux")
end
if core.status_view then -- May be not yet initialized.
show_max_files_warning(dir)
end
else
if not dir.force_rescan then
dir.watch_id = system.watch_dir(dir.name, true)
end
end
dir.files = t
if dir.force_rescan then
add_dir_scan_thread(dir)
else
core.dir_rescan_add_job(dir, ".")
end
end
function core.add_project_directory(path)
-- top directories has a file-like "item" but the item.filename
-- will be simply the name of the directory, without its path.
-- The field item.topdir will identify it as a top level directory.
path = common.normalize_volume(path)
local dir = {
name = path,
item = {filename = common.basename(path), type = "dir", topdir = true},
files_limit = false,
is_dirty = true,
shown_subdir = {},
}
table.insert(core.project_directories, dir)
scan_project_folder(#core.project_directories)
if path == core.project_dir then
core.project_files = dir.files
end
core.redraw = true
end
function core.update_project_subdir(dir, filename, expanded)
local index, n, file = project_subdir_bounds(dir, filename)
if index then
local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
files_list_replace(dir.files, index, n, new_files)
dir.is_dirty = true
return true
end
end
-- Find files and directories recursively reading from the filesystem.
-- Filter files and yields file's directory and info table. This latter
-- is filled to be like required by project directories "files" list.
local function find_files_rec(root, path)
local all = system.list_dir(root .. path) or {} local all = system.list_dir(root .. path) or {}
for _, file in ipairs(all) do for _, file in ipairs(all) do
if not common.match_pattern(file, config.ignore_files) then
local file = path .. PATHSEP .. file local file = path .. PATHSEP .. file
local info = system.get_file_info(root .. file) local info = system.get_file_info(root .. file)
if info and info.size < size_limit then if info then
info.filename = strip_leading_path(file) info.filename = strip_leading_path(file)
if info.type == "file" then if info.type == "file" then
coroutine.yield(root, info) coroutine.yield(root, info)
else else
find_project_files_co(root, PATHSEP .. info.filename) find_files_rec(root, PATHSEP .. info.filename)
end
end end
end end
end end
end end
-- Iterator function to list all project files
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]
if state.co then
-- We have a coroutine to fetch for files, use the coroutine.
-- Used for directories that exceeds the files nuumber limit.
local ok, name, file = coroutine.resume(state.co, dir.name, "")
if ok and name then
return name, file
else
-- The coroutine terminated, increment file/dir counter to scan
-- next project directory.
state.co = false
state.file_index = 1
state.dir_index = state.dir_index + 1
dir = core.project_directories[state.dir_index]
end
else
-- Increase file/dir counter
state.file_index = state.file_index + 1 state.file_index = state.file_index + 1
while dir and state.file_index > #dir.files do while dir and state.file_index > #dir.files do
state.dir_index = state.dir_index + 1 state.dir_index = state.dir_index + 1
state.file_index = 1 state.file_index = 1
dir = core.project_directories[state.dir_index] dir = core.project_directories[state.dir_index]
end end
end
if not dir then return end if not dir then return end
if dir.files_limit then
-- The current project directory is files limited: create a couroutine
-- to read files from the filesystem.
state.co = coroutine.create(find_files_rec)
return project_files_iter(state)
end
return dir.name, dir.files[state.file_index] return dir.name, dir.files[state.file_index]
end end
function core.get_project_files() function core.get_project_files()
if core.project_files_limit then
return coroutine.wrap(function()
for _, dir in ipairs(core.project_directories) do
find_project_files_co(dir.name, "")
end
end)
else
local state = { dir_index = 1, file_index = 0 } local state = { dir_index = 1, file_index = 0 }
return project_files_iter, state return project_files_iter, state
end end
end
function core.project_files_number() function core.project_files_number()
if not core.project_files_limit then
local n = 0 local n = 0
for i = 1, #core.project_directories do for i = 1, #core.project_directories do
if core.project_directories[i].files_limit then return end
n = n + #core.project_directories[i].files n = n + #core.project_directories[i].files
end end
return n return n
end end
local function project_dir_by_watch_id(watch_id)
for i = 1, #core.project_directories do
if core.project_directories[i].watch_id == watch_id then
return core.project_directories[i]
end
end
end
local function project_scan_remove_file(dir, filepath)
local fileinfo = { filename = filepath }
for _, filetype in ipairs {"dir", "file"} do
fileinfo.type = filetype
local index, match = file_search(dir.files, fileinfo)
if match then
table.remove(dir.files, index)
dir.is_dirty = true
return
end
end
end
local function project_scan_add_file(dir, filepath)
for fragment in string.gmatch(filepath, "([^/\\]+)") do
if common.match_pattern(fragment, config.ignore_files) then
return
end
end
local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath)
if fileinfo then
project_scan_add_entry(dir, fileinfo)
end
end end
@ -371,19 +565,6 @@ function core.load_user_directory()
end end
function core.add_project_directory(path)
-- top directories has a file-like "item" but the item.filename
-- will be simply the name of the directory, without its path.
-- The field item.topdir will identify it as a top level directory.
path = common.normalize_path(path)
table.insert(core.project_directories, {
name = path,
item = {filename = common.basename(path), type = "dir", topdir = true},
files = {}
})
end
function core.remove_project_directory(path) function core.remove_project_directory(path)
-- skip the fist directory because it is the project's directory -- skip the fist directory because it is the project's directory
for i = 2, #core.project_directories do for i = 2, #core.project_directories do
@ -422,9 +603,9 @@ function core.init()
Doc = require "core.doc" Doc = require "core.doc"
if PATHSEP == '\\' then if PATHSEP == '\\' then
USERDIR = common.normalize_path(USERDIR) USERDIR = common.normalize_volume(USERDIR)
DATADIR = common.normalize_path(DATADIR) DATADIR = common.normalize_volume(DATADIR)
EXEDIR = common.normalize_path(EXEDIR) EXEDIR = common.normalize_volume(EXEDIR)
end end
do do
@ -509,7 +690,6 @@ function core.init()
cur_node = cur_node:split("down", core.command_view, {y = true}) cur_node = cur_node:split("down", core.command_view, {y = true})
cur_node = cur_node:split("down", core.status_view, {y = true}) cur_node = cur_node:split("down", core.status_view, {y = true})
core.project_scan_thread_id = core.add_thread(project_scan_thread)
command.add_defaults() command.add_defaults()
local got_user_error = not core.load_user_directory() local got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins() local plugins_success, plugins_refuse_list = core.load_plugins()
@ -520,6 +700,12 @@ function core.init()
end end
local got_project_error = not core.load_project_module() local got_project_error = not core.load_project_module()
-- We assume we have just a single project directory here. Now that StatusView
-- is there show max files warning if needed.
if core.project_directories[1].files_limit then
show_max_files_warning(core.project_directories[1])
end
for _, filename in ipairs(files) do for _, filename in ipairs(files) do
core.root_view:open_doc(core.open_doc(filename)) core.root_view:open_doc(core.open_doc(filename))
end end
@ -675,16 +861,18 @@ function core.load_plugins()
userdir = {dir = USERDIR, plugins = {}}, userdir = {dir = USERDIR, plugins = {}},
datadir = {dir = DATADIR, plugins = {}}, datadir = {dir = DATADIR, plugins = {}},
} }
local files = {} local files, ordered = {}, {}
for _, root_dir in ipairs {DATADIR, USERDIR} do for _, root_dir in ipairs {DATADIR, USERDIR} do
local plugin_dir = root_dir .. "/plugins" local plugin_dir = root_dir .. "/plugins"
for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
if not files[filename] then table.insert(ordered, filename) end
files[filename] = plugin_dir -- user plugins will always replace system plugins files[filename] = plugin_dir -- user plugins will always replace system plugins
end end
end end
table.sort(ordered)
for filename, plugin_dir in pairs(files) do for _, filename in ipairs(ordered) do
local basename = filename:match("(.-)%.lua$") or filename local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
if is_lua_file then if is_lua_file then
if not version_match then if not version_match then
@ -908,6 +1096,84 @@ function core.try(fn, ...)
return false, err return false, err
end end
local scheduled_rescan = {}
function core.has_pending_rescan()
for _ in pairs(scheduled_rescan) do
return true
end
end
function core.dir_rescan_add_job(dir, filepath)
local dirpath = filepath:match("^(.+)[/\\].+$")
local dirpath_rooted = dirpath and PATHSEP .. dirpath or ""
local abs_dirpath = dir.name .. dirpath_rooted
if dirpath then
-- check if the directory is in the project files list, if not exit
local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"})
-- Note that is dir_match is false dir_index greaten than the last valid index.
-- We use dir_index to index dir.files below only if dir_match is true.
if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end
end
local new_time = system.get_time() + 1
-- evaluate new rescan request versus existing rescan
local remove_list = {}
for _, rescan in pairs(scheduled_rescan) do
if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then
-- abs_dirpath is a subpath of a scan already ongoing: skip
rescan.time_limit = new_time
return
elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then
-- abs_dirpath already cover this rescan: add to the list of rescan to be removed
table.insert(remove_list, rescan.abs_path)
end
end
for _, key_path in ipairs(remove_list) do
scheduled_rescan[key_path] = nil
end
scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time}
core.add_thread(function()
while true do
local rescan = scheduled_rescan[abs_dirpath]
if not rescan then return end
if system.get_time() > rescan.time_limit then
local has_changes = rescan_project_subdir(rescan.dir, rescan.path)
if has_changes then
core.redraw = true -- we run without an event, from a thread
rescan.time_limit = new_time
else
scheduled_rescan[rescan.abs_path] = nil
return
end
end
coroutine.yield(0.2)
end
end)
end
-- no-op but can be overrided by plugins
function core.on_dirmonitor_modify(dir, filepath)
end
function core.on_dir_change(watch_id, action, filepath)
local dir = project_dir_by_watch_id(watch_id)
if not dir then return end
core.dir_rescan_add_job(dir, filepath)
if action == "delete" then
project_scan_remove_file(dir, filepath)
elseif action == "create" then
project_scan_add_file(dir, filepath)
core.on_dirmonitor_modify(dir, filepath);
elseif action == "modify" then
core.on_dirmonitor_modify(dir, filepath);
end
end
function core.on_event(type, ...) function core.on_event(type, ...)
local did_keymap = false local did_keymap = false
@ -920,11 +1186,15 @@ function core.on_event(type, ...)
elseif type == "mousemoved" then elseif type == "mousemoved" then
core.root_view:on_mouse_moved(...) core.root_view:on_mouse_moved(...)
elseif type == "mousepressed" then elseif type == "mousepressed" then
core.root_view:on_mouse_pressed(...) if not core.root_view:on_mouse_pressed(...) then
did_keymap = keymap.on_mouse_pressed(...)
end
elseif type == "mousereleased" then elseif type == "mousereleased" then
core.root_view:on_mouse_released(...) core.root_view:on_mouse_released(...)
elseif type == "mousewheel" then elseif type == "mousewheel" then
core.root_view:on_mouse_wheel(...) if not core.root_view:on_mouse_wheel(...) then
did_keymap = keymap.on_mouse_wheel(...)
end
elseif type == "resized" then elseif type == "resized" then
core.window_mode = system.get_window_mode() core.window_mode = system.get_window_mode()
elseif type == "minimized" or type == "maximized" or type == "restored" then elseif type == "minimized" or type == "maximized" or type == "restored" then
@ -944,6 +1214,8 @@ function core.on_event(type, ...)
end end
elseif type == "focuslost" then elseif type == "focuslost" then
core.root_view:on_focus_lost(...) core.root_view:on_focus_lost(...)
elseif type == "dirchange" then
core.on_dir_change(...)
elseif type == "quit" then elseif type == "quit" then
core.quit() core.quit()
end end
@ -1050,7 +1322,7 @@ function core.run()
while true do while true do
core.frame_start = system.get_time() core.frame_start = system.get_time()
local did_redraw = core.step() local did_redraw = core.step()
local need_more_work = run_threads() local need_more_work = run_threads() or core.has_pending_rescan()
if core.restart_request or core.quit_request then break end if core.restart_request or core.quit_request then break end
if not did_redraw and not need_more_work then if not did_redraw and not need_more_work then
idle_iterations = idle_iterations + 1 idle_iterations = idle_iterations + 1

View File

@ -32,6 +32,8 @@ local function keymap_macos(keymap)
["cmd+7"] = "root:switch-to-tab-7", ["cmd+7"] = "root:switch-to-tab-7",
["cmd+8"] = "root:switch-to-tab-8", ["cmd+8"] = "root:switch-to-tab-8",
["cmd+9"] = "root:switch-to-tab-9", ["cmd+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["cmd+f"] = "find-replace:find", ["cmd+f"] = "find-replace:find",
["cmd+r"] = "find-replace:replace", ["cmd+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find", ["f3"] = "find-replace:repeat-find",
@ -93,6 +95,11 @@ local function keymap_macos(keymap)
["pageup"] = "doc:move-to-previous-page", ["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page", ["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char", ["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char", ["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line", ["shift+up"] = "doc:select-to-previous-line",

View File

@ -1,4 +1,5 @@
local command = require "core.command" local command = require "core.command"
local config = require "core.config"
local keymap = {} local keymap = {}
keymap.modkeys = {} keymap.modkeys = {}
@ -30,7 +31,8 @@ function keymap.add_direct(map)
end end
keymap.map[stroke] = commands keymap.map[stroke] = commands
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = stroke keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
@ -52,18 +54,43 @@ function keymap.add(map, overwrite)
end end
end end
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = stroke keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
local function remove_only(tbl, k, v)
for key, values in pairs(tbl) do
if key == k then
if v then
for i, value in ipairs(values) do
if value == v then
table.remove(values, i)
end
end
else
tbl[key] = nil
end
break
end
end
end
function keymap.unbind(key, cmd)
remove_only(keymap.map, key, cmd)
remove_only(keymap.reverse_map, cmd, key)
end
function keymap.get_binding(cmd) function keymap.get_binding(cmd)
return keymap.reverse_map[cmd] return table.unpack(keymap.reverse_map[cmd] or {})
end end
function keymap.on_key_pressed(k) function keymap.on_key_pressed(k, ...)
local mk = modkey_map[k] local mk = modkey_map[k]
if mk then if mk then
keymap.modkeys[mk] = true keymap.modkeys[mk] = true
@ -73,18 +100,30 @@ function keymap.on_key_pressed(k)
end end
else else
local stroke = key_to_stroke(k) local stroke = key_to_stroke(k)
local commands = keymap.map[stroke] local commands, performed = keymap.map[stroke]
if commands then if commands then
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
local performed = command.perform(cmd) performed = command.perform(cmd, ...)
if performed then break end if performed then break end
end end
return true return performed
end end
end end
return false return false
end end
function keymap.on_mouse_wheel(delta, ...)
return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...)
or keymap.on_key_pressed("wheel", delta, ...))
end
function keymap.on_mouse_pressed(button, x, y, clicks)
local click_number = (((clicks - 1) % config.max_clicks) + 1)
return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(click_number .. "click", x, y, clicks) or
keymap.on_key_pressed("click", x, y, clicks))
end
function keymap.on_key_released(k) function keymap.on_key_released(k)
local mk = modkey_map[k] local mk = modkey_map[k]
@ -133,6 +172,7 @@ keymap.add_direct {
["alt+7"] = "root:switch-to-tab-7", ["alt+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8", ["alt+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9", ["alt+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["ctrl+f"] = "find-replace:find", ["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace", ["ctrl+r"] = "find-replace:replace",
@ -170,6 +210,7 @@ keymap.add_direct {
["ctrl+a"] = "doc:select-all", ["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
["ctrl+f3"] = "find-replace:select-next", ["ctrl+f3"] = "find-replace:select-next",
["ctrl+shift+f3"] = "find-replace:select-previous",
["ctrl+l"] = "doc:select-lines", ["ctrl+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+/"] = "doc:toggle-line-comments",
@ -193,6 +234,11 @@ keymap.add_direct {
["pageup"] = "doc:move-to-previous-page", ["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page", ["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char", ["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char", ["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line", ["shift+up"] = "doc:select-to-previous-line",

View File

@ -1,4 +1,3 @@
-- So that in addition to regex.gsub(pattern, string), we can also do -- So that in addition to regex.gsub(pattern, string), we can also do
-- pattern:gsub(string). -- pattern:gsub(string).
regex.__index = function(table, key) return regex[key]; end regex.__index = function(table, key) return regex[key]; end
@ -6,7 +5,8 @@ regex.__index = function(table, key) return regex[key]; end
regex.match = function(pattern_string, string, offset, options) regex.match = function(pattern_string, string, offset, options)
local pattern = type(pattern_string) == "table" and local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string) pattern_string or regex.compile(pattern_string)
return regex.cmatch(pattern, string, offset or 1, options or 0) local s, e = regex.cmatch(pattern, string, offset or 1, options or 0)
return s, e and e - 1
end end
-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- Will iterate back through any UTF-8 bytes so that we don't replace bits

View File

@ -149,10 +149,17 @@ function Node:remove_view(root, view)
else else
locked_size = locked_size_y locked_size = locked_size_y
end end
if self.is_primary_node or locked_size then local next_primary
if self.is_primary_node then
next_primary = core.root_view:select_next_primary_node()
end
if locked_size or (self.is_primary_node and not next_primary) then
self.views = {} self.views = {}
self:add_view(EmptyView()) self:add_view(EmptyView())
else else
if other == next_primary then
next_primary = parent
end
parent:consume(other) parent:consume(other)
local p = parent local p = parent
while p.type ~= "leaf" do while p.type ~= "leaf" do
@ -160,7 +167,7 @@ function Node:remove_view(root, view)
end end
p:set_active_view(p.active_view) p:set_active_view(p.active_view)
if self.is_primary_node then if self.is_primary_node then
p.is_primary_node = true next_primary.is_primary_node = true
end end
end end
end end
@ -411,15 +418,8 @@ end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y -- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both -- axis are swapped; this function lets us use the same code for both
local function calc_split_sizes(self, x, y, x1, x2, y1, y2) local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
local n
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
if x1 then local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
n = x1 + ds
elseif x2 then
n = self.size[x] - x2
else
n = math.floor(self.size[x] * self.divider)
end
self.a.position[x] = self.position[x] self.a.position[x] = self.position[x]
self.a.position[y] = self.position[y] self.a.position[y] = self.position[y]
self.a.size[x] = n - ds self.a.size[x] = n - ds
@ -602,7 +602,7 @@ function Node:draw()
self:draw_tabs() self:draw_tabs()
end end
local pos, size = self.active_view.position, self.active_view.size local pos, size = self.active_view.position, self.active_view.size
core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1) core.push_clip_rect(pos.x, pos.y, size.x, size.y)
self.active_view:draw() self.active_view:draw()
core.pop_clip_rect() core.pop_clip_rect()
else else
@ -682,6 +682,10 @@ end
function Node:resize(axis, value) function Node:resize(axis, value)
-- the application works fine with non-integer values but to have pixel-perfect
-- placements of view elements, like the scrollbar, we round the value to be
-- an integer.
value = math.floor(value)
if self.type == 'leaf' then if self.type == 'leaf' then
-- If it is not locked we don't accept the -- If it is not locked we don't accept the
-- resize operation here because for proportional panes the resize is -- resize operation here because for proportional panes the resize is
@ -826,6 +830,24 @@ function RootView:get_primary_node()
end end
local function select_next_primary_node(node)
if node.is_primary_node then return end
if node.type ~= "leaf" then
return select_next_primary_node(node.a) or select_next_primary_node(node.b)
else
local lx, ly = node:get_locked_size()
if not lx and not ly then
return node
end
end
end
function RootView:select_next_primary_node()
return select_next_primary_node(self.root_node)
end
function RootView:open_doc(doc) function RootView:open_doc(doc)
local node = self:get_active_node_default() local node = self:get_active_node_default()
for i, view in ipairs(node.views) do for i, view in ipairs(node.views) do
@ -855,30 +877,30 @@ 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
self.dragged_divider = div
return
end
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
if div and (node and not node.active_view:scrollbar_overlaps_point(x, y)) then
self.dragged_divider = div
return true
end
if node.hovered_scroll_button > 0 then if node.hovered_scroll_button > 0 then
node:scroll_tabs(node.hovered_scroll_button) node:scroll_tabs(node.hovered_scroll_button)
return return true
end 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
node:close_view(self.root_node, node.views[idx]) node:close_view(self.root_node, node.views[idx])
return true
else else
if button == "left" then if button == "left" then
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y} self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
end end
node:set_active_view(node.views[idx]) node:set_active_view(node.views[idx])
return true
end end
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
core.set_active_view(node.active_view) core.set_active_view(node.active_view)
if not self.on_view_mouse_pressed(button, x, y, clicks) then return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks)
node.active_view:on_mouse_pressed(button, x, y, clicks)
end
end end
end end
@ -1000,17 +1022,18 @@ function RootView:on_mouse_moved(x, y, dx, dy)
self.root_node:on_mouse_moved(x, y, dx, dy) self.root_node:on_mouse_moved(x, y, dx, dy)
local node = self.root_node:get_child_overlapping_point(x, y) self.overlapping_node = self.root_node:get_child_overlapping_point(x, y)
local div = self.root_node:get_divider_overlapping_point(x, y) local 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 = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
if node and node:get_scroll_button_index(x, y) then if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
core.request_cursor("arrow") core.request_cursor("arrow")
elseif div then elseif div and (self.overlapping_node and not self.overlapping_node.active_view:scrollbar_overlaps_point(x, y)) then
core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
elseif tab_index then elseif tab_index then
core.request_cursor("arrow") core.request_cursor("arrow")
elseif node then elseif self.overlapping_node then
core.request_cursor(node.active_view.cursor) core.request_cursor(self.overlapping_node.active_view.cursor)
end end
end end
@ -1018,7 +1041,7 @@ end
function RootView:on_mouse_wheel(...) function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
node.active_view:on_mouse_wheel(...) return node.active_view:on_mouse_wheel(...)
end end

View File

@ -2,7 +2,7 @@
VERSION = "@PROJECT_VERSION@" VERSION = "@PROJECT_VERSION@"
MOD_VERSION = "2" MOD_VERSION = "2"
SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE
PATHSEP = package.config:sub(1, 1) PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$") EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
@ -28,3 +28,6 @@ package.searchers = { package.searchers[1], package.searchers[2], function(modna
if not path then return nil end if not path then return nil end
return system.load_native_plugin, path return system.load_native_plugin, path
end } end }
table.pack = table.pack or pack or function(...) return {...} end
table.unpack = table.unpack or unpack

View File

@ -108,9 +108,9 @@ function StatusView:get_items()
local dv = core.active_view local dv = core.active_view
local line, col = dv.doc:get_selection() local line, col = dv.doc:get_selection()
local dirty = dv.doc:is_dirty() local dirty = dv.doc:is_dirty()
local indent = dv.doc.indent_info local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info()
local indent_label = (indent and indent.type == "hard") and "tabs: " or "spaces: " local indent_label = (indent_type == "hard") and "tabs: " or "spaces: "
local indent_size = indent and tostring(indent.size) .. (indent.confirmed and "" or "*") or "unknown" local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown"
return { return {
dirty and style.accent or style.text, style.icon_font, "f", dirty and style.accent or style.text, style.icon_font, "f",

View File

@ -3,7 +3,7 @@ local common = require "core.common"
local syntax = {} local syntax = {}
syntax.items = {} syntax.items = {}
local plain_text_syntax = { patterns = {}, symbols = {} } local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
function syntax.add(t) function syntax.add(t)
@ -22,7 +22,7 @@ end
function syntax.get(filename, header) function syntax.get(filename, header)
return find(filename, "files") return find(filename, "files")
or find(header, "headers") or (header and find(header, "headers"))
or plain_text_syntax or plain_text_syntax
end end

View File

@ -1,4 +1,5 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
local common = require "core.common"
local tokenizer = {} local tokenizer = {}
@ -142,8 +143,13 @@ function tokenizer.tokenize(incoming_syntax, text, state)
code = p._regex code = p._regex
end end
repeat repeat
res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) } local next = res[2] + 1
or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) } -- go to the start of the next utf-8 character
while text:byte(next) and common.is_utf8_cont(text, next) do
next = next + 1
end
res = p.pattern and { text:find(at_start and "^" .. code or code, next) }
or { regex.match(code, text, next, at_start and regex.ANCHORED or 0) }
if res[1] and close and target[3] then if res[1] and close and target[3] then
local count = 0 local count = 0
for i = res[1] - 1, 1, -1 do for i = res[1] - 1, 1, -1 do
@ -155,7 +161,7 @@ function tokenizer.tokenize(incoming_syntax, text, state)
if count % 2 == 0 then break end if count % 2 == 0 then break end
end end
until not res[1] or not close or not target[3] until not res[1] or not close or not target[3]
return unpack(res) return table.unpack(res)
end end
while i <= #text do while i <= #text do

View File

@ -102,13 +102,9 @@ function View:on_text_input(text)
-- no-op -- no-op
end end
function View:on_mouse_wheel(y) function View:on_mouse_wheel(y)
if self.scrollable then
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
end
end
end
function View:get_content_bounds() function View:get_content_bounds()
local x = self.scroll.x local x = self.scroll.x
@ -140,7 +136,7 @@ end
function View:draw_background(color) function View:draw_background(color)
local x, y = self.position.x, self.position.y local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y local w, h = self.size.x, self.size.y
renderer.draw_rect(x, y, w + x % 1, h + y % 1, color) renderer.draw_rect(x, y, w, h, color)
end end

View File

@ -3,7 +3,6 @@ local core = require "core"
local config = require "core.config" local config = require "core.config"
local Doc = require "core.doc" local Doc = require "core.doc"
local times = setmetatable({}, { __mode = "k" }) local times = setmetatable({}, { __mode = "k" })
local function update_time(doc) local function update_time(doc)
@ -11,7 +10,6 @@ local function update_time(doc)
times[doc] = info.modified times[doc] = info.modified
end end
local function reload_doc(doc) local function reload_doc(doc)
local fp = io.open(doc.filename, "r") local fp = io.open(doc.filename, "r")
local text = fp:read("*a") local text = fp:read("*a")
@ -27,23 +25,19 @@ local function reload_doc(doc)
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end end
local on_modify = core.on_dirmonitor_modify
core.add_thread(function() core.on_dirmonitor_modify = function(dir, filepath)
while true do local abs_filename = dir.name .. PATHSEP .. filepath
-- check all doc modified times
for _, doc in ipairs(core.docs) do for _, doc in ipairs(core.docs) do
local info = system.get_file_info(doc.filename or "") local info = system.get_file_info(doc.filename or "")
if info and times[doc] ~= info.modified then if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then
reload_doc(doc) reload_doc(doc)
break
end end
coroutine.yield()
end end
on_modify(dir, filepath)
-- wait for next scan
coroutine.yield(config.project_scan_rate)
end end
end)
-- patch `Doc.save|load` to store modified time -- patch `Doc.save|load` to store modified time
local load = Doc.load local load = Doc.load

View File

@ -62,15 +62,15 @@ menu:register("core.logview", {
if require("plugins.scale") then if require("plugins.scale") then
menu:register("core.docview", { menu:register("core.docview", {
{ text = "Cut", command = "doc:cut" },
{ text = "Copy", command = "doc:copy" },
{ text = "Paste", command = "doc:paste" },
{ text = "Font +", command = "scale:increase" }, { text = "Font +", command = "scale:increase" },
{ text = "Font -", command = "scale:decrease" }, { text = "Font -", command = "scale:decrease" },
{ text = "Font Reset", command = "scale:reset" }, { text = "Font Reset", command = "scale:reset" },
ContextMenu.DIVIDER, ContextMenu.DIVIDER,
{ text = "Find", command = "find-replace:find" }, { text = "Find", command = "find-replace:find" },
{ text = "Replace", command = "find-replace:replace" }, { text = "Replace", command = "find-replace:replace" }
ContextMenu.DIVIDER,
{ text = "Find Pattern", command = "find-replace:find-pattern" },
{ text = "Replace Pattern", command = "find-replace:replace-pattern" },
}) })
end end

View File

@ -121,40 +121,17 @@ end
local clean = Doc.clean local clean = Doc.clean
function Doc:clean(...) function Doc:clean(...)
clean(self, ...) clean(self, ...)
if not cache[self].confirmed then local _, _, confirmed = self:get_indent_info()
if not confirmed then
update_cache(self) update_cache(self)
end end
end end
local function with_indent_override(doc, fn, ...)
local c = cache[doc]
if not c then
return fn(...)
end
local type, size = config.tab_type, config.indent_size
config.tab_type, config.indent_size = c.type, c.size or config.indent_size
local r1, r2, r3 = fn(...)
config.tab_type, config.indent_size = type, size
return r1, r2, r3
end
local perform = command.perform
function command.perform(...)
return with_indent_override(core.active_view.doc, perform, ...)
end
local draw = DocView.draw
function DocView:draw(...)
return with_indent_override(self.doc, draw, self, ...)
end
local function set_indent_type(doc, type) local function set_indent_type(doc, type)
local _, indent_size = doc:get_indent_info()
cache[doc] = {type = type, cache[doc] = {type = type,
size = cache[doc].value or config.indent_size, size = indent_size,
confirmed = true} confirmed = true}
doc.indent_info = cache[doc] doc.indent_info = cache[doc]
end end
@ -180,7 +157,8 @@ end
local function set_indent_size(doc, size) local function set_indent_size(doc, size)
cache[doc] = {type = cache[doc].type or config.tab_type, local indent_type = doc:get_indent_info()
cache[doc] = {type = indent_type,
size = size, size = size,
confirmed = true} confirmed = true}
doc.indent_info = cache[doc] doc.indent_info = cache[doc]

View File

@ -7,26 +7,30 @@ local common = require "core.common"
local draw_line_text = DocView.draw_line_text local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(idx, x, y) function DocView:draw_line_text(idx, x, y)
local font = (self:get_font() or style.syntax_fonts["comment"]) local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
local color = style.syntax.comment local color = style.syntax.whitespace or style.syntax.comment
local ty, tx = y + self:get_line_text_y_offset() local ty = y + self:get_line_text_y_offset()
local tx
local text, offset, s, e = self.doc.lines[idx], 1 local text, offset, s, e = self.doc.lines[idx], 1
local x1, _, x2, _ = self:get_content_bounds()
local _offset = self:get_x_offset_col(idx, x1)
offset = _offset
while true do while true do
s, e = text:find(" +", offset) s, e = text:find(" +", offset)
if not s then break end if not s then break end
tx = self:get_col_x_offset(idx, s) + x tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color) renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color)
if tx > x + x2 then break end
offset = e + 1 offset = e + 1
end end
offset = 1 offset = _offset
while true do while true do
s, e = text:find("\t", offset) s, e = text:find("\t", offset)
if not s then break end if not s then break end
tx = self:get_col_x_offset(idx, s) + x tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, "»", tx, ty, color) renderer.draw_text(font, "»", tx, ty, color)
if tx > x + x2 then break end
offset = e + 1 offset = e + 1
end end
draw_line_text(self, idx, x, y) draw_line_text(self, idx, x, y)
end end

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "C",
files = { "%.c$", "%.h$", "%.inl$" }, files = { "%.c$", "%.h$", "%.inl$" },
comment = "//", comment = "//",
patterns = { patterns = {

View File

@ -4,6 +4,7 @@ pcall(require, "plugins.language_c")
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "C++",
files = { files = {
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "CSS",
files = { "%.css$" }, files = { "%.css$" },
patterns = { patterns = {
{ pattern = "\\.", type = "normal" }, { pattern = "\\.", type = "normal" },

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "HTML",
files = { "%.html?$" }, files = { "%.html?$" },
patterns = { patterns = {
{ {

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$" }, files = { "%.js$", "%.json$", "%.cson$" },
comment = "//", comment = "//",
patterns = { patterns = {
@ -11,8 +12,8 @@ syntax.add {
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" }, { pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" }, { pattern = "0x[%da-fA-F_]+n?", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" }, { pattern = "-?%d+[%d%.eE_n]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" }, { pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "Lua",
files = "%.lua$", files = "%.lua$",
headers = "^#!.*[ /]lua", headers = "^#!.*[ /]lua",
comment = "--", comment = "--",

View File

@ -4,11 +4,11 @@ local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "Markdown",
files = { "%.md$", "%.markdown$" }, files = { "%.md$", "%.markdown$" },
patterns = { patterns = {
{ pattern = "\\.", type = "normal" }, { pattern = "\\.", type = "normal" },
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" }, { pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" }, { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "```python", "```" }, type = "string", syntax = ".py" }, { pattern = { "```python", "```" }, type = "string", syntax = ".py" },
{ pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" }, { pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
@ -25,6 +25,21 @@ syntax.add {
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" }, { pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" }, { pattern = { "```d", "```" }, type = "string", syntax = ".d" },
{ pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" }, { pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" },
{ pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = { "```julia", "```" }, type = "string", syntax = ".jl" },
{ pattern = { "```rust", "```" }, type = "string", syntax = ".rs" },
{ pattern = { "```dart", "```" }, type = "string", syntax = ".dart" },
{ pattern = { "```v", "```" }, type = "string", syntax = ".v" },
{ pattern = { "```toml", "```" }, type = "string", syntax = ".toml" },
{ pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" },
{ pattern = { "```php", "```" }, type = "string", syntax = ".php" },
{ pattern = { "```nim", "```" }, type = "string", syntax = ".nim" },
{ pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" },
{ pattern = { "```rescript", "```" }, type = "string", syntax = ".res" },
{ pattern = { "```moon", "```" }, type = "string", syntax = ".moon" },
{ pattern = { "```go", "```" }, type = "string", syntax = ".go" },
{ pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" },
{ pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" },
{ pattern = { "```", "```" }, type = "string" }, { pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``", "\\" }, type = "string" }, { pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "`", "`", "\\" }, type = "string" }, { pattern = { "`", "`", "\\" }, type = "string" },

View File

@ -2,14 +2,15 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "Python",
files = { "%.py$", "%.pyw$", "%.rpy$" }, files = { "%.py$", "%.pyw$", "%.rpy$" },
headers = "^#!.*[ /]python", headers = "^#!.*[ /]python",
comment = "#", comment = "#",
patterns = { patterns = {
{ pattern = { "#", "\n" }, type = "comment" }, { pattern = { "#", "\n" }, type = "comment" },
{ pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
{ pattern = { '"""', '"""' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" }, { pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" }, { pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" }, { pattern = "-?%.?%d+", type = "number" },

View File

@ -2,6 +2,7 @@
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "XML",
files = { "%.xml$" }, files = { "%.xml$" },
headers = "<%?xml", headers = "<%?xml",
patterns = { patterns = {

View File

@ -2,12 +2,13 @@
local config = require "core.config" local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
local CommandView = require "core.commandview"
local draw_overlay = DocView.draw_overlay local draw_overlay = DocView.draw_overlay
function DocView:draw_overlay(...) function DocView:draw_overlay(...)
local ns = ("n"):rep(config.line_limit) if not self:is(CommandView) then
local offset = self:get_font():get_width(ns) local offset = self:get_font():get_width("n") * config.line_limit
local x = self:get_line_screen_position(1) + offset local x = self:get_line_screen_position(1) + offset
local y = self.position.y local y = self.position.y
local w = math.ceil(SCALE * 1) local w = math.ceil(SCALE * 1)
@ -15,6 +16,6 @@ function DocView:draw_overlay(...)
local color = style.guide or style.selection local color = style.guide or style.selection
renderer.draw_rect(x, y, w, h, color) renderer.draw_rect(x, y, w, h, color)
end
draw_overlay(self, ...) draw_overlay(self, ...)
end end

View File

@ -92,7 +92,7 @@ end
function ResultsView:on_mouse_pressed(...) function ResultsView:on_mouse_pressed(...)
local caught = ResultsView.super.on_mouse_pressed(self, ...) local caught = ResultsView.super.on_mouse_pressed(self, ...)
if not caught then if not caught then
self:open_selected_result() return self:open_selected_result()
end end
end end
@ -108,6 +108,7 @@ function ResultsView:open_selected_result()
dv.doc:set_selection(res.line, res.col) dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true) dv:scroll_to_line(res.line, false, true)
end) end)
return true
end end

View File

@ -54,6 +54,10 @@ local function set_scale(scale)
renderer.font.set_size(font, s * font:get_size()) renderer.font.set_size(font, s * font:get_size())
end end
for _, font in pairs(style.syntax_fonts) do
renderer.font.set_size(font, s * font:get_size())
end
-- restore scroll positions -- restore scroll positions
for view, n in pairs(scrolls) do for view, n in pairs(scrolls) do
view.scroll.y = n * (view:get_scrollable_size() - view.size.y) view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
@ -67,17 +71,6 @@ local function get_scale()
return current_scale return current_scale
end end
local on_mouse_wheel = RootView.on_mouse_wheel
function RootView:on_mouse_wheel(d, ...)
if keymap.modkeys["ctrl"] and config.plugins.scale.use_mousewheel then
if d < 0 then command.perform "scale:decrease" end
if d > 0 then command.perform "scale:increase" end
else
return on_mouse_wheel(self, d, ...)
end
end
local function res_scale() local function res_scale()
set_scale(default_scale) set_scale(default_scale)
end end
@ -101,6 +94,8 @@ keymap.add {
["ctrl+0"] = "scale:reset", ["ctrl+0"] = "scale:reset",
["ctrl+-"] = "scale:decrease", ["ctrl+-"] = "scale:decrease",
["ctrl+="] = "scale:increase", ["ctrl+="] = "scale:increase",
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
} }
return { return {

View File

@ -41,8 +41,15 @@ function TreeView:new()
self.init_size = true self.init_size = true
self.target_size = default_treeview_size self.target_size = default_treeview_size
self.cache = {} self.cache = {}
self.last = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
local on_dirmonitor_modify = core.on_dirmonitor_modify
function core.on_dirmonitor_modify(dir, filepath)
if self.cache[dir.name] then
self.cache[dir.name][filepath] = nil
end
on_dirmonitor_modify(dir, filepath)
end
end end
@ -54,7 +61,7 @@ function TreeView:set_target_size(axis, value)
end end
function TreeView:get_cached(item, dirname) function TreeView:get_cached(dir, item, dirname)
local dir_cache = self.cache[dirname] local dir_cache = self.cache[dirname]
if not dir_cache then if not dir_cache then
dir_cache = {} dir_cache = {}
@ -80,6 +87,7 @@ function TreeView:get_cached(item, dirname)
end end
t.name = basename t.name = basename
t.type = item.type t.type = item.type
t.dir = dir -- points to top level "dir" item
dir_cache[cache_name] = t dir_cache[cache_name] = t
end end
return t return t
@ -104,18 +112,13 @@ end
function TreeView:check_cache() function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do for i = 1, #core.project_directories do
local dir = core.project_directories[i] local dir = core.project_directories[i]
local last_files = self.last[dir.name] -- invalidate cache's skip values if directory is declared dirty
if not last_files then if dir.is_dirty and self.cache[dir.name] then
self.last[dir.name] = dir.files
else
if dir.files ~= last_files then
self:invalidate_cache(dir.name) self:invalidate_cache(dir.name)
self.last[dir.name] = dir.files
end
end end
dir.is_dirty = false
end end
end end
@ -131,14 +134,14 @@ function TreeView:each_item()
for k = 1, #core.project_directories do for k = 1, #core.project_directories do
local dir = core.project_directories[k] local dir = core.project_directories[k]
local dir_cached = self:get_cached(dir.item, dir.name) local dir_cached = self:get_cached(dir, dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h) coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1 count_lines = count_lines + 1
y = y + h y = y + h
local i = 1 local i = 1
while i <= #dir.files and dir_cached.expanded do while i <= #dir.files and dir_cached.expanded do
local item = dir.files[i] local item = dir.files[i]
local cached = self:get_cached(item, dir.name) local cached = self:get_cached(dir, item, dir.name)
coroutine.yield(cached, ox, y, w, h) coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1 count_lines = count_lines + 1
@ -206,7 +209,6 @@ local function create_directory_in(item)
core.error("cannot create directory %q: %s", dirname, err) core.error("cannot create directory %q: %s", dirname, err)
end end
item.expanded = true item.expanded = true
core.reschedule_project_scan()
end) end)
end end
@ -223,26 +225,17 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
if keymap.modkeys["ctrl"] and button == "left" then if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item) create_directory_in(hovered_item)
else else
if core.project_files_limit and not hovered_item.expanded then
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
local index = 0
-- The loop below is used to find the first match starting from the end
-- in case there are multiple matches.
while index and index + #filename < #abs_filename do
index = string.find(abs_filename, filename, index + 1, true)
end
-- we assume here index is not nil because the abs_filename must contain the
-- relative filename
local dirname = string.sub(abs_filename, 1, index - 2)
if core.is_project_folder(dirname) then
core.scan_project_folder(dirname, filename)
self:invalidate_cache(dirname)
end
end
hovered_item.expanded = not hovered_item.expanded hovered_item.expanded = not hovered_item.expanded
if hovered_item.dir.files_limit then
core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
end
end end
else else
core.try(function() core.try(function()
if core.last_active_view and core.active_view == self then
core.set_active_view(core.last_active_view)
end
local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename) local doc_filename = core.normalize_to_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)
@ -295,6 +288,12 @@ function TreeView:draw_tooltip()
end end
function TreeView:color_for_item(abs_filename)
-- other plugins can override this to customize the color of each icon
return nil
end
function TreeView:draw() function TreeView:draw()
self:draw_background(style.background2) self:draw_background(style.background2)
@ -318,6 +317,9 @@ function TreeView:draw()
color = style.accent color = style.accent
end end
-- allow for color overrides
local icon_color = self:color_for_item(item.abs_filename) or color
-- icons -- icons
x = x + item.depth * style.padding.x + style.padding.x x = x + item.depth * style.padding.x + style.padding.x
if item.type == "dir" then if item.type == "dir" then
@ -325,11 +327,11 @@ function TreeView:draw()
local icon2 = item.expanded and "D" or "d" local icon2 = item.expanded and "D" or "d"
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h) common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
x = x + style.padding.x x = x + style.padding.x
common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h) common.draw_text(style.icon_font, icon_color, icon2, nil, x, y, 0, h)
x = x + icon_width x = x + icon_width
else else
x = x + style.padding.x x = x + style.padding.x
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h) common.draw_text(style.icon_font, icon_color, "f", nil, x, y, 0, h)
x = x + icon_width x = x + icon_width
end end
@ -404,7 +406,7 @@ function RootView:draw(...)
end end
local function is_project_folder(path) local function is_project_folder(path)
return common.basename(core.project_dir) == path return core.project_dir == path
end end
menu:register(function() return view.hovered_item end, { menu:register(function() return view.hovered_item end, {
@ -415,7 +417,7 @@ menu:register(function() return view.hovered_item end, {
menu:register( menu:register(
function() function()
return view.hovered_item return view.hovered_item
and not is_project_folder(view.hovered_item.filename) and not is_project_folder(view.hovered_item.abs_filename)
end, end,
{ {
{ text = "Rename", command = "treeview:rename" }, { text = "Rename", command = "treeview:rename" },
@ -461,14 +463,12 @@ command.add(function() return view.hovered_item ~= nil end, {
else else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end end
core.reschedule_project_scan()
end, common.path_suggest) end, common.path_suggest)
end, end,
["treeview:new-file"] = function() ["treeview:new-file"] = function()
local dir_name = view.hovered_item.filename if not is_project_folder(view.hovered_item.abs_filename) then
if not is_project_folder(dir_name) then core.command_view:set_text(view.hovered_item.filename .. "/")
core.command_view:set_text(dir_name .. "/")
end end
core.command_view:enter("Filename", function(filename) core.command_view:enter("Filename", function(filename)
local doc_filename = core.project_dir .. PATHSEP .. filename local doc_filename = core.project_dir .. PATHSEP .. filename
@ -476,20 +476,17 @@ command.add(function() return view.hovered_item ~= nil end, {
file:write("") file:write("")
file:close() file:close()
core.root_view:open_doc(core.open_doc(doc_filename)) core.root_view:open_doc(core.open_doc(doc_filename))
core.reschedule_project_scan()
core.log("Created %s", doc_filename) core.log("Created %s", doc_filename)
end, common.path_suggest) end, common.path_suggest)
end, end,
["treeview:new-folder"] = function() ["treeview:new-folder"] = function()
local dir_name = view.hovered_item.filename if not is_project_folder(view.hovered_item.abs_filename) then
if not is_project_folder(dir_name) then core.command_view:set_text(view.hovered_item.filename .. "/")
core.command_view:set_text(dir_name .. "/")
end end
core.command_view:enter("Folder Name", function(filename) core.command_view:enter("Folder Name", function(filename)
local dir_path = core.project_dir .. PATHSEP .. filename local dir_path = core.project_dir .. PATHSEP .. filename
common.mkdirp(dir_path) common.mkdirp(dir_path)
core.reschedule_project_scan()
core.log("Created %s", dir_path) core.log("Created %s", dir_path)
end, common.path_suggest) end, common.path_suggest)
end, end,
@ -526,7 +523,6 @@ command.add(function() return view.hovered_item ~= nil end, {
return return
end end
end end
core.reschedule_project_scan()
core.log("Deleted \"%s\"", filename) core.log("Deleted \"%s\"", filename)
end end
end end

View File

@ -19,6 +19,9 @@ renderer.color = {}
---@class renderer.fontoptions ---@class renderer.fontoptions
---@field public antialiasing "'grayscale'" | "'subpixel'" ---@field public antialiasing "'grayscale'" | "'subpixel'"
---@field public hinting "'slight'" | "'none'" | '"full"' ---@field public hinting "'slight'" | "'none'" | '"full"'
-- @field public bold boolean
-- @field public italic boolean
-- @field public underline boolean
renderer.fontoptions = {} renderer.fontoptions = {}
--- ---
@ -58,15 +61,6 @@ function renderer.font:set_tab_size(chars) end
---@return number ---@return number
function renderer.font:get_width(text) end function renderer.font:get_width(text) end
---
---Get the width in subpixels of the given text when
---rendered with this font.
---
---@param text string
---
---@return number
function renderer.font:get_width_subpixel(text) end
--- ---
---Get the height in pixels that occupies a single character ---Get the height in pixels that occupies a single character
---when rendered with this font. ---when rendered with this font.
@ -74,12 +68,6 @@ function renderer.font:get_width_subpixel(text) end
---@return number ---@return number
function renderer.font:get_height() end function renderer.font:get_height() end
---
---Gets the font subpixel scale.
---
---@return number
function renderer.font:subpixel_scale() end
--- ---
---Get the current size of the font. ---Get the current size of the font.
--- ---

1591
lib/dmon/dmon.h Normal file

File diff suppressed because it is too large Load Diff

162
lib/dmon/dmon_extra.h Normal file
View File

@ -0,0 +1,162 @@
#ifndef __DMON_EXTRA_H__
#define __DMON_EXTRA_H__
//
// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
// License: https://github.com/septag/dmon#license-bsd-2-clause
//
// Extra header functionality for dmon.h for the backend based on inotify
//
// Add/Remove directory functions:
// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take
// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one
// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user
// will be reached. The default maximum is 8192.
// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the
// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched.
// The function dmon_watch_add and dmon_watch_rm are used to this purpose.
//
#ifndef __DMON_H__
#error "Include 'dmon.h' before including this file"
#endif
#ifdef __cplusplus
extern "C" {
#endif
DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir);
DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir);
#ifdef __cplusplus
}
#endif
#ifdef DMON_IMPL
#if DMON_OS_LINUX
DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
{
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock)
pthread_mutex_lock(&_dmon.mutex);
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
// check if the directory exists
// if watchdir contains absolute/root-included path, try to strip the rootdir from it
// else, we assume that watchdir is correct, so save it as it is
struct stat st;
dmon__watch_subdir subdir;
if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
}
} else {
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), watchdir);
if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
}
int dirlen = (int)strlen(subdir.rootdir);
if (subdir.rootdir[dirlen - 1] != '/') {
subdir.rootdir[dirlen] = '/';
subdir.rootdir[dirlen + 1] = '\0';
}
// check that the directory is not already added
for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) {
if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) {
_DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
}
const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir);
int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask);
if (wd == -1) {
_DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
stb_sb_push(watch->subdirs, subdir);
stb_sb_push(watch->wds, wd);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
{
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock)
pthread_mutex_lock(&_dmon.mutex);
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
char subdir[DMON_MAX_PATH];
dmon__strcpy(subdir, sizeof(subdir), watchdir);
if (strstr(subdir, watch->rootdir) == subdir) {
dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir));
}
int dirlen = (int)strlen(subdir);
if (subdir[dirlen - 1] != '/') {
subdir[dirlen] = '/';
subdir[dirlen + 1] = '\0';
}
int i, c = stb_sb_count(watch->subdirs);
for (i = 0; i < c; i++) {
if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) {
break;
}
}
if (i >= c) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
inotify_rm_watch(watch->fd, watch->wds[i]);
/* Remove entry from subdirs and wds by swapping position with the last entry */
watch->subdirs[i] = stb_sb_last(watch->subdirs);
stb_sb_pop(watch->subdirs);
watch->wds[i] = stb_sb_last(watch->wds);
stb_sb_pop(watch->wds);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
#endif // DMON_OS_LINUX
#endif // DMON_IMPL
#endif // __DMON_EXTRA_H__

1
lib/dmon/meson.build Normal file
View File

@ -0,0 +1 @@
lite_includes += include_directories('.')

View File

@ -22,6 +22,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
## septag/dmon
Copyright 2019 Sepehr Taghdisian. All rights reserved.
https://github.com/septag/dmon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Fira Sans ## Fira Sans
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.

View File

@ -1,6 +1,6 @@
project('lite-xl', project('lite-xl',
['c'], ['c'],
version : '2.0.2', version : '2.0.3',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.54', meson_version : '>= 0.54',
default_options : ['c_std=gnu11'] default_options : ['c_std=gnu11']
@ -23,6 +23,7 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
lite_includes = []
lite_cargs = [] lite_cargs = []
# On macos we need to use the SDL renderer to support retina displays # On macos we need to use the SDL renderer to support retina displays
if get_option('renderer') or host_machine.system() == 'darwin' if get_option('renderer') or host_machine.system() == 'darwin'
@ -45,6 +46,7 @@ endif
if not get_option('source-only') if not get_option('source-only')
libm = cc.find_library('m', required : false) libm = cc.find_library('m', required : false)
libdl = cc.find_library('dl', required : false) libdl = cc.find_library('dl', required : false)
threads_dep = dependency('threads')
lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'],
default_options: ['shared=false', 'use_readline=false', 'app=false'] default_options: ['shared=false', 'use_readline=false', 'app=false']
) )
@ -58,7 +60,7 @@ if not get_option('source-only')
] ]
) )
lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep] lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep, threads_dep]
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
# Note that we need to explicitly add the windows socket DLL because # Note that we need to explicitly add the windows socket DLL because
@ -118,10 +120,8 @@ configure_file(
install_dir : lite_datadir / 'core', install_dir : lite_datadir / 'core',
) )
#===============================================================================
# Targets
#===============================================================================
if not get_option('source-only') if not get_option('source-only')
subdir('lib/dmon')
subdir('src') subdir('src')
subdir('scripts') subdir('scripts')
endif endif

View File

@ -0,0 +1,54 @@
`core.set_project_dir`:
Reset project directories and set its directory.
It chdir into the directory, empty the `core.project_directories` and add
the given directory.
`core.add_project_directory`:
Add a new top-level directory to the project.
Also called from modules and commands outside core.init.
local function `scan_project_folder`:
Scan all files for a given top-level project directory.
Can emit a warning about file limit.
Called only from within core.init module.
`core.scan_project_subdir`: (before was named `core.scan_project_folder`)
scan a single folder, without recursion. Used when too many files.
Local function `scan_project_folder`:
Populate the project folder top directory. Done only once when the directory
is added to the project.
`core.add_project_directory`:
Add a new top-level folder to the project.
`core.set_project_dir`:
Set the initial project directory.
`core.dir_rescan_add_job`:
Add a job to rescan after an elapsed time a project's subdirectory to fix for any
changes.
Local function `rescan_project_subdir`:
Rescan a project's subdirectory, compare to the current version and patch the list if
a difference is found.
`core.project_scan_thread`:
Should disappear now that we use dmon.
`core.project_scan_topdir`:
New function to scan a top level project folder.
`config.project_scan_rate`:
`core.project_scan_thread_id`:
`core.reschedule_project_scan`:
`core.project_files_limit`:
A eliminer.
`core.get_project_files`:
To be fixed. Use `find_project_files_co` for a single directory
In TreeView remove usage of self.last to detect new scan that changed the files list.

View File

@ -23,6 +23,7 @@ Various scripts and configurations used to configure, build, and package Lite XL
Preferably not to be used in user systems. Preferably not to be used in user systems.
- **fontello-config.json**: Used by the icons generator. - **fontello-config.json**: Used by the icons generator.
- **generate_header.sh**: Generates a header file for native plugin API - **generate_header.sh**: Generates a header file for native plugin API
- **keymap-generator**: Generates a JSON file containing the keymap
[1]: https://github.com/LinusU/node-appdmg [1]: https://github.com/LinusU/node-appdmg
[2]: https://docs.appimage.org/ [2]: https://docs.appimage.org/

View File

@ -0,0 +1,714 @@
-- Module options:
local always_try_using_lpeg = true
local register_global_module_table = false
local global_module_name = 'json'
--[==[
David Kolf's JSON module for Lua 5.1/5.2
Version 2.5
For the documentation see the corresponding readme.txt or visit
<http://dkolf.de/src/dkjson-lua.fsl/>.
You can contact the author by sending an e-mail to 'david' at the
domain 'dkolf.de'.
Copyright (C) 2010-2013 David Heiko Kolf
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]==]
-- global dependencies:
local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
local error, require, pcall, select = error, require, pcall, select
local floor, huge = math.floor, math.huge
local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
string.rep, string.gsub, string.sub, string.byte, string.char,
string.find, string.len, string.format
local strmatch = string.match
local concat = table.concat
local json = { version = "dkjson 2.5" }
if register_global_module_table then
_G[global_module_name] = json
end
local _ENV = nil -- blocking globals in Lua 5.2
pcall (function()
-- Enable access to blocked metatables.
-- Don't worry, this module doesn't change anything in them.
local debmeta = require "debug".getmetatable
if debmeta then getmetatable = debmeta end
end)
json.null = setmetatable ({}, {
__tojson = function () return "null" end
})
local function isarray (tbl)
local max, n, arraylen = 0, 0, 0
for k,v in pairs (tbl) do
if k == 'n' and type(v) == 'number' then
arraylen = v
if v > max then
max = v
end
else
if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
return false
end
if k > max then
max = k
end
n = n + 1
end
end
if max > 10 and max > arraylen and max > n * 2 then
return false -- don't create an array with too many holes
end
return true, max
end
local escapecodes = {
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
}
local function escapeutf8 (uchar)
local value = escapecodes[uchar]
if value then
return value
end
local a, b, c, d = strbyte (uchar, 1, 4)
a, b, c, d = a or 0, b or 0, c or 0, d or 0
if a <= 0x7f then
value = a
elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
value = (a - 0xc0) * 0x40 + b - 0x80
elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
else
return ""
end
if value <= 0xffff then
return strformat ("\\u%.4x", value)
elseif value <= 0x10ffff then
-- encode as UTF-16 surrogate pair
value = value - 0x10000
local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
else
return ""
end
end
local function fsub (str, pattern, repl)
-- gsub always builds a new string in a buffer, even when no match
-- exists. First using find should be more efficient when most strings
-- don't contain the pattern.
if strfind (str, pattern) then
return gsub (str, pattern, repl)
else
return str
end
end
local function quotestring (value)
-- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
if strfind (value, "[\194\216\220\225\226\239]") then
value = fsub (value, "\194[\128-\159\173]", escapeutf8)
value = fsub (value, "\216[\128-\132]", escapeutf8)
value = fsub (value, "\220\143", escapeutf8)
value = fsub (value, "\225\158[\180\181]", escapeutf8)
value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
value = fsub (value, "\226\129[\160-\175]", escapeutf8)
value = fsub (value, "\239\187\191", escapeutf8)
value = fsub (value, "\239\191[\176-\191]", escapeutf8)
end
return "\"" .. value .. "\""
end
json.quotestring = quotestring
local function replace(str, o, n)
local i, j = strfind (str, o, 1, true)
if i then
return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
else
return str
end
end
-- locale independent num2str and str2num functions
local decpoint, numfilter
local function updatedecpoint ()
decpoint = strmatch(tostring(0.5), "([^05+])")
-- build a filter that can be used to remove group separators
numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
end
updatedecpoint()
local function num2str (num)
return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
end
local function str2num (str)
local num = tonumber(replace(str, ".", decpoint))
if not num then
updatedecpoint()
num = tonumber(replace(str, ".", decpoint))
end
return num
end
local function addnewline2 (level, buffer, buflen)
buffer[buflen+1] = "\n"
buffer[buflen+2] = strrep (" ", level)
buflen = buflen + 2
return buflen
end
function json.addnewline (state)
if state.indent then
state.bufferlen = addnewline2 (state.level or 0,
state.buffer, state.bufferlen or #(state.buffer))
end
end
local encode2 -- forward declaration
local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
local kt = type (key)
if kt ~= 'string' and kt ~= 'number' then
return nil, "type '" .. kt .. "' is not supported as a key by JSON."
end
if prev then
buflen = buflen + 1
buffer[buflen] = ","
end
if indent then
buflen = addnewline2 (level, buffer, buflen)
end
buffer[buflen+1] = quotestring (key)
buffer[buflen+2] = ":"
return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
end
local function appendcustom(res, buffer, state)
local buflen = state.bufferlen
if type (res) == 'string' then
buflen = buflen + 1
buffer[buflen] = res
end
return buflen
end
local function exception(reason, value, state, buffer, buflen, defaultmessage)
defaultmessage = defaultmessage or reason
local handler = state.exception
if not handler then
return nil, defaultmessage
else
state.bufferlen = buflen
local ret, msg = handler (reason, value, state, defaultmessage)
if not ret then return nil, msg or defaultmessage end
return appendcustom(ret, buffer, state)
end
end
function json.encodeexception(reason, value, state, defaultmessage)
return quotestring("<" .. defaultmessage .. ">")
end
encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
local valtype = type (value)
local valmeta = getmetatable (value)
valmeta = type (valmeta) == 'table' and valmeta -- only tables
local valtojson = valmeta and valmeta.__tojson
if valtojson then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
state.bufferlen = buflen
local ret, msg = valtojson (value, state)
if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
tables[value] = nil
buflen = appendcustom(ret, buffer, state)
elseif value == nil then
buflen = buflen + 1
buffer[buflen] = "null"
elseif valtype == 'number' then
local s
if value ~= value or value >= huge or -value >= huge then
-- This is the behaviour of the original JSON implementation.
s = "null"
else
s = num2str (value)
end
buflen = buflen + 1
buffer[buflen] = s
elseif valtype == 'boolean' then
buflen = buflen + 1
buffer[buflen] = value and "true" or "false"
elseif valtype == 'string' then
buflen = buflen + 1
buffer[buflen] = quotestring (value)
elseif valtype == 'table' then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
level = level + 1
local isa, n = isarray (value)
if n == 0 and valmeta and valmeta.__jsontype == 'object' then
isa = false
end
local msg
if isa then -- JSON array
buflen = buflen + 1
buffer[buflen] = "["
for i = 1, n do
buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
if i < n then
buflen = buflen + 1
buffer[buflen] = ","
end
end
buflen = buflen + 1
buffer[buflen] = "]"
else -- JSON object
local prev = false
buflen = buflen + 1
buffer[buflen] = "{"
local order = valmeta and valmeta.__jsonorder or globalorder
if order then
local used = {}
n = #order
for i = 1, n do
local k = order[i]
local v = value[k]
if v then
used[k] = true
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
prev = true -- add a seperator before the next element
end
end
for k,v in pairs (value) do
if not used[k] then
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
else -- unordered
for k,v in pairs (value) do
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
if indent then
buflen = addnewline2 (level - 1, buffer, buflen)
end
buflen = buflen + 1
buffer[buflen] = "}"
end
tables[value] = nil
else
return exception ('unsupported type', value, state, buffer, buflen,
"type '" .. valtype .. "' is not supported by JSON.")
end
return buflen
end
function json.encode (value, state)
state = state or {}
local oldbuffer = state.buffer
local buffer = oldbuffer or {}
state.buffer = buffer
updatedecpoint()
local ret, msg = encode2 (value, state.indent, state.level or 0,
buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
if not ret then
error (msg, 2)
elseif oldbuffer == buffer then
state.bufferlen = ret
return true
else
state.bufferlen = nil
state.buffer = nil
return concat (buffer)
end
end
local function loc (str, where)
local line, pos, linepos = 1, 1, 0
while true do
pos = strfind (str, "\n", pos, true)
if pos and pos < where then
line = line + 1
linepos = pos
pos = pos + 1
else
break
end
end
return "line " .. line .. ", column " .. (where - linepos)
end
local function unterminated (str, what, where)
return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
end
local function scanwhite (str, pos)
while true do
pos = strfind (str, "%S", pos)
if not pos then return nil end
local sub2 = strsub (str, pos, pos + 1)
if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
-- UTF-8 Byte Order Mark
pos = pos + 3
elseif sub2 == "//" then
pos = strfind (str, "[\n\r]", pos + 2)
if not pos then return nil end
elseif sub2 == "/*" then
pos = strfind (str, "*/", pos + 2)
if not pos then return nil end
pos = pos + 2
else
return pos
end
end
end
local escapechars = {
["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
}
local function unichar (value)
if value < 0 then
return nil
elseif value <= 0x007f then
return strchar (value)
elseif value <= 0x07ff then
return strchar (0xc0 + floor(value/0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0xffff then
return strchar (0xe0 + floor(value/0x1000),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0x10ffff then
return strchar (0xf0 + floor(value/0x40000),
0x80 + (floor(value/0x1000) % 0x40),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
else
return nil
end
end
local function scanstring (str, pos)
local lastpos = pos + 1
local buffer, n = {}, 0
while true do
local nextpos = strfind (str, "[\"\\]", lastpos)
if not nextpos then
return unterminated (str, "string", pos)
end
if nextpos > lastpos then
n = n + 1
buffer[n] = strsub (str, lastpos, nextpos - 1)
end
if strsub (str, nextpos, nextpos) == "\"" then
lastpos = nextpos + 1
break
else
local escchar = strsub (str, nextpos + 1, nextpos + 1)
local value
if escchar == "u" then
value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
if value then
local value2
if 0xD800 <= value and value <= 0xDBff then
-- we have the high surrogate of UTF-16. Check if there is a
-- low surrogate escaped nearby to combine them.
if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
else
value2 = nil -- in case it was out of range for a low surrogate
end
end
end
value = value and unichar (value)
if value then
if value2 then
lastpos = nextpos + 12
else
lastpos = nextpos + 6
end
end
end
end
if not value then
value = escapechars[escchar] or escchar
lastpos = nextpos + 2
end
n = n + 1
buffer[n] = value
end
end
if n == 1 then
return buffer[1], lastpos
elseif n > 1 then
return concat (buffer), lastpos
else
return "", lastpos
end
end
local scanvalue -- forward declaration
local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
local len = strlen (str)
local tbl, n = {}, 0
local pos = startpos + 1
if what == 'object' then
setmetatable (tbl, objectmeta)
else
setmetatable (tbl, arraymeta)
end
while true do
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
local char = strsub (str, pos, pos)
if char == closechar then
return tbl, pos + 1
end
local val1, err
val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
if char == ":" then
if val1 == nil then
return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
end
pos = scanwhite (str, pos + 1)
if not pos then return unterminated (str, what, startpos) end
local val2
val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
tbl[val1] = val2
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
else
n = n + 1
tbl[n] = val1
end
if char == "," then
pos = pos + 1
end
end
end
scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
pos = pos or 1
pos = scanwhite (str, pos)
if not pos then
return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
end
local char = strsub (str, pos, pos)
if char == "{" then
return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
elseif char == "[" then
return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
elseif char == "\"" then
return scanstring (str, pos)
else
local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
if pstart then
local number = str2num (strsub (str, pstart, pend))
if number then
return number, pend + 1
end
end
pstart, pend = strfind (str, "^%a%w*", pos)
if pstart then
local name = strsub (str, pstart, pend)
if name == "true" then
return true, pend + 1
elseif name == "false" then
return false, pend + 1
elseif name == "null" then
return nullval, pend + 1
end
end
return nil, pos, "no valid JSON value at " .. loc (str, pos)
end
end
local function optionalmetatables(...)
if select("#", ...) > 0 then
return ...
else
return {__jsontype = 'object'}, {__jsontype = 'array'}
end
end
function json.decode (str, pos, nullval, ...)
local objectmeta, arraymeta = optionalmetatables(...)
return scanvalue (str, pos, nullval, objectmeta, arraymeta)
end
function json.use_lpeg ()
local g = require ("lpeg")
if g.version() == "0.11" then
error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
end
local pegmatch = g.match
local P, S, R = g.P, g.S, g.R
local function ErrorCall (str, pos, msg, state)
if not state.msg then
state.msg = msg .. " at " .. loc (str, pos)
state.pos = pos
end
return false
end
local function Err (msg)
return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
end
local SingleLineComment = P"//" * (1 - S"\n\r")^0
local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
local PlainChar = 1 - S"\"\\\n\r"
local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
local HexDigit = R("09", "af", "AF")
local function UTF16Surrogate (match, pos, high, low)
high, low = tonumber (high, 16), tonumber (low, 16)
if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
else
return false
end
end
local function UTF16BMP (hex)
return unichar (tonumber (hex, 16))
end
local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
local Char = UnicodeEscape + EscapeSequence + PlainChar
local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
local Fractal = P"." * R"09"^0
local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
local SimpleValue = Number + String + Constant
local ArrayContent, ObjectContent
-- The functions parsearray and parseobject parse only a single value/pair
-- at a time and store them directly to avoid hitting the LPeg limits.
local function parsearray (str, pos, nullval, state)
local obj, cont
local npos
local t, nt = {}, 0
repeat
obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
if not npos then break end
pos = npos
nt = nt + 1
t[nt] = obj
until cont == 'last'
return pos, setmetatable (t, state.arraymeta)
end
local function parseobject (str, pos, nullval, state)
local obj, key, cont
local npos
local t = {}
repeat
key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
if not npos then break end
pos = npos
t[key] = obj
until cont == 'last'
return pos, setmetatable (t, state.objectmeta)
end
local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
local Value = Space * (Array + Object + SimpleValue)
local ExpectedValue = Value + Space * Err "value expected"
ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local DecodeValue = ExpectedValue * g.Cp ()
function json.decode (str, pos, nullval, ...)
local state = {}
state.objectmeta, state.arraymeta = optionalmetatables(...)
local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
if state.msg then
return nil, state.pos, state.msg
else
return obj, retpos
end
end
-- use this function only once:
json.use_lpeg = function () return json end
json.using_lpeg = true
return json -- so you can get the module using json = require "dkjson".use_lpeg()
end
if always_try_using_lpeg then
pcall (json.use_lpeg)
end
return json

View File

@ -0,0 +1,75 @@
#!/usr/bin/env lua
local dkjson = require "dkjson"
local function load_keymap(target, target_map, macos)
_G.MACOS = macos
package.loaded["core.keymap"] = nil
local keymap = require "core.keymap"
if target then
keymap.map = {}
require(target)
end
target_map = target_map or {}
-- keymap.reverse_map does not do this?
for key, actions in pairs(keymap.map) do
for _, action in ipairs(actions) do
target_map[action] = target_map[action] or {}
table.insert(target_map[action], key)
end
end
return target_map
end
local function normalize(map)
local result = {}
for action, keys in pairs(map) do
local uniq = {}
local r = { combination = {}, action = action }
for _, v in ipairs(keys) do
if not uniq[v] then
uniq[v] = true
r.combination[#r.combination+1] = v
end
end
result[#result+1] = r
end
table.sort(result, function(a, b) return a.action < b.action end)
return result
end
local function process_module(mod, filename)
local map = {}
load_keymap(mod, map)
load_keymap(mod, map, true)
map = normalize(map)
local f = assert(io.open(filename, "wb"))
f:write(dkjson.encode(map, { indent = true }))
f:close()
end
print("Warning: this is not guaranteed to work outside lite-xl's own keymap. Proceed with caution")
local LITE_ROOT = arg[1]
if not LITE_ROOT then
error("LITE_ROOT is not given")
end
package.path = package.path .. ";" .. LITE_ROOT .. "/?.lua;" .. LITE_ROOT .. "/?/init.lua"
-- fix core.command (because we don't want load the entire thing)
package.loaded["core.command"] = {}
if #arg > 1 then
for i = 2, #arg do
process_module(arg[i], arg[i] .. ".json")
print(string.format("Exported keymap in %q.", arg[i]))
end
else
process_module(nil, "core.keymap.json")
print("Exported the default keymap.")
end

View File

@ -186,7 +186,7 @@ main() {
rm -rf "${dest_dir}" rm -rf "${dest_dir}"
DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}" DESTDIR="$(pwd)/${dest_dir}" meson install --skip-subprojects -C "${build_dir}"
local data_dir="$(pwd)/${dest_dir}/data" local data_dir="$(pwd)/${dest_dir}/data"
local exe_file="$(pwd)/${dest_dir}/lite-xl" local exe_file="$(pwd)/${dest_dir}/lite-xl"

View File

@ -68,8 +68,11 @@ static int f_pcre_match(lua_State *L) {
int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL); int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL);
if (rc < 0) { if (rc < 0) {
pcre2_match_data_free(md); pcre2_match_data_free(md);
if (rc != PCRE2_ERROR_NOMATCH) if (rc != PCRE2_ERROR_NOMATCH) {
luaL_error(L, "regex matching error %d", rc); PCRE2_UCHAR buffer[120];
pcre2_get_error_message(rc, buffer, sizeof(buffer));
luaL_error(L, "regex matching error %d: %s", rc, buffer);
}
return 0; return 0;
} }
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md);

View File

@ -6,16 +6,18 @@ static int f_font_load(lua_State *L) {
const char *filename = luaL_checkstring(L, 1); const char *filename = luaL_checkstring(L, 1);
float size = luaL_checknumber(L, 2); float size = luaL_checknumber(L, 2);
unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0; unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0;
bool subpixel = true; ERenFontAntialiasing font_antialiasing = FONT_ANTIALIASING_SUBPIXEL;
if (lua_gettop(L) > 2 && lua_istable(L, 3)) { if (lua_gettop(L) > 2 && lua_istable(L, 3)) {
lua_getfield(L, 3, "antialiasing"); lua_getfield(L, 3, "antialiasing");
if (lua_isstring(L, -1)) { if (lua_isstring(L, -1)) {
const char *antialiasing = lua_tostring(L, -1); const char *antialiasing = lua_tostring(L, -1);
if (antialiasing) { if (antialiasing) {
if (strcmp(antialiasing, "grayscale") == 0) { if (strcmp(antialiasing, "none") == 0) {
subpixel = false; font_antialiasing = FONT_ANTIALIASING_NONE;
} else if (strcmp(antialiasing, "grayscale") == 0) {
font_antialiasing = FONT_ANTIALIASING_GRAYSCALE;
} else if (strcmp(antialiasing, "subpixel") == 0) { } else if (strcmp(antialiasing, "subpixel") == 0) {
subpixel = true; font_antialiasing = FONT_ANTIALIASING_SUBPIXEL;
} else { } else {
return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing); return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing);
} }
@ -48,7 +50,7 @@ static int f_font_load(lua_State *L) {
lua_pop(L, 5); lua_pop(L, 5);
} }
RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_load(filename, size, subpixel, font_hinting, font_style); *font = ren_font_load(filename, size, font_antialiasing, font_hinting, font_style);
if (!*font) if (!*font)
return luaL_error(L, "failed to load font"); return luaL_error(L, "failed to load font");
luaL_setmetatable(L, API_TYPE_FONT); luaL_setmetatable(L, API_TYPE_FONT);
@ -174,23 +176,30 @@ static int f_end_frame(lua_State *L) {
} }
static RenRect rect_to_grid(lua_Number x, lua_Number y, lua_Number w, lua_Number h) {
int x1 = (int) (x + 0.5), y1 = (int) (y + 0.5);
int x2 = (int) (x + w + 0.5), y2 = (int) (y + h + 0.5);
return (RenRect) {x1, y1, x2 - x1, y2 - y1};
}
static int f_set_clip_rect(lua_State *L) { static int f_set_clip_rect(lua_State *L) {
RenRect rect; lua_Number x = luaL_checknumber(L, 1);
rect.x = luaL_checknumber(L, 1); lua_Number y = luaL_checknumber(L, 2);
rect.y = luaL_checknumber(L, 2); lua_Number w = luaL_checknumber(L, 3);
rect.width = luaL_checknumber(L, 3); lua_Number h = luaL_checknumber(L, 4);
rect.height = luaL_checknumber(L, 4); RenRect rect = rect_to_grid(x, y, w, h);
rencache_set_clip_rect(rect); rencache_set_clip_rect(rect);
return 0; return 0;
} }
static int f_draw_rect(lua_State *L) { static int f_draw_rect(lua_State *L) {
RenRect rect; lua_Number x = luaL_checknumber(L, 1);
rect.x = luaL_checknumber(L, 1); lua_Number y = luaL_checknumber(L, 2);
rect.y = luaL_checknumber(L, 2); lua_Number w = luaL_checknumber(L, 3);
rect.width = luaL_checknumber(L, 3); lua_Number h = luaL_checknumber(L, 4);
rect.height = luaL_checknumber(L, 4); RenRect rect = rect_to_grid(x, y, w, h);
RenColor color = checkcolor(L, 5, 255); RenColor color = checkcolor(L, 5, 255);
rencache_draw_rect(rect, color); rencache_draw_rect(rect, color);
return 0; return 0;

View File

@ -6,11 +6,14 @@
#include <errno.h> #include <errno.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "api.h" #include "api.h"
#include "dirmonitor.h"
#include "rencache.h" #include "rencache.h"
#ifdef _WIN32 #ifdef _WIN32
#include <direct.h> #include <direct.h>
#include <windows.h> #include <windows.h>
#include <fileapi.h> #include <fileapi.h>
#elif __linux__
#include <sys/vfs.h>
#endif #endif
extern SDL_Window *window; extern SDL_Window *window;
@ -18,9 +21,11 @@ extern SDL_Window *window;
static const char* button_name(int button) { static const char* button_name(int button) {
switch (button) { switch (button) {
case 1 : return "left"; case SDL_BUTTON_LEFT : return "left";
case 2 : return "middle"; case SDL_BUTTON_MIDDLE : return "middle";
case 3 : return "right"; case SDL_BUTTON_RIGHT : return "right";
case SDL_BUTTON_X1 : return "x";
case SDL_BUTTON_X2 : return "y";
default : return "?"; default : return "?";
} }
} }
@ -236,6 +241,26 @@ top:
lua_pushnumber(L, e.wheel.y); lua_pushnumber(L, e.wheel.y);
return 2; return 2;
case SDL_USEREVENT:
lua_pushstring(L, "dirchange");
lua_pushnumber(L, e.user.code >> 16);
switch (e.user.code & 0xffff) {
case DMON_ACTION_DELETE:
lua_pushstring(L, "delete");
break;
case DMON_ACTION_CREATE:
lua_pushstring(L, "create");
break;
case DMON_ACTION_MODIFY:
lua_pushstring(L, "modify");
break;
default:
return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff);
}
lua_pushstring(L, e.user.data1);
free(e.user.data1);
return 4;
default: default:
goto top; goto top;
} }
@ -524,6 +549,45 @@ static int f_get_file_info(lua_State *L) {
return 1; return 1;
} }
#if __linux__
// https://man7.org/linux/man-pages/man2/statfs.2.html
struct f_type_names {
uint32_t magic;
const char *name;
};
static struct f_type_names fs_names[] = {
{ 0xef53, "ext2/ext3" },
{ 0x6969, "nfs" },
{ 0x65735546, "fuse" },
{ 0x517b, "smb" },
{ 0xfe534d42, "smb2" },
{ 0x52654973, "reiserfs" },
{ 0x01021994, "tmpfs" },
{ 0x858458f6, "ramfs" },
{ 0x5346544e, "ntfs" },
{ 0x0, NULL },
};
static int f_get_fs_type(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
struct statfs buf;
int status = statfs(path, &buf);
if (status != 0) {
return luaL_error(L, "error calling statfs on %s", path);
}
for (int i = 0; fs_names[i].magic; i++) {
if (fs_names[i].magic == buf.f_type) {
lua_pushstring(L, fs_names[i].name);
return 1;
}
}
lua_pushstring(L, "unknown");
return 1;
}
#endif
static int f_mkdir(lua_State *L) { static int f_mkdir(lua_State *L) {
const char *path = luaL_checkstring(L, 1); const char *path = luaL_checkstring(L, 1);
@ -591,56 +655,32 @@ static int f_exec(lua_State *L) {
return 0; return 0;
} }
static int f_fuzzy_match(lua_State *L) { static int f_fuzzy_match(lua_State *L) {
size_t strLen, ptnLen; size_t strLen, ptnLen;
const char *str = luaL_checklstring(L, 1, &strLen); const char *str = luaL_checklstring(L, 1, &strLen);
const char *ptn = luaL_checklstring(L, 2, &ptnLen); const char *ptn = luaL_checklstring(L, 2, &ptnLen);
bool files = false; // If true match things *backwards*. This allows for better matching on filenames than the above
if (lua_gettop(L) > 2 && lua_isboolean(L,3))
files = lua_toboolean(L, 3);
int score = 0;
int run = 0;
// Match things *backwards*. This allows for better matching on filenames than the above
// function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh // function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh
// as the first result, rather than src/renderer.c. Clearly that's wrong. // as the first result, rather than src/renderer.c. Clearly that's wrong.
if (files) { bool files = lua_gettop(L) > 2 && lua_isboolean(L,3) && lua_toboolean(L, 3);
const char* strEnd = str + strLen - 1; int score = 0, run = 0, increment = files ? -1 : 1;
const char* ptnEnd = ptn + ptnLen - 1; const char* strTarget = files ? str + strLen - 1 : str;
while (strEnd >= str && ptnEnd >= ptn) { const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn;
while (*strEnd == ' ') { strEnd--; } while (strTarget >= str && ptnTarget >= ptn && *strTarget && *ptnTarget) {
while (*ptnEnd == ' ') { ptnEnd--; } while (strTarget >= str && *strTarget == ' ') { strTarget += increment; }
if (tolower(*strEnd) == tolower(*ptnEnd)) { while (ptnTarget >= ptn && *ptnTarget == ' ') { ptnTarget += increment; }
score += run * 10 - (*strEnd != *ptnEnd); if (tolower(*strTarget) == tolower(*ptnTarget)) {
score += run * 10 - (*strTarget != *ptnTarget);
run++; run++;
ptnEnd--; ptnTarget += increment;
} else { } else {
score -= 10; score -= 10;
run = 0; run = 0;
} }
strEnd--; strTarget += increment;
} }
if (ptnEnd >= ptn) { return 0; } if (ptnTarget >= ptn && *ptnTarget) { return 0; }
} else { lua_pushnumber(L, score - (int)strLen * 10);
while (*str && *ptn) {
while (*str == ' ') { str++; }
while (*ptn == ' ') { ptn++; }
if (tolower(*str) == tolower(*ptn)) {
score += run * 10 - (*str != *ptn);
run++;
ptn++;
} else {
score -= 10;
run = 0;
}
str++;
}
if (*ptn) { return 0; }
}
lua_pushnumber(L, score - (int)strLen);
return 1; return 1;
} }
@ -738,6 +778,91 @@ static int f_load_native_plugin(lua_State *L) {
return result; return result;
} }
static int f_watch_dir(lua_State *L) {
const char *path = luaL_checkstring(L, 1);
const int recursive = lua_toboolean(L, 2);
uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0);
dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL);
if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); }
lua_pushnumber(L, watch_id.id);
return 1;
}
#if __linux__
static int f_watch_dir_add(lua_State *L) {
dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1);
const char *subdir = luaL_checkstring(L, 2);
lua_pushboolean(L, dmon_watch_add(watch_id, subdir));
return 1;
}
static int f_watch_dir_rm(lua_State *L) {
dmon_watch_id watch_id;
watch_id.id = luaL_checkinteger(L, 1);
const char *subdir = luaL_checkstring(L, 2);
lua_pushboolean(L, dmon_watch_rm(watch_id, subdir));
return 1;
}
#endif
#ifdef _WIN32
#define PATHSEP '\\'
#else
#define PATHSEP '/'
#endif
/* Special purpose filepath compare function. Corresponds to the
order used in the TreeView view of the project's files. Returns true iff
path1 < path2 in the TreeView order. */
static int f_path_compare(lua_State *L) {
const char *path1 = luaL_checkstring(L, 1);
const char *type1_s = luaL_checkstring(L, 2);
const char *path2 = luaL_checkstring(L, 3);
const char *type2_s = luaL_checkstring(L, 4);
const int len1 = strlen(path1), len2 = strlen(path2);
int type1 = strcmp(type1_s, "dir") != 0;
int type2 = strcmp(type2_s, "dir") != 0;
/* Find the index of the common part of the path. */
int offset = 0, i;
for (i = 0; i < len1 && i < len2; i++) {
if (path1[i] != path2[i]) break;
if (path1[i] == PATHSEP) {
offset = i + 1;
}
}
/* If a path separator is present in the name after the common part we consider
the entry like a directory. */
if (strchr(path1 + offset, PATHSEP)) {
type1 = 0;
}
if (strchr(path2 + offset, PATHSEP)) {
type2 = 0;
}
/* If types are different "dir" types comes before "file" types. */
if (type1 != type2) {
lua_pushboolean(L, type1 < type2);
return 1;
}
/* If types are the same compare the files' path alphabetically. */
int cfr = 0;
int len_min = (len1 < len2 ? len1 : len2);
for (int j = offset; j <= len_min; j++) {
if (path1[j] == path2[j]) continue;
if (path1[j] == 0 || path2[j] == 0) {
cfr = (path1[j] == 0);
} else if (path1[j] == PATHSEP || path2[j] == PATHSEP) {
/* For comparison we treat PATHSEP as if it was the string terminator. */
cfr = (path1[j] == PATHSEP);
} else {
cfr = (path1[j] < path2[j]);
}
break;
}
lua_pushboolean(L, cfr);
return 1;
}
static const luaL_Reg lib[] = { static const luaL_Reg lib[] = {
{ "poll_event", f_poll_event }, { "poll_event", f_poll_event },
@ -766,6 +891,13 @@ static const luaL_Reg lib[] = {
{ "fuzzy_match", f_fuzzy_match }, { "fuzzy_match", f_fuzzy_match },
{ "set_window_opacity", f_set_window_opacity }, { "set_window_opacity", f_set_window_opacity },
{ "load_native_plugin", f_load_native_plugin }, { "load_native_plugin", f_load_native_plugin },
{ "watch_dir", f_watch_dir },
{ "path_compare", f_path_compare },
#if __linux__
{ "watch_dir_add", f_watch_dir_add },
{ "watch_dir_rm", f_watch_dir_rm },
{ "get_fs_type", f_get_fs_type },
#endif
{ NULL, NULL } { NULL, NULL }
}; };

59
src/dirmonitor.c Normal file
View File

@ -0,0 +1,59 @@
#include <stdio.h>
#include <string.h>
#include <SDL.h>
#define DMON_IMPL
#include "dmon.h"
#include "dmon_extra.h"
#include "dirmonitor.h"
static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) {
SDL_Event ev;
const int size = strlen(filepath) + 1;
/* The string allocated below should be deallocated as soon as the event is
treated in the SDL main loop. */
char *new_filepath = malloc(size);
if (!new_filepath) return;
memcpy(new_filepath, filepath, size);
#ifdef _WIN32
for (int i = 0; i < size; i++) {
if (new_filepath[i] == '/') {
new_filepath[i] = '\\';
}
}
#endif
SDL_zero(ev);
ev.type = SDL_USEREVENT;
ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff);
ev.user.data1 = new_filepath;
SDL_PushEvent(&ev);
}
void dirmonitor_init() {
dmon_init();
/* In theory we should register our user event but since we
have just one type of user event this is not really needed. */
/* sdl_dmon_event_type = SDL_RegisterEvents(1); */
}
void dirmonitor_deinit() {
dmon_deinit();
}
void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
const char *filepath, const char *oldfilepath, void *user)
{
(void) rootdir;
(void) user;
switch (action) {
case DMON_ACTION_MOVE:
send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath);
send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath);
break;
default:
send_sdl_event(watch_id, action, filepath);
}
}

15
src/dirmonitor.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef DIRMONITOR_H
#define DIRMONITOR_H
#include <stdint.h>
#include "dmon.h"
#include "dmon_extra.h"
void dirmonitor_init();
void dirmonitor_deinit();
void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
const char *filepath, const char *oldfilepath, void *user);
#endif

View File

@ -14,6 +14,8 @@
#include <mach-o/dyld.h> #include <mach-o/dyld.h>
#endif #endif
#include "dirmonitor.h"
SDL_Window *window; SDL_Window *window;
@ -106,6 +108,8 @@ int main(int argc, char **argv) {
SDL_DisplayMode dm; SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm); SDL_GetCurrentDisplayMode(0, &dm);
dirmonitor_init();
window = SDL_CreateWindow( window = SDL_CreateWindow(
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8, "", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
@ -188,6 +192,7 @@ init_lua:
lua_close(L); lua_close(L);
ren_free_window_resources(); ren_free_window_resources();
dirmonitor_deinit();
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }

View File

@ -4,6 +4,7 @@ lite_sources = [
'api/regex.c', 'api/regex.c',
'api/system.c', 'api/system.c',
'api/process.c', 'api/process.c',
'dirmonitor.c',
'renderer.c', 'renderer.c',
'renwindow.c', 'renwindow.c',
'rencache.c', 'rencache.c',
@ -18,11 +19,11 @@ elif host_machine.system() == 'darwin'
lite_sources += 'bundle_open.m' lite_sources += 'bundle_open.m'
endif endif
lite_include = include_directories('.') lite_includes += include_directories('.')
executable('lite-xl', executable('lite-xl',
lite_sources + lite_rc, lite_sources + lite_rc,
include_directories: [lite_include], include_directories: lite_includes,
dependencies: lite_deps, dependencies: lite_deps,
c_args: lite_cargs, c_args: lite_cargs,
objc_args: lite_cargs, objc_args: lite_cargs,

View File

@ -123,7 +123,9 @@ void rencache_set_clip_rect(RenRect rect) {
void rencache_draw_rect(RenRect rect, RenColor color) { void rencache_draw_rect(RenRect rect, RenColor color) {
if (!rects_overlap(screen_rect, rect)) { return; } if (!rects_overlap(screen_rect, rect) || rect.width == 0 || rect.height == 0) {
return;
}
Command *cmd = push_command(DRAW_RECT, COMMAND_BARE_SIZE); Command *cmd = push_command(DRAW_RECT, COMMAND_BARE_SIZE);
if (cmd) { if (cmd) {
cmd->rect = rect; cmd->rect = rect;

View File

@ -43,7 +43,7 @@ typedef struct RenFont {
GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS]; GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS];
float size, space_advance, tab_advance; float size, space_advance, tab_advance;
short max_height; short max_height;
bool subpixel; ERenFontAntialiasing antialiasing;
ERenFontHinting hinting; ERenFontHinting hinting;
unsigned char style; unsigned char style;
char path[0]; char path[0];
@ -66,16 +66,16 @@ static const char* utf8_to_codepoint(const char *p, unsigned *dst) {
} }
static int font_set_load_options(RenFont* font) { static int font_set_load_options(RenFont* font) {
switch (font->hinting) { int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO
case FONT_HINTING_SLIGHT: return FT_LOAD_TARGET_LIGHT | FT_LOAD_FORCE_AUTOHINT; : (font->hinting == FONT_HINTING_SLIGHT ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_NORMAL);
case FONT_HINTING_FULL: return FT_LOAD_TARGET_NORMAL | FT_LOAD_FORCE_AUTOHINT; int hinting = font->hinting == FONT_HINTING_NONE ? FT_LOAD_NO_HINTING : FT_LOAD_FORCE_AUTOHINT;
case FONT_HINTING_NONE: return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING; return load_target | hinting;
}
return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING;
} }
static int font_set_render_options(RenFont* font) { static int font_set_render_options(RenFont* font) {
if (font->subpixel) { if (font->antialiasing == FONT_ANTIALIASING_NONE)
return FT_RENDER_MODE_MONO;
if (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL) {
unsigned char weights[] = { 0x10, 0x40, 0x70, 0x40, 0x10 } ; unsigned char weights[] = { 0x10, 0x40, 0x70, 0x40, 0x10 } ;
switch (font->hinting) { switch (font->hinting) {
case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break; case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break;
@ -106,8 +106,8 @@ static int font_set_style(FT_Outline* outline, int x_translation, unsigned char
static void font_load_glyphset(RenFont* font, int idx) { static void font_load_glyphset(RenFont* font, int idx) {
unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font); unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font);
int bitmaps_cached = font->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1; int bitmaps_cached = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1;
unsigned int byte_width = font->subpixel ? 3 : 1; unsigned int byte_width = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1;
for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) { for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) {
GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet))); GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet)));
font->sets[j][idx] = set; font->sets[j][idx] = set;
@ -117,13 +117,15 @@ static void font_load_glyphset(RenFont* font, int idx) {
continue; continue;
FT_GlyphSlot slot = font->face->glyph; FT_GlyphSlot slot = font->face->glyph;
int glyph_width = slot->bitmap.width / byte_width; int glyph_width = slot->bitmap.width / byte_width;
if (font->antialiasing == FONT_ANTIALIASING_NONE)
glyph_width *= 8;
set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f}; set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f};
pen_x += glyph_width; pen_x += glyph_width;
font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height; font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height;
} }
if (pen_x == 0) if (pen_x == 0)
continue; continue;
set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->subpixel ? 24 : 8, 0, 0, 0, 0)); set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 24 : 8, 0, 0, 0, 0));
unsigned char* pixels = set->surface->pixels; unsigned char* pixels = set->surface->pixels;
for (int i = 0; i < MAX_GLYPHSET; ++i) { for (int i = 0; i < MAX_GLYPHSET; ++i) {
int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET);
@ -136,6 +138,13 @@ static void font_load_glyphset(RenFont* font, int idx) {
for (int line = 0; line < slot->bitmap.rows; ++line) { for (int line = 0; line < slot->bitmap.rows; ++line) {
int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width; int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width;
int source_offset = line * slot->bitmap.pitch; int source_offset = line * slot->bitmap.pitch;
if (font->antialiasing == FONT_ANTIALIASING_NONE) {
for (int column = 0; column < slot->bitmap.width; ++column) {
int current_source_offset = source_offset + (column / 8);
int source_pixel = slot->bitmap.buffer[current_source_offset];
pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7;
}
} else
memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width);
} }
} }
@ -144,9 +153,9 @@ static void font_load_glyphset(RenFont* font, int idx) {
static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) { static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) {
int idx = (codepoint >> 8) % MAX_LOADABLE_GLYPHSETS; int idx = (codepoint >> 8) % MAX_LOADABLE_GLYPHSETS;
if (!font->sets[font->subpixel ? subpixel_idx : 0][idx]) if (!font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx])
font_load_glyphset(font, idx); font_load_glyphset(font, idx);
return font->sets[font->subpixel ? subpixel_idx : 0][idx]; return font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx];
} }
static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) { static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) {
@ -163,7 +172,7 @@ static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFo
return fonts[0]; return fonts[0];
} }
RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned char hinting, unsigned char style) { RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) {
FT_Face face; FT_Face face;
if (FT_New_Face( library, path, 0, &face)) if (FT_New_Face( library, path, 0, &face))
return NULL; return NULL;
@ -175,7 +184,7 @@ RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned cha
strcpy(font->path, path); strcpy(font->path, path);
font->face = face; font->face = face;
font->size = size; font->size = size;
font->subpixel = subpixel; font->antialiasing = antialiasing;
font->hinting = hinting; font->hinting = hinting;
font->style = style; font->style = style;
font->space_advance = (int)font_get_glyphset(font, ' ', 0)->metrics[' '].xadvance; font->space_advance = (int)font_get_glyphset(font, ' ', 0)->metrics[' '].xadvance;
@ -187,7 +196,7 @@ RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned cha
} }
RenFont* ren_font_copy(RenFont* font, float size) { RenFont* ren_font_copy(RenFont* font, float size) {
return ren_font_load(font->path, size, font->subpixel, font->hinting, font->style); return ren_font_load(font->path, size, font->antialiasing, font->hinting, font->style);
} }
void ren_font_free(RenFont* font) { void ren_font_free(RenFont* font) {
@ -206,7 +215,7 @@ void ren_font_free(RenFont* font) {
void ren_font_group_set_tab_size(RenFont **fonts, int n) { void ren_font_group_set_tab_size(RenFont **fonts, int n) {
for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) { for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) {
for (int i = 0; i < (fonts[j]->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i)
font_get_glyphset(fonts[j], '\t', i)->metrics['\t'].xadvance = fonts[j]->space_advance * n; font_get_glyphset(fonts[j], '\t', i)->metrics['\t'].xadvance = fonts[j]->space_advance * n;
} }
} }
@ -247,6 +256,7 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor
const char* end = text + strlen(text); const char* end = text + strlen(text);
unsigned char* destination_pixels = surface->pixels; unsigned char* destination_pixels = surface->pixels;
int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height; int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height;
while (text < end) { while (text < end) {
unsigned int codepoint, r, g, b; unsigned int codepoint, r, g, b;
text = utf8_to_codepoint(text, &codepoint); text = utf8_to_codepoint(text, &codepoint);
@ -273,15 +283,15 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor
glyph_start += offset; glyph_start += offset;
} }
unsigned int* destination_pixel = (unsigned int*)&destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]; unsigned int* destination_pixel = (unsigned int*)&destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel];
unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->subpixel ? 3 : 1)]; unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1)];
for (int x = glyph_start; x < glyph_end; ++x) { for (int x = glyph_start; x < glyph_end; ++x) {
unsigned int destination_color = *destination_pixel; unsigned int destination_color = *destination_pixel;
SDL_Color dst = { (destination_color >> 16) & 0xFF, (destination_color >> 8) & 0xFF, (destination_color >> 0) & 0xFF, (destination_color >> 24) & 0xFF }; SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift };
SDL_Color src = { *(font->subpixel ? source_pixel++ : source_pixel), *(font->subpixel ? source_pixel++ : source_pixel), *source_pixel++ }; SDL_Color src = { *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *source_pixel++ };
r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025; r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025;
g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025;
b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025;
*destination_pixel++ = dst.a << 24 | r << 16 | g << 8 | b; *destination_pixel++ = dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift;
} }
} }
} }
@ -324,14 +334,15 @@ void ren_draw_rect(RenRect rect, RenColor color) {
RenColor *d = (RenColor*) surface->pixels; RenColor *d = (RenColor*) surface->pixels;
d += x1 + y1 * surface->w; d += x1 + y1 * surface->w;
int dr = surface->w - (x2 - x1); int dr = surface->w - (x2 - x1);
unsigned int translated = SDL_MapRGB(surface->format, color.r, color.g, color.b);
if (color.a == 0xff) { if (color.a == 0xff) {
SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 }; SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 };
SDL_FillRect(surface, &rect, SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a)); SDL_FillRect(surface, &rect, translated);
} else { } else {
RenColor translated_color = (RenColor){ translated & 0xFF, (translated >> 8) & 0xFF, (translated >> 16) & 0xFF, color.a };
for (int j = y1; j < y2; j++) { for (int j = y1; j < y2; j++) {
for (int i = x1; i < x2; i++, d++) for (int i = x1; i < x2; i++, d++)
*d = blend_pixel(*d, color); *d = blend_pixel(*d, translated_color);
d += dr; d += dr;
} }
} }

View File

@ -8,11 +8,12 @@
#define FONT_FALLBACK_MAX 4 #define FONT_FALLBACK_MAX 4
typedef struct RenFont RenFont; typedef struct RenFont RenFont;
typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting; typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting;
typedef enum { FONT_ANTIALIASING_NONE, FONT_ANTIALIASING_GRAYSCALE, FONT_ANTIALIASING_SUBPIXEL } ERenFontAntialiasing;
typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4 } ERenFontStyle; typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4 } ERenFontStyle;
typedef struct { uint8_t b, g, r, a; } RenColor; typedef struct { uint8_t b, g, r, a; } RenColor;
typedef struct { int x, y, width, height; } RenRect; typedef struct { int x, y, width, height; } RenRect;
RenFont* ren_font_load(const char *filename, float size, bool subpixel, unsigned char hinting, unsigned char style); RenFont* ren_font_load(const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style);
RenFont* ren_font_copy(RenFont* font, float size); RenFont* ren_font_copy(RenFont* font, float size);
void ren_font_free(RenFont *font); void ren_font_free(RenFont *font);
int ren_font_group_get_tab_size(RenFont **font); int ren_font_group_get_tab_size(RenFont **font);