editing source code from within the app
integrated from pong.love via text.love: https://merveilles.town/@akkartik/108933336531898243
This commit is contained in:
parent
9c72ff1bb4
commit
e1c5a42f31
|
@ -3,17 +3,28 @@ program before it ever runs. However, some things don't have tests yet, either
|
|||
because I don't know how to test them or because I've been lazy. I'll at least
|
||||
record those here.
|
||||
|
||||
* Initializing settings:
|
||||
- from previous session
|
||||
- Filename as absolute path
|
||||
- Filename as relative path
|
||||
- from defaults
|
||||
Startup:
|
||||
- terminal log shows unit tests running
|
||||
|
||||
Initializing settings:
|
||||
- delete app settings, start; window opens running the text editor
|
||||
- quit while running the text editor, restart; window opens running the text editor in same position+dimensions
|
||||
- quit while editing source (color; no drawings; no selection), restart; window opens editing source in same position+dimensions
|
||||
- start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions
|
||||
- start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions
|
||||
- no log file; switching to source works
|
||||
|
||||
Code loading:
|
||||
* run love with directory; text editor runs
|
||||
* run love with zip file; text editor runs
|
||||
|
||||
* How the screen looks. Our tests use a level of indirection to check text and
|
||||
graphics printed to screen, but not the precise pixels they translate to.
|
||||
- where exactly the cursor is drawn to highlight a given character
|
||||
- analogously, how a shape precisely looks as you draw it
|
||||
|
||||
* start out running the text editor, press ctrl+e to edit source, make a change to the source, press ctrl+e twice to return to the source editor; the change should be preserved.
|
||||
|
||||
### Other compromises
|
||||
|
||||
Lua is dynamically typed. Tests can't patch over lack of type-checking.
|
||||
|
|
|
@ -29,6 +29,7 @@ While editing text:
|
|||
* `ctrl+z` to undo, `ctrl+y` to redo
|
||||
* `ctrl+=` to zoom in, `ctrl+-` to zoom out, `ctrl+0` to reset zoom
|
||||
* `alt+right`/`alt+left` to jump to the next/previous word, respectively
|
||||
* `ctrl+e` to modify the sources
|
||||
|
||||
For shortcuts while editing drawings, consult the online help. Either:
|
||||
* hover on a drawing and hit `ctrl+h`, or
|
||||
|
@ -78,6 +79,10 @@ found anything amiss: http://akkartik.name/contact
|
|||
|
||||
* No scrollbars yet. That stuff is hard.
|
||||
|
||||
* There are some temporary limitations when editing sources:
|
||||
- no line drawings
|
||||
- no selecting text
|
||||
|
||||
## Mirrors and Forks
|
||||
|
||||
Updates to lines.love can be downloaded from the following mirrors in addition
|
||||
|
|
29
app.lua
29
app.lua
|
@ -1,4 +1,4 @@
|
|||
-- main entrypoint for LÖVE
|
||||
-- love.run: main entrypoint function for LÖVE
|
||||
--
|
||||
-- Most apps can just use the default, but we need to override it to
|
||||
-- install a test harness.
|
||||
|
@ -11,13 +11,10 @@
|
|||
--
|
||||
-- Scroll below this function for more details.
|
||||
function love.run()
|
||||
App.snapshot_love()
|
||||
-- Tests always run at the start.
|
||||
App.run_tests()
|
||||
|
||||
App.run_tests_and_initialize()
|
||||
--? print('==')
|
||||
App.disable_tests()
|
||||
App.initialize_globals()
|
||||
App.initialize(love.arg.parseGameArguments(arg), arg)
|
||||
|
||||
love.timer.step()
|
||||
local dt = 0
|
||||
|
@ -123,6 +120,26 @@ end
|
|||
|
||||
App = {screen={}}
|
||||
|
||||
-- save/restore various framework globals we care about -- only on very first load
|
||||
function App.snapshot_love()
|
||||
if Love_snapshot then return end
|
||||
Love_snapshot = {}
|
||||
-- save the entire initial font; it doesn't seem reliably recreated using newFont
|
||||
Love_snapshot.initial_font = love.graphics.getFont()
|
||||
end
|
||||
|
||||
function App.undo_initialize()
|
||||
love.graphics.setFont(Love_snapshot.initial_font)
|
||||
end
|
||||
|
||||
function App.run_tests_and_initialize()
|
||||
App.load()
|
||||
App.run_tests()
|
||||
App.disable_tests()
|
||||
App.initialize_globals()
|
||||
App.initialize(love.arg.parseGameArguments(arg), arg)
|
||||
end
|
||||
|
||||
function App.initialize_for_test()
|
||||
App.screen.init({width=100, height=50})
|
||||
App.screen.contents = {} -- clear screen
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
-- State transitions while colorizing a single line.
|
||||
-- Just for comments and strings.
|
||||
-- Limitation: each fragment gets a uniform color so we can only change color
|
||||
-- at word boundaries.
|
||||
Next_state = {
|
||||
normal={
|
||||
{prefix='--', target='comment'},
|
||||
{prefix='"', target='dstring'},
|
||||
{prefix="'", target='sstring'},
|
||||
},
|
||||
dstring={
|
||||
{suffix='"', target='normal'},
|
||||
},
|
||||
sstring={
|
||||
{suffix="'", target='normal'},
|
||||
},
|
||||
-- comments are a sink
|
||||
}
|
||||
|
||||
Comments_color = {r=0, g=0, b=1}
|
||||
String_color = {r=0, g=0.5, b=0.5}
|
||||
Divider_color = {r=0.7, g=0.7, b=0.7}
|
||||
|
||||
Colors = {
|
||||
normal=Text_color,
|
||||
comment=Comments_color,
|
||||
sstring=String_color,
|
||||
dstring=String_color
|
||||
}
|
||||
|
||||
Current_state = 'normal'
|
||||
|
||||
function initialize_color()
|
||||
--? print('new line')
|
||||
Current_state = 'normal'
|
||||
end
|
||||
|
||||
function select_color(frag)
|
||||
--? print('before', '^'..frag..'$', Current_state)
|
||||
switch_color_based_on_prefix(frag)
|
||||
--? print('using color', Current_state, Colors[Current_state])
|
||||
App.color(Colors[Current_state])
|
||||
switch_color_based_on_suffix(frag)
|
||||
--? print('state after suffix', Current_state)
|
||||
end
|
||||
|
||||
function switch_color_based_on_prefix(frag)
|
||||
if Next_state[Current_state] == nil then
|
||||
return
|
||||
end
|
||||
frag = rtrim(frag)
|
||||
for _,edge in pairs(Next_state[Current_state]) do
|
||||
if edge.prefix and find(frag, edge.prefix, nil, --[[plain]] true) == 1 then
|
||||
Current_state = edge.target
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function switch_color_based_on_suffix(frag)
|
||||
if Next_state[Current_state] == nil then
|
||||
return
|
||||
end
|
||||
frag = rtrim(frag)
|
||||
for _,edge in pairs(Next_state[Current_state]) do
|
||||
if edge.suffix and rfind(frag, edge.suffix, nil, --[[plain]] true) == #frag then
|
||||
Current_state = edge.target
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function trim(s)
|
||||
return s:gsub('^%s+', ''):gsub('%s+$', '')
|
||||
end
|
||||
|
||||
function ltrim(s)
|
||||
return s:gsub('^%s+', '')
|
||||
end
|
||||
|
||||
function rtrim(s)
|
||||
return s:gsub('%s+$', '')
|
||||
end
|
|
@ -0,0 +1,100 @@
|
|||
Menu_background_color = {r=0.6, g=0.8, b=0.6}
|
||||
Menu_border_color = {r=0.6, g=0.7, b=0.6}
|
||||
Menu_command_color = {r=0.2, g=0.2, b=0.2}
|
||||
Menu_highlight_color = {r=0.5, g=0.7, b=0.3}
|
||||
|
||||
function source.draw_menu_bar()
|
||||
if App.run_tests then return end -- disable in tests
|
||||
App.color(Menu_background_color)
|
||||
love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)
|
||||
App.color(Menu_border_color)
|
||||
love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)
|
||||
App.color(Menu_command_color)
|
||||
Menu_cursor = 5
|
||||
if Show_file_navigator then
|
||||
source.draw_file_navigator()
|
||||
return
|
||||
end
|
||||
add_hotkey_to_menu('ctrl+e: run')
|
||||
if Focus == 'edit' then
|
||||
add_hotkey_to_menu('ctrl+g: switch file')
|
||||
if Show_log_browser_side then
|
||||
add_hotkey_to_menu('ctrl+l: hide log browser')
|
||||
else
|
||||
add_hotkey_to_menu('ctrl+l: show log browser')
|
||||
end
|
||||
if Editor_state.expanded then
|
||||
add_hotkey_to_menu('ctrl+b: collapse debug prints')
|
||||
else
|
||||
add_hotkey_to_menu('ctrl+b: expand debug prints')
|
||||
end
|
||||
add_hotkey_to_menu('ctrl+d: create/edit debug print')
|
||||
add_hotkey_to_menu('ctrl+f: find in file')
|
||||
add_hotkey_to_menu('alt+left alt+right: prev/next word')
|
||||
elseif Focus == 'log_browser' then
|
||||
-- nothing yet
|
||||
else
|
||||
assert(false, 'unknown focus "'..Focus..'"')
|
||||
end
|
||||
add_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')
|
||||
add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')
|
||||
add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')
|
||||
end
|
||||
|
||||
function add_hotkey_to_menu(s)
|
||||
if Text_cache[s] == nil then
|
||||
Text_cache[s] = App.newText(love.graphics.getFont(), s)
|
||||
end
|
||||
local width = App.width(Text_cache[s])
|
||||
if Menu_cursor + width > App.screen.width - 5 then
|
||||
return
|
||||
end
|
||||
App.color(Menu_command_color)
|
||||
App.screen.draw(Text_cache[s], Menu_cursor,5)
|
||||
Menu_cursor = Menu_cursor + width + 30
|
||||
end
|
||||
|
||||
function source.draw_file_navigator()
|
||||
for i,file in ipairs(File_navigation.candidates) do
|
||||
if file == 'source' then
|
||||
App.color(Menu_border_color)
|
||||
love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
|
||||
end
|
||||
add_file_to_menu(file, i == File_navigation.index)
|
||||
end
|
||||
end
|
||||
|
||||
function add_file_to_menu(s, cursor_highlight)
|
||||
if Text_cache[s] == nil then
|
||||
Text_cache[s] = App.newText(love.graphics.getFont(), s)
|
||||
end
|
||||
local width = App.width(Text_cache[s])
|
||||
if Menu_cursor + width > App.screen.width - 5 then
|
||||
return
|
||||
end
|
||||
if cursor_highlight then
|
||||
App.color(Menu_highlight_color)
|
||||
love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)
|
||||
end
|
||||
App.color(Menu_command_color)
|
||||
App.screen.draw(Text_cache[s], Menu_cursor,5)
|
||||
Menu_cursor = Menu_cursor + width + 30
|
||||
end
|
||||
|
||||
function keychord_pressed_on_file_navigator(chord, key)
|
||||
if chord == 'escape' then
|
||||
Show_file_navigator = false
|
||||
elseif chord == 'return' then
|
||||
local candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')
|
||||
source.switch_to_file(candidate)
|
||||
Show_file_navigator = false
|
||||
elseif chord == 'left' then
|
||||
if File_navigation.index > 1 then
|
||||
File_navigation.index = File_navigation.index-1
|
||||
end
|
||||
elseif chord == 'right' then
|
||||
if File_navigation.index < #File_navigation.candidates then
|
||||
File_navigation.index = File_navigation.index+1
|
||||
end
|
||||
end
|
||||
end
|
9
edit.lua
9
edit.lua
|
@ -20,15 +20,6 @@ Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
|
|||
|
||||
Same_point_distance = 4 -- pixel distance at which two points are considered the same
|
||||
|
||||
utf8 = require 'utf8'
|
||||
|
||||
require 'file'
|
||||
require 'text'
|
||||
require 'drawing'
|
||||
require 'geom'
|
||||
require 'help'
|
||||
require 'icons'
|
||||
|
||||
edit = {}
|
||||
|
||||
-- run in both tests and a real run
|
||||
|
|
1
file.lua
1
file.lua
|
@ -50,7 +50,6 @@ function save_to_disk(State)
|
|||
outfile:close()
|
||||
end
|
||||
|
||||
json = require 'json'
|
||||
function load_drawing(infile_next_line)
|
||||
local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
|
||||
while true do
|
||||
|
|
14
keychord.lua
14
keychord.lua
|
@ -56,9 +56,17 @@ end
|
|||
array = {}
|
||||
|
||||
function array.find(arr, elem)
|
||||
for i,x in ipairs(arr) do
|
||||
if x == elem then
|
||||
return i
|
||||
if type(elem) == 'function' then
|
||||
for i,x in ipairs(arr) do
|
||||
if elem(x) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
else
|
||||
for i,x in ipairs(arr) do
|
||||
if x == elem then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
function log(stack_frame_index, obj)
|
||||
local info = debug.getinfo(stack_frame_index, 'Sl')
|
||||
local msg
|
||||
if type(obj) == 'string' then
|
||||
msg = obj
|
||||
else
|
||||
msg = json.encode(obj)
|
||||
end
|
||||
love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n')
|
||||
end
|
||||
|
||||
-- for section delimiters we'll use specific Unicode box characters
|
||||
function log_start(name, stack_frame_index)
|
||||
if stack_frame_index == nil then
|
||||
stack_frame_index = 3
|
||||
end
|
||||
log(stack_frame_index, '\u{250c} ' .. name)
|
||||
end
|
||||
function log_end(name, stack_frame_index)
|
||||
if stack_frame_index == nil then
|
||||
stack_frame_index = 3
|
||||
end
|
||||
log(stack_frame_index, '\u{2518} ' .. name)
|
||||
end
|
||||
|
||||
function log_new(name, stack_frame_index)
|
||||
if stack_frame_index == nil then
|
||||
stack_frame_index = 4
|
||||
end
|
||||
log_end(name, stack_frame_index)
|
||||
log_start(name, stack_frame_index)
|
||||
end
|
||||
|
||||
-- vim:noexpandtab
|
|
@ -0,0 +1,316 @@
|
|||
-- environment for immutable logs
|
||||
-- optionally reads extensions for rendering some types from the source codebase that generated them
|
||||
--
|
||||
-- We won't care too much about long, wrapped lines. If they lines get too
|
||||
-- long to manage, you need a better, graphical rendering for them. Load
|
||||
-- functions to render them into the log_render namespace.
|
||||
|
||||
function source.initialize_log_browser_side()
|
||||
Log_browser_state = edit.initialize_state(Margin_top, Editor_state.right + Margin_right + Margin_left, (Editor_state.right+Margin_right)*2, Editor_state.font_height, Editor_state.line_height)
|
||||
Log_browser_state.filename = 'log'
|
||||
load_from_disk(Log_browser_state) -- TODO: pay no attention to Fold
|
||||
log_browser.parse(Log_browser_state)
|
||||
Text.redraw_all(Log_browser_state)
|
||||
Log_browser_state.screen_top1 = {line=1, pos=1}
|
||||
Log_browser_state.cursor1 = {line=1, pos=nil}
|
||||
end
|
||||
|
||||
Section_stack = {}
|
||||
Section_border_color = {r=0.7, g=0.7, b=0.7}
|
||||
Cursor_line_background_color = {r=0.7, g=0.7, b=0, a=0.1}
|
||||
|
||||
Section_border_padding_horizontal = 30 -- TODO: adjust this based on font height (because we draw text vertically along the borders
|
||||
Section_border_padding_vertical = 15 -- TODO: adjust this based on font height
|
||||
|
||||
log_browser = {}
|
||||
|
||||
function log_browser.parse(State)
|
||||
for _,line in ipairs(State.lines) do
|
||||
if line.data ~= '' then
|
||||
line.filename, line.line_number, line.data = line.data:match('%[string "([^:]*)"%]:([^:]*):%s*(.*)')
|
||||
line.filename = guess_source(line.filename)
|
||||
line.line_number = tonumber(line.line_number)
|
||||
if line.data:sub(1,1) == '{' then
|
||||
local data = json.decode(line.data)
|
||||
if log_render[data.name] then
|
||||
line.data = data
|
||||
end
|
||||
line.section_stack = table.shallowcopy(Section_stack)
|
||||
elseif line.data:match('\u{250c}') then
|
||||
line.section_stack = table.shallowcopy(Section_stack) -- as it is at the beginning
|
||||
local section_name = line.data:match('\u{250c}%s*(.*)')
|
||||
table.insert(Section_stack, {name=section_name})
|
||||
line.section_begin = true
|
||||
line.section_name = section_name
|
||||
line.data = nil
|
||||
elseif line.data:match('\u{2518}') then
|
||||
local section_name = line.data:match('\u{2518}%s*(.*)')
|
||||
if array.find(Section_stack, function(x) return x.name == section_name end) then
|
||||
while table.remove(Section_stack).name ~= section_name do
|
||||
--
|
||||
end
|
||||
line.section_end = true
|
||||
line.section_name = section_name
|
||||
line.data = nil
|
||||
end
|
||||
line.section_stack = table.shallowcopy(Section_stack)
|
||||
else
|
||||
-- string
|
||||
line.section_stack = table.shallowcopy(Section_stack)
|
||||
end
|
||||
else
|
||||
line.section_stack = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function table.shallowcopy(x)
|
||||
return {unpack(x)}
|
||||
end
|
||||
|
||||
function guess_source(filename)
|
||||
local possible_source = filename:gsub('%.lua$', '%.splua')
|
||||
if file_exists(possible_source) then
|
||||
return possible_source
|
||||
else
|
||||
return filename
|
||||
end
|
||||
end
|
||||
|
||||
function log_browser.draw(State)
|
||||
assert(#State.lines == #State.line_cache)
|
||||
local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())
|
||||
local y = State.top
|
||||
for line_index = State.screen_top1.line,#State.lines do
|
||||
App.color(Text_color)
|
||||
local line = State.lines[line_index]
|
||||
if y + State.line_height > App.screen.height then break end
|
||||
local height = State.line_height
|
||||
if should_show(line) then
|
||||
local xleft = render_stack_left_margin(State, line_index, line, y)
|
||||
local xright = render_stack_right_margin(State, line_index, line, y)
|
||||
if line.section_name then
|
||||
App.color(Section_border_color)
|
||||
local section_text = to_text(line.section_name)
|
||||
if line.section_begin then
|
||||
local sectiony = y+Section_border_padding_vertical
|
||||
love.graphics.line(xleft,sectiony, xleft,y+State.line_height)
|
||||
love.graphics.line(xright,sectiony, xright,y+State.line_height)
|
||||
love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
|
||||
love.graphics.draw(section_text, xleft+50,y)
|
||||
love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
|
||||
else assert(line.section_end)
|
||||
local sectiony = y+State.line_height-Section_border_padding_vertical
|
||||
love.graphics.line(xleft,y, xleft,sectiony)
|
||||
love.graphics.line(xright,y, xright,sectiony)
|
||||
love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
|
||||
love.graphics.draw(section_text, xleft+50,y)
|
||||
love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
|
||||
end
|
||||
else
|
||||
if type(line.data) == 'string' then
|
||||
local old_left, old_right = State.left,State.right
|
||||
State.left,State.right = xleft,xright
|
||||
y = Text.draw(State, line_index, y, --[[startpos]] 1)
|
||||
State.left,State.right = old_left,old_right
|
||||
else
|
||||
height = log_render[line.data.name](line.data, xleft, y, xright-xleft)
|
||||
end
|
||||
end
|
||||
if App.mouse_x() > Log_browser_state.left and line_index == mouse_line_index then
|
||||
App.color(Cursor_line_background_color)
|
||||
love.graphics.rectangle('fill', xleft,y, xright-xleft, height)
|
||||
end
|
||||
y = y + height
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function render_stack_left_margin(State, line_index, line, y)
|
||||
if line.section_stack == nil then
|
||||
-- assertion message
|
||||
for k,v in pairs(line) do
|
||||
print(k)
|
||||
end
|
||||
end
|
||||
App.color(Section_border_color)
|
||||
for i=1,#line.section_stack do
|
||||
local x = State.left + (i-1)*Section_border_padding_horizontal
|
||||
love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
|
||||
if y < 30 then
|
||||
love.graphics.print(line.section_stack[i].name, x+State.font_height+5, y+5, --[[vertically]] math.pi/2)
|
||||
end
|
||||
if y > App.screen.height-log_browser.height(State, line_index) then
|
||||
love.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
|
||||
end
|
||||
end
|
||||
return log_browser.left_margin(State, line)
|
||||
end
|
||||
|
||||
function render_stack_right_margin(State, line_index, line, y)
|
||||
App.color(Section_border_color)
|
||||
for i=1,#line.section_stack do
|
||||
local x = State.right - (i-1)*Section_border_padding_horizontal
|
||||
love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
|
||||
if y < 30 then
|
||||
love.graphics.print(line.section_stack[i].name, x, y+5, --[[vertically]] math.pi/2)
|
||||
end
|
||||
if y > App.screen.height-log_browser.height(State, line_index) then
|
||||
love.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
|
||||
end
|
||||
end
|
||||
return log_browser.right_margin(State, line)
|
||||
end
|
||||
|
||||
function should_show(line)
|
||||
-- Show a line if every single section it's in is expanded.
|
||||
for i=1,#line.section_stack do
|
||||
local section = line.section_stack[i]
|
||||
if not section.expanded then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function log_browser.left_margin(State, line)
|
||||
return State.left + #line.section_stack*Section_border_padding_horizontal
|
||||
end
|
||||
|
||||
function log_browser.right_margin(State, line)
|
||||
return State.right - #line.section_stack*Section_border_padding_horizontal
|
||||
end
|
||||
|
||||
function log_browser.update(State, dt)
|
||||
end
|
||||
|
||||
function log_browser.quit(State)
|
||||
end
|
||||
|
||||
function log_browser.mouse_pressed(State, x,y, mouse_button)
|
||||
local line_index = log_browser.line_index(State, x,y)
|
||||
if line_index == nil then
|
||||
-- below lower margin
|
||||
return
|
||||
end
|
||||
-- leave some space to click without focusing
|
||||
local line = State.lines[line_index]
|
||||
local xleft = log_browser.left_margin(State, line)
|
||||
local xright = log_browser.right_margin(State, line)
|
||||
if x < xleft or x > xright then
|
||||
return
|
||||
end
|
||||
-- if it's a section begin/end and the section is collapsed, expand it
|
||||
-- TODO: how to collapse?
|
||||
if line.section_begin or line.section_end then
|
||||
-- HACK: get section reference from next/previous line
|
||||
local new_section
|
||||
if line.section_begin then
|
||||
if line_index < #State.lines then
|
||||
local next_section_stack = State.lines[line_index+1].section_stack
|
||||
if next_section_stack then
|
||||
new_section = next_section_stack[#next_section_stack]
|
||||
end
|
||||
end
|
||||
elseif line.section_end then
|
||||
if line_index > 1 then
|
||||
local previous_section_stack = State.lines[line_index-1].section_stack
|
||||
if previous_section_stack then
|
||||
new_section = previous_section_stack[#previous_section_stack]
|
||||
end
|
||||
end
|
||||
end
|
||||
if new_section and new_section.expanded == nil then
|
||||
new_section.expanded = true
|
||||
return
|
||||
end
|
||||
end
|
||||
-- open appropriate file in source side
|
||||
if line.filename ~= Editor_state.filename then
|
||||
source.switch_to_file(line.filename)
|
||||
end
|
||||
-- set cursor
|
||||
Editor_state.cursor1 = {line=line.line_number, pos=1, posB=nil}
|
||||
-- make sure it's visible
|
||||
-- TODO: handle extremely long lines
|
||||
Editor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)
|
||||
-- show cursor
|
||||
Focus = 'edit'
|
||||
-- expand B side
|
||||
Editor_state.expanded = true
|
||||
end
|
||||
|
||||
function log_browser.line_index(State, mx,my)
|
||||
-- duplicate some logic from log_browser.draw
|
||||
local y = State.top
|
||||
for line_index = State.screen_top1.line,#State.lines do
|
||||
local line = State.lines[line_index]
|
||||
if should_show(line) then
|
||||
y = y + log_browser.height(State, line_index)
|
||||
if my < y then
|
||||
return line_index
|
||||
end
|
||||
if y > App.screen.height then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function log_browser.mouse_released(State, x,y, mouse_button)
|
||||
end
|
||||
|
||||
function log_browser.textinput(State, t)
|
||||
end
|
||||
|
||||
function log_browser.keychord_pressed(State, chord, key)
|
||||
-- move
|
||||
if chord == 'up' then
|
||||
while State.screen_top1.line > 1 do
|
||||
State.screen_top1.line = State.screen_top1.line-1
|
||||
if should_show(State.lines[State.screen_top1.line]) then
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif chord == 'down' then
|
||||
while State.screen_top1.line < #State.lines do
|
||||
State.screen_top1.line = State.screen_top1.line+1
|
||||
if should_show(State.lines[State.screen_top1.line]) then
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif chord == 'pageup' then
|
||||
local y = 0
|
||||
while State.screen_top1.line > 1 and y < App.screen.height - 100 do
|
||||
State.screen_top1.line = State.screen_top1.line - 1
|
||||
if should_show(State.lines[State.screen_top1.line]) then
|
||||
y = y + log_browser.height(State, State.screen_top1.line)
|
||||
end
|
||||
end
|
||||
elseif chord == 'pagedown' then
|
||||
local y = 0
|
||||
while State.screen_top1.line < #State.lines and y < App.screen.height - 100 do
|
||||
if should_show(State.lines[State.screen_top1.line]) then
|
||||
y = y + log_browser.height(State, State.screen_top1.line)
|
||||
end
|
||||
State.screen_top1.line = State.screen_top1.line + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function log_browser.height(State, line_index)
|
||||
local line = State.lines[line_index]
|
||||
if line.data == nil then
|
||||
-- section header
|
||||
return State.line_height
|
||||
elseif type(line.data) == 'string' then
|
||||
return State.line_height
|
||||
else
|
||||
if line.height == nil then
|
||||
--? print('nil line height! rendering off screen to calculate')
|
||||
line.height = log_render[line.data.name](line.data, State.left, App.screen.height, State.right-State.left)
|
||||
end
|
||||
return line.height
|
||||
end
|
||||
end
|
||||
|
||||
function log_browser.keyreleased(State, key, scancode)
|
||||
end
|
381
main.lua
381
main.lua
|
@ -1,217 +1,260 @@
|
|||
-- Entrypoint for the app. You can edit this file from within the app if
|
||||
-- you're careful.
|
||||
|
||||
-- files that come with LÖVE; we can't edit those from within the app
|
||||
utf8 = require 'utf8'
|
||||
|
||||
require 'app'
|
||||
require 'test'
|
||||
function load_file_from_source_or_save_directory(filename)
|
||||
local contents = love.filesystem.read(filename)
|
||||
local code, err = loadstring(contents, filename)
|
||||
if code == nil then
|
||||
error(err)
|
||||
end
|
||||
return code()
|
||||
end
|
||||
|
||||
require 'keychord'
|
||||
require 'button'
|
||||
json = load_file_from_source_or_save_directory('json.lua')
|
||||
|
||||
require 'main_tests'
|
||||
load_file_from_source_or_save_directory('app.lua')
|
||||
load_file_from_source_or_save_directory('test.lua')
|
||||
|
||||
-- delegate most business logic to a layer that can be reused by other projects
|
||||
require 'edit'
|
||||
Editor_state = {}
|
||||
load_file_from_source_or_save_directory('keychord.lua')
|
||||
load_file_from_source_or_save_directory('button.lua')
|
||||
|
||||
-- both sides require (different parts of) the logging framework
|
||||
load_file_from_source_or_save_directory('log.lua')
|
||||
|
||||
-- but some files we want to only load sometimes
|
||||
function App.load()
|
||||
if love.filesystem.getInfo('config') then
|
||||
Settings = json.decode(love.filesystem.read('config'))
|
||||
Current_app = Settings.current_app
|
||||
end
|
||||
|
||||
if Current_app == nil then
|
||||
Current_app = 'run'
|
||||
end
|
||||
|
||||
if Current_app == 'run' then
|
||||
load_file_from_source_or_save_directory('file.lua')
|
||||
load_file_from_source_or_save_directory('run.lua')
|
||||
load_file_from_source_or_save_directory('edit.lua')
|
||||
load_file_from_source_or_save_directory('text.lua')
|
||||
load_file_from_source_or_save_directory('search.lua')
|
||||
load_file_from_source_or_save_directory('select.lua')
|
||||
load_file_from_source_or_save_directory('undo.lua')
|
||||
load_file_from_source_or_save_directory('icons.lua')
|
||||
load_file_from_source_or_save_directory('text_tests.lua')
|
||||
load_file_from_source_or_save_directory('run_tests.lua')
|
||||
load_file_from_source_or_save_directory('drawing.lua')
|
||||
load_file_from_source_or_save_directory('geom.lua')
|
||||
load_file_from_source_or_save_directory('help.lua')
|
||||
load_file_from_source_or_save_directory('drawing_tests.lua')
|
||||
else
|
||||
load_file_from_source_or_save_directory('source_file.lua')
|
||||
load_file_from_source_or_save_directory('source.lua')
|
||||
load_file_from_source_or_save_directory('commands.lua')
|
||||
load_file_from_source_or_save_directory('source_edit.lua')
|
||||
load_file_from_source_or_save_directory('log_browser.lua')
|
||||
load_file_from_source_or_save_directory('source_text.lua')
|
||||
load_file_from_source_or_save_directory('search.lua')
|
||||
load_file_from_source_or_save_directory('select.lua')
|
||||
load_file_from_source_or_save_directory('source_undo.lua')
|
||||
load_file_from_source_or_save_directory('colorize.lua')
|
||||
load_file_from_source_or_save_directory('source_text_tests.lua')
|
||||
load_file_from_source_or_save_directory('source_tests.lua')
|
||||
end
|
||||
end
|
||||
|
||||
-- called both in tests and real run
|
||||
function App.initialize_globals()
|
||||
-- tests currently mostly clear their own state
|
||||
|
||||
-- a few text objects we can avoid recomputing unless the font changes
|
||||
Text_cache = {}
|
||||
|
||||
-- blinking cursor
|
||||
Cursor_time = 0
|
||||
if Current_app == 'run' then
|
||||
run.initialize_globals()
|
||||
elseif Current_app == 'source' then
|
||||
source.initialize_globals()
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
|
||||
-- for hysteresis in a few places
|
||||
Last_resize_time = App.getTime()
|
||||
Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700
|
||||
Last_resize_time = App.getTime()
|
||||
end
|
||||
|
||||
-- called only for real run
|
||||
function App.initialize(arg)
|
||||
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
|
||||
love.keyboard.setKeyRepeat(true)
|
||||
|
||||
love.graphics.setBackgroundColor(1,1,1)
|
||||
|
||||
if love.filesystem.getInfo('config') then
|
||||
load_settings()
|
||||
if Current_app == 'run' then
|
||||
run.initialize(arg)
|
||||
elseif Current_app == 'source' then
|
||||
source.initialize(arg)
|
||||
else
|
||||
initialize_default_settings()
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
love.window.setTitle('text.love - '..Current_app)
|
||||
end
|
||||
|
||||
if #arg > 0 then
|
||||
Editor_state.filename = arg[1]
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
edit.fixup_cursor(Editor_state)
|
||||
function App.resize(w,h)
|
||||
if Current_app == 'run' then
|
||||
if run.resize then run.resize(w,h) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.resize then source.resize(w,h) end
|
||||
else
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
if Editor_state.cursor1.line > #Editor_state.lines or Editor_state.lines[Editor_state.cursor1.line].mode ~= 'text' then
|
||||
edit.fixup_cursor(Editor_state)
|
||||
end
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
love.window.setTitle('lines.love - '..Editor_state.filename)
|
||||
|
||||
if #arg > 1 then
|
||||
print('ignoring commandline args after '..arg[1])
|
||||
end
|
||||
|
||||
if rawget(_G, 'jit') then
|
||||
jit.off()
|
||||
jit.flush()
|
||||
end
|
||||
end
|
||||
|
||||
function load_settings()
|
||||
local settings = json.decode(love.filesystem.read('config'))
|
||||
love.graphics.setFont(love.graphics.newFont(settings.font_height))
|
||||
-- maximize window to determine maximum allowable dimensions
|
||||
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
|
||||
-- set up desired window dimensions
|
||||
love.window.setPosition(settings.x, settings.y, settings.displayindex)
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(App.screen.width, 200)
|
||||
App.screen.flags.minheight = math.min(App.screen.width, 200)
|
||||
App.screen.width, App.screen.height = settings.width, settings.height
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, settings.font_height, math.floor(settings.font_height*1.3))
|
||||
Editor_state.filename = settings.filename
|
||||
Editor_state.screen_top1 = settings.screen_top
|
||||
Editor_state.cursor1 = settings.cursor
|
||||
end
|
||||
|
||||
function initialize_default_settings()
|
||||
local font_height = 20
|
||||
love.graphics.setFont(love.graphics.newFont(font_height))
|
||||
local em = App.newText(love.graphics.getFont(), 'm')
|
||||
initialize_window_geometry(App.width(em))
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
|
||||
Editor_state.font_height = font_height
|
||||
Editor_state.line_height = math.floor(font_height*1.3)
|
||||
Editor_state.em = em
|
||||
end
|
||||
|
||||
function initialize_window_geometry(em_width)
|
||||
-- maximize window
|
||||
love.window.setMode(0, 0) -- maximize
|
||||
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
|
||||
-- shrink height slightly to account for window decoration
|
||||
App.screen.height = App.screen.height-100
|
||||
App.screen.width = 40*em_width
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(App.screen.width, 200)
|
||||
App.screen.flags.minheight = math.min(App.screen.width, 200)
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
end
|
||||
|
||||
function App.resize(w, h)
|
||||
--? print(("Window resized to width: %d and height: %d."):format(w, h))
|
||||
App.screen.width, App.screen.height = w, h
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.selection1 = {} -- no support for shift drag while we're resizing
|
||||
Editor_state.right = App.screen.width-Margin_right
|
||||
Editor_state.width = Editor_state.right-Editor_state.left
|
||||
Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
|
||||
Last_resize_time = App.getTime()
|
||||
end
|
||||
|
||||
function App.filedropped(file)
|
||||
-- first make sure to save edits on any existing file
|
||||
if Editor_state.next_save then
|
||||
save_to_disk(Editor_state)
|
||||
if Current_app == 'run' then
|
||||
if run.filedropped then run.filedropped(file) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.filedropped then source.filedropped(file) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
-- clear the slate for the new file
|
||||
App.initialize_globals()
|
||||
Editor_state.filename = file:getFilename()
|
||||
file:open('r')
|
||||
Editor_state.lines = load_from_file(file)
|
||||
file:close()
|
||||
Text.redraw_all(Editor_state)
|
||||
edit.fixup_cursor(Editor_state)
|
||||
love.window.setTitle('lines.love - '..Editor_state.filename)
|
||||
end
|
||||
|
||||
function App.draw()
|
||||
edit.draw(Editor_state)
|
||||
end
|
||||
|
||||
function App.update(dt)
|
||||
Cursor_time = Cursor_time + dt
|
||||
-- some hysteresis while resizing
|
||||
if App.getTime() < Last_resize_time + 0.1 then
|
||||
return
|
||||
end
|
||||
edit.update(Editor_state, dt)
|
||||
end
|
||||
|
||||
function love.quit()
|
||||
edit.quit(Editor_state)
|
||||
-- save some important settings
|
||||
local x,y,displayindex = love.window.getPosition()
|
||||
local filename = Editor_state.filename
|
||||
if filename:sub(1,1) ~= '/' then
|
||||
filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
|
||||
end
|
||||
local settings = {
|
||||
x=x, y=y, displayindex=displayindex,
|
||||
width=App.screen.width, height=App.screen.height,
|
||||
font_height=Editor_state.font_height,
|
||||
filename=filename,
|
||||
screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1}
|
||||
love.filesystem.write('config', json.encode(settings))
|
||||
end
|
||||
|
||||
function App.mousepressed(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.mouse_pressed(Editor_state, x,y, mouse_button)
|
||||
end
|
||||
|
||||
function App.mousereleased(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.mouse_released(Editor_state, x,y, mouse_button)
|
||||
love.window.setTitle('text.love - '..Current_app)
|
||||
end
|
||||
|
||||
function App.focus(in_focus)
|
||||
if in_focus then
|
||||
Last_focus_time = App.getTime()
|
||||
end
|
||||
if Current_app == 'run' then
|
||||
if run.focus then run.focus(in_focus) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.focus then source.focus(in_focus) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.textinput(t)
|
||||
-- ignore events for some time after window in focus
|
||||
if App.getTime() < Last_focus_time + 0.01 then
|
||||
function App.draw()
|
||||
if Current_app == 'run' then
|
||||
run.draw()
|
||||
elseif Current_app == 'source' then
|
||||
source.draw()
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.update(dt)
|
||||
-- some hysteresis while resizing
|
||||
if App.getTime() < Last_resize_time + 0.1 then
|
||||
return
|
||||
end
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.textinput(Editor_state, t)
|
||||
--
|
||||
if Current_app == 'run' then
|
||||
run.update(dt)
|
||||
elseif Current_app == 'source' then
|
||||
source.update(dt)
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.keychord_pressed(chord, key)
|
||||
-- ignore events for some time after window in focus
|
||||
-- ignore events for some time after window in focus (mostly alt-tab)
|
||||
if App.getTime() < Last_focus_time + 0.01 then
|
||||
return
|
||||
end
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.keychord_pressed(Editor_state, chord, key)
|
||||
--
|
||||
if chord == 'C-e' then
|
||||
-- carefully save settings
|
||||
if Current_app == 'run' then
|
||||
local source_settings = Settings.source
|
||||
Settings = run.settings()
|
||||
Settings.source = source_settings
|
||||
if run.quit then run.quit() end
|
||||
Current_app = 'source'
|
||||
elseif Current_app == 'source' then
|
||||
Settings.source = source.settings()
|
||||
if source.quit then source.quit() end
|
||||
Current_app = 'run'
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
Settings.current_app = Current_app
|
||||
love.filesystem.write('config', json.encode(Settings))
|
||||
-- reboot
|
||||
load_file_from_source_or_save_directory('main.lua')
|
||||
App.undo_initialize()
|
||||
App.run_tests_and_initialize()
|
||||
return
|
||||
end
|
||||
if Current_app == 'run' then
|
||||
if run.keychord_pressed then run.keychord_pressed(chord, key) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.keychord_pressed then source.keychord_pressed(chord, key) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.keyreleased(key, scancode)
|
||||
-- ignore events for some time after window in focus
|
||||
function App.textinput(t)
|
||||
-- ignore events for some time after window in focus (mostly alt-tab)
|
||||
if App.getTime() < Last_focus_time + 0.01 then
|
||||
return
|
||||
end
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.key_released(Editor_state, key, scancode)
|
||||
--
|
||||
if Current_app == 'run' then
|
||||
if run.textinput then run.textinput(t) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.textinput then source.textinput(t) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
-- use this sparingly
|
||||
function to_text(s)
|
||||
if Text_cache[s] == nil then
|
||||
Text_cache[s] = App.newText(love.graphics.getFont(), s)
|
||||
function App.keyreleased(chord, key)
|
||||
-- ignore events for some time after window in focus (mostly alt-tab)
|
||||
if App.getTime() < Last_focus_time + 0.01 then
|
||||
return
|
||||
end
|
||||
--
|
||||
if Current_app == 'run' then
|
||||
if run.key_released then run.key_released(chord, key) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.key_released then source.key_released(chord, key) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.mousepressed(x,y, mouse_button)
|
||||
--? print('mouse press', x,y)
|
||||
if Current_app == 'run' then
|
||||
if run.mouse_pressed then run.mouse_pressed(x,y, mouse_button) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.mouse_pressed then source.mouse_pressed(x,y, mouse_button) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function App.mousereleased(x,y, mouse_button)
|
||||
if Current_app == 'run' then
|
||||
if run.mouse_released then run.mouse_released(x,y, mouse_button) end
|
||||
elseif Current_app == 'source' then
|
||||
if source.mouse_released then source.mouse_released(x,y, mouse_button) end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
end
|
||||
|
||||
function love.quit()
|
||||
if Current_app == 'run' then
|
||||
local source_settings = Settings.source
|
||||
Settings = run.settings()
|
||||
Settings.source = source_settings
|
||||
else
|
||||
Settings.source = source.settings()
|
||||
end
|
||||
Settings.current_app = Current_app
|
||||
love.filesystem.write('config', json.encode(Settings))
|
||||
if Current_app == 'run' then
|
||||
if run.quit then run.quit() end
|
||||
elseif Current_app == 'source' then
|
||||
if source.quit then source.quit() end
|
||||
else
|
||||
assert(false, 'unknown app "'..Current_app..'"')
|
||||
end
|
||||
return Text_cache[s]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
run = {}
|
||||
|
||||
Editor_state = {}
|
||||
|
||||
-- called both in tests and real run
|
||||
function run.initialize_globals()
|
||||
-- tests currently mostly clear their own state
|
||||
|
||||
-- a few text objects we can avoid recomputing unless the font changes
|
||||
Text_cache = {}
|
||||
|
||||
-- blinking cursor
|
||||
Cursor_time = 0
|
||||
end
|
||||
|
||||
-- called only for real run
|
||||
function run.initialize(arg)
|
||||
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
|
||||
love.keyboard.setKeyRepeat(true)
|
||||
|
||||
love.graphics.setBackgroundColor(1,1,1)
|
||||
|
||||
if Settings then
|
||||
run.load_settings()
|
||||
else
|
||||
run.initialize_default_settings()
|
||||
end
|
||||
|
||||
if #arg > 0 then
|
||||
Editor_state.filename = arg[1]
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
edit.fixup_cursor(Editor_state)
|
||||
else
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
if Editor_state.cursor1.line > #Editor_state.lines or Editor_state.lines[Editor_state.cursor1.line].mode ~= 'text' then
|
||||
edit.fixup_cursor(Editor_state)
|
||||
end
|
||||
end
|
||||
love.window.setTitle('lines.love - '..Editor_state.filename)
|
||||
|
||||
if #arg > 1 then
|
||||
print('ignoring commandline args after '..arg[1])
|
||||
end
|
||||
|
||||
if rawget(_G, 'jit') then
|
||||
jit.off()
|
||||
jit.flush()
|
||||
end
|
||||
end
|
||||
|
||||
function run.load_settings()
|
||||
love.graphics.setFont(love.graphics.newFont(Settings.font_height))
|
||||
-- maximize window to determine maximum allowable dimensions
|
||||
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
|
||||
-- set up desired window dimensions
|
||||
love.window.setPosition(Settings.x, Settings.y, Settings.displayindex)
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(App.screen.width, 200)
|
||||
App.screen.flags.minheight = math.min(App.screen.width, 200)
|
||||
App.screen.width, App.screen.height = Settings.width, Settings.height
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, Settings.font_height, math.floor(Settings.font_height*1.3))
|
||||
Editor_state.filename = Settings.filename
|
||||
Editor_state.screen_top1 = Settings.screen_top
|
||||
Editor_state.cursor1 = Settings.cursor
|
||||
end
|
||||
|
||||
function run.initialize_default_settings()
|
||||
local font_height = 20
|
||||
love.graphics.setFont(love.graphics.newFont(font_height))
|
||||
local em = App.newText(love.graphics.getFont(), 'm')
|
||||
run.initialize_window_geometry(App.width(em))
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
|
||||
Editor_state.font_height = font_height
|
||||
Editor_state.line_height = math.floor(font_height*1.3)
|
||||
Editor_state.em = em
|
||||
Settings = run.settings()
|
||||
end
|
||||
|
||||
function run.initialize_window_geometry(em_width)
|
||||
-- maximize window
|
||||
love.window.setMode(0, 0) -- maximize
|
||||
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
|
||||
-- shrink height slightly to account for window decoration
|
||||
App.screen.height = App.screen.height-100
|
||||
App.screen.width = 40*em_width
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(App.screen.width, 200)
|
||||
App.screen.flags.minheight = math.min(App.screen.width, 200)
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
end
|
||||
|
||||
function run.resize(w, h)
|
||||
--? print(("Window resized to width: %d and height: %d."):format(w, h))
|
||||
App.screen.width, App.screen.height = w, h
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.selection1 = {} -- no support for shift drag while we're resizing
|
||||
Editor_state.right = App.screen.width-Margin_right
|
||||
Editor_state.width = Editor_state.right-Editor_state.left
|
||||
Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
|
||||
end
|
||||
|
||||
function run.filedropped(file)
|
||||
-- first make sure to save edits on any existing file
|
||||
if Editor_state.next_save then
|
||||
save_to_disk(Editor_state)
|
||||
end
|
||||
-- clear the slate for the new file
|
||||
App.initialize_globals()
|
||||
Editor_state.filename = file:getFilename()
|
||||
file:open('r')
|
||||
Editor_state.lines = load_from_file(file)
|
||||
file:close()
|
||||
Text.redraw_all(Editor_state)
|
||||
edit.fixup_cursor(Editor_state)
|
||||
love.window.setTitle('lines.love - '..Editor_state.filename)
|
||||
end
|
||||
|
||||
function run.draw()
|
||||
edit.draw(Editor_state)
|
||||
end
|
||||
|
||||
function run.update(dt)
|
||||
Cursor_time = Cursor_time + dt
|
||||
edit.update(Editor_state, dt)
|
||||
end
|
||||
|
||||
function run.quit()
|
||||
edit.quit(Editor_state)
|
||||
end
|
||||
|
||||
function run.settings()
|
||||
local x,y,displayindex = love.window.getPosition()
|
||||
local filename = Editor_state.filename
|
||||
if filename:sub(1,1) ~= '/' then
|
||||
filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
|
||||
end
|
||||
return {
|
||||
x=x, y=y, displayindex=displayindex,
|
||||
width=App.screen.width, height=App.screen.height,
|
||||
font_height=Editor_state.font_height,
|
||||
filename=filename,
|
||||
screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1
|
||||
}
|
||||
end
|
||||
|
||||
function run.mouse_pressed(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.mouse_pressed(Editor_state, x,y, mouse_button)
|
||||
end
|
||||
|
||||
function run.mouse_released(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.mouse_released(Editor_state, x,y, mouse_button)
|
||||
end
|
||||
|
||||
function run.textinput(t)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.textinput(Editor_state, t)
|
||||
end
|
||||
|
||||
function run.keychord_pressed(chord, key)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.keychord_pressed(Editor_state, chord, key)
|
||||
end
|
||||
|
||||
function run.key_released(key, scancode)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
return edit.key_released(Editor_state, key, scancode)
|
||||
end
|
||||
|
||||
-- use this sparingly
|
||||
function to_text(s)
|
||||
if Text_cache[s] == nil then
|
||||
Text_cache[s] = App.newText(love.graphics.getFont(), s)
|
||||
end
|
||||
return Text_cache[s]
|
||||
end
|
20
search.lua
20
search.lua
|
@ -30,8 +30,7 @@ function Text.search_next(State)
|
|||
for i=State.cursor1.line+1,#State.lines do
|
||||
pos = find(State.lines[i].data, State.search_term)
|
||||
if pos then
|
||||
State.cursor1.line = i
|
||||
State.cursor1.pos = pos
|
||||
State.cursor1 = {line=i, pos=pos}
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -41,8 +40,7 @@ function Text.search_next(State)
|
|||
for i=1,State.cursor1.line-1 do
|
||||
pos = find(State.lines[i].data, State.search_term)
|
||||
if pos then
|
||||
State.cursor1.line = i
|
||||
State.cursor1.pos = pos
|
||||
State.cursor1 = {line=i, pos=pos}
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -78,8 +76,7 @@ function Text.search_previous(State)
|
|||
for i=State.cursor1.line-1,1,-1 do
|
||||
pos = rfind(State.lines[i].data, State.search_term)
|
||||
if pos then
|
||||
State.cursor1.line = i
|
||||
State.cursor1.pos = pos
|
||||
State.cursor1 = {line=i, pos=pos}
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -89,8 +86,7 @@ function Text.search_previous(State)
|
|||
for i=#State.lines,State.cursor1.line+1,-1 do
|
||||
pos = rfind(State.lines[i].data, State.search_term)
|
||||
if pos then
|
||||
State.cursor1.line = i
|
||||
State.cursor1.pos = pos
|
||||
State.cursor1 = {line=i, pos=pos}
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -115,18 +111,18 @@ function Text.search_previous(State)
|
|||
end
|
||||
end
|
||||
|
||||
function find(s, pat, i)
|
||||
function find(s, pat, i, plain)
|
||||
if s == nil then return end
|
||||
return s:find(pat, i)
|
||||
return s:find(pat, i, plain)
|
||||
end
|
||||
|
||||
function rfind(s, pat, i)
|
||||
function rfind(s, pat, i, plain)
|
||||
if s == nil then return end
|
||||
local rs = s:reverse()
|
||||
local rpat = pat:reverse()
|
||||
if i == nil then i = #s end
|
||||
local ri = #s - i + 1
|
||||
local rendpos = rs:find(rpat, ri)
|
||||
local rendpos = rs:find(rpat, ri, plain)
|
||||
if rendpos == nil then return nil end
|
||||
local endpos = #s - rendpos + 1
|
||||
assert (endpos >= #pat)
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
source = {}
|
||||
|
||||
Editor_state = {}
|
||||
|
||||
-- called both in tests and real run
|
||||
function source.initialize_globals()
|
||||
-- tests currently mostly clear their own state
|
||||
|
||||
Show_log_browser_side = false
|
||||
Focus = 'edit'
|
||||
Show_file_navigator = false
|
||||
File_navigation = {
|
||||
candidates = {
|
||||
'run',
|
||||
'run_tests',
|
||||
'log',
|
||||
'edit',
|
||||
'text',
|
||||
'search',
|
||||
'select',
|
||||
'undo',
|
||||
'text_tests',
|
||||
'file',
|
||||
'source',
|
||||
'source_tests',
|
||||
'commands',
|
||||
'log_browser',
|
||||
'source_edit',
|
||||
'source_text',
|
||||
'source_undo',
|
||||
'colorize',
|
||||
'source_text_tests',
|
||||
'source_file',
|
||||
'main',
|
||||
'button',
|
||||
'keychord',
|
||||
'app',
|
||||
'test',
|
||||
'json',
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
Menu_status_bar_height = nil -- initialized below
|
||||
|
||||
-- a few text objects we can avoid recomputing unless the font changes
|
||||
Text_cache = {}
|
||||
|
||||
-- blinking cursor
|
||||
Cursor_time = 0
|
||||
end
|
||||
|
||||
-- called only for real run
|
||||
function source.initialize()
|
||||
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
|
||||
love.keyboard.setKeyRepeat(true)
|
||||
|
||||
love.graphics.setBackgroundColor(1,1,1)
|
||||
|
||||
if Settings and Settings.source then
|
||||
source.load_settings()
|
||||
else
|
||||
source.initialize_default_settings()
|
||||
end
|
||||
|
||||
source.initialize_edit_side{'run.lua'}
|
||||
source.initialize_log_browser_side()
|
||||
|
||||
Menu_status_bar_height = 5 + Editor_state.line_height + 5
|
||||
Editor_state.top = Editor_state.top + Menu_status_bar_height
|
||||
Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height
|
||||
end
|
||||
|
||||
-- environment for a mutable file of bifolded text
|
||||
-- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
|
||||
function source.initialize_edit_side(arg)
|
||||
if #arg > 0 then
|
||||
Editor_state.filename = arg[1]
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
else
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
end
|
||||
|
||||
if #arg > 1 then
|
||||
print('ignoring commandline args after '..arg[1])
|
||||
end
|
||||
|
||||
-- We currently start out with side B collapsed.
|
||||
-- Other options:
|
||||
-- * save all expanded state by line
|
||||
-- * expand all if any location is in side B
|
||||
if Editor_state.cursor1.line > #Editor_state.lines then
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
end
|
||||
if Editor_state.screen_top1.line > #Editor_state.lines then
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
end
|
||||
edit.eradicate_locations_after_the_fold(Editor_state)
|
||||
|
||||
if rawget(_G, 'jit') then
|
||||
jit.off()
|
||||
jit.flush()
|
||||
end
|
||||
end
|
||||
|
||||
function source.load_settings()
|
||||
local settings = Settings.source
|
||||
love.graphics.setFont(love.graphics.newFont(settings.font_height))
|
||||
-- maximize window to determine maximum allowable dimensions
|
||||
love.window.setMode(0, 0) -- maximize
|
||||
Display_width, Display_height, App.screen.flags = love.window.getMode()
|
||||
-- set up desired window dimensions
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(Display_width, 200)
|
||||
App.screen.flags.minheight = math.min(Display_height, 200)
|
||||
App.screen.width, App.screen.height = settings.width, settings.height
|
||||
--? print('setting window from settings:', App.screen.width, App.screen.height)
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
--? print('loading source position', settings.x, settings.y, settings.displayindex)
|
||||
source.set_window_position_from_settings(settings)
|
||||
Show_log_browser_side = settings.show_log_browser_side
|
||||
local right = App.screen.width - Margin_right
|
||||
if Show_log_browser_side then
|
||||
right = App.screen.width/2 - Margin_right
|
||||
end
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, right, settings.font_height, math.floor(settings.font_height*1.3))
|
||||
Editor_state.filename = settings.filename
|
||||
Editor_state.screen_top1 = settings.screen_top
|
||||
Editor_state.cursor1 = settings.cursor
|
||||
end
|
||||
|
||||
function source.set_window_position_from_settings(settings)
|
||||
-- setPosition doesn't quite seem to do what is asked of it on Linux.
|
||||
love.window.setPosition(settings.x, settings.y-37, settings.displayindex)
|
||||
end
|
||||
|
||||
function source.initialize_default_settings()
|
||||
local font_height = 20
|
||||
love.graphics.setFont(love.graphics.newFont(font_height))
|
||||
local em = App.newText(love.graphics.getFont(), 'm')
|
||||
source.initialize_window_geometry(App.width(em))
|
||||
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
|
||||
Editor_state.font_height = font_height
|
||||
Editor_state.line_height = math.floor(font_height*1.3)
|
||||
Editor_state.em = em
|
||||
end
|
||||
|
||||
function source.initialize_window_geometry(em_width)
|
||||
-- maximize window
|
||||
love.window.setMode(0, 0) -- maximize
|
||||
Display_width, Display_height, App.screen.flags = love.window.getMode()
|
||||
-- shrink height slightly to account for window decoration
|
||||
App.screen.height = Display_height-100
|
||||
App.screen.width = 40*em_width
|
||||
App.screen.flags.resizable = true
|
||||
App.screen.flags.minwidth = math.min(App.screen.width, 200)
|
||||
App.screen.flags.minheight = math.min(App.screen.width, 200)
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
print('initializing source position')
|
||||
if Settings == nil then Settings = {} end
|
||||
if Settings.source == nil then Settings.source = {} end
|
||||
Settings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()
|
||||
end
|
||||
|
||||
function source.resize(w, h)
|
||||
--? print(("Window resized to width: %d and height: %d."):format(w, h))
|
||||
App.screen.width, App.screen.height = w, h
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.selection1 = {} -- no support for shift drag while we're resizing
|
||||
if Show_log_browser_side then
|
||||
Editor_state.right = App.screen.width/2 - Margin_right
|
||||
else
|
||||
Editor_state.right = App.screen.width-Margin_right
|
||||
end
|
||||
Log_browser_state.left = App.screen.width/2 + Margin_right
|
||||
Log_browser_state.right = App.screen.width-Margin_right
|
||||
Editor_state.width = Editor_state.right-Editor_state.left
|
||||
Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
|
||||
--? print('end resize')
|
||||
end
|
||||
|
||||
function source.filedropped(file)
|
||||
-- first make sure to save edits on any existing file
|
||||
if Editor_state.next_save then
|
||||
save_to_disk(Editor_state)
|
||||
end
|
||||
-- clear the slate for the new file
|
||||
Editor_state.filename = file:getFilename()
|
||||
file:open('r')
|
||||
Editor_state.lines = load_from_file(file)
|
||||
file:close()
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
end
|
||||
|
||||
-- a copy of source.filedropped when given a filename
|
||||
function source.switch_to_file(filename)
|
||||
-- first make sure to save edits on any existing file
|
||||
if Editor_state.next_save then
|
||||
save_to_disk(Editor_state)
|
||||
end
|
||||
-- clear the slate for the new file
|
||||
Editor_state.filename = filename
|
||||
load_from_disk(Editor_state)
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
end
|
||||
|
||||
function source.draw()
|
||||
source.draw_menu_bar()
|
||||
edit.draw(Editor_state)
|
||||
if Show_log_browser_side then
|
||||
-- divider
|
||||
App.color(Divider_color)
|
||||
love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)
|
||||
--
|
||||
log_browser.draw(Log_browser_state)
|
||||
end
|
||||
end
|
||||
|
||||
function source.update(dt)
|
||||
Cursor_time = Cursor_time + dt
|
||||
if App.mouse_x() < Editor_state.right then
|
||||
edit.update(Editor_state, dt)
|
||||
elseif Show_log_browser_side then
|
||||
log_browser.update(Log_browser_state, dt)
|
||||
end
|
||||
end
|
||||
|
||||
function source.quit()
|
||||
edit.quit(Editor_state)
|
||||
log_browser.quit(Log_browser_state)
|
||||
-- convert any bifold files here
|
||||
end
|
||||
|
||||
function source.convert_bifold_text(infilename, outfilename)
|
||||
local contents = love.filesystem.read(infilename)
|
||||
contents = contents:gsub('\u{1e}', ';')
|
||||
love.filesystem.write(outfilename, contents)
|
||||
end
|
||||
|
||||
function source.settings()
|
||||
if Current_app == 'source' then
|
||||
--? print('reading source window position')
|
||||
Settings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()
|
||||
end
|
||||
local filename = Editor_state.filename
|
||||
if filename:sub(1,1) ~= '/' then
|
||||
filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
|
||||
end
|
||||
--? print('saving source settings', Settings.source.x, Settings.source.y, Settings.source.displayindex)
|
||||
return {
|
||||
x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,
|
||||
width=App.screen.width, height=App.screen.height,
|
||||
font_height=Editor_state.font_height,
|
||||
filename=filename,
|
||||
screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1,
|
||||
show_log_browser_side=Show_log_browser_side,
|
||||
focus=Focus,
|
||||
}
|
||||
end
|
||||
|
||||
function source.mouse_pressed(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
--? print('mouse click', x, y)
|
||||
--? print(Editor_state.left, Editor_state.right)
|
||||
--? print(Log_browser_state.left, Log_browser_state.right)
|
||||
if Editor_state.left <= x and x < Editor_state.right then
|
||||
--? print('click on edit side')
|
||||
if Focus ~= 'edit' then
|
||||
Focus = 'edit'
|
||||
end
|
||||
edit.mouse_pressed(Editor_state, x,y, mouse_button)
|
||||
elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then
|
||||
--? print('click on log_browser side')
|
||||
if Focus ~= 'log_browser' then
|
||||
Focus = 'log_browser'
|
||||
end
|
||||
log_browser.mouse_pressed(Log_browser_state, x,y, mouse_button)
|
||||
for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
end
|
||||
end
|
||||
|
||||
function source.mouse_released(x,y, mouse_button)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
if Focus == 'edit' then
|
||||
return edit.mouse_released(Editor_state, x,y, mouse_button)
|
||||
else
|
||||
return log_browser.mouse_released(Log_browser_state, x,y, mouse_button)
|
||||
end
|
||||
end
|
||||
|
||||
function source.textinput(t)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
if Focus == 'edit' then
|
||||
return edit.textinput(Editor_state, t)
|
||||
else
|
||||
return log_browser.textinput(Log_browser_state, t)
|
||||
end
|
||||
end
|
||||
|
||||
function source.keychord_pressed(chord, key)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
--? print('source keychord')
|
||||
if Show_file_navigator then
|
||||
keychord_pressed_on_file_navigator(chord, key)
|
||||
return
|
||||
end
|
||||
if chord == 'C-l' then
|
||||
--? print('C-l')
|
||||
Show_log_browser_side = not Show_log_browser_side
|
||||
if Show_log_browser_side then
|
||||
App.screen.width = Log_browser_state.right + Margin_right
|
||||
else
|
||||
App.screen.width = Editor_state.right + Margin_right
|
||||
end
|
||||
--? print('setting window:', App.screen.width, App.screen.height)
|
||||
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
|
||||
--? print('done setting window')
|
||||
-- try to restore position if possible
|
||||
-- if the window gets wider the window manager may not respect this
|
||||
source.set_window_position_from_settings(Settings.source)
|
||||
return
|
||||
end
|
||||
if chord == 'C-g' then
|
||||
Show_file_navigator = true
|
||||
File_navigation.index = 1
|
||||
return
|
||||
end
|
||||
if Focus == 'edit' then
|
||||
return edit.keychord_pressed(Editor_state, chord, key)
|
||||
else
|
||||
return log_browser.keychord_pressed(Log_browser_state, chord, key)
|
||||
end
|
||||
end
|
||||
|
||||
function source.key_released(key, scancode)
|
||||
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
|
||||
if Focus == 'edit' then
|
||||
return edit.key_released(Editor_state, key, scancode)
|
||||
else
|
||||
return log_browser.keychord_pressed(Log_browser_state, chordkey, scancode)
|
||||
end
|
||||
end
|
||||
|
||||
-- use this sparingly
|
||||
function to_text(s)
|
||||
if Text_cache[s] == nil then
|
||||
Text_cache[s] = App.newText(love.graphics.getFont(), s)
|
||||
end
|
||||
return Text_cache[s]
|
||||
end
|
|
@ -0,0 +1,377 @@
|
|||
-- some constants people might like to tweak
|
||||
Text_color = {r=0, g=0, b=0}
|
||||
Cursor_color = {r=1, g=0, b=0}
|
||||
Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering over
|
||||
Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text
|
||||
Fold_color = {r=0, g=0.6, b=0}
|
||||
Fold_background_color = {r=0, g=0.7, b=0}
|
||||
|
||||
Margin_top = 15
|
||||
Margin_left = 25
|
||||
Margin_right = 25
|
||||
|
||||
edit = {}
|
||||
|
||||
-- run in both tests and a real run
|
||||
function edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screen
|
||||
local result = {
|
||||
-- a line of bifold text consists of an A side and an optional B side, each of which is a string
|
||||
-- expanded: whether to show B side
|
||||
lines = {{data='', dataB=nil, expanded=nil}}, -- array of lines
|
||||
|
||||
-- Lines can be too long to fit on screen, in which case they _wrap_ into
|
||||
-- multiple _screen lines_.
|
||||
|
||||
-- rendering wrapped text lines needs some additional short-lived data per line:
|
||||
-- startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen
|
||||
-- starty, the y coord in pixels the line starts rendering from
|
||||
-- fragments: snippets of rendered love.graphics.Text, guaranteed to not straddle screen lines
|
||||
-- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
|
||||
line_cache = {},
|
||||
|
||||
-- Given wrapping, any potential location for the text cursor can be described in two ways:
|
||||
-- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
|
||||
-- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
|
||||
-- Positions (and screen line indexes) can be in either the A or the B side.
|
||||
--
|
||||
-- Most of the time we'll only persist positions in schema 1, translating to
|
||||
-- schema 2 when that's convenient.
|
||||
--
|
||||
-- Make sure these coordinates are never aliased, so that changing one causes
|
||||
-- action at a distance.
|
||||
screen_top1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at top of screen
|
||||
cursor1 = {line=1, pos=1, posB=nil}, -- position of cursor
|
||||
screen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen
|
||||
|
||||
-- cursor coordinates in pixels
|
||||
cursor_x = 0,
|
||||
cursor_y = 0,
|
||||
|
||||
font_height = font_height,
|
||||
line_height = line_height,
|
||||
em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width
|
||||
|
||||
top = top,
|
||||
left = left,
|
||||
right = right,
|
||||
width = right-left,
|
||||
|
||||
filename = love.filesystem.getUserDirectory()..'/lines.txt',
|
||||
next_save = nil,
|
||||
|
||||
-- undo
|
||||
history = {},
|
||||
next_history = 1,
|
||||
|
||||
-- search
|
||||
search_term = nil,
|
||||
search_text = nil,
|
||||
search_backup = nil, -- stuff to restore when cancelling search
|
||||
}
|
||||
return result
|
||||
end -- App.initialize_state
|
||||
|
||||
function edit.draw(State)
|
||||
State.button_handlers = {}
|
||||
App.color(Text_color)
|
||||
assert(#State.lines == #State.line_cache)
|
||||
if not Text.le1(State.screen_top1, State.cursor1) then
|
||||
print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
|
||||
assert(false)
|
||||
end
|
||||
State.cursor_x = nil
|
||||
State.cursor_y = nil
|
||||
local y = State.top
|
||||
--? print('== draw')
|
||||
for line_index = State.screen_top1.line,#State.lines do
|
||||
local line = State.lines[line_index]
|
||||
--? print('draw:', y, line_index, line)
|
||||
if y + State.line_height > App.screen.height then break end
|
||||
State.screen_bottom1 = {line=line_index, pos=nil, posB=nil}
|
||||
--? print('text.draw', y, line_index)
|
||||
local startpos, startposB = 1, nil
|
||||
if line_index == State.screen_top1.line then
|
||||
if State.screen_top1.pos then
|
||||
startpos = State.screen_top1.pos
|
||||
else
|
||||
startpos, startposB = nil, State.screen_top1.posB
|
||||
end
|
||||
end
|
||||
y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)
|
||||
y = y + State.line_height
|
||||
--? print('=> y', y)
|
||||
end
|
||||
if State.search_term then
|
||||
Text.draw_search_bar(State)
|
||||
end
|
||||
end
|
||||
|
||||
function edit.update(State, dt)
|
||||
if State.next_save and State.next_save < App.getTime() then
|
||||
save_to_disk(State)
|
||||
State.next_save = nil
|
||||
end
|
||||
end
|
||||
|
||||
function schedule_save(State)
|
||||
if State.next_save == nil then
|
||||
State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
|
||||
end
|
||||
end
|
||||
|
||||
function edit.quit(State)
|
||||
-- make sure to save before quitting
|
||||
if State.next_save then
|
||||
save_to_disk(State)
|
||||
end
|
||||
end
|
||||
|
||||
function edit.mouse_pressed(State, x,y, mouse_button)
|
||||
if State.search_term then return end
|
||||
--? print('press', State.selection1.line, State.selection1.pos)
|
||||
if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
|
||||
-- press on a button and it returned 'true' to short-circuit
|
||||
return
|
||||
end
|
||||
|
||||
for line_index,line in ipairs(State.lines) do
|
||||
if Text.in_line(State, line_index, x,y) then
|
||||
local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
|
||||
--? print(x,y, 'setting cursor:', line_index, pos, posB)
|
||||
State.cursor1 = {line=line_index, pos=pos, posB=posB}
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function edit.mouse_released(State, x,y, mouse_button)
|
||||
end
|
||||
|
||||
function edit.textinput(State, t)
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
if State.search_term then
|
||||
State.search_term = State.search_term..t
|
||||
State.search_text = nil
|
||||
Text.search_next(State)
|
||||
else
|
||||
Text.textinput(State, t)
|
||||
end
|
||||
schedule_save(State)
|
||||
end
|
||||
|
||||
function edit.keychord_pressed(State, chord, key)
|
||||
if State.search_term then
|
||||
if chord == 'escape' then
|
||||
State.search_term = nil
|
||||
State.search_text = nil
|
||||
State.cursor1 = State.search_backup.cursor
|
||||
State.screen_top1 = State.search_backup.screen_top
|
||||
State.search_backup = nil
|
||||
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
elseif chord == 'return' then
|
||||
State.search_term = nil
|
||||
State.search_text = nil
|
||||
State.search_backup = nil
|
||||
elseif chord == 'backspace' then
|
||||
local len = utf8.len(State.search_term)
|
||||
local byte_offset = Text.offset(State.search_term, len)
|
||||
State.search_term = string.sub(State.search_term, 1, byte_offset-1)
|
||||
State.search_text = nil
|
||||
elseif chord == 'down' then
|
||||
if State.cursor1.pos then
|
||||
State.cursor1.pos = State.cursor1.pos+1
|
||||
else
|
||||
State.cursor1.posB = State.cursor1.posB+1
|
||||
end
|
||||
Text.search_next(State)
|
||||
elseif chord == 'up' then
|
||||
Text.search_previous(State)
|
||||
end
|
||||
return
|
||||
elseif chord == 'C-f' then
|
||||
State.search_term = ''
|
||||
State.search_backup = {
|
||||
cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},
|
||||
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},
|
||||
}
|
||||
assert(State.search_text == nil)
|
||||
-- bifold text
|
||||
elseif chord == 'C-b' then
|
||||
State.expanded = not State.expanded
|
||||
Text.redraw_all(State)
|
||||
if not State.expanded then
|
||||
for _,line in ipairs(State.lines) do
|
||||
line.expanded = nil
|
||||
end
|
||||
edit.eradicate_locations_after_the_fold(State)
|
||||
end
|
||||
elseif chord == 'C-d' then
|
||||
if State.cursor1.posB == nil then
|
||||
local before = snapshot(State, State.cursor1.line)
|
||||
if State.lines[State.cursor1.line].dataB == nil then
|
||||
State.lines[State.cursor1.line].dataB = ''
|
||||
end
|
||||
State.lines[State.cursor1.line].expanded = true
|
||||
State.cursor1.pos = nil
|
||||
State.cursor1.posB = 1
|
||||
if Text.cursor_out_of_screen(State) then
|
||||
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
|
||||
end
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
|
||||
end
|
||||
-- zoom
|
||||
elseif chord == 'C-=' then
|
||||
edit.update_font_settings(State, State.font_height+2)
|
||||
Text.redraw_all(State)
|
||||
elseif chord == 'C--' then
|
||||
edit.update_font_settings(State, State.font_height-2)
|
||||
Text.redraw_all(State)
|
||||
elseif chord == 'C-0' then
|
||||
edit.update_font_settings(State, 20)
|
||||
Text.redraw_all(State)
|
||||
-- undo
|
||||
elseif chord == 'C-z' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local event = undo_event(State)
|
||||
if event then
|
||||
local src = event.before
|
||||
State.screen_top1 = deepcopy(src.screen_top)
|
||||
State.cursor1 = deepcopy(src.cursor)
|
||||
patch(State.lines, event.after, event.before)
|
||||
patch_placeholders(State.line_cache, event.after, event.before)
|
||||
-- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
Text.redraw_all(State)
|
||||
schedule_save(State)
|
||||
end
|
||||
elseif chord == 'C-y' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local event = redo_event(State)
|
||||
if event then
|
||||
local src = event.after
|
||||
State.screen_top1 = deepcopy(src.screen_top)
|
||||
State.cursor1 = deepcopy(src.cursor)
|
||||
patch(State.lines, event.before, event.after)
|
||||
-- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
Text.redraw_all(State)
|
||||
schedule_save(State)
|
||||
end
|
||||
-- clipboard
|
||||
elseif chord == 'C-c' then
|
||||
local s = Text.selection(State)
|
||||
if s then
|
||||
App.setClipboardText(s)
|
||||
end
|
||||
elseif chord == 'C-x' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local s = Text.cut_selection(State, State.left, State.right)
|
||||
if s then
|
||||
App.setClipboardText(s)
|
||||
end
|
||||
schedule_save(State)
|
||||
elseif chord == 'C-v' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
-- We don't have a good sense of when to scroll, so we'll be conservative
|
||||
-- and sometimes scroll when we didn't quite need to.
|
||||
local before_line = State.cursor1.line
|
||||
local before = snapshot(State, before_line)
|
||||
local clipboard_data = App.getClipboardText()
|
||||
for _,code in utf8.codes(clipboard_data) do
|
||||
local c = utf8.char(code)
|
||||
if c == '\n' then
|
||||
Text.insert_return(State)
|
||||
else
|
||||
Text.insert_at_cursor(State, c)
|
||||
end
|
||||
end
|
||||
if Text.cursor_out_of_screen(State) then
|
||||
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
|
||||
end
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
|
||||
-- dispatch to text
|
||||
else
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
Text.keychord_pressed(State, chord)
|
||||
end
|
||||
end
|
||||
|
||||
function edit.eradicate_locations_after_the_fold(State)
|
||||
-- eradicate side B from any locations we track
|
||||
if State.cursor1.posB then
|
||||
State.cursor1.posB = nil
|
||||
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
|
||||
State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
|
||||
end
|
||||
if State.screen_top1.posB then
|
||||
State.screen_top1.posB = nil
|
||||
State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
|
||||
State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
|
||||
end
|
||||
end
|
||||
|
||||
function edit.key_released(State, key, scancode)
|
||||
end
|
||||
|
||||
function edit.update_font_settings(State, font_height)
|
||||
State.font_height = font_height
|
||||
love.graphics.setFont(love.graphics.newFont(Editor_state.font_height))
|
||||
State.line_height = math.floor(font_height*1.3)
|
||||
State.em = App.newText(love.graphics.getFont(), 'm')
|
||||
Text_cache = {}
|
||||
end
|
||||
|
||||
--== some methods for tests
|
||||
|
||||
Test_margin_left = 25
|
||||
|
||||
function edit.initialize_test_state()
|
||||
-- if you change these values, tests will start failing
|
||||
return edit.initialize_state(
|
||||
15, -- top margin
|
||||
Test_margin_left,
|
||||
App.screen.width, -- right margin = 0
|
||||
14, -- font height assuming default LÖVE font
|
||||
15) -- line height
|
||||
end
|
||||
|
||||
-- all textinput events are also keypresses
|
||||
-- TODO: handle chords of multiple keys
|
||||
function edit.run_after_textinput(State, t)
|
||||
edit.keychord_pressed(State, t)
|
||||
edit.textinput(State, t)
|
||||
edit.key_released(State, t)
|
||||
App.screen.contents = {}
|
||||
edit.draw(State)
|
||||
end
|
||||
|
||||
-- not all keys are textinput
|
||||
function edit.run_after_keychord(State, chord)
|
||||
edit.keychord_pressed(State, chord)
|
||||
edit.key_released(State, chord)
|
||||
App.screen.contents = {}
|
||||
edit.draw(State)
|
||||
end
|
||||
|
||||
function edit.run_after_mouse_click(State, x,y, mouse_button)
|
||||
App.fake_mouse_press(x,y, mouse_button)
|
||||
edit.mouse_pressed(State, x,y, mouse_button)
|
||||
App.fake_mouse_release(x,y, mouse_button)
|
||||
edit.mouse_released(State, x,y, mouse_button)
|
||||
App.screen.contents = {}
|
||||
edit.draw(State)
|
||||
end
|
||||
|
||||
function edit.run_after_mouse_press(State, x,y, mouse_button)
|
||||
App.fake_mouse_press(x,y, mouse_button)
|
||||
edit.mouse_pressed(State, x,y, mouse_button)
|
||||
App.screen.contents = {}
|
||||
edit.draw(State)
|
||||
end
|
||||
|
||||
function edit.run_after_mouse_release(State, x,y, mouse_button)
|
||||
App.fake_mouse_release(x,y, mouse_button)
|
||||
edit.mouse_released(State, x,y, mouse_button)
|
||||
App.screen.contents = {}
|
||||
edit.draw(State)
|
||||
end
|
|
@ -0,0 +1,89 @@
|
|||
-- primitives for saving to file and loading from file
|
||||
|
||||
Fold = '\x1e' -- ASCII RS (record separator)
|
||||
|
||||
function file_exists(filename)
|
||||
local infile = App.open_for_reading(filename)
|
||||
if infile then
|
||||
infile:close()
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function load_from_disk(State)
|
||||
local infile = App.open_for_reading(State.filename)
|
||||
State.lines = load_from_file(infile)
|
||||
if infile then infile:close() end
|
||||
end
|
||||
|
||||
function load_from_file(infile)
|
||||
local result = {}
|
||||
if infile then
|
||||
local infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)
|
||||
while true do
|
||||
local line = infile_next_line()
|
||||
if line == nil then break end
|
||||
local line_info = {}
|
||||
if line:find(Fold) then
|
||||
_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
|
||||
else
|
||||
line_info.data = line
|
||||
end
|
||||
table.insert(result, line_info)
|
||||
end
|
||||
end
|
||||
if #result == 0 then
|
||||
table.insert(result, {data=''})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function save_to_disk(State)
|
||||
local outfile = App.open_for_writing(State.filename)
|
||||
if outfile == nil then
|
||||
error('failed to write to "'..State.filename..'"')
|
||||
end
|
||||
for _,line in ipairs(State.lines) do
|
||||
outfile:write(line.data)
|
||||
if line.dataB and #line.dataB > 0 then
|
||||
outfile:write(Fold)
|
||||
outfile:write(line.dataB)
|
||||
end
|
||||
outfile:write('\n')
|
||||
end
|
||||
outfile:close()
|
||||
end
|
||||
|
||||
function file_exists(filename)
|
||||
local infile = App.open_for_reading(filename)
|
||||
if infile then
|
||||
infile:close()
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- for tests
|
||||
function load_array(a)
|
||||
local result = {}
|
||||
local next_line = ipairs(a)
|
||||
local i,line,drawing = 0, ''
|
||||
while true do
|
||||
i,line = next_line(a, i)
|
||||
if i == nil then break end
|
||||
local line_info = {}
|
||||
if line:find(Fold) then
|
||||
_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
|
||||
else
|
||||
line_info.data = line
|
||||
end
|
||||
table.insert(result, line_info)
|
||||
end
|
||||
if #result == 0 then
|
||||
table.insert(result, {data=''})
|
||||
end
|
||||
return result
|
||||
end
|
|
@ -0,0 +1,77 @@
|
|||
function test_resize_window()
|
||||
io.write('\ntest_resize_window')
|
||||
App.screen.init{width=300, height=300}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.filename = 'foo'
|
||||
Log_browser_state = edit.initialize_test_state()
|
||||
check_eq(App.screen.width, 300, 'F - test_resize_window/baseline/width')
|
||||
check_eq(App.screen.height, 300, 'F - test_resize_window/baseline/height')
|
||||
check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/baseline/left_margin')
|
||||
App.resize(200, 400)
|
||||
check_eq(App.screen.width, 200, 'F - test_resize_window/width')
|
||||
check_eq(App.screen.height, 400, 'F - test_resize_window/height')
|
||||
check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/left_margin')
|
||||
-- ugly; right margin switches from 0 after resize
|
||||
check_eq(Editor_state.right, 200-Margin_right, 'F - test_resize_window/right_margin')
|
||||
check_eq(Editor_state.width, 200-Test_margin_left-Margin_right, 'F - test_resize_window/drawing_width')
|
||||
-- TODO: how to make assertions about when App.update got past the early exit?
|
||||
end
|
||||
|
||||
function test_drop_file()
|
||||
io.write('\ntest_drop_file')
|
||||
App.screen.init{width=Editor_state.left+300, height=300}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
App.filesystem['foo'] = 'abc\ndef\nghi\n'
|
||||
local fake_dropped_file = {
|
||||
opened = false,
|
||||
getFilename = function(self)
|
||||
return 'foo'
|
||||
end,
|
||||
open = function(self)
|
||||
self.opened = true
|
||||
end,
|
||||
lines = function(self)
|
||||
assert(self.opened)
|
||||
return App.filesystem['foo']:gmatch('[^\n]+')
|
||||
end,
|
||||
close = function(self)
|
||||
self.opened = false
|
||||
end,
|
||||
}
|
||||
App.filedropped(fake_dropped_file)
|
||||
check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines')
|
||||
check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1')
|
||||
check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2')
|
||||
check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')
|
||||
edit.draw(Editor_state)
|
||||
end
|
||||
|
||||
function test_drop_file_saves_previous()
|
||||
io.write('\ntest_drop_file_saves_previous')
|
||||
App.screen.init{width=Editor_state.left+300, height=300}
|
||||
-- initially editing a file called foo that hasn't been saved to filesystem yet
|
||||
Editor_state.lines = load_array{'abc', 'def'}
|
||||
Editor_state.filename = 'foo'
|
||||
schedule_save(Editor_state)
|
||||
-- now drag a new file bar from the filesystem
|
||||
App.filesystem['bar'] = 'abc\ndef\nghi\n'
|
||||
local fake_dropped_file = {
|
||||
opened = false,
|
||||
getFilename = function(self)
|
||||
return 'bar'
|
||||
end,
|
||||
open = function(self)
|
||||
self.opened = true
|
||||
end,
|
||||
lines = function(self)
|
||||
assert(self.opened)
|
||||
return App.filesystem['bar']:gmatch('[^\n]+')
|
||||
end,
|
||||
close = function(self)
|
||||
self.opened = false
|
||||
end,
|
||||
}
|
||||
App.filedropped(fake_dropped_file)
|
||||
-- filesystem now contains a file called foo
|
||||
check_eq(App.filesystem['foo'], 'abc\ndef\n', 'F - test_drop_file_saves_previous')
|
||||
end
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,110 @@
|
|||
-- undo/redo by managing the sequence of events in the current session
|
||||
-- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu
|
||||
|
||||
-- Incredibly inefficient; we make a copy of lines on every single keystroke.
|
||||
-- The hope here is that we're either editing small files or just reading large files.
|
||||
-- TODO: highlight stuff inserted by any undo/redo operation
|
||||
-- TODO: coalesce multiple similar operations
|
||||
|
||||
function record_undo_event(State, data)
|
||||
State.history[State.next_history] = data
|
||||
State.next_history = State.next_history+1
|
||||
for i=State.next_history,#State.history do
|
||||
State.history[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function undo_event(State)
|
||||
if State.next_history > 1 then
|
||||
--? print('moving to history', State.next_history-1)
|
||||
State.next_history = State.next_history-1
|
||||
local result = State.history[State.next_history]
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function redo_event(State)
|
||||
if State.next_history <= #State.history then
|
||||
--? print('restoring history', State.next_history+1)
|
||||
local result = State.history[State.next_history]
|
||||
State.next_history = State.next_history+1
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Copy all relevant global state.
|
||||
-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
|
||||
function snapshot(State, s,e)
|
||||
-- Snapshot everything by default, but subset if requested.
|
||||
assert(s)
|
||||
if e == nil then
|
||||
e = s
|
||||
end
|
||||
assert(#State.lines > 0)
|
||||
if s < 1 then s = 1 end
|
||||
if s > #State.lines then s = #State.lines end
|
||||
if e < 1 then e = 1 end
|
||||
if e > #State.lines then e = #State.lines end
|
||||
-- compare with App.initialize_globals
|
||||
local event = {
|
||||
screen_top=deepcopy(State.screen_top1),
|
||||
selection=deepcopy(State.selection1),
|
||||
cursor=deepcopy(State.cursor1),
|
||||
lines={},
|
||||
start_line=s,
|
||||
end_line=e,
|
||||
-- no filename; undo history is cleared when filename changes
|
||||
}
|
||||
-- deep copy lines without cached stuff like text fragments
|
||||
for i=s,e do
|
||||
local line = State.lines[i]
|
||||
table.insert(event.lines, {data=line.data, dataB=line.dataB})
|
||||
end
|
||||
return event
|
||||
end
|
||||
|
||||
function patch(lines, from, to)
|
||||
--? if #from.lines == 1 and #to.lines == 1 then
|
||||
--? assert(from.start_line == from.end_line)
|
||||
--? assert(to.start_line == to.end_line)
|
||||
--? assert(from.start_line == to.start_line)
|
||||
--? lines[from.start_line] = to.lines[1]
|
||||
--? return
|
||||
--? end
|
||||
assert(from.start_line == to.start_line)
|
||||
for i=from.end_line,from.start_line,-1 do
|
||||
table.remove(lines, i)
|
||||
end
|
||||
assert(#to.lines == to.end_line-to.start_line+1)
|
||||
for i=1,#to.lines do
|
||||
table.insert(lines, to.start_line+i-1, to.lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
function patch_placeholders(line_cache, from, to)
|
||||
assert(from.start_line == to.start_line)
|
||||
for i=from.end_line,from.start_line,-1 do
|
||||
table.remove(line_cache, i)
|
||||
end
|
||||
assert(#to.lines == to.end_line-to.start_line+1)
|
||||
for i=1,#to.lines do
|
||||
table.insert(line_cache, to.start_line+i-1, {})
|
||||
end
|
||||
end
|
||||
|
||||
-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
|
||||
function deepcopy(obj, seen)
|
||||
if type(obj) ~= 'table' then return obj end
|
||||
if seen and seen[obj] then return seen[obj] end
|
||||
local s = seen or {}
|
||||
local result = setmetatable({}, getmetatable(obj))
|
||||
s[obj] = result
|
||||
for k,v in pairs(obj) do
|
||||
result[deepcopy(k, s)] = deepcopy(v, s)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function minmax(a, b)
|
||||
return math.min(a,b), math.max(a,b)
|
||||
end
|
5
text.lua
5
text.lua
|
@ -1,11 +1,6 @@
|
|||
-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
|
||||
Text = {}
|
||||
|
||||
require 'search'
|
||||
require 'select'
|
||||
require 'undo'
|
||||
require 'text_tests'
|
||||
|
||||
-- draw a line starting from startpos to screen at y between State.left and State.right
|
||||
-- return the final y, and position of start of final screen line drawn
|
||||
function Text.draw(State, line_index, y, startpos)
|
||||
|
|
Loading…
Reference in New Issue