Merge branch 'master' into plugin_api_h_fix
This commit is contained in:
commit
8120654c59
|
@ -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/**/*
|
|
@ -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
|
51
README.md
51
README.md
|
@ -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
|
||||
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
|
||||
|
||||
Any additional functionality that can be added through a plugin should be done
|
||||
|
|
26
changelog.md
26
changelog.md
|
@ -1,5 +1,31 @@
|
|||
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
|
||||
|
||||
Fix problem project directory when starting the application from Launcher on macOS.
|
||||
|
|
|
@ -41,11 +41,14 @@ function command.get_all_valid()
|
|||
return res
|
||||
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]
|
||||
if cmd and cmd.predicate() then
|
||||
cmd.perform()
|
||||
if cmd and cmd.predicate(...) then
|
||||
cmd.perform(...)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
|
|
@ -6,10 +6,12 @@ local LogView = require "core.logview"
|
|||
|
||||
|
||||
local fullscreen = false
|
||||
local restore_title_view = false
|
||||
|
||||
local function suggest_directory(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
|
||||
|
||||
command.add(nil, {
|
||||
|
@ -27,9 +29,12 @@ command.add(nil, {
|
|||
|
||||
["core:toggle-fullscreen"] = function()
|
||||
fullscreen = not fullscreen
|
||||
if fullscreen then
|
||||
restore_title_view = core.title_view.visible
|
||||
end
|
||||
system.set_window_mode(fullscreen and "fullscreen" or "normal")
|
||||
core.show_title_bar(not fullscreen)
|
||||
core.title_view:configure_hit_test(not fullscreen)
|
||||
core.show_title_bar(not fullscreen and restore_title_view)
|
||||
core.title_view:configure_hit_test(not fullscreen and restore_title_view)
|
||||
end,
|
||||
|
||||
["core:reload-module"] = function()
|
||||
|
@ -66,8 +71,8 @@ command.add(nil, {
|
|||
end,
|
||||
|
||||
["core:find-file"] = function()
|
||||
if core.project_files_limit then
|
||||
return command.perform "core:open-file"
|
||||
if not core.project_files_number() then
|
||||
return command.perform "core:open-file"
|
||||
end
|
||||
local files = {}
|
||||
for dir, item in core.get_project_files() do
|
||||
|
@ -149,7 +154,7 @@ command.add(nil, {
|
|||
["core:change-project-folder"] = function()
|
||||
local dirname = common.dirname(core.project_dir)
|
||||
if dirname then
|
||||
core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
|
||||
core.command_view:set_text(common.home_encode(dirname))
|
||||
end
|
||||
core.command_view:enter("Change Project Folder", function(text, item)
|
||||
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()
|
||||
local dirname = common.dirname(core.project_dir)
|
||||
if dirname then
|
||||
core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
|
||||
core.command_view:set_text(common.home_encode(dirname))
|
||||
end
|
||||
core.command_view:enter("Open Project", function(text, item)
|
||||
text = common.home_expand(item and item.text or text)
|
||||
|
@ -191,8 +196,6 @@ command.add(nil, {
|
|||
return
|
||||
end
|
||||
core.add_project_directory(system.absolute_path(text))
|
||||
-- TODO: add the name of directory to prioritize
|
||||
core.reschedule_project_scan()
|
||||
end, suggest_directory)
|
||||
end,
|
||||
|
||||
|
|
|
@ -16,14 +16,6 @@ local function doc()
|
|||
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 iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
|
||||
return function()
|
||||
|
@ -82,6 +74,31 @@ local function split_cursor(direction)
|
|||
core.blink_reset()
|
||||
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 = {
|
||||
["doc:undo"] = function()
|
||||
doc():undo()
|
||||
|
@ -91,14 +108,6 @@ local commands = {
|
|||
doc():redo()
|
||||
end,
|
||||
|
||||
["doc:cut"] = function()
|
||||
cut_or_copy(true)
|
||||
end,
|
||||
|
||||
["doc:copy"] = function()
|
||||
cut_or_copy(false)
|
||||
end,
|
||||
|
||||
["doc:paste"] = function()
|
||||
local clipboard = system.get_clipboard()
|
||||
-- If the clipboard has changed since our last look, use that instead
|
||||
|
@ -147,11 +156,12 @@ local commands = {
|
|||
end,
|
||||
|
||||
["doc:backspace"] = function()
|
||||
local _, indent_size = doc():get_indent_info()
|
||||
for idx, line1, col1, line2, col2 in doc():get_selections() do
|
||||
if line1 == line2 and col1 == col2 then
|
||||
local text = doc():get_text(line1, 1, line1, col1)
|
||||
if #text >= config.indent_size and text:find("^ *$") then
|
||||
doc():delete_to_cursor(idx, 0, -config.indent_size)
|
||||
if #text >= indent_size and text:find("^ *$") then
|
||||
doc():delete_to_cursor(idx, 0, -indent_size)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
@ -163,11 +173,6 @@ local commands = {
|
|||
doc():set_selection(1, 1, math.huge, math.huge)
|
||||
end,
|
||||
|
||||
["doc:select-none"] = function()
|
||||
local line, col = doc():get_selection()
|
||||
doc():set_selection(line, col)
|
||||
end,
|
||||
|
||||
["doc:select-lines"] = function()
|
||||
for idx, line1, _, line2 in doc():get_selections(true) do
|
||||
append_line_if_last_line(line2)
|
||||
|
@ -261,7 +266,7 @@ local commands = {
|
|||
["doc:toggle-line-comments"] = function()
|
||||
local comment = doc().syntax.comment
|
||||
if not comment then return end
|
||||
local indentation = get_indent_string()
|
||||
local indentation = doc():get_indent_string()
|
||||
local comment_text = comment .. " "
|
||||
for idx, line1, _, line2 in doc_multiline_selections(true) do
|
||||
local uncomment = true
|
||||
|
@ -388,6 +393,30 @@ local commands = {
|
|||
os.remove(filename)
|
||||
core.log("Removed \"%s\"", filename)
|
||||
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()
|
||||
split_cursor(-1)
|
||||
|
@ -447,3 +476,6 @@ commands["doc:move-to-next-char"] = function()
|
|||
end
|
||||
|
||||
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)
|
||||
|
|
|
@ -7,15 +7,15 @@ local DocView = require "core.docview"
|
|||
local CommandView = require "core.commandview"
|
||||
local StatusView = require "core.statusview"
|
||||
|
||||
local max_last_finds = 50
|
||||
local last_finds, last_view, last_fn, last_text, last_sel
|
||||
local last_view, last_fn, last_text, last_sel
|
||||
|
||||
local case_sensitive = config.find_case_sensitive or false
|
||||
local find_regex = config.find_regex or false
|
||||
local found_expression
|
||||
|
||||
local function doc()
|
||||
return core.active_view:is(DocView) and core.active_view.doc or last_view.doc
|
||||
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
|
||||
|
||||
local function get_find_tooltip()
|
||||
|
@ -37,7 +37,7 @@ local function update_preview(sel, search_fn, text)
|
|||
last_view:scroll_to_line(line2, true)
|
||||
found_expression = true
|
||||
else
|
||||
last_view.doc:set_selection(unpack(sel))
|
||||
last_view.doc:set_selection(table.unpack(sel))
|
||||
found_expression = false
|
||||
end
|
||||
end
|
||||
|
@ -53,8 +53,8 @@ end
|
|||
|
||||
|
||||
local function find(label, search_fn)
|
||||
last_view, last_sel, last_finds = core.active_view,
|
||||
{ core.active_view.doc:get_selection() }, {}
|
||||
last_view, last_sel = core.active_view,
|
||||
{ core.active_view.doc:get_selection() }
|
||||
local text = last_view.doc:get_text(unpack(last_sel))
|
||||
found_expression = false
|
||||
|
||||
|
@ -69,8 +69,8 @@ local function find(label, search_fn)
|
|||
last_fn, last_text = search_fn, text
|
||||
else
|
||||
core.error("Couldn't find %q", text)
|
||||
last_view.doc:set_selection(unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(unpack(last_sel))
|
||||
last_view.doc:set_selection(table.unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(table.unpack(last_sel))
|
||||
end
|
||||
end, function(text)
|
||||
update_preview(last_sel, search_fn, text)
|
||||
|
@ -79,8 +79,8 @@ local function find(label, search_fn)
|
|||
end, function(explicit)
|
||||
core.status_view:remove_tooltip()
|
||||
if explicit then
|
||||
last_view.doc:set_selection(unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(unpack(last_sel))
|
||||
last_view.doc:set_selection(table.unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(table.unpack(last_sel))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
@ -117,7 +117,7 @@ local function has_selection()
|
|||
end
|
||||
|
||||
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
|
||||
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
|
||||
if line1 == line2 and col1 == col2 then return false end
|
||||
|
@ -142,7 +142,7 @@ local function is_in_any_selection(line, col)
|
|||
return false
|
||||
end
|
||||
|
||||
local function select_next(all)
|
||||
local function select_add_next(all)
|
||||
local il1, ic1 = doc():get_selection(true)
|
||||
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
|
||||
local text = doc():get_text(l1, c1, l2, c2)
|
||||
|
@ -161,21 +161,28 @@ local function select_next(all)
|
|||
end
|
||||
end
|
||||
|
||||
command.add(has_unique_selection, {
|
||||
["find-replace:select-next"] = function()
|
||||
local l1, c1, l2, c2 = doc():get_selection(true)
|
||||
local text = doc():get_text(l1, c1, l2, c2)
|
||||
local function select_next(reverse)
|
||||
local l1, c1, l2, c2 = doc():get_selection(true)
|
||||
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 })
|
||||
if l2 then doc():set_selection(l2, c2, l1, c1) end
|
||||
end,
|
||||
["find-replace:select-add-next"] = function() select_next(false) end,
|
||||
["find-replace:select-add-all"] = function() select_next(true) end
|
||||
end
|
||||
if l2 then doc():set_selection(l2, c2, l1, c1) end
|
||||
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", {
|
||||
["find-replace:find"] = function()
|
||||
find("Find Text", function(doc, line, col, text, case_sensitive, find_regex)
|
||||
local opt = { wrap = true, no_case = not case_sensitive, regex = 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, reverse = find_reverse }
|
||||
return search.find(doc, line, col, text, opt)
|
||||
end)
|
||||
end,
|
||||
|
@ -221,29 +228,29 @@ command.add(valid_for_finding, {
|
|||
core.error("No find to continue from")
|
||||
else
|
||||
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
|
||||
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex)
|
||||
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
|
||||
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)
|
||||
last_view:scroll_to_line(line2, true)
|
||||
else
|
||||
core.error("Couldn't find %q", last_text)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["find-replace:previous-find"] = function()
|
||||
local sel = table.remove(last_finds)
|
||||
if not sel or doc() ~= last_view.doc then
|
||||
core.error("No previous finds")
|
||||
return
|
||||
if not last_fn then
|
||||
core.error("No find to continue from")
|
||||
else
|
||||
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
|
||||
local line1, col1, line2, col2 = last_fn(doc(), sl1, 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
|
||||
doc():set_selection(table.unpack(sel))
|
||||
last_view:scroll_to_line(sel[3], true)
|
||||
end,
|
||||
})
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ local style = require "core.style"
|
|||
local DocView = require "core.docview"
|
||||
local command = require "core.command"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
|
||||
|
||||
local t = {
|
||||
|
@ -63,7 +64,7 @@ local t = {
|
|||
table.insert(node.views, idx + 1, core.active_view)
|
||||
end
|
||||
end,
|
||||
|
||||
|
||||
["root:shrink"] = function()
|
||||
local node = core.root_view:get_active_node()
|
||||
local parent = node:get_parent_node(core.root_view.root_node)
|
||||
|
@ -76,7 +77,7 @@ local t = {
|
|||
local parent = node:get_parent_node(core.root_view.root_node)
|
||||
local n = (parent.a == node) and 0.1 or -0.1
|
||||
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)
|
||||
end
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
@ -120,5 +122,17 @@ end
|
|||
|
||||
command.add(function()
|
||||
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)
|
||||
|
||||
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
|
||||
})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
local common = {}
|
||||
|
||||
|
||||
function common.is_utf8_cont(char)
|
||||
local byte = char:byte()
|
||||
function common.is_utf8_cont(s, offset)
|
||||
local byte = s:byte(offset or 1)
|
||||
return byte >= 0x80 and byte < 0xc0
|
||||
end
|
||||
|
||||
|
@ -280,24 +280,61 @@ local function split_on_slash(s, sep_pattern)
|
|||
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 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
|
||||
filename = filename:gsub('[/\\]', '\\')
|
||||
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
|
||||
filename = drive and drive:upper() .. rem or filename
|
||||
local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
|
||||
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
|
||||
local parts = split_on_slash(filename, PATHSEP)
|
||||
local accu = {}
|
||||
for _, part in ipairs(parts) do
|
||||
if part == '..' and #accu > 0 and accu[#accu] ~= ".." then
|
||||
table.remove(accu)
|
||||
if part == '..' then
|
||||
if #accu > 0 and accu[#accu] ~= ".." then
|
||||
table.remove(accu)
|
||||
elseif volume then
|
||||
error("invalid path " .. volume .. filename)
|
||||
else
|
||||
table.insert(accu, part)
|
||||
end
|
||||
elseif part ~= '.' then
|
||||
table.insert(accu, part)
|
||||
end
|
||||
end
|
||||
local npath = table.concat(accu, PATHSEP)
|
||||
return npath == "" and PATHSEP or npath
|
||||
return (volume or "") .. (npath == "" and PATHSEP or npath)
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
local config = {}
|
||||
|
||||
config.project_scan_rate = 5
|
||||
config.fps = 60
|
||||
config.max_log_items = 80
|
||||
config.message_timeout = 5
|
||||
|
@ -12,8 +11,8 @@ config.symbol_pattern = "[%a_][%w_]*"
|
|||
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
|
||||
config.undo_merge_timeout = 0.3
|
||||
config.max_undos = 10000
|
||||
config.max_tabs = 10
|
||||
config.always_show_tabs = false
|
||||
config.max_tabs = 8
|
||||
config.always_show_tabs = true
|
||||
config.highlight_current_line = true
|
||||
config.line_height = 1.2
|
||||
config.indent_size = 2
|
||||
|
@ -28,6 +27,7 @@ config.disable_blink = false
|
|||
config.draw_whitespace = false
|
||||
config.borderless = false
|
||||
config.tab_close_button = true
|
||||
config.max_clicks = 3
|
||||
|
||||
-- Disable plugin loading setting to false the config entry
|
||||
-- of the same name.
|
||||
|
|
|
@ -49,7 +49,7 @@ function ContextMenu:register(predicate, items)
|
|||
local width, height = 0, 0 --precalculate the size of context menu
|
||||
for i, item in ipairs(items) do
|
||||
if item ~= DIVIDER then
|
||||
item.info = keymap.reverse_map[item.command]
|
||||
item.info = keymap.get_binding(item.command)
|
||||
end
|
||||
local lw, lh = get_item_size(item)
|
||||
width = math.max(width, lw)
|
||||
|
@ -66,9 +66,13 @@ function ContextMenu:show(x, y)
|
|||
for _, items in ipairs(self.itemset) do
|
||||
if items.predicate(x, y) then
|
||||
items_list.width = math.max(items_list.width, items.items.width)
|
||||
items_list.height = items_list.height + items.items.height
|
||||
items_list.height = items_list.height
|
||||
for _, subitems in ipairs(items.items) do
|
||||
table.insert(items_list, subitems)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local tokenizer = require "core.tokenizer"
|
||||
local Object = require "core.object"
|
||||
|
@ -40,6 +41,13 @@ end
|
|||
|
||||
function Highlighter:reset()
|
||||
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.max_wanted_line = 0
|
||||
end
|
||||
|
@ -51,16 +59,16 @@ end
|
|||
|
||||
function Highlighter:insert_notify(line, n)
|
||||
self:invalidate(line)
|
||||
local blanks = { }
|
||||
for i = 1, n do
|
||||
table.insert(self.lines, line, nil)
|
||||
blanks[i] = false
|
||||
end
|
||||
common.splice(self.lines, line, 0, blanks)
|
||||
end
|
||||
|
||||
function Highlighter:remove_notify(line, n)
|
||||
self:invalidate(line)
|
||||
for i = 1, n do
|
||||
table.remove(self.lines, line)
|
||||
end
|
||||
common.splice(self.lines, line, n)
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ function Doc:reset_syntax()
|
|||
local syn = syntax.get(self.filename or "", header)
|
||||
if self.syntax ~= syn then
|
||||
self.syntax = syn
|
||||
self.highlighter:reset()
|
||||
self.highlighter:soft_reset()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -62,12 +62,15 @@ function Doc:load(filename)
|
|||
local fp = assert( io.open(filename, "rb") )
|
||||
self:reset()
|
||||
self.lines = {}
|
||||
local i = 1
|
||||
for line in fp:lines() do
|
||||
if line:byte(-1) == 13 then
|
||||
line = line:sub(1, -2)
|
||||
self.crlf = true
|
||||
end
|
||||
table.insert(self.lines, line .. "\n")
|
||||
self.highlighter.lines[i] = false
|
||||
i = i + 1
|
||||
end
|
||||
if #self.lines == 0 then
|
||||
table.insert(self.lines, "\n")
|
||||
|
@ -115,6 +118,14 @@ function Doc:clean()
|
|||
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()
|
||||
return self.undo_stack.idx
|
||||
end
|
||||
|
@ -146,6 +157,13 @@ function Doc:has_selection()
|
|||
return line1 ~= line2 or col1 ~= col2
|
||||
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()
|
||||
for idx, line1, col1, line2, col2 in self:get_selections() do
|
||||
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)
|
||||
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -305,7 +323,8 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
|
|||
local line1, col1, line2, col2 = table.unpack(cmd)
|
||||
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
|
||||
elseif cmd.type == "selection" then
|
||||
self.selections = { unpack(cmd) }
|
||||
self.selections = { table.unpack(cmd) }
|
||||
self:sanitize_selection()
|
||||
end
|
||||
|
||||
modified = modified or (cmd.type ~= "selection")
|
||||
|
@ -348,7 +367,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
|
|||
|
||||
-- push undo
|
||||
local line2, col2 = self:position_offset(line, col, #text)
|
||||
push_undo(undo_stack, time, "selection", unpack(self.selections))
|
||||
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
|
||||
push_undo(undo_stack, time, "remove", line, col, line2, col2)
|
||||
|
||||
-- update highlighter and assure selection is in bounds
|
||||
|
@ -360,7 +379,7 @@ end
|
|||
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
|
||||
-- push undo
|
||||
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)
|
||||
|
||||
-- get line content before/after removed text
|
||||
|
@ -486,19 +505,21 @@ end
|
|||
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
|
||||
|
||||
|
||||
local function get_indent_string()
|
||||
if config.tab_type == "hard" then
|
||||
function Doc:get_indent_string()
|
||||
local indent_type, indent_size = self:get_indent_info()
|
||||
if indent_type == "hard" then
|
||||
return "\t"
|
||||
end
|
||||
return string.rep(" ", config.indent_size)
|
||||
return string.rep(" ", indent_size)
|
||||
end
|
||||
|
||||
-- returns the size of the original indent, and the indent
|
||||
-- in your config format, rounded either up or down
|
||||
local function get_line_indent(line, rnd_up)
|
||||
function Doc:get_line_indent(line, rnd_up)
|
||||
local _, e = line:find("^[ \t]+")
|
||||
local soft_tab = string.rep(" ", config.indent_size)
|
||||
if config.tab_type == "hard" then
|
||||
local indent_type, indent_size = self:get_indent_info()
|
||||
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 ""
|
||||
return e, indent:gsub(" +", rnd_up and "\t" or "")
|
||||
else
|
||||
|
@ -520,14 +541,14 @@ end
|
|||
-- * if you are unindenting, the cursor will jump to the start of the line,
|
||||
-- and remove the appropriate amount of spaces (or a tab).
|
||||
function Doc:indent_text(unindent, line1, col1, line2, col2)
|
||||
local text = get_indent_string()
|
||||
local text = self:get_indent_string()
|
||||
local _, se = self.lines[line1]:find("^[ \t]+")
|
||||
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
|
||||
local has_selection = line1 ~= line2 or col1 ~= col2
|
||||
if unindent or has_selection or in_beginning_whitespace then
|
||||
local l1d, l2d = #self.lines[line1], #self.lines[line2]
|
||||
for line = line1, line2 do
|
||||
local e, rnded = get_line_indent(self.lines[line], unindent)
|
||||
local e, rnded = self:get_line_indent(self.lines[line], unindent)
|
||||
self:remove(line, 1, line, (e or 0) + 1)
|
||||
self:insert(line, 1,
|
||||
unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
|
||||
|
|
|
@ -22,37 +22,62 @@ local function init_args(doc, line, col, text, opt)
|
|||
return doc, line, col, text, opt
|
||||
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)
|
||||
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
|
||||
|
||||
local re
|
||||
local plain = not opt.pattern
|
||||
local pattern = text
|
||||
local search_func = string.find
|
||||
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
|
||||
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]
|
||||
if opt.regex then
|
||||
local s, e = re:cmatch(line_text, col)
|
||||
if s then
|
||||
return line, s, line, e
|
||||
end
|
||||
col = 1
|
||||
else
|
||||
if opt.no_case then
|
||||
line_text = line_text:lower()
|
||||
end
|
||||
local s, e = line_text:find(text, col, true)
|
||||
if s then
|
||||
return line, s, line, e + 1
|
||||
end
|
||||
col = 1
|
||||
if opt.no_case and not opt.regex then
|
||||
line_text = line_text:lower()
|
||||
end
|
||||
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
|
||||
return line, s, line, e + 1
|
||||
end
|
||||
col = opt.reverse and -1 or 1
|
||||
end
|
||||
|
||||
if opt.wrap then
|
||||
opt = { no_case = opt.no_case, regex = opt.regex }
|
||||
return search.find(doc, 1, 1, text, opt)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -225,51 +225,6 @@ function DocView:scroll_to_make_visible(line, col)
|
|||
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, ...)
|
||||
DocView.super.on_mouse_moved(self, x, y, ...)
|
||||
|
||||
|
@ -281,8 +236,7 @@ function DocView:on_mouse_moved(x, y, ...)
|
|||
|
||||
if self.mouse_selecting then
|
||||
local l1, c1 = self:resolve_screen_position(x, y)
|
||||
local l2, c2 = table.unpack(self.mouse_selecting)
|
||||
local clicks = self.mouse_selecting.clicks
|
||||
local l2, c2, snap_type = table.unpack(self.mouse_selecting)
|
||||
if keymap.modkeys["ctrl"] then
|
||||
if l1 > l2 then l1, l2 = l2, l1 end
|
||||
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]))
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
DocView.super.on_mouse_released(self, button)
|
||||
self.mouse_selecting = nil
|
||||
|
@ -354,6 +329,18 @@ function DocView:draw_caret(x, y)
|
|||
end
|
||||
|
||||
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
|
||||
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
|
||||
if idx >= line1 and idx <= line2 then
|
||||
|
@ -368,15 +355,6 @@ function DocView:draw_line_body(idx, x, y)
|
|||
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
|
||||
self:draw_line_text(idx, x, y)
|
||||
|
@ -417,8 +395,8 @@ end
|
|||
|
||||
function DocView:draw()
|
||||
self:draw_background(style.background)
|
||||
|
||||
self:get_font():set_tab_size(config.indent_size)
|
||||
local _, indent_size = self.doc:get_indent_info()
|
||||
self:get_font():set_tab_size(indent_size)
|
||||
|
||||
local minline, maxline = self:get_visible_line_range()
|
||||
local lh = self:get_line_height()
|
||||
|
@ -432,7 +410,9 @@ function DocView:draw()
|
|||
|
||||
local pos = self.position
|
||||
x, y = self:get_line_screen_position(minline)
|
||||
core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
|
||||
-- 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
|
||||
self:draw_line_body(i, x, y)
|
||||
y = y + lh
|
||||
|
|
|
@ -36,7 +36,7 @@ end
|
|||
|
||||
|
||||
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
|
||||
local recents = core.recent_projects
|
||||
local n = #recents
|
||||
|
@ -52,23 +52,13 @@ local function update_recents_project(action, dir_path_abs)
|
|||
end
|
||||
|
||||
|
||||
function core.reschedule_project_scan()
|
||||
if core.project_scan_thread_id then
|
||||
core.threads[core.project_scan_thread_id].wake = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function core.set_project_dir(new_dir, change_project_fn)
|
||||
local chdir_ok = pcall(system.chdir, new_dir)
|
||||
if chdir_ok then
|
||||
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.add_project_directory(new_dir)
|
||||
core.project_files = {}
|
||||
core.project_files_limit = false
|
||||
core.reschedule_project_scan()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
@ -102,6 +92,29 @@ local function compare_file(a, b)
|
|||
return a.filename < b.filename
|
||||
end
|
||||
|
||||
|
||||
-- compute a file's info entry completed with "filename" to be used
|
||||
-- in project scan or falsy if it shouldn't appear in the list.
|
||||
local function get_project_file_info(root, file)
|
||||
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 '/'
|
||||
-- "path" will be a path starting with '/' and without trailing '/'
|
||||
-- or the empty string.
|
||||
|
@ -110,34 +123,31 @@ end
|
|||
-- When recursing "root" will always be the same, only "path" will change.
|
||||
-- Returns a list of file "items". In eash item the "filename" will be the
|
||||
-- complete file path relative to "root" *without* the trailing '/'.
|
||||
local function get_directory_files(root, path, t, recursive, begin_hook)
|
||||
local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook)
|
||||
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 t_elapsed = system.get_time() - t0
|
||||
local dirs, files = {}, {}
|
||||
|
||||
local entries_count = 0
|
||||
local max_entries = config.max_project_files
|
||||
for _, file in ipairs(all) do
|
||||
if not common.match_pattern(file, config.ignore_files) then
|
||||
local file = path .. PATHSEP .. file
|
||||
local info = system.get_file_info(root .. file)
|
||||
if info and info.size < size_limit then
|
||||
info.filename = strip_leading_path(file)
|
||||
table.insert(info.type == "dir" and dirs or files, info)
|
||||
entries_count = entries_count + 1
|
||||
if recursive and entries_count > max_entries then return nil, entries_count end
|
||||
end
|
||||
local info = get_project_file_info(root, path .. PATHSEP .. file)
|
||||
if info then
|
||||
table.insert(info.type == "dir" and dirs or files, info)
|
||||
entries_count = entries_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local recurse_complete = true
|
||||
table.sort(dirs, compare_file)
|
||||
for _, f in ipairs(dirs) do
|
||||
table.insert(t, f)
|
||||
if recursive and entries_count <= max_entries then
|
||||
local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive)
|
||||
entries_count = entries_count + subdir_count
|
||||
f.scanned = true
|
||||
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
|
||||
local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook)
|
||||
recurse_complete = recurse_complete and complete
|
||||
entries_count = n
|
||||
else
|
||||
recurse_complete = false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -146,135 +156,319 @@ local function get_directory_files(root, path, t, recursive, begin_hook)
|
|||
table.insert(t, f)
|
||||
end
|
||||
|
||||
return t, entries_count
|
||||
return t, recurse_complete, entries_count
|
||||
end
|
||||
|
||||
local function project_scan_thread()
|
||||
local function diff_files(a, b)
|
||||
if #a ~= #b then return true end
|
||||
for i, v in ipairs(a) do
|
||||
if b[i].filename ~= v.filename
|
||||
or b[i].modified ~= v.modified then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
while true do
|
||||
-- get project files and replace previous table if the new table is
|
||||
-- different
|
||||
local i = 1
|
||||
while not core.project_files_limit and i <= #core.project_directories do
|
||||
local dir = core.project_directories[i]
|
||||
local t, entries_count = get_directory_files(dir.name, "", {}, true)
|
||||
if diff_files(dir.files, t) then
|
||||
if entries_count > config.max_project_files then
|
||||
core.project_files_limit = true
|
||||
core.status_view:show_message("!", style.accent,
|
||||
"Too many files in project directory: stopped reading at "..
|
||||
config.max_project_files.." files. For more information see "..
|
||||
"usage.md at github.com/franko/lite-xl."
|
||||
)
|
||||
end
|
||||
dir.files = t
|
||||
core.redraw = true
|
||||
end
|
||||
if dir.name == core.project_dir then
|
||||
core.project_files = dir.files
|
||||
end
|
||||
i = i + 1
|
||||
function core.project_subdir_set_show(dir, filename, show)
|
||||
dir.shown_subdir[filename] = show
|
||||
if dir.files_limit and PLATFORM == "Linux" then
|
||||
local fullpath = dir.name .. PATHSEP .. filename
|
||||
local watch_fn = show and system.watch_dir_add or system.watch_dir_rm
|
||||
local success = watch_fn(dir.watch_id, fullpath)
|
||||
if not success then
|
||||
core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm")
|
||||
end
|
||||
|
||||
-- wait for next scan
|
||||
coroutine.yield(config.project_scan_rate)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function core.is_project_folder(dirname)
|
||||
for _, dir in ipairs(core.project_directories) do
|
||||
if dir.name == dirname then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
function core.project_subdir_is_shown(dir, filename)
|
||||
return not dir.files_limit or dir.shown_subdir[filename]
|
||||
end
|
||||
|
||||
|
||||
function core.scan_project_folder(dirname, filename)
|
||||
for _, dir in ipairs(core.project_directories) do
|
||||
if dir.name == dirname then
|
||||
for i, file in ipairs(dir.files) do
|
||||
local file = dir.files[i]
|
||||
if file.filename == filename then
|
||||
if file.scanned then return end
|
||||
local new_files = get_directory_files(dirname, PATHSEP .. filename, {})
|
||||
for j, new_file in ipairs(new_files) do
|
||||
table.insert(dir.files, i + j, new_file)
|
||||
end
|
||||
file.scanned = true
|
||||
return
|
||||
local function show_max_files_warning(dir)
|
||||
local message = dir.slow_filesystem and
|
||||
"Filesystem is too slow: project files will not be indexed." or
|
||||
"Too many files in project directory: stopped reading at "..
|
||||
config.max_project_files.." files. For more information see "..
|
||||
"usage.md at github.com/franko/lite-xl."
|
||||
core.status_view:show_message("!", style.accent, message)
|
||||
end
|
||||
|
||||
|
||||
local function file_search(files, info)
|
||||
local filename, type = info.filename, info.type
|
||||
local inf, sup = 1, #files
|
||||
while sup - inf > 8 do
|
||||
local curr = math.floor((inf + sup) / 2)
|
||||
if system.path_compare(filename, type, files[curr].filename, files[curr].type) then
|
||||
sup = curr - 1
|
||||
else
|
||||
inf = curr
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
local function files_info_equal(a, b)
|
||||
return a.filename == b.filename and a.type == b.type
|
||||
end
|
||||
|
||||
-- for "a" inclusive from i1 + 1 and i1 + n
|
||||
local function files_list_match(a, i1, n, b)
|
||||
if n ~= #b then return false end
|
||||
for i = 1, n do
|
||||
if not files_info_equal(a[i1 + i], b[i]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
local function project_subdir_bounds(dir, filename)
|
||||
local index, n = 0, #dir.files
|
||||
for i, file in ipairs(dir.files) do
|
||||
local file = dir.files[i]
|
||||
if file.filename == filename then
|
||||
index, n = i, #dir.files - i
|
||||
for j = 1, #dir.files - i do
|
||||
if not common.path_belongs_to(dir.files[i + j].filename, filename) then
|
||||
n = j - 1
|
||||
break
|
||||
end
|
||||
end
|
||||
return index, n, file
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function rescan_project_subdir(dir, filename_rooted)
|
||||
local new_files = get_directory_files(dir, 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)
|
||||
local size_limit = config.file_size_limit * 10e5
|
||||
if not files_list_match(dir.files, index, n, new_files) then
|
||||
files_list_replace(dir.files, index, n, new_files)
|
||||
dir.is_dirty = true
|
||||
return true
|
||||
end
|
||||
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 {}
|
||||
for _, file in ipairs(all) do
|
||||
if not common.match_pattern(file, config.ignore_files) then
|
||||
local file = path .. PATHSEP .. file
|
||||
local info = system.get_file_info(root .. file)
|
||||
if info and info.size < size_limit then
|
||||
info.filename = strip_leading_path(file)
|
||||
if info.type == "file" then
|
||||
coroutine.yield(root, info)
|
||||
else
|
||||
find_project_files_co(root, PATHSEP .. info.filename)
|
||||
end
|
||||
local file = path .. PATHSEP .. file
|
||||
local info = system.get_file_info(root .. file)
|
||||
if info then
|
||||
info.filename = strip_leading_path(file)
|
||||
if info.type == "file" then
|
||||
coroutine.yield(root, info)
|
||||
else
|
||||
find_files_rec(root, PATHSEP .. info.filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Iterator function to list all project files
|
||||
local function project_files_iter(state)
|
||||
local dir = core.project_directories[state.dir_index]
|
||||
state.file_index = state.file_index + 1
|
||||
while dir and state.file_index > #dir.files do
|
||||
state.dir_index = state.dir_index + 1
|
||||
state.file_index = 1
|
||||
dir = core.project_directories[state.dir_index]
|
||||
if state.co then
|
||||
-- We have a coroutine to fetch for files, use the coroutine.
|
||||
-- Used for directories that exceeds the files nuumber limit.
|
||||
local ok, name, file = coroutine.resume(state.co, dir.name, "")
|
||||
if ok and name then
|
||||
return name, file
|
||||
else
|
||||
-- The coroutine terminated, increment file/dir counter to scan
|
||||
-- next project directory.
|
||||
state.co = false
|
||||
state.file_index = 1
|
||||
state.dir_index = state.dir_index + 1
|
||||
dir = core.project_directories[state.dir_index]
|
||||
end
|
||||
else
|
||||
-- Increase file/dir counter
|
||||
state.file_index = state.file_index + 1
|
||||
while dir and state.file_index > #dir.files do
|
||||
state.dir_index = state.dir_index + 1
|
||||
state.file_index = 1
|
||||
dir = core.project_directories[state.dir_index]
|
||||
end
|
||||
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]
|
||||
end
|
||||
|
||||
|
||||
function core.get_project_files()
|
||||
if core.project_files_limit then
|
||||
return coroutine.wrap(function()
|
||||
for _, dir in ipairs(core.project_directories) do
|
||||
find_project_files_co(dir.name, "")
|
||||
end
|
||||
end)
|
||||
else
|
||||
local state = { dir_index = 1, file_index = 0 }
|
||||
return project_files_iter, state
|
||||
end
|
||||
local state = { dir_index = 1, file_index = 0 }
|
||||
return project_files_iter, state
|
||||
end
|
||||
|
||||
|
||||
function core.project_files_number()
|
||||
if not core.project_files_limit then
|
||||
local n = 0
|
||||
for i = 1, #core.project_directories do
|
||||
n = n + #core.project_directories[i].files
|
||||
local n = 0
|
||||
for i = 1, #core.project_directories do
|
||||
if core.project_directories[i].files_limit then return end
|
||||
n = n + #core.project_directories[i].files
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
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
|
||||
return n
|
||||
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
|
||||
|
||||
|
@ -371,19 +565,6 @@ function core.load_user_directory()
|
|||
end
|
||||
|
||||
|
||||
function core.add_project_directory(path)
|
||||
-- top directories has a file-like "item" but the item.filename
|
||||
-- will be simply the name of the directory, without its path.
|
||||
-- The field item.topdir will identify it as a top level directory.
|
||||
path = common.normalize_path(path)
|
||||
table.insert(core.project_directories, {
|
||||
name = path,
|
||||
item = {filename = common.basename(path), type = "dir", topdir = true},
|
||||
files = {}
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
function core.remove_project_directory(path)
|
||||
-- skip the fist directory because it is the project's directory
|
||||
for i = 2, #core.project_directories do
|
||||
|
@ -422,9 +603,9 @@ function core.init()
|
|||
Doc = require "core.doc"
|
||||
|
||||
if PATHSEP == '\\' then
|
||||
USERDIR = common.normalize_path(USERDIR)
|
||||
DATADIR = common.normalize_path(DATADIR)
|
||||
EXEDIR = common.normalize_path(EXEDIR)
|
||||
USERDIR = common.normalize_volume(USERDIR)
|
||||
DATADIR = common.normalize_volume(DATADIR)
|
||||
EXEDIR = common.normalize_volume(EXEDIR)
|
||||
end
|
||||
|
||||
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.status_view, {y = true})
|
||||
|
||||
core.project_scan_thread_id = core.add_thread(project_scan_thread)
|
||||
command.add_defaults()
|
||||
local got_user_error = not core.load_user_directory()
|
||||
local plugins_success, plugins_refuse_list = core.load_plugins()
|
||||
|
@ -520,6 +700,12 @@ function core.init()
|
|||
end
|
||||
local got_project_error = not core.load_project_module()
|
||||
|
||||
-- We assume we have just a single project directory here. Now that StatusView
|
||||
-- is there show max files warning if needed.
|
||||
if core.project_directories[1].files_limit then
|
||||
show_max_files_warning(core.project_directories[1])
|
||||
end
|
||||
|
||||
for _, filename in ipairs(files) do
|
||||
core.root_view:open_doc(core.open_doc(filename))
|
||||
end
|
||||
|
@ -675,16 +861,18 @@ function core.load_plugins()
|
|||
userdir = {dir = USERDIR, plugins = {}},
|
||||
datadir = {dir = DATADIR, plugins = {}},
|
||||
}
|
||||
local files = {}
|
||||
local files, ordered = {}, {}
|
||||
for _, root_dir in ipairs {DATADIR, USERDIR} do
|
||||
local plugin_dir = root_dir .. "/plugins"
|
||||
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
|
||||
end
|
||||
end
|
||||
table.sort(ordered)
|
||||
|
||||
for filename, plugin_dir in pairs(files) do
|
||||
local basename = filename:match("(.-)%.lua$") or filename
|
||||
for _, filename in ipairs(ordered) do
|
||||
local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename
|
||||
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
|
||||
if is_lua_file then
|
||||
if not version_match then
|
||||
|
@ -908,6 +1096,84 @@ function core.try(fn, ...)
|
|||
return false, err
|
||||
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, ...)
|
||||
local did_keymap = false
|
||||
|
@ -920,11 +1186,15 @@ function core.on_event(type, ...)
|
|||
elseif type == "mousemoved" then
|
||||
core.root_view:on_mouse_moved(...)
|
||||
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
|
||||
core.root_view:on_mouse_released(...)
|
||||
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
|
||||
core.window_mode = system.get_window_mode()
|
||||
elseif type == "minimized" or type == "maximized" or type == "restored" then
|
||||
|
@ -944,6 +1214,8 @@ function core.on_event(type, ...)
|
|||
end
|
||||
elseif type == "focuslost" then
|
||||
core.root_view:on_focus_lost(...)
|
||||
elseif type == "dirchange" then
|
||||
core.on_dir_change(...)
|
||||
elseif type == "quit" then
|
||||
core.quit()
|
||||
end
|
||||
|
@ -1050,7 +1322,7 @@ function core.run()
|
|||
while true do
|
||||
core.frame_start = system.get_time()
|
||||
local did_redraw = core.step()
|
||||
local need_more_work = run_threads()
|
||||
local need_more_work = run_threads() or core.has_pending_rescan()
|
||||
if core.restart_request or core.quit_request then break end
|
||||
if not did_redraw and not need_more_work then
|
||||
idle_iterations = idle_iterations + 1
|
||||
|
|
|
@ -32,6 +32,8 @@ local function keymap_macos(keymap)
|
|||
["cmd+7"] = "root:switch-to-tab-7",
|
||||
["cmd+8"] = "root:switch-to-tab-8",
|
||||
["cmd+9"] = "root:switch-to-tab-9",
|
||||
["wheel"] = "root:scroll",
|
||||
|
||||
["cmd+f"] = "find-replace:find",
|
||||
["cmd+r"] = "find-replace:replace",
|
||||
["f3"] = "find-replace:repeat-find",
|
||||
|
@ -93,6 +95,11 @@ local function keymap_macos(keymap)
|
|||
["pageup"] = "doc:move-to-previous-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+right"] = "doc:select-to-next-char",
|
||||
["shift+up"] = "doc:select-to-previous-line",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
local keymap = {}
|
||||
|
||||
keymap.modkeys = {}
|
||||
|
@ -30,7 +31,8 @@ function keymap.add_direct(map)
|
|||
end
|
||||
keymap.map[stroke] = commands
|
||||
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
|
||||
|
@ -52,18 +54,43 @@ function keymap.add(map, overwrite)
|
|||
end
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
function keymap.get_binding(cmd)
|
||||
return keymap.reverse_map[cmd]
|
||||
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.on_key_pressed(k)
|
||||
function keymap.unbind(key, cmd)
|
||||
remove_only(keymap.map, key, cmd)
|
||||
remove_only(keymap.reverse_map, cmd, key)
|
||||
end
|
||||
|
||||
|
||||
function keymap.get_binding(cmd)
|
||||
return table.unpack(keymap.reverse_map[cmd] or {})
|
||||
end
|
||||
|
||||
|
||||
function keymap.on_key_pressed(k, ...)
|
||||
local mk = modkey_map[k]
|
||||
if mk then
|
||||
keymap.modkeys[mk] = true
|
||||
|
@ -73,18 +100,30 @@ function keymap.on_key_pressed(k)
|
|||
end
|
||||
else
|
||||
local stroke = key_to_stroke(k)
|
||||
local commands = keymap.map[stroke]
|
||||
local commands, performed = keymap.map[stroke]
|
||||
if commands then
|
||||
for _, cmd in ipairs(commands) do
|
||||
local performed = command.perform(cmd)
|
||||
performed = command.perform(cmd, ...)
|
||||
if performed then break end
|
||||
end
|
||||
return true
|
||||
return performed
|
||||
end
|
||||
end
|
||||
return false
|
||||
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)
|
||||
local mk = modkey_map[k]
|
||||
|
@ -133,6 +172,7 @@ keymap.add_direct {
|
|||
["alt+7"] = "root:switch-to-tab-7",
|
||||
["alt+8"] = "root:switch-to-tab-8",
|
||||
["alt+9"] = "root:switch-to-tab-9",
|
||||
["wheel"] = "root:scroll",
|
||||
|
||||
["ctrl+f"] = "find-replace:find",
|
||||
["ctrl+r"] = "find-replace:replace",
|
||||
|
@ -170,6 +210,7 @@ keymap.add_direct {
|
|||
["ctrl+a"] = "doc:select-all",
|
||||
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
|
||||
["ctrl+f3"] = "find-replace:select-next",
|
||||
["ctrl+shift+f3"] = "find-replace:select-previous",
|
||||
["ctrl+l"] = "doc:select-lines",
|
||||
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
|
||||
["ctrl+/"] = "doc:toggle-line-comments",
|
||||
|
@ -193,6 +234,11 @@ keymap.add_direct {
|
|||
["pageup"] = "doc:move-to-previous-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+right"] = "doc:select-to-next-char",
|
||||
["shift+up"] = "doc:select-to-previous-line",
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
-- So that in addition to regex.gsub(pattern, string), we can also do
|
||||
-- pattern:gsub(string).
|
||||
regex.__index = function(table, key) return regex[key]; end
|
||||
|
@ -6,7 +5,8 @@ regex.__index = function(table, key) return regex[key]; end
|
|||
regex.match = function(pattern_string, string, offset, options)
|
||||
local pattern = type(pattern_string) == "table" and
|
||||
pattern_string or regex.compile(pattern_string)
|
||||
return regex.cmatch(pattern, string, offset or 1, options or 0)
|
||||
local s, e = regex.cmatch(pattern, string, offset or 1, options or 0)
|
||||
return s, e and e - 1
|
||||
end
|
||||
|
||||
-- Will iterate back through any UTF-8 bytes so that we don't replace bits
|
||||
|
|
|
@ -149,10 +149,17 @@ function Node:remove_view(root, view)
|
|||
else
|
||||
locked_size = locked_size_y
|
||||
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:add_view(EmptyView())
|
||||
else
|
||||
if other == next_primary then
|
||||
next_primary = parent
|
||||
end
|
||||
parent:consume(other)
|
||||
local p = parent
|
||||
while p.type ~= "leaf" do
|
||||
|
@ -160,7 +167,7 @@ function Node:remove_view(root, view)
|
|||
end
|
||||
p:set_active_view(p.active_view)
|
||||
if self.is_primary_node then
|
||||
p.is_primary_node = true
|
||||
next_primary.is_primary_node = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -411,15 +418,8 @@ end
|
|||
-- 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
|
||||
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
|
||||
if x1 then
|
||||
n = x1 + ds
|
||||
elseif x2 then
|
||||
n = self.size[x] - x2
|
||||
else
|
||||
n = math.floor(self.size[x] * self.divider)
|
||||
end
|
||||
local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
|
||||
self.a.position[x] = self.position[x]
|
||||
self.a.position[y] = self.position[y]
|
||||
self.a.size[x] = n - ds
|
||||
|
@ -602,7 +602,7 @@ function Node:draw()
|
|||
self:draw_tabs()
|
||||
end
|
||||
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()
|
||||
core.pop_clip_rect()
|
||||
else
|
||||
|
@ -682,6 +682,10 @@ end
|
|||
|
||||
|
||||
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 it is not locked we don't accept the
|
||||
-- resize operation here because for proportional panes the resize is
|
||||
|
@ -826,6 +830,24 @@ function RootView:get_primary_node()
|
|||
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)
|
||||
local node = self:get_active_node_default()
|
||||
for i, view in ipairs(node.views) do
|
||||
|
@ -855,30 +877,30 @@ end
|
|||
|
||||
function RootView:on_mouse_pressed(button, x, y, clicks)
|
||||
local div = self.root_node:get_divider_overlapping_point(x, y)
|
||||
if div then
|
||||
self.dragged_divider = div
|
||||
return
|
||||
end
|
||||
local node = self.root_node:get_child_overlapping_point(x, y)
|
||||
if 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
|
||||
node:scroll_tabs(node.hovered_scroll_button)
|
||||
return
|
||||
return true
|
||||
end
|
||||
local idx = node:get_tab_overlapping_point(x, y)
|
||||
if idx then
|
||||
if button == "middle" or node.hovered_close == idx then
|
||||
node:close_view(self.root_node, node.views[idx])
|
||||
return true
|
||||
else
|
||||
if button == "left" then
|
||||
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
|
||||
end
|
||||
node:set_active_view(node.views[idx])
|
||||
return true
|
||||
end
|
||||
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
|
||||
core.set_active_view(node.active_view)
|
||||
if not self.on_view_mouse_pressed(button, x, y, clicks) then
|
||||
node.active_view:on_mouse_pressed(button, x, y, clicks)
|
||||
end
|
||||
return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks)
|
||||
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)
|
||||
|
||||
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 tab_index = node and node:get_tab_overlapping_point(x, y)
|
||||
if node and node:get_scroll_button_index(x, y) then
|
||||
local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
|
||||
if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
|
||||
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")
|
||||
elseif tab_index then
|
||||
core.request_cursor("arrow")
|
||||
elseif node then
|
||||
core.request_cursor(node.active_view.cursor)
|
||||
elseif self.overlapping_node then
|
||||
core.request_cursor(self.overlapping_node.active_view.cursor)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1018,7 +1041,7 @@ end
|
|||
function RootView:on_mouse_wheel(...)
|
||||
local x, y = self.mouse.x, self.mouse.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
|
||||
|
||||
|
||||
|
@ -1100,10 +1123,10 @@ function RootView:update_drag_overlay()
|
|||
if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then
|
||||
local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y)
|
||||
self:set_drag_overlay(self.drag_overlay_tab,
|
||||
tab_x + (tab_index and 0 or tab_w), tab_y,
|
||||
style.caret_width, tab_h,
|
||||
-- avoid showing tab overlay moving between nodes
|
||||
over ~= self.drag_overlay_tab.last_over)
|
||||
tab_x + (tab_index and 0 or tab_w), tab_y,
|
||||
style.caret_width, tab_h,
|
||||
-- avoid showing tab overlay moving between nodes
|
||||
over ~= self.drag_overlay_tab.last_over)
|
||||
self:set_show_overlay(self.drag_overlay, false)
|
||||
self.drag_overlay_tab.last_over = over
|
||||
else
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
VERSION = "@PROJECT_VERSION@"
|
||||
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)
|
||||
|
||||
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
|
||||
|
@ -28,3 +28,6 @@ package.searchers = { package.searchers[1], package.searchers[2], function(modna
|
|||
if not path then return nil end
|
||||
return system.load_native_plugin, path
|
||||
end }
|
||||
|
||||
table.pack = table.pack or pack or function(...) return {...} end
|
||||
table.unpack = table.unpack or unpack
|
||||
|
|
|
@ -108,9 +108,9 @@ function StatusView:get_items()
|
|||
local dv = core.active_view
|
||||
local line, col = dv.doc:get_selection()
|
||||
local dirty = dv.doc:is_dirty()
|
||||
local indent = dv.doc.indent_info
|
||||
local indent_label = (indent and indent.type == "hard") and "tabs: " or "spaces: "
|
||||
local indent_size = indent and tostring(indent.size) .. (indent.confirmed and "" or "*") or "unknown"
|
||||
local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info()
|
||||
local indent_label = (indent_type == "hard") and "tabs: " or "spaces: "
|
||||
local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown"
|
||||
|
||||
return {
|
||||
dirty and style.accent or style.text, style.icon_font, "f",
|
||||
|
|
|
@ -3,7 +3,7 @@ local common = require "core.common"
|
|||
local syntax = {}
|
||||
syntax.items = {}
|
||||
|
||||
local plain_text_syntax = { patterns = {}, symbols = {} }
|
||||
local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
|
||||
|
||||
|
||||
function syntax.add(t)
|
||||
|
@ -22,7 +22,7 @@ end
|
|||
|
||||
function syntax.get(filename, header)
|
||||
return find(filename, "files")
|
||||
or find(header, "headers")
|
||||
or (header and find(header, "headers"))
|
||||
or plain_text_syntax
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
local syntax = require "core.syntax"
|
||||
local common = require "core.common"
|
||||
|
||||
local tokenizer = {}
|
||||
|
||||
|
@ -142,8 +143,13 @@ function tokenizer.tokenize(incoming_syntax, text, state)
|
|||
code = p._regex
|
||||
end
|
||||
repeat
|
||||
res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) }
|
||||
or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) }
|
||||
local next = res[2] + 1
|
||||
-- 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
|
||||
local count = 0
|
||||
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
|
||||
end
|
||||
until not res[1] or not close or not target[3]
|
||||
return unpack(res)
|
||||
return table.unpack(res)
|
||||
end
|
||||
|
||||
while i <= #text do
|
||||
|
|
|
@ -102,13 +102,9 @@ function View:on_text_input(text)
|
|||
-- no-op
|
||||
end
|
||||
|
||||
|
||||
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()
|
||||
local x = self.scroll.x
|
||||
|
@ -140,7 +136,7 @@ end
|
|||
function View:draw_background(color)
|
||||
local x, y = self.position.x, self.position.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
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ local core = require "core"
|
|||
local config = require "core.config"
|
||||
local Doc = require "core.doc"
|
||||
|
||||
|
||||
local times = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function update_time(doc)
|
||||
|
@ -11,7 +10,6 @@ local function update_time(doc)
|
|||
times[doc] = info.modified
|
||||
end
|
||||
|
||||
|
||||
local function reload_doc(doc)
|
||||
local fp = io.open(doc.filename, "r")
|
||||
local text = fp:read("*a")
|
||||
|
@ -27,23 +25,19 @@ local function reload_doc(doc)
|
|||
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
|
||||
end
|
||||
|
||||
local on_modify = core.on_dirmonitor_modify
|
||||
|
||||
core.add_thread(function()
|
||||
while true do
|
||||
-- check all doc modified times
|
||||
for _, doc in ipairs(core.docs) do
|
||||
local info = system.get_file_info(doc.filename or "")
|
||||
if info and times[doc] ~= info.modified then
|
||||
reload_doc(doc)
|
||||
end
|
||||
coroutine.yield()
|
||||
core.on_dirmonitor_modify = function(dir, filepath)
|
||||
local abs_filename = dir.name .. PATHSEP .. filepath
|
||||
for _, doc in ipairs(core.docs) do
|
||||
local info = system.get_file_info(doc.filename or "")
|
||||
if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then
|
||||
reload_doc(doc)
|
||||
break
|
||||
end
|
||||
|
||||
-- wait for next scan
|
||||
coroutine.yield(config.project_scan_rate)
|
||||
end
|
||||
end)
|
||||
|
||||
on_modify(dir, filepath)
|
||||
end
|
||||
|
||||
-- patch `Doc.save|load` to store modified time
|
||||
local load = Doc.load
|
||||
|
|
|
@ -62,15 +62,15 @@ menu:register("core.logview", {
|
|||
|
||||
if require("plugins.scale") then
|
||||
menu:register("core.docview", {
|
||||
{ text = "Font +", command = "scale:increase" },
|
||||
{ text = "Font -", command = "scale:decrease" },
|
||||
{ text = "Font Reset", command = "scale:reset" },
|
||||
{ text = "Cut", command = "doc:cut" },
|
||||
{ text = "Copy", command = "doc:copy" },
|
||||
{ text = "Paste", command = "doc:paste" },
|
||||
{ text = "Font +", command = "scale:increase" },
|
||||
{ text = "Font -", command = "scale:decrease" },
|
||||
{ text = "Font Reset", command = "scale:reset" },
|
||||
ContextMenu.DIVIDER,
|
||||
{ text = "Find", command = "find-replace:find" },
|
||||
{ text = "Replace", command = "find-replace:replace" },
|
||||
ContextMenu.DIVIDER,
|
||||
{ text = "Find Pattern", command = "find-replace:find-pattern" },
|
||||
{ text = "Replace Pattern", command = "find-replace:replace-pattern" },
|
||||
{ text = "Find", command = "find-replace:find" },
|
||||
{ text = "Replace", command = "find-replace:replace" }
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -121,40 +121,17 @@ end
|
|||
local clean = Doc.clean
|
||||
function Doc:clean(...)
|
||||
clean(self, ...)
|
||||
if not cache[self].confirmed then
|
||||
local _, _, confirmed = self:get_indent_info()
|
||||
if not confirmed then
|
||||
update_cache(self)
|
||||
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 _, indent_size = doc:get_indent_info()
|
||||
cache[doc] = {type = type,
|
||||
size = cache[doc].value or config.indent_size,
|
||||
size = indent_size,
|
||||
confirmed = true}
|
||||
doc.indent_info = cache[doc]
|
||||
end
|
||||
|
@ -180,7 +157,8 @@ end
|
|||
|
||||
|
||||
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,
|
||||
confirmed = true}
|
||||
doc.indent_info = cache[doc]
|
||||
|
|
|
@ -7,26 +7,30 @@ local common = require "core.common"
|
|||
local draw_line_text = DocView.draw_line_text
|
||||
|
||||
function DocView:draw_line_text(idx, x, y)
|
||||
local font = (self:get_font() or style.syntax_fonts["comment"])
|
||||
local color = style.syntax.comment
|
||||
local ty, tx = y + self:get_line_text_y_offset()
|
||||
local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
|
||||
local color = style.syntax.whitespace or style.syntax.comment
|
||||
local ty = y + self:get_line_text_y_offset()
|
||||
local tx
|
||||
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
|
||||
s, e = text:find(" +", offset)
|
||||
if not s then break end
|
||||
tx = self:get_col_x_offset(idx, s) + x
|
||||
renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color)
|
||||
if tx > x + x2 then break end
|
||||
offset = e + 1
|
||||
end
|
||||
offset = 1
|
||||
offset = _offset
|
||||
while true do
|
||||
s, e = text:find("\t", offset)
|
||||
if not s then break end
|
||||
tx = self:get_col_x_offset(idx, s) + x
|
||||
renderer.draw_text(font, "»", tx, ty, color)
|
||||
if tx > x + x2 then break end
|
||||
offset = e + 1
|
||||
end
|
||||
draw_line_text(self, idx, x, y)
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "C",
|
||||
files = { "%.c$", "%.h$", "%.inl$" },
|
||||
comment = "//",
|
||||
patterns = {
|
||||
|
|
|
@ -4,6 +4,7 @@ pcall(require, "plugins.language_c")
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "C++",
|
||||
files = {
|
||||
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
|
||||
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "CSS",
|
||||
files = { "%.css$" },
|
||||
patterns = {
|
||||
{ pattern = "\\.", type = "normal" },
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "HTML",
|
||||
files = { "%.html?$" },
|
||||
patterns = {
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "JavaScript",
|
||||
files = { "%.js$", "%.json$", "%.cson$" },
|
||||
comment = "//",
|
||||
patterns = {
|
||||
|
@ -11,8 +12,8 @@ syntax.add {
|
|||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||
{ pattern = { "`", "`", '\\' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||
{ pattern = "0x[%da-fA-F_]+n?", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE_n]*", type = "number" },
|
||||
{ pattern = "-?%.?%d+", type = "number" },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "Lua",
|
||||
files = "%.lua$",
|
||||
headers = "^#!.*[ /]lua",
|
||||
comment = "--",
|
||||
|
|
|
@ -4,11 +4,11 @@ local syntax = require "core.syntax"
|
|||
|
||||
|
||||
syntax.add {
|
||||
name = "Markdown",
|
||||
files = { "%.md$", "%.markdown$" },
|
||||
patterns = {
|
||||
{ pattern = "\\.", type = "normal" },
|
||||
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
|
||||
{ pattern = { "```c", "```" }, type = "string", syntax = ".c" },
|
||||
{ pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
|
||||
{ pattern = { "```python", "```" }, type = "string", syntax = ".py" },
|
||||
{ pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
|
||||
|
@ -25,6 +25,21 @@ syntax.add {
|
|||
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
|
||||
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" },
|
||||
{ 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" },
|
||||
|
|
|
@ -2,20 +2,21 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "Python",
|
||||
files = { "%.py$", "%.pyw$", "%.rpy$" },
|
||||
headers = "^#!.*[ /]python",
|
||||
comment = "#",
|
||||
patterns = {
|
||||
{ pattern = { "#", "\n" }, type = "comment" },
|
||||
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
|
||||
{ pattern = { '"""', '"""' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||
{ pattern = "-?%.?%d+", type = "number" },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
{ pattern = { "#", "\n" }, type = "comment" },
|
||||
{ pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
|
||||
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||
{ pattern = "-?%.?%d+", type = "number" },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
},
|
||||
symbols = {
|
||||
["class"] = "keyword",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
name = "XML",
|
||||
files = { "%.xml$" },
|
||||
headers = "<%?xml",
|
||||
patterns = {
|
||||
|
|
|
@ -2,19 +2,20 @@
|
|||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local DocView = require "core.docview"
|
||||
local CommandView = require "core.commandview"
|
||||
|
||||
local draw_overlay = DocView.draw_overlay
|
||||
|
||||
function DocView:draw_overlay(...)
|
||||
local ns = ("n"):rep(config.line_limit)
|
||||
local offset = self:get_font():get_width(ns)
|
||||
local x = self:get_line_screen_position(1) + offset
|
||||
local y = self.position.y
|
||||
local w = math.ceil(SCALE * 1)
|
||||
local h = self.size.y
|
||||
|
||||
local color = style.guide or style.selection
|
||||
renderer.draw_rect(x, y, w, h, color)
|
||||
|
||||
if not self:is(CommandView) then
|
||||
local offset = self:get_font():get_width("n") * config.line_limit
|
||||
local x = self:get_line_screen_position(1) + offset
|
||||
local y = self.position.y
|
||||
local w = math.ceil(SCALE * 1)
|
||||
local h = self.size.y
|
||||
|
||||
local color = style.guide or style.selection
|
||||
renderer.draw_rect(x, y, w, h, color)
|
||||
end
|
||||
draw_overlay(self, ...)
|
||||
end
|
||||
|
|
|
@ -92,7 +92,7 @@ end
|
|||
function ResultsView:on_mouse_pressed(...)
|
||||
local caught = ResultsView.super.on_mouse_pressed(self, ...)
|
||||
if not caught then
|
||||
self:open_selected_result()
|
||||
return self:open_selected_result()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -108,6 +108,7 @@ function ResultsView:open_selected_result()
|
|||
dv.doc:set_selection(res.line, res.col)
|
||||
dv:scroll_to_line(res.line, false, true)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ local function set_scale(scale)
|
|||
renderer.font.set_size(font, s * font:get_size())
|
||||
end
|
||||
|
||||
for _, font in pairs(style.syntax_fonts) do
|
||||
renderer.font.set_size(font, s * font:get_size())
|
||||
end
|
||||
|
||||
-- restore scroll positions
|
||||
for view, n in pairs(scrolls) do
|
||||
view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
|
||||
|
@ -67,17 +71,6 @@ local function get_scale()
|
|||
return current_scale
|
||||
end
|
||||
|
||||
local on_mouse_wheel = RootView.on_mouse_wheel
|
||||
|
||||
function RootView:on_mouse_wheel(d, ...)
|
||||
if keymap.modkeys["ctrl"] and config.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()
|
||||
set_scale(default_scale)
|
||||
end
|
||||
|
@ -101,6 +94,8 @@ keymap.add {
|
|||
["ctrl+0"] = "scale:reset",
|
||||
["ctrl+-"] = "scale:decrease",
|
||||
["ctrl+="] = "scale:increase",
|
||||
["ctrl+wheelup"] = "scale:increase",
|
||||
["ctrl+wheeldown"] = "scale:decrease"
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -41,8 +41,15 @@ function TreeView:new()
|
|||
self.init_size = true
|
||||
self.target_size = default_treeview_size
|
||||
self.cache = {}
|
||||
self.last = {}
|
||||
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -54,7 +61,7 @@ function TreeView:set_target_size(axis, value)
|
|||
end
|
||||
|
||||
|
||||
function TreeView:get_cached(item, dirname)
|
||||
function TreeView:get_cached(dir, item, dirname)
|
||||
local dir_cache = self.cache[dirname]
|
||||
if not dir_cache then
|
||||
dir_cache = {}
|
||||
|
@ -80,6 +87,7 @@ function TreeView:get_cached(item, dirname)
|
|||
end
|
||||
t.name = basename
|
||||
t.type = item.type
|
||||
t.dir = dir -- points to top level "dir" item
|
||||
dir_cache[cache_name] = t
|
||||
end
|
||||
return t
|
||||
|
@ -104,18 +112,13 @@ end
|
|||
|
||||
|
||||
function TreeView:check_cache()
|
||||
-- invalidate cache's skip values if project_files has changed
|
||||
for i = 1, #core.project_directories do
|
||||
local dir = core.project_directories[i]
|
||||
local last_files = self.last[dir.name]
|
||||
if not last_files then
|
||||
self.last[dir.name] = dir.files
|
||||
else
|
||||
if dir.files ~= last_files then
|
||||
self:invalidate_cache(dir.name)
|
||||
self.last[dir.name] = dir.files
|
||||
end
|
||||
-- invalidate cache's skip values if directory is declared dirty
|
||||
if dir.is_dirty and self.cache[dir.name] then
|
||||
self:invalidate_cache(dir.name)
|
||||
end
|
||||
dir.is_dirty = false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -131,14 +134,14 @@ function TreeView:each_item()
|
|||
|
||||
for k = 1, #core.project_directories do
|
||||
local dir = core.project_directories[k]
|
||||
local dir_cached = self:get_cached(dir.item, dir.name)
|
||||
local dir_cached = self:get_cached(dir, dir.item, dir.name)
|
||||
coroutine.yield(dir_cached, ox, y, w, h)
|
||||
count_lines = count_lines + 1
|
||||
y = y + h
|
||||
local i = 1
|
||||
while i <= #dir.files and dir_cached.expanded do
|
||||
local item = dir.files[i]
|
||||
local cached = self:get_cached(item, dir.name)
|
||||
local cached = self:get_cached(dir, item, dir.name)
|
||||
|
||||
coroutine.yield(cached, ox, y, w, h)
|
||||
count_lines = count_lines + 1
|
||||
|
@ -206,7 +209,6 @@ local function create_directory_in(item)
|
|||
core.error("cannot create directory %q: %s", dirname, err)
|
||||
end
|
||||
item.expanded = true
|
||||
core.reschedule_project_scan()
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -223,26 +225,17 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
|
|||
if keymap.modkeys["ctrl"] and button == "left" then
|
||||
create_directory_in(hovered_item)
|
||||
else
|
||||
if core.project_files_limit and not hovered_item.expanded then
|
||||
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
|
||||
local index = 0
|
||||
-- The loop below is used to find the first match starting from the end
|
||||
-- in case there are multiple matches.
|
||||
while index and index + #filename < #abs_filename do
|
||||
index = string.find(abs_filename, filename, index + 1, true)
|
||||
end
|
||||
-- we assume here index is not nil because the abs_filename must contain the
|
||||
-- relative filename
|
||||
local dirname = string.sub(abs_filename, 1, index - 2)
|
||||
if core.is_project_folder(dirname) then
|
||||
core.scan_project_folder(dirname, filename)
|
||||
self:invalidate_cache(dirname)
|
||||
end
|
||||
end
|
||||
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
|
||||
else
|
||||
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)
|
||||
core.root_view:open_doc(core.open_doc(doc_filename))
|
||||
end)
|
||||
|
@ -295,6 +288,12 @@ function TreeView:draw_tooltip()
|
|||
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()
|
||||
self:draw_background(style.background2)
|
||||
|
||||
|
@ -318,6 +317,9 @@ function TreeView:draw()
|
|||
color = style.accent
|
||||
end
|
||||
|
||||
-- allow for color overrides
|
||||
local icon_color = self:color_for_item(item.abs_filename) or color
|
||||
|
||||
-- icons
|
||||
x = x + item.depth * style.padding.x + style.padding.x
|
||||
if item.type == "dir" then
|
||||
|
@ -325,11 +327,11 @@ function TreeView:draw()
|
|||
local icon2 = item.expanded and "D" or "d"
|
||||
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
|
||||
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
|
||||
else
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -404,7 +406,7 @@ function RootView:draw(...)
|
|||
end
|
||||
|
||||
local function is_project_folder(path)
|
||||
return common.basename(core.project_dir) == path
|
||||
return core.project_dir == path
|
||||
end
|
||||
|
||||
menu:register(function() return view.hovered_item end, {
|
||||
|
@ -415,7 +417,7 @@ menu:register(function() return view.hovered_item end, {
|
|||
menu:register(
|
||||
function()
|
||||
return view.hovered_item
|
||||
and not is_project_folder(view.hovered_item.filename)
|
||||
and not is_project_folder(view.hovered_item.abs_filename)
|
||||
end,
|
||||
{
|
||||
{ text = "Rename", command = "treeview:rename" },
|
||||
|
@ -461,14 +463,12 @@ command.add(function() return view.hovered_item ~= nil end, {
|
|||
else
|
||||
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
|
||||
end
|
||||
core.reschedule_project_scan()
|
||||
end, common.path_suggest)
|
||||
end,
|
||||
|
||||
["treeview:new-file"] = function()
|
||||
local dir_name = view.hovered_item.filename
|
||||
if not is_project_folder(dir_name) then
|
||||
core.command_view:set_text(dir_name .. "/")
|
||||
if not is_project_folder(view.hovered_item.abs_filename) then
|
||||
core.command_view:set_text(view.hovered_item.filename .. "/")
|
||||
end
|
||||
core.command_view:enter("Filename", function(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:close()
|
||||
core.root_view:open_doc(core.open_doc(doc_filename))
|
||||
core.reschedule_project_scan()
|
||||
core.log("Created %s", doc_filename)
|
||||
end, common.path_suggest)
|
||||
end,
|
||||
|
||||
["treeview:new-folder"] = function()
|
||||
local dir_name = view.hovered_item.filename
|
||||
if not is_project_folder(dir_name) then
|
||||
core.command_view:set_text(dir_name .. "/")
|
||||
if not is_project_folder(view.hovered_item.abs_filename) then
|
||||
core.command_view:set_text(view.hovered_item.filename .. "/")
|
||||
end
|
||||
core.command_view:enter("Folder Name", function(filename)
|
||||
local dir_path = core.project_dir .. PATHSEP .. filename
|
||||
common.mkdirp(dir_path)
|
||||
core.reschedule_project_scan()
|
||||
core.log("Created %s", dir_path)
|
||||
end, common.path_suggest)
|
||||
end,
|
||||
|
@ -526,7 +523,6 @@ command.add(function() return view.hovered_item ~= nil end, {
|
|||
return
|
||||
end
|
||||
end
|
||||
core.reschedule_project_scan()
|
||||
core.log("Deleted \"%s\"", filename)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,9 @@ renderer.color = {}
|
|||
---@class renderer.fontoptions
|
||||
---@field public antialiasing "'grayscale'" | "'subpixel'"
|
||||
---@field public hinting "'slight'" | "'none'" | '"full"'
|
||||
-- @field public bold boolean
|
||||
-- @field public italic boolean
|
||||
-- @field public underline boolean
|
||||
renderer.fontoptions = {}
|
||||
|
||||
---
|
||||
|
@ -58,15 +61,6 @@ function renderer.font:set_tab_size(chars) end
|
|||
---@return number
|
||||
function renderer.font:get_width(text) end
|
||||
|
||||
---
|
||||
---Get the width in subpixels of the given text when
|
||||
---rendered with this font.
|
||||
---
|
||||
---@param text string
|
||||
---
|
||||
---@return number
|
||||
function renderer.font:get_width_subpixel(text) end
|
||||
|
||||
---
|
||||
---Get the height in pixels that occupies a single character
|
||||
---when rendered with this font.
|
||||
|
@ -74,12 +68,6 @@ function renderer.font:get_width_subpixel(text) end
|
|||
---@return number
|
||||
function renderer.font:get_height() end
|
||||
|
||||
---
|
||||
---Gets the font subpixel scale.
|
||||
---
|
||||
---@return number
|
||||
function renderer.font:subpixel_scale() end
|
||||
|
||||
---
|
||||
---Get the current size of the font.
|
||||
---
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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__
|
||||
|
|
@ -0,0 +1 @@
|
|||
lite_includes += include_directories('.')
|
|
@ -22,6 +22,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## septag/dmon
|
||||
|
||||
Copyright 2019 Sepehr Taghdisian. All rights reserved.
|
||||
|
||||
https://github.com/septag/dmon
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## Fira Sans
|
||||
|
||||
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
|
||||
|
|
10
meson.build
10
meson.build
|
@ -1,6 +1,6 @@
|
|||
project('lite-xl',
|
||||
['c'],
|
||||
version : '2.0.2',
|
||||
version : '2.0.3',
|
||||
license : 'MIT',
|
||||
meson_version : '>= 0.54',
|
||||
default_options : ['c_std=gnu11']
|
||||
|
@ -23,6 +23,7 @@ endif
|
|||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
lite_includes = []
|
||||
lite_cargs = []
|
||||
# On macos we need to use the SDL renderer to support retina displays
|
||||
if get_option('renderer') or host_machine.system() == 'darwin'
|
||||
|
@ -45,6 +46,7 @@ endif
|
|||
if not get_option('source-only')
|
||||
libm = cc.find_library('m', required : false)
|
||||
libdl = cc.find_library('dl', required : false)
|
||||
threads_dep = dependency('threads')
|
||||
lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'],
|
||||
default_options: ['shared=false', 'use_readline=false', 'app=false']
|
||||
)
|
||||
|
@ -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'
|
||||
# Note that we need to explicitly add the windows socket DLL because
|
||||
|
@ -118,10 +120,8 @@ configure_file(
|
|||
install_dir : lite_datadir / 'core',
|
||||
)
|
||||
|
||||
#===============================================================================
|
||||
# Targets
|
||||
#===============================================================================
|
||||
if not get_option('source-only')
|
||||
subdir('lib/dmon')
|
||||
subdir('src')
|
||||
subdir('scripts')
|
||||
endif
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -23,6 +23,7 @@ Various scripts and configurations used to configure, build, and package Lite XL
|
|||
Preferably not to be used in user systems.
|
||||
- **fontello-config.json**: Used by the icons generator.
|
||||
- **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
|
||||
[2]: https://docs.appimage.org/
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -186,7 +186,7 @@ main() {
|
|||
|
||||
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 exe_file="$(pwd)/${dest_dir}/lite-xl"
|
||||
|
|
|
@ -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);
|
||||
if (rc < 0) {
|
||||
pcre2_match_data_free(md);
|
||||
if (rc != PCRE2_ERROR_NOMATCH)
|
||||
luaL_error(L, "regex matching error %d", rc);
|
||||
if (rc != PCRE2_ERROR_NOMATCH) {
|
||||
PCRE2_UCHAR buffer[120];
|
||||
pcre2_get_error_message(rc, buffer, sizeof(buffer));
|
||||
luaL_error(L, "regex matching error %d: %s", rc, buffer);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md);
|
||||
|
|
|
@ -6,16 +6,18 @@ static int f_font_load(lua_State *L) {
|
|||
const char *filename = luaL_checkstring(L, 1);
|
||||
float size = luaL_checknumber(L, 2);
|
||||
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)) {
|
||||
lua_getfield(L, 3, "antialiasing");
|
||||
if (lua_isstring(L, -1)) {
|
||||
const char *antialiasing = lua_tostring(L, -1);
|
||||
if (antialiasing) {
|
||||
if (strcmp(antialiasing, "grayscale") == 0) {
|
||||
subpixel = false;
|
||||
if (strcmp(antialiasing, "none") == 0) {
|
||||
font_antialiasing = FONT_ANTIALIASING_NONE;
|
||||
} else if (strcmp(antialiasing, "grayscale") == 0) {
|
||||
font_antialiasing = FONT_ANTIALIASING_GRAYSCALE;
|
||||
} else if (strcmp(antialiasing, "subpixel") == 0) {
|
||||
subpixel = true;
|
||||
font_antialiasing = FONT_ANTIALIASING_SUBPIXEL;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
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)
|
||||
return luaL_error(L, "failed to load 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) {
|
||||
RenRect rect;
|
||||
rect.x = luaL_checknumber(L, 1);
|
||||
rect.y = luaL_checknumber(L, 2);
|
||||
rect.width = luaL_checknumber(L, 3);
|
||||
rect.height = luaL_checknumber(L, 4);
|
||||
lua_Number x = luaL_checknumber(L, 1);
|
||||
lua_Number y = luaL_checknumber(L, 2);
|
||||
lua_Number w = luaL_checknumber(L, 3);
|
||||
lua_Number h = luaL_checknumber(L, 4);
|
||||
RenRect rect = rect_to_grid(x, y, w, h);
|
||||
rencache_set_clip_rect(rect);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int f_draw_rect(lua_State *L) {
|
||||
RenRect rect;
|
||||
rect.x = luaL_checknumber(L, 1);
|
||||
rect.y = luaL_checknumber(L, 2);
|
||||
rect.width = luaL_checknumber(L, 3);
|
||||
rect.height = luaL_checknumber(L, 4);
|
||||
lua_Number x = luaL_checknumber(L, 1);
|
||||
lua_Number y = luaL_checknumber(L, 2);
|
||||
lua_Number w = luaL_checknumber(L, 3);
|
||||
lua_Number h = luaL_checknumber(L, 4);
|
||||
RenRect rect = rect_to_grid(x, y, w, h);
|
||||
RenColor color = checkcolor(L, 5, 255);
|
||||
rencache_draw_rect(rect, color);
|
||||
return 0;
|
||||
|
|
222
src/api/system.c
222
src/api/system.c
|
@ -6,11 +6,14 @@
|
|||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include "api.h"
|
||||
#include "dirmonitor.h"
|
||||
#include "rencache.h"
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <windows.h>
|
||||
#include <fileapi.h>
|
||||
#elif __linux__
|
||||
#include <sys/vfs.h>
|
||||
#endif
|
||||
|
||||
extern SDL_Window *window;
|
||||
|
@ -18,9 +21,11 @@ extern SDL_Window *window;
|
|||
|
||||
static const char* button_name(int button) {
|
||||
switch (button) {
|
||||
case 1 : return "left";
|
||||
case 2 : return "middle";
|
||||
case 3 : return "right";
|
||||
case SDL_BUTTON_LEFT : return "left";
|
||||
case SDL_BUTTON_MIDDLE : return "middle";
|
||||
case SDL_BUTTON_RIGHT : return "right";
|
||||
case SDL_BUTTON_X1 : return "x";
|
||||
case SDL_BUTTON_X2 : return "y";
|
||||
default : return "?";
|
||||
}
|
||||
}
|
||||
|
@ -236,6 +241,26 @@ top:
|
|||
lua_pushnumber(L, e.wheel.y);
|
||||
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:
|
||||
goto top;
|
||||
}
|
||||
|
@ -524,6 +549,45 @@ static int f_get_file_info(lua_State *L) {
|
|||
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) {
|
||||
const char *path = luaL_checkstring(L, 1);
|
||||
|
@ -591,56 +655,32 @@ static int f_exec(lua_State *L) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int f_fuzzy_match(lua_State *L) {
|
||||
size_t strLen, ptnLen;
|
||||
const char *str = luaL_checklstring(L, 1, &strLen);
|
||||
const char *ptn = luaL_checklstring(L, 2, &ptnLen);
|
||||
bool files = false;
|
||||
if (lua_gettop(L) > 2 && lua_isboolean(L,3))
|
||||
files = lua_toboolean(L, 3);
|
||||
|
||||
int score = 0;
|
||||
int run = 0;
|
||||
|
||||
// Match things *backwards*. This allows for better matching on filenames than the above
|
||||
// If true match things *backwards*. This allows for better matching on filenames than the above
|
||||
// function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh
|
||||
// as the first result, rather than src/renderer.c. Clearly that's wrong.
|
||||
if (files) {
|
||||
const char* strEnd = str + strLen - 1;
|
||||
const char* ptnEnd = ptn + ptnLen - 1;
|
||||
while (strEnd >= str && ptnEnd >= ptn) {
|
||||
while (*strEnd == ' ') { strEnd--; }
|
||||
while (*ptnEnd == ' ') { ptnEnd--; }
|
||||
if (tolower(*strEnd) == tolower(*ptnEnd)) {
|
||||
score += run * 10 - (*strEnd != *ptnEnd);
|
||||
run++;
|
||||
ptnEnd--;
|
||||
} else {
|
||||
score -= 10;
|
||||
run = 0;
|
||||
}
|
||||
strEnd--;
|
||||
bool files = lua_gettop(L) > 2 && lua_isboolean(L,3) && lua_toboolean(L, 3);
|
||||
int score = 0, run = 0, increment = files ? -1 : 1;
|
||||
const char* strTarget = files ? str + strLen - 1 : str;
|
||||
const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn;
|
||||
while (strTarget >= str && ptnTarget >= ptn && *strTarget && *ptnTarget) {
|
||||
while (strTarget >= str && *strTarget == ' ') { strTarget += increment; }
|
||||
while (ptnTarget >= ptn && *ptnTarget == ' ') { ptnTarget += increment; }
|
||||
if (tolower(*strTarget) == tolower(*ptnTarget)) {
|
||||
score += run * 10 - (*strTarget != *ptnTarget);
|
||||
run++;
|
||||
ptnTarget += increment;
|
||||
} else {
|
||||
score -= 10;
|
||||
run = 0;
|
||||
}
|
||||
if (ptnEnd >= ptn) { return 0; }
|
||||
} else {
|
||||
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; }
|
||||
strTarget += increment;
|
||||
}
|
||||
|
||||
lua_pushnumber(L, score - (int)strLen);
|
||||
if (ptnTarget >= ptn && *ptnTarget) { return 0; }
|
||||
lua_pushnumber(L, score - (int)strLen * 10);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -738,6 +778,91 @@ static int f_load_native_plugin(lua_State *L) {
|
|||
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[] = {
|
||||
{ "poll_event", f_poll_event },
|
||||
|
@ -766,6 +891,13 @@ static const luaL_Reg lib[] = {
|
|||
{ "fuzzy_match", f_fuzzy_match },
|
||||
{ "set_window_opacity", f_set_window_opacity },
|
||||
{ "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 }
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
|
||||
#include "dirmonitor.h"
|
||||
|
||||
|
||||
SDL_Window *window;
|
||||
|
||||
|
@ -106,6 +108,8 @@ int main(int argc, char **argv) {
|
|||
SDL_DisplayMode dm;
|
||||
SDL_GetCurrentDisplayMode(0, &dm);
|
||||
|
||||
dirmonitor_init();
|
||||
|
||||
window = SDL_CreateWindow(
|
||||
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
|
||||
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
|
||||
|
@ -188,6 +192,7 @@ init_lua:
|
|||
|
||||
lua_close(L);
|
||||
ren_free_window_resources();
|
||||
dirmonitor_deinit();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ lite_sources = [
|
|||
'api/regex.c',
|
||||
'api/system.c',
|
||||
'api/process.c',
|
||||
'dirmonitor.c',
|
||||
'renderer.c',
|
||||
'renwindow.c',
|
||||
'rencache.c',
|
||||
|
@ -18,11 +19,11 @@ elif host_machine.system() == 'darwin'
|
|||
lite_sources += 'bundle_open.m'
|
||||
endif
|
||||
|
||||
lite_include = include_directories('.')
|
||||
lite_includes += include_directories('.')
|
||||
|
||||
executable('lite-xl',
|
||||
lite_sources + lite_rc,
|
||||
include_directories: [lite_include],
|
||||
include_directories: lite_includes,
|
||||
dependencies: lite_deps,
|
||||
c_args: lite_cargs,
|
||||
objc_args: lite_cargs,
|
||||
|
|
|
@ -123,7 +123,9 @@ void rencache_set_clip_rect(RenRect rect) {
|
|||
|
||||
|
||||
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);
|
||||
if (cmd) {
|
||||
cmd->rect = rect;
|
||||
|
|
|
@ -43,7 +43,7 @@ typedef struct RenFont {
|
|||
GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS];
|
||||
float size, space_advance, tab_advance;
|
||||
short max_height;
|
||||
bool subpixel;
|
||||
ERenFontAntialiasing antialiasing;
|
||||
ERenFontHinting hinting;
|
||||
unsigned char style;
|
||||
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) {
|
||||
switch (font->hinting) {
|
||||
case FONT_HINTING_SLIGHT: return FT_LOAD_TARGET_LIGHT | FT_LOAD_FORCE_AUTOHINT;
|
||||
case FONT_HINTING_FULL: return FT_LOAD_TARGET_NORMAL | FT_LOAD_FORCE_AUTOHINT;
|
||||
case FONT_HINTING_NONE: return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING;
|
||||
}
|
||||
return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING;
|
||||
int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO
|
||||
: (font->hinting == FONT_HINTING_SLIGHT ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_NORMAL);
|
||||
int hinting = font->hinting == FONT_HINTING_NONE ? FT_LOAD_NO_HINTING : FT_LOAD_FORCE_AUTOHINT;
|
||||
return load_target | hinting;
|
||||
}
|
||||
|
||||
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 } ;
|
||||
switch (font->hinting) {
|
||||
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) {
|
||||
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;
|
||||
unsigned int byte_width = font->subpixel ? 3 : 1;
|
||||
int bitmaps_cached = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1;
|
||||
unsigned int byte_width = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1;
|
||||
for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) {
|
||||
GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet)));
|
||||
font->sets[j][idx] = set;
|
||||
|
@ -117,13 +117,15 @@ static void font_load_glyphset(RenFont* font, int idx) {
|
|||
continue;
|
||||
FT_GlyphSlot slot = font->face->glyph;
|
||||
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};
|
||||
pen_x += glyph_width;
|
||||
font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height;
|
||||
}
|
||||
if (pen_x == 0)
|
||||
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;
|
||||
for (int i = 0; i < MAX_GLYPHSET; ++i) {
|
||||
int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET);
|
||||
|
@ -136,7 +138,14 @@ static void font_load_glyphset(RenFont* font, int idx) {
|
|||
for (int line = 0; line < slot->bitmap.rows; ++line) {
|
||||
int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width;
|
||||
int source_offset = line * slot->bitmap.pitch;
|
||||
memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
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);
|
||||
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) {
|
||||
|
@ -163,7 +172,7 @@ static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFo
|
|||
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;
|
||||
if (FT_New_Face( library, path, 0, &face))
|
||||
return NULL;
|
||||
|
@ -175,7 +184,7 @@ RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned cha
|
|||
strcpy(font->path, path);
|
||||
font->face = face;
|
||||
font->size = size;
|
||||
font->subpixel = subpixel;
|
||||
font->antialiasing = antialiasing;
|
||||
font->hinting = hinting;
|
||||
font->style = style;
|
||||
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) {
|
||||
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) {
|
||||
|
@ -206,7 +215,7 @@ void ren_font_free(RenFont* font) {
|
|||
|
||||
void ren_font_group_set_tab_size(RenFont **fonts, int n) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +256,7 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor
|
|||
const char* end = text + strlen(text);
|
||||
unsigned char* destination_pixels = surface->pixels;
|
||||
int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height;
|
||||
|
||||
while (text < end) {
|
||||
unsigned int codepoint, r, g, b;
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
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 src = { *(font->subpixel ? source_pixel++ : source_pixel), *(font->subpixel ? source_pixel++ : source_pixel), *source_pixel++ };
|
||||
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->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;
|
||||
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;
|
||||
*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;
|
||||
d += x1 + y1 * surface->w;
|
||||
int dr = surface->w - (x2 - x1);
|
||||
|
||||
unsigned int translated = SDL_MapRGB(surface->format, color.r, color.g, color.b);
|
||||
if (color.a == 0xff) {
|
||||
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 {
|
||||
RenColor translated_color = (RenColor){ translated & 0xFF, (translated >> 8) & 0xFF, (translated >> 16) & 0xFF, color.a };
|
||||
for (int j = y1; j < y2; j++) {
|
||||
for (int i = x1; i < x2; i++, d++)
|
||||
*d = blend_pixel(*d, color);
|
||||
*d = blend_pixel(*d, translated_color);
|
||||
d += dr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
#define FONT_FALLBACK_MAX 4
|
||||
typedef struct RenFont RenFont;
|
||||
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 struct { uint8_t b, g, r, a; } RenColor;
|
||||
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);
|
||||
void ren_font_free(RenFont *font);
|
||||
int ren_font_group_get_tab_size(RenFont **font);
|
||||
|
|
Loading…
Reference in New Issue