commit
2957968afc
@ -0,0 +1,3 @@ |
||||
winlib/* linguist-vendored |
||||
src/lib/* linguist-vendored |
||||
icon.inl linguist-vendored |
@ -0,0 +1,3 @@ |
||||
# These are supported funding model platforms |
||||
|
||||
github: rxi |
@ -0,0 +1,3 @@ |
||||
**/*.o |
||||
lite |
||||
TODO |
@ -0,0 +1,19 @@ |
||||
Copyright (c) 2020 rxi |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
of the Software, and to permit persons to whom the Software is furnished to do |
||||
so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,41 @@ |
||||
# lite |
||||
![screenshot](https://user-images.githubusercontent.com/3920290/81471642-6c165880-91ea-11ea-8cd1-fae7ae8f0bc4.png) |
||||
|
||||
A lightweight text editor written in Lua |
||||
|
||||
* **[Get lite](https://github.com/rxi/lite/releases/latest)** — Download |
||||
for Windows and Linux |
||||
* **[Get started](doc/usage.md)** — A quick overview on how to get started |
||||
* **[Get plugins](https://github.com/rxi/lite-plugins)** — Add additional |
||||
functionality |
||||
* **[Get color themes](https://github.com/rxi/lite-colors)** — Add additional colors |
||||
themes |
||||
|
||||
## Overview |
||||
lite is a lightweight text editor written mostly in Lua — it aims to provide |
||||
something practical, pretty, *small* and fast, implemented as simply as |
||||
possible; easy to modify and extend, or to use without doing either. |
||||
|
||||
## Customization |
||||
Additional functionality can be added through plugins which are available from |
||||
the [plugins repository](https://github.com/rxi/lite-plugins); additional color |
||||
themes can be found in the [colors repository](https://github.com/rxi/lite-colors). |
||||
The editor can be customized by making changes to the |
||||
[user module](data/user/init.lua). |
||||
|
||||
## Building |
||||
You can build the project yourself on Linux using the `build.sh` script |
||||
or on Windows using the `build.bat` script *([MinGW](https://nuwen.net/mingw.html) is required)*. |
||||
Note that the project does not need to be rebuilt if you are only making changes |
||||
to the Lua portion of the code. |
||||
|
||||
## Contributing |
||||
Any additional functionality that can be added through a plugin should be done |
||||
so as a plugin, after which a pull request to the |
||||
[plugins repository](https://github.com/rxi/lite-plugins) can be made. In hopes |
||||
of remaining lightweight, pull requests adding additional functionality to the |
||||
core will likely not be merged. Bug reports and bug fixes are welcome. |
||||
|
||||
## License |
||||
This project is free software; you can redistribute it and/or modify it under |
||||
the terms of the MIT license. See [LICENSE](LICENSE) for details. |
@ -0,0 +1,69 @@ |
||||
local core = require "core" |
||||
local command = {} |
||||
|
||||
command.map = {} |
||||
|
||||
local always_true = function() return true end |
||||
|
||||
|
||||
function command.add(predicate, map) |
||||
predicate = predicate or always_true |
||||
if type(predicate) == "string" then |
||||
predicate = require(predicate) |
||||
end |
||||
if type(predicate) == "table" then |
||||
local class = predicate |
||||
predicate = function() return core.active_view:is(class) end |
||||
end |
||||
for name, fn in pairs(map) do |
||||
assert(not command.map[name], "command already exists: " .. name) |
||||
command.map[name] = { predicate = predicate, perform = fn } |
||||
end |
||||
end |
||||
|
||||
|
||||
local function capitalize_first(str) |
||||
return str:sub(1, 1):upper() .. str:sub(2) |
||||
end |
||||
|
||||
function command.prettify_name(name) |
||||
return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first) |
||||
end |
||||
|
||||
|
||||
function command.get_all_valid() |
||||
local res = {} |
||||
for name, cmd in pairs(command.map) do |
||||
if cmd.predicate() then |
||||
table.insert(res, name) |
||||
end |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function perform(name) |
||||
local cmd = command.map[name] |
||||
if cmd and cmd.predicate() then |
||||
cmd.perform() |
||||
return true |
||||
end |
||||
return false |
||||
end |
||||
|
||||
|
||||
function command.perform(...) |
||||
local ok, res = core.try(perform, ...) |
||||
return not ok or res |
||||
end |
||||
|
||||
|
||||
function command.add_defaults() |
||||
local reg = { "core", "root", "command", "doc", "findreplace" } |
||||
for _, name in ipairs(reg) do |
||||
require("core.commands." .. name) |
||||
end |
||||
end |
||||
|
||||
|
||||
return command |
@ -0,0 +1,30 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local CommandView = require "core.commandview" |
||||
|
||||
local function has_commandview() |
||||
return core.active_view:is(CommandView) |
||||
end |
||||
|
||||
|
||||
command.add(has_commandview, { |
||||
["command:submit"] = function() |
||||
core.active_view:submit() |
||||
end, |
||||
|
||||
["command:complete"] = function() |
||||
core.active_view:complete() |
||||
end, |
||||
|
||||
["command:escape"] = function() |
||||
core.active_view:exit() |
||||
end, |
||||
|
||||
["command:select-previous"] = function() |
||||
core.active_view:move_suggestion_idx(1) |
||||
end, |
||||
|
||||
["command:select-next"] = function() |
||||
core.active_view:move_suggestion_idx(-1) |
||||
end, |
||||
}) |
@ -0,0 +1,101 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local command = require "core.command" |
||||
local keymap = require "core.keymap" |
||||
local LogView = require "core.logview" |
||||
|
||||
|
||||
local fullscreen = false |
||||
|
||||
command.add(nil, { |
||||
["core:quit"] = function() |
||||
core.quit() |
||||
end, |
||||
|
||||
["core:force-quit"] = function() |
||||
core.quit(true) |
||||
end, |
||||
|
||||
["core:toggle-fullscreen"] = function() |
||||
fullscreen = not fullscreen |
||||
system.set_window_mode(fullscreen and "fullscreen" or "normal") |
||||
end, |
||||
|
||||
["core:reload-module"] = function() |
||||
core.command_view:enter("Reload Module", function(text, item) |
||||
local text = item and item.text or text |
||||
core.reload_module(text) |
||||
core.log("Reloaded module %q", text) |
||||
end, function(text) |
||||
local items = {} |
||||
for name in pairs(package.loaded) do |
||||
table.insert(items, name) |
||||
end |
||||
return common.fuzzy_match(items, text) |
||||
end) |
||||
end, |
||||
|
||||
["core:find-command"] = function() |
||||
local commands = command.get_all_valid() |
||||
core.command_view:enter("Do Command", function(text, item) |
||||
if item then |
||||
command.perform(item.command) |
||||
end |
||||
end, function(text) |
||||
local res = common.fuzzy_match(commands, text) |
||||
for i, name in ipairs(res) do |
||||
res[i] = { |
||||
text = command.prettify_name(name), |
||||
info = keymap.get_binding(name), |
||||
command = name, |
||||
} |
||||
end |
||||
return res |
||||
end) |
||||
end, |
||||
|
||||
["core:find-file"] = function() |
||||
core.command_view:enter("Open File From Project", function(text, item) |
||||
text = item and item.text or text |
||||
core.root_view:open_doc(core.open_doc(text)) |
||||
end, function(text) |
||||
local files = {} |
||||
for _, item in pairs(core.project_files) do |
||||
if item.type == "file" then |
||||
table.insert(files, item.filename) |
||||
end |
||||
end |
||||
return common.fuzzy_match(files, text) |
||||
end) |
||||
end, |
||||
|
||||
["core:new-doc"] = function() |
||||
core.root_view:open_doc(core.open_doc()) |
||||
end, |
||||
|
||||
["core:open-file"] = function() |
||||
core.command_view:enter("Open File", function(text) |
||||
core.root_view:open_doc(core.open_doc(text)) |
||||
end, common.path_suggest) |
||||
end, |
||||
|
||||
["core:open-log"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
node:add_view(LogView()) |
||||
end, |
||||
|
||||
["core:open-user-module"] = function() |
||||
core.root_view:open_doc(core.open_doc(EXEDIR .. "/data/user/init.lua")) |
||||
end, |
||||
|
||||
["core:open-project-module"] = function() |
||||
local filename = ".lite_project.lua" |
||||
if system.get_file_info(filename) then |
||||
core.root_view:open_doc(core.open_doc(filename)) |
||||
else |
||||
local doc = core.open_doc() |
||||
core.root_view:open_doc(doc) |
||||
doc:save(filename) |
||||
end |
||||
end, |
||||
}) |
@ -0,0 +1,365 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
local translate = require "core.doc.translate" |
||||
local DocView = require "core.docview" |
||||
|
||||
|
||||
local function dv() |
||||
return core.active_view |
||||
end |
||||
|
||||
|
||||
local function doc() |
||||
return core.active_view.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 insert_at_start_of_selected_lines(text, skip_empty) |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
for line = line1, line2 do |
||||
local line_text = doc().lines[line] |
||||
if (not skip_empty or line_text:find("%S")) then |
||||
doc():insert(line, 1, text) |
||||
end |
||||
end |
||||
doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap) |
||||
end |
||||
|
||||
|
||||
local function remove_from_start_of_selected_lines(text, skip_empty) |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
for line = line1, line2 do |
||||
local line_text = doc().lines[line] |
||||
if line_text:sub(1, #text) == text |
||||
and (not skip_empty or line_text:find("%S")) |
||||
then |
||||
doc():remove(line, 1, line, #text + 1) |
||||
end |
||||
end |
||||
doc():set_selection(line1, col1 - #text, line2, col2 - #text, swap) |
||||
end |
||||
|
||||
|
||||
local function append_line_if_last_line(line) |
||||
if line >= #doc().lines then |
||||
doc():insert(line, math.huge, "\n") |
||||
end |
||||
end |
||||
|
||||
|
||||
local function save(filename) |
||||
doc():save(filename) |
||||
core.log("Saved \"%s\"", doc().filename) |
||||
end |
||||
|
||||
|
||||
local commands = { |
||||
["doc:undo"] = function() |
||||
doc():undo() |
||||
end, |
||||
|
||||
["doc:redo"] = function() |
||||
doc():redo() |
||||
end, |
||||
|
||||
["doc:cut"] = function() |
||||
if doc():has_selection() then |
||||
local text = doc():get_text(doc():get_selection()) |
||||
system.set_clipboard(text) |
||||
doc():delete_to(0) |
||||
end |
||||
end, |
||||
|
||||
["doc:copy"] = function() |
||||
if doc():has_selection() then |
||||
local text = doc():get_text(doc():get_selection()) |
||||
system.set_clipboard(text) |
||||
end |
||||
end, |
||||
|
||||
["doc:paste"] = function() |
||||
doc():text_input(system.get_clipboard():gsub("\r", "")) |
||||
end, |
||||
|
||||
["doc:newline"] = function() |
||||
local line, col = doc():get_selection() |
||||
local indent = doc().lines[line]:match("^[\t ]*") |
||||
if col <= #indent then |
||||
indent = indent:sub(#indent + 2 - col) |
||||
end |
||||
doc():text_input("\n" .. indent) |
||||
end, |
||||
|
||||
["doc:newline-below"] = function() |
||||
local line = doc():get_selection() |
||||
local indent = doc().lines[line]:match("^[\t ]*") |
||||
doc():insert(line, math.huge, "\n" .. indent) |
||||
doc():set_selection(line + 1, math.huge) |
||||
end, |
||||
|
||||
["doc:newline-above"] = function() |
||||
local line = doc():get_selection() |
||||
local indent = doc().lines[line]:match("^[\t ]*") |
||||
doc():insert(line, 1, indent .. "\n") |
||||
doc():set_selection(line, math.huge) |
||||
end, |
||||
|
||||
["doc:delete"] = function() |
||||
local line, col = doc():get_selection() |
||||
if not doc():has_selection() and doc().lines[line]:find("^%s*$", col) then |
||||
doc():remove(line, col, line, math.huge) |
||||
end |
||||
doc():delete_to(translate.next_char) |
||||
end, |
||||
|
||||
["doc:backspace"] = function() |
||||
local line, col = doc():get_selection() |
||||
if not doc():has_selection() then |
||||
local text = doc():get_text(line, 1, line, col) |
||||
if #text >= config.indent_size and text:find("^ *$") then |
||||
doc():delete_to(0, -config.indent_size) |
||||
return |
||||
end |
||||
end |
||||
doc():delete_to(translate.previous_char) |
||||
end, |
||||
|
||||
["doc:select-all"] = function() |
||||
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() |
||||
local line1, _, line2, _, swap = doc():get_selection(true) |
||||
append_line_if_last_line(line2) |
||||
doc():set_selection(line1, 1, line2 + 1, 1, swap) |
||||
end, |
||||
|
||||
["doc:select-word"] = function() |
||||
local line1, col1 = doc():get_selection(true) |
||||
local line1, col1 = translate.start_of_word(doc(), line1, col1) |
||||
local line2, col2 = translate.end_of_word(doc(), line1, col1) |
||||
doc():set_selection(line2, col2, line1, col1) |
||||
end, |
||||
|
||||
["doc:join-lines"] = function() |
||||
local line1, _, line2 = doc():get_selection(true) |
||||
if line1 == line2 then line2 = line2 + 1 end |
||||
local text = doc():get_text(line1, 1, line2, math.huge) |
||||
text = text:gsub("(.-)\n[\t ]*", function(x) |
||||
return x:find("^%s*$") and x or x .. " " |
||||
end) |
||||
doc():insert(line1, 1, text) |
||||
doc():remove(line1, #text + 1, line2, math.huge) |
||||
if doc():has_selection() then |
||||
doc():set_selection(line1, math.huge) |
||||
end |
||||
end, |
||||
|
||||
["doc:indent"] = function() |
||||
local text = get_indent_string() |
||||
if doc():has_selection() then |
||||
insert_at_start_of_selected_lines(text) |
||||
else |
||||
doc():text_input(text) |
||||
end |
||||
end, |
||||
|
||||
["doc:unindent"] = function() |
||||
local text = get_indent_string() |
||||
remove_from_start_of_selected_lines(text) |
||||
end, |
||||
|
||||
["doc:duplicate-lines"] = function() |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
append_line_if_last_line(line2) |
||||
local text = doc():get_text(line1, 1, line2 + 1, 1) |
||||
doc():insert(line2 + 1, 1, text) |
||||
local n = line2 - line1 + 1 |
||||
doc():set_selection(line1 + n, col1, line2 + n, col2, swap) |
||||
end, |
||||
|
||||
["doc:delete-lines"] = function() |
||||
local line1, col1, line2 = doc():get_selection(true) |
||||
append_line_if_last_line(line2) |
||||
doc():remove(line1, 1, line2 + 1, 1) |
||||
doc():set_selection(line1, col1) |
||||
end, |
||||
|
||||
["doc:move-lines-up"] = function() |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
append_line_if_last_line(line2) |
||||
if line1 > 1 then |
||||
local text = doc().lines[line1 - 1] |
||||
doc():insert(line2 + 1, 1, text) |
||||
doc():remove(line1 - 1, 1, line1, 1) |
||||
doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap) |
||||
end |
||||
end, |
||||
|
||||
["doc:move-lines-down"] = function() |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
append_line_if_last_line(line2 + 1) |
||||
if line2 < #doc().lines then |
||||
local text = doc().lines[line2 + 1] |
||||
doc():remove(line2 + 1, 1, line2 + 2, 1) |
||||
doc():insert(line1, 1, text) |
||||
doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap) |
||||
end |
||||
end, |
||||
|
||||
["doc:toggle-line-comments"] = function() |
||||
local comment = doc().syntax.comment |
||||
if not comment then return end |
||||
local comment_text = comment .. " " |
||||
local line1, _, line2 = doc():get_selection(true) |
||||
local uncomment = true |
||||
for line = line1, line2 do |
||||
local text = doc().lines[line] |
||||
if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then |
||||
uncomment = false |
||||
end |
||||
end |
||||
if uncomment then |
||||
remove_from_start_of_selected_lines(comment_text, true) |
||||
else |
||||
insert_at_start_of_selected_lines(comment_text, true) |
||||
end |
||||
end, |
||||
|
||||
["doc:upper-case"] = function() |
||||
doc():replace(string.upper) |
||||
end, |
||||
|
||||
["doc:lower-case"] = function() |
||||
doc():replace(string.lower) |
||||
end, |
||||
|
||||
["doc:go-to-line"] = function() |
||||
local dv = dv() |
||||
|
||||
local items |
||||
local function init_items() |
||||
if items then return end |
||||
items = {} |
||||
local mt = { __tostring = function(x) return x.text end } |
||||
for i, line in ipairs(dv.doc.lines) do |
||||
local item = { text = line:sub(1, -2), line = i, info = "line: " .. i } |
||||
table.insert(items, setmetatable(item, mt)) |
||||
end |
||||
end |
||||
|
||||
core.command_view:enter("Go To Line", function(text, item) |
||||
local line = item and item.line or tonumber(text) |
||||
if not line then |
||||
core.error("Invalid line number or unmatched string") |
||||
return |
||||
end |
||||
dv.doc:set_selection(line, 1 ) |
||||
dv:scroll_to_line(line, true) |
||||
|
||||
end, function(text) |
||||
if not text:find("^%d*$") then |
||||
init_items() |
||||
return common.fuzzy_match(items, text) |
||||
end |
||||
end) |
||||
end, |
||||
|
||||
["doc:toggle-line-ending"] = function() |
||||
doc().crlf = not doc().crlf |
||||
end, |
||||
|
||||
["doc:save-as"] = function() |
||||
if doc().filename then |
||||
core.command_view:set_text(doc().filename) |
||||
end |
||||
core.command_view:enter("Save As", function(filename) |
||||
save(filename) |
||||
end, common.path_suggest) |
||||
end, |
||||
|
||||
["doc:save"] = function() |
||||
if doc().filename then |
||||
save() |
||||
else |
||||
command.perform("doc:save-as") |
||||
end |
||||
end, |
||||
|
||||
["doc:rename"] = function() |
||||
local old_filename = doc().filename |
||||
if not old_filename then |
||||
core.error("Cannot rename unsaved doc") |
||||
return |
||||
end |
||||
core.command_view:set_text(old_filename) |
||||
core.command_view:enter("Rename", function(filename) |
||||
doc():save(filename) |
||||
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) |
||||
if filename ~= old_filename then |
||||
os.remove(old_filename) |
||||
end |
||||
end, common.path_suggest) |
||||
end, |
||||
} |
||||
|
||||
|
||||
local translations = { |
||||
["previous-char"] = translate.previous_char, |
||||
["next-char"] = translate.next_char, |
||||
["previous-word-start"] = translate.previous_word_start, |
||||
["next-word-end"] = translate.next_word_end, |
||||
["previous-block-start"] = translate.previous_block_start, |
||||
["next-block-end"] = translate.next_block_end, |
||||
["start-of-doc"] = translate.start_of_doc, |
||||
["end-of-doc"] = translate.end_of_doc, |
||||
["start-of-line"] = translate.start_of_line, |
||||
["end-of-line"] = translate.end_of_line, |
||||
["start-of-word"] = translate.start_of_word, |
||||
["end-of-word"] = translate.end_of_word, |
||||
["previous-line"] = DocView.translate.previous_line, |
||||
["next-line"] = DocView.translate.next_line, |
||||
["previous-page"] = DocView.translate.previous_page, |
||||
["next-page"] = DocView.translate.next_page, |
||||
} |
||||
|
||||
for name, fn in pairs(translations) do |
||||
commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end |
||||
commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end |
||||
commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end |
||||
end |
||||
|
||||
commands["doc:move-to-previous-char"] = function() |
||||
if doc():has_selection() then |
||||
local line, col = doc():get_selection(true) |
||||
doc():set_selection(line, col) |
||||
else |
||||
doc():move_to(translate.previous_char) |
||||
end |
||||
end |
||||
|
||||
commands["doc:move-to-next-char"] = function() |
||||
if doc():has_selection() then |
||||
local _, _, line, col = doc():get_selection(true) |
||||
doc():set_selection(line, col) |
||||
else |
||||
doc():move_to(translate.next_char) |
||||
end |
||||
end |
||||
|
||||
command.add("core.docview", commands) |
@ -0,0 +1,170 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local config = require "core.config" |
||||
local search = require "core.doc.search" |
||||
local DocView = require "core.docview" |
||||
|
||||
local max_previous_finds = 50 |
||||
|
||||
|
||||
local function doc() |
||||
return core.active_view.doc |
||||
end |
||||
|
||||
|
||||
local previous_finds |
||||
local last_doc |
||||
local last_fn, last_text |
||||
|
||||
|
||||
local function push_previous_find(doc, sel) |
||||
if last_doc ~= doc then |
||||
last_doc = doc |
||||
previous_finds = {} |
||||
end |
||||
if #previous_finds >= max_previous_finds then |
||||
table.remove(previous_finds, 1) |
||||
end |
||||
table.insert(previous_finds, sel or { doc:get_selection() }) |
||||
end |
||||
|
||||
|
||||
local function find(label, search_fn) |
||||
local dv = core.active_view |
||||
local sel = { dv.doc:get_selection() } |
||||
local text = dv.doc:get_text(table.unpack(sel)) |
||||
local found = false |
||||
|
||||
core.command_view:set_text(text, true) |
||||
|
||||
core.command_view:enter(label, function(text) |
||||
if found then |
||||
last_fn, last_text = search_fn, text |
||||
previous_finds = {} |
||||
push_previous_find(dv.doc, sel) |
||||
else |
||||
core.error("Couldn't find %q", text) |
||||
dv.doc:set_selection(table.unpack(sel)) |
||||
dv:scroll_to_make_visible(sel[1], sel[2]) |
||||
end |
||||
|
||||
end, function(text) |
||||
local ok, line1, col1, line2, col2 = pcall(search_fn, dv.doc, sel[1], sel[2], text) |
||||
if ok and line1 and text ~= "" then |
||||
dv.doc:set_selection(line2, col2, line1, col1) |
||||
dv:scroll_to_line(line2, true) |
||||
found = true |
||||
else |
||||
dv.doc:set_selection(table.unpack(sel)) |
||||
found = false |
||||
end |
||||
|
||||
end, function(explicit) |
||||
if explicit then |
||||
dv.doc:set_selection(table.unpack(sel)) |
||||
dv:scroll_to_make_visible(sel[1], sel[2]) |
||||
end |
||||
end) |
||||
end |
||||
|
||||
|
||||
local function replace(kind, default, fn) |
||||
core.command_view:set_text(default, true) |
||||
|
||||
core.command_view:enter("Find To Replace " .. kind, function(old) |
||||
core.command_view:set_text(old, true) |
||||
|
||||
local s = string.format("Replace %s %q With", kind, old) |
||||
core.command_view:enter(s, function(new) |
||||
local n = doc():replace(function(text) |
||||
return fn(text, old, new) |
||||
end) |
||||
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) |
||||
end) |
||||
end) |
||||
end |
||||
|
||||
|
||||
local function has_selection() |
||||
return core.active_view:is(DocView) |
||||
and core.active_view.doc:has_selection() |
||||
end |
||||
|
||||
command.add(has_selection, { |
||||
["find-replace:select-next"] = function() |
||||
local l1, c1, l2, c2 = doc():get_selection(true) |
||||
local text = doc():get_text(l1, c1, l2, c2) |
||||
local l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) |
||||
if l2 then doc():set_selection(l2, c2, l1, c1) end |
||||
end |
||||
}) |
||||
|
||||
command.add("core.docview", { |
||||
["find-replace:find"] = function() |
||||
find("Find Text", function(doc, line, col, text) |
||||
local opt = { wrap = true, no_case = true } |
||||
return search.find(doc, line, col, text, opt) |
||||
end) |
||||
end, |
||||
|
||||
["find-replace:find-pattern"] = function() |
||||
find("Find Text Pattern", function(doc, line, col, text) |
||||
local opt = { wrap = true, no_case = true, pattern = true } |
||||
return search.find(doc, line, col, text, opt) |
||||
end) |
||||
end, |
||||
|
||||
["find-replace:repeat-find"] = function() |
||||
if not last_fn then |
||||
core.error("No find to continue from") |
||||
else |
||||
local line, col = doc():get_selection() |
||||
local line1, col1, line2, col2 = last_fn(doc(), line, col, last_text) |
||||
if line1 then |
||||
push_previous_find(doc()) |
||||
doc():set_selection(line2, col2, line1, col1) |
||||
core.active_view:scroll_to_line(line2, true) |
||||
end |
||||
end |
||||
end, |
||||
|
||||
["find-replace:previous-find"] = function() |
||||
local sel = table.remove(previous_finds) |
||||
if not sel or doc() ~= last_doc then |
||||
core.error("No previous finds") |
||||
return |
||||
end |
||||
doc():set_selection(table.unpack(sel)) |
||||
core.active_view:scroll_to_line(sel[3], true) |
||||
end, |
||||
|
||||
["find-replace:replace"] = function() |
||||
replace("Text", "", function(text, old, new) |
||||
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) |
||||
end) |
||||
end, |
||||
|
||||
["find-replace:replace-pattern"] = function() |
||||
replace("Pattern", "", function(text, old, new) |
||||
return text:gsub(old, new) |
||||
end) |
||||
end, |
||||
|
||||
["find-replace:replace-symbol"] = function() |
||||
local first = "" |
||||
if doc():has_selection() then |
||||
local text = doc():get_text(doc():get_selection()) |
||||
first = text:match(config.symbol_pattern) or "" |
||||
end |
||||
replace("Symbol", first, function(text, old, new) |
||||
local n = 0 |
||||
local res = text:gsub(config.symbol_pattern, function(sym) |
||||
if old == sym then |
||||
n = n + 1 |
||||
return new |
||||
end |
||||
end) |
||||
return res, n |
||||
end) |
||||
end, |
||||
}) |
@ -0,0 +1,105 @@ |
||||
local core = require "core" |
||||
local style = require "core.style" |
||||
local DocView = require "core.docview" |
||||
local command = require "core.command" |
||||
local common = require "core.common" |
||||
|
||||
|
||||
local t = { |
||||
["root:close"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
node:close_active_view(core.root_view.root_node) |
||||
end, |
||||
|
||||
["root:switch-to-previous-tab"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local idx = node:get_view_idx(core.active_view) |
||||
idx = idx - 1 |
||||
if idx < 1 then idx = #node.views end |
||||
node:set_active_view(node.views[idx]) |
||||
end, |
||||
|
||||
["root:switch-to-next-tab"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local idx = node:get_view_idx(core.active_view) |
||||
idx = idx + 1 |
||||
if idx > #node.views then idx = 1 end |
||||
node:set_active_view(node.views[idx]) |
||||
end, |
||||
|
||||
["root:move-tab-left"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local idx = node:get_view_idx(core.active_view) |
||||
if idx > 1 then |
||||
table.remove(node.views, idx) |
||||
table.insert(node.views, idx - 1, core.active_view) |
||||
end |
||||
end, |
||||
|
||||
["root:move-tab-right"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local idx = node:get_view_idx(core.active_view) |
||||
if idx < #node.views then |
||||
table.remove(node.views, idx) |
||||
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) |
||||
local n = (parent.a == node) and -0.1 or 0.1 |
||||
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) |
||||
end, |
||||
|
||||
["root:grow"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
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, |
||||
} |
||||
|
||||
|
||||
for i = 1, 9 do |
||||
t["root:switch-to-tab-" .. i] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local view = node.views[i] |
||||
if view then |
||||
node:set_active_view(view) |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
for _, dir in ipairs { "left", "right", "up", "down" } do |
||||
t["root:split-" .. dir] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local av = node.active_view |
||||
node:split(dir) |
||||
if av:is(DocView) then |
||||
core.root_view:open_doc(av.doc) |
||||
end |
||||
end |
||||
|
||||
t["root:switch-to-" .. dir] = function() |
||||
local node = core.root_view:get_active_node() |
||||
local x, y |
||||
if dir == "left" or dir == "right" then |
||||
y = node.position.y + node.size.y / 2 |
||||
x = node.position.x + (dir == "left" and -1 or node.size.x + style.divider_size) |
||||
else |
||||
x = node.position.x + node.size.x / 2 |
||||
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 |
||||
core.set_active_view(node.active_view) |
||||
end |
||||
end |
||||
end |
||||
|
||||
command.add(function() |
||||
local node = core.root_view:get_active_node() |
||||
return not node:get_locked_size() |
||||
end, t) |
@ -0,0 +1,256 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local style = require "core.style" |
||||
local Doc = require "core.doc" |
||||
local DocView = require "core.docview" |
||||
local View = require "core.view" |
||||
|
||||
|
||||
local SingleLineDoc = Doc:extend() |
||||
|
||||
function SingleLineDoc:insert(line, col, text) |
||||
SingleLineDoc.super.insert(self, line, col, text:gsub("\n", "")) |
||||
end |
||||
|
||||
|
||||
local CommandView = DocView:extend() |
||||
|
||||
local max_suggestions = 10 |
||||
|
||||
local noop = function() end |
||||
|
||||
local default_state = { |
||||
submit = noop, |
||||
suggest = noop, |
||||
cancel = noop, |
||||
} |
||||
|
||||
|
||||
function CommandView:new() |
||||
CommandView.super.new(self, SingleLineDoc()) |
||||
self.suggestion_idx = 1 |
||||
self.suggestions = {} |
||||
self.suggestions_height = 0 |
||||
self.last_change_id = 0 |
||||
self.gutter_width = 0 |
||||
self.gutter_text_brightness = 0 |
||||
self.selection_offset = 0 |
||||
self.state = default_state |
||||
self.font = "font" |
||||
self.size.y = 0 |
||||
self.label = "" |
||||
end |
||||
|
||||
|
||||
function CommandView:get_name() |
||||
return View.get_name(self) |
||||
end |
||||
|
||||
|
||||
function CommandView:get_line_screen_position() |
||||
local x = CommandView.super.get_line_screen_position(self, 1) |
||||
local _, y = self:get_content_offset() |
||||
local lh = self:get_line_height() |
||||
return x, y + (self.size.y - lh) / 2 |
||||
end |
||||
|
||||
|
||||
function CommandView:get_scrollable_size() |
||||
return 0 |
||||
end |
||||
|
||||
|
||||
function CommandView:scroll_to_make_visible() |
||||
-- no-op function to disable this functionality |
||||
end |
||||
|
||||
|
||||
function CommandView:get_text() |
||||
return self.doc:get_text(1, 1, 1, math.huge) |
||||
end |
||||
|
||||
|
||||
function CommandView:set_text(text, select) |
||||
self.doc:remove(1, 1, math.huge, math.huge) |
||||
self.doc:text_input(text) |
||||
if select then |
||||
self.doc:set_selection(math.huge, math.huge, 1, 1) |
||||
end |
||||
end |
||||
|
||||
|
||||
function CommandView:move_suggestion_idx(dir) |
||||
local n = self.suggestion_idx + dir |
||||
self.suggestion_idx = common.clamp(n, 1, #self.suggestions) |
||||
self:complete() |
||||
self.last_change_id = self.doc:get_change_id() |
||||
end |
||||
|
||||
|
||||
function CommandView:complete() |
||||
if #self.suggestions > 0 then |
||||
self:set_text(self.suggestions[self.suggestion_idx].text) |
||||
end |
||||
end |
||||
|
||||
|
||||
function CommandView:submit() |
||||
local suggestion = self.suggestions[self.suggestion_idx] |
||||
local text = self:get_text() |
||||
local submit = self.state.submit |
||||
self:exit(true) |
||||
submit(text, suggestion) |
||||
end |
||||
|
||||
|
||||
function CommandView:enter(text, submit, suggest, cancel) |
||||
if self.state ~= default_state then |
||||
return |
||||
end |
||||
self.state = { |
||||
submit = submit or noop, |
||||
suggest = suggest or noop, |
||||
cancel = cancel or noop, |
||||
} |
||||
core.set_active_view(self) |
||||
self:update_suggestions() |
||||
self.gutter_text_brightness = 100 |
||||
self.label = text .. ": " |
||||
end |
||||
|
||||
|
||||
function CommandView:exit(submitted, inexplicit) |
||||
if core.active_view == self then |
||||
core.set_active_view(core.last_active_view) |
||||
end |
||||
local cancel = self.state.cancel |
||||
self.state = default_state |
||||
self.doc:reset() |
||||
self.suggestions = {} |
||||
if not submitted then cancel(not inexplicit) end |
||||
end |
||||
|
||||
|
||||
function CommandView:get_gutter_width() |
||||
return self.gutter_width |
||||
end |
||||
|
||||
|
||||
function CommandView:get_suggestion_line_height() |
||||
return self:get_font():get_height() + style.padding.y |
||||
end |
||||
|
||||
|
||||
function CommandView:update_suggestions() |
||||
local t = self.state.suggest(self:get_text()) or {} |
||||
local res = {} |
||||
for i, item in ipairs(t) do |
||||
if i == max_suggestions then |
||||
break |
||||
end |
||||
if type(item) == "string" then |
||||
item = { text = item } |
||||
end |
||||
res[i] = item |
||||
end |
||||
self.suggestions = res |
||||
self.suggestion_idx = 1 |
||||
end |
||||
|
||||
|
||||
function CommandView:update() |
||||
CommandView.super.update(self) |
||||
|
||||
if core.active_view ~= self and self.state ~= default_state then |
||||
self:exit(false, true) |
||||
end |
||||
|
||||
-- update suggestions if text has changed |
||||
if self.last_change_id ~= self.doc:get_change_id() then |
||||
self:update_suggestions() |
||||
self.last_change_id = self.doc:get_change_id() |
||||
end |
||||
|
||||
-- update gutter text color brightness |
||||
self:move_towards("gutter_text_brightness", 0, 0.1) |
||||
|
||||
-- update gutter width |
||||
local dest = self:get_font():get_width(self.label) + style.padding.x |
||||
if self.size.y <= 0 then |
||||
self.gutter_width = dest |
||||
else |
||||
self:move_towards("gutter_width", dest) |
||||
end |
||||
|
||||
-- update suggestions box height |
||||
local lh = self:get_suggestion_line_height() |
||||
local dest = #self.suggestions * lh |
||||
self:move_towards("suggestions_height", dest) |
||||
|
||||
-- update suggestion cursor offset |
||||
local dest = self.suggestion_idx * self:get_suggestion_line_height() |
||||
self:move_towards("selection_offset", dest) |
||||
|
||||
-- update size based on whether this is the active_view |
||||
local dest = 0 |
||||
if self == core.active_view then |
||||
dest = style.font:get_height() + style.padding.y * 2 |
||||
end |
||||
self:move_towards(self.size, "y", dest) |
||||
end |
||||
|
||||
|
||||
function CommandView:draw_line_highlight() |
||||
-- no-op function to disable this functionality |
||||
end |
||||
|
||||
|
||||
function CommandView:draw_line_gutter(idx, x, y) |
||||
local yoffset = self:get_line_text_y_offset() |
||||
local pos = self.position |
||||
local color = common.lerp(style.text, style.accent, self.gutter_text_brightness / 100) |
||||
core.push_clip_rect(pos.x, pos.y, self:get_gutter_width(), self.size.y) |
||||
x = x + style.padding.x |
||||
renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color) |
||||
core.pop_clip_rect() |
||||
end |
||||
|
||||
|
||||
local function draw_suggestions_box(self) |
||||
local lh = self:get_suggestion_line_height() |
||||
local dh = style.divider_size |
||||
local x, _ = self:get_line_screen_position() |
||||
local h = math.ceil(self.suggestions_height) |
||||
local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h |
||||
|
||||
-- draw suggestions background |
||||
if #self.suggestions > 0 then |
||||
renderer.draw_rect(rx, ry, rw, rh, style.background3) |
||||
renderer.draw_rect(rx, ry - dh, rw, dh, style.divider) |
||||
local y = self.position.y - self.selection_offset - dh |
||||
renderer.draw_rect(rx, y, rw, lh, style.line_highlight) |
||||
end |
||||
|
||||
-- draw suggestion text |
||||
core.push_clip_rect(rx, ry, rw, rh) |
||||
for i, item in ipairs(self.suggestions) do |
||||
local color = (i == self.suggestion_idx) and style.accent or style.text |
||||
local y = self.position.y - i * lh - dh |
||||
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh) |
||||
|
||||
if item.info then |
||||
local w = self.size.x - x - style.padding.x |
||||
common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh) |
||||
end |
||||
end |
||||
core.pop_clip_rect() |
||||
end |
||||
|
||||
|
||||
function CommandView:draw() |
||||
CommandView.super.draw(self) |
||||
core.root_view:defer_draw(draw_suggestions_box, self) |
||||
end |
||||
|
||||
|
||||
return CommandView |
@ -0,0 +1,140 @@ |
||||
local common = {} |
||||
|
||||
|
||||
function common.is_utf8_cont(char) |
||||
local byte = char:byte() |
||||
return byte >= 0x80 and byte < 0xc0 |
||||
end |
||||
|
||||
|
||||
function common.utf8_chars(text) |
||||
return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*") |
||||
end |
||||
|
||||
|
||||
function common.clamp(n, lo, hi) |
||||
return math.max(math.min(n, hi), lo) |
||||
end |
||||
|
||||
|
||||
function common.round(n) |
||||
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5) |
||||
end |
||||
|
||||
|
||||
function common.lerp(a, b, t) |
||||
if type(a) ~= "table" then |
||||
return a + (b - a) * t |
||||
end |
||||
local res = {} |
||||
for k, v in pairs(b) do |
||||
res[k] = common.lerp(a[k], v, t) |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
function common.color(str) |
||||
local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") |
||||
if r then |
||||
r = tonumber(r, 16) |
||||
g = tonumber(g, 16) |
||||
b = tonumber(b, 16) |
||||
a = 1 |
||||
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then |
||||
local f = str:gmatch("[%d.]+") |
||||
r = (f() or 0) |
||||
g = (f() or 0) |
||||
b = (f() or 0) |
||||
a = f() or 1 |
||||
else |
||||
error(string.format("bad color string '%s'", str)) |
||||
end |
||||
return r, g, b, a * 0xff |
||||
end |
||||
|
||||
|
||||
local function compare_score(a, b) |
||||
return a.score > b.score |
||||
end |
||||
|
||||
local function fuzzy_match_items(items, needle) |
||||
local res = {} |
||||
for _, item in ipairs(items) do |
||||
local score = system.fuzzy_match(tostring(item), needle) |
||||
if score then |
||||
table.insert(res, { text = item, score = score }) |
||||
end |
||||
end |
||||
table.sort(res, compare_score) |
||||
for i, item in ipairs(res) do |
||||
res[i] = item.text |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
function common.fuzzy_match(haystack, needle) |
||||
if type(haystack) == "table" then |
||||
return fuzzy_match_items(haystack, needle) |
||||
end |
||||
return system.fuzzy_match(haystack, needle) |
||||
end |
||||
|
||||
|
||||
function common.path_suggest(text) |
||||
local path, name = text:match("^(.-)([^/\\]*)$") |
||||
local files = system.list_dir(path == "" and "." or path) or {} |
||||
local res = {} |
||||
for _, file in ipairs(files) do |
||||
file = path .. file |
||||
local info = system.get_file_info(file) |
||||
if info then |
||||
if info.type == "dir" then |
||||
file = file .. PATHSEP |
||||
end |
||||
if file:lower():find(text:lower(), nil, true) == 1 then |
||||
table.insert(res, file) |
||||
end |
||||
end |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
function common.match_pattern(text, pattern, ...) |
||||
if type(pattern) == "string" then |
||||
return text:find(pattern, ...) |
||||
end |
||||
for _, p in ipairs(pattern) do |
||||
local s, e = common.match_pattern(text, p, ...) |
||||
if s then return s, e end |
||||
end |
||||
return false |
||||
end |
||||
|
||||
|
||||
function common.draw_text(font, color, text, align, x,y,w,h) |
||||
local tw, th = font:get_width(text), font:get_height(text) |
||||
if align == "center" then |
||||
x = x + (w - tw) / 2 |
||||
elseif align == "right" then |
||||
x = x + (w - tw) |
||||
end |
||||
y = common.round(y + (h - th) / 2) |
||||
return renderer.draw_text(font, text, x, y, color), y + th |
||||
end |
||||
|
||||
|
||||
function common.bench(name, fn, ...) |
||||
local start = system.get_time() |
||||
local res = fn(...) |
||||
local t = system.get_time() - start |
||||
local ms = t * 1000 |
||||
local per = (t / (1 / 60)) * 100 |
||||
print(string.format("*** %-16s : %8.3fms %6.2f%%", name, ms, per)) |
||||
return res |
||||
end |
||||
|
||||
|
||||
return common |
@ -0,0 +1,20 @@ |
||||
local config = {} |
||||
|
||||
config.project_scan_rate = 5 |
||||
config.fps = 60 |
||||
config.max_log_items = 80 |
||||
config.message_timeout = 3 |
||||
config.mouse_wheel_scroll = 50 * SCALE |
||||
config.file_size_limit = 10 |
||||
config.ignore_files = "^%." |
||||
config.symbol_pattern = "[%a_][%w_]*" |
||||
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" |
||||
config.undo_merge_timeout = 0.3 |
||||
config.max_undos = 10000 |
||||
config.highlight_current_line = true |
||||
config.line_height = 1.2 |
||||
config.indent_size = 8 |
||||
config.tab_type = "hard" |
||||
config.line_limit = 80 |
||||
|
||||
return config |
@ -0,0 +1,80 @@ |
||||
local core = require "core" |
||||
local config = require "core.config" |
||||
local tokenizer = require "core.tokenizer" |
||||
local Object = require "core.object" |
||||
|
||||
|
||||
local Highlighter = Object:extend() |
||||
|
||||
|
||||
function Highlighter:new(doc) |
||||
self.doc = doc |
||||
self:reset() |
||||
|
||||
-- init incremental syntax highlighting |
||||
core.add_thread(function() |
||||
while true do |
||||
if self.first_invalid_line > self.max_wanted_line then |
||||
self.max_wanted_line = 0 |
||||
coroutine.yield(1 / config.fps) |
||||
|
||||
else |
||||
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) |
||||
|
||||
for i = self.first_invalid_line, max do |
||||
local state = (i > 1) and self.lines[i - 1].state |
||||
local line = self.lines[i] |
||||
if not (line and line.init_state == state) then |
||||
self.lines[i] = self:tokenize_line(i, state) |
||||
end |
||||
end |
||||
|
||||
self.first_invalid_line = max + 1 |
||||
core.redraw = true |
||||
coroutine.yield() |
||||
end |
||||
end |
||||
end, self) |
||||
end |
||||
|
||||
|
||||
function Highlighter:reset() |
||||
self.lines = {} |
||||
self.first_invalid_line = 1 |
||||
self.max_wanted_line = 0 |
||||
end |
||||
|
||||
|
||||
function Highlighter:invalidate(idx) |
||||
self.first_invalid_line = math.min(self.first_invalid_line, idx) |
||||
self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) |
||||
end |
||||
|
||||
|
||||
function Highlighter:tokenize_line(idx, state) |
||||
local res = {} |
||||
res.init_state = state |
||||
res.text = self.doc.lines[idx] |
||||
res.tokens, res.state = tokenizer.tokenize(self.doc.syntax, res.text, state) |
||||
return res |
||||
end |
||||
|
||||
|
||||
function Highlighter:get_line(idx) |
||||
local line = self.lines[idx] |
||||
if not line or line.text ~= self.doc.lines[idx] then |
||||
local prev = self.lines[idx - 1] |
||||
line = self:tokenize_line(idx, prev and prev.state) |
||||
self.lines[idx] = line |
||||
end |
||||
self.max_wanted_line = math.max(self.max_wanted_line, idx) |
||||
return line |
||||
end |
||||
|
||||
|
||||
function Highlighter:each_token(idx) |
||||
return tokenizer.each_token(self:get_line(idx).tokens) |
||||
end |
||||
|
||||
|
||||
return Highlighter |
@ -0,0 +1,393 @@ |
||||
local Object = require "core.object" |
||||
local Highlighter = require "core.doc.highlighter" |
||||
local syntax = require "core.syntax" |
||||
local config = require "core.config" |
||||
local common = require "core.common" |
||||
|
||||
|
||||
local Doc = Object:extend() |
||||
|
||||
|
||||
local function split_lines(text) |
||||
local res = {} |
||||
for line in (text .. "\n"):gmatch("(.-)\n") do |
||||
table.insert(res, line) |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function splice(t, at, remove, insert) |
||||
insert = insert or {} |
||||
local offset = #insert - remove |
||||
local old_len = #t |
||||
if offset < 0 then |
||||
for i = at - offset, old_len - offset do |
||||
t[i + offset] = t[i] |
||||
end |
||||
elseif offset > 0 then |
||||
for i = old_len, at, -1 do |
||||
t[i + offset] = t[i] |
||||
end |
||||
end |
||||
for i, item in ipairs(insert) do |
||||
t[at + i - 1] = item |
||||
end |
||||
end |
||||
|
||||
|
||||
function Doc:new(filename) |
||||
self:reset() |
||||
if filename then |
||||
self:load(filename) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Doc:reset() |
||||
self.lines = { "\n" } |
||||
self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } } |
||||
self.undo_stack = { idx = 1 } |
||||
self.redo_stack = { idx = 1 } |
||||
self.clean_change_id = 1 |
||||
self.highlighter = Highlighter(self) |
||||
self:reset_syntax() |
||||
end |
||||
|
||||
|
||||
function Doc:reset_syntax() |
||||
local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) |
||||
local syn = syntax.get(self.filename or "", header) |
||||
if self.syntax ~= syn then |
||||
self.syntax = syn |
||||
self.highlighter:reset() |
||||
end |
||||
end |
||||
|
||||
|
||||
function Doc:load(filename) |
||||
local fp = assert( io.open(filename, "rb") ) |
||||
self:reset() |
||||
self.filename = filename |
||||
self.lines = {} |
||||
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") |
||||
end |
||||
if #self.lines == 0 then |
||||
table.insert(self.lines, "\n") |
||||
end |
||||
fp:close() |
||||
self:reset_syntax() |
||||
end |
||||
|
||||
|
||||
function Doc:save(filename) |
||||
filename = filename or assert(self.filename, "no filename set to default to") |
||||
local fp = assert( io.open(filename, "wb") ) |
||||
for _, line in ipairs(self.lines) do |
||||
if self.crlf then line = line:gsub("\n", "\r\n") end |
||||
fp:write(line) |
||||
end |
||||
fp:close() |
||||
self.filename = filename or self.filename |
||||
self:reset_syntax() |
||||
self:clean() |
||||
end |
||||
|
||||
|
||||
function Doc:get_name() |
||||
return self.filename or "unsaved" |
||||
end |
||||
|
||||
|
||||
function Doc:is_dirty() |
||||
return self.clean_change_id ~= self:get_change_id() |
||||
end |
||||
|
||||
|
||||
function Doc:clean() |
||||
self.clean_change_id = self:get_change_id() |
||||
end |
||||
|
||||
|
||||
function Doc:get_change_id() |
||||
return self.undo_stack.idx |
||||
end |
||||
|
||||
|
||||
function Doc:set_selection(line1, col1, line2, col2, swap) |
||||
assert(not line2 == not col2, "expected 2 or 4 arguments") |
||||
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end |
||||
line1, col1 = self:sanitize_position(line1, col1) |
||||
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) |
||||
self.selection.a.line, self.selection.a.col = line1, col1 |
||||
self.selection.b.line, self.selection.b.col = line2, col2 |
||||
end |
||||
|
||||
|
||||
local function sort_positions(line1, col1, line2, col2) |
||||
if line1 > line2 |
||||
or line1 == line2 and col1 > col2 then |
||||
return line2, col2, line1, col1, true |
||||
end |
||||
return line1, col1, line2, col2, false |
||||
end |
||||
|
||||
|
||||
function Doc:get_selection(sort) |
||||
local a, b = self.selection.a, self.selection.b |
||||
if sort then |
||||
return sort_positions(a.line, a.col, b.line, b.col) |
||||
end |
||||
return a.line, a.col, b.line, b.col |
||||
end |
||||
|
||||
|
||||
function Doc:has_selection() |
||||
local a, b = self.selection.a, self.selection.b |
||||
return not (a.line == b.line and a.col == b.col) |
||||
end |
||||
|
||||
|
||||
function Doc:sanitize_selection() |
||||
self:set_selection(self:get_selection()) |
||||
end |
||||
|
||||
|
||||
function Doc:sanitize_position(line, col) |
||||
line = common.clamp(line, 1, #self.lines) |
||||
col = common.clamp(col, 1, #self.lines[line]) |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
local function position_offset_func(self, line, col, fn, ...) |
||||
line, col = self:sanitize_position(line, col) |
||||
return fn(self, line, col, ...) |
||||
end |
||||
|
||||
|
||||
local function position_offset_byte(self, line, col, offset) |
||||
line, col = self:sanitize_position(line, col) |
||||
col = col + offset |
||||
while line > 1 and col < 1 do |
||||
line = line - 1 |
||||
col = col + #self.lines[line] |
||||
end |
||||
while line < #self.lines and col > #self.lines[line] do |
||||
col = col - #self.lines[line] |
||||
line = line + 1 |
||||
end |
||||
return self:sanitize_position(line, col) |
||||
end |
||||
|
||||
|
||||
local function position_offset_linecol(self, line, col, lineoffset, coloffset) |
||||
return self:sanitize_position(line + lineoffset, col + coloffset) |
||||
end |
||||
|
||||
|
||||
function Doc:position_offset(line, col, ...) |
||||
if type(...) ~= "number" then |
||||
return position_offset_func(self, line, col, ...) |
||||
elseif select("#", ...) == 1 then |
||||
return position_offset_byte(self, line, col, ...) |
||||
elseif select("#", ...) == 2 then |
||||
return position_offset_linecol(self, line, col, ...) |
||||
else |
||||
error("bad number of arguments") |
||||
end |
||||
end |
||||
|
||||
|
||||
function Doc:get_text(line1, col1, line2, col2) |
||||
line1, col1 = self:sanitize_position(line1, col1) |
||||
line2, col2 = self:sanitize_position(line2, col2) |
||||
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) |
||||
if line1 == line2 then |
||||
return self.lines[line1]:sub(col1, col2 - 1) |
||||
end |
||||
local lines = { self.lines[line1]:sub(col1) } |
||||
for i = line1 + 1, line2 - 1 do |
||||
table.insert(lines, self.lines[i]) |
||||
end |
||||
table.insert(lines, self.lines[line2]:sub(1, col2 - 1)) |
||||
return table.concat(lines) |
||||
end |
||||
|
||||
|
||||
function Doc:get_char(line, col) |
||||
line, col = self:sanitize_position(line, col) |
||||
return self.lines[line]:sub(col, col) |
||||
end |
||||
|
||||
|
||||
local function push_undo(undo_stack, time, type, ...) |
||||
undo_stack[undo_stack.idx] = { type = type, time = time, ... } |
||||
undo_stack[undo_stack.idx - config.max_undos] = nil |
||||
undo_stack.idx = undo_stack.idx + 1 |
||||
end |
||||
|
||||
|
||||
local function pop_undo(self, undo_stack, redo_stack) |
||||
-- pop command |
||||
local cmd = undo_stack[undo_stack.idx - 1] |
||||
if not cmd then return end |
||||
undo_stack.idx = undo_stack.idx - 1 |
||||
|
||||
-- handle command |
||||
if cmd.type == "insert" then |
||||
local line, col, text = table.unpack(cmd) |
||||
self:raw_insert(line, col, text, redo_stack, cmd.time) |
||||
|
||||
elseif cmd.type == "remove" then |
||||
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.selection.a.line, self.selection.a.col = cmd[1], cmd[2] |
||||
self.selection.b.line, self.selection.b.col = cmd[3], cmd[4] |
||||
end |
||||
|
||||
-- if next undo command is within the merge timeout then treat as a single |
||||
-- command and continue to execute it |
||||
local next = undo_stack[undo_stack.idx - 1] |
||||
if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then |
||||
return pop_undo(self, undo_stack, redo_stack) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Doc:raw_insert(line, col, text, undo_stack, time) |
||||
-- split text into lines and merge with line at insertion point |
||||
local lines = split_lines(text) |
||||
local before = self.lines[line]:sub(1, col - 1) |
||||
local after = self.lines[line]:sub(col) |
||||
for i = 1, #lines - 1 do |
||||
lines[i] = lines[i] .. "\n" |
||||
end |
||||
lines[1] = before .. lines[1] |
||||
lines[#lines] = lines[#lines] .. after |
||||
|
||||
-- splice lines into line array |
||||
splice(self.lines, line, 1, lines) |
||||
|
||||
-- push undo |
||||
local line2, col2 = self:position_offset(line, col, #text) |
||||
push_undo(undo_stack, time, "selection", self:get_selection()) |
||||
push_undo(undo_stack, time, "remove", line, col, line2, col2) |
||||
|
||||
-- update highlighter and assure selection is in bounds |
||||
self.highlighter:invalidate(line) |
||||
self:sanitize_selection() |
||||
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", self:get_selection()) |
||||
push_undo(undo_stack, time, "insert", line1, col1, text) |
||||
|
||||
-- get line content before/after removed text |
||||
local before = self.lines[line1]:sub(1, col1 - 1) |
||||
local after = self.lines[line2]:sub(col2) |
||||
|
||||
-- splice line into line array |
||||
splice(self.lines, line1, line2 - line1 + 1, { before .. after }) |
||||
|
||||
-- update highlighter and assure selection is in bounds |
||||
self.highlighter:invalidate(line1) |
||||
self:sanitize_selection() |
||||
end |
||||
|
||||
|
||||
function Doc:insert(line, col, text) |
||||
self.redo_stack = { idx = 1 } |
||||
line, col = self:sanitize_position(line, col) |
||||
self:raw_insert(line, col, text, self.undo_stack, system.get_time()) |
||||
end |
||||
|
||||
|
||||
function Doc:remove(line1, col1, line2, col2) |
||||
self.redo_stack = { idx = 1 } |
||||
line1, col1 = self:sanitize_position(line1, col1) |
||||
line2, col2 = self:sanitize_position(line2, col2) |
||||
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) |
||||
self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) |
||||
end |
||||
|
||||
|
||||
function Doc:undo() |
||||
pop_undo(self, self.undo_stack, self.redo_stack) |
||||
end |
||||
|
||||
|
||||
function Doc:redo() |
||||
pop_undo(self, self.redo_stack, self.undo_stack) |
||||
end |
||||
|
||||
|
||||
function Doc:text_input(text) |
||||
if self:has_selection() then |
||||
self:delete_to() |
||||
end |
||||
local line, col = self:get_selection() |
||||
self:insert(line, col, text) |
||||
self:move_to(#text) |
||||
end |
||||
|
||||
|
||||
function Doc:replace(fn) |
||||
local line1, col1, line2, col2, swap |
||||
local had_selection = self:has_selection() |
||||
if had_selection then |
||||
line1, col1, line2, col2, swap = self:get_selection(true) |
||||
else |
||||
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] |
||||
end |
||||
local old_text = self:get_text(line1, col1, line2, col2) |
||||
local new_text, n = fn(old_text) |
||||
if old_text ~= new_text then |
||||
self:insert(line2, col2, new_text) |
||||
self:remove(line1, col1, line2, col2) |
||||
if had_selection then |
||||
line2, col2 = self:position_offset(line1, col1, #new_text) |
||||
self:set_selection(line1, col1, line2, col2, swap) |
||||
end |
||||
end |
||||
return n |
||||
end |
||||
|
||||
|
||||
function Doc:delete_to(...) |
||||
local line, col = self:get_selection(true) |
||||
if self:has_selection() then |
||||
self:remove(self:get_selection()) |
||||
else |
||||
local line2, col2 = self:position_offset(line, col, ...) |
||||
self:remove(line, col, line2, col2) |
||||
line, col = sort_positions(line, col, line2, col2) |
||||
end |
||||
self:set_selection(line, col) |
||||
end |
||||
|
||||
|
||||
function Doc:move_to(...) |
||||
local line, col = self:get_selection() |
||||
self:set_selection(self:position_offset(line, col, ...)) |
||||
end |
||||
|
||||
|
||||
function Doc:select_to(...) |
||||
local line, col, line2, col2 = self:get_selection() |
||||
line, col = self:position_offset(line, col, ...) |
||||
self:set_selection(line, col, line2, col2) |
||||
end |
||||
|
||||
|
||||
return Doc |
@ -0,0 +1,52 @@ |
||||
local search = {} |
||||
|
||||
local default_opt = {} |
||||
|
||||
|
||||
local function pattern_lower(str) |
||||
if str:sub(1, 1) == "%" then |
||||
return str |
||||
end |
||||
return str:lower() |
||||
end |
||||
|
||||
|
||||
local function init_args(doc, line, col, text, opt) |
||||
opt = opt or default_opt |
||||
line, col = doc:sanitize_position(line, col) |
||||
|
||||
if opt.no_case then |
||||
if opt.pattern then |
||||
text = text:gsub("%%?.", pattern_lower) |
||||
else |
||||
text = text:lower() |
||||
end |
||||
end |
||||
|
||||
return doc, line, col, text, opt |
||||
end |
||||
|
||||
|
||||
function search.find(doc, line, col, text, opt) |
||||
doc, line, col, text, opt = init_args(doc, line, col, text, opt) |
||||
|
||||
for line = line, #doc.lines do |
||||
local line_text = doc.lines[line] |
||||
if opt.no_case then |
||||
line_text = line_text:lower() |
||||
end |
||||
local s, e = line_text:find(text, col, not opt.pattern) |
||||
if s then |
||||
return line, s, line, e + 1 |
||||
end |
||||
col = 1 |
||||
end |
||||
|
||||
if opt.wrap then |
||||
opt = { no_case = opt.no_case, pattern = opt.pattern } |
||||
return search.find(doc, 1, 1, text, opt) |
||||
end |
||||
end |
||||
|
||||
|
||||
return search |
@ -0,0 +1,136 @@ |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
|
||||
-- functions for translating a Doc position to another position these functions |
||||
-- can be passed to Doc:move_to|select_to|delete_to() |
||||
|
||||
local translate = {} |
||||
|
||||
|
||||
local function is_non_word(char) |
||||
return config.non_word_chars:find(char, nil, true) |
||||
end |
||||
|
||||
|
||||
function translate.previous_char(doc, line, col) |
||||
repeat |
||||
line, col = doc:position_offset(line, col, -1) |
||||
until not common.is_utf8_cont(doc:get_char(line, col)) |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
function translate.next_char(doc, line, col) |
||||
repeat |
||||
line, col = doc:position_offset(line, col, 1) |
||||
until not common.is_utf8_cont(doc:get_char(line, col)) |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
function translate.previous_word_start(doc, line, col) |
||||
local prev |
||||
while line > 1 or col > 1 do |
||||
local l, c = doc:position_offset(line, col, -1) |
||||
local char = doc:get_char(l, c) |
||||
if prev and prev ~= char or not is_non_word(char) then |
||||
break |
||||
end |
||||
prev, line, col = char, l, c |
||||
end |
||||
return translate.start_of_word(doc, line, col) |
||||
end |
||||
|
||||
|
||||
function translate.next_word_end(doc, line, col) |
||||
local prev |
||||
local end_line, end_col = translate.end_of_doc(doc, line, col) |
||||
while line < end_line or col < end_col do |
||||
local char = doc:get_char(line, col) |
||||
if prev and prev ~= char or not is_non_word(char) then |
||||
break |
||||
end |
||||
line, col = doc:position_offset(line, col, 1) |
||||
prev = char |
||||
end |
||||
return translate.end_of_word(doc, line, col) |
||||
end |
||||
|
||||
|
||||
function translate.start_of_word(doc, line, col) |
||||
while true do |
||||
local line2, col2 = doc:position_offset(line, col, -1) |
||||
local char = doc:get_char(line2, col2) |
||||
if is_non_word(char) |
||||
or line == line2 and col == col2 then |
||||
break |
||||
end |
||||
line, col = line2, col2 |
||||
end |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
function translate.end_of_word(doc, line, col) |
||||
while true do |
||||
local line2, col2 = doc:position_offset(line, col, 1) |
||||
local char = doc:get_char(line, col) |
||||
if is_non_word(char) |
||||
or line == line2 and col == col2 then |
||||
break |
||||
end |
||||
line, col = line2, col2 |
||||
end |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
function translate.previous_block_start(doc, line, col) |
||||
while true do |
||||
line = line - 1 |
||||
if line <= 1 then |
||||
return 1, 1 |
||||
end |
||||
if doc.lines[line-1]:find("^%s*$") |
||||
and not doc.lines[line]:find("^%s*$") then |
||||
return line, (doc.lines[line]:find("%S")) |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function translate.next_block_end(doc, line, col) |
||||
while true do |
||||
if line >= #doc.lines then |
||||
return #doc.lines, 1 |
||||
end |
||||
if doc.lines[line+1]:find("^%s*$") |
||||
and not doc.lines[line]:find("^%s*$") then |
||||
return line+1, #doc.lines[line+1] |
||||
end |
||||
line = line + 1 |
||||
end |
||||
end |
||||
|
||||
|
||||
function translate.start_of_line(doc, line, col) |
||||
return line, 1 |
||||
end |
||||
|
||||
|
||||
function translate.end_of_line(doc, line, col) |
||||
return line, math.huge |
||||
end |
||||
|
||||
|
||||
function translate.start_of_doc(doc, line, col) |
||||
return 1, 1 |
||||
end |
||||
|
||||
|
||||
function translate.end_of_doc(doc, line, col) |
||||
return #doc.lines, #doc.lines[#doc.lines] |
||||
end |
||||
|
||||
|
||||
return translate |
@ -0,0 +1,383 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
local keymap = require "core.keymap" |
||||
local translate = require "core.doc.translate" |
||||
local View = require "core.view" |
||||
|
||||
|
||||
local DocView = View:extend() |
||||
|
||||
|
||||
local function move_to_line_offset(dv, line, col, offset) |
||||
local xo = dv.last_x_offset |
||||
if xo.line ~= line or xo.col ~= col then |
||||
xo.offset = dv:get_col_x_offset(line, col) |
||||
end |
||||
xo.line = line + offset |
||||
xo.col = dv:get_x_offset_col(line + offset, xo.offset) |
||||
return xo.line, xo.col |
||||
end |
||||
|
||||
|
||||
DocView.translate = { |
||||
["previous_page"] = function(doc, line, col, dv) |
||||
local min, max = dv:get_visible_line_range() |
||||
return line - (max - min), 1 |
||||
end, |
||||
|
||||
["next_page"] = function(doc, line, col, dv) |
||||
local min, max = dv:get_visible_line_range() |
||||
return line + (max - min), 1 |
||||
end, |
||||
|
||||
["previous_line"] = function(doc, line, col, dv) |
||||
if line == 1 then |
||||
return 1, 1 |
||||
end |
||||
return move_to_line_offset(dv, line, col, -1) |
||||
end, |
||||
|
||||
["next_line"] = function(doc, line, col, dv) |
||||
if line == #doc.lines then |
||||
return #doc.lines, math.huge |
||||
end |
||||
return move_to_line_offset(dv, line, col, 1) |
||||
end, |
||||
} |
||||
|
||||
local blink_period = 0.8 |
||||
|
||||
|
||||
function DocView:new(doc) |
||||
DocView.super.new(self) |
||||
self.cursor = "ibeam" |
||||
self.scrollable = true |
||||
self.doc = assert(doc) |
||||
self.font = "code_font" |
||||
self.last_x_offset = {} |
||||
self.blink_timer = 0 |
||||
end |
||||
|
||||
|
||||
function DocView:try_close(do_close) |
||||
if self.doc:is_dirty() |
||||
and #core.get_views_referencing_doc(self.doc) == 1 then |
||||
core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item) |
||||
if item.text:match("^[cC]") then |
||||
do_close() |
||||
elseif item.text:match("^[sS]") then |
||||
self.doc:save() |
||||
do_close() |
||||
end |
||||
end, function(text) |
||||
local items = {} |
||||
if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end |
||||
if not text:find("^[^sS]") then table.insert(items, "Save And Close") end |
||||
return items |
||||
end) |
||||
else |
||||
do_close() |
||||
end |
||||
end |
||||
|
||||
|
||||
function DocView:get_name() |
||||
local post = self.doc:is_dirty() and "*" or "" |
||||
local name = self.doc:get_name() |
||||
return name:match("[^/%\\]*$") .. post |
||||
end |
||||
|
||||
|
||||
function DocView:get_scrollable_size() |
||||
return self:get_line_height() * (#self.doc.lines - 1) + self.size.y |
||||
end |
||||
|
||||
|
||||
function DocView:get_font() |
||||
return style[self.font] |
||||
end |
||||
|
||||
|
||||
function DocView:get_line_height() |
||||
return math.floor(self:get_font():get_height() * config.line_height) |
||||
end |
||||
|
||||
|
||||
function DocView:get_gutter_width() |
||||
return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2 |
||||
end |
||||
|
||||
|
||||
function DocView:get_line_screen_position(idx) |
||||
local x, y = self:get_content_offset() |
||||
local lh = self:get_line_height() |
||||
local gw = self:get_gutter_width() |
||||
return x + gw, y + (idx-1) * lh + style.padding.y |
||||
end |
||||
|
||||
|
||||
function DocView:get_line_text_y_offset() |
||||
local lh = self:get_line_height() |
||||
local th = self:get_font():get_height() |
||||
return (lh - th) / 2 |
||||
end |
||||
|
||||
|
||||
function DocView:get_visible_line_range() |
||||
local x, y, x2, y2 = self:get_content_bounds() |
||||
local lh = self:get_line_height() |
||||
local minline = math.max(1, math.floor(y / lh)) |
||||
local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1) |
||||
return minline, maxline |
||||
end |
||||
|
||||
|
||||
function DocView:get_col_x_offset(line, col) |
||||
local text = self.doc.lines[line] |
||||
if not text then return 0 end |
||||
return self:get_font():get_width(text:sub(1, col - 1)) |
||||
end |
||||
|
||||
|
||||
function DocView:get_x_offset_col(line, x) |
||||
local text = self.doc.lines[line] |
||||
|
||||
local xoffset, last_i, i = 0, 1, 1 |
||||
for char in common.utf8_chars(text) do |
||||
local w = self:get_font():get_width(char) |
||||
if xoffset >= x then |
||||
return (xoffset - x > w / 2) and last_i or i |
||||
end |
||||
xoffset = xoffset + w |
||||
last_i = i |
||||
i = i + #char |
||||
end |
||||
|
||||
return #text |
||||
end |
||||
|
||||
|
||||
function DocView:resolve_screen_position(x, y) |
||||
local ox, oy = self:get_line_screen_position(1) |
||||
local line = math.floor((y - oy) / self:get_line_height()) + 1 |
||||
line = common.clamp(line, 1, #self.doc.lines) |
||||
local col = self:get_x_offset_col(line, x - ox) |
||||
return line, col |
||||
end |
||||
|
||||
|
||||
function DocView:scroll_to_line(line, ignore_if_visible, instant) |
||||
local min, max = self:get_visible_line_range() |
||||
if not (ignore_if_visible and line > min and line < max) then |
||||
local lh = self:get_line_height() |
||||
self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2) |
||||
if instant then |
||||
self.scroll.y = self.scroll.to.y |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function DocView:scroll_to_make_visible(line, col) |
||||
local min = self:get_line_height() * (line - 1) |
||||
local max = self:get_line_height() * (line + 2) - self.size.y |
||||
self.scroll.to.y = math.min(self.scroll.to.y, min) |
||||
self.scroll.to.y = math.max(self.scroll.to.y, max) |
||||
local gw = self:get_gutter_width() |
||||
local xoffset = self:get_col_x_offset(line, col) |
||||
local max = xoffset - self.size.x + gw + self.size.x / 5 |
||||
self.scroll.to.x = math.max(0, max) |
||||
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 == 2 then |
||||
line1, col1 = translate.start_of_word(doc, line1, col1) |
||||
line2, col2 = translate.end_of_word(doc, line2, col2) |
||||
elseif clicks == 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 == 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) |
||||
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col)) |
||||
self.mouse_selecting = { line, col, clicks = clicks } |
||||
end |
||||
self.blink_timer = 0 |
||||
end |
||||
|
||||
|
||||
function DocView:on_mouse_moved(x, y, ...) |
||||
DocView.super.on_mouse_moved(self, x, y, ...) |
||||
|
||||
if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then |
||||
self.cursor = "arrow" |
||||
else |
||||
self.cursor = "ibeam" |
||||
end |
||||
|
||||
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 |
||||
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) |
||||
end |
||||
end |
||||
|
||||
|
||||
function DocView:on_mouse_released(button) |
||||
DocView.super.on_mouse_released(self, button) |
||||
self.mouse_selecting = nil |
||||
end |
||||
|
||||
|
||||
function DocView:on_text_input(text) |
||||
self.doc:text_input(text) |
||||
end |
||||
|
||||
|
||||
function DocView:update() |
||||
-- scroll to make caret visible and reset blink timer if it moved |
||||
local line, col = self.doc:get_selection() |
||||
if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then |
||||
if core.active_view == self then |
||||
self:scroll_to_make_visible(line, col) |
||||
end |
||||
self.blink_timer = 0 |
||||
self.last_line, self.last_col = line, col |
||||
end |
||||
|
||||
-- update blink timer |
||||
if self == core.active_view and not self.mouse_selecting then |
||||
local n = blink_period / 2 |
||||
local prev = self.blink_timer |
||||
self.blink_timer = (self.blink_timer + 1 / config.fps) % blink_period |
||||
if (self.blink_timer > n) ~= (prev > n) then |
||||
core.redraw = true |
||||
end |
||||
end |
||||
|
||||
DocView.super.update(self) |
||||
end |
||||
|
||||
|
||||
function DocView:draw_line_highlight(x, y) |
||||
local lh = self:get_line_height() |
||||
renderer.draw_rect(x, y, self.size.x, lh, style.line_highlight) |
||||
end |
||||
|
||||
|
||||
function DocView:draw_line_text(idx, x, y) |
||||
local tx, ty = x, y + self:get_line_text_y_offset() |
||||
local font = self:get_font() |
||||
for _, type, text in self.doc.highlighter:each_token(idx) do |
||||
local color = style.syntax[type] |
||||
tx = renderer.draw_text(font, text, tx, ty, color) |
||||
end |
||||
end |
||||
|
||||
|
||||
function DocView:draw_line_body(idx, x, y) |
||||
local line, col = self.doc:get_selection() |
||||
|
||||
-- draw selection if it overlaps this line |
||||
local line1, col1, line2, col2 = self.doc:get_selection(true) |
||||
if idx >= line1 and idx <= line2 then |
||||
local text = self.doc.lines[idx] |
||||
if line1 ~= idx then col1 = 1 end |
||||
if line2 ~= idx then col2 = #text + 1 end |
||||
local x1 = x + self:get_col_x_offset(idx, col1) |
||||
local x2 = x + self:get_col_x_offset(idx, col2) |
||||
local lh = self:get_line_height() |
||||
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) |
||||
end |
||||
|
||||
-- draw line highlight if caret is on this line |
||||
if config.highlight_current_line and not self.doc:has_selection() |
||||
and line == idx and core.active_view == self then |
||||
self:draw_line_highlight(x + self.scroll.x, y) |
||||
end |
||||
|
||||
-- draw line's text |
||||
self:draw_line_text(idx, x, y) |
||||
|
||||
-- draw caret if it overlaps this line |
||||
if line == idx and core.active_view == self |
||||
and self.blink_timer < blink_period / 2 |
||||
and system.window_has_focus() then |
||||
local lh = self:get_line_height() |
||||
local x1 = x + self:get_col_x_offset(line, col) |
||||
renderer.draw_rect(x1, y, style.caret_width, lh, style.caret) |
||||
end |
||||
end |
||||
|
||||
|
||||
function DocView:draw_line_gutter(idx, x, y) |
||||
local color = style.line_number |
||||
local line1, _, line2, _ = self.doc:get_selection(true) |
||||
if idx >= line1 and idx <= line2 then |
||||
color = style.line_number2 |
||||
end |
||||
local yoffset = self:get_line_text_y_offset() |
||||
x = x + style.padding.x |
||||
renderer.draw_text(self:get_font(), idx, x, y + yoffset, color) |
||||
end |
||||
|
||||
|
||||
function DocView:draw() |
||||
self:draw_background(style.background) |
||||
|
||||
local font = self:get_font() |
||||
font:set_tab_width(font:get_width(" ") * config.indent_size) |
||||
|
||||
local minline, maxline = self:get_visible_line_range() |
||||
local lh = self:get_line_height() |
||||
|
||||
local _, y = self:get_line_screen_position(minline) |
||||
local x = self.position.x |
||||
for i = minline, maxline do |
||||
self:draw_line_gutter(i, x, y) |
||||
y = y + lh |
||||
end |
||||
|
||||
local x, y = self:get_line_screen_position(minline) |
||||
local gw = self:get_gutter_width() |
||||
local pos = self.position |
||||
core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y) |
||||
for i = minline, maxline do |
||||
self:draw_line_body(i, x, y) |
||||
y = y + lh |
||||
end |
||||
core.pop_clip_rect() |
||||
|
||||
self:draw_scrollbar() |
||||
end |
||||
|
||||
|
||||
return DocView |
@ -0,0 +1,480 @@ |
||||
require "core.strict" |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
local command |
||||
local keymap |
||||
local RootView |
||||
local StatusView |
||||
local CommandView |
||||
local Doc |
||||
|
||||
local core = {} |
||||
|
||||
|
||||
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 |
||||
|
||||
local function compare_file(a, b) |
||||
return a.filename < b.filename |
||||
end |
||||
|
||||
local function get_files(path, t) |
||||
coroutine.yield() |
||||
t = t or {} |
||||
local size_limit = config.file_size_limit * 10e5 |
||||
local all = system.list_dir(path) or {} |
||||
local dirs, files = {}, {} |
||||
|
||||
for _, file in ipairs(all) do |
||||
if not common.match_pattern(file, config.ignore_files) then |
||||
local file = (path ~= "." and path .. PATHSEP or "") .. file |
||||
local info = system.get_file_info(file) |
||||
if info and info.size < size_limit then |
||||
info.filename = file |
||||
table.insert(info.type == "dir" and dirs or files, info) |
||||
end |
||||
end |
||||
end |
||||
|
||||
table.sort(dirs, compare_file) |
||||
for _, f in ipairs(dirs) do |
||||
table.insert(t, f) |
||||
get_files(f.filename, t) |
||||
end |
||||
|
||||
table.sort(files, compare_file) |
||||
for _, f in ipairs(files) do |
||||
table.insert(t, f) |
||||
end |
||||
|
||||
return t |
||||
end |
||||
|
||||
while true do |
||||
-- get project files and replace previous table if the new table is |
||||
-- different |
||||
local t = get_files(".") |
||||
if diff_files(core.project_files, t) then |
||||
core.project_files = t |
||||
core.redraw = true |
||||
end |
||||
|
||||
-- wait for next scan |
||||
coroutine.yield(config.project_scan_rate) |
||||
end |
||||
end |
||||
|
||||
|
||||
function core.init() |
||||
command = require "core.command" |
||||
keymap = require "core.keymap" |
||||
RootView = require "core.rootview" |
||||
StatusView = require "core.statusview" |
||||
CommandView = require "core.commandview" |
||||
Doc = require "core.doc" |
||||
|
||||
local project_dir = EXEDIR |
||||
local files = {} |
||||
for i = 2, #ARGS do |
||||
local info = system.get_file_info(ARGS[i]) or {} |
||||
if info.type == "file" then |
||||
table.insert(files, system.absolute_path(ARGS[i])) |
||||
elseif info.type == "dir" then |
||||
project_dir = ARGS[i] |
||||
end |
||||
end |
||||
|
||||
system.chdir(project_dir) |
||||
|
||||
core.frame_start = 0 |
||||
core.clip_rect_stack = {{ 0,0,0,0 }} |
||||
core.log_items = {} |
||||
core.docs = {} |
||||
core.threads = setmetatable({}, { __mode = "k" }) |
||||
core.project_files = {} |
||||
core.redraw = true |
||||
|
||||
core.root_view = RootView() |
||||
core.command_view = CommandView() |
||||
core.status_view = StatusView() |
||||
|
||||
core.root_view.root_node:split("down", core.command_view, true) |
||||
core.root_view.root_node.b:split("down", core.status_view, true) |
||||
|
||||
core.add_thread(project_scan_thread) |
||||
command.add_defaults() |
||||
local got_plugin_error = not core.load_plugins() |
||||
local got_user_error = not core.try(require, "user") |
||||
local got_project_error = not core.load_project_module() |
||||
|
||||
for _, filename in ipairs(files) do |
||||
core.root_view:open_doc(core.open_doc(filename)) |
||||
end |
||||
|
||||
if got_plugin_error or got_user_error or got_project_error then |
||||
command.perform("core:open-log") |
||||
end |
||||
end |
||||
|
||||
|
||||
local temp_uid = (system.get_time() * 1000) % 0xffffffff |
||||
local temp_file_prefix = string.format(".lite_temp_%08x", temp_uid) |
||||
local temp_file_counter = 0 |
||||
|
||||
local function delete_temp_files() |
||||
for _, filename in ipairs(system.list_dir(EXEDIR)) do |
||||
if filename:find(temp_file_prefix, 1, true) == 1 then |
||||
os.remove(EXEDIR .. PATHSEP .. filename) |
||||
end |
||||
end |
||||
end |
||||
|
||||
function core.temp_filename(ext) |
||||
temp_file_counter = temp_file_counter + 1 |
||||
return EXEDIR .. PATHSEP .. temp_file_prefix |
||||
.. string.format("%06x", temp_file_counter) .. (ext or "") |
||||
end |
||||
|
||||
|
||||
function core.quit(force) |
||||
if force then |
||||
delete_temp_files() |
||||
os.exit() |
||||
end |
||||
local dirty_count = 0 |
||||
local dirty_name |
||||
for _, doc in ipairs(core.docs) do |
||||
if doc:is_dirty() then |
||||
dirty_count = dirty_count + 1 |
||||
dirty_name = doc:get_name() |
||||
end |
||||
end |
||||
if dirty_count > 0 then |
||||
local text |
||||
if dirty_count == 1 then |
||||
text = string.format("\"%s\" has unsaved changes. Quit anyway?", dirty_name) |
||||
else |
||||
text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count) |
||||
end |
||||
local confirm = system.show_confirm_dialog("Unsaved Changes", text) |
||||
if not confirm then return end |
||||
end |
||||
core.quit(true) |
||||
end |
||||
|
||||
|
||||
function core.load_plugins() |
||||
local no_errors = true |
||||
local files = system.list_dir(EXEDIR .. "/data/plugins") |
||||
for _, filename in ipairs(files) do |
||||
local modname = "plugins." .. filename:gsub(".lua$", "") |
||||
local ok = core.try(require, modname) |
||||
if ok then |
||||
core.log_quiet("Loaded plugin %q", modname) |
||||
else |
||||
no_errors = false |
||||
end |
||||
end |
||||
return no_errors |
||||
end |
||||
|
||||
|
||||
function core.load_project_module() |
||||
local filename = ".lite_project.lua" |
||||
if system.get_file_info(filename) then |
||||
return core.try(function() |
||||
local fn, err = loadfile(filename) |
||||
if not fn then error("Error when loading project module:\n\t" .. err) end |
||||
fn() |
||||
core.log_quiet("Loaded project module") |
||||
end) |
||||
end |
||||
return true |
||||
end |
||||
|
||||
|
||||
function core.reload_module(name) |
||||
local old = package.loaded[name] |
||||
package.loaded[name] = nil |
||||
local new = require(name) |
||||
if type(old) == "table" then |
||||
for k, v in pairs(new) do old[k] = v end |
||||
package.loaded[name] = old |
||||
end |
||||
end |
||||
|
||||
|
||||
function core.set_active_view(view) |
||||
assert(view, "Tried to set active view to nil") |
||||
if view ~= core.active_view then |
||||
core.last_active_view = core.active_view |
||||
core.active_view = view |
||||
end |
||||
end |
||||
|
||||
|
||||
function core.add_thread(f, weak_ref) |
||||
local key = weak_ref or #core.threads + 1 |
||||
local fn = function() return core.try(f) end |
||||
core.threads[key] = { cr = coroutine.create(fn), wake = 0 } |
||||
end |
||||
|
||||
|
||||
function core.push_clip_rect(x, y, w, h) |
||||
local x2, y2, w2, h2 = table.unpack(core.clip_rect_stack[#core.clip_rect_stack]) |
||||
local r, b, r2, b2 = x+w, y+h, x2+w2, y2+h2 |
||||
x, y = math.max(x, x2), math.max(y, y2) |
||||
b, r = math.min(b, b2), math.min(r, r2) |
||||
w, h = r-x, b-y |
||||
table.insert(core.clip_rect_stack, { x, y, w, h }) |
||||
renderer.set_clip_rect(x, y, w, h) |
||||
end |
||||
|
||||
|
||||
function core.pop_clip_rect() |
||||
table.remove(core.clip_rect_stack) |
||||
local x, y, w, h = table.unpack(core.clip_rect_stack[#core.clip_rect_stack]) |
||||
renderer.set_clip_rect(x, y, w, h) |
||||
end |
||||
|
||||
|
||||
function core.open_doc(filename) |
||||
if filename then |
||||
-- try to find existing doc for filename |
||||
local abs_filename = system.absolute_path(filename) |
||||
for _, doc in ipairs(core.docs) do |
||||
if doc.filename |
||||
and abs_filename == system.absolute_path(doc.filename) then |
||||
return doc |
||||
end |
||||
end |
||||
end |
||||
-- no existing doc for filename; create new |
||||
local doc = Doc(filename) |
||||
table.insert(core.docs, doc) |
||||
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) |
||||
return doc |
||||
end |
||||
|
||||
|
||||
function core.get_views_referencing_doc(doc) |
||||
local res = {} |
||||
local views = core.root_view.root_node:get_children() |
||||
for _, view in ipairs(views) do |
||||
if view.doc == doc then table.insert(res, view) end |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function log(icon, icon_color, fmt, ...) |
||||
local text = string.format(fmt, ...) |
||||
if icon then |
||||
core.status_view:show_message(icon, icon_color, text) |
||||
end |
||||
|
||||
local info = debug.getinfo(2, "Sl") |
||||
local at = string.format("%s:%d", info.short_src, info.currentline) |
||||
local item = { text = text, time = os.time(), at = at } |
||||
table.insert(core.log_items, item) |
||||
if #core.log_items > config.max_log_items then |
||||
table.remove(core.log_items, 1) |
||||
end |
||||
return item |
||||
end |
||||
|
||||
|
||||
function core.log(...) |
||||
return log("i", style.text, ...) |
||||
end |
||||
|
||||
|
||||
function core.log_quiet(...) |
||||
return log(nil, nil, ...) |
||||
end |
||||
|
||||
|
||||
function core.error(...) |
||||
return log("!", style.accent, ...) |
||||
end |
||||
|
||||
|
||||
function core.try(fn, ...) |
||||
local err |
||||
local ok, res = xpcall(fn, function(msg) |
||||
local item = core.error("%s", msg) |
||||
item.info = debug.traceback(nil, 2):gsub("\t", "") |
||||
err = msg |
||||
end, ...) |
||||
if ok then |
||||
return true, res |
||||
end |
||||
return false, err |
||||
end |
||||
|
||||
|
||||
function core.on_event(type, ...) |
||||
local did_keymap = false |
||||
if type == "textinput" then |
||||
core.root_view:on_text_input(...) |
||||
elseif type == "keypressed" then |
||||
did_keymap = keymap.on_key_pressed(...) |
||||
elseif type == "keyreleased" then |
||||
keymap.on_key_released(...) |
||||
elseif type == "mousemoved" then |
||||
core.root_view:on_mouse_moved(...) |
||||
elseif type == "mousepressed" then |
||||
core.root_view:on_mouse_pressed(...) |
||||
elseif type == "mousereleased" then |
||||
core.root_view:on_mouse_released(...) |
||||
elseif type == "mousewheel" then |
||||
core.root_view:on_mouse_wheel(...) |
||||
elseif type == "filedropped" then |
||||
local filename, mx, my = ... |
||||
local info = system.get_file_info(filename) |
||||
if info and info.type == "dir" then |
||||
system.exec(string.format("%q %q", EXEFILE, filename)) |
||||
else |
||||
local ok, doc = core.try(core.open_doc, filename) |
||||
if ok then |
||||
local node = core.root_view.root_node:get_child_overlapping_point(mx, my) |
||||
node:set_active_view(node.active_view) |
||||
core.root_view:open_doc(doc) |
||||
end |
||||
end |
||||
elseif type == "quit" then |
||||
core.quit() |
||||
end |
||||
return did_keymap |
||||
end |
||||
|
||||
|
||||
function core.step() |
||||
-- handle events |
||||
local did_keymap = false |
||||
local mouse_moved = false |
||||
local mouse = { x = 0, y = 0, dx = 0, dy = 0 } |
||||
|
||||
for type, a,b,c,d in system.poll_event do |
||||
if type == "mousemoved" then |
||||
mouse_moved = true |
||||
mouse.x, mouse.y = a, b |
||||
mouse.dx, mouse.dy = mouse.dx + c, mouse.dy + d |
||||
elseif type == "textinput" and did_keymap then |
||||
did_keymap = false |
||||
else |
||||
local _, res = core.try(core.on_event, type, a, b, c, d) |
||||
did_keymap = res or did_keymap |
||||
end |
||||
core.redraw = true |
||||
end |
||||
if mouse_moved then |
||||
core.try(core.on_event, "mousemoved", mouse.x, mouse.y, mouse.dx, mouse.dy) |
||||
end |
||||
|
||||
local width, height = renderer.get_size() |
||||
|
||||
-- update |
||||
core.root_view.size.x, core.root_view.size.y = width, height |
||||
core.root_view:update() |
||||
if not core.redraw then return false end |
||||
core.redraw = false |
||||
|
||||
-- close unreferenced docs |
||||
for i = #core.docs, 1, -1 do |
||||
local doc = core.docs[i] |
||||
if #core.get_views_referencing_doc(doc) == 0 then |
||||
table.remove(core.docs, i) |
||||
core.log_quiet("Closed doc \"%s\"", doc:get_name()) |
||||
end |
||||
end |
||||
|
||||
-- update window title |
||||
local name = core.active_view:get_name() |
||||
local title = (name ~= "---") and (name .. " - lite") or "lite" |
||||
if title ~= core.window_title then |
||||
system.set_window_title(title) |
||||
core.window_title = title |
||||
end |
||||
|
||||
-- draw |
||||
renderer.begin_frame() |
||||
core.clip_rect_stack[1] = { 0, 0, width, height } |
||||
renderer.set_clip_rect(table.unpack(core.clip_rect_stack[1])) |
||||
core.root_view:draw() |
||||
renderer.end_frame() |
||||
return true |
||||
end |
||||
|
||||
|
||||
local run_threads = coroutine.wrap(function() |
||||
while true do |
||||
local max_time = 1 / config.fps - 0.004 |
||||
local ran_any_threads = false |
||||
|
||||
for k, thread in pairs(core.threads) do |
||||
-- run thread |
||||
if thread.wake < system.get_time() then |
||||
local _, wait = assert(coroutine.resume(thread.cr)) |
||||
if coroutine.status(thread.cr) == "dead" then |
||||
if type(k) == "number" then |
||||
table.remove(core.threads, k) |
||||
else |
||||
core.threads[k] = nil |
||||
end |
||||
elseif wait then |
||||
thread.wake = system.get_time() + wait |
||||
end |
||||
ran_any_threads = true |
||||
end |
||||
|
||||
-- stop running threads if we're about to hit the end of frame |
||||
if system.get_time() - core.frame_start > max_time then |
||||
coroutine.yield() |
||||
end |
||||
end |
||||
|
||||
if not ran_any_threads then coroutine.yield() end |
||||
end |
||||
end) |
||||
|
||||
|
||||
function core.run() |
||||
while true do |
||||
core.frame_start = system.get_time() |
||||
local did_redraw = core.step() |
||||
run_threads() |
||||
if not did_redraw and not system.window_has_focus() then |
||||
system.wait_event(0.25) |
||||
end |
||||
local elapsed = system.get_time() - core.frame_start |
||||
system.sleep(math.max(0, 1 / config.fps - elapsed)) |
||||
end |
||||
end |
||||
|
||||
|
||||
function core.on_error(err) |
||||
-- write error to file |
||||
local fp = io.open(EXEDIR .. "/error.txt", "wb") |
||||
fp:write("Error: " .. tostring(err) .. "\n") |
||||
fp:write(debug.traceback(nil, 4)) |
||||
fp:close() |
||||
-- save copy of all unsaved documents |
||||
for _, doc in ipairs(core.docs) do |
||||
if doc:is_dirty() and doc.filename then |
||||
doc:save(doc.filename .. "~") |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
return core |
@ -0,0 +1,186 @@ |
||||
local command = require "core.command" |
||||
local keymap = {} |
||||
|
||||
keymap.modkeys = {} |
||||
keymap.map = {} |
||||
keymap.reverse_map = {} |
||||
|
||||
local modkey_map = { |
||||
["left ctrl"] = "ctrl", |
||||
["right ctrl"] = "ctrl", |
||||
["left shift"] = "shift", |
||||
["right shift"] = "shift", |
||||
["left alt"] = "alt", |
||||
["right alt"] = "altgr", |
||||
} |
||||
|
||||
local modkeys = { "ctrl", "alt", "altgr", "shift" } |
||||
|
||||
local function key_to_stroke(k) |
||||
local stroke = "" |
||||
for _, mk in ipairs(modkeys) do |
||||
if keymap.modkeys[mk] then |
||||
stroke = stroke .. mk .. "+" |
||||
end |
||||
end |
||||
return stroke .. k |
||||
end |
||||
|
||||
|
||||
function keymap.add(map, overwrite) |
||||
for stroke, commands in pairs(map) do |
||||
if type(commands) == "string" then |
||||
commands = { commands } |
||||
end |
||||
if overwrite then |
||||
keymap.map[stroke] = commands |
||||
else |
||||
keymap.map[stroke] = keymap.map[stroke] or {} |
||||
for i = #commands, 1, -1 do |
||||
table.insert(keymap.map[stroke], 1, commands[i]) |
||||
end |
||||
end |
||||
for _, cmd in ipairs(commands) do |
||||
keymap.reverse_map[cmd] = stroke |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function keymap.get_binding(cmd) |
||||
return keymap.reverse_map[cmd] |
||||
end |
||||
|
||||
|
||||
function keymap.on_key_pressed(k) |
||||
local mk = modkey_map[k] |
||||
if mk then |
||||
keymap.modkeys[mk] = true |
||||
-- work-around for windows where `altgr` is treated as `ctrl+alt` |
||||
if mk == "altgr" then |
||||
keymap.modkeys["ctrl"] = false |
||||
end |
||||
else |
||||
local stroke = key_to_stroke(k) |
||||
local commands = keymap.map[stroke] |
||||
if commands then |
||||
for _, cmd in ipairs(commands) do |
||||
local performed = command.perform(cmd) |
||||
if performed then break end |
||||
end |
||||
return true |
||||
end |
||||
end |
||||
return false |
||||
end |
||||
|
||||
|
||||
function keymap.on_key_released(k) |
||||
local mk = modkey_map[k] |
||||
if mk then |
||||
keymap.modkeys[mk] = false |
||||
end |
||||
end |
||||
|
||||
|
||||
keymap.add { |
||||
["ctrl+shift+p"] = "core:find-command", |
||||
["ctrl+p"] = "core:find-file", |
||||
["ctrl+o"] = "core:open-file", |
||||
["ctrl+n"] = "core:new-doc", |
||||
["alt+return"] = "core:toggle-fullscreen", |
||||
|
||||
["alt+shift+j"] = "root:split-left", |
||||
["alt+shift+l"] = "root:split-right", |
||||
["alt+shift+i"] = "root:split-up", |
||||
["alt+shift+k"] = "root:split-down", |
||||
["alt+j"] = "root:switch-to-left", |
||||
["alt+l"] = "root:switch-to-right", |
||||
["alt+i"] = "root:switch-to-up", |
||||
["alt+k"] = "root:switch-to-down", |
||||
|
||||
["ctrl+w"] = "root:close", |
||||
["ctrl+tab"] = "root:switch-to-next-tab", |
||||
["ctrl+shift+tab"] = "root:switch-to-previous-tab", |
||||
["ctrl+pageup"] = "root:move-tab-left", |
||||
["ctrl+pagedown"] = "root:move-tab-right", |
||||
["alt+1"] = "root:switch-to-tab-1", |
||||
["alt+2"] = "root:switch-to-tab-2", |
||||
["alt+3"] = "root:switch-to-tab-3", |
||||
["alt+4"] = "root:switch-to-tab-4", |
||||
["alt+5"] = "root:switch-to-tab-5", |
||||
["alt+6"] = "root:switch-to-tab-6", |
||||
["alt+7"] = "root:switch-to-tab-7", |
||||
["alt+8"] = "root:switch-to-tab-8", |
||||
["alt+9"] = "root:switch-to-tab-9", |
||||
|
||||
["ctrl+f"] = "find-replace:find", |
||||
["ctrl+r"] = "find-replace:replace", |
||||
["f3"] = "find-replace:repeat-find", |
||||
["shift+f3"] = "find-replace:previous-find", |
||||
["ctrl+g"] = "doc:go-to-line", |
||||
["ctrl+s"] = "doc:save", |
||||
["ctrl+shift+s"] = "doc:save-as", |
||||
|
||||
["ctrl+z"] = "doc:undo", |
||||
["ctrl+y"] = "doc:redo", |
||||
["ctrl+x"] = "doc:cut", |
||||
["ctrl+c"] = "doc:copy", |
||||
["ctrl+v"] = "doc:paste", |
||||
["escape"] = { "command:escape", "doc:select-none" }, |
||||
["tab"] = { "command:complete", "doc:indent" }, |
||||
["shift+tab"] = "doc:unindent", |
||||
["backspace"] = "doc:backspace", |
||||
["shift+backspace"] = "doc:backspace", |
||||
["ctrl+backspace"] = "doc:delete-to-previous-word-start", |
||||
["ctrl+shift+backspace"] = "doc:delete-to-previous-word-start", |
||||
["delete"] = "doc:delete", |
||||
["shift+delete"] = "doc:delete", |
||||
["ctrl+delete"] = "doc:delete-to-next-word-end", |
||||
["ctrl+shift+delete"] = "doc:delete-to-next-word-end", |
||||
["return"] = { "command:submit", "doc:newline" }, |
||||
["keypad enter"] = { "command:submit", "doc:newline" }, |
||||
["ctrl+return"] = "doc:newline-below", |
||||
["ctrl+shift+return"] = "doc:newline-above", |
||||
["ctrl+j"] = "doc:join-lines", |
||||
["ctrl+a"] = "doc:select-all", |
||||
["ctrl+d"] = { "find-replace:select-next", "doc:select-word" }, |
||||
["ctrl+l"] = "doc:select-lines", |
||||
["ctrl+/"] = "doc:toggle-line-comments", |
||||
["ctrl+up"] = "doc:move-lines-up", |
||||
["ctrl+down"] = "doc:move-lines-down", |
||||
["ctrl+shift+d"] = "doc:duplicate-lines", |
||||
["ctrl+shift+k"] = "doc:delete-lines", |
||||
|
||||
["left"] = "doc:move-to-previous-char", |
||||
["right"] = "doc:move-to-next-char", |
||||
["up"] = { "command:select-previous", "doc:move-to-previous-line" }, |
||||
["down"] = { "command:select-next", "doc:move-to-next-line" }, |
||||
["ctrl+left"] = "doc:move-to-previous-word-start", |
||||
["ctrl+right"] = "doc:move-to-next-word-end", |
||||
["ctrl+["] = "doc:move-to-previous-block-start", |
||||
["ctrl+]"] = "doc:move-to-next-block-end", |
||||
["home"] = "doc:move-to-start-of-line", |
||||
["end"] = "doc:move-to-end-of-line", |
||||
["ctrl+home"] = "doc:move-to-start-of-doc", |
||||
["ctrl+end"] = "doc:move-to-end-of-doc", |
||||
["pageup"] = "doc:move-to-previous-page", |
||||
["pagedown"] = "doc:move-to-next-page", |
||||
|
||||
["shift+left"] = "doc:select-to-previous-char", |
||||
["shift+right"] = "doc:select-to-next-char", |
||||
["shift+up"] = "doc:select-to-previous-line", |
||||
["shift+down"] = "doc:select-to-next-line", |
||||
["ctrl+shift+left"] = "doc:select-to-previous-word-start", |
||||
["ctrl+shift+right"] = "doc:select-to-next-word-end", |
||||
["ctrl+shift+["] = "doc:select-to-previous-block-start", |
||||
["ctrl+shift+]"] = "doc:select-to-next-block-end", |
||||
["shift+home"] = "doc:select-to-start-of-line", |
||||
["shift+end"] = "doc:select-to-end-of-line", |
||||
["ctrl+shift+home"] = "doc:select-to-start-of-doc", |
||||
["ctrl+shift+end"] = "doc:select-to-end-of-doc", |
||||
["shift+pageup"] = "doc:select-to-previous-page", |
||||
["shift+pagedown"] = "doc:select-to-next-page", |
||||
} |
||||
|
||||
return keymap |
@ -0,0 +1,74 @@ |
||||
local core = require "core" |
||||
local style = require "core.style" |
||||
local View = require "core.view" |
||||
|
||||
|
||||
local LogView = View:extend() |
||||
|
||||
|
||||
function LogView:new() |
||||
LogView.super.new(self) |
||||
self.last_item = core.log_items[#core.log_items] |
||||
self.scrollable = true |
||||
self.yoffset = 0 |
||||
end |
||||
|
||||
|
||||
function LogView:get_name() |
||||
return "Log" |
||||
end |
||||
|
||||
|
||||
function LogView:update() |
||||
local item = core.log_items[#core.log_items] |
||||
if self.last_item ~= item then |
||||
self.last_item = item |
||||
self.scroll.to.y = 0 |
||||
self.yoffset = -(style.font:get_height() + style.padding.y) |
||||
end |
||||
|
||||
self:move_towards("yoffset", 0) |
||||
|
||||
LogView.super.update(self) |
||||
end |
||||
|
||||
|
||||
local function draw_text_multiline(font, text, x, y, color) |
||||
local th = font:get_height() |
||||
local resx, resy = x, y |
||||
for line in text:gmatch("[^\n]+") do |
||||
resy = y |
||||
resx = renderer.draw_text(style.font, line, x, y, color) |
||||
y = y + th |
||||
end |
||||
return resx, resy |
||||
end |
||||
|
||||
|
||||
function LogView:draw() |
||||
self:draw_background(style.background) |
||||
|
||||
local ox, oy = self:get_content_offset() |
||||
local th = style.font:get_height() |
||||
local y = oy + style.padding.y + self.yoffset |
||||
|
||||
for i = #core.log_items, 1, -1 do |
||||
local x = ox + style.padding.x |
||||
local item = core.log_items[i] |
||||
local time = os.date(nil, item.time) |
||||
x = renderer.draw_text(style.font, time, x, y, style.dim) |
||||
x = x + style.padding.x |
||||
local subx = x |
||||
x, y = draw_text_multiline(style.font, item.text, x, y, style.text) |
||||
renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) |
||||
y = y + th |
||||
if item.info then |
||||
subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) |
||||
y = y + th |
||||
end |
||||
y = y + style.padding.y |
||||
end |
||||
end |
||||
|
||||
|
||||
return LogView |
@ -0,0 +1,58 @@ |
||||
local Object = {} |
||||
Object.__index = Object |
||||
|
||||
|
||||
function Object:new() |
||||
end |
||||
|
||||
|
||||
function Object:extend() |
||||
local cls = {} |
||||
for k, v in pairs(self) do |
||||
if k:find("__") == 1 then |
||||
cls[k] = v |
||||
end |
||||
end |
||||
cls.__index = cls |
||||
cls.super = self |
||||
setmetatable(cls, self) |
||||
return cls |
||||
end |
||||
|
||||
|
||||
function Object:implement(...) |
||||
for _, cls in pairs({...}) do |
||||
for k, v in pairs(cls) do |
||||
if self[k] == nil and type(v) == "function" then |
||||
self[k] = v |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function Object:is(T) |
||||
local mt = getmetatable(self) |
||||
while mt do |
||||
if mt == T then |
||||
return true |
||||
end |
||||
mt = getmetatable(mt) |
||||
end |
||||
return false |
||||
end |
||||
|
||||
|
||||
function Object:__tostring() |
||||
return "Object" |
||||
end |
||||
|
||||
|
||||
function Object:__call(...) |
||||
local obj = setmetatable({}, self) |
||||
obj:new(...) |
||||
return obj |
||||
end |
||||
|
||||
|
||||
return Object |
@ -0,0 +1,504 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local style = require "core.style" |
||||
local keymap = require "core.keymap" |
||||
local Object = require "core.object" |
||||
local View = require "core.view" |
||||
local DocView = require "core.docview" |
||||
|
||||
|
||||
local EmptyView = View:extend() |
||||
|
||||
local function draw_text(x, y, color) |
||||
local th = style.big_font:get_height() |
||||
local dh = th + style.padding.y * 2 |
||||
x = renderer.draw_text(style.big_font, "lite", x, y + (dh - th) / 2, color) |
||||
x = x + style.padding.x |
||||
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color) |
||||
local lines = { |
||||
{ fmt = "%s to run a command", cmd = "core:find-command" }, |
||||
{ fmt = "%s to open a file from the project", cmd = "core:find-file" }, |
||||
} |
||||
th = style.font:get_height() |
||||
y = y + (dh - th * 2 - style.padding.y) / 2 |
||||
local w = 0 |
||||
for _, line in ipairs(lines) do |
||||
local text = string.format(line.fmt, keymap.get_binding(line.cmd)) |
||||
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color)) |
||||
y = y + th + style.padding.y |
||||
end |
||||
return w, dh |
||||
end |
||||
|
||||
function EmptyView:draw() |
||||
self:draw_background(style.background) |
||||
local w, h = draw_text(0, 0, { 0, 0, 0, 0 }) |
||||
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2) |
||||
local y = self.position.y + (self.size.y - h) / 2 |
||||
draw_text(x, y, style.dim) |
||||
end |
||||
|
||||
|
||||
|
||||
local Node = Object:extend() |
||||
|
||||
function Node:new(type) |
||||
self.type = type or "leaf" |
||||
self.position = { x = 0, y = 0 } |
||||
self.size = { x = 0, y = 0 } |
||||
self.views = {} |
||||
self.divider = 0.5 |
||||
if self.type == "leaf" then |
||||
self:add_view(EmptyView()) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:propagate(fn, ...) |
||||
self.a[fn](self.a, ...) |
||||
self.b[fn](self.b, ...) |
||||
end |
||||
|
||||
|
||||
function Node:on_mouse_moved(x, y, ...) |
||||
self.hovered_tab = self:get_tab_overlapping_point(x, y) |
||||
if self.type == "leaf" then |
||||
self.active_view:on_mouse_moved(x, y, ...) |
||||
else |
||||
self:propagate("on_mouse_moved", x, y, ...) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:on_mouse_released(...) |
||||
if self.type == "leaf" then |
||||
self.active_view:on_mouse_released(...) |
||||
else |
||||
self:propagate("on_mouse_released", ...) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:consume(node) |
||||
for k, _ in pairs(self) do self[k] = nil end |
||||
for k, v in pairs(node) do self[k] = v end |
||||
end |
||||
|
||||
|
||||
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" } |
||||
|
||||
function Node:split(dir, view, locked) |
||||
assert(self.type == "leaf", "Tried to split non-leaf node") |
||||
local type = assert(type_map[dir], "Invalid direction") |
||||
local last_active = core.active_view |
||||
local child = Node() |
||||
child:consume(self) |
||||
self:consume(Node(type)) |
||||
self.a = child |
||||
self.b = Node() |
||||
if view then self.b:add_view(view) end |
||||
if locked then |
||||
self.b.locked = locked |
||||
core.set_active_view(last_active) |
||||
end |
||||
if dir == "up" or dir == "left" then |
||||
self.a, self.b = self.b, self.a |
||||
end |
||||
return child |
||||
end |
||||
|
||||
|
||||
function Node:close_active_view(root) |
||||
local do_close = function() |
||||
if #self.views > 1 then |
||||
local idx = self:get_view_idx(self.active_view) |
||||
table.remove(self.views, idx) |
||||
self:set_active_view(self.views[idx] or self.views[#self.views]) |
||||
else |
||||
local parent = self:get_parent_node(root) |
||||
local is_a = (parent.a == self) |
||||
local other = parent[is_a and "b" or "a"] |
||||
if other:get_locked_size() then |
||||
self.views = {} |
||||
self:add_view(EmptyView()) |
||||
else |
||||
parent:consume(other) |
||||
local p = parent |
||||
while p.type ~= "leaf" do |
||||
p = p[is_a and "a" or "b"] |
||||
end |
||||
p:set_active_view(p.active_view) |
||||
end |
||||
end |
||||
core.last_active_view = nil |
||||
end |
||||
self.active_view:try_close(do_close) |
||||
end |
||||
|
||||
|
||||
function Node:add_view(view) |
||||
assert(self.type == "leaf", "Tried to add view to non-leaf node") |
||||
assert(not self.locked, "Tried to add view to locked node") |
||||
if self.views[1] and self.views[1]:is(EmptyView) then |
||||
table.remove(self.views) |
||||
end |
||||
table.insert(self.views, view) |
||||
self:set_active_view(view) |
||||
end |
||||
|
||||
|
||||
function Node:set_active_view(view) |
||||
assert(self.type == "leaf", "Tried to set active view on non-leaf node") |
||||
self.active_view = view |
||||
core.set_active_view(view) |
||||
end |
||||
|
||||
|
||||
function Node:get_view_idx(view) |
||||
for i, v in ipairs(self.views) do |
||||
if v == view then return i end |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_node_for_view(view) |
||||
for _, v in ipairs(self.views) do |
||||
if v == view then return self end |
||||
end |
||||
if self.type ~= "leaf" then |
||||
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_parent_node(root) |
||||
if root.a == self or root.b == self then |
||||
return root |
||||
elseif root.type ~= "leaf" then |
||||
return self:get_parent_node(root.a) or self:get_parent_node(root.b) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_children(t) |
||||
t = t or {} |
||||
for _, view in ipairs(self.views) do |
||||
table.insert(t, view) |
||||
end |
||||
if self.a then self.a:get_children(t) end |
||||
if self.b then self.b:get_children(t) end |
||||
return t |
||||
end |
||||
|
||||
|
||||
function Node:get_divider_overlapping_point(px, py) |
||||
if self.type ~= "leaf" then |
||||
local p = 6 |
||||
local x, y, w, h = self:get_divider_rect() |
||||
x, y = x - p, y - p |
||||
w, h = w + p * 2, h + p * 2 |
||||
if px > x and py > y and px < x + w and py < y + h then |
||||
return self |
||||
end |
||||
return self.a:get_divider_overlapping_point(px, py) |
||||
or self.b:get_divider_overlapping_point(px, py) |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_tab_overlapping_point(px, py) |
||||
if #self.views == 1 then return nil end |
||||
local x, y, w, h = self:get_tab_rect(1) |
||||
if px >= x and py >= y and px < x + w * #self.views and py < y + h then |
||||
return math.floor((px - x) / w) + 1 |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_child_overlapping_point(x, y) |
||||
local child |
||||
if self.type == "leaf" then |
||||
return self |
||||
elseif self.type == "hsplit" then |
||||
child = (x < self.b.position.x) and self.a or self.b |
||||
elseif self.type == "vsplit" then |
||||
child = (y < self.b.position.y) and self.a or self.b |
||||
end |
||||
return child:get_child_overlapping_point(x, y) |
||||
end |
||||
|
||||
|
||||
function Node:get_tab_rect(idx) |
||||
local tw = math.min(style.tab_width, math.ceil(self.size.x / #self.views)) |
||||
local h = style.font:get_height() + style.padding.y * 2 |
||||
return self.position.x + (idx-1) * tw, self.position.y, tw, h |
||||
end |
||||
|
||||
|
||||
function Node:get_divider_rect() |
||||
local x, y = self.position.x, self.position.y |
||||
if self.type == "hsplit" then |
||||
return x + self.a.size.x, y, style.divider_size, self.size.y |
||||
elseif self.type == "vsplit" then |
||||
return x, y + self.a.size.y, self.size.x, style.divider_size |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:get_locked_size() |
||||
if self.type == "leaf" then |
||||
if self.locked then |
||||
local size = self.active_view.size |
||||
return size.x, size.y |
||||
end |
||||
else |
||||
local x1, y1 = self.a:get_locked_size() |
||||
local x2, y2 = self.b:get_locked_size() |
||||
if x1 and x2 then |
||||
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size |
||||
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size |
||||
return x1 + x2 + dsx, y1 + y2 + dsy |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
local function copy_position_and_size(dst, src) |
||||
dst.position.x, dst.position.y = src.position.x, src.position.y |
||||
dst.size.x, dst.size.y = src.size.x, src.size.y |
||||
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) |
||||
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 |
||||
self.a.position[x] = self.position[x] |
||||
self.a.position[y] = self.position[y] |
||||
self.a.size[x] = n - ds |
||||
self.a.size[y] = self.size[y] |
||||
self.b.position[x] = self.position[x] + n |
||||
self.b.position[y] = self.position[y] |
||||
self.b.size[x] = self.size[x] - n |
||||
self.b.size[y] = self.size[y] |
||||
end |
||||
|
||||
|
||||
function Node:update_layout() |
||||
if self.type == "leaf" then |
||||
local av = self.active_view |
||||
if #self.views > 1 then |
||||
local _, _, _, th = self:get_tab_rect(1) |
||||
av.position.x, av.position.y = self.position.x, self.position.y + th |
||||
av.size.x, av.size.y = self.size.x, self.size.y - th |
||||
else |
||||
copy_position_and_size(av, self) |
||||
end |
||||
else |
||||
local x1, y1 = self.a:get_locked_size() |
||||
local x2, y2 = self.b:get_locked_size() |
||||
if self.type == "hsplit" then |
||||
calc_split_sizes(self, "x", "y", x1, x2) |
||||
elseif self.type == "vsplit" then |
||||
calc_split_sizes(self, "y", "x", y1, y2) |
||||
end |
||||
self.a:update_layout() |
||||
self.b:update_layout() |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:update() |
||||
if self.type == "leaf" then |
||||
for _, view in ipairs(self.views) do |
||||
view:update() |
||||
end |
||||
else |
||||
self.a:update() |
||||
self.b:update() |
||||
end |
||||
end |
||||
|
||||
|
||||
function Node:draw_tabs() |
||||
local x, y, _, h = self:get_tab_rect(1) |
||||
local ds = style.divider_size |
||||
core.push_clip_rect(x, y, self.size.x, h) |
||||
renderer.draw_rect(x, y, self.size.x, h, style.background2) |
||||
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider) |
||||
|
||||
for i, view in ipairs(self.views) do |
||||
local x, y, w, h = self:get_tab_rect(i) |
||||
local text = view:get_name() |
||||
local color = style.dim |
||||
if view == self.active_view then |
||||
color = style.text |
||||
renderer.draw_rect(x, y, w, h, style.background) |
||||
renderer.draw_rect(x + w, y, ds, h, style.divider) |
||||
renderer.draw_rect(x - ds, y, ds, h, style.divider) |
||||
end |
||||
if i == self.hovered_tab then |
||||
color = style.text |
||||
end |
||||
core.push_clip_rect(x, y, w, h) |
||||
x, w = x + style.padding.x, w - style.padding.x * 2 |
||||
local align = style.font:get_width(text) > w and "left" or "center" |
||||
common.draw_text(style.font, color, text, align, x, y, w, h) |
||||
core.pop_clip_rect() |
||||
end |
||||
|
||||
core.pop_clip_rect() |
||||
end |
||||
|
||||
|
||||
function Node:draw() |
||||
if self.type == "leaf" then |
||||
if #self.views > 1 then |
||||
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) |
||||
self.active_view:draw() |
||||
core.pop_clip_rect() |
||||
else |
||||
local x, y, w, h = self:get_divider_rect() |
||||
renderer.draw_rect(x, y, w, h, style.divider) |
||||
self:propagate("draw") |
||||
end |
||||
end |
||||
|
||||
|
||||
|
||||
local RootView = View:extend() |
||||
|
||||
function RootView:new() |
||||
RootView.super.new(self) |
||||
self.root_node = Node() |
||||
self.deferred_draws = {} |
||||
self.mouse = { x = 0, y = 0 } |
||||
end |
||||
|
||||
|
||||
function RootView:defer_draw(fn, ...) |
||||
table.insert(self.deferred_draws, 1, { fn = fn, ... }) |
||||
end |
||||
|
||||
|
||||
function RootView:get_active_node() |
||||
return self.root_node:get_node_for_view(core.active_view) |
||||
end |
||||
|
||||
|
||||
function RootView:open_doc(doc) |
||||
local node = self:get_active_node() |
||||
if node.locked and core.last_active_view then |
||||
core.set_active_view(core.last_active_view) |
||||
node = self:get_active_node() |
||||
end |
||||
assert(not node.locked, "Cannot open doc on locked node") |
||||
for i, view in ipairs(node.views) do |
||||
if view.doc == doc then |
||||
node:set_active_view(node.views[i]) |
||||
return view |
||||
end |
||||
end |
||||
local view = DocView(doc) |
||||
node:add_view(view) |
||||
self.root_node:update_layout() |
||||
view:scroll_to_line(view.doc:get_selection(), true, true) |
||||
return view |
||||
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) |
||||
local idx = node:get_tab_overlapping_point(x, y) |
||||
if idx then |
||||
node:set_active_view(node.views[idx]) |
||||
if button == "middle" then |
||||
node:close_active_view(self.root_node) |
||||
end |
||||
else |
||||
core.set_active_view(node.active_view) |
||||
node.active_view:on_mouse_pressed(button, x, y, clicks) |
||||
end |
||||
end |
||||
|
||||
|
||||
function RootView:on_mouse_released(...) |
||||
if self.dragged_divider then |
||||
self.dragged_divider = nil |
||||
end |
||||
self.root_node:on_mouse_released(...) |
||||
end |
||||
|
||||
|
||||
function RootView:on_mouse_moved(x, y, dx, dy) |
||||
if self.dragged_divider then |
||||
local node = self.dragged_divider |
||||
if node.type == "hsplit" then |
||||
node.divider = node.divider + dx / node.size.x |
||||
else |
||||
node.divider = node.divider + dy / node.size.y |
||||
end |
||||
node.divider = common.clamp(node.divider, 0.01, 0.99) |
||||
return |
||||
end |
||||
|
||||
self.mouse.x, self.mouse.y = x, y |
||||
self.root_node:on_mouse_moved(x, y, dx, dy) |
||||
|
||||
local node = self.root_node:get_child_overlapping_point(x, y) |
||||
local div = self.root_node:get_divider_overlapping_point(x, y) |
||||
if div then |
||||
system.set_cursor(div.type == "hsplit" and "sizeh" or "sizev") |
||||
elseif node:get_tab_overlapping_point(x, y) then |
||||
system.set_cursor("arrow") |
||||
else |
||||
system.set_cursor(node.active_view.cursor) |
||||
end |
||||
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(...) |
||||
end |
||||
|
||||
|
||||
function RootView:on_text_input(...) |
||||
core.active_view:on_text_input(...) |
||||
end |
||||
|
||||
|
||||
function RootView:update() |
||||
copy_position_and_size(self.root_node, self) |
||||
self.root_node:update() |
||||
self.root_node:update_layout() |
||||
end |
||||
|
||||
|
||||
function RootView:draw() |
||||
self.root_node:draw() |
||||
while #self.deferred_draws > 0 do |
||||
local t = table.remove(self.deferred_draws) |
||||
t.fn(table.unpack(t)) |
||||
end |
||||
end |
||||
|
||||
|
||||
return RootView |
@ -0,0 +1,141 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local command = require "core.command" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
local DocView = require "core.docview" |
||||
local LogView = require "core.logview" |
||||
local View = require "core.view" |
||||
|
||||
|
||||
local StatusView = View:extend() |
||||
|
||||
StatusView.separator = " " |
||||
StatusView.separator2 = " | " |
||||
|
||||
|
||||
function StatusView:new() |
||||
StatusView.super.new(self) |
||||
self.message_timeout = 0 |
||||
self.message = {} |
||||
end |
||||
|
||||
|
||||
function StatusView:on_mouse_pressed() |
||||
core.set_active_view(core.last_active_view) |
||||
if system.get_time() < self.message_timeout |
||||
and not core.active_view:is(LogView) then |
||||
command.perform "core:open-log" |
||||
end |
||||
end |
||||
|
||||
|
||||
function StatusView:show_message(icon, icon_color, text) |
||||
self.message = { |
||||
icon_color, style.icon_font, icon, |
||||
style.dim, style.font, StatusView.separator2, style.text, text |
||||
} |
||||
self.message_timeout = system.get_time() + config.message_timeout |
||||
end |
||||
|
||||
|
||||
function StatusView:update() |
||||
self.size.y = style.font:get_height() + style.padding.y * 2 |
||||
|
||||
if system.get_time() < self.message_timeout then |
||||
self.scroll.to.y = self.size.y |
||||
else |
||||
self.scroll.to.y = 0 |
||||
end |
||||
|
||||
StatusView.super.update(self) |
||||
end |
||||
|
||||
|
||||
local function draw_items(self, items, x, y, draw_fn) |
||||
local font = style.font |
||||
local color = style.text |
||||
|
||||
for _, item in ipairs(items) do |
||||
if type(item) == "userdata" then |
||||
font = item |
||||
elseif type(item) == "table" then |
||||
color = item |
||||
else |
||||
x = draw_fn(font, color, item, nil, x, y, 0, self.size.y) |
||||
end |
||||
end |
||||
|
||||
return x |
||||
end |
||||
|
||||
|
||||
local function text_width(font, _, text, _, x) |
||||
return x + font:get_width(text) |
||||
end |
||||
|
||||
|
||||
function StatusView:draw_items(items, right_align, yoffset) |
||||
local x, y = self:get_content_offset() |
||||
y = y + (yoffset or 0) |
||||
if right_align then |
||||
local w = draw_items(self, items, 0, 0, text_width) |
||||
x = x + self.size.x - w - style.padding.x |
||||
draw_items(self, items, x, y, common.draw_text) |
||||
else |
||||
x = x + style.padding.x |
||||
draw_items(self, items, x, y, common.draw_text) |
||||
end |
||||
end |
||||
|
||||
|
||||
function StatusView:get_items() |
||||
if getmetatable(core.active_view) == DocView then |
||||
local dv = core.active_view |
||||
local line, col = dv.doc:get_selection() |
||||
local dirty = dv.doc:is_dirty() |
||||
|
||||
return { |
||||
dirty and style.accent or style.text, style.icon_font, "f", |
||||
style.dim, style.font, self.separator2, style.text, |
||||
dv.doc.filename and style.text or style.dim, dv.doc:get_name(), |
||||
style.text, |
||||
self.separator, |
||||
"line: ", line, |
||||
self.separator, |
||||
col > config.line_limit and style.accent or style.text, "col: ", col, |
||||
style.text, |
||||
self.separator, |
||||
string.format("%d%%", line / #dv.doc.lines * 100), |
||||
}, { |
||||
style.icon_font, "g", |
||||
style.font, style.dim, self.separator2, style.text, |
||||
#dv.doc.lines, " lines", |
||||
self.separator, |
||||
dv.doc.crlf and "CRLF" or "LF" |
||||
} |
||||
end |
||||
|
||||
return {}, { |
||||
style.icon_font, "g", |
||||
style.font, style.dim, self.separator2, |
||||
#core.docs, style.text, " / ", |
||||
#core.project_files, " files" |
||||
} |
||||
end |
||||
|
||||
|
||||
function StatusView:draw() |
||||
self:draw_background(style.background2) |
||||
|
||||
if self.message then |
||||
self:draw_items(self.message, false, self.size.y) |
||||
end |
||||
|
||||
local left, right = self:get_items() |
||||
self:draw_items(left) |
||||
self:draw_items(right, true) |
||||
end |
||||
|
||||
|
||||
return StatusView |
@ -0,0 +1,26 @@ |
||||
local strict = {} |
||||
strict.defined = {} |
||||
|
||||
|
||||
-- used to define a global variable |
||||
function global(t) |
||||
for k, v in pairs(t) do |
||||
strict.defined[k] = true |
||||
rawset(_G, k, v) |
||||
end |
||||
end |
||||
|
||||
|
||||
function strict.__newindex(t, k, v) |
||||
error("cannot set undefined variable: " .. k, 2) |
||||
end |
||||
|
||||
|
||||
function strict.__index(t, k) |
||||
if not strict.defined[k] then |
||||
error("cannot get undefined variable: " .. k, 2) |
||||
end |
||||
end |
||||
|
||||
|
||||
setmetatable(_G, strict) |
@ -0,0 +1,42 @@ |
||||
local common = require "core.common" |
||||
local style = {} |
||||
|
||||
style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) } |
||||
style.divider_size = common.round(1 * SCALE) |
||||
style.scrollbar_size = common.round(4 * SCALE) |
||||
style.caret_width = common.round(2 * SCALE) |
||||
style.tab_width = common.round(170 * SCALE) |
||||
|
||||
style.font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 14 * SCALE) |
||||
style.big_font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 34 * SCALE) |
||||
style.icon_font = renderer.font.load(EXEDIR .. "/data/fonts/icons.ttf", 14 * SCALE) |
||||
style.code_font = renderer.font.load(EXEDIR .. "/data/fonts/monospace.ttf", 13.5 * SCALE) |
||||
|
||||
style.background = { common.color "#2e2e32" } |
||||
style.background2 = { common.color "#252529" } |
||||
style.background3 = { common.color "#252529" } |
||||
style.text = { common.color "#97979c" } |
||||
style.caret = { common.color "#93DDFA" } |
||||
style.accent = { common.color "#e1e1e6" } |
||||
style.dim = { common.color "#525257" } |
||||
style.divider = { common.color "#202024" } |
||||
style.selection = { common.color "#48484f" } |
||||
style.line_number = { common.color "#525259" } |
||||
style.line_number2 = { common.color "#83838f" } |
||||
style.line_highlight = { common.color "#343438" } |
||||
style.scrollbar = { common.color "#414146" } |
||||
style.scrollbar2 = { common.color "#4b4b52" } |
||||
|
||||
style.syntax = {} |
||||
style.syntax["normal"] = { common.color "#e1e1e6" } |
||||
style.syntax["symbol"] = { common.color "#e1e1e6" } |
||||
style.syntax["comment"] = { common.color "#676b6f" } |
||||
style.syntax["keyword"] = { common.color "#E58AC9" } |
||||
style.syntax["keyword2"] = { common.color "#F77483" } |
||||
style.syntax["number"] = { common.color "#FFA94D" } |
||||
style.syntax["literal"] = { common.color "#FFA94D" } |
||||
style.syntax["string"] = { common.color "#f7c95c" } |
||||
style.syntax["operator"] = { common.color "#93DDFA" } |
||||
style.syntax["function"] = { common.color "#93DDFA" } |
||||
|
||||
return style |
@ -0,0 +1,30 @@ |
||||
local common = require "core.common" |
||||
|
||||
local syntax = {} |
||||
syntax.items = {} |
||||
|
||||
local plain_text_syntax = { patterns = {}, symbols = {} } |
||||
|
||||
|
||||
function syntax.add(t) |
||||
table.insert(syntax.items, t) |
||||
end |
||||
|
||||
|
||||
local function find(string, field) |
||||
for i = #syntax.items, 1, -1 do |
||||
local t = syntax.items[i] |
||||
if common.match_pattern(string, t[field] or {}) then |
||||
return t |
||||
end |
||||
end |
||||
end |
||||
|
||||
function syntax.get(filename, header) |
||||
return find(filename, "files") |
||||
or find(header, "headers") |
||||
or plain_text_syntax |
||||
end |
||||
|
||||
|
||||
return syntax |
@ -0,0 +1,112 @@ |
||||
local tokenizer = {} |
||||
|
||||
|
||||
local function push_token(t, type, text) |
||||
local prev_type = t[#t-1] |
||||
local prev_text = t[#t] |
||||
if prev_type and (prev_type == type or prev_text:find("^%s*$")) then |
||||
t[#t-1] = type |
||||
t[#t] = prev_text .. text |
||||
else |
||||
table.insert(t, type) |
||||
table.insert(t, text) |
||||
end |
||||
end |
||||
|
||||
|
||||
local function is_escaped(text, idx, esc) |
||||
local byte = esc:byte() |
||||
local count = 0 |
||||
for i = idx - 1, 1, -1 do |
||||
if text:byte(i) ~= byte then break end |
||||
count = count + 1 |
||||
end |
||||
return count % 2 == 1 |
||||
end |
||||
|
||||
|
||||
local function find_non_escaped(text, pattern, offset, esc) |
||||
while true do |
||||
local s, e = text:find(pattern, offset) |
||||
if not s then break end |
||||
if esc and is_escaped(text, s, esc) then |
||||
offset = e + 1 |
||||
else |
||||
return s, e |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function tokenizer.tokenize(syntax, text, state) |
||||
local res = {} |
||||
local i = 1 |
||||
|
||||
if #syntax.patterns == 0 then |
||||
return { "normal", text } |
||||
end |
||||
|
||||
while i <= #text do |
||||
-- continue trying to match the end pattern of a pair if we have a state set |
||||
if state then |
||||
local p = syntax.patterns[state] |
||||
local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3]) |
||||
|
||||
if s then |
||||
push_token(res, p.type, text:sub(i, e)) |
||||
state = nil |
||||
i = e + 1 |
||||
else |
||||
push_token(res, p.type, text:sub(i)) |
||||
break |
||||
end |
||||
end |
||||
|
||||
-- find matching pattern |
||||
local matched = false |
||||
for n, p in ipairs(syntax.patterns) do |
||||
local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern |
||||
local s, e = text:find("^" .. pattern, i) |
||||
|
||||
if s then |
||||
-- matched pattern; make and add token |
||||
local t = text:sub(s, e) |
||||
push_token(res, syntax.symbols[t] or p.type, t) |
||||
|
||||
-- update state if this was a start|end pattern pair |
||||
if type(p.pattern) == "table" then |
||||
state = n |
||||
end |
||||
|
||||
-- move cursor past this token |
||||
i = e + 1 |
||||
matched = true |
||||
break |
||||
end |
||||
end |
||||
|
||||
-- consume character if we didn't match |
||||
if not matched then |
||||
push_token(res, "normal", text:sub(i, i)) |
||||
i = i + 1 |
||||
end |
||||
end |
||||
|
||||
return res, state |
||||
end |
||||
|
||||
|
||||
local function iter(t, i) |
||||
i = i + 2 |
||||
local type, text = t[i], t[i+1] |
||||
if type then |
||||
return i, type, text |
||||
end |
||||
end |
||||
|
||||
function tokenizer.each_token(t) |
||||
return iter, t, -1 |
||||
end |
||||
|
||||
|
||||
return tokenizer |
@ -0,0 +1,151 @@ |
||||
local core = require "core" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
local common = require "core.common" |
||||
local Object = require "core.object" |
||||
|
||||
|
||||
local View = Object:extend() |
||||
|
||||
|
||||
function View:new() |
||||
self.position = { x = 0, y = 0 } |
||||
self.size = { x = 0, y = 0 } |
||||
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } |
||||
self.cursor = "arrow" |
||||
self.scrollable = false |
||||
end |
||||
|
||||
|
||||
function View:move_towards(t, k, dest, rate) |
||||
if type(t) ~= "table" then |
||||
return self:move_towards(self, t, k, dest, rate) |
||||
end |
||||
local val = t[k] |
||||
if math.abs(val - dest) < 0.5 then |
||||
t[k] = dest |
||||
else |
||||
t[k] = common.lerp(val, dest, rate or 0.5) |
||||
end |
||||
if val ~= dest then |
||||
core.redraw = true |
||||
end |
||||
end |
||||
|
||||
|
||||
function View:try_close(do_close) |
||||
do_close() |
||||
end |
||||
|
||||
|
||||
function View:get_name() |
||||
return "---" |
||||
end |
||||
|
||||
|
||||
function View:get_scrollable_size() |
||||
return math.huge |
||||
end |
||||
|
||||
|
||||
function View:get_scrollbar_rect() |
||||
local sz = self:get_scrollable_size() |
||||
if sz <= self.size.y or sz == math.huge then |
||||
return 0, 0, 0, 0 |
||||
end |
||||
local h = math.max(20, self.size.y * self.size.y / sz) |
||||
return |
||||
self.position.x + self.size.x - style.scrollbar_size, |
||||
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y), |
||||
style.scrollbar_size, |
||||
h |
||||
end |
||||
|
||||
|
||||
function View:scrollbar_overlaps_point(x, y) |
||||
local sx, sy, sw, sh = self:get_scrollbar_rect() |
||||
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh |
||||
end |
||||
|
||||
|
||||
function View:on_mouse_pressed(button, x, y, clicks) |
||||
if self:scrollbar_overlaps_point(x, y) then |
||||
self.dragging_scrollbar = true |
||||
return true |
||||
end |
||||
end |
||||
|
||||
|
||||
function View:on_mouse_released(button, x, y) |
||||
self.dragging_scrollbar = false |
||||
end |
||||
|
||||
|
||||
function View:on_mouse_moved(x, y, dx, dy) |
||||
if self.dragging_scrollbar then |
||||
local delta = self:get_scrollable_size() / self.size.y * dy |
||||
self.scroll.to.y = self.scroll.to.y + delta |
||||
end |
||||
self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y) |
||||
end |
||||
|
||||
|
||||
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 |
||||
|
||||
|
||||
function View:get_content_bounds() |
||||
local x = self.scroll.x |
||||
local y = self.scroll.y |
||||
return x, y, x + self.size.x, y + self.size.y |
||||
end |
||||
|
||||
|
||||
function View:get_content_offset() |
||||
local x = common.round(self.position.x - self.scroll.x) |
||||
local y = common.round(self.position.y - self.scroll.y) |
||||
return x, y |
||||
end |
||||
|
||||
|
||||
function View:clamp_scroll_position() |
||||
local max = self:get_scrollable_size() - self.size.y |
||||
self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max) |
||||
end |
||||
|
||||
|
||||
function View:update() |
||||
self:clamp_scroll_position() |
||||
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3) |
||||
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3) |
||||
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) |
||||
end |
||||
|
||||
|
||||
function View:draw_scrollbar() |
||||
local x, y, w, h = self:get_scrollbar_rect() |
||||
local highlight = self.hovered_scrollbar or self.dragging_scrollbar |
||||
local color = highlight and style.scrollbar2 or style.scrollbar |
||||
renderer.draw_rect(x, y, w, h, color) |
||||
end |
||||
|
||||
|
||||
function View:draw() |
||||
end |
||||
|
||||
|
||||
return View |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,284 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
local command = require "core.command" |
||||
local style = require "core.style" |
||||
local keymap = require "core.keymap" |
||||
local translate = require "core.doc.translate" |
||||
local RootView = require "core.rootview" |
||||
local DocView = require "core.docview" |
||||
|
||||
config.autocomplete_max_suggestions = 6 |
||||
|
||||
local autocomplete = {} |
||||
autocomplete.map = {} |
||||
|
||||
|
||||
local mt = { __tostring = function(t) return t.text end } |
||||
|
||||
function autocomplete.add(t) |
||||
local items = {} |
||||
for text, info in pairs(t.items) do |
||||
info = (type(info) == "string") and info |
||||
table.insert(items, setmetatable({ text = text, info = info }, mt)) |
||||
end |
||||
autocomplete.map[t.name] = { files = t.files or ".*", items = items } |
||||
end |
||||
|
||||
|
||||
core.add_thread(function() |
||||
local cache = setmetatable({}, { __mode = "k" }) |
||||
|
||||
local function get_symbols(doc) |
||||
local i = 1 |
||||
local s = {} |
||||
while i < #doc.lines do |
||||
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do |
||||
s[sym] = true |
||||
end |
||||
i = i + 1 |
||||
if i % 100 == 0 then coroutine.yield() end |
||||
end |
||||
return s |
||||
end |
||||
|
||||
local function cache_is_valid(doc) |
||||
local c = cache[doc] |
||||
return c and c.last_change_id == doc:get_change_id() |
||||
end |
||||
|
||||
while true do |
||||
local symbols = {} |
||||
|
||||
-- lift all symbols from all docs |
||||
for _, doc in ipairs(core.docs) do |
||||
-- update the cache if the doc has changed since the last iteration |
||||
if not cache_is_valid(doc) then |
||||
cache[doc] = { |
||||
last_change_id = doc:get_change_id(), |
||||
symbols = get_symbols(doc) |
||||
} |
||||
end |
||||
-- update symbol set with doc's symbol set |
||||
for sym in pairs(cache[doc].symbols) do |
||||
symbols[sym] = true |
||||
end |
||||
coroutine.yield() |
||||
end |
||||
|
||||
-- update symbols list |
||||
autocomplete.add { name = "open-docs", items = symbols } |
||||
|
||||
-- wait for next scan |
||||
local valid = true |
||||
while valid do |
||||
coroutine.yield(1) |
||||
for _, doc in ipairs(core.docs) do |
||||
if not cache_is_valid(doc) then |
||||
valid = false |
||||
end |
||||
end |
||||
end |
||||
|
||||
end |
||||
end) |
||||
|
||||
|
||||
local partial = "" |
||||
local suggestions_idx = 1 |
||||
local suggestions = {} |
||||
local last_line, last_col |
||||
|
||||
|
||||
local function reset_suggestions() |
||||
suggestions_idx = 1 |
||||
suggestions = {} |
||||
end |
||||
|
||||
|
||||
local function update_suggestions() |
||||
local doc = core.active_view.doc |
||||
local filename = doc and doc.filename or "" |
||||
|
||||
-- get all relevant suggestions for given filename |
||||
local items = {} |
||||
for _, v in pairs(autocomplete.map) do |
||||
if common.match_pattern(filename, v.files) then |
||||
for _, item in pairs(v.items) do |
||||
table.insert(items, item) |
||||
end |
||||
end |
||||
end |
||||
|
||||
-- fuzzy match, remove duplicates and store |
||||
items = common.fuzzy_match(items, partial) |
||||
local j = 1 |
||||
for i = 1, config.autocomplete_max_suggestions do |
||||
suggestions[i] = items[j] |
||||
while items[j] and items[i].text == items[j].text do |
||||
items[i].info = items[i].info or items[j].info |
||||
j = j + 1 |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
local function get_partial_symbol() |
||||
local doc = core.active_view.doc |
||||
local line2, col2 = doc:get_selection() |
||||
local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word) |
||||
return doc:get_text(line1, col1, line2, col2) |
||||
end |
||||
|
||||
|
||||
local function get_active_view() |
||||
if getmetatable(core.active_view) == DocView then |
||||
return core.active_view |
||||
end |
||||
end |
||||
|
||||
|
||||
local function get_suggestions_rect(av) |
||||
if #suggestions == 0 then |
||||
return 0, 0, 0, 0 |
||||
end |
||||
|
||||
local line, col = av.doc:get_selection() |
||||
local x, y = av:get_line_screen_position(line) |
||||
x = x + av:get_col_x_offset(line, col - #partial) |
||||
y = y + av:get_line_height() + style.padding.y |
||||
local font = av:get_font() |
||||
local th = font:get_height() |
||||
|
||||
local max_width = 0 |
||||
for _, s in ipairs(suggestions) do |
||||
local w = font:get_width(s.text) |
||||
if s.info then |
||||
w = w + style.font:get_width(s.info) + style.padding.x |
||||
end |
||||
max_width = math.max(max_width, w) |
||||
end |
||||
|
||||
return |
||||
x - style.padding.x, |
||||
y - style.padding.y, |
||||
max_width + style.padding.x * 2, |
||||
#suggestions * (th + style.padding.y) + style.padding.y |
||||
end |
||||
|
||||
|
||||
local function draw_suggestions_box(av) |
||||
-- draw background rect |
||||
local rx, ry, rw, rh = get_suggestions_rect(av) |
||||
renderer.draw_rect(rx, ry, rw, rh, style.background3) |
||||
|
||||
-- draw text |
||||
local font = av:get_font() |
||||
local lh = font:get_height() + style.padding.y |
||||
local y = ry + style.padding.y / 2 |
||||
for i, s in ipairs(suggestions) do |
||||
local color = (i == suggestions_idx) and style.accent or style.text |
||||
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) |
||||
if s.info then |
||||
color = (i == suggestions_idx) and style.text or style.dim |
||||
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) |
||||
end |
||||
y = y + lh |
||||
end |
||||
end |
||||
|
||||
|
||||
-- patch event logic into RootView |
||||
local on_text_input = RootView.on_text_input |
||||
local update = RootView.update |
||||
local draw = RootView.draw |
||||
|
||||
|
||||
RootView.on_text_input = function(...) |
||||
on_text_input(...) |
||||
|
||||
local av = get_active_view() |
||||
if av then |
||||
-- update partial symbol and suggestions |
||||
partial = get_partial_symbol() |
||||
if #partial >= 3 then |
||||
update_suggestions() |
||||
last_line, last_col = av.doc:get_selection() |
||||
else |
||||
reset_suggestions() |
||||
end |
||||
|
||||
-- scroll if rect is out of bounds of view |
||||
local _, y, _, h = get_suggestions_rect(av) |
||||
local limit = av.position.y + av.size.y |
||||
if y + h > limit then |
||||
av.scroll.to.y = av.scroll.y + y + h - limit |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
RootView.update = function(...) |
||||
update(...) |
||||
|
||||
local av = get_active_view() |
||||
if av then |
||||
-- reset suggestions if caret was moved |
||||
local line, col = av.doc:get_selection() |
||||
if line ~= last_line or col ~= last_col then |
||||
reset_suggestions() |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
RootView.draw = function(...) |
||||
draw(...) |
||||
|
||||
local av = get_active_view() |
||||
if av then |
||||
-- draw suggestions box after everything else |
||||
core.root_view:defer_draw(draw_suggestions_box, av) |
||||
end |
||||
end |
||||
|
||||
|
||||
local function predicate() |
||||
return get_active_view() and #suggestions > 0 |
||||
end |
||||
|
||||
|
||||
command.add(predicate, { |
||||
["autocomplete:complete"] = function() |
||||
local doc = core.active_view.doc |
||||
local line, col = doc:get_selection() |
||||
local text = suggestions[suggestions_idx].text |
||||
doc:insert(line, col, text) |
||||
doc:remove(line, col, line, col - #partial) |
||||
doc:set_selection(line, col + #text - #partial) |
||||
reset_suggestions() |
||||
end, |
||||
|
||||
["autocomplete:previous"] = function() |
||||
suggestions_idx = math.max(suggestions_idx - 1, 1) |
||||
end, |
||||
|
||||
["autocomplete:next"] = function() |
||||
suggestions_idx = math.min(suggestions_idx + 1, #suggestions) |
||||
end, |
||||
|
||||
["autocomplete:cancel"] = function() |
||||
reset_suggestions() |
||||
end, |
||||
}) |
||||
|
||||
|
||||
keymap.add { |
||||
["tab"] = "autocomplete:complete", |
||||
["up"] = "autocomplete:previous", |
||||
["down"] = "autocomplete:next", |
||||
["escape"] = "autocomplete:cancel", |
||||
} |
||||
|
||||
|
||||
return autocomplete |
@ -0,0 +1,61 @@ |
||||
local core = require "core" |
||||
local config = require "core.config" |
||||
local Doc = require "core.doc" |
||||
|
||||
|
||||
local times = setmetatable({}, { __mode = "k" }) |
||||
|
||||
local function update_time(doc) |
||||
local info = system.get_file_info(doc.filename) |
||||
times[doc] = info.modified |
||||
end |
||||
|
||||
|
||||
local function reload_doc(doc) |
||||
local fp = io.open(doc.filename, "r") |
||||
local text = fp:read("*a") |
||||
fp:close() |
||||
|
||||
local sel = { doc:get_selection() } |
||||
doc:remove(1, 1, math.huge, math.huge) |
||||
doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", "")) |
||||
doc:set_selection(table.unpack(sel)) |
||||
|
||||
update_time(doc) |
||||
doc:clean() |
||||
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) |
||||
end |
||||
|
||||
|
||||
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() |
||||
end |
||||
|
||||
-- wait for next scan |
||||
coroutine.yield(config.project_scan_rate) |
||||
end |
||||
end) |
||||
|
||||
|
||||
-- patch `Doc.save|load` to store modified time |
||||
local load = Doc.load |
||||
local save = Doc.save |
||||
|
||||
Doc.load = function(self, ...) |
||||
local res = load(self, ...) |
||||
update_time(self) |
||||
return res |
||||
end |
||||
|
||||
Doc.save = function(self, ...) |
||||
local res = save(self, ...) |
||||
update_time(self) |
||||
return res |
||||
end |
@ -0,0 +1,59 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, |
||||
comment = "//", |
||||
patterns = { |
||||
{ pattern = "//.-\n", type = "comment" }, |
||||
{ pattern = { "/%*", "%*/" }, type = "comment" }, |
||||
{ pattern = { "#", "[^\\]\n" }, type = "comment" }, |
||||
{ pattern = { '"', '"', '\\' }, type = "string" }, |
||||
{ pattern = { "'", "'", '\\' }, type = "string" }, |
||||
{ pattern = "-?0x%x+", type = "number" }, |
||||
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" }, |
||||
{ pattern = "-?%.?%d+f?", type = "number" }, |
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, |
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, |
||||
{ pattern = "[%a_][%w_]*", type = "symbol" }, |
||||
}, |
||||
symbols = { |
||||
["if"] = "keyword", |
||||
["then"] = "keyword", |
||||
["else"] = "keyword", |
||||
["elseif"] = "keyword", |
||||
["do"] = "keyword", |
||||
["while"] = "keyword", |
||||
["for"] = "keyword", |
||||
["break"] = "keyword", |
||||
["continue"] = "keyword", |
||||
["return"] = "keyword", |
||||
["goto"] = "keyword", |
||||
["struct"] = "keyword", |
||||
["union"] = "keyword", |
||||
["typedef"] = "keyword", |
||||
["enum"] = "keyword", |
||||
["extern"] = "keyword", |
||||
["static"] = "keyword", |
||||
["volatile"] = "keyword", |
||||
["const"] = "keyword", |
||||
["inline"] = "keyword", |
||||
["switch"] = "keyword", |
||||
["case"] = "keyword", |
||||
["default"] = "keyword", |
||||
["auto"] = "keyword", |
||||
["const"] = "keyword", |
||||
["void"] = "keyword", |
||||
["int"] = "keyword2", |
||||
["short"] = "keyword2", |
||||
["long"] = "keyword2", |
||||
["float"] = "keyword2", |
||||
["double"] = "keyword2", |
||||
["char"] = "keyword2", |
||||
["unsigned"] = "keyword2", |
||||
["bool"] = "keyword2", |
||||
["true"] = "literal", |
||||
["false"] = "literal", |
||||
["NULL"] = "literal", |
||||
}, |
||||
} |
||||
|
@ -0,0 +1,23 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.css$" }, |
||||
patterns = { |
||||
{ pattern = "\\.", type = "normal" }, |
||||
{ pattern = "//.-\n", type = "comment" }, |
||||
{ pattern = { "/%*", "%*/" }, type = "comment" }, |
||||
{ pattern = { '"', '"', '\\' }, type = "string" }, |
||||
{ pattern = { "'", "'", '\\' }, type = "string" }, |
||||
{ pattern = "[%a][%w-]*%s*%f[:]", type = "keyword" }, |
||||
{ pattern = "#%x+", type = "string" }, |
||||
{ pattern = "-?%d+[%d%.]*p[xt]", type = "number" }, |
||||
{ pattern = "-?%d+[%d%.]*deg", type = "number" }, |
||||
{ pattern = "-?%d+[%d%.]*", type = "number" }, |
||||
{ pattern = "[%a_][%w_]*", type = "symbol" }, |
||||
{ pattern = "#[%a][%w_-]*", type = "keyword2" }, |
||||
{ pattern = "@[%a][%w_-]*", type = "keyword2" }, |
||||
{ pattern = "%.[%a][%w_-]*", type = "keyword2" }, |
||||
{ pattern = "[{}:]", type = "operator" }, |
||||
}, |
||||
symbols = {}, |
||||
} |
@ -0,0 +1,67 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.js$", "%.json$", "%.cson$" }, |
||||
comment = "//", |
||||
patterns = { |
||||
{ pattern = "//.-\n", type = "comment" }, |
||||
{ pattern = { "/%*", "%*/" }, type = "comment" }, |
||||
{ pattern = { '"', '"', '\\' }, type = "string" }, |
||||
{ pattern = { "'", "'", '\\' }, 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" }, |
||||
}, |
||||
symbols = { |
||||
["async"] = "keyword", |
||||
["await"] = "keyword", |
||||
["break"] = "keyword", |
||||
["case"] = "keyword", |
||||
["catch"] = "keyword", |
||||
["class"] = "keyword", |
||||
["const"] = "keyword", |
||||
["continue"] = "keyword", |
||||
["debugger"] = "keyword", |
||||
["default"] = "keyword", |
||||
["delete"] = "keyword", |
||||
["do"] = "keyword", |
||||
["else"] = "keyword", |
||||
["export"] = "keyword", |
||||
["extends"] = "keyword", |
||||
["finally"] = "keyword", |
||||
["for"] = "keyword", |
||||
["function"] = "keyword", |
||||
["get"] = "keyword", |
||||
["if"] = "keyword", |
||||
["import"] = "keyword", |
||||
["in"] = "keyword", |
||||
["instanceof"] = "keyword", |
||||
["let"] = "keyword", |
||||
["new"] = "keyword", |
||||
["return"] = "keyword", |
||||
["set"] = "keyword", |
||||
["static"] = "keyword", |
||||
["super"] = "keyword", |
||||
["switch"] = "keyword", |
||||
["throw"] = "keyword", |
||||
["try"] = "keyword", |
||||
["typeof"] = "keyword", |
||||
["var"] = "keyword", |
||||
["void"] = "keyword", |
||||
["while"] = "keyword", |
||||
["with"] = "keyword", |
||||
["yield"] = "keyword", |
||||
["true"] = "literal", |
||||
["false"] = "literal", |
||||
["null"] = "literal", |
||||
["undefined"] = "literal", |
||||
["arguments"] = "keyword2", |
||||
["Infinity"] = "keyword2", |
||||
["NaN"] = "keyword2", |
||||
["this"] = "keyword2", |
||||
}, |
||||
} |
@ -0,0 +1,50 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = "%.lua$", |
||||
headers = "^#!.*[ /]lua", |
||||
comment = "--", |
||||
patterns = { |
||||
{ pattern = { '"', '"', '\\' }, type = "string" }, |
||||
{ pattern = { "'", "'", '\\' }, type = "string" }, |
||||
{ pattern = { "%[%[", "%]%]" }, type = "string" }, |
||||
{ pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, |
||||
{ pattern = "%-%-.-\n", type = "comment" }, |
||||
{ pattern = "-?0x%x+", type = "number" }, |
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" }, |
||||
{ pattern = "-?%.?%d+", type = "number" }, |
||||
{ pattern = "<%a+>", type = "keyword2" }, |
||||
{ pattern = "%.%.%.?", type = "operator" }, |
||||
{ pattern = "[<>~=]=", type = "operator" }, |
||||
{ pattern = "[%+%-=/%*%^%%#<>]", type = "operator" }, |
||||
{ pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" }, |
||||
{ pattern = "[%a_][%w_]*", type = "symbol" }, |
||||
{ pattern = "::[%a_][%w_]*::", type = "function" }, |
||||
}, |
||||
symbols = { |
||||
["if"] = "keyword", |
||||
["then"] = "keyword", |
||||
["else"] = "keyword", |
||||
["elseif"] = "keyword", |
||||
["end"] = "keyword", |
||||
["do"] = "keyword", |
||||
["function"] = "keyword", |
||||
["repeat"] = "keyword", |
||||
["until"] = "keyword", |
||||
["while"] = "keyword", |
||||
["for"] = "keyword", |
||||
["break"] = "keyword", |
||||
["return"] = "keyword", |
||||
["local"] = "keyword", |
||||
["in"] = "keyword", |
||||
["not"] = "keyword", |
||||
["and"] = "keyword", |
||||
["or"] = "keyword", |
||||
["goto"] = "keyword", |
||||
["self"] = "keyword2", |
||||
["true"] = "literal", |
||||
["false"] = "literal", |
||||
["nil"] = "literal", |
||||
}, |
||||
} |
||||
|
@ -0,0 +1,17 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "Makefile", "makefile", "%.mk$" }, |
||||
comment = "#", |
||||
patterns = { |
||||
{ pattern = "#.*\n", type = "comment" }, |
||||
{ pattern = [[\.]], type = "normal" }, |
||||
{ pattern = "$[@^<%%?+|*]", type = "keyword2" }, |
||||
{ pattern = "$%(.-%)", type = "variable" }, |
||||
{ pattern = "%f[%w_][%d%.]+%f[^%w_]", type = "number" }, |
||||
{ pattern = "%..*:", type = "keyword2" }, |
||||
{ pattern = ".*:", type = "function" }, |
||||
}, |
||||
symbols = { |
||||
}, |
||||
} |
@ -0,0 +1,21 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.md$", "%.markdown$" }, |
||||
patterns = { |
||||
{ pattern = "\\.", type = "normal" }, |
||||
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" }, |
||||
{ pattern = { "```", "```" }, type = "string" }, |
||||
{ pattern = { "``", "``", "\\" }, type = "string" }, |
||||
{ pattern = { "`", "`", "\\" }, type = "string" }, |
||||
{ pattern = { "~~", "~~", "\\" }, type = "keyword2" }, |
||||
{ pattern = "%-%-%-+", type = "comment" }, |
||||
{ pattern = "%*%s+", type = "operator" }, |
||||
{ pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, |
||||
{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, |
||||
{ pattern = "#.-\n", type = "keyword" }, |
||||
{ pattern = "!?%[.-%]%(.-%)", type = "function" }, |
||||
{ pattern = "https?://%S+", type = "function" }, |
||||
}, |
||||
symbols = { }, |
||||
} |
@ -0,0 +1,55 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.py$", "%.pyw$" }, |
||||
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" }, |
||||
}, |
||||
symbols = { |
||||
["class"] = "keyword", |
||||
["finally"] = "keyword", |
||||
["is"] = "keyword", |
||||
["return"] = "keyword", |
||||
["continue"] = "keyword", |
||||
["for"] = "keyword", |
||||
["lambda"] = "keyword", |
||||
["try"] = "keyword", |
||||
["def"] = "keyword", |
||||
["from"] = "keyword", |
||||
["nonlocal"] = "keyword", |
||||
["while"] = "keyword", |
||||
["and"] = "keyword", |
||||
["global"] = "keyword", |
||||
["not"] = "keyword", |
||||
["with"] = "keyword", |
||||
["as"] = "keyword", |
||||
["elif"] = "keyword", |
||||
["if"] = "keyword", |
||||
["or"] = "keyword", |
||||
["else"] = "keyword", |
||||
["import"] = "keyword", |
||||
["pass"] = "keyword", |
||||
["break"] = "keyword", |
||||
["except"] = "keyword", |
||||
["in"] = "keyword", |
||||
["del"] = "keyword", |
||||
["raise"] = "keyword", |
||||
["yield"] = "keyword", |
||||
["assert"] = "keyword", |
||||
["self"] = "keyword2", |
||||
["None"] = "literal", |
||||
["True"] = "literal", |
||||
["False"] = "literal", |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
local syntax = require "core.syntax" |
||||
|
||||
syntax.add { |
||||
files = { "%.xml$", "%.html?$" }, |
||||
headers = "<%?xml", |
||||
patterns = { |
||||
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" }, |
||||
{ pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" }, |
||||
{ pattern = { '"', '"', '\\' }, type = "string" }, |
||||
{ pattern = { "'", "'", '\\' }, type = "string" }, |
||||
{ pattern = "0x[%da-fA-F]+", type = "number" }, |
||||
{ pattern = "-?%d+[%d%.]*f?", type = "number" }, |
||||
{ pattern = "-?%.?%d+f?", type = "number" }, |
||||
{ pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" }, |
||||
{ pattern = "%f[^<][%a_][%w_]*", type = "function" }, |
||||
{ pattern = "%f[^<]/[%a_][%w_]*", type = "function" }, |
||||
{ pattern = "[%a_][%w_]*", type = "keyword" }, |
||||
{ pattern = "[/<>=]", type = "operator" }, |
||||
}, |
||||
symbols = {}, |
||||
} |
@ -0,0 +1,69 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local keymap = require "core.keymap" |
||||
|
||||
local handled_events = { |
||||
["keypressed"] = true, |
||||
["keyreleased"] = true, |
||||
["textinput"] = true, |
||||
} |
||||
|
||||
local state = "stopped" |
||||
local event_buffer = {} |
||||
local modkeys = {} |
||||
|
||||
local on_event = core.on_event |
||||
|
||||
core.on_event = function(type, ...) |
||||
local res = on_event(type, ...) |
||||
if state == "recording" and handled_events[type] then |
||||
table.insert(event_buffer, { type, ... }) |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function clone(t) |
||||
local res = {} |
||||
for k, v in pairs(t) do res[k] = v end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function predicate() |
||||
return state ~= "playing" |
||||
end |
||||
|
||||
|
||||
command.add(predicate, { |
||||
["macro:toggle-record"] = function() |
||||
if state == "stopped" then |
||||
state = "recording" |
||||
event_buffer = {} |
||||
modkeys = clone(keymap.modkeys) |
||||
core.log("Recording macro...") |
||||
else |
||||
state = "stopped" |
||||
core.log("Stopped recording macro (%d events)", #event_buffer) |
||||
end |
||||
end, |
||||
|
||||
["macro:play"] = function() |
||||
state = "playing" |
||||
core.log("Playing macro... (%d events)", #event_buffer) |
||||
local mk = keymap.modkeys |
||||
keymap.modkeys = clone(modkeys) |
||||
for _, ev in ipairs(event_buffer) do |
||||
on_event(table.unpack(ev)) |
||||
core.root_view:update() |
||||
end |
||||
keymap.modkeys = mk |
||||
state = "stopped" |
||||
end, |
||||
}) |
||||
|
||||
|
||||
keymap.add { |
||||
["ctrl+shift+;"] = "macro:toggle-record", |
||||
["ctrl+;"] = "macro:play", |
||||
} |
@ -0,0 +1,271 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local keymap = require "core.keymap" |
||||
local command = require "core.command" |
||||
local style = require "core.style" |
||||
local View = require "core.view" |
||||
|
||||
|
||||
local ResultsView = View:extend() |
||||
|
||||
|
||||
function ResultsView:new(text, fn) |
||||
ResultsView.super.new(self) |
||||
self.scrollable = true |
||||
self.brightness = 0 |
||||
self:begin_search(text, fn) |
||||
end |
||||
|
||||
|
||||
function ResultsView:get_name() |
||||
return "Search Results" |
||||
end |
||||
|
||||
|
||||
local function find_all_matches_in_file(t, filename, fn) |
||||
local fp = io.open(filename) |
||||
if not fp then return t end |
||||
local n = 1 |
||||
for line in fp:lines() do |
||||
local s = fn(line) |
||||
if s then |
||||
table.insert(t, { file = filename, text = line, line = n, col = s }) |
||||
core.redraw = true |
||||
end |
||||
if n % 100 == 0 then coroutine.yield() end |
||||
n = n + 1 |
||||
core.redraw = true |
||||
end |
||||
fp:close() |
||||
end |
||||
|
||||
|
||||
function ResultsView:begin_search(text, fn) |
||||
self.search_args = { text, fn } |
||||
self.results = {} |
||||
self.last_file_idx = 1 |
||||
self.query = text |
||||
self.searching = true |
||||
self.selected_idx = 0 |
||||
|
||||
core.add_thread(function() |
||||
for i, file in ipairs(core.project_files) do |
||||
if file.type == "file" then |
||||
find_all_matches_in_file(self.results, file.filename, fn) |
||||
end |
||||
self.last_file_idx = i |
||||
end |
||||
self.searching = false |
||||
self.brightness = 100 |
||||
core.redraw = true |
||||
end, self.results) |
||||
|
||||
self.scroll.to.y = 0 |
||||
end |
||||
|
||||
|
||||
function ResultsView:refresh() |
||||
self:begin_search(table.unpack(self.search_args)) |
||||
end |
||||
|
||||
|
||||
function ResultsView:on_mouse_moved(mx, my, ...) |
||||
ResultsView.super.on_mouse_moved(self, mx, my, ...) |
||||
self.selected_idx = 0 |
||||
for i, item, x,y,w,h in self:each_visible_result() do |
||||
if mx >= x and my >= y and mx < x + w and my < y + h then |
||||
self.selected_idx = i |
||||
break |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function ResultsView:on_mouse_pressed(...) |
||||
local caught = ResultsView.super.on_mouse_pressed(self, ...) |
||||
if not caught then |
||||
self:open_selected_result() |
||||
end |
||||
end |
||||
|
||||
|
||||
function ResultsView:open_selected_result() |
||||
local res = self.results[self.selected_idx] |
||||
if not res then |
||||
return |
||||
end |
||||
core.try(function() |
||||
local dv = core.root_view:open_doc(core.open_doc(res.file)) |
||||
core.root_view.root_node:update_layout() |
||||
dv.doc:set_selection(res.line, res.col) |
||||
dv:scroll_to_line(res.line, false, true) |
||||
end) |
||||
end |
||||
|
||||
|
||||
function ResultsView:update() |
||||
self:move_towards("brightness", 0, 0.1) |
||||
ResultsView.super.update(self) |
||||
end |
||||
|
||||
|
||||
function ResultsView:get_results_yoffset() |
||||
return style.font:get_height() + style.padding.y * 3 |
||||
end |
||||
|
||||
|
||||
function ResultsView:get_line_height() |
||||
return style.padding.y + style.font:get_height() |
||||
end |
||||
|
||||
|
||||
function ResultsView:get_scrollable_size() |
||||
return self:get_results_yoffset() + #self.results * self:get_line_height() |
||||
end |
||||
|
||||
|
||||
function ResultsView:get_visible_results_range() |
||||
local lh = self:get_line_height() |
||||
local oy = self:get_results_yoffset() |
||||
local min = math.max(1, math.floor((self.scroll.y - oy) / lh)) |
||||
return min, min + math.floor(self.size.y / lh) + 1 |
||||
end |
||||
|
||||
|
||||
function ResultsView:each_visible_result() |
||||
return coroutine.wrap(function() |
||||
local lh = self:get_line_height() |
||||
local x, y = self:get_content_offset() |
||||
local min, max = self:get_visible_results_range() |
||||
y = y + self:get_results_yoffset() + lh * (min - 1) |
||||
for i = min, max do |
||||
local item = self.results[i] |
||||
if not item then break end |
||||
coroutine.yield(i, item, x, y, self.size.x, lh) |
||||
y = y + lh |
||||
end |
||||
end) |
||||
end |
||||
|
||||
|
||||
function ResultsView:scroll_to_make_selected_visible() |
||||
local h = self:get_line_height() |
||||
local y = self:get_results_yoffset() + h * (self.selected_idx - 1) |
||||
self.scroll.to.y = math.min(self.scroll.to.y, y) |
||||
self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y) |
||||
end |
||||
|
||||
|
||||
function ResultsView:draw() |
||||
self:draw_background(style.background) |
||||
|
||||
-- status |
||||
local ox, oy = self:get_content_offset() |
||||
local x, y = ox + style.padding.x, oy + style.padding.y |
||||
local per = self.last_file_idx / #core.project_files |
||||
local text |
||||
if self.searching then |
||||
text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", |
||||
per * 100, self.last_file_idx, #core.project_files, |
||||
#self.results, self.query) |
||||
else |
||||
text = string.format("Found %d matches for %q", |
||||
#self.results, self.query) |
||||
end |
||||
local color = common.lerp(style.text, style.accent, self.brightness / 100) |
||||
renderer.draw_text(style.font, text, x, y, color) |
||||
|
||||
-- horizontal line |
||||
local yoffset = self:get_results_yoffset() |
||||
local x = ox + style.padding.x |
||||
local w = self.size.x - style.padding.x * 2 |
||||
local h = style.divider_size |
||||
local color = common.lerp(style.dim, style.text, self.brightness / 100) |
||||
renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, color) |
||||
if self.searching then |
||||
renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text) |
||||
end |
||||
|
||||
-- results |
||||
local y1, y2 = self.position.y, self.position.y + self.size.y |
||||
for i, item, x,y,w,h in self:each_visible_result() do |
||||
local color = style.text |
||||
if i == self.selected_idx then |
||||
color = style.accent |
||||
renderer.draw_rect(x, y, w, h, style.line_highlight) |
||||
end |
||||
x = x + style.padding.x |
||||
local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col) |
||||
x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h) |
||||
x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h) |
||||
end |
||||
|
||||
self:draw_scrollbar() |
||||
end |
||||
|
||||
|
||||
local function begin_search(text, fn) |
||||
if text == "" then |
||||
core.error("Expected non-empty string") |
||||
return |
||||
end |
||||
local rv = ResultsView(text, fn) |
||||
core.root_view:get_active_node():add_view(rv) |
||||
end |
||||
|
||||
|
||||
command.add(nil, { |
||||
["project-search:find"] = function() |
||||
core.command_view:enter("Find Text In Project", function(text) |
||||
text = text:lower() |
||||
begin_search(text, function(line_text) |
||||
return line_text:lower():find(text, nil, true) |
||||
end) |
||||
end) |
||||
end, |
||||
|
||||
["project-search:find-pattern"] = function() |
||||
core.command_view:enter("Find Pattern In Project", function(text) |
||||
begin_search(text, function(line_text) return line_text:find(text) end) |
||||
end) |
||||
end, |
||||
|
||||
["project-search:fuzzy-find"] = function() |
||||
core.command_view:enter("Fuzzy Find Text In Project", function(text) |
||||
begin_search(text, function(line_text) |
||||
return common.fuzzy_match(line_text, text) and 1 |
||||
end) |
||||
end) |
||||
end, |
||||
}) |
||||
|
||||
|
||||
command.add(ResultsView, { |
||||
["project-search:select-previous"] = function() |
||||
local view = core.active_view |
||||
view.selected_idx = math.max(view.selected_idx - 1, 1) |
||||
view:scroll_to_make_selected_visible() |
||||
end, |
||||
|
||||
["project-search:select-next"] = function() |
||||
local view = core.active_view |
||||
view.selected_idx = math.min(view.selected_idx + 1, #view.results) |
||||
view:scroll_to_make_selected_visible() |
||||
end, |
||||
|
||||
["project-search:open-selected"] = function() |
||||
core.active_view:open_selected_result() |
||||
end, |
||||
|
||||
["project-search:refresh"] = function() |
||||
core.active_view:refresh() |
||||
end, |
||||
}) |
||||
|
||||
keymap.add { |
||||
["f5"] = "project-search:refresh", |
||||
["ctrl+shift+f"] = "project-search:find", |
||||
["up"] = "project-search:select-previous", |
||||
["down"] = "project-search:select-next", |
||||
["return"] = "project-search:open-selected", |
||||
} |
@ -0,0 +1,30 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local keymap = require "core.keymap" |
||||
|
||||
|
||||
local escapes = { |
||||
["\\"] = "\\\\", |
||||
["\""] = "\\\"", |
||||
["\n"] = "\\n", |
||||
["\r"] = "\\r", |
||||
["\t"] = "\\t", |
||||
["\b"] = "\\b", |
||||
} |
||||
|
||||
local function replace(chr) |
||||
return escapes[chr] or string.format("\\x%02x", chr:byte()) |
||||
end |
||||
|
||||
|
||||
command.add("core.docview", { |
||||
["quote:quote"] = function() |
||||
core.active_view.doc:replace(function(text) |
||||
return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"' |
||||
end) |
||||
end, |
||||
}) |
||||
|
||||
keymap.add { |
||||
["ctrl+'"] = "quote:quote", |
||||
} |
@ -0,0 +1,63 @@ |
||||
local core = require "core" |
||||
local config = require "core.config" |
||||
local command = require "core.command" |
||||
local keymap = require "core.keymap" |
||||
|
||||
|
||||
local function wordwrap_text(text, limit) |
||||
local t = {} |
||||
local n = 0 |
||||
|
||||
for word in text:gmatch("%S+") do |
||||
if n + #word > limit then |
||||
table.insert(t, "\n") |
||||
n = 0 |
||||
elseif #t > 0 then |
||||
table.insert(t, " ") |
||||
end |
||||
table.insert(t, word) |
||||
n = n + #word + 1 |
||||
end |
||||
|
||||
return table.concat(t) |
||||
end |
||||
|
||||
|
||||
command.add("core.docview", { |
||||
["reflow:reflow"] = function() |
||||
local doc = core.active_view.doc |
||||
doc:replace(function(text) |
||||
local prefix_set = "[^%w\n%[%](){}`'\"]*" |
||||
|
||||
-- get line prefix and trailing whitespace |
||||
local prefix1 = text:match("^\n*" .. prefix_set) |
||||
local prefix2 = text:match("\n(" .. prefix_set .. ")", #prefix1+1) |
||||
local trailing = text:match("%s*$") |
||||
if not prefix2 or prefix2 == "" then |
||||
prefix2 = prefix1 |
||||
end |
||||
|
||||
-- strip all line prefixes and trailing whitespace |
||||
text = text:sub(#prefix1+1, -#trailing - 1):gsub("\n" .. prefix_set, "\n") |
||||
|
||||
-- split into blocks, wordwrap and join |
||||
local line_limit = config.line_limit - #prefix1 |
||||
local blocks = {} |
||||
text = text:gsub("\n\n", "\0") |
||||
for block in text:gmatch("%Z+") do |
||||
table.insert(blocks, wordwrap_text(block, line_limit)) |
||||
end |
||||
text = table.concat(blocks, "\n\n") |
||||
|
||||
-- add prefix to start of lines |
||||
text = prefix1 .. text:gsub("\n", "\n" .. prefix2) .. trailing |
||||
|
||||
return text |
||||
end) |
||||
end, |
||||
}) |
||||
|
||||
|
||||
keymap.add { |
||||
["ctrl+shift+q"] = "reflow:reflow" |
||||
} |
@ -0,0 +1,60 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local translate = require "core.doc.translate" |
||||
|
||||
|
||||
local function gmatch_to_array(text, ptn) |
||||
local res = {} |
||||
for x in text:gmatch(ptn) do |
||||
table.insert(res, x) |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function tabularize_lines(lines, delim) |
||||
local rows = {} |
||||
local cols = {} |
||||
|
||||
-- split lines at delimiters and get maximum width of columns |
||||
local ptn = "[^" .. delim:sub(1,1):gsub("%W", "%%%1") .. "]+" |
||||
for i, line in ipairs(lines) do |
||||
rows[i] = gmatch_to_array(line, ptn) |
||||
for j, col in ipairs(rows[i]) do |
||||
cols[j] = math.max(#col, cols[j] or 0) |
||||
end |
||||
end |
||||
|
||||
-- pad columns with space |
||||
for _, row in ipairs(rows) do |
||||
for i = 1, #row - 1 do |
||||
row[i] = row[i] .. string.rep(" ", cols[i] - #row[i]) |
||||
end |
||||
end |
||||
|
||||
-- write columns back to lines array |
||||
for i, line in ipairs(lines) do |
||||
lines[i] = table.concat(rows[i], delim) |
||||
end |
||||
end |
||||
|
||||
|
||||
command.add("core.docview", { |
||||
["tabularize:tabularize"] = function() |
||||
core.command_view:enter("Tabularize On Delimiter", function(delim) |
||||
if delim == "" then delim = " " end |
||||
|
||||
local doc = core.active_view.doc |
||||
local line1, col1, line2, col2, swap = doc:get_selection(true) |
||||
line1, col1 = doc:position_offset(line1, col1, translate.start_of_line) |
||||
line2, col2 = doc:position_offset(line2, col2, translate.end_of_line) |
||||
doc:set_selection(line1, col1, line2, col2, swap) |
||||
|
||||
doc:replace(function(text) |
||||
local lines = gmatch_to_array(text, "[^\n]*\n?") |
||||
tabularize_lines(lines, delim) |
||||
return table.concat(lines) |
||||
end) |
||||
end) |
||||
end, |
||||
}) |
@ -0,0 +1,197 @@ |
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local command = require "core.command" |
||||
local config = require "core.config" |
||||
local keymap = require "core.keymap" |
||||
local style = require "core.style" |
||||
local View = require "core.view" |
||||
|
||||
config.treeview_size = 200 * SCALE |
||||
|
||||
local function get_depth(filename) |
||||
local n = 0 |
||||
for sep in filename:gmatch("[\\/]") do |
||||
n = n + 1 |
||||
end |
||||
return n |
||||
end |
||||
|
||||
|
||||
local TreeView = View:extend() |
||||
|
||||
function TreeView:new() |
||||
TreeView.super.new(self) |
||||
self.scrollable = true |
||||
self.visible = true |
||||
self.init_size = true |
||||
self.cache = {} |
||||
end |
||||
|
||||
|
||||
function TreeView:get_cached(item) |
||||
local t = self.cache[item.filename] |
||||
if not t then |
||||
t = {} |
||||
t.filename = item.filename |
||||
t.abs_filename = system.absolute_path(item.filename) |
||||
t.name = t.filename:match("[^\\/]+$") |
||||
t.depth = get_depth(t.filename) |
||||
t.type = item.type |
||||
self.cache[t.filename] = t |
||||
end |
||||
return t |
||||
end |
||||
|
||||
|
||||
function TreeView:get_name() |
||||
return "Project" |
||||
end |
||||
|
||||
|
||||
function TreeView:get_item_height() |
||||
return style.font:get_height() + style.padding.y |
||||
end |
||||
|
||||
|
||||
function TreeView:check_cache() |
||||
-- invalidate cache's skip values if project_files has changed |
||||
if core.project_files ~= self.last_project_files then |
||||
for _, v in pairs(self.cache) do |
||||
v.skip = nil |
||||
end |
||||
self.last_project_files = core.project_files |
||||
end |
||||
end |
||||
|
||||
|
||||
function TreeView:each_item() |
||||
return coroutine.wrap(function() |
||||
self:check_cache() |
||||
local ox, oy = self:get_content_offset() |
||||
local y = oy + style.padding.y |
||||
local w = self.size.x |
||||
local h = self:get_item_height() |
||||
|
||||
local i = 1 |
||||
while i <= #core.project_files do |
||||
local item = core.project_files[i] |
||||
local cached = self:get_cached(item) |
||||
|
||||
coroutine.yield(cached, ox, y, w, h) |
||||
y = y + h |
||||
i = i + 1 |
||||
|
||||
if not cached.expanded then |
||||
if cached.skip then |
||||
i = cached.skip |
||||
else |
||||
local depth = cached.depth |
||||
while i <= #core.project_files do |
||||
local filename = core.project_files[i].filename |
||||
if get_depth(filename) <= depth then break end |
||||
i = i + 1 |
||||
end |
||||
cached.skip = i |
||||
end |
||||
end |
||||
end |
||||
end) |
||||
end |
||||
|
||||
|
||||
function TreeView:on_mouse_moved(px, py) |
||||
self.hovered_item = nil |
||||
for item, x,y,w,h in self:each_item() do |
||||
if px > x and py > y and px <= x + w and py <= y + h then |
||||
self.hovered_item = item |
||||
break |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
function TreeView:on_mouse_pressed(button, x, y) |
||||
if not self.hovered_item then |
||||
return |
||||
elseif self.hovered_item.type == "dir" then |
||||
self.hovered_item.expanded = not self.hovered_item.expanded |
||||
else |
||||
core.try(function() |
||||
core.root_view:open_doc(core.open_doc(self.hovered_item.filename)) |
||||
end) |
||||
end |
||||
end |
||||
|
||||
|
||||
function TreeView:update() |
||||
-- update width |
||||
local dest = self.visible and config.treeview_size or 0 |
||||
if self.init_size then |
||||
self.size.x = dest |
||||
self.init_size = false |
||||
else |
||||
self:move_towards(self.size, "x", dest) |
||||
end |
||||
|
||||
TreeView.super.update(self) |
||||
end |
||||
|
||||
|
||||
function TreeView:draw() |
||||
self:draw_background(style.background2) |
||||
|
||||
local icon_width = style.icon_font:get_width("D") |
||||
local spacing = style.font:get_width(" ") * 2 |
||||
|
||||
local doc = core.active_view.doc |
||||
local active_filename = doc and system.absolute_path(doc.filename or "") |
||||
|
||||
for item, x,y,w,h in self:each_item() do |
||||
local color = style.text |
||||
|
||||
-- highlight active_view doc |
||||
if item.abs_filename == active_filename then |
||||
color = style.accent |
||||
end |
||||
|
||||
-- hovered item background |
||||
if item == self.hovered_item then |
||||
renderer.draw_rect(x, y, w, h, style.line_highlight) |
||||
color = style.accent |
||||
end |
||||
|
||||
-- icons |
||||
x = x + item.depth * style.padding.x + style.padding.x |
||||
if item.type == "dir" then |
||||
local icon1 = item.expanded and "-" or "+" |
||||
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) |
||||
x = x + icon_width |
||||
else |
||||
x = x + style.padding.x |
||||
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h) |
||||
x = x + icon_width |
||||
end |
||||
|
||||
-- text |
||||
x = x + spacing |
||||
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h) |
||||
end |
||||
end |
||||
|
||||
|
||||
-- init |
||||
local view = TreeView() |
||||
local node = core.root_view:get_active_node() |
||||
node:split("left", view, true) |
||||
|
||||
-- register commands and keymap |
||||
command.add(nil, { |
||||
["treeview:toggle"] = function() |
||||
view.visible = not view.visible |
||||
end, |
||||
}) |
||||
|
||||
keymap.add { ["ctrl+\\"] = "treeview:toggle" } |
@ -0,0 +1,36 @@ |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
local Doc = require "core.doc" |
||||
|
||||
|
||||
local function trim_trailing_whitespace(doc) |
||||
local cline, ccol = doc:get_selection() |
||||
for i = 1, #doc.lines do |
||||
local old_text = doc:get_text(i, 1, i, math.huge) |
||||
local new_text = old_text:gsub("%s*$", "") |
||||
|
||||
-- don't remove whitespace which would cause the caret to reposition |
||||
if cline == i and ccol > #new_text then |
||||
new_text = old_text:sub(1, ccol - 1) |
||||
end |
||||
|
||||
if old_text ~= new_text then |
||||
doc:insert(i, 1, new_text) |
||||
doc:remove(i, #new_text + 1, i, math.huge) |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
command.add("core.docview", { |
||||
["trim-whitespace:trim-trailing-whitespace"] = function() |
||||
trim_trailing_whitespace(core.active_view.doc) |
||||
end, |
||||
}) |
||||
|
||||
|
||||
local save = Doc.save |
||||
Doc.save = function(self, ...) |
||||
trim_trailing_whitespace(self) |
||||
save(self, ...) |
||||
end |
@ -0,0 +1,28 @@ |
||||
local style = require "core.style" |
||||
local common = require "core.common" |
||||
|
||||
style.background = { common.color "#343233" } |
||||
style.background2 = { common.color "#2c2a2b" } |
||||
style.background3 = { common.color "#2c2a2b" } |
||||
style.text = { common.color "#c4b398" } |
||||
style.caret = { common.color "#61efce" } |
||||
style.accent = { common.color "#ffd152" } |
||||
style.dim = { common.color "#615d5f" } |
||||
style.divider = { common.color "#242223" } |
||||
style.selection = { common.color "#454244" } |
||||
style.line_number = { common.color "#454244" } |
||||
style.line_number2 = { common.color "#615d5f" } |
||||
style.line_highlight = { common.color "#383637" } |
||||
style.scrollbar = { common.color "#454344" } |
||||
style.scrollbar2 = { common.color "#524F50" } |
||||
|
||||
style.syntax["normal"] = { common.color "#efdab9" } |
||||
style.syntax["symbol"] = { common.color "#efdab9" } |
||||
style.syntax["comment"] = { common.color "#615d5f" } |
||||
style.syntax["keyword"] = { common.color "#d36e2d" } |
||||
style.syntax["keyword2"] = { common.color "#ef6179" } |
||||
style.syntax["number"] = { common.color "#ffd152" } |
||||
style.syntax["literal"] = { common.color "#ffd152" } |
||||
style.syntax["string"] = { common.color "#ffd152" } |
||||
style.syntax["operator"] = { common.color "#efdab9" } |
||||
style.syntax["function"] = { common.color "#61efce" } |
@ -0,0 +1,28 @@ |
||||
local style = require "core.style" |
||||
local common = require "core.common" |
||||
|
||||
style.background = { common.color "#282828" } |
||||
style.background2 = { common.color "#1d2021" } |
||||
style.background3 = { common.color "#1d2021" } |
||||
style.text = { common.color "#928374" } |
||||
style.caret = { common.color "#fbf1c7" } |
||||
style.accent = { common.color "#ebdbb2" } |
||||
style.dim = { common.color "#928374" } |
||||
style.divider = { common.color "#1d2021" } |
||||
style.selection = { common.color "#3c3836" } |
||||
style.line_number = { common.color "#928374" } |
||||
style.line_number2 = { common.color "#ebdbb2" } |
||||
style.line_highlight = { common.color "#32302f" } |
||||
style.scrollbar = { common.color "#928374" } |
||||
style.scrollbar2 = { common.color "#fbf1c7" } |
||||
|
||||
style.syntax["normal"] = { common.color "#ebdbb2" } |
||||
style.syntax["symbol"] = { common.color "#ebdbb2" } |
||||
style.syntax["comment"] = { common.color "#928374" } |
||||
style.syntax["keyword"] = { common.color "#fb4934" } |
||||
style.syntax["keyword2"] = { common.color "#83a598" } |
||||
style.syntax["number"] = { common.color "#d3869b" } |
||||
style.syntax["literal"] = { common.color "#d3869b" } |
||||
style.syntax["string"] = { common.color "#b8bb26" } |
||||
style.syntax["operator"] = { common.color "#ebdbb2" } |
||||
style.syntax["function"] = { common.color "#8ec07c" } |
@ -0,0 +1,28 @@ |
||||
local style = require "core.style" |
||||
local common = require "core.common" |
||||
|
||||
style.background = { common.color "#fbfbfb" } |
||||
style.background2 = { common.color "#f2f2f2" } |
||||
style.background3 = { common.color "#f2f2f2" } |
||||
style.text = { common.color "#404040" } |
||||
style.caret = { common.color "#fc1785" } |
||||
style.accent = { common.color "#fc1785" } |
||||
style.dim = { common.color "#b0b0b0" } |
||||
style.divider = { common.color "#e8e8e8" } |
||||
style.selection = { common.color "#b7dce8" } |
||||
style.line_number = { common.color "#d0d0d0" } |
||||
style.line_number2 = { common.color "#808080" } |
||||
style.line_highlight = { common.color "#f2f2f2" } |
||||
style.scrollbar = { common.color "#e0e0e0" } |
||||
style.scrollbar2 = { common.color "#c0c0c0" } |
||||
|
||||
style.syntax["normal"] = { common.color "#181818" } |
||||
style.syntax["symbol"] = { common.color "#181818" } |
||||
style.syntax["comment"] = { common.color "#22a21f" } |
||||
style.syntax["keyword"] = { common.color "#fb6620" } |
||||
style.syntax["keyword2"] = { common.color "#fc1785" } |
||||
style.syntax["number"] = { common.color "#1586d2" } |
||||
style.syntax["literal"] = { common.color "#1586d2" } |
||||
style.syntax["string"] = { common.color "#1586d2" } |
||||
style.syntax["operator"] = { common.color "#fb6620" } |
||||
style.syntax["function"] = { common.color "#fc1785" } |
@ -0,0 +1,14 @@ |
||||
-- put user settings here |
||||
-- this module will be loaded after everything else when the application starts |
||||
|
||||
local keymap = require "core.keymap" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
|
||||
-- light theme: |
||||
-- require "user.colors.summer" |
||||
require "user.colors.gruvbox_dark" |
||||
|
||||
-- key binding: |
||||
-- keymap.add { ["ctrl+escape"] = "core:quit" } |
||||
|
@ -0,0 +1,146 @@ |
||||
# lite |
||||
|
||||
![screenshot](https://user-images.githubusercontent.com/3920290/81471642-6c165880-91ea-11ea-8cd1-fae7ae8f0bc4.png) |
||||
|
||||
## Overview |
||||
lite is a lightweight text editor written mostly in Lua — it aims to provide |
||||
something practical, pretty, *small* and fast, implemented as simply as |
||||
possible; easy to modify and extend, or to use without doing either. |
||||
|
||||
|
||||
## Getting Started |
||||
When lite is started it's typically opened with a *project directory* — this |
||||
is the directory where your project's code and other data resides. The project |
||||
directory is set once when lite is started and, for the duration of the |
||||
session, cannot be changed. |
||||
|
||||
To open lite with a specific project directory the directory name can be passed |
||||
as a command-line argument *(`.` can be passed to use the current directory)* or |
||||
the directory can be dragged onto either the lite executable or a running |
||||
instance of lite. |
||||
|
||||
The main way of opening files in lite is through the `core:find-file` command |
||||
— this provides a fuzzy finder over all of the project's files and can be |
||||
opened using the **`ctrl+p`** shortcut by default. |
||||
|
||||
Commands can be run using keyboard shortcuts, or by using the `core:find-command` |
||||
command bound to **`ctrl+shift+p`** by default. For example, pressing |
||||
`ctrl+shift+p` and typing `newdoc` then pressing `return` would open a new |
||||
document. The current keyboard shortcut for a command can be seen to the right |
||||
of the command name on the command finder, thus to find the shortcut for a command |
||||
`ctrl+shift+p` can be pressed and the command name typed. |
||||
|
||||
|
||||
## User Module |
||||
lite can be configured through use of the user module. The user module can be |
||||
used for changing options in the config module, adding additional key bindings, |
||||
loading custom color themes, modifying the style or changing any other part of |
||||
lite to your personal preference. |
||||
|
||||
The user module is loaded by lite when the application starts, after the plugins |
||||
have been loaded. |
||||
|
||||
The user module can be modified by running the `core:open-user-module` command |
||||
or otherwise directly opening the `data/user/init.lua` file. |
||||
|
||||
|
||||
## Project Module |
||||
The project module is an optional module which is loaded from the current |
||||
project's directory when lite is started. Project modules can be useful for |
||||
things like adding custom commands for project-specific build systems, or |
||||
loading project-specific plugins. |
||||
|
||||
The project module is loaded by lite when the application starts, after both the |
||||
plugins and user module have been loaded. |
||||
|
||||
The project module can be edited by running the `core:open-project-module` |
||||
command — if the module does not exist for the current project when the |
||||
command is run it will be created. |
||||
|
||||
|
||||
## Commands |
||||
Commands in lite are used both through the command finder (`ctrl+shift+p`) and |
||||
by lite's keyboard shortcut system. Commands consist of 3 components: |
||||
* **Name** — The command name in the form of `namespace:action-name`, for |
||||
example: `doc:select-all` |
||||
* **Predicate** — A function that returns true if the command can be ran, for |
||||
example, for any document commands the predicate checks whether the active |
||||
view is a document |
||||
* **Function** — The function which performs the command itself |
||||
|
||||
Commands can be added using the `command.add` function provided by the |
||||
`core.command` module: |
||||
```lua |
||||
local core = require "core" |
||||
local command = require "core.command" |
||||
|
||||
command.add("core.docview", { |
||||
["doc:save"] = function() |
||||
core.active_view.doc:save() |
||||
core.log("Saved '%s', core.active_view.doc.filename) |
||||
end |
||||
}) |
||||
``` |
||||
|
||||
Commands can be performed programatically (eg. from another command or by your |
||||
user module) by calling the `command.perform` function after requiring the |
||||
`command` module: |
||||
```lua |
||||
local command = require "core.command" |
||||
command.perform "core:quit" |
||||
``` |
||||
|
||||
|
||||
## Keymap |
||||
All keyboard shortcuts in lite are handled by the `core.keymap` module. A key |
||||
binding in lite maps a "stroke" (eg. `ctrl+q`) to one or more commands (eg. |
||||
`core:quit`). When the shortcut is pressed lite will iterate each command |
||||
assigned to that key and run the *predicate function* for that command — if the |
||||
predicate passes it stops iterating and runs the command. |
||||
|
||||
An example of where this used is the default binding of the `tab` key: |
||||
``` lua |
||||
["tab"] = { "command:complete", "doc:indent" }, |
||||
``` |
||||
When tab is pressed the `command:complete` command is attempted which will only |
||||
succeed if the command-input at the bottom of the window is active. Otherwise |
||||
the `doc:indent` command is attempted which will only succeed if we have a |
||||
document as our active view. |
||||
|
||||
A new mapping can be added by your user module as follows: |
||||
```lua |
||||
local keymap = require "core.keymap" |
||||
keymap.add { ["ctrl+q"] = "core:quit" } |
||||
``` |
||||
|
||||
|
||||
## Plugins |
||||
Plugins in lite are normal lua modules and are treated as such — no |
||||
complicated plugin manager is provided, and, once a plugin is loaded, it is never |
||||
expected be to have to unload itself. |
||||
|
||||
To install a plugin simply drop it in the `data/plugins` directory — installed |
||||
plugins will be automatically loaded when lite starts. To uninstall a plugin the |
||||
plugin file can be deleted — any plugin (including those included with lite's |
||||
default installation) can be deleted to remove its functionality. |
||||
|
||||
If you want to load a plugin only under a certain circumstance (for example, |
||||
only on a given project) the plugin can be placed somewhere other than the |
||||
`data/plugins` directory so that it is not automatically loaded. The plugin can |
||||
then be loaded manually as needed by using the `require` function. |
||||
|
||||
Plugins can be downloaded from the [plugins repository](https://github.com/rxi/lite-plugins). |
||||
|
||||
|
||||
## Color Themes |
||||
Colors themes in lite are lua modules which overwrite the color fields of lite's |
||||
`core.style` module. Color themes should be placed in the `data/user/colors` |
||||
directory. |
||||
|
||||
A color theme can be set by requiring it in your user module: |
||||
```lua |
||||
require "user.colors.winter" |
||||
``` |
||||
|
||||
Color themes can be downloaded from the [color themes repository](https://github.com/rxi/lite-colors). |
||||
|
@ -0,0 +1,16 @@ |
||||
.POSIX: |
||||
|
||||
CC ?= gcc
|
||||
SRCDIR = ./src
|
||||
CFLAGS = -Wall -Werror -pedantic -O3 -std=gnu11 -fno-strict-aliasing -I${SRCDIR}\
|
||||
-DLUA_USE_POSIX
|
||||
# remove this
|
||||
LDFLAGS = -lSDL2 -lm -llua5.2
|
||||
FILES != find ${SRCDIR} -name '*.c'
|
||||
OBJS = $(FILES:.c=.o)
|
||||
|
||||
lite: ${OBJS} |
||||
${CC} ${LDFLAGS} $^ -o $@
|
||||
|
||||
clean: |
||||
rm -f lite ${OBJS}
|
@ -0,0 +1,16 @@ |
||||
#include "api.h" |
||||
|
||||
int luaopen_system(lua_State *L); |
||||
int luaopen_renderer(lua_State *L); |
||||
|
||||
static const luaL_Reg libs[] = { |
||||
{ "system", luaopen_system }, |
||||
{ "renderer", luaopen_renderer }, |
||||
{ NULL, NULL } |
||||
}; |
||||
|
||||
void api_load_libs(lua_State *L) { |
||||
for (int i = 0; libs[i].name; i++) { |
||||
luaL_requiref(L, libs[i].name, libs[i].func, 1); |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
#ifndef API_H |
||||
#define API_H |
||||
|
||||
#include <lua5.2/lua.h> |
||||
#include <lua5.2/lauxlib.h> |
||||
#include <lua5.2/lualib.h> |
||||
|
||||
#define API_TYPE_FONT "Font" |
||||
|
||||
void api_load_libs(lua_State *L); |
||||
|
||||
#endif |
@ -0,0 +1,112 @@ |
||||
#include "api.h" |
||||
#include "renderer.h" |
||||
#include "rencache.h" |
||||
|
||||
static RenColor checkcolor(lua_State *L, int idx, int def) |
||||
{ |
||||
RenColor color; |
||||
if (lua_isnoneornil(L, idx)) |
||||
return (RenColor) { def, def, def, 255 }; |
||||
lua_rawgeti(L, idx, 1); |
||||
lua_rawgeti(L, idx, 2); |
||||
lua_rawgeti(L, idx, 3); |
||||
lua_rawgeti(L, idx, 4); |
||||
color.r = luaL_checknumber(L, -4); |
||||
color.g = luaL_checknumber(L, -3); |
||||
color.b = luaL_checknumber(L, -2); |
||||
color.a = luaL_optnumber(L, -1, 255); |
||||
lua_pop(L, 4); |
||||
return color; |
||||
} |
||||
|
||||
|
||||
static int f_show_debug(lua_State *L) |
||||
{ |
||||
luaL_checkany(L, 1); |
||||
rencache_show_debug(lua_toboolean(L, 1)); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_get_size(lua_State *L) |
||||
{ |
||||
int w, h; |
||||
ren_get_size(&w, &h); |
||||
lua_pushnumber(L, w); |
||||
lua_pushnumber(L, h); |
||||
return 2; |
||||
} |
||||
|
||||
|
||||
static int f_begin_frame(lua_State *L) |
||||
{ |
||||
rencache_begin_frame(); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_end_frame(lua_State *L) |
||||
{ |
||||
rencache_end_frame(); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
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); |
||||
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); |
||||
RenColor color = checkcolor(L, 5, 255); |
||||
rencache_draw_rect(rect, color); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_draw_text(lua_State *L) |
||||
{ |
||||
RenFont **font = luaL_checkudata(L, 1, API_TYPE_FONT); |
||||
const char *text = luaL_checkstring(L, 2); |
||||
int x = luaL_checknumber(L, 3); |
||||
int y = luaL_checknumber(L, 4); |
||||
RenColor color = checkcolor(L, 5, 255); |
||||
x = rencache_draw_text(*font, text, x, y, color); |
||||
lua_pushnumber(L, x); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static const luaL_Reg lib[] = { |
||||
{ "show_debug", f_show_debug }, |
||||
{ "get_size", f_get_size }, |
||||
{ "begin_frame", f_begin_frame }, |
||||
{ "end_frame", f_end_frame }, |
||||
{ "set_clip_rect", f_set_clip_rect }, |
||||
{ "draw_rect", f_draw_rect }, |
||||
{ "draw_text", f_draw_text }, |
||||
{ NULL, NULL } |
||||
}; |
||||
|
||||
|
||||
int luaopen_renderer_font(lua_State *L); |
||||
|
||||
int luaopen_renderer(lua_State *L) |
||||
{ |
||||
luaL_newlib(L, lib); |
||||
luaopen_renderer_font(L); |
||||
lua_setfield(L, -2, "font"); |
||||
return 1; |
||||
} |
@ -0,0 +1,69 @@ |
||||
#include "api.h" |
||||
#include "renderer.h" |
||||
#include "rencache.h" |
||||
|
||||
static int f_load(lua_State *L) |
||||
{ |
||||
const char *filename = luaL_checkstring(L, 1); |
||||
float size = luaL_checknumber(L, 2); |
||||
RenFont **self = lua_newuserdata(L, sizeof(*self)); |
||||
luaL_setmetatable(L, API_TYPE_FONT); |
||||
*self = ren_load_font(filename, size); |
||||
if (!*self)
|
||||
luaL_error(L, "failed to load font"); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static int f_set_tab_width(lua_State *L) |
||||
{ |
||||
RenFont **self = luaL_checkudata(L, 1, API_TYPE_FONT); |
||||
int n = luaL_checknumber(L, 2); |
||||
ren_set_font_tab_width(*self, n); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_gc(lua_State *L) |
||||
{ |
||||
RenFont **self = luaL_checkudata(L, 1, API_TYPE_FONT); |
||||
if (*self)
|
||||
rencache_free_font(*self); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_get_width(lua_State *L) |
||||
{ |
||||
RenFont **self = luaL_checkudata(L, 1, API_TYPE_FONT); |
||||
const char *text = luaL_checkstring(L, 2); |
||||
lua_pushnumber(L, ren_get_font_width(*self, text) ); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static int f_get_height(lua_State *L) |
||||
{ |
||||
RenFont **self = luaL_checkudata(L, 1, API_TYPE_FONT); |
||||
lua_pushnumber(L, ren_get_font_height(*self) ); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static const luaL_Reg lib[] = { |
||||
{ "__gc", f_gc }, |
||||
{ "load", f_load }, |
||||
{ "set_tab_width", f_set_tab_width }, |
||||
{ "get_width", f_get_width }, |
||||
{ "get_height", f_get_height }, |
||||
{ NULL, NULL } |
||||
}; |
||||
|
||||
int luaopen_renderer_font(lua_State *L) |
||||
{ |
||||
luaL_newmetatable(L, API_TYPE_FONT); |
||||
luaL_setfuncs(L, lib, 0); |
||||
lua_pushvalue(L, -1); |
||||
lua_setfield(L, -2, "__index"); |
||||
return 1; |
||||
} |
@ -0,0 +1,402 @@ |
||||
#include <SDL2/SDL.h> |
||||
#include <stdbool.h> |
||||
#include <ctype.h> |
||||
#include <dirent.h> |
||||
#include <unistd.h> |
||||
#include <errno.h> |
||||
#include <sys/stat.h> |
||||
#include "api.h" |
||||
#include "rencache.h" |
||||
|
||||
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"; |
||||
default : return "?"; |
||||
} |
||||
} |
||||
|
||||
static char* key_name(char *dst, int sym) |
||||
{ |
||||
strcpy(dst, SDL_GetKeyName(sym)); |
||||
for (char *p = dst; *p; p++) |
||||
*p = tolower(*p); |
||||
return dst; |
||||
} |
||||
|
||||
|
||||
static int f_poll_event(lua_State *L) |
||||
{ |
||||
char buf[16]; |
||||
int mx, my, wx, wy; |
||||
SDL_Event e; |
||||
|
||||
top: |
||||
if ( !SDL_PollEvent(&e) ) |
||||
return 0; |
||||
|
||||
switch (e.type) { |
||||
case SDL_QUIT: |
||||
lua_pushstring(L, "quit"); |
||||
return 1; |
||||
|
||||
case SDL_WINDOWEVENT: |
||||
if (e.window.event == SDL_WINDOWEVENT_RESIZED) { |
||||
lua_pushstring(L, "resized"); |
||||
lua_pushnumber(L, e.window.data1); |
||||
lua_pushnumber(L, e.window.data2); |
||||
return 3; |
||||
} else if (e.window.event == SDL_WINDOWEVENT_EXPOSED) { |
||||
rencache_invalidate(); |
||||
lua_pushstring(L, "exposed"); |
||||
return 1; |
||||
} |
||||
/* on some systems, when alt-tabbing to the window SDL will
|
||||
* queue up several KEYDOWN events for the `tab` key; we flush |
||||
* all keydown events on focus so these are discarded */ |
||||
if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) |
||||
SDL_FlushEvent(SDL_KEYDOWN); |
||||
goto top; |
||||
|
||||
case SDL_DROPFILE: |
||||
SDL_GetGlobalMouseState(&mx, &my); |
||||
SDL_GetWindowPosition(window, &wx, &wy); |
||||
lua_pushstring(L, "filedropped"); |
||||
lua_pushstring(L, e.drop.file); |
||||
lua_pushnumber(L, mx - wx); |
||||
lua_pushnumber(L, my - wy); |
||||
SDL_free(e.drop.file); |
||||
return 4; |
||||
|
||||
case SDL_KEYDOWN: |
||||
lua_pushstring(L, "keypressed"); |
||||
lua_pushstring(L, key_name(buf, e.key.keysym.sym)); |
||||
return 2; |
||||
|
||||
case SDL_KEYUP: |
||||
lua_pushstring(L, "keyreleased"); |
||||
lua_pushstring(L, key_name(buf, e.key.keysym.sym)); |
||||
return 2; |
||||
|
||||
case SDL_TEXTINPUT: |
||||
lua_pushstring(L, "textinput"); |
||||
lua_pushstring(L, e.text.text); |
||||
return 2; |
||||
|
||||
case SDL_MOUSEBUTTONDOWN: |
||||
if (e.button.button == 1)
|
||||
SDL_CaptureMouse(1); |
||||
lua_pushstring(L, "mousepressed"); |
||||
lua_pushstring(L, button_name(e.button.button)); |
||||
lua_pushnumber(L, e.button.x); |
||||
lua_pushnumber(L, e.button.y); |
||||
lua_pushnumber(L, e.button.clicks); |
||||
return 5; |
||||
|
||||
case SDL_MOUSEBUTTONUP: |
||||
if (e.button.button == 1)
|
||||
SDL_CaptureMouse(0); |
||||
lua_pushstring(L, "mousereleased"); |
||||
lua_pushstring(L, button_name(e.button.button)); |
||||
lua_pushnumber(L, e.button.x); |
||||
lua_pushnumber(L, e.button.y); |
||||
return 4; |
||||
|
||||
case SDL_MOUSEMOTION: |
||||
lua_pushstring(L, "mousemoved"); |
||||
lua_pushnumber(L, e.motion.x); |
||||
lua_pushnumber(L, e.motion.y); |
||||
lua_pushnumber(L, e.motion.xrel); |
||||
lua_pushnumber(L, e.motion.yrel); |
||||
return 5; |
||||
|
||||
case SDL_MOUSEWHEEL: |
||||
lua_pushstring(L, "mousewheel"); |
||||
lua_pushnumber(L, e.wheel.y); |
||||
return 2; |
||||
|
||||
default: |
||||
goto top; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
static int f_wait_event(lua_State *L) |
||||
{ |
||||
double n = luaL_checknumber(L, 1); |
||||
lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000)); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static SDL_Cursor* cursor_cache[SDL_SYSTEM_CURSOR_HAND + 1]; |
||||
|
||||
static const char *cursor_opts[] = { |
||||
"arrow", |
||||
"ibeam", |
||||
"sizeh", |
||||
"sizev", |
||||
"hand", |
||||
NULL |
||||
}; |
||||
|
||||
static const int cursor_enums[] = { |
||||
SDL_SYSTEM_CURSOR_ARROW, |
||||
SDL_SYSTEM_CURSOR_IBEAM, |
||||
SDL_SYSTEM_CURSOR_SIZEWE, |
||||
SDL_SYSTEM_CURSOR_SIZENS, |
||||
SDL_SYSTEM_CURSOR_HAND |
||||
}; |
||||
|
||||
static int f_set_cursor(lua_State *L) |
||||
{ |
||||
int opt = luaL_checkoption(L, 1, "arrow", cursor_opts); |
||||
int n = cursor_enums[opt]; |
||||
SDL_Cursor *cursor = cursor_cache[n]; |
||||
if (!cursor) { |
||||
cursor = SDL_CreateSystemCursor(n); |
||||
cursor_cache[n] = cursor; |
||||
} |
||||
SDL_SetCursor(cursor); |
||||
return 0; |
||||
} |
||||
|
||||
static int f_set_window_title(lua_State *L) |
||||
{ |
||||
const char *title = luaL_checkstring(L, 1); |
||||
SDL_SetWindowTitle(window, title); |
||||
return 0; |
||||
} |
||||
|
||||
static const char *window_opts[] = { "normal", "maximized", "fullscreen", 0 }; |
||||
enum { WIN_NORMAL, WIN_MAXIMIZED, WIN_FULLSCREEN }; |
||||
|
||||
static int f_set_window_mode(lua_State *L) |
||||
{ |
||||
int n = luaL_checkoption(L, 1, "normal", window_opts); |
||||
SDL_SetWindowFullscreen(window, |
||||
n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); |
||||
if (n == WIN_NORMAL) |
||||
SDL_RestoreWindow(window); |
||||
if (n == WIN_MAXIMIZED)
|
||||
SDL_MaximizeWindow(window); |
||||
return 0; |
||||
} |
||||
|
||||
static int f_window_has_focus(lua_State *L) |
||||
{ |
||||
unsigned flags = SDL_GetWindowFlags(window); |
||||
lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_show_confirm_dialog(lua_State *L) |
||||
{ |
||||
const char *title = luaL_checkstring(L, 1); |
||||
const char *msg = luaL_checkstring(L, 2); |
||||
|
||||
SDL_MessageBoxButtonData buttons[] = { |
||||
{ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes" }, |
||||
{ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 0, "No" }, |
||||
}; |
||||
SDL_MessageBoxData data = { |
||||
.title = title, |
||||
.message = msg, |
||||
.numbuttons = 2, |
||||
.buttons = buttons, |
||||
}; |
||||
int buttonid; |
||||
SDL_ShowMessageBox(&data, &buttonid); |
||||
lua_pushboolean(L, buttonid == 1); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_chdir(lua_State *L) |
||||
{ |
||||
const char *path = luaL_checkstring(L, 1); |
||||
int err = chdir(path); |
||||
if (err)
|
||||
luaL_error(L, "chdir() failed"); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_list_dir(lua_State *L) |
||||
{ |
||||
const char *path = luaL_checkstring(L, 1); |
||||
|
||||
DIR *dir = opendir(path); |
||||
if (!dir) { |
||||
lua_pushnil(L); |
||||
lua_pushstring(L, strerror(errno)); |
||||
return 2; |
||||
} |
||||
|
||||
lua_newtable(L); |
||||
int i = 1; |
||||
struct dirent *entry; |
||||
while ( (entry = readdir(dir)) ) { |
||||
if (strcmp(entry->d_name, "." ) == 0)
|
||||
continue; |
||||
if (strcmp(entry->d_name, "..") == 0)
|
||||
continue; |
||||
lua_pushstring(L, entry->d_name); |
||||
lua_rawseti(L, -2, i); |
||||
i++; |
||||
} |
||||
|
||||
closedir(dir); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_absolute_path(lua_State *L) |
||||
{ |
||||
const char *path = luaL_checkstring(L, 1); |
||||
char *res = realpath(path, NULL); |
||||
if (!res)
|
||||
return 0; |
||||
lua_pushstring(L, res); |
||||
free(res); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_get_file_info(lua_State *L) |
||||
{ |
||||
const char *path = luaL_checkstring(L, 1); |
||||
|
||||
struct stat s; |
||||
int err = stat(path, &s); |
||||
if (err < 0) { |
||||
lua_pushnil(L); |
||||
lua_pushstring(L, strerror(errno)); |
||||
return 2; |
||||
} |
||||
|
||||
lua_newtable(L); |
||||
lua_pushnumber(L, s.st_mtime); |
||||
lua_setfield(L, -2, "modified"); |
||||
|
||||
lua_pushnumber(L, s.st_size); |
||||
lua_setfield(L, -2, "size"); |
||||
|
||||
if (S_ISREG(s.st_mode)) { |
||||
lua_pushstring(L, "file"); |
||||
} else if (S_ISDIR(s.st_mode)) { |
||||
lua_pushstring(L, "dir"); |
||||
} else { |
||||
lua_pushnil(L); |
||||
} |
||||
lua_setfield(L, -2, "type"); |
||||
|
||||
return 1; |
||||
} |
||||
|
||||
static int f_get_clipboard(lua_State *L) |
||||
{ |
||||
char *text = SDL_GetClipboardText(); |
||||
if (!text) |
||||
return 0; |
||||
lua_pushstring(L, text); |
||||
SDL_free(text); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_set_clipboard(lua_State *L) |
||||
{ |
||||
const char *text = luaL_checkstring(L, 1); |
||||
SDL_SetClipboardText(text); |
||||
return 0; |
||||
} |
||||
|
||||
static int f_get_time(lua_State *L) |
||||
{ |
||||
double n = SDL_GetPerformanceCounter() / (double)SDL_GetPerformanceFrequency(); |
||||
lua_pushnumber(L, n); |
||||
return 1; |
||||
} |
||||
|
||||
static int f_sleep(lua_State *L) |
||||
{ |
||||
double n = luaL_checknumber(L, 1); |
||||
SDL_Delay(n * 1000); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_exec(lua_State *L) |
||||
{ |
||||
size_t len; |
||||
const char *cmd = luaL_checklstring(L, 1, &len); |
||||
char *buf = malloc(len + 32); |
||||
if (!buf)
|
||||
luaL_error(L, "buffer allocation failed"); |
||||
sprintf(buf, "%s &", cmd); |
||||
int res = system(buf); |
||||
(void) res; |
||||
free(buf); |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
static int f_fuzzy_match(lua_State *L) |
||||
{ |
||||
const char *str = luaL_checkstring(L, 1); |
||||
const char *ptn = luaL_checkstring(L, 2); |
||||
int score = 0; |
||||
int run = 0; |
||||
|
||||
while (*str && *ptn) { |
||||
while (*str == ' ')
|
||||
str++; |
||||
while (*ptn == ' ') |
||||
ptn++; |
||||
if (tolower(*str) == tolower(*ptn)) { |
||||
score += run * 10 - (*str != *ptn); |
||||
run++; |
||||
ptn++; |
||||
} else { |
||||
score -= 10; |
||||
run = 0; |
||||
} |
||||
str++; |
||||
} |
||||
if (*ptn)
|
||||
return 0; |
||||
|
||||
lua_pushnumber(L, score - (int) strlen(str)); |
||||
return 1; |
||||
} |
||||
|
||||
|
||||
static const luaL_Reg lib[] = { |
||||
{ "poll_event", f_poll_event }, |
||||
{ "wait_event", f_wait_event }, |
||||
{ "set_cursor", f_set_cursor }, |
||||
{ "set_window_title", f_set_window_title }, |
||||
{ "set_window_mode", f_set_window_mode }, |
||||
{ "window_has_focus", f_window_has_focus }, |
||||
{ "show_confirm_dialog", f_show_confirm_dialog }, |
||||
{ "chdir", f_chdir }, |
||||
{ "list_dir", f_list_dir }, |
||||
{ "absolute_path", f_absolute_path }, |
||||
{ "get_file_info", f_get_file_info }, |
||||
{ "get_clipboard", f_get_clipboard }, |
||||
{ "set_clipboard", f_set_clipboard }, |
||||
{ "get_time", f_get_time }, |
||||
{ "sleep", f_sleep }, |
||||
{ "exec", f_exec }, |
||||
{ "fuzzy_match", f_fuzzy_match }, |
||||
{ NULL, NULL } |
||||
}; |
||||
|
||||
|
||||
int luaopen_system(lua_State *L) |
||||
{ |
||||
luaL_newlib(L, lib); |
||||
return 1; |
||||
} |
@ -0,0 +1,2 @@ |
||||
#define STB_TRUETYPE_IMPLEMENTATION |
||||
#include "stb_truetype.h" |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,123 @@ |
||||
#include <stdio.h> |
||||
#include <unistd.h> |
||||
#include <SDL2/SDL.h> |
||||
|
||||
#include "api/api.h" |
||||
#include "renderer.h" |
||||
|
||||
|
||||
SDL_Window *window; |
||||
|
||||
|
||||
static double get_scale(void) |
||||
{ |
||||
float dpi; |
||||
SDL_GetDisplayDPI(0, NULL, &dpi, NULL); |
||||
return 1.0; |
||||
} |
||||
|
||||
|
||||
static void get_exe_filename(char *buf, int sz) |
||||
{ |
||||
char path[512]; |
||||
sprintf(path, "/proc/%d/exe", getpid()); |
||||
int len = readlink(path, buf, sz - 1); |
||||
buf[len] = '\0'; |
||||
} |
||||
|
||||
|
||||
static void init_window_icon(void) |
||||
{ |
||||
// FIXME: include inside function!?
|
||||
#include "../icon.inl" |
||||
(void) icon_rgba_len; /* unused */ |
||||
SDL_Surface *surf = SDL_CreateRGBSurfaceFrom( |
||||
icon_rgba, 64, 64, |
||||
32, 64 * 4, |
||||
0x000000ff, |
||||
0x0000ff00, |
||||
0x00ff0000, |
||||
0xff000000); |
||||
SDL_SetWindowIcon(window, surf); |
||||
SDL_FreeSurface(surf); |
||||
} |
||||
|
||||
|
||||
int main(int argc, char **argv) |
||||
{ |
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); |
||||
SDL_EnableScreenSaver(); |
||||
SDL_EventState(SDL_DROPFILE, SDL_ENABLE); |
||||
atexit(SDL_Quit); |
||||
|
||||
#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR /* Available since 2.0.8 */ |
||||
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); |
||||
#endif |
||||
#if SDL_VERSION_ATLEAST(2, 0, 5) |
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); |
||||
#endif |
||||
|
||||
SDL_DisplayMode dm; |
||||
SDL_GetCurrentDisplayMode(0, &dm); |
||||
|
||||
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); |
||||
init_window_icon(); |
||||
ren_init(window); |
||||
|
||||
|
||||
lua_State *L = luaL_newstate(); |
||||
luaL_openlibs(L); |
||||
api_load_libs(L); |
||||
|
||||
|
||||
lua_newtable(L); |
||||
for (int i = 0; i < argc; i++) { |
||||
lua_pushstring(L, argv[i]); |
||||
lua_rawseti(L, -2, i + 1); |
||||
} |
||||
lua_setglobal(L, "ARGS"); |
||||
|
||||
lua_pushstring(L, "1.11"); |
||||
lua_setglobal(L, "VERSION"); |
||||
|
||||
lua_pushstring(L, SDL_GetPlatform()); |
||||
lua_setglobal(L, "PLATFORM"); |
||||
|
||||
lua_pushnumber(L, get_scale()); |
||||
lua_setglobal(L, "SCALE"); |
||||
|
||||
char exename[2048]; |
||||
get_exe_filename(exename, sizeof(exename)); |
||||
lua_pushstring(L, exename); |
||||
lua_setglobal(L, "EXEFILE"); |
||||
|
||||
|
||||
(void) luaL_dostring(L, |
||||
"local core\n" |
||||
"xpcall(function()\n" |
||||
" SCALE = tonumber(os.getenv(\"LITE_SCALE\")) or SCALE\n" |
||||
" PATHSEP = package.config:sub(1, 1)\n" |
||||
" EXEDIR = EXEFILE:match(\"^(.+)[/\\\\].*$\")\n" |
||||
" package.path = EXEDIR .. '/data/?.lua;' .. package.path\n" |
||||
" package.path = EXEDIR .. '/data/?/init.lua;' .. package.path\n" |
||||
" core = require('core')\n" |
||||
" core.init()\n" |
||||
" core.run()\n" |
||||
"end, function(err)\n" |
||||
" print('Error: ' .. tostring(err))\n" |
||||
" print(debug.traceback(nil, 2))\n" |
||||
" if core and core.on_error then\n" |
||||
" pcall(core.on_error, err)\n" |
||||
" end\n" |
||||
" os.exit(1)\n" |
||||
"end)"); |
||||
|
||||
|
||||
lua_close(L); |
||||
SDL_DestroyWindow(window); |
||||
|
||||
return EXIT_SUCCESS; |
||||
} |
@ -0,0 +1,303 @@ |
||||
#include <stdio.h> |
||||
#include "rencache.h" |
||||
|
||||
/* a cache over the software renderer -- all drawing operations are stored as
|
||||
** commands when issued. At the end of the frame we write the commands to a grid |
||||
** of hash values, take the cells that have changed since the previous frame, |
||||
** merge them into dirty rectangles and redraw only those regions */ |
||||
|
||||
#define CELLS_X 80 |
||||
#define CELLS_Y 50 |
||||
#define CELL_SIZE 96 |
||||
#define COMMAND_BUF_SIZE (1024 * 512) |
||||
|
||||
enum { FREE_FONT, SET_CLIP, DRAW_TEXT, DRAW_RECT }; |
||||
|
||||
typedef struct { |
||||
int type, size; |
||||
RenRect rect; |
||||
RenColor color; |
||||
RenFont *font; |
||||
int tab_width; |
||||
// this was of size 0
|
||||
char text[1]; |
||||
} Command; |
||||
|
||||
|
||||
static unsigned cells_buf1[CELLS_X * CELLS_Y]; |
||||
static unsigned cells_buf2[CELLS_X * CELLS_Y]; |
||||
static unsigned *cells_prev = cells_buf1; |
||||
static unsigned *cells = cells_buf2; |
||||
static RenRect rect_buf[CELLS_X * CELLS_Y / 2]; |
||||
static char command_buf[COMMAND_BUF_SIZE]; |
||||
static int command_buf_idx; |
||||
static RenRect screen_rect; |
||||
static bool show_debug; |
||||
|
||||
|
||||
static inline int min(int a, int b) { return a < b ? a : b; } |
||||
static inline int max(int a, int b) { return a > b ? a : b; } |
||||
static inline int cell_idx(int x, int y) { return x + y * CELLS_X; } |
||||
|
||||
/* 32bit fnv-1a hash */ |
||||
#define HASH_INITIAL 2166136261 |
||||
|
||||
static void hash(unsigned *h, const void *data, int size) |
||||
{ |
||||
const unsigned char *p = data; |
||||
while (size--) |
||||
*h = (*h ^ *p++) * 16777619; |
||||
} |
||||
|
||||
static inline bool rects_overlap(RenRect a, RenRect b) |
||||
{ |
||||
return b.x + b.width >= a.x && b.x <= a.x + a.width |
||||
&& b.y + b.height >= a.y && b.y <= a.y + a.height; |
||||
} |
||||
|
||||
|
||||
static RenRect intersect_rects(RenRect a, RenRect b) |
||||
{ |
||||
int x1 = max(a.x, b.x); |
||||
int y1 = max(a.y, b.y); |
||||
int x2 = min(a.x + a.width, b.x + b.width); |
||||
int y2 = min(a.y + a.height, b.y + b.height); |
||||
return (RenRect) { x1, y1, max(0, x2 - x1), max(0, y2 - y1) }; |
||||
} |
||||
|
||||
|
||||
static RenRect merge_rects(RenRect a, RenRect b) |
||||
{ |
||||
int x1 = min(a.x, b.x); |
||||
int y1 = min(a.y, b.y); |
||||
int x2 = max(a.x + a.width, b.x + b.width); |
||||
int y2 = max(a.y + a.height, b.y + b.height); |
||||
return (RenRect) { x1, y1, x2 - x1, y2 - y1 }; |
||||
} |
||||
|
||||
|
||||
static Command* push_command(int type, int size) |
||||
{ |
||||
Command *cmd = (Command*) (command_buf + command_buf_idx); |
||||
int n = command_buf_idx + size; |
||||
if (n > COMMAND_BUF_SIZE) { |
||||
fprintf(stderr, "Warning: (" __FILE__ "): exhausted command buffer\n"); |
||||
return NULL; |
||||
} |
||||
command_buf_idx = n; |
||||
memset(cmd, 0, sizeof(Command)); |
||||
cmd->type = type; |
||||
cmd->size = size; |
||||
return cmd; |
||||
} |
||||
|
||||
|
||||
static bool next_command(Command **prev) |
||||
{ |
||||
if (*prev == NULL) |
||||
*prev = (Command*) command_buf; |
||||
else |
||||
*prev = (Command*) (((char*) *prev) + (*prev)->size); |
||||
return *prev != ((Command*) (command_buf + command_buf_idx)); |
||||
} |
||||
|
||||
|
||||
void rencache_show_debug(bool enable) |
||||
{ |
||||
show_debug = enable; |
||||
} |
||||
|
||||
|
||||
void rencache_free_font(RenFont *font) |
||||
{ |
||||
Command *cmd = push_command(FREE_FONT, sizeof(Command)); |
||||
if (cmd)
|
||||
cmd->font = font; |
||||
} |
||||
|
||||
|
||||
void rencache_set_clip_rect(RenRect rect) |
||||
{ |
||||
Command *cmd = push_command(SET_CLIP, sizeof(Command)); |
||||
if (cmd)
|
||||
cmd->rect = intersect_rects(rect, screen_rect); |
||||
} |
||||
|
||||
|
||||
void rencache_draw_rect(RenRect rect, RenColor color) |
||||
{ |
||||
if (!rects_overlap(screen_rect, rect)) |
||||
return; |
||||
Command *cmd = push_command(DRAW_RECT, sizeof(Command)); |
||||
if (cmd) { |
||||
cmd->rect = rect; |
||||
cmd->color = color; |
||||
} |
||||
} |
||||
|
||||
|
||||
int rencache_draw_text(RenFont *font, const char *text, int x, int y, RenColor color) |
||||
{ |
||||
RenRect rect; |
||||
rect.x = x; |
||||
rect.y = y; |
||||
rect.width = ren_get_font_width(font, text); |
||||
rect.height = ren_get_font_height(font); |
||||
|
||||
if (rects_overlap(screen_rect, rect)) { |
||||
int sz = strlen(text) + 1; |
||||
Command *cmd = push_command(DRAW_TEXT, sizeof(Command) + sz); |
||||
if (cmd) { |
||||
memcpy(cmd->text, text, sz); |
||||
cmd->color = color; |
||||
cmd->font = font; |
||||
cmd->rect = rect; |
||||
cmd->tab_width = ren_get_font_tab_width(font); |
||||
} |
||||
} |
||||
|
||||
return x + rect.width; |
||||
} |
||||
|
||||
void rencache_invalidate(void) |
||||
{ |
||||
memset(cells_prev, 0xff, sizeof(cells_buf1)); |
||||
} |
||||
|
||||
|
||||
void rencache_begin_frame(void) |
||||
{ |
||||
/* reset all cells if the screen width/height has changed */ |
||||
int w, h; |
||||
ren_get_size(&w, &h); |
||||
if (screen_rect.width != w || h != screen_rect.height) { |
||||
screen_rect.width = w; |
||||
screen_rect.height = h; |
||||
rencache_invalidate(); |
||||
} |
||||
} |
||||
|
||||
|
||||
static void update_overlapping_cells(RenRect r, unsigned h) |
||||
{ |
||||
int x1 = r.x / CELL_SIZE; |
||||
int y1 = r.y / CELL_SIZE; |
||||
int x2 = (r.x + r.width) / CELL_SIZE; |
||||
int y2 = (r.y + r.height) / CELL_SIZE; |
||||
|
||||
for (int y = y1; y <= y2; y++) { |
||||
for (int x = x1; x <= x2; x++) { |
||||
int idx = cell_idx(x, y); |
||||
hash(&cells[idx], &h, sizeof(h)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
static void push_rect(RenRect r, int *count) |
||||
{ |
||||
/* try to merge with existing rectangle */ |
||||
for (int i = *count - 1; i >= 0; i--) { |
||||
RenRect *rp = &rect_buf[i]; |
||||
if (rects_overlap(*rp, r)) { |
||||
*rp = merge_rects(*rp, r); |
||||
return; |
||||
} |
||||
} |
||||
/* couldn't merge with previous rectangle: push */ |
||||
rect_buf[(*count)++] = r; |
||||
} |
||||
|
||||
|
||||
void rencache_end_frame(void) |
||||
{ |
||||
/* update cells from commands */ |
||||
Command *cmd = NULL; |
||||
RenRect cr = screen_rect; |
||||
while (next_command(&cmd)) { |
||||
if (cmd->type == SET_CLIP)
|
||||
cr = cmd->rect; |
||||
RenRect r = intersect_rects(cmd->rect, cr); |
||||
if (r.width == 0 || r.height == 0)
|
||||
continue; |
||||
unsigned h = HASH_INITIAL; |
||||
hash(&h, cmd, cmd->size); |
||||
update_overlapping_cells(r, h); |
||||
} |
||||
|
||||
/* push rects for all cells changed from last frame, reset cells */ |
||||
int rect_count = 0; |
||||
int max_x = screen_rect.width / CELL_SIZE + 1; |
||||
int max_y = screen_rect.height / CELL_SIZE + 1; |
||||
for (int y = 0; y < max_y; y++) { |
||||
for (int x = 0; x < max_x; x++) { |
||||
/* compare previous and current cell for change */ |
||||
int idx = cell_idx(x, y); |
||||
if (cells[idx] != cells_prev[idx]) |
||||
push_rect((RenRect) { x, y, 1, 1 }, &rect_count); |
||||
cells_prev[idx] = HASH_INITIAL; |
||||
} |
||||
} |
||||
|
||||
/* expand rects from cells to pixels */ |
||||
for (int i = 0; i < rect_count; i++) { |
||||
RenRect *r = &rect_buf[i]; |
||||
r->x *= CELL_SIZE; |
||||
r->y *= CELL_SIZE; |
||||
r->width *= CELL_SIZE; |
||||
r->height *= CELL_SIZE; |
||||
*r = intersect_rects(*r, screen_rect); |
||||
} |
||||
|
||||
/* redraw updated regions */ |
||||
bool has_free_commands = false; |
||||
for (int i = 0; i < rect_count; i++) { |
||||
/* draw */ |
||||
RenRect r = rect_buf[i]; |
||||
ren_set_clip_rect(r); |
||||
|
||||
cmd = NULL; |
||||
while (next_command(&cmd)) { |
||||
switch (cmd->type) { |
||||
case FREE_FONT: |
||||
has_free_commands = true; |
||||
break; |
||||
case SET_CLIP: |
||||
ren_set_clip_rect(intersect_rects(cmd->rect, r)); |
||||
break; |
||||
case DRAW_RECT: |
||||
ren_draw_rect(cmd->rect, cmd->color); |
||||
break; |
||||
case DRAW_TEXT: |
||||
ren_set_font_tab_width(cmd->font, cmd->tab_width); |
||||
ren_draw_text(cmd->font, cmd->text, cmd->rect.x, |
||||
cmd->rect.y, cmd->color); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (show_debug) { |
||||
RenColor color = { rand(), rand(), rand(), 50 }; |
||||
ren_draw_rect(r, color); |
||||
} |
||||
} |
||||
|
||||
/* update dirty rects */ |
||||
if (rect_count > 0) |
||||
ren_update_rects(rect_buf, rect_count); |
||||
|
||||
/* free fonts */ |
||||
if (has_free_commands) { |
||||
cmd = NULL; |
||||
while (next_command(&cmd)) { |
||||
if (cmd->type == FREE_FONT) |
||||
ren_free_font(cmd->font); |
||||
} |
||||
} |
||||
|
||||
/* swap cell buffer and reset */ |
||||
unsigned *tmp = cells; |
||||
cells = cells_prev; |
||||
cells_prev = tmp; |
||||
command_buf_idx = 0; |
||||
} |
@ -0,0 +1,16 @@ |
||||
#ifndef RENCACHE_H |
||||
#define RENCACHE_H |
||||
|
||||
#include <stdbool.h> |
||||
#include "renderer.h" |
||||
|
||||
void rencache_show_debug(bool enable); |
||||
void rencache_free_font(RenFont *font); |
||||
void rencache_set_clip_rect(RenRect rect); |
||||
void rencache_draw_rect(RenRect rect, RenColor color); |
||||
int rencache_draw_text(RenFont *font, const char *text, int x, int y, RenColor color); |
||||
void rencache_invalidate(void); |
||||
void rencache_begin_frame(void); |
||||
void rencache_end_frame(void); |
||||
|
||||
#endif |
@ -0,0 +1,378 @@ |
||||
#include <stdio.h> |
||||
#include <stdbool.h> |
||||
#include <assert.h> |
||||
#include <math.h> |
||||
#include "lib/stb/stb_truetype.h" |
||||
#include "renderer.h" |
||||
|
||||
#define MAX_GLYPHSET 256 |
||||
|
||||
struct RenImage { |
||||
RenColor *pixels; |
||||
int width, height; |
||||
}; |
||||
|
||||
typedef struct { |
||||
RenImage *image; |
||||
stbtt_bakedchar glyphs[256]; |
||||
} GlyphSet; |
||||
|
||||
struct RenFont { |
||||
void *data; |
||||
stbtt_fontinfo stbfont; |
||||
GlyphSet *sets[MAX_GLYPHSET]; |
||||
float size; |
||||
int height; |
||||
}; |
||||
|
||||
static SDL_Window *window; |
||||
static struct { int left, top, right, bottom; } clip; |
||||
|
||||
static void* check_alloc(void *ptr) |
||||
{ |
||||
if (!ptr) { |
||||
fprintf(stderr, "Fatal error: memory allocation failed\n"); |
||||
exit(EXIT_FAILURE); |
||||
} |
||||
return ptr; |
||||
} |
||||
|
||||
|
||||
static const char* utf8_to_codepoint(const char *p, unsigned *dst) |
||||
{ |
||||
unsigned res, n; |
||||
switch (*p & 0xf0) { |
||||
case 0xf0 : res = *p & 0x07; n = 3; break; |
||||
case 0xe0 : res = *p & 0x0f; n = 2; break; |
||||
case 0xd0 : |
||||
case 0xc0 : res = *p & 0x1f; n = 1; break; |
||||
default : res = *p; n = 0; break; |
||||
} |
||||
while (n--) |
||||
res = (res << 6) | (*(++p) & 0x3f); |
||||
*dst = res; |
||||
return p + 1; |
||||
} |
||||
|
||||
|
||||
void ren_init(SDL_Window *win) |
||||
{ |
||||
assert(win); |
||||
window = win; |
||||
SDL_Surface *surf = SDL_GetWindowSurface(window); |
||||
ren_set_clip_rect( (RenRect) { 0, 0, surf->w, surf->h } ); |
||||
} |
||||
|
||||
|
||||
void ren_update_rects(RenRect *rects, int count) |
||||
{ |
||||
SDL_UpdateWindowSurfaceRects(window, (SDL_Rect*) rects, count); |
||||
static bool initial_frame = true; |
||||
if (initial_frame) { |
||||
SDL_ShowWindow(window); |
||||
initial_frame = false; |
||||
} |
||||
} |
||||
|
||||
|
||||
void ren_set_clip_rect(RenRect rect) |
||||
{ |
||||
clip.left = rect.x; |
||||
clip.top = rect.y; |
||||
clip.right = rect.x + rect.width; |
||||
clip.bottom = rect.y + rect.height; |
||||
} |
||||
|
||||
|
||||
void ren_get_size(int *x, int *y) |
||||
{ |
||||
SDL_Surface *surf = SDL_GetWindowSurface(window); |
||||
*x = surf->w; |
||||
*y = surf->h; |
||||
} |
||||
|
||||
|
||||
RenImage* ren_new_image(int width, int height) |
||||
{ |
||||
assert(width > 0 && height > 0); |
||||
RenImage *image = malloc(sizeof(RenImage) + width * height * sizeof(RenColor)); |
||||
check_alloc(image); |
||||
image->pixels = (void*) (image + 1); |
||||
image->width = width; |
||||
image->height = height; |
||||
return image; |
||||
} |
||||
|
||||
|
||||
inline void ren_free_image(RenImage *image) |
||||
{ |
||||
free(image); |
||||
} |
||||
|
||||
|
||||
static GlyphSet* load_glyphset(RenFont *font, int idx) |
||||
{ |
||||
GlyphSet *set = check_alloc(calloc(1, sizeof(GlyphSet))); |
||||
|
||||
/* init image */ |
||||
int width = 128; |
||||
int height = 128; |
||||
retry: |
||||
set->image = ren_new_image(width, height); |
||||
|
||||
/* load glyphs */ |
||||
float s = |
||||
stbtt_ScaleForMappingEmToPixels(&font->stbfont, 1) / |
||||
stbtt_ScaleForPixelHeight(&font->stbfont, 1); |
||||
int res = stbtt_BakeFontBitmap( |
||||
font->data, 0, font->size * s, (void*) set->image->pixels, |
||||
width, height, idx * 256, 256, set->glyphs); |
||||
|
||||
/* retry with a larger image buffer if the buffer wasn't large enough */ |
||||
if (res < 0) { |
||||
width *= 2; |
||||
height *= 2; |
||||
ren_free_image(set->image); |
||||
goto retry; |
||||
} |
||||
|
||||
/* adjust glyph yoffsets and xadvance */ |
||||
int ascent, descent, linegap; |
||||
stbtt_GetFontVMetrics(&font->stbfont, &ascent, &descent, &linegap); |
||||
float scale = stbtt_ScaleForMappingEmToPixels(&font->stbfont, font->size); |
||||
int scaled_ascent = ascent * scale + 0.5; |
||||
for (int i = 0; i < 256; i++) { |
||||
set->glyphs[i].yoff += scaled_ascent; |
||||
set->glyphs[i].xadvance = floor(set->glyphs[i].xadvance); |
||||
} |
||||
|
||||
/* convert 8bit data to 32bit */ |
||||
for (int i = width * height - 1; i >= 0; i--) { |
||||
uint8_t n = *((uint8_t*) set->image->pixels + i); |
||||
set->image->pixels[i] = (RenColor) { .r = 255, .g = 255, |
||||
.b = 255, .a = n }; |
||||
} |
||||
|
||||
return set; |
||||
} |
||||
|
||||
|
||||
static GlyphSet* get_glyphset(RenFont *font, int codepoint) |
||||
{ |
||||
int idx = (codepoint >> 8) % MAX_GLYPHSET; |
||||
if (!font->sets[idx]) |
||||
font->sets[idx] = load_glyphset(font, idx); |
||||
return font->sets[idx]; |
||||
} |
||||
|
||||
|
||||
RenFont* ren_load_font(const char *filename, float size) |
||||
{ |
||||
RenFont *font = NULL; |
||||
FILE *fp = NULL; |
||||
|
||||
/* init font */ |
||||
font = check_alloc(calloc(1, sizeof(RenFont))); |
||||
font->size = size; |
||||
|
||||
/* load font into buffer */ |
||||
fp = fopen(filename, "rb"); |
||||
if (!fp)
|
||||
return NULL; |
||||
/* get size */ |
||||
fseek(fp, 0, SEEK_END); |
||||
int buf_size = ftell(fp); |
||||
fseek(fp, 0, SEEK_SET); |
||||
/* load */ |
||||
font->data = check_alloc(malloc(buf_size)); |
||||
int _ = fread(font->data, 1, buf_size, fp); (void) _; |
||||
fclose(fp); |
||||
fp = NULL; |
||||
|
||||
/* init stbfont */ |
||||
int ok = stbtt_InitFont(&font->stbfont, font->data, 0); |
||||
if (!ok) |
||||
goto fail; |
||||
|
||||
/* get height and scale */ |
||||
int ascent, descent, linegap; |
||||
stbtt_GetFontVMetrics(&font->stbfont, &ascent, &descent, &linegap); |
||||
float scale = stbtt_ScaleForMappingEmToPixels(&font->stbfont, size); |
||||
font->height = (ascent - descent + linegap) * scale + 0.5; |
||||
|
||||
/* make tab and newline glyphs invisible */ |
||||
stbtt_bakedchar *g = get_glyphset(font, '\n')->glyphs; |
||||
g['\t'].x1 = g['\t'].x0; |
||||
g['\n'].x1 = g['\n'].x0; |
||||
|
||||
return font; |
||||
|
||||
fail: |
||||
if (fp) |
||||
fclose(fp); |
||||
if (font)
|
||||
free(font->data); |
||||
free(font); |
||||
return NULL; |
||||
} |
||||
|
||||
|
||||
void ren_free_font(RenFont *font) |
||||
{ |
||||
for (int i = 0; i < MAX_GLYPHSET; i++) { |
||||
GlyphSet *set = font->sets[i]; |
||||
if (set) { |
||||
ren_free_image(set->image); |
||||
free(set); |
||||
} |
||||
} |
||||
free(font->data); |
||||
free(font); |
||||
} |
||||
|
||||
|
||||
void ren_set_font_tab_width(RenFont *font, int n) |
||||
{ |
||||
GlyphSet *set = get_glyphset(font, '\t'); |
||||
set->glyphs['\t'].xadvance = n; |
||||
} |
||||
|
||||
|
||||
int ren_get_font_tab_width(RenFont *font) |
||||
{ |
||||
GlyphSet *set = get_glyphset(font, '\t'); |
||||
return set->glyphs['\t'].xadvance; |
||||
} |
||||
|
||||
|
||||
int ren_get_font_width(RenFont *font, const char *text) |
||||
{ |
||||
int x = 0; |
||||
const char *p = text; |
||||
unsigned codepoint; |
||||
while (*p) { |
||||
p = utf8_to_codepoint(p, &codepoint); |
||||
GlyphSet *set = get_glyphset(font, codepoint); |
||||
stbtt_bakedchar *g = &set->glyphs[codepoint & 0xff]; |
||||
x += g->xadvance; |
||||
} |
||||
return x; |
||||
} |
||||
|
||||
|
||||
inline int ren_get_font_height(RenFont *font) |
||||
{ |
||||
return font->height; |
||||
} |
||||
|
||||
|
||||
static inline RenColor blend_pixel(RenColor dst, RenColor src) |
||||
{ |
||||
int ia = 0xff - src.a; |
||||
dst.r = ((src.r * src.a) + (dst.r * ia)) >> 8; |
||||
dst.g = ((src.g * src.a) + (dst.g * ia)) >> 8; |
||||
dst.b = ((src.b * src.a) + (dst.b * ia)) >> 8; |
||||
return dst; |
||||
} |
||||
|
||||
|
||||
static inline RenColor blend_pixel2(RenColor dst, RenColor src, RenColor color) { |
||||
src.a = (src.a * color.a) >> 8; |
||||
int ia = 0xff - src.a; |
||||
dst.r = ((src.r * color.r * src.a) >> 16) + ((dst.r * ia) >> 8); |
||||
dst.g = ((src.g * color.g * src.a) >> 16) + ((dst.g * ia) >> 8); |
||||
dst.b = ((src.b * color.b * src.a) >> 16) + ((dst.b * ia) >> 8); |
||||
return dst; |
||||
} |
||||
|
||||
|
||||
#define rect_draw_loop(expr) \ |
||||
{ \
|
||||
for (int j = y1; j < y2; j++) { \
|
||||
for (int i = x1; i < x2; i++) { \
|
||||
*d = expr; \
|
||||
d++; \
|
||||
} \
|
||||
d += dr; \
|
||||
} \
|
||||
} |
||||
|
||||
void ren_draw_rect(RenRect rect, RenColor color) |
||||
{ |
||||
if (color.a == 0) |
||||
return; |
||||
|
||||
int x1 = rect.x < clip.left ? clip.left : rect.x; |
||||
int y1 = rect.y < clip.top ? clip.top : rect.y; |
||||
int x2 = rect.x + rect.width; |
||||
int y2 = rect.y + rect.height; |
||||
x2 = x2 > clip.right ? clip.right : x2; |
||||
y2 = y2 > clip.bottom ? clip.bottom : y2; |
||||
|
||||
SDL_Surface *surf = SDL_GetWindowSurface(window); |
||||
RenColor *d = (RenColor*) surf->pixels; |
||||
d += x1 + y1 * surf->w; |
||||
int dr = surf->w - (x2 - x1); |
||||
|
||||
if (color.a == 0xff) { |
||||
rect_draw_loop(color); |
||||
} else { |
||||
rect_draw_loop(blend_pixel(*d, color)); |
||||
} |
||||
} |
||||
|
||||
|
||||
void ren_draw_image(RenImage *image, RenRect *sub, int x, int y, RenColor color) |
||||
{ |
||||
if (color.a == 0)
|
||||
return; |
||||
|
||||
/* clip */ |
||||
int n; |
||||
if ((n = clip.left - x) > 0) { sub->width -= n; sub->x += n; x += n; } |
||||
if ((n = clip.top - y) > 0) { sub->height -= n; sub->y += n; y += n; } |
||||
if ((n = x + sub->width - clip.right ) > 0) { sub->width -= n; } |
||||
if ((n = y + sub->height - clip.bottom) > 0) { sub->height -= n; } |
||||
|
||||
if (sub->width <= 0 || sub->height <= 0) |
||||
return; |
||||
|
||||
/* draw */ |
||||
SDL_Surface *surf = SDL_GetWindowSurface(window); |
||||
RenColor *s = image->pixels; |
||||
RenColor *d = (RenColor*) surf->pixels; |
||||
s += sub->x + sub->y * image->width; |
||||
d += x + y * surf->w; |
||||
int sr = image->width - sub->width; |
||||
int dr = surf->w - sub->width; |
||||
|
||||
for (int j = 0; j < sub->height; j++) { |
||||
for (int i = 0; i < sub->width; i++) { |
||||
*d = blend_pixel2(*d, *s, color); |
||||
d++; |
||||
s++; |
||||
} |
||||
d += dr; |
||||
s += sr; |
||||
} |
||||
} |
||||
|
||||
|
||||
int ren_draw_text(RenFont *font, const char *text, int x, int y, RenColor color) |
||||
{ |
||||
RenRect rect; |
||||
const char *p = text; |
||||
unsigned codepoint; |
||||
while (*p) { |
||||
p = utf8_to_codepoint(p, &codepoint); |
||||
GlyphSet *set = get_glyphset(font, codepoint); |
||||
stbtt_bakedchar *g = &set->glyphs[codepoint & 0xff]; |
||||
rect.x = g->x0; |
||||
rect.y = g->y0; |
||||
rect.width = g->x1 - g->x0; |
||||
rect.height = g->y1 - g->y0; |
||||
ren_draw_image(set->image, &rect, x + g->xoff, y + g->yoff, color); |
||||
x += g->xadvance; |
||||
} |
||||
return x; |
||||
} |
@ -0,0 +1,33 @@ |
||||
#ifndef RENDERER_H |
||||
#define RENDERER_H |
||||
|
||||
#include <SDL2/SDL.h> |
||||
#include <stdint.h> |
||||
|
||||
typedef struct RenImage RenImage; |
||||
typedef struct RenFont RenFont; |
||||
|
||||
typedef struct { uint8_t b, g, r, a; } RenColor; |
||||
typedef struct { int x, y, width, height; } RenRect; |
||||
|
||||
|
||||
void ren_init(SDL_Window *win); |
||||
void ren_update_rects(RenRect *rects, int count); |
||||
void ren_set_clip_rect(RenRect rect); |
||||
void ren_get_size(int *x, int *y); |
||||
|
||||
RenImage* ren_new_image(int width, int height); |
||||
void ren_free_image(RenImage *image); |
||||
|
||||
RenFont* ren_load_font(const char *filename, float size); |
||||
void ren_free_font(RenFont *font); |
||||
void ren_set_font_tab_width(RenFont *font, int n); |
||||
int ren_get_font_tab_width(RenFont *font); |
||||
int ren_get_font_width(RenFont *font, const char *text); |
||||
int ren_get_font_height(RenFont *font); |
||||
|
||||
void ren_draw_rect(RenRect rect, RenColor color); |
||||
void ren_draw_image(RenImage *image, RenRect *sub, int x, int y, RenColor color); |
||||
int ren_draw_text(RenFont *font, const char *text, int x, int y, RenColor color); |
||||
|
||||
#endif |
Loading…
Reference in new issue