added "install" instruction
[Leditor.git] / src / editor.lua
blobefeb8ada00fec8b2d1a20fda8e615dc62b8a174c
1 #!/usr/bin/lua
3 if not QObject then
4 require'qt'
5 require'QAbstractScrollArea'
6 require'QAction'
7 require'QApplication'
8 require'QBoxLayout'
9 require'QColor'
10 require'QCoreApplication'
11 require'QEvent'
12 require'QFileDialog'
13 require'QFont'
14 require'QFrame'
15 require'QGraphicsItem'
16 require'QGraphicsScene'
17 require'QGraphicsView'
18 require'QHBoxLayout'
19 require'QIcon'
20 require'QInputEvent'
21 require'QKeyEvent'
22 require'QKeySequence'
23 require'QLabel'
24 require'QLayout'
25 require'QLayoutItem'
26 require'QLineEdit'
27 require'QMainWindow'
28 require'QMenuBar'
29 require'QMenu'
30 require'QMessageBox'
31 require'QObject'
32 require'QPaintDevice'
33 require'QShortcut'
34 require'QSize'
35 require'QSyntaxHighlighter'
36 require'Qt'
37 require'QTextBlockUserData'
38 require'QTextCharFormat'
39 require'QTextCursor'
40 require'QTextDocument'
41 require'QTextEdit'
42 require'QToolBar'
43 require'QVBoxLayout'
44 require'QWidget'
45 else
46 qt = { signal = signal, derive = derive,
47 pass = function(...) return ... end, pick = function(...) return ... end,
48 slot = function(a)
49 if type(a)=='string' then return '1'..a end
50 if type(a)=='function' then return slot_from_function(a) end
51 end,
53 end
56 local sc
57 local args = { "editor", "-sync" }
58 --print(#args)
59 app = qt.pass(QApplication.new(#args, args))
61 mw = QMainWindow.new()
62 mw:setWindowTitle'Leditor'
63 mw:show()
64 mw:resize(800, 600)
68 te = QTextEdit.new()
69 te:setFont(QFont.new'Monospace')
70 te:setAcceptRichText(false)
71 --te:setTabStopWidth(20)
72 --te.starting_document = te:document()
74 local sd = te.setDocument
75 local olddocs = {}
76 function te:setDocument (d)
77 local olddoc = self:document()
78 table.insert(olddocs, olddoc)
79 --print(d, olddoc)
80 sd(self, d)
81 ls:setDocument(d)
82 d:connect(qt.signal'destroyed()', qt.slot(
83 function ()
84 d:disconnect(qt.signal'destroyed()')
85 -- shouldn't need more than this. title and filename will be erased later
86 if QTextDocument.openDocuments[d] then
87 QTextDocument.openDocuments[d] = nil
88 end
89 local back_doc = table.remove(olddocs)
90 while back_doc and not QTextDocument.openDocuments[back_doc] do back_doc = table.remove(olddocs) end
91 self:setDocument(back_doc or QTextDocument.new())
92 end
94 local title = d:title() or d:fileName() or '<Untitled>'
95 mw:setWindowTitle('Leditor: '..title)
96 end
97 end
99 le = QLineEdit.new()
100 le:setFont(QFont.new'Monospace')
101 le.m_history = {}
102 le.m_history_index = 0
103 function le:addHistoryLine(t)
104 if t=='' then return false end
105 table.insert(self.m_history, t)
106 self.m_history_index = table.maxn(self.m_history) + 1
108 function le:adjustHistory()
109 if type(self.m_history[self.m_history_index])=='string' then
110 self:setText(self.m_history[self.m_history_index])
111 else
112 self.m_history_index = table.maxn(self.m_history) + 1
113 self:setText(self.m_current_line or '')
116 function le:goToHistoryIndex(i)
117 self.m_history_index = tonumber(i)
118 self:adjustHistory()
120 function le:historyBack()
121 self.m_history_index = self.m_history_index - 1
122 self:adjustHistory()
124 function le:historyForward()
125 self.m_history_index = self.m_history_index + 1
126 self:adjustHistory()
128 sc = QShortcut.new(QKeySequence.new(Qt.Key['Key_Up']), le) qt.pass(sc)
129 sc:setContext'WidgetShortcut'
130 sc:connect(qt.signal'activated()', qt.slot(
131 function()
132 le:historyBack()
135 sc = QShortcut.new(QKeySequence.new(Qt.Key['Key_Down']), le) qt.pass(sc)
136 sc:setContext'WidgetShortcut'
137 sc:connect(qt.signal'activated()', qt.slot(
138 function()
139 le:historyForward()
143 -- OK
145 el = QVBoxLayout.new()
146 editor = QWidget.new()
147 editor:setLayout(qt.pass(el))
148 el:addWidget(qt.pass(te))
149 el:addWidget(qt.pass(le))
151 mw:setCentralWidget(editor)
153 ls = QSyntaxHighlighter.new(editor)
154 qt.derive(ls)
156 local keywords = {
157 ['for'] = true,
158 ['in'] = true,
159 ['do'] = true,
160 ['end'] = true,
161 ['function'] = true,
162 ['while'] = true,
163 ['local'] = true,
164 ['if'] = true,
165 ['then'] = true,
166 ['break'] = true,
167 ['return'] = true,
169 function ls:highlightBlock(text)
170 --print(text)
171 for m, f, l in string.gmatch(text, '(()[%w_]+())') do
172 --print(f, l)
173 --print(QFont.Weight.Bold)
174 if keywords[m] then
175 self:setFormat(f-1, l-f, QFont.new('Monospace', 10, QFont.Weight.Bold))
179 ls:setDocument(te:document())
181 -- CONFIG
183 --table.foreach( getmetatable(QMessageBox.StandardButton.QFlags(QMessageBox.StandardButton.Discard, QMessageBox.StandardButton.Cancel)), print )
184 --print( QMessageBox.StandardButton.QFlags(QMessageBox.StandardButton.Discard, QMessageBox.StandardButton.Cancel) )
185 --print( QMessageBox.StandardButton.QFlags(QMessageBox.StandardButton.Discard, QMessageBox.StandardButton.Cancel).__qtype )
186 --print(QMessageBox.StandardButton.Discard)
188 --[[
189 function QTextDocument:discard(sure)
190 if self:isModified() and not sure then
191 local bpress = QMessageBox.question(mw, "Discard File?", "There are unsaved changes too this file. Proceeding will discard these changes", QMessageBox.StandardButton.QFlags('Discard', 'Cancel'))
192 if bpress=='Discard' then
193 elseif bpress=='Cancel' then
194 error'Action Canceled'
197 te.documents[self] = { files={} }
198 self:clear()
199 --te.documents[self].files = {}
201 function QTextDocument:load(f)
202 self:discard()
203 f = f or QFileDialog.getOpenFileName(nil, "Open Document")
204 self:setMetaInformation(QTextDocument.MetaInformation.DocumentTitle, f)
205 local file, err = io.open(f, "r")
206 if not file then error(err) end
207 self:setPlainText(file:read'*a')
208 file:close()
210 --]]
212 QTextDocument.titles = {}
213 QTextDocument.filenames = {}
214 QTextDocument.openDocuments = {}
216 function QTextDocument:fileName ()
217 return QTextDocument.filenames[self]
219 function QTextDocument:title ()
220 return QTextDocument.titles[self]
223 function QTextDocument:setFileName (fn)
224 QTextDocument.filenames[self] = fn
226 function QTextDocument:setTitle (t)
227 QTextDocument.titles[self] = t
231 local oldnew = QTextDocument.new
232 local olddelete = QTextDocument.delete
233 QTextDocument.new = function(...)
234 local ret = oldnew(...)
235 ret.openDocuments[ret] = true
236 return ret
238 QTextDocument.delete = function(doc, ...)
239 doc.openDocuments[doc] = nil
240 QTextDocument.filenames[doc] = nil
241 QTextDocument.titles[doc] = nil
242 return olddelete(doc, ...)
247 function QTextDocument:close ()
248 if not self.openDocuments[self] then return end
249 if self:fileName() and self:isModified() then
250 local bpress = QMessageBox.question(mw, "Discard File?", "There are unsaved changes too this file. Proceeding will discard these changes", QMessageBox.StandardButton.QFlags('Discard', 'Save', 'Cancel'))
251 if bpress=='Discard' then
252 print'Discarding'
253 elseif bpress=='Save' then
254 self:save()
255 else
256 error'Action Canceled'
259 self:delete()
262 function QTextDocument:save (f)
263 local text = self:toPlainText()
264 f = f or self:fileName() or ''
265 -- particularly proud of the next line as it uses the difference between a?b:c and (a and b) or c
266 f = f~='' and f or QFileDialog.getSaveFileName(nil, "Save Document As")
267 if type(f)=='string' then
268 self:setFileName(f)
270 local file, err = io.open(f, "w")
271 if not file then error(err) end
272 file:write(text)
273 file:close()
274 self:setFileName(f)
275 --self:setModified(false)
278 function te:save(...)
279 return te:document():save(...)
281 function te:closeDocument(...)
282 return te:document():close(...)
285 function te:loadfile(fn)
286 local doc = nil
287 for d, n in pairs(QTextDocument.filenames) do
288 if n==fn then
289 --print'reused'
290 return self:setDocument(d)
293 -- FIXME: reusing starting document seems broken
294 --[[
295 if nil and not doc and self.starting_document then
296 doc = self.starting_document
297 self.starting_document = nil
299 --]]
300 local text, filename = '', nil
301 fn = fn~='' and fn or QFileDialog.getOpenFileName(nil, "Open Document")
302 if fn~='' then
303 local file, err = io.open(fn, "r")
304 if not file then error('Cannot open file '..fn..': '..err) end
305 text = file:read'*a'
306 file:close()
307 filename = fn
308 if not doc then doc = QTextDocument.new() end
309 if not doc then error'Cannot spawn a new document' end
310 doc:setPlainText(text)
311 if filename then doc:setFileName(filename) end
312 self:setDocument(doc)
315 function te:loaddoc(d)
316 if type(obj)=='userdata' and obj.__qtype=='QTextDocument' then
317 return self:setDocument(d)
318 else
319 return self:setDocument(QTextDocument.new())
322 function te:load(obj)
323 if type(obj)=='string' then
324 te:loadfile(obj)
325 elseif type(obj)=='userdata' and obj.__qtype=='QTextDocument' then
326 te:loaddoc(obj)
327 elseif obj==nil then
328 te:setDocument(QTextDocument.new())
329 --elseif type(obj)=='number' and QTextDocument.openDocuments[obj] then
330 --te:loaddoc(QTextDocument.openDocuments[obj])
333 function te:next(obj)
334 local init = self:document()
335 init = init.openDocuments[init] and init or nil
336 local newdoc = next(init.openDocuments, init)
337 if not newdoc then newdoc = next(init.openDocuments, nil) end
338 self:setDocument(newdoc)
342 --[[
343 function preprocess (widget)
346 qt.connect(le, qt.signal'textChanged(const QString&)', qt.slot(
347 function(...)
348 --print'textChanged'
349 preprocess(le)
352 -- ]]
354 --[[
355 for k, v in pairs(Qt.Key) do
356 if k~=Qt.Key[v] then print(k, v, Qt.Key[v]) end
358 -- ]]
360 sc = QShortcut.new(QKeySequence.new(Qt.Key['Key_Less']+Qt.Modifier['CTRL']), mw)
362 sc:connect(qt.signal'activated()', qt.slot(
363 function()
364 if not le:hasFocus() then
365 le:setFocus()
366 else
367 te:setFocus()
369 end))
371 function vim_std_command(text)
372 local cmd, args = string.match(text, '^:(%w+)%s*(.*)$')
373 args = string.match(args, '^(.*[^%s]*)%s*$')
374 --print(text, cmd, args)
375 if string.match('quit', cmd) then
376 mw:close()
377 elseif string.match('edit', cmd) then
378 --print('edit', args)
379 te:load(args~='' and args or nil)
380 elseif string.match('write', cmd) then
381 te:save(args~='' and args or nil)
382 elseif string.match('tetris', cmd) then
383 if tetris then
384 tetris:show()
385 else
386 tetris = dofile'tetris.lua'
387 tetris:show()
389 elseif string.match('substitute', cmd) then
390 local pat, sub, opt = string.match(args, '/(.*)/(.*)/([%dgc]*)')
391 opt = opt or ''
392 local cursor = te:textCursor()
393 if not cursor:hasSelection() then
394 cursor:select'LineUnderCursor'
396 local txt = cursor:selectedText()
397 local n = 1
398 if string.match(opt, 'g') then
399 n=nil
400 elseif string.match(opt, '%d+') and tonumber(string.match(opt, '%d+'))>0 then
401 n = tonumber(string.match(opt, '%d*'))
403 local ret = string.gsub(txt, pat, sub, n)
404 cursor:insertText(ret)
405 elseif string.match('close', cmd) then
406 te:closeDocument()
410 function vim_search_command(text)
411 local dir, args = string.match(text, '^([/?])(.*)$')
412 --print(text, dir, args)
413 local pos = te:textCursor():position()
414 local found = te:find(args, dir=='?' and QTextDocument.FindFlag.QFlags'FindBackward' or nil)
415 --print(found)
419 interpreters = {
420 [":"] = vim_std_command,
421 ["/"] = vim_search_command,
422 ["?"] = vim_search_command,
424 function process (text)
425 --print('processing', text)
426 local init = string.sub(text, 1, 1)
427 if interpreters[init] then
428 local st, err = pcall(interpreters[init], text)
429 if not st then
430 error(err)
431 else
432 return err
434 else
435 local st, err = loadstring(text)
436 --print(st, err)
437 if st then st, err = pcall(st) end
438 --print(st, err)
439 if not st then error(err) end
442 QObject.connect(
443 le, qt.signal'returnPressed()', qt.slot(
444 function()
445 --print'returnPressed'
446 local text = le:text()
447 local st, err = pcall(process, text)
448 if not st then
449 QMessageBox.critical(mw, "Error", 'Error while processing: "'..text..'":\n'..err)
451 le:addHistoryLine(text)
452 --[[
453 print('===========', text)
454 see_doc(te:document())
455 all_docs()
456 --]]
457 le:clear()
458 --te:setFocus()
462 function see_doc(k,v) print(k, k:title(), k:fileName()) end
463 function all_docs()
464 table.foreach(QTextDocument.openDocuments, function(k) print(k, k:title(), k:fileName()) end)
467 mb = QMenuBar.new()
468 mw:setMenuBar(qt.pass(mb))
469 tb = QMenu.new'File'
470 mb:addMenu(qt.pass(tb))
471 sw = tb:addAction'New' sw:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:setDocument(QTextDocument.new()) end), qt.slot'function(bool)')
472 tb:addSeparator()
473 sl = tb:addAction'Load' sl:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:loadfile() end), qt.slot'function(bool)')
474 tb:addSeparator()
475 sv = tb:addAction'Save' sv:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:save() end), qt.slot'function(bool)')
476 sa = tb:addAction'Save As' sa:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:save'' end), qt.slot'function(bool)')
477 tb:addSeparator()
478 sn = tb:addAction'Next' sn:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:next() end), qt.slot'function(bool)')
479 sc = tb:addAction'Close' sc:connect(qt.signal'triggered(bool)', qt.slot(function(b) te:closeDocument() end), qt.slot'function(bool)')
481 --[[
482 qt.pass(sw)
483 qt.pass(sl)
484 qt.pass(sv)
485 qt.pass(sa)
486 qt.pass(sn)
487 qt.pass(sc)
490 sw:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/filenew.png')
491 sl:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/fileopen.png')
492 sv:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/filesave.png')
493 sa:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/filesaveas.png')
494 sc:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/fileclose.png')
495 sn:setIcon(QIcon.new'/usr/kde/3.5/share/icons/crystalsvg/32x32/actions/next.png')
498 st = app:exec()
499 app:delete()
500 return st