View difference between Paste ID: 6R9jAK9r and D7hU5pCf
SHOW: | | - or go back to the newest paste.
1
if not fs.exists('lex') then
2
	shell.run('pastebin get edyuQ5xY lex')
3
end
4
5
local lexSuccess = os.loadAPI('lex')
6
7
if not lexSuccess or not lex or not lex.lex then
8
	print('A valid lexer is required (file `lex` seems to be broken or not a lexer)')
9
	return
10
end
11
12
-- Get file to edit
13
local tArgs = { ... }
14
if #tArgs == 0 then
15
	print( "Usage: shedit <path>" )
16
	return
17
end
18
19
-- Error checking
20
local sPath = shell.resolve( tArgs[1] )
21
local bReadOnly = fs.isReadOnly( sPath )
22
if fs.exists( sPath ) and fs.isDir( sPath ) then
23
	print( "Cannot edit a directory." )
24
	return
25
end
26
27
local x,y = 1,1
28
local markX, markY = 1, 1
29
local w,h = term.getSize()
30
local scrollX, scrollY = 0,0
31
32
local tLines = {}
33
local bRunning = true
34
35
-- Colors
36
local highlightColor, keywordColor, commentColor, textColor, bgColor, stringColor
37
if term.isColor() then
38
	bgColor = colors.black
39
	textColor = colors.white
40
	highlightColor = colors.yellow
41
	keywordColor = colors.yellow
42
	commentColor = colors.green
43
	stringColor = colors.red
44
else
45
	bgColor = colors.black
46
	textColor = colors.white
47
	highlightColor = colors.white
48
	keywordColor = colors.white
49
	commentColor = colors.white
50
	stringColor = colors.white
51
end
52
53
-- Menus
54
local bMenu = false
55
local nMenuItem = 1
56
local tMenuItems = {}
57
if not bReadOnly then
58
	table.insert( tMenuItems, "Save" )
59
end
60
if shell.openTab then
61
	table.insert( tMenuItems, "Run" )
62
end
63
if peripheral.find( "printer" ) then
64
	table.insert( tMenuItems, "Print" )
65
end
66
table.insert( tMenuItems, "Exit" )
67
table.insert(tMenuItems, 'Tools')
68
table.insert(tMenuItems, 'Find')
69
table.insert(tMenuItems, 'Jump')
70
table.insert(tMenuItems, 'Copy')
71
table.insert(tMenuItems, 'VPaste')
72
73
local sStatus = "Press Ctrl to access menu"
74
if string.len( sStatus ) > w - 5 then
75
	sStatus = "Press Ctrl for menu"
76
end
77
78
local function load( _sPath )
79
	tLines = {}
80
	if fs.exists( _sPath ) then
81
		local file = io.open( _sPath, "r" )
82
		local sLine = file:read()
83
		while sLine do
84
			table.insert( tLines, sLine )
85
			sLine = file:read()
86
		end
87
		file:close()
88
	end
89
90
	if #tLines == 0 then
91
		table.insert( tLines, "" )
92
	end
93
end
94
95
local function save( _sPath )
96
	-- Create intervening folder
97
	local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len() )
98
	if not fs.exists( sDir ) then
99
		fs.makeDir( sDir )
100
	end
101
102
	-- Save
103
	local file = nil
104
	local function innerSave()
105
		file = fs.open( _sPath, "w" )
106
		if file then
107
			for n, sLine in ipairs( tLines ) do
108
				file.write( sLine .. "\n" )
109
			end
110
		else
111
			error( "Failed to open ".._sPath )
112
		end
113
	end
114
115
	local ok, err = pcall( innerSave )
116
	if file then
117
		file.close()
118
	end
119
	return ok, err
120
end
121
122
local tKeywords = {
123
	["and"] = true,
124
	["break"] = true,
125
	["do"] = true,
126
	["else"] = true,
127
	["elseif"] = true,
128
	["end"] = true,
129
	["false"] = true,
130
	["for"] = true,
131
	["function"] = true,
132
	["if"] = true,
133
	["in"] = true,
134
	["local"] = true,
135
	["nil"] = true,
136
	["not"] = true,
137
	["or"] = true,
138
	["repeat"] = true,
139
	["return"] = true,
140
	["then"] = true,
141
	["true"] = true,
142
	["until"]= true,
143
	["while"] = true,
144
}
145
146
local function tryWrite( sLine, regex, color )
147
	local match = string.match( sLine, regex )
148
	if match then
149
		if type(color) == "number" then
150
			term.setTextColor( color )
151
		else
152
			term.setTextColor( color(match) )
153
		end
154
		term.write( match )
155
		term.setTextColor( textColor )
156
		return string.sub( sLine, string.len(match) + 1 )
157
	end
158
	return nil
159
end
160
161
--[[local function writeHighlighted( sLine )
162
	while string.len(sLine) > 0 do
163
		sLine =
164
			tryWrite( sLine, "^%-%-%[%[.-%]%]", commentColor ) or
165
			tryWrite( sLine, "^%-%-.*", commentColor ) or
166
			tryWrite( sLine, "^\"\"", stringColor ) or
167
			tryWrite( sLine, "^\".-[^\\]\"", stringColor ) or
168
			tryWrite( sLine, "^\'\'", stringColor ) or
169
			tryWrite( sLine, "^\'.-[^\\]\'", stringColor ) or
170
			tryWrite( sLine, "^%[%[.-%]%]", stringColor ) or
171
			tryWrite( sLine, "^[%w_]+", function( match )
172
				if tKeywords[ match ] then
173
					return keywordColor
174
				end
175
				return textColor
176
			end ) or
177
			tryWrite( sLine, "^[^%w_]", textColor )
178
	end
179
end]]
180
181
local apis = {
182
	-- tables
183
	bit = true,
184
	bit32 = true,
185
	bitop = true,
186
	colors = true,
187
	colours = true,
188
	coroutine = true,
189
	disk = true,
190
	fs = true,
191
	gps = true,
192
	help = true,
193
	http = true,
194
	io = true,
195
	keys = true,
196
	math = true,
197
	os = true,
198
	paintutils = true,
199
	parallel = true,
200
	peripheral = true,
201
	rednet = true,
202
	redstone = true,
203
	rs = true,
204
	settings = true,
205
	shell = true,
206
	socket = true,
207
	string = true,
208
	table = true,
209
	term = true,
210
	textutils = true,
211
	vector = true,
212
	window = true,
213
214
	-- functions
215
	assert = true,
216
	collectgarbage = true,
217
	dofile = true,
218
	error = true,
219
	getfenv = true,
220
	getmetatable = true,
221
	ipairs = true,
222
	loadfile = true,
223
	loadstring = true,
224
	module = true,
225
	next = true,
226
	pairs = true,
227
	pcall = true,
228
	print = true,
229
	rawequal = true,
230
	rawget = true,
231
	rawset = true,
232
	require = true,
233
	select = true,
234
	setfenv = true,
235
	setmetatable = true,
236
	tonumber = true,
237
	tostring = true,
238
	type = true,
239
	unpack = true,
240
	xpcall =  true,
241
	printError = true,
242
	write = true
243
}
244
245
local function onUpdate()
246
	tokenized = lex.lex(table.concat(tLines, '\n'))
247
end
248
249
local function markExists()
250
	if markX ~= x or markY ~= y then
251
		return true
252
	else
253
		return false
254
	end
255
end
256
257
local function isMarked(newX, newY)
258
	if (newY > markY and newY < y) or (newY > y and newY < markY) then
259
		return true
260
	end
261
262
	if newY == markY and newY == y then
263
		if markX > x and newX >= x and newX < markX then
264
			return true
265
		elseif markX < x and newX >= markX and newX < x then
266
			return true
267
		end
268
	elseif newY == markY then
269
		if newX < markX and y < markY then
270
			return true
271
		elseif newX >= markX and y > markY then
272
			return true
273
		end
274
	elseif newY == y then
275
		if newX < x and y > markY then
276
			return true
277
		elseif newX >= x and y < markY then
278
			return true
279
		end
280
	end
281
282
	return false
283
end
284
285
local function getMarks()
286
	local msx, msy
287
	local mex, mey
288
289
	if markY == y then
290
		if markX > x then
291
			msx = x
292
			msy = y
293
			mex = markX
294
			mey = markY
295
		else
296
			msx = markX
297
			msy = markY
298
			mex = x
299
			mey = y
300
		end
301
	else
302
		if markY > y then
303
			msx = x
304
			msy = y
305
			mex = markX
306
			mey = markY
307
		else
308
			msx = markX
309
			msy = markY
310
			mex = x
311
			mey = y
312
		end
313
	end
314
315
	return msx, msy, mex, mey
316
end
317
318
local function getCharDisplay(char)
319
	if char == '\t' then
320
		return '    '
321
	else
322
		return char
323
	end
324
end
325
326
local function getCharWidth(char)
327
	return getCharDisplay(char):len()
328
end
329
330
local function writeHighlighted(lineNr)
331
	local tokens = tokenized[lineNr] or {}
332
	local textColor = term.getTextColor()
333
	local bgColor = term.getBackgroundColor()
334
	local setX, setY = term.getCursorPos()
335
	local msx, msy, mex, mey = getMarks()
336
337
	if lineNr < mey and lineNr >= msy then
338
		term.setBackgroundColor(colors.white)
339
	end
340
341
	term.clearLine()
342
	term.setCursorPos(setX, setY)
343
344
	for t = 1, #tokens do
345
		local token = tokens[t]
346
		local color = colors.white
347
348
		if token.type == 'keyword' or token.type == 'operator' then
349
			color = colors.yellow
350
		elseif token.type == 'number' or token.type == 'value' then
351
			color = colors.purple
352
		elseif token.type == 'comment' then
353
			color = colors.green
354
		elseif token.type == 'ident' and apis[token.data] then
355
			color = colors.lightGray
356
		elseif token.type == 'string' then
357
			color = colors.cyan
358
		elseif token.type == 'escape' then
359
			color = colors.purple
360
		elseif token.type == 'unidentified' then
361
			color = colors.red
362
		end
363
364
		for p = 1, #token.data do
365
			local sub = getCharDisplay(token.data:sub(p, p))
366
367
			local cX, cY = p + token.posFirst - 1, lineNr
368
369
			if (cY == msy and cY == mey and cX >= msx and cX < mex) or (cY == msy and cY ~= mey and cX >= msx) or (cY == mey and cY ~= msy and cX < mex) or (cY > msy and cY < mey) then
370
				term.setTextColor(colors.black)
371
				term.setBackgroundColor(color)
372
			else
373
				term.setBackgroundColor(colors.black)
374
				term.setTextColor(color)
375
			end
376
377
			term.write(sub)
378
		end
379
	end
380
381
	term.setTextColor(textColor)
382
	term.setBackgroundColor(bgColor)
383
end
384
385
local tCompletions
386
local nCompletion
387
388
local tCompleteEnv = _ENV
389
local function complete( sLine )
390
	if settings.get( "edit.autocomplete" ) then
391
		local nStartPos = string.find( sLine, "[a-zA-Z0-9_%.]+$" )
392
		if nStartPos then
393
			sLine = string.sub( sLine, nStartPos )
394
		end
395
		if #sLine > 0 then
396
			return textutils.complete( sLine, tCompleteEnv )
397
		end
398
	end
399
	return nil
400
end
401
402
local function recomplete()
403
	local sLine = tLines[y]
404
	if not bMenu and not bReadOnly and x == string.len(sLine) + 1 then
405
		tCompletions = complete( sLine )
406
		if tCompletions and #tCompletions > 0 then
407
			nCompletion = 1
408
		else
409
			nCompletion = nil
410
		end
411
	else
412
		tCompletions = nil
413
		nCompletion = nil
414
	end
415
end
416
417
local function writeCompletion( sLine )
418
	if nCompletion then
419
		local sCompletion = tCompletions[ nCompletion ]
420
		term.setTextColor( colors.white )
421
		term.setBackgroundColor( colors.gray )
422
		term.write( sCompletion )
423
		term.setTextColor( textColor )
424
		term.setBackgroundColor( bgColor )
425
	end
426
end
427
428
local function getCursorX(lineNr)
429
	local line = tLines[lineNr]
430
	local cx = 1
431
432
	for i = 1, x - 1 do
433
		cx = cx + getCharWidth(line:sub(i, i))
434
	end
435
436
	return cx
437
end
438
439
local function cxToPos(lineNr, cx)
440
	local line = tLines[lineNr]
441
	local num = 1
442
	local pos = 1
443
444
	for i = 1, #line do
445
		amt = getCharWidth(line:sub(i, i))
446
447
		if num + amt > cx then
448
			return pos
449
		else
450
			num = num + amt
451
			pos = pos + 1
452
		end
453
	end
454
455
	return pos
456
end
457
458
local function redrawText()
459
	local cursorX, cursorY = x, y
460
	for y=1,h-1 do
461
		term.setCursorPos( 1 - scrollX, y )
462
		term.clearLine()
463
464
		local sLine = tLines[ y + scrollY ]
465
		if sLine ~= nil then
466
			--writeHighlighted( sLine )
467
			writeHighlighted(y + scrollY)
468
			if cursorY == y and cursorX == #sLine + 1 then
469
				writeCompletion()
470
			end
471
		end
472
	end
473
474
	local cx = getCursorX(y)
475
476
	term.setCursorPos( cx - scrollX, y - scrollY )
477
end
478
479
--[[local function redrawLine(_nY)
480
	local sLine = tLines[_nY]
481
	if sLine then
482
		term.setCursorPos( 1 - scrollX, _nY - scrollY )
483
		term.clearLine()
484
		--writeHighlighted( sLine )
485
		writeHighlighted(_nY)
486
		if _nY == y and x == #sLine + 1 then
487
			writeCompletion()
488
		end
489
		term.setCursorPos( x - scrollX, _nY - scrollY )
490
	end
491
end]]
492
493
local redrawLine = redrawText
494
495
local function redrawMenu()
496
	-- Clear line
497
	term.setCursorPos( 1, h )
498
	term.clearLine()
499
500
	-- Draw line numbers
501
	--[[term.setCursorPos( w - string.len( "Ln "..y ) + 1, h )
502
	term.setTextColor( highlightColor )
503
	term.write( "Ln " )
504
	term.setTextColor( textColor )
505
	term.write( y )]]
506
507
	if not bMenu then
508
		-- Draw status
509
		term.setTextColor( highlightColor )
510
		term.write( sStatus )
511
		term.setTextColor( textColor )
512
513
		term.setCursorPos(w - ('[' .. x .. ':' .. y .. ']:[' .. markX .. ':' .. markY .. ']'):len() + 1, h)
514
		term.setTextColor(highlightColor)
515
		term.write('[')
516
		term.setTextColor(textColor)
517
		term.write(x)
518
		term.setTextColor(highlightColor)
519
		term.write(':')
520
		term.setTextColor(textColor)
521
		term.write(y)
522
		term.setTextColor(highlightColor)
523
		term.write(']:[')
524
		term.setTextColor(textColor)
525
		term.write(markX)
526
		term.setTextColor(highlightColor)
527
		term.write(':')
528
		term.setTextColor(textColor)
529
		term.write(markY)
530
		term.setTextColor(highlightColor)
531
		term.write(']')
532
		term.setTextColor(textColor)
533
	else
534
		-- Draw menu
535
		term.setTextColor( textColor )
536
		for nItem,sItem in pairs( tMenuItems ) do
537
			if nItem == nMenuItem then
538
				term.setTextColor( highlightColor )
539
				term.write( "[" )
540
				term.setTextColor( textColor )
541
				term.write( sItem )
542
				term.setTextColor( highlightColor )
543
				term.write( "]" )
544
				term.setTextColor( textColor )
545
			else
546
				term.write( " "..sItem.." " )
547
			end
548
		end
549
	end
550
551
	-- Reset cursor
552
	term.setCursorPos( getCursorX(y) - scrollX, y - scrollY )
553
end
554
555
local function setCursor( newX, newY )
556
	local oldX, oldY = x, y
557
	x, y = newX, newY
558
	local screenX = getCursorX(y) - scrollX
559
	local screenY = y - scrollY
560
561
	local bRedraw = false
562
	if screenX < 1 then
563
		scrollX = getCursorX(y) - 1
564
		screenX = 1
565
		bRedraw = true
566
	elseif screenX > w then
567
		scrollX = scrollX + (screenX - w)
568
		screenX = w
569
		bRedraw = true
570
	end
571
572
	if screenY < 1 then
573
		scrollY = y - 1
574
		screenY = 1
575
		bRedraw = true
576
	elseif screenY > h-1 then
577
		scrollY = y - (h-1)
578
		screenY = h-1
579
		bRedraw = true
580
	end
581
582
	recomplete()
583
	if bRedraw then
584
		redrawText()
585
	elseif y ~= oldY then
586
		redrawLine( oldY )
587
		redrawLine( y )
588
	else
589
		redrawLine( y )
590
	end
591
	term.setCursorPos( screenX, screenY )
592
593
	redrawMenu()
594
end
595
596
local function setCursorMark(newX, newY)
597
	markX, markY = newX, newY
598
	setCursor(newX, newY)
599
end
600
601
local function deleteMarked()
602
	if markExists() then
603
		local msx, msy, mex, mey = getMarks()
604
605
		if msy == mey then
606
			local line = tLines[mey]
607
608
			tLines[mey] = line:sub(1, msx - 1) .. line:sub(mex)
609
		else
610
			if mey - msy > 1 then
611
				for i = 1, mey - msy - 1 do
612
					table.remove(tLines, msy + 1)
613
				end
614
			end
615
616
			local line = tLines[msy]
617
			local line2 = tLines[msy + 1]
618
619
			tLines[msy] = line:sub(1, msx - 1) .. line2:sub(mex)
620
			table.remove(tLines, msy + 1)
621
		end
622
623
		setCursorMark(msx, msy)
624
	end
625
end
626
627
local function getMarked()
628
	if markExists() then
629
		local msx, msy, mex, mey = getMarks()
630
631
		if msy == mey then
632
			return tLines[msy]:sub(msx, mex - 1)
633
		else
634
			local marked = tLines[msy]:sub(msx)
635
636
			if mey - msy > 1 then
637
				for i = msy + 1, mey - 1 do
638
					marked = marked .. '\n' .. tLines[i]
639
				end
640
			end
641
642
			marked = marked .. '\n' .. tLines[mey]:sub(1, mex - 1)
643
644
			return marked
645
		end
646
	end
647
end
648
649
local toolsFuncs = {
650
	{
651
		'Convert 2 spaces to tabs',
652
		function()
653
			for i = 1, #tLines do
654
				local line = tLines[i]
655
				local count = 0
656
657
				while true do
658
					if line:sub(count * 2 + 1, count * 2 + 2) == '  ' then
659
						count = count + 1
660
					else
661
						break
662
					end
663
				end
664
665
				tLines[i] = ('\t'):rep(count) .. line:sub(count * 2 + 1)
666
			end
667
		end
668
	}, {
669
		'Convert tabs to 2 spaces',
670
		function()
671
			for i = 1, #tLines do
672
				local line = tLines[i]
673
				local count = 0
674
675
				while true do
676
					if line:sub(count + 1, count + 1) == '\t' then
677
						count = count + 1
678
					else
679
						break
680
					end
681
				end
682
683
				tLines[i] = ('  '):rep(count) .. line:sub(count + 1)
684
			end
685
		end
686
	}
687
}
688
689
-- for testing scrolling, etc in the tools menu
690
--[[for i = 1, 50 do
691
	table.insert(toolsFuncs, {'Do nothing ' .. tostring(i), function() end})
692
end]]
693
694
695
local findHistory = {}
696
local jumpHistory = {}
697
local vClipboard = ''
698
699
local tMenuFuncs = {
700
	Save = function()
701
		if bReadOnly then
702
			sStatus = "Access denied"
703
		else
704
			local ok, err = save( sPath )
705
			if ok then
706
				sStatus="Saved to "..sPath
707
			else
708
				sStatus="Error saving to "..sPath
709
			end
710
		end
711
		redrawMenu()
712
	end,
713
	Print = function()
714
		local printer = peripheral.find( "printer" )
715
		if not printer then
716
			sStatus = "No printer attached"
717
			return
718
		end
719
720
		local nPage = 0
721
		local sName = fs.getName( sPath )
722
		if printer.getInkLevel() < 1 then
723
			sStatus = "Printer out of ink"
724
			return
725
		elseif printer.getPaperLevel() < 1 then
726
			sStatus = "Printer out of paper"
727
			return
728
		end
729
730
		local screenTerminal = term.current()
731
		local printerTerminal = {
732
			getCursorPos = printer.getCursorPos,
733
			setCursorPos = printer.setCursorPos,
734
			getSize = printer.getPageSize,
735
			write = printer.write,
736
		}
737
		printerTerminal.scroll = function()
738
			if nPage == 1 then
739
				printer.setPageTitle( sName.." (page "..nPage..")" )
740
			end
741
742
			while not printer.newPage()	do
743
				if printer.getInkLevel() < 1 then
744
					sStatus = "Printer out of ink, please refill"
745
				elseif printer.getPaperLevel() < 1 then
746
					sStatus = "Printer out of paper, please refill"
747
				else
748
					sStatus = "Printer output tray full, please empty"
749
				end
750
751
				term.redirect( screenTerminal )
752
				redrawMenu()
753
				term.redirect( printerTerminal )
754
755
				local timer = os.startTimer(0.5)
756
				sleep(0.5)
757
			end
758
759
			nPage = nPage + 1
760
			if nPage == 1 then
761
				printer.setPageTitle( sName )
762
			else
763
				printer.setPageTitle( sName.." (page "..nPage..")" )
764
			end
765
		end
766
767
		bMenu = false
768
		term.redirect( printerTerminal )
769
		local ok, error = pcall( function()
770
			term.scroll()
771
			for n, sLine in ipairs( tLines ) do
772
				print( sLine )
773
			end
774
		end )
775
		term.redirect( screenTerminal )
776
		if not ok then
777
			print( error )
778
		end
779
780
		while not printer.endPage() do
781
			sStatus = "Printer output tray full, please empty"
782
			redrawMenu()
783
			sleep( 0.5 )
784
		end
785
		bMenu = true
786
787
		if nPage > 1 then
788
			sStatus = "Printed "..nPage.." Pages"
789
		else
790
			sStatus = "Printed 1 Page"
791
		end
792
		redrawMenu()
793
	end,
794
	Exit = function()
795
		bRunning = false
796
	end,
797
	Run = function()
798
		local sTempPath = "/.temp"
799
		local ok, err = save( sTempPath )
800
		if ok then
801
			local nTask = shell.openTab( sTempPath )
802
			if nTask then
803
				shell.switchTab( nTask )
804
			else
805
				sStatus="Error starting Task"
806
			end
807
			fs.delete( sTempPath )
808
		else
809
			sStatus="Error saving to "..sTempPath
810
		end
811
		redrawMenu()
812
	end,
813
	Tools = function()
814
		bMenu = false
815
		redrawText()
816
		redrawMenu()
817
818
		local bgColor = term.getBackgroundColor()
819
		paintutils.drawFilledBox(2, 2, w - 1, h - 1, colors.gray)
820
		term.setCursorPos(3, 3)
821
		term.write('ShEdit Tools')
822
823
		local scrollY = 0
824
		local height = h - 8
825
		local offset = 5
826
		local selected = 1
827
		local run = false
828
829
		term.setTextColor(colors.lightGray)
830
		term.setCursorPos(3, offset + height + 1)
831
		term.write('Arrows to select. Enter to run. Ctrl to cancel.')
832
833
		while true do
834
			term.setBackgroundColor(colors.gray)
835
			term.setTextColor(colors.lightGray)
836
			local toWrite = '(' .. tostring(selected) .. '/' .. tostring(#toolsFuncs) .. ')'
837
			term.setCursorPos(w - toWrite:len() - 1, 3)
838
			term.write(toWrite)
839
			term.setTextColor(colors.white)
840
841
			for i = 1, height do
842
				local index = i + scrollY
843
				term.setTextColor(colors.white)
844
845
				if selected == index then
846
					term.setTextColor(keywordColor)
847
				end
848
849
				if index <= #toolsFuncs then
850
					paintutils.drawLine(2, offset + i - 1, w - 1, offset + i - 1, color)
851
					term.setCursorPos(3, offset + index - scrollY - 1)
852
853
					if selected == index then
854
						term.write('> ')
855
					else
856
						term.write('  ')
857
					end
858
859
					term.write(toolsFuncs[index][1])
860
				end
861
			end
862
863
			local evt, arg1, arg2, arg3 = os.pullEvent()
864
			local clampScroll = false
865
866
			if evt == 'key' then
867
				clampScroll = true
868
869
				if arg1 == keys.up then
870
					if selected == 1 then
871
						selected = #toolsFuncs
872
					else
873
						selected = selected - 1
874
					end
875
				elseif arg1 == keys.down then
876
					if selected == #toolsFuncs then
877
						selected = 1
878
					else
879
						selected = selected + 1
880
					end
881
				elseif arg1 == keys.pageUp then
882
					selected = math.max(1, math.min(#toolsFuncs, selected - height))
883
				elseif arg1 == keys.pageDown then
884
					selected = math.max(1, math.min(#toolsFuncs, selected + height))
885
				elseif arg1 == keys.enter then
886
					run = true
887
888
					break
889
				elseif arg1 == keys.leftCtrl then
890
					break
891
				else
892
					clampScroll = false
893
				end
894
			elseif evt == 'mouse_click' then
895
				if arg1 == 1 then
896
					if arg2 > 1 and arg2 < w then
897
						if arg3 >= offset and arg3 < offset + height then
898
							local index = arg3 - offset + scrollY + 1
899
900
							if index <= #toolsFuncs then
901
								selected = index
902
								run = true
903
904
								break
905
							end
906
						end
907
					end
908
				end
909
			elseif evt == 'mouse_scroll' then
910
				if arg2 > 1 and arg2 < w then
911
					if arg3 >= offset and arg3 < offset + height then
912
						scrollY = math.max(0, math.min(#toolsFuncs - height, scrollY + arg1))
913
					end
914
				end
915
			end
916
917
			if clampScroll == true then
918
				if selected > height + scrollY then
919
					scrollY = selected - height
920
				elseif selected <= scrollY then
921
					scrollY = selected - 1
922
				end
923
			end
924
		end
925
926
		term.setBackgroundColor(bgColor)
927
		redrawText()
928
		bMenu = true
929
930
		if run then
931
			if not bReadOnly then
932
				bMenu = false
933
				redrawMenu()
934
				toolsFuncs[selected][2]()
935
				onUpdate()
936
				redrawText()
937
				bMenu = true
938
			else
939
				sStatus = 'File is read-only'
940
			end
941
		end
942
	end,
943
	Find = function()
944
		term.setCursorPos(1, h)
945
		term.clearLine()
946
947
		term.setTextColor(highlightColor)
948
		term.write('Find: ')
949
		term.setTextColor(textColor)
950
951
		local text = read(nil, findHistory)
952
		table.insert(findHistory, text)
953
954
		local fLine, fPos
955
		local found = false
956
957
		for i = y, #tLines do
958
			local searchText = tLines[i]
959
960
			if i == y then
961
				searchText = searchText:sub(x + 1)
962
			end
963
964
			local searchPat = ''
965
966
			local escapedChars = {
967
				['%'] = true, ['.'] = true, ['('] = true, [')'] = true,
968
				['%'] = true, ['+'] = true, ['-'] = true, ['*'] = true,
969
				['?'] = true, ['['] = true, ['^'] = true, ['$'] = true
970
			}
971
972
			for i = 1, #text do
973
				local char = text:sub(i, i)
974
975
				if escapedChars[char] == true then
976
					searchPat = searchPat .. '%'
977
				end
978
979
				searchPat = searchPat .. char
980
			end
981
982
			local pos = searchText:find(searchPat)
983
984
			if pos then
985
				fLine = i
986
				fPos = pos
987
988
				if i == y then
989
					fPos = fPos + x
990
				end
991
992
				found = true
993
994
				break
995
			end
996
		end
997
998
		if not found then
999
			sStatus = 'Found no matches'
1000
		else
1001
			setCursorMark(fPos, fLine)
1002
			setCursor(fPos + text:len(), fLine)
1003
			sStatus = 'Found a match on line ' .. tostring(fLine)
1004
		end
1005
1006
		redrawText()
1007
	end,
1008
	Jump = function()
1009
		term.setCursorPos(1, h)
1010
		term.clearLine()
1011
1012
		term.setTextColor(highlightColor)
1013
		term.write('Jump: ')
1014
		term.setTextColor(textColor)
1015
1016
		local toJump = read(nil, jumpHistory)
1017
		table.insert(jumpHistory, toJump)
1018
1019
		local num = tonumber(toJump)
1020
1021
		if not num then
1022
			sStatus = 'Invalid line'
1023
		else
1024
			if num % 1 == 0 then
1025
				if num >= 1 and num <= #tLines then
1026
					setCursorMark(1, num)
1027
					sStatus = 'Successfully jumped to line ' .. toJump
1028
				else
1029
					sStatus = 'Line is not in the range 1 - ' .. tostring(#tLines)
1030
				end
1031
			else
1032
				sStatus = 'Line must be an integer'
1033
			end
1034
		end
1035
1036
		redrawText()
1037
	end,
1038
	Copy = function()
1039
		vClipboard = getMarked()
1040
1041
		local lines = 1
1042
1043
		for i = 1, #vClipboard do
1044
			if vClipboard:sub(i, i) == '\n' then
1045
				lines = lines + 1
1046
			end
1047
		end
1048
1049
		sStatus = 'Copied ' .. tostring(lines) .. ' lines'
1050
	end,
1051
	VPaste = function()
1052
		if not bReadOnly then
1053
			deleteMarked()
1054
			local lines = 1
1055
1056
			for i = 1, #vClipboard do
1057
				local char = vClipboard:sub(i, i)
1058
1059
				if char == '\n' then
1060
					lines = lines + 1
1061
					local sLine = tLines[y]
1062
					tLines[y] = string.sub(sLine,1,x-1)
1063
					table.insert( tLines, y+1, string.sub(sLine,x) )
1064
					x = 1
1065
					y = y + 1
1066
				else
1067
					tLines[y] = tLines[y]:sub(1, x - 1) .. char .. tLines[y]:sub(x)
1068
					x = x + 1
1069
				end
1070
			end
1071
1072
			setCursorMark(x, y)
1073
			onUpdate()
1074
			redrawText()
1075
1076
			sStatus = 'Pasted ' .. tostring(lines) .. ' lines'
1077
		else
1078
			sStatus = 'File is read-only'
1079
		end
1080
	end
1081
}
1082
1083
local function doMenuItem( _n )
1084
	tMenuFuncs[tMenuItems[_n]]()
1085
	if bMenu then
1086
		bMenu = false
1087
		term.setCursorBlink( true )
1088
	end
1089
	redrawMenu()
1090
end
1091
1092
-- Actual program functionality begins
1093
load(sPath)
1094
1095
term.setBackgroundColor( bgColor )
1096
term.clear()
1097
term.setCursorPos(getCursorX(y), y)
1098
term.setCursorBlink( true )
1099
1100
recomplete()
1101
onUpdate()
1102
redrawText()
1103
redrawMenu()
1104
1105
local function acceptCompletion()
1106
	if nCompletion then
1107
		-- Append the completion
1108
		local sCompletion = tCompletions[ nCompletion ]
1109
		tLines[y] = tLines[y] .. sCompletion
1110
		onUpdate()
1111
		setCursorMark( x + string.len( sCompletion ), y )
1112
	end
1113
end
1114
1115
local holdingShift = false
1116
1117
-- Handle input
1118
while bRunning do
1119
	local sEvent, param, param2, param3 = os.pullEvent()
1120
	if sEvent == "key" then
1121
		local oldX, oldY = x, y
1122
		if param == keys.up then
1123
			local cursorSet = setCursorMark
1124
1125
			if holdingShift then
1126
				cursorSet = setCursor
1127
			end
1128
1129
			-- Up
1130
			if not bMenu then
1131
				if nCompletion then
1132
					-- Cycle completions
1133
					nCompletion = nCompletion - 1
1134
					if nCompletion < 1 then
1135
						nCompletion = #tCompletions
1136
					end
1137
					redrawLine(y)
1138
1139
				elseif y > 1 then
1140
					-- Move cursor up
1141
					--[[cursorSet(
1142
						math.min( x, string.len( tLines[y - 1] ) + 1 ),
1143
						y - 1
1144
					)]]
1145
1146
					local cx = getCursorX(y)
1147
					local pos = cxToPos(y - 1, cx)
1148
1149
					cursorSet(pos, y - 1)
1150
				else
1151
					cursorSet(1, y)
1152
				end
1153
			end
1154
1155
		elseif param == keys.down then
1156
			local cursorSet = setCursorMark
1157
1158
			if holdingShift then
1159
				cursorSet = setCursor
1160
			end
1161
1162
			-- Down
1163
			if not bMenu then
1164
				-- Move cursor down
1165
				if nCompletion then
1166
					-- Cycle completions
1167
					nCompletion = nCompletion + 1
1168
					if nCompletion > #tCompletions then
1169
						nCompletion = 1
1170
					end
1171
					redrawLine(y)
1172
1173
				elseif y < #tLines then
1174
					-- Move cursor down
1175
					--[[cursorSet(
1176
						math.min( x, string.len( tLines[y + 1] ) + 1 ),
1177
						y + 1
1178
					)]]
1179
1180
					local cx = getCursorX(y)
1181
					local pos = cxToPos(y + 1, cx)
1182
1183
					cursorSet(pos, y + 1)
1184
				else
1185
					cursorSet(#tLines[y] + 1, y)
1186
				end
1187
			end
1188
1189
		elseif param == keys.tab then
1190
			-- Tab
1191
			if not bMenu and not bReadOnly then
1192
				if nCompletion and x == string.len(tLines[y]) + 1 then
1193
					-- Accept autocomplete
1194
					acceptCompletion()
1195
				else
1196
					local msx, msy, mex, mey = getMarks()
1197
1198
					for i = msy, mey do
1199
						local line = tLines[i]
1200
1201
						if holdingShift then
1202
							-- Unindent line
1203
							if line:sub(1, 1) == '\t' then
1204
								line = line:sub(2)
1205
							end
1206
						else
1207
							-- Indent line
1208
							if markExists() then
1209
								line = '\t' .. line
1210
							else
1211
								line = line:sub(1, x - 1) .. '\t' .. line:sub(x)
1212
							end
1213
						end
1214
1215
						tLines[i] = line
1216
					end
1217
1218
					if holdingShift then
1219
						markX = math.max(1, markX - 1)
1220
						setCursor(math.max(1, x - 1), y)
1221
					else
1222
						markX = markX + 1
1223
						setCursor(x + 1, y)
1224
					end
1225
1226
					onUpdate()
1227
					redrawText()
1228
				end
1229
			end
1230
1231
		elseif param == keys.pageUp then
1232
			local cursorSet = setCursorMark
1233
1234
			if holdingShift then
1235
				cursorSet = setCursor
1236
			end
1237
1238
			-- Page Up
1239
			if not bMenu then
1240
				-- Move up a page
1241
				local newY
1242
				if y - (h - 1) >= 1 then
1243
					newY = y - (h - 1)
1244
				else
1245
					newY = 1
1246
				end
1247
				cursorSet(
1248
					math.min( x, string.len( tLines[newY] ) + 1 ),
1249
					newY
1250
				)
1251
			end
1252
1253
		elseif param == keys.pageDown then
1254
			local cursorSet = setCursorMark
1255
1256
			if holdingShift then
1257
				cursorSet = setCursor
1258
			end
1259
1260
			-- Page Down
1261
			if not bMenu then
1262
				-- Move down a page
1263
				local newY
1264
				if y + (h - 1) <= #tLines then
1265
					newY = y + (h - 1)
1266
				else
1267
					newY = #tLines
1268
				end
1269
				local newX = math.min( x, string.len( tLines[newY] ) + 1 )
1270
				cursorSet( newX, newY )
1271
			end
1272
1273
		elseif param == keys.home then
1274
			local cursorSet = setCursorMark
1275
1276
			if holdingShift then
1277
				cursorSet = setCursor
1278
			end
1279
1280
			-- Home
1281
			if not bMenu then
1282
				-- Move cursor to the beginning
1283
				if x > 1 then
1284
					cursorSet(1,y)
1285
				end
1286
			end
1287
1288
		elseif param == keys["end"] then
1289
			local cursorSet = setCursorMark
1290
1291
			if holdingShift then
1292
				cursorSet = setCursor
1293
			end
1294
1295
			-- End
1296
			if not bMenu then
1297
				-- Move cursor to the end
1298
				local nLimit = string.len( tLines[y] ) + 1
1299
				if x < nLimit then
1300
					cursorSet( nLimit, y )
1301
				end
1302
			end
1303
1304
		elseif param == keys.left then
1305
			local cursorSet = setCursorMark
1306
1307
			if holdingShift then
1308
				cursorSet = setCursor
1309
			end
1310
1311
			-- Left
1312
			if not bMenu then
1313
				if x > 1 then
1314
					-- Move cursor left
1315
					cursorSet( x - 1, y )
1316
				elseif x==1 and y>1 then
1317
					cursorSet( string.len( tLines[y-1] ) + 1, y - 1 )
1318
				end
1319
			else
1320
				-- Move menu left
1321
				nMenuItem = nMenuItem - 1
1322
				if nMenuItem < 1 then
1323
					nMenuItem = #tMenuItems
1324
				end
1325
				redrawMenu()
1326
			end
1327
1328
		elseif param == keys.right then
1329
			local cursorSet = setCursorMark
1330
1331
			if holdingShift then
1332
				cursorSet = setCursor
1333
			end
1334
1335
			-- Right
1336
			if not bMenu then
1337
				local nLimit = string.len( tLines[y] ) + 1
1338
				if x < nLimit then
1339
					-- Move cursor right
1340
					cursorSet( x + 1, y )
1341
				elseif nCompletion and x == string.len(tLines[y]) + 1 then
1342
					-- Accept autocomplete
1343
					acceptCompletion()
1344
				elseif x==nLimit and y<#tLines then
1345
					-- Go to next line
1346
					cursorSet( 1, y + 1 )
1347
				end
1348
			else
1349
				-- Move menu right
1350
				nMenuItem = nMenuItem + 1
1351
				if nMenuItem > #tMenuItems then
1352
					nMenuItem = 1
1353
				end
1354
				redrawMenu()
1355
			end
1356
1357
		elseif param == keys.delete then
1358
			-- Delete
1359
			if not bMenu and not bReadOnly then
1360
				if markExists() then
1361
					deleteMarked()
1362
				else
1363
					local nLimit = string.len( tLines[y] ) + 1
1364
					if x < nLimit then
1365
						local sLine = tLines[y]
1366
						tLines[y] = string.sub(sLine,1,x-1) .. string.sub(sLine,x+1)
1367
					elseif y<#tLines then
1368
						tLines[y] = tLines[y] .. tLines[y+1]
1369
						table.remove( tLines, y+1 )
1370
					end
1371
				end
1372
1373
				onUpdate()
1374
				recomplete()
1375
				redrawText()
1376
			end
1377
1378
		elseif param == keys.backspace then
1379
			-- Backspace
1380
			if not bMenu and not bReadOnly then
1381
				if markExists() then
1382
					deleteMarked()
1383
				else
1384
					if x > 1 then
1385
						-- Remove character
1386
						local sLine = tLines[y]
1387
						tLines[y] = string.sub(sLine,1,x-2) .. string.sub(sLine,x)
1388
						setCursorMark( x - 1, y )
1389
					elseif y > 1 then
1390
						-- Remove newline
1391
						local sPrevLen = string.len( tLines[y-1] )
1392
						tLines[y-1] = tLines[y-1] .. tLines[y]
1393
						table.remove( tLines, y )
1394
						setCursorMark( sPrevLen + 1, y - 1 )
1395
					end
1396
				end
1397
1398
				onUpdate()
1399
				recomplete()
1400
				redrawText()
1401
			end
1402
1403
		elseif param == keys.enter then
1404
			-- Enter
1405
			if not bMenu and not bReadOnly then
1406
				deleteMarked()
1407
1408
				-- Newline
1409
				local sLine = tLines[y]
1410
				local _,tabs=string.find(sLine,"^[\t]+")
1411
				if not tabs then
1412
					tabs=0
1413
				end
1414
				tLines[y] = string.sub(sLine,1,x-1)
1415
				table.insert( tLines, y+1, string.rep('\t',tabs)..string.sub(sLine,x) )
1416
				onUpdate()
1417
				setCursorMark( tabs + 1, y + 1 )
1418
				redrawText()
1419
1420
			elseif bMenu then
1421
				-- Menu selection
1422
				doMenuItem( nMenuItem )
1423
1424
			end
1425
1426
		elseif (param == keys.leftCtrl or param == keys.rightCtrl) and not param2 then
1427
			-- Menu toggle
1428
			bMenu = not bMenu
1429
			if bMenu then
1430
				term.setCursorBlink( false )
1431
			else
1432
				term.setCursorBlink( true )
1433
			end
1434
			redrawMenu()
1435
1436
   elseif (param == keys.rightAlt) then
1437
       if bMenu then
1438
           bMenu = false
1439
           term.setCursorBlink( true )
1440
           redrawMenu()
1441
       end
1442
1443
		elseif param == 42 then
1444
			holdingShift = true
1445
		end
1446
1447
	elseif sEvent == 'key_up' then
1448
		if param == 42 then
1449
			holdingShift = false
1450
		end
1451
1452
	elseif sEvent == "char" then
1453
		if not bMenu and not bReadOnly then
1454
			deleteMarked()
1455
1456
			-- Input text
1457
			local sLine = tLines[y]
1458
			tLines[y] = string.sub(sLine,1,x-1) .. param .. string.sub(sLine,x)
1459
			onUpdate()
1460
			setCursorMark( x + 1, y )
1461
1462
		elseif bMenu then
1463
			-- Select menu items
1464
			for n,sMenuItem in ipairs( tMenuItems ) do
1465
				if string.lower(string.sub(sMenuItem,1,1)) == string.lower(param) then
1466
					doMenuItem( n )
1467
					break
1468
				end
1469
			end
1470
		end
1471
1472
	elseif sEvent == "paste" then
1473
		if not bMenu and not bReadOnly then
1474
			-- Input text
1475
			--[[local sLine = tLines[y]
1476
			tLines[y] = string.sub(sLine,1,x-1) .. param .. string.sub(sLine,x)
1477
			onUpdate()
1478
			setCursorMark( x + string.len( param ), y )]]
1479
1480
			--[[deleteMarked()
1481
1482
			local newX, newY = x, y
1483
1484
			for i = 1, #param do
1485
				local char = param:sub(i, i)
1486
1487
				if char == '\n' then
1488
					local rest = tLines[newY]:sub(newX)
1489
					tLines[newY] = tLines[newY]:sub(1, newX - 1)
1490
1491
					table.insert(tLines, rest, newY + 1)
1492
1493
					newY = newY + 1
1494
					newX = 1
1495
				else
1496
					local line = tLines[newY]
1497
1498
					line = line:sub(1, newX - 1) .. char .. line:sub(newX)
1499
1500
					tLines[newY] = line
1501
					newX = newX + 1
1502
				end
1503
			end
1504
1505
			onUpdate()
1506
			setCursorMark(newX, newY)]]
1507
1508
			local oldvClipboard = vClipboard
1509
			vClipboard = param
1510
			tMenuFuncs.VPaste()
1511
			redrawMenu()
1512
			vClipboard = oldvClipboard
1513
		end
1514
1515
	elseif sEvent == "mouse_click" then
1516
		local cursorSet = setCursorMark
1517
1518
		if holdingShift then
1519
			cursorSet = setCursor
1520
		end
1521
1522
		if not bMenu then
1523
			if param == 1 then
1524
				-- Left click
1525
				local cx,cy = param2, param3
1526
				if cy < h then
1527
					local tx, ty = cx + scrollX, cy + scrollY
1528
					local newX, newY
1529
1530
					if ty <= #tLines then
1531
						newY = math.min(#tLines, math.max(ty, 1))
1532
						tx = cxToPos(ty, tx)
1533
						newX = math.min(tLines[newY]:len() + 1, math.max(1, tx))
1534
					else
1535
						newY = #tLines
1536
						newX = #tLines[newY] + 1
1537
					end
1538
1539
					cursorSet(newX, newY)
1540
				end
1541
			end
1542
		end
1543
1544
	elseif sEvent == "mouse_drag" then
1545
		if not bMenu then
1546
			if param == 1 then
1547
				-- Left click
1548
				local cx,cy = param2, param3
1549
				if cy < h then
1550
					local tx, ty = cx + scrollX, cy + scrollY
1551
					local newX, newY
1552
1553
					if ty <= #tLines then
1554
						newY = math.min(#tLines, math.max(ty, 1))
1555
						tx = cxToPos(ty, tx)
1556
						newX = math.min(tLines[newY]:len() + 1, math.max(1, tx))
1557
					else
1558
						newY = #tLines
1559
						newX = #tLines[newY] + 1
1560
					end
1561
1562
					setCursor(newX, newY)
1563
				end
1564
			end
1565
		end
1566
1567
	elseif sEvent == "mouse_scroll" then
1568
		if not bMenu then
1569
			if param == -1 then
1570
				-- Scroll up
1571
				if scrollY > 0 then
1572
					-- Move cursor up
1573
					scrollY = scrollY - 1
1574
					redrawText()
1575
				end
1576
1577
			elseif param == 1 then
1578
				-- Scroll down
1579
				local nMaxScroll = #tLines - (h-1)
1580
				if scrollY < nMaxScroll then
1581
					-- Move cursor down
1582
					scrollY = scrollY + 1
1583
					redrawText()
1584
				end
1585
1586
			end
1587
		end
1588
1589
	elseif sEvent == "term_resize" then
1590
		w,h = term.getSize()
1591
		setCursor( x, y )
1592
		redrawMenu()
1593
		redrawText()
1594
1595
	end
1596
end
1597
1598
-- Cleanup
1599
term.clear()
1600
term.setCursorBlink( false )
1601
term.setCursorPos( 1, 1 )