always show line numbers in source editor
[lines.love.git] / source.lua
blob4a8bb48aeb97d712b8b1e068690e228e1bbe0e37
1 source = {}
3 Editor_state = {}
4 Line_number_width = 3 -- in ems
6 -- called both in tests and real run
7 function source.initialize_globals()
8 -- tests currently mostly clear their own state
10 Show_log_browser_side = false
11 Focus = 'edit'
12 Show_file_navigator = false
13 File_navigation = {
14 all_candidates = {
15 'run',
16 'run_tests',
17 'log',
18 'edit',
19 'drawing',
20 'help',
21 'text',
22 'search',
23 'select',
24 'undo',
25 'text_tests',
26 'geom',
27 'drawing_tests',
28 'file',
29 'source',
30 'source_tests',
31 'commands',
32 'log_browser',
33 'source_edit',
34 'source_text',
35 'source_undo',
36 'colorize',
37 'source_text_tests',
38 'source_file',
39 'main',
40 'button',
41 'keychord',
42 'app',
43 'test',
44 'json',
46 index = 1,
47 filter = '',
48 cursors = {}, -- filename to cursor1, screen_top1
50 File_navigation.candidates = File_navigation.all_candidates -- modified with filter
52 Menu_status_bar_height = 5 + --[[line height in tests]] 15 + 5
54 -- blinking cursor
55 Cursor_time = 0
56 end
58 -- called only for real run
59 function source.initialize()
60 log_new('source')
61 if Settings and Settings.source then
62 source.load_settings()
63 else
64 source.initialize_default_settings()
65 end
67 source.initialize_edit_side()
68 source.initialize_log_browser_side()
70 Menu_status_bar_height = 5 + Editor_state.line_height + 5
71 Editor_state.top = Editor_state.top + Menu_status_bar_height
72 Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height
76 -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
77 love.window.setTitle('lines.love - source')
81 end
83 -- environment for a mutable file
84 -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
85 function source.initialize_edit_side()
86 load_from_disk(Editor_state)
87 Text.redraw_all(Editor_state)
88 if File_navigation.cursors[Editor_state.filename] then
89 Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
90 Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
91 else
92 Editor_state.screen_top1 = {line=1, pos=1}
93 Editor_state.cursor1 = {line=1, pos=1}
94 end
95 edit.check_locs(Editor_state)
97 if Editor_state.cursor1.line > #Editor_state.lines then
98 Editor_state.cursor1 = {line=1, pos=1}
99 end
100 if Editor_state.screen_top1.line > #Editor_state.lines then
101 Editor_state.screen_top1 = {line=1, pos=1}
104 if rawget(_G, 'jit') then
105 jit.off()
106 jit.flush()
110 function print_and_log(s)
111 print(s)
112 log(3, s)
115 function source.load_settings()
116 local settings = Settings.source
117 love.graphics.setFont(love.graphics.newFont(settings.font_height))
118 -- set up desired window dimensions and make window resizable
119 _, _, App.screen.flags = App.screen.size()
120 App.screen.flags.resizable = true
121 App.screen.width, App.screen.height = settings.width, settings.height
122 App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
123 source.set_window_position_from_settings(settings)
124 Show_log_browser_side = settings.show_log_browser_side
125 local right = App.screen.width - Margin_right
126 if Show_log_browser_side then
127 right = App.screen.width/2 - Margin_right
129 Editor_state = edit.initialize_state(Margin_top, Margin_left + Line_number_width*App.width('m'), right, settings.font_height, math.floor(settings.font_height*1.3))
130 Editor_state.filename = settings.filename
131 Editor_state.filename = basename(Editor_state.filename) -- migrate settings that used full paths; we now support only relative paths within the app
132 if settings.cursors then
133 File_navigation.cursors = settings.cursors
134 Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
135 Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
136 else
137 -- migrate old settings
138 Editor_state.screen_top1 = {line=1, pos=1}
139 Editor_state.cursor1 = {line=1, pos=1}
143 function source.set_window_position_from_settings(settings)
144 local os = love.system.getOS()
145 if os == 'Linux' then
146 -- love.window.setPosition doesn't quite seem to do what is asked of it on Linux.
147 App.screen.move(settings.x, settings.y-37, settings.displayindex)
148 else
149 App.screen.move(settings.x, settings.y, settings.displayindex)
153 function source.initialize_default_settings()
154 local font_height = 20
155 love.graphics.setFont(love.graphics.newFont(font_height))
156 source.initialize_window_geometry()
157 Editor_state = edit.initialize_state(Margin_top, Margin_left + Line_number_width*App.width('m'), App.screen.width-Margin_right)
158 Editor_state.filename = 'run.lua'
159 Editor_state.font_height = font_height
160 Editor_state.line_height = math.floor(font_height*1.3)
163 function source.initialize_window_geometry()
164 -- Initialize window width/height and make window resizable.
166 -- I get tempted to have opinions about window dimensions here, but they're
167 -- non-portable:
168 -- - maximizing doesn't work on mobile and messes things up
169 -- - maximizing keeps the title bar on screen in Linux, but off screen on
170 -- Windows. And there's no way to get the height of the title bar.
171 -- It seems more robust to just follow LÖVE's default window size until
172 -- someone overrides it.
173 App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
174 App.screen.flags.resizable = true
175 App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
178 function source.resize(w, h)
179 --? print(("Window resized to width: %d and height: %d."):format(w, h))
180 App.screen.width, App.screen.height = w, h
181 Text.redraw_all(Editor_state)
182 Editor_state.selection1 = {} -- no support for shift drag while we're resizing
183 if Show_log_browser_side then
184 Editor_state.right = App.screen.width/2 - Margin_right
185 else
186 Editor_state.right = App.screen.width-Margin_right
188 Log_browser_state.left = App.screen.width/2 + Margin_right
189 Log_browser_state.right = App.screen.width-Margin_right
190 Editor_state.width = Editor_state.right-Editor_state.left
191 Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
192 --? print('end resize')
195 function source.file_drop(file)
196 -- first make sure to save edits on any existing file
197 if Editor_state.next_save then
198 save_to_disk(Editor_state)
200 -- clear the slate for the new file
201 Editor_state.filename = file:getFilename()
202 file:open('r')
203 Editor_state.lines = load_from_file(file)
204 file:close()
205 Text.redraw_all(Editor_state)
206 Editor_state.screen_top1 = {line=1, pos=1}
207 Editor_state.cursor1 = {line=1, pos=1}
211 -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
212 love.window.setTitle('lines.love - source')
218 -- a copy of source.file_drop when given a filename
219 function source.switch_to_file(filename)
220 -- first make sure to save edits on any existing file
221 if Editor_state.next_save then
222 save_to_disk(Editor_state)
224 -- save cursor position
225 File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
226 -- clear the slate for the new file
227 Editor_state.filename = filename
228 load_from_disk(Editor_state)
229 Text.redraw_all(Editor_state)
230 if File_navigation.cursors[filename] then
231 Editor_state.screen_top1 = File_navigation.cursors[filename].screen_top1
232 Editor_state.cursor1 = File_navigation.cursors[filename].cursor1
233 else
234 Editor_state.screen_top1 = {line=1, pos=1}
235 Editor_state.cursor1 = {line=1, pos=1}
239 function source.draw()
240 edit.draw(Editor_state, --[[hide cursor?]] Show_file_navigator)
241 if Show_log_browser_side then
242 -- divider
243 App.color(Divider_color)
244 love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)
246 log_browser.draw(Log_browser_state, --[[hide_cursor]] Focus ~= 'log_browser')
248 source.draw_menu_bar()
249 if Error_message then
250 local height = math.min(20*Editor_state.line_height, App.screen.height*0.2)
251 App.color{r=0.8,g=0,b=0}
252 love.graphics.rectangle('fill', 150, App.screen.height - height-10, App.screen.width, height+10)
253 App.color{r=0,g=0,b=0}
254 love.graphics.print(Error_message, 150+10, App.screen.height - height)
258 function source.update(dt)
259 Cursor_time = Cursor_time + dt
260 if App.mouse_x() < Editor_state.right then
261 edit.update(Editor_state, dt)
262 elseif Show_log_browser_side then
263 log_browser.update(Log_browser_state, dt)
267 function source.quit()
268 edit.quit(Editor_state)
269 log_browser.quit(Log_browser_state)
272 function source.settings()
273 if Settings == nil then Settings = {} end
274 if Settings.source == nil then Settings.source = {} end
275 Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()
276 File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
277 return {
278 x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,
279 width=App.screen.width, height=App.screen.height,
280 font_height=Editor_state.font_height,
281 filename=Editor_state.filename,
282 cursors=File_navigation.cursors,
283 show_log_browser_side=Show_log_browser_side,
284 focus=Focus,
288 function source.mouse_press(x,y, mouse_button)
289 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
290 --? print('mouse click', x, y)
291 --? print(Editor_state.left, Editor_state.right)
292 --? print(Log_browser_state.left, Log_browser_state.right)
293 if Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then
294 -- send click to buttons
295 edit.mouse_press(Editor_state, x,y, mouse_button)
296 return
298 if x < Editor_state.right + Margin_right then
299 --? print('click on edit side')
300 if Focus ~= 'edit' then
301 Focus = 'edit'
302 return
304 edit.mouse_press(Editor_state, x,y, mouse_button)
305 elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then
306 --? print('click on log_browser side')
307 if Focus ~= 'log_browser' then
308 Focus = 'log_browser'
309 return
311 log_browser.mouse_press(Log_browser_state, x,y, mouse_button)
312 for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scroll
316 function source.mouse_release(x,y, mouse_button)
317 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
318 if Focus == 'edit' then
319 return edit.mouse_release(Editor_state, x,y, mouse_button)
320 else
321 return log_browser.mouse_release(Log_browser_state, x,y, mouse_button)
325 function source.mouse_wheel_move(dx,dy)
326 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
327 if Focus == 'edit' then
328 return edit.mouse_wheel_move(Editor_state, dx,dy)
329 else
330 return log_browser.mouse_wheel_move(Log_browser_state, dx,dy)
334 function source.text_input(t)
335 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
336 if Show_file_navigator then
337 text_input_on_file_navigator(t)
338 return
340 if Focus == 'edit' then
341 return edit.text_input(Editor_state, t)
342 else
343 return log_browser.text_input(Log_browser_state, t)
347 function source.keychord_press(chord, key)
348 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
349 --? print('source keychord')
350 if Show_file_navigator then
351 keychord_press_on_file_navigator(chord, key)
352 return
354 if chord == 'C-l' then
355 --? print('C-l')
356 Show_log_browser_side = not Show_log_browser_side
357 if Show_log_browser_side then
358 Editor_state.right = App.screen.width/2 - Margin_right
359 Editor_state.width = Editor_state.right-Editor_state.left
360 Text.redraw_all(Editor_state)
361 Log_browser_state.left = App.screen.width/2 + Margin_left
362 Log_browser_state.right = App.screen.width - Margin_right
363 else
364 Editor_state.right = App.screen.width - Margin_right
365 Editor_state.width = Editor_state.right-Editor_state.left
366 Text.redraw_all(Editor_state)
368 return
370 if chord == 'C-k' then
371 -- clear logs
372 love.filesystem.remove('log')
373 -- restart to reload state of logs on screen
374 Settings.source = source.settings()
375 source.quit()
376 love.filesystem.write('config', json.encode(Settings))
377 load_file_from_source_or_save_directory('main.lua')
378 App.undo_initialize()
379 App.run_tests_and_initialize()
380 return
382 if chord == 'C-g' then
383 Show_file_navigator = true
384 return
386 if Focus == 'edit' then
387 return edit.keychord_press(Editor_state, chord, key)
388 else
389 return log_browser.keychord_press(Log_browser_state, chord, key)
393 function source.key_release(key, scancode)
394 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
395 if Focus == 'edit' then
396 return edit.key_release(Editor_state, key, scancode)
397 else
398 return log_browser.keychord_press(Log_browser_state, chordkey, scancode)