commit
d8c4bfa6ba
486 changed files with 146040 additions and 0 deletions
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019 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,30 @@
|
||||
# lite |
||||
 |
||||
|
||||
A lightweight text editor written in Lua |
||||
|
||||
## Overview |
||||
lite is a lightweight text editor written mostly in Lua โ it aims to provide |
||||
something practical, pretty, *small* and responsive, implemented as simply as |
||||
possible; easy to modify and extend, or to use without doing either. |
||||
|
||||
## Get |
||||
Go to the [Releases](#TODO) page to download precompiled binaries for Windows or |
||||
Linux. Additional functionality can be added through plugins which are available |
||||
from the [plugins repository](#TODO). |
||||
|
||||
## Building |
||||
You can build the project yourself on Linux using the provided `build.py` |
||||
script. 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](#TODO) |
||||
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,29 @@
|
||||
import os |
||||
|
||||
cflags = [ "-Wall", "-O3", "-g", "-DLUA_USE_POPEN" ] |
||||
lflags = [ "-lSDL2", "-lm" ] |
||||
include = [ "src" ] |
||||
output = "lite" |
||||
|
||||
|
||||
if "sanitize" in opt: |
||||
log("address sanitizer enabled") |
||||
cflags += [ "-fsanitize=address" ] |
||||
lflags += [ "-fsanitize=address" ] |
||||
|
||||
|
||||
if "windows" in opt: |
||||
compiler = "x86_64-w64-mingw32-gcc" |
||||
output += ".exe" |
||||
cflags += [ "-Iwinlib/SDL2-2.0.10/x86_64-w64-mingw32/include" ] |
||||
lflags += [ "-Lwinlib/SDL2-2.0.10/x86_64-w64-mingw32/lib" ] |
||||
lflags = [ "-lmingw32", "-lSDL2main" ] + lflags |
||||
lflags += [ "-lwinmm" ] |
||||
lflags += [ "-mwindows" ] |
||||
lflags += [ "res.res" ] |
||||
|
||||
def pre(): |
||||
os.system("x86_64-w64-mingw32-windres res.rc -O coff -o res.res") |
||||
|
||||
def post(): |
||||
os.remove("res.res") |
@ -0,0 +1,306 @@
|
||||
#!/usr/bin/python2.7 |
||||
import os, sys, platform, shutil |
||||
import re, threading, time, json |
||||
from os import path |
||||
from hashlib import sha1 |
||||
from multiprocessing import cpu_count |
||||
|
||||
|
||||
config_file = "build.config.py" |
||||
cache_dir = ".buildcache" |
||||
object_dir = path.join(cache_dir, "obj") |
||||
cache_file = path.join(cache_dir, "cache.json") |
||||
max_workers = cpu_count() |
||||
|
||||
|
||||
config = { |
||||
"compiler" : "gcc", |
||||
"output" : "a.out", |
||||
"source" : [ "src" ], |
||||
"include" : [], |
||||
"cflags" : [], |
||||
"lflags" : [], |
||||
"run" : "./{output}" |
||||
} |
||||
|
||||
|
||||
Hint, Warn, Error = range(3) |
||||
log_prefix = { |
||||
Hint: "\x1b[32mHint:\x1b[0m", |
||||
Warn: "\x1b[33mWarn:\x1b[0m", |
||||
Error: "\x1b[31;1mError:\x1b[0m" |
||||
} |
||||
|
||||
|
||||
log_lock = threading.Lock() |
||||
|
||||
def log(msg, mode=Hint): |
||||
log_lock.acquire() |
||||
print log_prefix[mode], msg |
||||
log_lock.release() |
||||
|
||||
|
||||
def error(msg): |
||||
log(msg, mode=Error) |
||||
os._exit(1) |
||||
|
||||
|
||||
def load_config(filename): |
||||
""" loads the given config file into the `config` global dict """ |
||||
if not path.exists(filename): |
||||
error("config file does not exist: '%s'" % filename) |
||||
|
||||
d = { |
||||
"opt": sys.argv, |
||||
"platform": platform.system(), |
||||
"error": error, |
||||
"log": log, |
||||
"Hint": Hint, |
||||
"Warn": Warn, |
||||
"Error": Error |
||||
} |
||||
execfile(filename, d) |
||||
config.update(d) |
||||
|
||||
if len(config["source"]) == 0: |
||||
error("no source directories specified in config") |
||||
|
||||
|
||||
def load_cache(cache_file): |
||||
if not path.exists(cache_file): |
||||
return { "hashes": [], "cmd": "" } |
||||
with open(cache_file) as fp: |
||||
log("loaded cache") |
||||
return json.load(fp) |
||||
|
||||
|
||||
def update_cache(cache_file, obj): |
||||
with open(cache_file, "wb") as fp: |
||||
json.dump(obj, fp, indent=2) |
||||
log("updated cache") |
||||
|
||||
|
||||
def resolve_file(filename, dir): |
||||
""" finds the actual location of an included file """ |
||||
f = path.join(dir, filename) |
||||
if path.exists(f): |
||||
return short_name(f) |
||||
|
||||
for dir in config["include"]: |
||||
f = path.join(dir, filename) |
||||
if path.exists(f): |
||||
return short_name(f) |
||||
|
||||
|
||||
file_info_cache = {} |
||||
|
||||
def get_file_info(filename): |
||||
""" returns a dict of file info for the given file """ |
||||
if filename in file_info_cache: |
||||
return file_info_cache[filename] |
||||
|
||||
hash = sha1() |
||||
includes = [] |
||||
|
||||
with open(filename) as fp: |
||||
for line in fp.readlines(): |
||||
# get includes |
||||
if "#include" in line: |
||||
match = re.match('^\s*#include\s+"(.*?)"', line) |
||||
if match: |
||||
includes.append( match.group(1) ) |
||||
# update hash |
||||
hash.update(line) |
||||
hash.update("\n") |
||||
|
||||
res = { "hash": hash.hexdigest(), "includes": includes } |
||||
file_info_cache[filename] = res |
||||
return res |
||||
|
||||
|
||||
def short_name(filename): |
||||
""" returns the filename relative to the current path """ |
||||
n = len(path.abspath(".")) |
||||
return path.abspath(filename)[n+1:] |
||||
|
||||
|
||||
def get_deep_hash(filename): |
||||
""" creates a hash from the file and all its includes """ |
||||
h = sha1() |
||||
processed = set() |
||||
files = [ resolve_file(filename, ".") ] |
||||
|
||||
while len(files) > 0: |
||||
f = files.pop() |
||||
info = get_file_info(f) |
||||
processed.add(f) |
||||
|
||||
# update hash |
||||
h.update(info["hash"]) |
||||
|
||||
# add includes |
||||
for x in info["includes"]: |
||||
resolved = resolve_file(x, path.dirname(f)) |
||||
if resolved: |
||||
if resolved not in processed: |
||||
files.append(resolved) |
||||
else: |
||||
log("could not resolve file '%s'" % x, mode=Warn) |
||||
|
||||
return h.hexdigest() |
||||
|
||||
|
||||
def build_deep_hash_dict(cfiles): |
||||
""" returns a dict mapping each cfile to its hash """ |
||||
res = {} |
||||
for f in cfiles: |
||||
res[f] = get_deep_hash(f) |
||||
return res |
||||
|
||||
|
||||
def get_cfiles(): |
||||
""" returns all .h and .c files in source directories """ |
||||
res = [] |
||||
for dir in config["source"]: |
||||
for root, dirs, files in os.walk(dir): |
||||
for file in files: |
||||
if file.endswith((".c", ".h")): |
||||
f = path.join(root, file) |
||||
res.append( short_name(f) ) |
||||
return res |
||||
|
||||
|
||||
def build_compile_cmd(): |
||||
""" creates the command used to compile files """ |
||||
lst = [ |
||||
config["compiler"], |
||||
" ".join(map(lambda x: "-I" + x, config["include"])), |
||||
" ".join(config["cflags"]), |
||||
"-c", "{infile}", "-o", "{outfile}" |
||||
] |
||||
return " ".join(lst) |
||||
|
||||
|
||||
def obj_name(filename): |
||||
""" creates the object file name for a given filename """ |
||||
filename = re.sub("[^\w]+", "_", filename) |
||||
return filename[:-2] + "_" + sha1(filename).hexdigest()[:8] + ".o" |
||||
|
||||
|
||||
def compile(cmd, filename): |
||||
""" compiles the given file into an object file using the cmd """ |
||||
log("compiling '%s'" % filename) |
||||
|
||||
outfile = path.join(object_dir, obj_name(filename)) |
||||
|
||||
res = os.system(cmd.format(infile=filename, outfile=outfile)) |
||||
if res != 0: |
||||
error("failed to compile '%s'" % filename) |
||||
|
||||
|
||||
def link(): |
||||
""" links objects and outputs the final binary """ |
||||
log("linking") |
||||
lst = [ |
||||
config["compiler"], |
||||
"-o", config["output"], |
||||
path.join(object_dir, "*"), |
||||
" ".join(config["lflags"]) |
||||
] |
||||
cmd = " ".join(lst) |
||||
res = os.system(cmd) |
||||
if res != 0: |
||||
error("failed to link") |
||||
|
||||
|
||||
def parallel(func, workers=4): |
||||
""" runs func on multiple threads and waits for them all to finish """ |
||||
threads = [] |
||||
for i in range(workers): |
||||
t = threading.Thread(target=func) |
||||
threads.append(t) |
||||
t.start() |
||||
for t in threads: |
||||
t.join() |
||||
|
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
|
||||
start_time = time.time() |
||||
|
||||
load_config(config_file) |
||||
run_at_exit = False |
||||
output_dir = path.join(".", path.dirname(config["output"])) |
||||
cache = load_cache(cache_file) |
||||
cmd = build_compile_cmd() |
||||
|
||||
if "run" in sys.argv: |
||||
run_at_exit = True |
||||
|
||||
if cache["cmd"] != cmd: |
||||
sys.argv.append("clean") |
||||
|
||||
if "clean" in sys.argv: |
||||
log("performing clean build") |
||||
shutil.rmtree(cache_dir, ignore_errors=True) |
||||
cache = load_cache(cache_file) |
||||
|
||||
|
||||
if not path.exists(object_dir): |
||||
os.makedirs(object_dir) |
||||
|
||||
if not path.exists(output_dir): |
||||
os.makedirs(output_dir) |
||||
|
||||
|
||||
if "pre" in config: |
||||
config["pre"]() |
||||
|
||||
|
||||
cfiles = get_cfiles() |
||||
hashes = build_deep_hash_dict(cfiles) |
||||
|
||||
|
||||
# delete object files for cfiles that no longer exist |
||||
obj_files = set(map(obj_name, cfiles)) |
||||
for f in os.listdir(object_dir): |
||||
if f not in obj_files: |
||||
os.remove(path.join(object_dir, f)) |
||||
|
||||
|
||||
# build list of all .c files that need compiling |
||||
pending = [] |
||||
for f in cfiles: |
||||
if f.endswith(".c"): |
||||
if f not in cache["hashes"] or cache["hashes"][f] != hashes[f]: |
||||
pending.append(f) |
||||
|
||||
|
||||
# compile files until there are none left |
||||
def worker(): |
||||
while True: |
||||
try: |
||||
f = pending.pop() |
||||
except: |
||||
break |
||||
compile(cmd, f) |
||||
|
||||
|
||||
parallel(worker, workers=max_workers) |
||||
|
||||
|
||||
link() |
||||
update_cache(cache_file, { "hashes": hashes, "cmd": cmd }) |
||||
|
||||
if "post" in config: |
||||
config["post"]() |
||||
|
||||
|
||||
log("done [%.2fs]" % (time.time() - start_time)) |
||||
|
||||
|
||||
if run_at_exit: |
||||
log("running") |
||||
cmd = config["run"].format(output=config["output"]) |
||||
os.system(cmd) |
@ -0,0 +1,10 @@
|
||||
#!/bin/bash |
||||
./build.py release windows |
||||
./build.py release |
||||
rm lite.zip 2>/dev/null |
||||
cp winlib/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll SDL2.dll |
||||
strip lite |
||||
strip lite.exe |
||||
strip SDL2.dll |
||||
zip lite.zip lite lite.exe SDL2.dll data -r |
||||
|
@ -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,81 @@
|
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
local command = require "core.command" |
||||
local keymap = require "core.keymap" |
||||
local Doc = require "core.doc" |
||||
local DocView = require "core.docview" |
||||
local LogView = require "core.logview" |
||||
|
||||
|
||||
command.add(nil, { |
||||
["core:quit"] = function() |
||||
core.quit() |
||||
end, |
||||
|
||||
["core:force-quit"] = function() |
||||
core.quit(true) |
||||
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:do-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:new-doc"] = function() |
||||
core.root_view:open_doc(core.open_doc()) |
||||
end, |
||||
|
||||
["core:open-project-file"] = function() |
||||
core.command_view:enter("Open Project File", function(text, item) |
||||
text = core.project_dir .. _PATHSEP .. (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:sub(#core.project_dir + 2)) |
||||
end |
||||
end |
||||
return common.fuzzy_match(files, text) |
||||
end) |
||||
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, |
||||
}) |
@ -0,0 +1,335 @@
|
||||
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 search = require "core.doc.search" |
||||
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) |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
for line = line1, line2 do |
||||
doc():insert(line, 1, text) |
||||
end |
||||
doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap) |
||||
end |
||||
|
||||
|
||||
local function remove_from_start_of_selected_lines(text) |
||||
local line1, col1, line2, col2, swap = doc():get_selection(true) |
||||
for line = line1, line2 do |
||||
if doc().lines[line]:sub(1, #text) == text then |
||||
doc():remove(line, 1, line, #text + 1) |
||||
if line == line1 then col1 = col1 - #text end |
||||
if line == line2 then col2 = col2 - #text end |
||||
end |
||||
end |
||||
doc():set_selection(line1, col1, line2, col2, 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 %q", doc().filename) |
||||
end |
||||
|
||||
|
||||
local commands = { |
||||
["doc:undo"] = function() |
||||
doc():undo() |
||||
end, |
||||
|
||||
["doc:redo"] = function() |
||||
doc():redo() |
||||
end, |
||||
|
||||
["doc:cut"] = function() |
||||
local text = doc():get_text(doc():get_selection()) |
||||
system.set_clipboard(text) |
||||
doc():delete_to(0) |
||||
end, |
||||
|
||||
["doc:copy"] = function() |
||||
local text = doc():get_text(doc():get_selection()) |
||||
system.set_clipboard(text) |
||||
end, |
||||
|
||||
["doc:paste"] = function() |
||||
doc():text_input(system.get_clipboard()) |
||||
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-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 ]*", " ") |
||||
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() |
||||
if not dv().syntax.comment then return end |
||||
local text = dv().syntax.comment .. " " |
||||
local line1, _, line2 = doc():get_selection(true) |
||||
local uncomment = true |
||||
for line = line1, line2 do |
||||
local str = doc().lines[line]:match("^[ \t]*(.*)$") |
||||
if str and str:sub(1, #text) ~= text then |
||||
uncomment = false |
||||
break |
||||
end |
||||
end |
||||
if uncomment then |
||||
remove_from_start_of_selected_lines(text) |
||||
else |
||||
insert_at_start_of_selected_lines(text) |
||||
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: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:toggle-line-ending"] = function() |
||||
doc().crlf = not doc().crlf |
||||
end, |
||||
} |
||||
|
||||
|
||||
local translations = { |
||||
["previous-char"] = translate.previous_char, |
||||
["next-char"] = translate.next_char, |
||||
["previous-word-boundary"] = translate.previous_word_boundary, |
||||
["next-word-boundary"] = translate.next_word_boundary, |
||||
["previous-start-of-block"] = translate.previous_start_of_block, |
||||
["next-start-of-block"] = translate.next_start_of_block, |
||||
["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,134 @@
|
||||
local core = require "core" |
||||
local common = require "core.common" |
||||
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) |
||||
core.command_view.doc:set_selection(math.huge, math.huge, 1, 1) |
||||
|
||||
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)) |
||||
end |
||||
|
||||
end, function(text) |
||||
local ok, line1, col1, line2, col2 = pcall(search_fn, dv.doc, sel[1], sel[2], text) |
||||
if text == "" then |
||||
dv.doc:set_selection(table.unpack(sel)) |
||||
elseif ok and line1 then |
||||
dv.doc:set_selection(line2, col2, line1, col1) |
||||
dv:scroll_to_line(line2, true) |
||||
found = true |
||||
else |
||||
found = false |
||||
end |
||||
|
||||
end, function(explicit) |
||||
if explicit then |
||||
dv.doc:set_selection(table.unpack(sel)) |
||||
end |
||||
end) |
||||
end |
||||
|
||||
|
||||
local function replace(pattern_escape) |
||||
core.command_view:enter("Find To Replace", function(old) |
||||
core.command_view:enter("Replace \"" .. old .. "\" With", function(new) |
||||
local n = doc():replace(function(text) |
||||
if pattern_escape then |
||||
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) |
||||
else |
||||
return text:gsub(old, new) |
||||
end |
||||
end) |
||||
core.log("Replaced %d instance(s) of %q with %q", n, old, new) |
||||
end) |
||||
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(true) |
||||
end, |
||||
|
||||
["find-replace:replace-pattern"] = function() |
||||
replace(false) |
||||
end, |
||||
}) |
@ -0,0 +1,89 @@
|
||||
local core = require "core" |
||||
local DocView = require "core.docview" |
||||
local command = require "core.command" |
||||
|
||||
|
||||
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, |
||||
} |
||||
|
||||
|
||||
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 + 1) |
||||
else |
||||
x = node.position.x + node.size.x / 2 |
||||
y = node.position.y + (dir == "up" and -1 or node.size.y + 1) |
||||
end |
||||
local node = core.root_view.root_node:get_child_overlapping_point(x, y) |
||||
if not node:get_locked_size() then |
||||
core.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,255 @@
|
||||
local core = require "core" |
||||
local config = require "core.config" |
||||
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) |
||||
self.doc:remove(1, 1, math.huge, math.huge) |
||||
self.doc:text_input(text) |
||||
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, |
||||
view = core.active_view |
||||
} |
||||
core.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.active_view = self.state.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_gutter_text(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) |
||||