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 |