Compare commits

...

4 Commits

Author SHA1 Message Date
Francesco Abbate ff6fffb8c0 More work in ReplView 2020-08-27 17:10:11 +02:00
Francesco Abbate 84509904b1 More modifications to implement ReplView
First stub writing of ReplNode before starting to work on ReplView
itself.
2020-08-12 15:38:45 +02:00
Francesco Abbate 7b12d0503d Trying again with a different approach 2020-08-02 22:11:34 +02:00
Francesco Abbate 7dda8143e4 Initial working on REPL 2020-07-31 17:12:08 +02:00
7 changed files with 1310 additions and 0 deletions

330
data/core/InputDoc.lua Normal file
View File

@ -0,0 +1,330 @@
local Object = require "core.object"
local Highlighter = require "core.doc.highlighter"
local syntax = require "core.syntax"
local config = require "core.config"
local common = require "core.common"
local InputDoc = Object:extend()
local function split_lines(text)
local res = {}
for line in (text .. "\n"):gmatch("(.-)\n") do
table.insert(res, line)
end
return res
end
local function splice(t, at, remove, insert)
insert = insert or {}
local offset = #insert - remove
local old_len = #t
if offset < 0 then
for i = at - offset, old_len - offset do
t[i + offset] = t[i]
end
elseif offset > 0 then
for i = old_len, at, -1 do
t[i + offset] = t[i]
end
end
for i, item in ipairs(insert) do
t[at + i - 1] = item
end
end
function InputDoc:new(syntax)
self.syntax = syntax
self:reset()
end
function InputDoc:reset()
self.lines = { "\n" }
self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } }
self.undo_stack = { idx = 1 }
self.redo_stack = { idx = 1 }
-- self.clean_change_id = 1
self.highlighter = Highlighter(self)
end
function InputDoc:get_change_id()
return self.undo_stack.idx
end
function InputDoc:set_selection(line1, col1, line2, col2, swap)
assert(not line2 == not col2, "expected 2 or 4 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
self.selection.a.line, self.selection.a.col = line1, col1
self.selection.b.line, self.selection.b.col = line2, col2
end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2
or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
function InputDoc:get_selection(sort)
local a, b = self.selection.a, self.selection.b
if sort then
return sort_positions(a.line, a.col, b.line, b.col)
end
return a.line, a.col, b.line, b.col
end
function InputDoc:has_selection()
local a, b = self.selection.a, self.selection.b
return not (a.line == b.line and a.col == b.col)
end
function InputDoc:sanitize_selection()
self:set_selection(self:get_selection())
end
function InputDoc:sanitize_position(line, col)
line = common.clamp(line, 1, #self.lines)
col = common.clamp(col, 1, #self.lines[line])
return line, col
end
local function position_offset_func(self, line, col, fn, ...)
line, col = self:sanitize_position(line, col)
return fn(self, line, col, ...)
end
local function position_offset_byte(self, line, col, offset)
line, col = self:sanitize_position(line, col)
col = col + offset
while line > 1 and col < 1 do
line = line - 1
col = col + #self.lines[line]
end
while line < #self.lines and col > #self.lines[line] do
col = col - #self.lines[line]
line = line + 1
end
return self:sanitize_position(line, col)
end
local function position_offset_linecol(self, line, col, lineoffset, coloffset)
return self:sanitize_position(line + lineoffset, col + coloffset)
end
function Doc:position_offset(line, col, ...)
if type(...) ~= "number" then
return position_offset_func(self, line, col, ...)
elseif select("#", ...) == 1 then
return position_offset_byte(self, line, col, ...)
elseif select("#", ...) == 2 then
return position_offset_linecol(self, line, col, ...)
else
error("bad number of arguments")
end
end
function Doc:get_text(line1, col1, line2, col2)
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2)
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
if line1 == line2 then
return self.lines[line1]:sub(col1, col2 - 1)
end
local lines = { self.lines[line1]:sub(col1) }
for i = line1 + 1, line2 - 1 do
table.insert(lines, self.lines[i])
end
table.insert(lines, self.lines[line2]:sub(1, col2 - 1))
return table.concat(lines)
end
function Doc:get_char(line, col)
line, col = self:sanitize_position(line, col)
return self.lines[line]:sub(col, col)
end
local function push_undo(undo_stack, time, type, ...)
undo_stack[undo_stack.idx] = { type = type, time = time, ... }
undo_stack[undo_stack.idx - config.max_undos] = nil
undo_stack.idx = undo_stack.idx + 1
end
local function pop_undo(self, undo_stack, redo_stack)
-- pop command
local cmd = undo_stack[undo_stack.idx - 1]
if not cmd then return end
undo_stack.idx = undo_stack.idx - 1
-- handle command
if cmd.type == "insert" then
local line, col, text = table.unpack(cmd)
self:raw_insert(line, col, text, redo_stack, cmd.time)
elseif cmd.type == "remove" then
local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then
self.selection.a.line, self.selection.a.col = cmd[1], cmd[2]
self.selection.b.line, self.selection.b.col = cmd[3], cmd[4]
end
-- if next undo command is within the merge timeout then treat as a single
-- command and continue to execute it
local next = undo_stack[undo_stack.idx - 1]
if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then
return pop_undo(self, undo_stack, redo_stack)
end
end
function Doc:raw_insert(line, col, text, undo_stack, time)
-- split text into lines and merge with line at insertion point
local lines = split_lines(text)
local before = self.lines[line]:sub(1, col - 1)
local after = self.lines[line]:sub(col)
for i = 1, #lines - 1 do
lines[i] = lines[i] .. "\n"
end
lines[1] = before .. lines[1]
lines[#lines] = lines[#lines] .. after
-- splice lines into line array
splice(self.lines, line, 1, lines)
-- push undo
local line2, col2 = self:position_offset(line, col, #text)
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line)
self:sanitize_selection()
end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo
local text = self:get_text(line1, col1, line2, col2)
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text
local before = self.lines[line1]:sub(1, col1 - 1)
local after = self.lines[line2]:sub(col2)
-- splice line into line array
splice(self.lines, line1, line2 - line1 + 1, { before .. after })
-- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line1)
self:sanitize_selection()
end
function Doc:insert(line, col, text)
self.redo_stack = { idx = 1 }
line, col = self:sanitize_position(line, col)
self:raw_insert(line, col, text, self.undo_stack, system.get_time())
end
function Doc:remove(line1, col1, line2, col2)
self.redo_stack = { idx = 1 }
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2)
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time())
end
function Doc:undo()
pop_undo(self, self.undo_stack, self.redo_stack)
end
function Doc:redo()
pop_undo(self, self.redo_stack, self.undo_stack)
end
function Doc:text_input(text)
if self:has_selection() then
self:delete_to()
end
local line, col = self:get_selection()
self:insert(line, col, text)
self:move_to(#text)
end
function Doc:replace(fn)
local line1, col1, line2, col2, swap
local had_selection = self:has_selection()
if had_selection then
line1, col1, line2, col2, swap = self:get_selection(true)
else
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
end
local old_text = self:get_text(line1, col1, line2, col2)
local new_text, n = fn(old_text)
if old_text ~= new_text then
self:insert(line2, col2, new_text)
self:remove(line1, col1, line2, col2)
if had_selection then
line2, col2 = self:position_offset(line1, col1, #new_text)
self:set_selection(line1, col1, line2, col2, swap)
end
end
return n
end
function Doc:delete_to(...)
local line, col = self:get_selection(true)
if self:has_selection() then
self:remove(self:get_selection())
else
local line2, col2 = self:position_offset(line, col, ...)
self:remove(line, col, line2, col2)
line, col = sort_positions(line, col, line2, col2)
end
self:set_selection(line, col)
end
function Doc:move_to(...)
local line, col = self:get_selection()
self:set_selection(self:position_offset(line, col, ...))
end
function Doc:select_to(...)
local line, col, line2, col2 = self:get_selection()
line, col = self:position_offset(line, col, ...)
self:set_selection(line, col, line2, col2)
end
return Doc

View File

@ -94,6 +94,10 @@ function DocView:get_scrollable_size()
return self:get_line_height() * (#self.doc.lines - 1) + self.size.y
end
function DocView:get_content_size()
return self:get_line_height() * #self.doc.lines
end
function DocView:get_font()
return style[self.font]

286
data/core/replview.lua Normal file
View File

@ -0,0 +1,286 @@
local core = require "core"
local common = require "core.common"
local style = require "core.style"
local keymap = require "core.keymap"
local Object = require "core.object"
local View = require "core.view"
local DocView = require "core.docview"
local XEmptyView = View:extend()
function XEmptyView:draw()
self:draw_background(style.background)
end
local ReplNode = Object:extend()
function ReplNode:new()
-- self.type = type or "leaf"
self.position = { x = 0, y = 0 }
self.size = { x = 0, y = 0 }
self.view = XEmptyView()
-- self.views = {}
self.divider = 0.5
self.next = nil
-- if self.type == "leaf" then
-- self:add_view(EmptyView())
-- end
end
-- TODO: check if still okay
function ReplNode:propagate(fn, ...)
if self.next then
self.next[fn](self.next, ...)
end
-- self.a[fn](self.a, ...)
-- self.b[fn](self.b, ...)
end
-- FIXME: figure out what to do here.
function ReplNode:on_mouse_moved(x, y, ...)
-- self.hovered_tab = self:get_tab_overlapping_point(x, y)
-- if self.type == "leaf" then
-- self.active_view:on_mouse_moved(x, y, ...)
-- else
-- self:propagate("on_mouse_moved", x, y, ...)
-- end
end
-- FIXME: line above
function ReplNode:on_mouse_released(...)
-- if self.type == "leaf" then
-- self.active_view:on_mouse_released(...)
-- else
-- self:propagate("on_mouse_released", ...)
-- end
end
function ReplNode:consume(node)
for k, _ in pairs(self) do self[k] = nil end
for k, v in pairs(node) do self[k] = v end
end
-- function ReplNode:split(dir, view, locked)
-- function ReplNode:close_active_view(root)
function ReplNode:append_view(view)
assert(not self.next, "Tried to add view to non-terminal node")
local new_node = ReplNode()
new_node:set_view(view)
self.next = new_node
end
-- function ReplNode:add_view(view)
-- function ReplNode:set_active_view(view)
function ReplNode:set_view(view)
self.view = view
end
-- TODO: check if needed
function ReplNode:get_view_idx(view)
assert(false, "Not yet implemented")
end
function ReplNode:get_node_for_view(view)
local current_node = self
while current_node do
if current_node.view == view then
return current_node
end
current_node = current_node.next
end
end
function ReplNode:get_last_node()
local current_node = self
while current_node do
if current_node.next == nil then
return current_node
end
current_node = current_node.next
end
end
-- TODO: check if needed
function ReplNode:get_parent_node(root)
assert(false, "Not yet implemented")
end
-- function ReplNode:get_children(t)
-- function ReplNode:get_divider_overlapping_point(px, py)
-- function ReplNode:get_tab_overlapping_point(px, py)
function ReplNode:get_child_overlapping_point(x, y)
if (y >= self.position.y) and (y <= self.position.y + self.size.y) then
return self
else
if self.next then
return self.next:get_child_overlapping_point(x, y)
end
end
end
-- function ReplNode:get_tab_rect(idx)
-- function ReplNode:get_divider_rect()
-- function ReplNode:get_locked_size()
local function copy_position_and_size(dst, src)
dst.position.x, dst.position.y = src.position.x, src.position.y
dst.size.x, dst.size.y = src.size.x, src.size.y
end
-- local function calc_split_sizes(self, x, y, x1, x2)
-- FIXME: do not hard code values. Figure out a more correct way. May be use SCALE.
local left_margin, top_margin = 20, 20
local spacing = { x = 20, y = 20 }
function ReplNode:update_layout(x, y, w)
-- we assume below that self.view is a DocView
local doc_size = self.view:get_content_size()
self.position.x = x
self.position.y = y
self.size.x = w
self.size.y = doc_size
if self.next then
self.next:update_layout(x, y + doc_size + spacing.y, w)
end
end
function ReplNode:update()
self.view:update()
if self.next then
self.next:update()
end
end
-- function ReplNode:draw_tabs()
function ReplNode:draw()
local pos, size = self.view.position, self.view.size
core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
self.view:draw()
core.pop_clip_rect()
if self.next then
self.next:draw()
end
end
local ReplView = View:extend()
function ReplView:new()
ReplView.super.new(self)
self.root_node = ReplNode()
self.deferred_draws = {}
self.mouse = { x = 0, y = 0 }
end
function ReplView:defer_draw(fn, ...)
table.insert(self.deferred_draws, 1, { fn = fn, ... })
end
-- FIXME: this function should be remove because it is needed only
-- for a RootView.
function ReplView:get_active_node()
return self.root_node:get_node_for_view(core.active_view)
end
-- Previously named "open_doc"
function ReplView:append_doc(doc)
local node = self.root_node:get_last_node()
local view = DocView(doc)
node:add_view(view)
self.root_node:update_layout()
view:scroll_to_line(view.doc:get_selection(), true, true)
return view
end
function ReplView:on_mouse_pressed(button, x, y, clicks)
local node = self.root_node:get_child_overlapping_point(x, y)
-- FIXME: Should not be called, it is only for RootView.
-- Verify is set_active_view below whould be removed.
core.set_active_view(node.view)
node.view:on_mouse_pressed(button, x, y, clicks)
end
function ReplView:on_mouse_released(...)
self.root_node:on_mouse_released(...)
end
-- CONTINUE FROM HERE:
function ReplView:on_mouse_moved(x, y, dx, dy)
self.mouse.x, self.mouse.y = x, y
-- CONTINUE HERE: implement the on_mouse_moved for ReplNode
self.root_node:on_mouse_moved(x, y, dx, dy)
local node = self.root_node:get_child_overlapping_point(x, y)
if node then
system.set_cursor(node.active_view.cursor)
else
system.set_cursor("arrow")
endif
end
function ReplView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y)
node.active_view:on_mouse_wheel(...)
end
function ReplView:on_text_input(...)
core.active_view:on_text_input(...)
end
function ReplView:update()
copy_position_and_size(self.root_node, self)
self.root_node:update()
self.root_node:update_layout()
end
function ReplView:draw()
self.root_node:draw()
while #self.deferred_draws > 0 do
local t = table.remove(self.deferred_draws)
t.fn(table.unpack(t))
end
end
return ReplView

383
data/plugins/console.lua Normal file
View File

@ -0,0 +1,383 @@
local core = require "core"
local keymap = require "core.keymap"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local View = require "core.view"
config.console_size = 250 * SCALE
config.max_console_lines = 200
config.autoscroll_console = true
local files = {
script = core.temp_filename(PLATFORM == "Windows" and ".bat"),
script2 = core.temp_filename(PLATFORM == "Windows" and ".bat"),
output = core.temp_filename(),
complete = core.temp_filename(),
}
local console = {}
local views = {}
local pending_threads = {}
local thread_active = false
local output = nil
local output_id = 0
local visible = false
function console.clear()
output = { { text = "", time = 0 } }
end
local function read_file(filename, offset)
local fp = io.open(filename, "rb")
fp:seek("set", offset or 0)
local res = fp:read("*a")
fp:close()
return res
end
local function write_file(filename, text)
local fp = io.open(filename, "w")
fp:write(text)
fp:close()
end
local function lines(text)
return (text .. "\n"):gmatch("(.-)\n")
end
local function push_output(str, opt)
local first = true
for line in lines(str) do
if first then
line = table.remove(output).text .. line
end
line = line:gsub("\x1b%[[%d;]+m", "") -- strip ANSI colors
table.insert(output, {
text = line,
time = os.time(),
icon = line:find(opt.error_pattern) and "!"
or line:find(opt.warning_pattern) and "i",
file_pattern = opt.file_pattern,
})
if #output > config.max_console_lines then
table.remove(output, 1)
for view in pairs(views) do
view:on_line_removed()
end
end
first = false
end
output_id = output_id + 1
core.redraw = true
end
local function init_opt(opt)
local res = {
command = "",
file_pattern = "[^?:%s]+%.[^?:%s]+",
error_pattern = "error",
warning_pattern = "warning",
on_complete = function() end,
}
for k, v in pairs(res) do
res[k] = opt[k] or v
end
return res
end
function console.run(opt)
opt = init_opt(opt)
local function thread()
-- init script file(s)
if PLATFORM == "Windows" then
write_file(files.script, opt.command .. "\n")
write_file(files.script2, string.format([[
@echo off
call %q >%q 2>&1
echo "" >%q
exit
]], files.script, files.output, files.complete))
system.exec(string.format("call %q", files.script2))
else
write_file(files.script, string.format([[
%s
touch %q
]], opt.command, files.complete))
system.exec(string.format("bash %q >%q 2>&1", files.script, files.output))
end
-- checks output file for change and reads
local last_size = 0
local function check_output_file()
if PLATFORM == "Windows" then
local fp = io.open(files.output)
if fp then fp:close() end
end
local info = system.get_file_info(files.output)
if info and info.size > last_size then
local text = read_file(files.output, last_size)
push_output(text, opt)
last_size = info.size
end
end
-- read output file until we get a file indicating completion
while not system.get_file_info(files.complete) do
check_output_file()
coroutine.yield(0.1)
end
check_output_file()
if output[#output].text ~= "" then
push_output("\n", opt)
end
push_output("!DIVIDER\n", opt)
-- clean up and finish
for _, file in pairs(files) do
os.remove(file)
end
opt.on_complete()
-- handle pending thread
local pending = table.remove(pending_threads, 1)
if pending then
core.add_thread(pending)
else
thread_active = false
end
end
-- push/init thread
if thread_active then
table.insert(pending_threads, thread)
else
core.add_thread(thread)
thread_active = true
end
-- make sure static console is visible if it's the only ConsoleView
local count = 0
for _ in pairs(views) do count = count + 1 end
if count == 1 then visible = true end
end
local ConsoleView = View:extend()
function ConsoleView:new()
ConsoleView.super.new(self)
self.scrollable = true
self.hovered_idx = -1
views[self] = true
end
function ConsoleView:try_close(...)
ConsoleView.super.try_close(self, ...)
views[self] = nil
end
function ConsoleView:get_name()
return "Console"
end
function ConsoleView:get_line_height()
return style.code_font:get_height() * config.line_height
end
function ConsoleView:get_line_count()
return #output - (output[#output].text == "" and 1 or 0)
end
function ConsoleView:get_scrollable_size()
return self:get_line_count() * self:get_line_height() + style.padding.y * 2
end
function ConsoleView:get_visible_line_range()
local lh = self:get_line_height()
local min = math.max(1, math.floor(self.scroll.y / lh))
return min, min + math.floor(self.size.y / lh) + 1
end
function ConsoleView:on_mouse_moved(mx, my, ...)
ConsoleView.super.on_mouse_moved(self, mx, my, ...)
self.hovered_idx = 0
for i, item, x,y,w,h in self:each_visible_line() do
if mx >= x and my >= y and mx < x + w and my < y + h then
if item.text:find(item.file_pattern) then
self.hovered_idx = i
end
break
end
end
end
local function resolve_file(name)
if system.get_file_info(name) then
return name
end
local filenames = {}
for _, f in ipairs(core.project_files) do
table.insert(filenames, f.filename)
end
local t = common.fuzzy_match(filenames, name)
return t[1]
end
function ConsoleView:on_line_removed()
local diff = self:get_line_height()
self.scroll.y = self.scroll.y - diff
self.scroll.to.y = self.scroll.to.y - diff
end
function ConsoleView:on_mouse_pressed(...)
local caught = ConsoleView.super.on_mouse_pressed(self, ...)
if caught then
return
end
local item = output[self.hovered_idx]
if item then
local file, line, col = item.text:match(item.file_pattern)
local resolved_file = resolve_file(file)
if not resolved_file then
core.error("Couldn't resolve file \"%s\"", file)
return
end
core.try(function()
core.set_active_view(core.last_active_view)
local dv = core.root_view:open_doc(core.open_doc(resolved_file))
if line then
dv.doc:set_selection(line, col or 0)
dv:scroll_to_line(line, false, true)
end
end)
end
end
function ConsoleView:each_visible_line()
return coroutine.wrap(function()
local x, y = self:get_content_offset()
local lh = self:get_line_height()
local min, max = self:get_visible_line_range()
y = y + lh * (min - 1) + style.padding.y
max = math.min(max, self:get_line_count())
for i = min, max do
local item = output[i]
if not item then break end
coroutine.yield(i, item, x, y, self.size.x, lh)
y = y + lh
end
end)
end
function ConsoleView:update(...)
if self.last_output_id ~= output_id then
if config.autoscroll_console then
self.scroll.to.y = self:get_scrollable_size()
end
self.last_output_id = output_id
end
ConsoleView.super.update(self, ...)
end
function ConsoleView:draw()
self:draw_background(style.background)
local icon_w = style.icon_font:get_width("!")
for i, item, x, y, w, h in self:each_visible_line() do
local tx = x + style.padding.x
local time = os.date("%H:%M:%S", item.time)
local color = style.text
if self.hovered_idx == i then
color = style.accent
renderer.draw_rect(x, y, w, h, style.line_highlight)
end
if item.text == "!DIVIDER" then
local w = style.font:get_width(time)
renderer.draw_rect(tx, y + h / 2, w, math.ceil(SCALE * 1), style.dim)
else
tx = common.draw_text(style.font, style.dim, time, "left", tx, y, w, h)
tx = tx + style.padding.x
if item.icon then
common.draw_text(style.icon_font, color, item.icon, "left", tx, y, w, h)
end
tx = tx + icon_w + style.padding.x
common.draw_text(style.code_font, color, item.text, "left", tx, y, w, h)
end
end
self:draw_scrollbar(self)
end
-- init static bottom-of-screen console
local view = ConsoleView()
local node = core.root_view:get_active_node()
node:split("down", view, true)
function view:update(...)
local dest = visible and config.console_size or 0
self:move_towards(self.size, "y", dest)
ConsoleView.update(self, ...)
end
local last_command = ""
command.add(nil, {
["console:reset-output"] = function()
output = { { text = "", time = 0 } }
end,
["console:open-console"] = function()
local node = core.root_view:get_active_node()
node:add_view(ConsoleView())
end,
["console:toggle"] = function()
visible = not visible
end,
["console:run"] = function()
core.command_view:set_text(last_command, true)
core.command_view:enter("Run Console Command", function(cmd)
console.run { command = cmd }
last_command = cmd
end)
end
})
keymap.add {
["ctrl+."] = "console:toggle",
["ctrl+shift+."] = "console:run",
}
-- for `workspace` plugin:
package.loaded["plugins.console.view"] = ConsoleView
console.clear()
return console

37
data/plugins/repl.lua Normal file
View File

@ -0,0 +1,37 @@
local core = require "core"
local keymap = require "core.keymap"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local View = require "core.view"
local function new_node() {
return { input = "" }
}
local nodes = {
new_node()
}
local ReplView = View:extend()
function ReplView:new()
ReplView.super.new(self)
self.scrollable = true
self.brightness = 0
-- self:begin_search(text, fn)
end
local function begin_repl()
local rv = ReplView()
core.root_view:get_active_node():add_view(rv)
end
command.add(nil, {
["repl:open"] = function()
begin_repl()
end
})

14
notes-repl.md Normal file
View File

@ -0,0 +1,14 @@
## Steps
- create an object similar to a Doc (core/doc/init.lua) but not tied to a file.
We may call it InputDoc.
- use DocView(s) (defined in core/docview.lua) but tied to an InputDoc instead
of to a Doc. To make this work InputDoc should implement the same methods
and behave in the same way.
- create a new object derived by View to "assemble" several InputDoc(s).
The InputDoc(s) should corresponds to the input and output snippet of the
REPL.
We may call this view ReplView.
The ReplView should draw its InputDoc(s) with some spacing and displatch
text input events mouve movements etc.
It could be similar to RootView.

256
replview.try1.lua Normal file
View File

@ -0,0 +1,256 @@
local core = require "core"
local common = require "core.common"
local style = require "core.style"
local keymap = require "core.keymap"
local Object = require "core.object"
local View = require "core.view"
local DocView = require "core.docview"
local ReplNode = Object:extend()
function ReplNode:new(view)
-- self.type = type or "leaf"
self.position = { x = 0, y = 0 }
self.size = { x = 0, y = 0 }
self.view = view
-- self.views = {}
self.divider = 0.5
end
function ReplNode:on_mouse_moved(x, y, ...)
self.view:on_mouse_moved(x, y, ...)
end
function ReplNode:on_mouse_released(...)
self.view:on_mouse_released(...)
end
-- local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
-- function Node:split(dir, view, locked)
-- function Node:close_active_view(root)
function ReplNode:set_active_view()
-- TODO: understand what the call below actually does.
core.set_active_view(self.view)
end
-- function Node:get_view_idx(view)
-- function Node:get_node_for_view(view)
-- function Node:get_parent_node(root)
-- function Node:get_children(t)
-- function ReplNode:get_divider_overlapping_point(px, py)
-- function Node:get_tab_overlapping_point(px, py)
-- function Node:get_child_overlapping_point(x, y)
-- function Node:get_tab_rect(idx)
-- function Node:get_divider_rect()
function ReplNode:get_locked_size()
if self.locked then
local size = self.view.size
return size.x, size.y
end
end
local function copy_position_and_size(dst, src)
dst.position.x, dst.position.y = src.position.x, src.position.y
dst.size.x, dst.size.y = src.size.x, src.size.y
end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both
-- local function calc_split_sizes(self, x, y, x1, x2)
function ReplNode:update_layout()
copy_position_and_size(self.view, self)
end
function ReplNode:update()
if self.view then
self.view:update()
end
end
-- function Node:draw_tabs()
function ReplNode:draw()
local pos, size = self.view.position, self.view.size
core.push_clip_rect(pos.x, pos.y, size.x + pos.x % 1, size.y + pos.y % 1)
self.view:draw()
core.pop_clip_rect()
end
local ReplView = View:extend()
function ReplView:new()
ReplView.super.new(self)
-- local first_node = ReplNode()
-- self.nodes = { first_node }
self.nodes = { }
self.active_node_index = 0
self.deferred_draws = {}
self.mouse = { x = 0, y = 0 }
end
-- TODO: figure out what the deferred_draws are doing
function ReplView:defer_draw(fn, ...)
table.insert(self.deferred_draws, 1, { fn = fn, ... })
end
function ReplView:get_active_node()
if self.active_node_index > 0 then
return self.nodes[self.active_node_index]
end
end
-- FIXME: find out something better
repl_left_margin, repl_top_margin = 10, 10
function ReplView:update_layout()
local x, y = repl_left_margin, repl_top_margin
-- CONTINUE HERE
end
function ReplView:open_doc(doc)
-- local node = self:get_active_node()
-- if node.locked and core.last_active_view then
-- core.set_active_view(core.last_active_view)
-- node = self:get_active_node()
-- end
-- assert(not node.locked, "Cannot open doc on locked node")
for i, node in ipairs(self.nodes) do
local view = node.view
-- for i, view in ipairs(node.views) do
if view.doc == doc then
node:set_active_view()
return view
end
end
local view = DocView(doc)
local node = ReplNode(view)
self.nodes[#self.nodes + 1] = node
self.active_node_index = #self.nodes
self:update_layout()
-- node:add_view(view)
-- TODO: replace the call below with something appropriate.
-- self.root_node:update_layout()
-- TODO: manage to assign the correct x, y position to the new node.
view:scroll_to_line(view.doc:get_selection(), true, true)
return view
end
-- TODO: CONTINUE HERE
function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y)
if div then
self.dragged_divider = div
return
end
local node = self.root_node:get_child_overlapping_point(x, y)
local idx = node:get_tab_overlapping_point(x, y)
if idx then
node:set_active_view(node.views[idx])
if button == "middle" then
node:close_active_view(self.root_node)
end
else
core.set_active_view(node.active_view)
node.active_view:on_mouse_pressed(button, x, y, clicks)
end
end
function RootView:on_mouse_released(...)
if self.dragged_divider then
self.dragged_divider = nil
end
self.root_node:on_mouse_released(...)
end
function RootView:on_mouse_moved(x, y, dx, dy)
if self.dragged_divider then
local node = self.dragged_divider
if node.type == "hsplit" then
node.divider = node.divider + dx / node.size.x
else
node.divider = node.divider + dy / node.size.y
end
node.divider = common.clamp(node.divider, 0.01, 0.99)
return
end
self.mouse.x, self.mouse.y = x, y
self.root_node:on_mouse_moved(x, y, dx, dy)
local node = self.root_node:get_child_overlapping_point(x, y)
local div = self.root_node:get_divider_overlapping_point(x, y)
if div then
system.set_cursor(div.type == "hsplit" and "sizeh" or "sizev")
elseif node:get_tab_overlapping_point(x, y) then
system.set_cursor("arrow")
else
system.set_cursor(node.active_view.cursor)
end
end
function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y)
node.active_view:on_mouse_wheel(...)
end
function RootView:on_text_input(...)
core.active_view:on_text_input(...)
end
function RootView:update()
copy_position_and_size(self.root_node, self)
self.root_node:update()
self.root_node:update_layout()
end
function RootView:draw()
self.root_node:draw()
while #self.deferred_draws > 0 do
local t = table.remove(self.deferred_draws)
t.fn(table.unpack(t))
end
end
return RootView