Initial commit
This commit is contained in:
commit
d8c4bfa6ba
|
@ -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)
|
||||
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 offsety = self:get_line_text_y_offset()
|
||||
local x, _ = self:get_line_screen_position()
|
||||
local h = 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,129 @@
|
|||
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 i, 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)
|
||||