Initial commit
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local command = require "core.command"
|
||||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local translate = require "core.doc.translate"
|
||||
local RootView = require "core.rootview"
|
||||
local DocView = require "core.docview"
|
||||
|
||||
local max_suggestions = 6
|
||||
|
||||
local symbols = {}
|
||||
|
||||
|
||||
core.add_thread(function()
|
||||
local cache = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function get_symbols(doc)
|
||||
local i = 1
|
||||
local s = {}
|
||||
while i < #doc.lines do
|
||||
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
|
||||
s[sym] = true
|
||||
end
|
||||
i = i + 1
|
||||
if i % 100 == 0 then coroutine.yield() end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function cache_is_valid(doc)
|
||||
local c = cache[doc]
|
||||
return c and c.last_change_id == doc:get_change_id()
|
||||
end
|
||||
|
||||
while true do
|
||||
-- lift all symbols from all docs
|
||||
local t = {}
|
||||
for _, doc in ipairs(core.docs) do
|
||||
-- update the cache if the doc has changed since the last iteration
|
||||
if not cache_is_valid(doc) then
|
||||
cache[doc] = {
|
||||
last_change_id = doc:get_change_id(),
|
||||
symbols = get_symbols(doc)
|
||||
}
|
||||
end
|
||||
-- update symbol set with doc's symbol set
|
||||
for sym in pairs(cache[doc].symbols) do
|
||||
t[sym] = true
|
||||
end
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
-- update symbols list
|
||||
symbols = {}
|
||||
for sym in pairs(t) do
|
||||
table.insert(symbols, sym)
|
||||
end
|
||||
|
||||
-- wait for next scan
|
||||
local valid = true
|
||||
while valid do
|
||||
coroutine.yield(1)
|
||||
for _, doc in ipairs(core.docs) do
|
||||
if not cache_is_valid(doc) then
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
local partial = ""
|
||||
local suggestions_idx = 1
|
||||
local suggestions = {}
|
||||
local last_active_view
|
||||
local last_line, last_col
|
||||
|
||||
|
||||
local function reset_suggestions()
|
||||
suggestions_idx = 1
|
||||
suggestions = {}
|
||||
end
|
||||
|
||||
|
||||
local function get_partial_symbol()
|
||||
local doc = core.active_view.doc
|
||||
local line2, col2 = doc:get_selection()
|
||||
local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
|
||||
return doc:get_text(line1, col1, line2, col2)
|
||||
end
|
||||
|
||||
|
||||
local function get_active_view()
|
||||
if getmetatable(core.active_view) == DocView then
|
||||
last_active_view = core.active_view
|
||||
return core.active_view
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function get_suggestions_rect(av)
|
||||
if #suggestions == 0 then
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
|
||||
local line, col = av.doc:get_selection()
|
||||
local x, y = av:get_line_screen_position(line)
|
||||
x = x + av:get_col_x_offset(line, col - #partial)
|
||||
y = y + av:get_line_height() + style.padding.y
|
||||
local font = av:get_font()
|
||||
local th = font:get_height()
|
||||
|
||||
local max_width = 0
|
||||
for i, sym in ipairs(suggestions) do
|
||||
max_width = math.max(max_width, font:get_width(sym))
|
||||
end
|
||||
|
||||
return
|
||||
x - style.padding.x,
|
||||
y - style.padding.y,
|
||||
max_width + style.padding.x * 2,
|
||||
#suggestions * (th + style.padding.y) + style.padding.y
|
||||
end
|
||||
|
||||
|
||||
local function draw_suggestions_box(av)
|
||||
-- draw background rect
|
||||
local rx, ry, rw, rh = get_suggestions_rect(av)
|
||||
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
||||
|
||||
-- draw text
|
||||
local font = av:get_font()
|
||||
local th = font:get_height()
|
||||
local x, y = rx + style.padding.x, ry + style.padding.y
|
||||
for i, sym in ipairs(suggestions) do
|
||||
local color = (i == suggestions_idx) and style.accent or style.text
|
||||
renderer.draw_text(font, sym, x, y, color)
|
||||
y = y + th + style.padding.y
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- patch event logic into RootView
|
||||
local on_text_input = RootView.on_text_input
|
||||
local update = RootView.update
|
||||
local draw = RootView.draw
|
||||
|
||||
|
||||
RootView.on_text_input = function(...)
|
||||
on_text_input(...)
|
||||
|
||||
local av = get_active_view()
|
||||
if av then
|
||||
-- update partial symbol and suggestions
|
||||
partial = get_partial_symbol()
|
||||
if #partial >= 3 then
|
||||
local t = common.fuzzy_match(symbols, partial)
|
||||
for i = 1, max_suggestions do
|
||||
suggestions[i] = t[i]
|
||||
end
|
||||
last_line, last_col = av.doc:get_selection()
|
||||
else
|
||||
reset_suggestions()
|
||||
end
|
||||
|
||||
-- scroll if rect is out of bounds of view
|
||||
local _, y, _, h = get_suggestions_rect(av)
|
||||
local limit = av.position.y + av.size.y
|
||||
if y + h > limit then
|
||||
av.scroll.to.y = av.scroll.y + y + h - limit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
RootView.update = function(...)
|
||||
update(...)
|
||||
|
||||
local av = get_active_view()
|
||||
if av then
|
||||
-- reset suggestions if caret was moved
|
||||
local line, col = av.doc:get_selection()
|
||||
if line ~= last_line or col ~= last_col then
|
||||
reset_suggestions()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
RootView.draw = function(...)
|
||||
draw(...)
|
||||
|
||||
local av = get_active_view()
|
||||
if av then
|
||||
-- draw suggestions box after everything else
|
||||
core.root_view:defer_draw(draw_suggestions_box, av)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function predicate()
|
||||
return get_active_view() and #suggestions > 0
|
||||
end
|
||||
|
||||
|
||||
command.add(predicate, {
|
||||
["autocomplete:complete"] = function()
|
||||
local doc = core.active_view.doc
|
||||
local line, col = doc:get_selection()
|
||||
local text = suggestions[suggestions_idx]
|
||||
doc:insert(line, col, text)
|
||||
doc:remove(line, col, line, col - #partial)
|
||||
doc:set_selection(line, col + #text - #partial)
|
||||
reset_suggestions()
|
||||
end,
|
||||
|
||||
["autocomplete:previous"] = function()
|
||||
suggestions_idx = math.max(suggestions_idx - 1, 1)
|
||||
end,
|
||||
|
||||
["autocomplete:next"] = function()
|
||||
suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
|
||||
end,
|
||||
|
||||
["autocomplete:cancel"] = function()
|
||||
reset_suggestions()
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
keymap.add {
|
||||
["tab"] = "autocomplete:complete",
|
||||
["up"] = "autocomplete:previous",
|
||||
["down"] = "autocomplete:next",
|
||||
["escape"] = "autocomplete:cancel",
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
local core = require "core"
|
||||
local config = require "core.config"
|
||||
local Doc = require "core.doc"
|
||||
|
||||
|
||||
local times = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function update_time(doc)
|
||||
local info = system.get_file_info(doc.filename)
|
||||
times[doc] = info.modified
|
||||
end
|
||||
|
||||
|
||||
local function reload_doc(doc)
|
||||
local fp = io.open(doc.filename, "r")
|
||||
local text = fp:read("*a")
|
||||
fp:close()
|
||||
|
||||
local sel = { doc:get_selection() }
|
||||
doc:remove(1, 1, math.huge, math.huge)
|
||||
doc:insert(1, 1, text:sub(1, -2))
|
||||
doc:set_selection(table.unpack(sel))
|
||||
|
||||
update_time(doc)
|
||||
doc:clean()
|
||||
core.log_quiet("Auto-reloaded doc %q", doc.filename)
|
||||
end
|
||||
|
||||
|
||||
core.add_thread(function()
|
||||
while true do
|
||||
-- check all doc modified times
|
||||
for _, doc in ipairs(core.docs) do
|
||||
local info = system.get_file_info(doc.filename or "")
|
||||
if info and times[doc] ~= info.modified then
|
||||
reload_doc(doc)
|
||||
end
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
-- wait for next scan
|
||||
coroutine.yield(config.project_scan_rate)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- patch `Doc.save|load` to store modified time
|
||||
local load = Doc.load
|
||||
local save = Doc.save
|
||||
|
||||
Doc.load = function(self, ...)
|
||||
local res = load(self, ...)
|
||||
update_time(self)
|
||||
return res
|
||||
end
|
||||
|
||||
Doc.save = function(self, ...)
|
||||
local res = save(self, ...)
|
||||
update_time(self)
|
||||
return res
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
|
||||
|
||||
local function exec(cmd)
|
||||
local fp = io.popen(cmd, "r")
|
||||
local res = fp:read("*a")
|
||||
fp:close()
|
||||
return res:gsub("%\n$", "")
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["exec:insert"] = function()
|
||||
core.command_view:enter("Insert Result Of Command", function(cmd)
|
||||
core.active_view.doc:text_input(exec(cmd))
|
||||
end)
|
||||
end,
|
||||
|
||||
["exec:replace"] = function()
|
||||
core.command_view:enter("Replace With Result Of Command", function(cmd)
|
||||
core.active_view.doc:replace(function(str)
|
||||
return exec(string.format("echo %q | %s", str, cmd))
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
files = { "%.css$" },
|
||||
patterns = {
|
||||
{ pattern = "\\.", type = "normal" },
|
||||
{ pattern = "//.-\n", type = "comment" },
|
||||
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||
{ pattern = "[%a][%w-]*%s*%f[:]", type = "keyword" },
|
||||
{ pattern = "#%x+", type = "string" },
|
||||
{ pattern = "-?%d+[%d%.]*p[xt]", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.]*deg", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.]*", type = "number" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
{ pattern = "#[%a][%w_-]*", type = "keyword2" },
|
||||
{ pattern = "@[%a][%w_-]*", type = "keyword2" },
|
||||
{ pattern = "%.[%a][%w_-]*", type = "keyword2" },
|
||||
{ pattern = "[{}:]", type = "operator" },
|
||||
},
|
||||
symbols = {},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
files = { "%.js$", "%.json$", "%.cson$" },
|
||||
comment = "//",
|
||||
patterns = {
|
||||
{ pattern = "//.-\n", type = "comment" },
|
||||
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||
{ pattern = "-?%.?%d+", type = "number" },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
},
|
||||
symbols = {
|
||||
["if"] = "keyword",
|
||||
["then"] = "keyword",
|
||||
["else"] = "keyword",
|
||||
["do"] = "keyword",
|
||||
["while"] = "keyword",
|
||||
["for"] = "keyword",
|
||||
["break"] = "keyword",
|
||||
["continue"] = "keyword",
|
||||
["return"] = "keyword",
|
||||
["switch"] = "keyword",
|
||||
["case"] = "keyword",
|
||||
["const"] = "keyword",
|
||||
["try"] = "keyword",
|
||||
["catch"] = "keyword",
|
||||
["throw"] = "keyword",
|
||||
["var"] = "keyword",
|
||||
["let"] = "keyword",
|
||||
["get"] = "keyword",
|
||||
["set"] = "keyword",
|
||||
["function"] = "keyword",
|
||||
["new"] = "keyword",
|
||||
["this"] = "keyword2",
|
||||
["true"] = "literal",
|
||||
["false"] = "literal",
|
||||
["null"] = "literal",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
files = "%.py$",
|
||||
comment = "#",
|
||||
patterns = {
|
||||
{ pattern = { "#", "\n" }, type = "comment" },
|
||||
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
|
||||
{ pattern = { '"""', '"""' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||
{ pattern = "-?%.?%d+", type = "number" },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
},
|
||||
symbols = {
|
||||
["class"] = "keyword",
|
||||
["finally"] = "keyword",
|
||||
["is"] = "keyword",
|
||||
["return"] = "keyword",
|
||||
["continue"] = "keyword",
|
||||
["for"] = "keyword",
|
||||
["lambda"] = "keyword",
|
||||
["try"] = "keyword",
|
||||
["def"] = "keyword",
|
||||
["from"] = "keyword",
|
||||
["nonlocal"] = "keyword",
|
||||
["while"] = "keyword",
|
||||
["and"] = "keyword",
|
||||
["global"] = "keyword",
|
||||
["not"] = "keyword",
|
||||
["with"] = "keyword",
|
||||
["as"] = "keyword",
|
||||
["elif"] = "keyword",
|
||||
["if"] = "keyword",
|
||||
["or"] = "keyword",
|
||||
["else"] = "keyword",
|
||||
["import"] = "keyword",
|
||||
["pass"] = "keyword",
|
||||
["break"] = "keyword",
|
||||
["except"] = "keyword",
|
||||
["in"] = "keyword",
|
||||
["del"] = "keyword",
|
||||
["raise"] = "keyword",
|
||||
["yield"] = "keyword",
|
||||
["assert"] = "keyword",
|
||||
["self"] = "keyword2",
|
||||
["None"] = "literal",
|
||||
["True"] = "literal",
|
||||
["False"] = "literal",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
local syntax = require "core.syntax"
|
||||
|
||||
syntax.add {
|
||||
files = { "%.xml$", "%.html?$" },
|
||||
patterns = {
|
||||
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
|
||||
{ pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" },
|
||||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||
{ pattern = "-?%d+[%d%.]*f?", type = "number" },
|
||||
{ pattern = "-?%.?%d+f?", type = "number" },
|
||||
{ pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" },
|
||||
{ pattern = "%f[^<][%a_][%w_]*", type = "function" },
|
||||
{ pattern = "%f[^<]/[%a_][%w_]*", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "keyword" },
|
||||
{ pattern = "[/<>=]", type = "operator" },
|
||||
},
|
||||
symbols = {},
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local keymap = require "core.keymap"
|
||||
|
||||
local handled_events = {
|
||||
["keypressed"] = true,
|
||||
["keyreleased"] = true,
|
||||
["textinput"] = true,
|
||||
}
|
||||
|
||||
local state = "stopped"
|
||||
local event_buffer = {}
|
||||
local modkeys = {}
|
||||
|
||||
local on_event = core.on_event
|
||||
|
||||
core.on_event = function(type, ...)
|
||||
local res = on_event(type, ...)
|
||||
if state == "recording" and handled_events[type] then
|
||||
table.insert(event_buffer, { type, ... })
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local function clone(t)
|
||||
local res = {}
|
||||
for k, v in pairs(t) do res[k] = v end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local function predicate()
|
||||
return state ~= "playing"
|
||||
end
|
||||
|
||||
|
||||
command.add(predicate, {
|
||||
["macro:toggle-record"] = function()
|
||||
if state == "stopped" then
|
||||
state = "recording"
|
||||
event_buffer = {}
|
||||
modkeys = clone(keymap.modkeys)
|
||||
core.log("Recording macro...")
|
||||
else
|
||||
state = "stopped"
|
||||
core.log("Stopped recording macro (%d events)", #event_buffer)
|
||||
end
|
||||
end,
|
||||
|
||||
["macro:play"] = function()
|
||||
state = "playing"
|
||||
core.log("Playing macro... (%d events)", #event_buffer)
|
||||
local mk = keymap.modkeys
|
||||
keymap.modkeys = clone(modkeys)
|
||||
for _, ev in ipairs(event_buffer) do
|
||||
on_event(table.unpack(ev))
|
||||
core.root_view:update()
|
||||
end
|
||||
keymap.modkeys = mk
|
||||
state = "stopped"
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
keymap.add {
|
||||
["ctrl+shift+;"] = "macro:toggle-record",
|
||||
["ctrl+;"] = "macro:play",
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local keymap = require "core.keymap"
|
||||
local command = require "core.command"
|
||||
local style = require "core.style"
|
||||
local View = require "core.view"
|
||||
|
||||
|
||||
local ResultsView = View:extend()
|
||||
|
||||
|
||||
function ResultsView:new(text, fn)
|
||||
ResultsView.super.new(self)
|
||||
self.scrollable = true
|
||||
self:begin_search(text, fn)
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:get_name()
|
||||
return "Search Results"
|
||||
end
|
||||
|
||||
|
||||
local function find_all_matches_in_file(t, filename, fn)
|
||||
local fp = io.open(filename)
|
||||
if not fp then return t end
|
||||
local n = 1
|
||||
for line in fp:lines() do
|
||||
local s = fn(line)
|
||||
if s then
|
||||
table.insert(t, { file = filename, text = line, line = n, col = s })
|
||||
core.redraw = true
|
||||
end
|
||||
if n % 100 == 0 then coroutine.yield() end
|
||||
n = n + 1
|
||||
core.redraw = true
|
||||
end
|
||||
fp:close()
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:begin_search(text, fn)
|
||||
self.results = {}
|
||||
self.last_file_idx = 1
|
||||
self.query = text
|
||||
self.searching = true
|
||||
self.selected_idx = 0
|
||||
|
||||
core.add_thread(function()
|
||||
for i, file in ipairs(core.project_files) do
|
||||
if file.type == "file" then
|
||||
find_all_matches_in_file(self.results, file.filename, fn)
|
||||
end
|
||||
self.last_file_idx = i
|
||||
end
|
||||
self.searching = false
|
||||
core.redraw = true
|
||||
end, self)
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:on_mouse_moved(mx, my, ...)
|
||||
ResultsView.super.on_mouse_moved(self, mx, my, ...)
|
||||
self.selected_idx = 0
|
||||
for i, item, x,y,w,h in self:each_visible_result() do
|
||||
if mx >= x and my >= y and mx < x + w and my < y + h then
|
||||
self.selected_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:on_mouse_pressed(...)
|
||||
local caught = ResultsView.super.on_mouse_pressed(self, ...)
|
||||
if not caught then
|
||||
self:open_selected_result()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:open_selected_result()
|
||||
local res = self.results[self.selected_idx]
|
||||
if not res then
|
||||
return
|
||||
end
|
||||
core.try(function()
|
||||
local dv = core.root_view:open_doc(core.open_doc(res.file))
|
||||
core.root_view.root_node:update_layout()
|
||||
dv.doc:set_selection(res.line, res.col)
|
||||
dv:scroll_to_line(res.line, false, true)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:update()
|
||||
self.scroll.to.y = math.max(0, self.scroll.to.y)
|
||||
ResultsView.super.update(self)
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:get_results_yoffset()
|
||||
return style.font:get_height() + style.padding.y * 3
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:get_line_height()
|
||||
return style.padding.y + style.font:get_height()
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:get_scrollable_size()
|
||||
local rh = style.padding.y + style.font:get_height()
|
||||
return self:get_results_yoffset() + #self.results * self:get_line_height()
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:get_visible_results_range()
|
||||
local lh = self:get_line_height()
|
||||
local oy = self:get_results_yoffset()
|
||||
local min = math.max(1, math.floor((self.scroll.y - oy) / lh))
|
||||
return min, min + math.floor(self.size.y / lh) + 1
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:each_visible_result()
|
||||
return coroutine.wrap(function()
|
||||
local lh = self:get_line_height()
|
||||
local x, y = self:get_content_offset()
|
||||
local min, max = self:get_visible_results_range()
|
||||
y = y + self:get_results_yoffset() + lh * (min - 1)
|
||||
for i = min, max do
|
||||
local item = self.results[i]
|
||||
if not item then break end
|
||||
coroutine.yield(i, item, x, y, self.size.x, lh)
|
||||
y = y + lh
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function ResultsView:draw()
|
||||
self:draw_background(style.background)
|
||||
|
||||
-- status
|
||||
local ox, oy = self:get_content_offset()
|
||||
local x, y = ox + style.padding.x, oy + style.padding.y
|
||||
local per = self.last_file_idx / #core.project_files
|
||||
local text
|
||||
if self.searching then
|
||||
text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
|
||||
per * 100, self.last_file_idx, #core.project_files,
|
||||
#self.results, self.query)
|
||||
else
|
||||
text = string.format("Found %d matches for %q",
|
||||
#self.results, self.query)
|
||||
end
|
||||
renderer.draw_text(style.font, text, x, y, style.text)
|
||||
|
||||
-- horizontal line
|
||||
local yoffset = self:get_results_yoffset()
|
||||
local x = ox + style.padding.x
|
||||
local w = self.size.x - style.padding.x * 2
|
||||
local h = style.divider_size
|
||||
renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, style.dim)
|
||||
if self.searching then
|
||||
renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text)
|
||||
end
|
||||
|
||||
-- results
|
||||
local y1, y2 = self.position.y, self.position.y + self.size.y
|
||||
for i, item, x,y,w,h in self:each_visible_result() do
|
||||
local color = style.text
|
||||
if i == self.selected_idx then
|
||||
color = style.accent
|
||||
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
||||
end
|
||||
x = x + style.padding.x
|
||||
local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col)
|
||||
x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h)
|
||||
x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h)
|
||||
end
|
||||
|
||||
self:draw_scrollbar()
|
||||
end
|
||||
|
||||
|
||||
local function begin_search(text, fn)
|
||||
if text == "" then
|
||||
core.error("Expected non-empty string")
|
||||
return
|
||||
end
|
||||
local rv = ResultsView(text, fn)
|
||||
core.root_view:get_active_node():add_view(rv)
|
||||
end
|
||||
|
||||
|
||||
command.add(nil, {
|
||||
["project-search:find"] = function()
|
||||
core.command_view:enter("Find Text In Project", function(text)
|
||||
text = text:lower()
|
||||
begin_search(text, function(line_text)
|
||||
return line_text:lower():find(text, nil, true)
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
|
||||
["project-search:find-pattern"] = function()
|
||||
core.command_view:enter("Find Pattern In Project", function(text)
|
||||
begin_search(text, function(line_text) return line_text:find(text) end)
|
||||
end)
|
||||
end,
|
||||
|
||||
["project-search:fuzzy-find"] = function()
|
||||
core.command_view:enter("Fuzzy Find Text In Project", function(text)
|
||||
begin_search(text, function(line_text)
|
||||
return common.fuzzy_match(line_text, text) and 1
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
command.add(ResultsView, {
|
||||
["project-search:select-previous"] = function()
|
||||
local view = core.active_view
|
||||
view.selected_idx = math.max(view.selected_idx - 1, 1)
|
||||
end,
|
||||
|
||||
["project-search:select-next"] = function()
|
||||
local view = core.active_view
|
||||
view.selected_idx = math.min(view.selected_idx + 1, #view.results)
|
||||
end,
|
||||
|
||||
["project-search:open-selected"] = function()
|
||||
core.active_view:open_selected_result()
|
||||
end,
|
||||
})
|
||||
|
||||
keymap.add {
|
||||
["ctrl+shift+f"] = "project-search:find",
|
||||
["up"] = "project-search:select-previous",
|
||||
["down"] = "project-search:select-next",
|
||||
["return"] = "project-search:open-selected",
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local keymap = require "core.keymap"
|
||||
|
||||
|
||||
local escapes = {
|
||||
["\\"] = "\\\\",
|
||||
["\""] = "\\\"",
|
||||
["\n"] = "\\n",
|
||||
["\r"] = "\\r",
|
||||
["\t"] = "\\t",
|
||||
["\b"] = "\\b",
|
||||
}
|
||||
|
||||
local function replace(chr)
|
||||
return escapes[chr] or string.format("\\x%02x", chr:byte())
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["quote:quote"] = function()
|
||||
core.active_view.doc:replace(function(text)
|
||||
return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"'
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
keymap.add {
|
||||
["ctrl+'"] = "quote:quote",
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
local core = require "core"
|
||||
local config = require "core.config"
|
||||
local command = require "core.command"
|
||||
local keymap = require "core.keymap"
|
||||
|
||||
|
||||
local function wordwrap_text(text, limit)
|
||||
local t = {}
|
||||
local n = 0
|
||||
|
||||
for word in text:gmatch("%S+") do
|
||||
if n + #word > limit then
|
||||
table.insert(t, "\n")
|
||||
n = 0
|
||||
elseif #t > 0 then
|
||||
table.insert(t, " ")
|
||||
end
|
||||
table.insert(t, word)
|
||||
n = n + #word + 1
|
||||
end
|
||||
|
||||
return table.concat(t)
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["reflow:reflow"] = function()
|
||||
local doc = core.active_view.doc
|
||||
doc:replace(function(text)
|
||||
local prefix_set = "[^%w\n%[%](){}`'\"]*"
|
||||
|
||||
-- get line prefix and trailing whitespace
|
||||
local prefix1 = text:match("^\n*" .. prefix_set)
|
||||
local prefix2 = text:match("\n(" .. prefix_set .. ")", #prefix1+1)
|
||||
local trailing = text:match("%s*$")
|
||||
if not prefix2 or prefix2 == "" then
|
||||
prefix2 = prefix1
|
||||
end
|
||||
|
||||
-- strip all line prefixes and trailing whitespace
|
||||
text = text:sub(#prefix1+1, -#trailing - 1):gsub("\n" .. prefix_set, "\n")
|
||||
|
||||
-- split into blocks, wordwrap and join
|
||||
local line_limit = config.line_limit - #prefix1
|
||||
local blocks = {}
|
||||
text = text:gsub("\n\n", "\0")
|
||||
for block in text:gmatch("%Z+") do
|
||||
table.insert(blocks, wordwrap_text(block, line_limit))
|
||||
end
|
||||
text = table.concat(blocks, "\n\n")
|
||||
|
||||
-- add prefix to start of lines
|
||||
text = prefix1 .. text:gsub("\n", "\n" .. prefix2) .. trailing
|
||||
|
||||
return text
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
keymap.add {
|
||||
["ctrl+shift+q"] = "reflow:reflow"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local translate = require "core.doc.translate"
|
||||
|
||||
|
||||
local function gmatch_to_array(text, ptn)
|
||||
local res = {}
|
||||
for x in text:gmatch(ptn) do
|
||||
table.insert(res, x)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local function tabularize_lines(lines, delim)
|
||||
local rows = {}
|
||||
local cols = {}
|
||||
|
||||
-- split lines at delimiters and get maximum width of columns
|
||||
local ptn = "[^" .. delim:sub(1,1):gsub("%W", "%%%1") .. "]+"
|
||||
for i, line in ipairs(lines) do
|
||||
rows[i] = gmatch_to_array(line, ptn)
|
||||
for j, col in ipairs(rows[i]) do
|
||||
cols[j] = math.max(#col, cols[j] or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- pad columns with space
|
||||
for _, row in ipairs(rows) do
|
||||
for i = 1, #row - 1 do
|
||||
row[i] = row[i] .. string.rep(" ", cols[i] - #row[i])
|
||||
end
|
||||
end
|
||||
|
||||
-- write columns back to lines array
|
||||
for i, line in ipairs(lines) do
|
||||
lines[i] = table.concat(rows[i], delim)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["tabularize:tabularize"] = function()
|
||||
core.command_view:enter("Tabularize On Delimiter", function(delim)
|
||||
if delim == "" then delim = " " end
|
||||
|
||||
local doc = core.active_view.doc
|
||||
local line1, col1, line2, col2, swap = doc:get_selection(true)
|
||||
line1, col1 = doc:position_offset(line1, col1, translate.start_of_line)
|
||||
line2, col2 = doc:position_offset(line2, col2, translate.end_of_line)
|
||||
doc:set_selection(line1, col1, line2, col2, swap)
|
||||
|
||||
doc:replace(function(text)
|
||||
local lines = gmatch_to_array(text, "[^\n]*\n?")
|
||||
tabularize_lines(lines, delim)
|
||||
return table.concat(lines)
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
local keymap = require "core.keymap"
|
||||
local style = require "core.style"
|
||||
local View = require "core.view"
|
||||
|
||||
|
||||
local TreeView = View:extend()
|
||||
|
||||
|
||||
local function get_depth(filename)
|
||||
local n = 0
|
||||
for sep in filename:gmatch("[\\/]") do
|
||||
n = n + 1
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
function TreeView:new()
|
||||
TreeView.super.new(self)
|
||||
self.scrollable = true
|
||||
self.focusable = false
|
||||
self.visible = true
|
||||
self.cache = {}
|
||||
end
|
||||
|
||||
|
||||
function TreeView:get_cached(item)
|
||||
local t = self.cache[item.filename]
|
||||
if not t then
|
||||
t = {}
|
||||
t.filename = item.filename
|
||||
t.abs_filename = system.absolute_path(item.filename)
|
||||
t.path, t.name = t.filename:match("^(.*)[\\/](.+)$")
|
||||
t.depth = get_depth(t.filename)
|
||||
t.type = item.type
|
||||
self.cache[t.filename] = t
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function TreeView:get_name()
|
||||
return "Project"
|
||||
end
|
||||
|
||||
|
||||
function TreeView:get_item_height()
|
||||
return style.font:get_height() + style.padding.y
|
||||
end
|
||||
|
||||
|
||||
function TreeView:check_cache()
|
||||
-- invalidate cache's skip values if project_files has changed
|
||||
if core.project_files ~= self.last_project_files then
|
||||
for _, v in pairs(self.cache) do
|
||||
v.skip = nil
|
||||
end
|
||||
self.last_project_files = core.project_files
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function TreeView:each_item()
|
||||
return coroutine.wrap(function()
|
||||
self:check_cache()
|
||||
local ox, oy = self:get_content_offset()
|
||||
local y = oy + style.padding.y
|
||||
local w = self.size.x
|
||||
local h = self:get_item_height()
|
||||
|
||||
local i = 1
|
||||
while i <= #core.project_files do
|
||||
local item = core.project_files[i]
|
||||
local cached = self:get_cached(item)
|
||||
|
||||
coroutine.yield(cached, ox, y, w, h)
|
||||
y = y + h
|
||||
i = i + 1
|
||||
|
||||
if not cached.expanded then
|
||||
if cached.skip then
|
||||
i = cached.skip
|
||||
else
|
||||
local depth = cached.depth
|
||||
while i <= #core.project_files do
|
||||
local filename = core.project_files[i].filename
|
||||
if get_depth(filename) <= depth then break end
|
||||
i = i + 1
|
||||
end
|
||||
cached.skip = i
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function TreeView:on_mouse_moved(px, py)
|
||||
self.hovered_item = nil
|
||||
for item, x,y,w,h in self:each_item() do
|
||||
if px > x and py > y and px <= x + w and py <= y + h then
|
||||
self.hovered_item = item
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function TreeView:on_mouse_pressed(button, x, y)
|
||||
if not self.hovered_item then
|
||||
return
|
||||
elseif self.hovered_item.type == "dir" then
|
||||
self.hovered_item.expanded = not self.hovered_item.expanded
|
||||
else
|
||||
core.try(function()
|
||||
core.root_view:open_doc(core.open_doc(self.hovered_item.filename))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function TreeView:update()
|
||||
self.scroll.to.y = math.max(0, self.scroll.to.y)
|
||||
|
||||
-- update width
|
||||
local dest = self.visible and config.treeview_size or 0
|
||||
self:move_towards(self.size, "x", dest)
|
||||
|
||||
TreeView.super.update(self)
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw()
|
||||
self:draw_background(style.background2)
|
||||
|
||||
local h = self:get_item_height()
|
||||
local icon_width = style.icon_font:get_width("D")
|
||||
local spacing = style.font:get_width(" ") * 2
|
||||
local root_depth = get_depth(core.project_dir) + 1
|
||||
|
||||
local doc = core.active_view.doc
|
||||
local active_filename = doc and system.absolute_path(doc.filename or "")
|
||||
|
||||
for item, x,y,w,h in self:each_item() do
|
||||
local color = style.text
|
||||
|
||||
-- highlight active_view doc
|
||||
if item.abs_filename == active_filename then
|
||||
color = style.accent
|
||||
end
|
||||
|
||||
-- hovered item background
|
||||
if item == self.hovered_item then
|
||||
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
||||
color = style.accent
|
||||
end
|
||||
|
||||
-- icons
|
||||
x = x + (item.depth - root_depth) * style.padding.x + style.padding.x
|
||||
if item.type == "dir" then
|
||||
local icon1 = item.expanded and "e" or "c"
|
||||
local icon2 = item.expanded and "D" or "d"
|
||||
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
|
||||
x = x + style.padding.x
|
||||
common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h)
|
||||
x = x + icon_width
|
||||
else
|
||||
x = x + style.padding.x
|
||||
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
|
||||
x = x + icon_width
|
||||
end
|
||||
|
||||
-- text
|
||||
x = x + spacing
|
||||
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- init
|
||||
local view = TreeView()
|
||||
local node = core.root_view:get_active_node()
|
||||
view.size.x = config.treeview_size
|
||||
node:split("left", view, true)
|
||||
|
||||
-- register commands and keymap
|
||||
command.add(nil, {
|
||||
["treeview:toggle"] = function()
|
||||
view.visible = not view.visible
|
||||
end,
|
||||
})
|
||||
|
||||
keymap.add { ["ctrl+\\"] = "treeview:toggle" }
|
||||
@@ -0,0 +1,29 @@
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local Doc = require "core.doc"
|
||||
|
||||
|
||||
local function trim_trailing_whitespace(doc)
|
||||
for i = 1, #doc.lines do
|
||||
local old_text = doc:get_text(i, 1, i, math.huge)
|
||||
local new_text = old_text:gsub("%s*$", "")
|
||||
if old_text ~= new_text then
|
||||
doc:insert(i, 1, new_text)
|
||||
doc:remove(i, #new_text + 1, i, math.huge)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["trim-whitespace:trim-trailing-whitespace"] = function()
|
||||
trim_trailing_whitespace(self.active_view.doc)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
local save = Doc.save
|
||||
Doc.save = function(self, ...)
|
||||
trim_trailing_whitespace(self)
|
||||
save(self, ...)
|
||||
end
|
||||
Reference in New Issue
Block a user