Initial commit

This commit is contained in:
rxi
2019-12-28 11:17:56 +00:00
commit d8c4bfa6ba
486 changed files with 146040 additions and 0 deletions
+240
View File
@@ -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",
}
+61
View File
@@ -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
+27
View File
@@ -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,
})
+23
View File
@@ -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 = {},
}
+45
View File
@@ -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",
},
}
+54
View File
@@ -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",
}
}
+20
View File
@@ -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 = {},
}
+69
View File
@@ -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",
}
+245
View File
@@ -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",
}
+30
View File
@@ -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",
}
+63
View File
@@ -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"
}
+60
View File
@@ -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,
})
+197
View File
@@ -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" }
+29
View File
@@ -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