975 lines
32 KiB
Lua
975 lines
32 KiB
Lua
-- Copyright 2011-14 Paul Kulchenko, ZeroBrane LLC
|
|
-- authors: Lomtik Software (J. Winwood & John Labenski)
|
|
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
|
|
---------------------------------------------------------
|
|
|
|
local ide = ide
|
|
local frame = ide.frame
|
|
local notebook = frame.notebook
|
|
local openDocuments = ide.openDocuments
|
|
local uimgr = frame.uimgr
|
|
local unpack = table.unpack or unpack
|
|
|
|
local CURRENT_LINE_MARKER = StylesGetMarker("currentline")
|
|
local CURRENT_LINE_MARKER_VALUE = 2^CURRENT_LINE_MARKER
|
|
|
|
function NewFile(filename)
|
|
filename = filename or ide.config.default.fullname
|
|
local editor = CreateEditor()
|
|
SetupKeywords(editor, GetFileExt(filename))
|
|
local doc = AddEditor(editor, filename)
|
|
if doc then
|
|
PackageEventHandle("onEditorNew", editor)
|
|
SetEditorSelection(doc.index)
|
|
end
|
|
return editor
|
|
end
|
|
|
|
-- Find an editor page that hasn't been used at all, eg. an untouched NewFile()
|
|
local function findUnusedEditor()
|
|
local editor
|
|
for _, document in pairs(openDocuments) do
|
|
if (document.editor:GetLength() == 0) and
|
|
(not document.isModified) and (not document.filePath) and
|
|
not (document.editor:GetReadOnly() == true) then
|
|
editor = document.editor
|
|
break
|
|
end
|
|
end
|
|
return editor
|
|
end
|
|
|
|
function LoadFile(filePath, editor, file_must_exist, skipselection)
|
|
local filePath = wx.wxFileName(filePath)
|
|
filePath:Normalize() -- make it absolute and remove all .. and . if possible
|
|
filePath = filePath:GetFullPath()
|
|
|
|
-- prevent files from being reopened again
|
|
if (not editor) then
|
|
local doc = ide:FindDocument(filePath)
|
|
if doc then
|
|
if not skipselection and doc.index ~= notebook:GetSelection() then
|
|
-- selecting the same tab doesn't trigger PAGE_CHANGE event,
|
|
-- but moves the focus to the tab bar, which needs to be avoided.
|
|
notebook:SetSelection(doc.index)
|
|
end
|
|
return doc.editor
|
|
end
|
|
end
|
|
|
|
-- if not opened yet, try open now
|
|
local file_text = FileRead(filePath)
|
|
if file_text then
|
|
if GetConfigIOFilter("input") then
|
|
file_text = GetConfigIOFilter("input")(filePath,file_text)
|
|
end
|
|
elseif file_must_exist then
|
|
return nil
|
|
end
|
|
|
|
local current = editor and editor:GetCurrentPos()
|
|
editor = editor or findUnusedEditor() or CreateEditor()
|
|
|
|
editor:Freeze()
|
|
SetupKeywords(editor, GetFileExt(filePath))
|
|
editor:MarkerDeleteAll(-1)
|
|
|
|
-- remove BOM from UTF-8 encoded files; store BOM to add back when saving
|
|
editor.bom = string.char(0xEF,0xBB,0xBF)
|
|
if file_text and editor:GetCodePage() == wxstc.wxSTC_CP_UTF8
|
|
and file_text:find("^"..editor.bom) then
|
|
file_text = file_text:gsub("^"..editor.bom, "")
|
|
else
|
|
-- set to 'false' as checks for nil on wxlua objects may fail at run-time
|
|
editor.bom = false
|
|
end
|
|
editor:SetText(file_text or "")
|
|
|
|
-- check the editor as it can be empty if the file has malformed UTF8;
|
|
-- skip binary files with unknown extensions as they may have any sequences;
|
|
-- can't show them anyway.
|
|
if file_text and #file_text > 0 and #(editor:GetText()) == 0
|
|
and (editor.spec ~= ide.specs.none or not isBinary(file_text)) then
|
|
local replacement, invalid = "\022"
|
|
file_text, invalid = FixUTF8(file_text, replacement)
|
|
if #invalid > 0 then
|
|
editor:AppendText(file_text)
|
|
local lastline = nil
|
|
for _, n in ipairs(invalid) do
|
|
local line = editor:LineFromPosition(n)
|
|
if line ~= lastline then
|
|
DisplayOutputLn(("%s:%d: %s")
|
|
:format(filePath, line+1, TR("Replaced an invalid UTF8 character with %s."):format(replacement)))
|
|
lastline = line
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
editor:Colourise(0, -1)
|
|
editor:Thaw()
|
|
|
|
if current then editor:GotoPos(current) end
|
|
if (file_text and ide.config.editor.autotabs) then
|
|
local found = string.find(file_text,"\t") ~= nil
|
|
editor:SetUseTabs(found)
|
|
end
|
|
|
|
if (file_text and ide.config.editor.checkeol) then
|
|
-- Auto-detect CRLF/LF line-endings
|
|
local foundcrlf = string.find(file_text,"\r\n") ~= nil
|
|
local foundlf = (string.find(file_text,"[^\r]\n") ~= nil)
|
|
or (string.find(file_text,"^\n") ~= nil) -- edge case: file beginning with LF and having no other LF
|
|
if foundcrlf and foundlf then -- file with mixed line-endings
|
|
DisplayOutputLn(("%s: %s")
|
|
:format(filePath, TR("Mixed end-of-line encodings detected.")..' '..
|
|
TR("Use '%s' to show line endings and '%s' to convert them.")
|
|
:format("GetEditor():SetViewEOL(1)", "GetEditor():ConvertEOLs(GetEditor():GetEOLMode())")))
|
|
elseif foundcrlf then
|
|
editor:SetEOLMode(wxstc.wxSTC_EOL_CRLF)
|
|
elseif foundlf then
|
|
editor:SetEOLMode(wxstc.wxSTC_EOL_LF)
|
|
-- else (e.g. file is 1 line long or uses another line-ending): use default EOL mode
|
|
end
|
|
end
|
|
|
|
editor:EmptyUndoBuffer()
|
|
local id = editor:GetId()
|
|
if openDocuments[id] then -- existing editor; switch to the tab
|
|
notebook:SetSelection(openDocuments[id].index)
|
|
else -- the editor has not been added to notebook
|
|
AddEditor(editor, wx.wxFileName(filePath):GetFullName()
|
|
or ide.config.default.fullname)
|
|
end
|
|
openDocuments[id].filePath = filePath
|
|
openDocuments[id].fileName = wx.wxFileName(filePath):GetFullName()
|
|
openDocuments[id].modTime = GetFileModTime(filePath)
|
|
|
|
SetDocumentModified(id, false, openDocuments[id].fileName)
|
|
|
|
-- activate the editor; this is needed for those cases when the editor is
|
|
-- created from some other element, for example, from a project tree.
|
|
if not skipselection then SetEditorSelection() end
|
|
|
|
PackageEventHandle("onEditorLoad", editor)
|
|
|
|
return editor
|
|
end
|
|
|
|
function ReLoadFile(filePath, editor, ...)
|
|
if not editor then return LoadFile(filePath, editor, ...) end
|
|
|
|
-- save all markers
|
|
local maskany = 2^24-1
|
|
local markers = {}
|
|
local line = editor:MarkerNext(0, maskany)
|
|
while line > -1 do
|
|
table.insert(markers, {line, editor:MarkerGet(line), editor:GetLine(line)})
|
|
line = editor:MarkerNext(line + 1, maskany)
|
|
end
|
|
local lines = editor:GetLineCount()
|
|
|
|
-- load file into the same editor
|
|
editor = LoadFile(filePath, editor, ...)
|
|
if not editor then return end
|
|
|
|
if #markers > 0 then -- restore all markers
|
|
local samelinecount = lines == editor:GetLineCount()
|
|
for _, marker in ipairs(markers) do
|
|
local line, mask, text = unpack(marker)
|
|
if samelinecount then
|
|
-- restore marker at the same line number
|
|
editor:MarkerAddSet(line, mask)
|
|
else
|
|
-- find matching line in the surrounding area and restore marker there
|
|
for _, l in ipairs({line, line-1, line-2, line+1, line+2}) do
|
|
if text == editor:GetLine(l) then
|
|
editor:MarkerAddSet(l, mask)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return editor
|
|
end
|
|
|
|
local function getExtsString()
|
|
local knownexts = ""
|
|
for _,spec in pairs(ide.specs) do
|
|
if (spec.exts) then
|
|
for _,ext in ipairs(spec.exts) do
|
|
knownexts = knownexts.."*."..ext..";"
|
|
end
|
|
end
|
|
end
|
|
knownexts = knownexts:len() > 0 and knownexts:sub(1,-2) or nil
|
|
|
|
local exts = knownexts and TR("Known Files").." ("..knownexts..")|"..knownexts.."|" or ""
|
|
return exts..TR("All files").." (*)|*"
|
|
end
|
|
|
|
function ReportError(msg)
|
|
return wx.wxMessageBox(msg, TR("Error"), wx.wxICON_ERROR + wx.wxOK + wx.wxCENTRE, ide.frame)
|
|
end
|
|
|
|
function OpenFile(event)
|
|
local editor = GetEditor()
|
|
local path = editor and ide:GetDocument(editor):GetFilePath() or nil
|
|
local fileDialog = wx.wxFileDialog(ide.frame, TR("Open file"),
|
|
(path and GetPathWithSep(path) or FileTreeGetDir() or ""),
|
|
"",
|
|
getExtsString(),
|
|
wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST)
|
|
if fileDialog:ShowModal() == wx.wxID_OK then
|
|
if not LoadFile(fileDialog:GetPath(), nil, true) then
|
|
ReportError(TR("Unable to load file '%s'."):format(fileDialog:GetPath()))
|
|
end
|
|
end
|
|
fileDialog:Destroy()
|
|
end
|
|
|
|
-- save the file to filePath or if filePath is nil then call SaveFileAs
|
|
function SaveFile(editor, filePath)
|
|
-- this event can be aborted
|
|
-- as SaveFileAs calls SaveFile, this event may be called two times:
|
|
-- first without filePath and then with filePath
|
|
if PackageEventHandle("onEditorPreSave", editor, filePath) == false then
|
|
return false
|
|
end
|
|
|
|
if not filePath then
|
|
return SaveFileAs(editor)
|
|
else
|
|
if ide.config.savebak then
|
|
local ok, err = FileRename(filePath, filePath..".bak")
|
|
if not ok then
|
|
ReportError(TR("Unable to save file '%s': %s"):format(filePath..".bak", err))
|
|
return
|
|
end
|
|
end
|
|
|
|
local st = (editor:GetCodePage() == wxstc.wxSTC_CP_UTF8 and editor.bom or "")
|
|
.. editor:GetText()
|
|
if GetConfigIOFilter("output") then
|
|
st = GetConfigIOFilter("output")(filePath,st)
|
|
end
|
|
|
|
local ok, err = FileWrite(filePath, st)
|
|
if ok then
|
|
editor:SetSavePoint()
|
|
local id = editor:GetId()
|
|
openDocuments[id].filePath = filePath
|
|
openDocuments[id].fileName = wx.wxFileName(filePath):GetFullName()
|
|
openDocuments[id].modTime = GetFileModTime(filePath)
|
|
SetDocumentModified(id, false, openDocuments[id].fileName)
|
|
SetAutoRecoveryMark()
|
|
|
|
PackageEventHandle("onEditorSave", editor)
|
|
|
|
return true
|
|
else
|
|
ReportError(TR("Unable to save file '%s': %s"):format(filePath, err))
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function ApproveFileOverwrite()
|
|
return wx.wxMessageBox(
|
|
TR("File already exists.").."\n"..TR("Do you want to overwrite it?"),
|
|
GetIDEString("editormessage"),
|
|
wx.wxYES_NO + wx.wxCENTRE, ide.frame) == wx.wxYES
|
|
end
|
|
|
|
function SaveFileAs(editor)
|
|
local id = editor:GetId()
|
|
local saved = false
|
|
local filePath = (openDocuments[id].filePath
|
|
or ((FileTreeGetDir() or "")
|
|
..(openDocuments[id].fileName or ide.config.default.name)))
|
|
|
|
local fn = wx.wxFileName(filePath)
|
|
fn:Normalize() -- want absolute path for dialog
|
|
|
|
local ext = fn:GetExt()
|
|
if (not ext or #ext == 0) and editor.spec and editor.spec.exts then
|
|
ext = editor.spec.exts[1]
|
|
-- set the extension on the file if assigned as this is used by OSX/Linux
|
|
-- to present the correct default "save as type" choice.
|
|
if ext then fn:SetExt(ext) end
|
|
end
|
|
local fileDialog = wx.wxFileDialog(ide.frame, TR("Save file as"),
|
|
fn:GetPath(wx.wxPATH_GET_VOLUME),
|
|
fn:GetFullName(),
|
|
-- specify the current extension plus all other extensions based on specs
|
|
(ext and #ext > 0 and "*."..ext.."|*."..ext.."|" or "")..getExtsString(),
|
|
wx.wxFD_SAVE)
|
|
|
|
if fileDialog:ShowModal() == wx.wxID_OK then
|
|
local filePath = fileDialog:GetPath()
|
|
|
|
-- check if there is another tab with the same name and prepare to close it
|
|
local existing = (ide:FindDocument(filePath) or {}).index
|
|
local cansave = fn:GetFullName() == filePath -- saving into the same file
|
|
or not wx.wxFileName(filePath):FileExists() -- or a new file
|
|
or ApproveFileOverwrite()
|
|
|
|
if cansave and SaveFile(editor, filePath) then
|
|
SetEditorSelection() -- update title of the editor
|
|
FileTreeMarkSelected(filePath)
|
|
if ext ~= GetFileExt(filePath) then
|
|
-- new extension, so setup new keywords and re-apply indicators
|
|
editor:ClearDocumentStyle() -- remove styles from the document
|
|
SetupKeywords(editor, GetFileExt(filePath))
|
|
IndicateAll(editor)
|
|
MarkupStyle(editor)
|
|
end
|
|
saved = true
|
|
|
|
if existing then
|
|
-- save the current selection as it may change after closing
|
|
local current = notebook:GetSelection()
|
|
ClosePage(existing)
|
|
-- restore the selection if it changed
|
|
if current ~= notebook:GetSelection() then
|
|
notebook:SetSelection(current)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
fileDialog:Destroy()
|
|
return saved
|
|
end
|
|
|
|
function SaveAll(quiet)
|
|
for _, document in pairs(openDocuments) do
|
|
local editor = document.editor
|
|
local filePath = document.filePath
|
|
|
|
if (document.isModified or not document.filePath) -- need to save
|
|
and (document.filePath or not quiet) then -- have path or can ask user
|
|
SaveFile(editor, filePath) -- will call SaveFileAs if necessary
|
|
end
|
|
end
|
|
end
|
|
|
|
local function removePage(index)
|
|
local prevIndex = nil
|
|
local nextIndex = nil
|
|
|
|
-- try to preserve old selection
|
|
local selectIndex = notebook:GetSelection()
|
|
selectIndex = selectIndex ~= index and selectIndex
|
|
|
|
local delid = nil
|
|
for id, document in pairsSorted(openDocuments,
|
|
function(a, b) -- sort by document index
|
|
return openDocuments[a].index < openDocuments[b].index
|
|
end) do
|
|
local wasselected = document.index == selectIndex
|
|
if document.index < index then
|
|
prevIndex = document.index
|
|
elseif document.index == index then
|
|
delid = id
|
|
document.editor:Destroy()
|
|
elseif document.index > index then
|
|
document.index = document.index - 1
|
|
if nextIndex == nil then
|
|
nextIndex = document.index
|
|
end
|
|
end
|
|
if (wasselected) then
|
|
selectIndex = document.index
|
|
end
|
|
end
|
|
|
|
if (delid) then
|
|
openDocuments[delid] = nil
|
|
end
|
|
|
|
notebook:RemovePage(index)
|
|
|
|
if selectIndex then
|
|
notebook:SetSelection(selectIndex)
|
|
elseif nextIndex then
|
|
notebook:SetSelection(nextIndex)
|
|
elseif prevIndex then
|
|
notebook:SetSelection(prevIndex)
|
|
end
|
|
|
|
-- need to set editor selection as it's called *after* PAGE_CHANGED event
|
|
SetEditorSelection()
|
|
end
|
|
|
|
function ClosePage(selection)
|
|
local editor = GetEditor(selection)
|
|
local id = editor:GetId()
|
|
if SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL then
|
|
DynamicWordsRemoveAll(editor)
|
|
local debugger = ide.debugger
|
|
-- check if the window with the scratchpad running is being closed
|
|
if debugger and debugger.scratchpad and debugger.scratchpad.editors
|
|
and debugger.scratchpad.editors[editor] then
|
|
DebuggerScratchpadOff()
|
|
end
|
|
-- check if the debugger is running and is using the current window;
|
|
-- abort the debugger if the current marker is in the window being closed
|
|
if debugger and debugger.server and
|
|
(editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE) >= 0) then
|
|
debugger.terminate()
|
|
end
|
|
PackageEventHandle("onEditorClose", editor)
|
|
removePage(ide.openDocuments[id].index)
|
|
|
|
-- disable full screen if the last tab is closed
|
|
if not (notebook:GetSelection() >= 0) then ShowFullScreen(false) end
|
|
end
|
|
end
|
|
|
|
function CloseAllPagesExcept(selection)
|
|
local toclose = {}
|
|
for _, document in pairs(ide.openDocuments) do
|
|
table.insert(toclose, document.index)
|
|
end
|
|
|
|
table.sort(toclose)
|
|
|
|
-- close pages for those files that match the project in the reverse order
|
|
-- (as ids shift when pages are closed)
|
|
for i = #toclose, 1, -1 do
|
|
if toclose[i] ~= selection then ClosePage(toclose[i]) end
|
|
end
|
|
end
|
|
|
|
-- Show a dialog to save a file before closing editor.
|
|
-- returns wxID_YES, wxID_NO, or wxID_CANCEL if allow_cancel
|
|
function SaveModifiedDialog(editor, allow_cancel)
|
|
local result = wx.wxID_NO
|
|
local id = editor:GetId()
|
|
local document = openDocuments[id]
|
|
local filePath = document.filePath
|
|
local fileName = document.fileName
|
|
if document.isModified then
|
|
local message = TR("Do you want to save the changes to '%s'?")
|
|
:format(fileName or ide.config.default.name)
|
|
local dlg_styles = wx.wxYES_NO + wx.wxCENTRE + wx.wxICON_QUESTION
|
|
if allow_cancel then dlg_styles = dlg_styles + wx.wxCANCEL end
|
|
local dialog = wx.wxMessageDialog(ide.frame, message,
|
|
TR("Save Changes?"),
|
|
dlg_styles)
|
|
result = dialog:ShowModal()
|
|
dialog:Destroy()
|
|
if result == wx.wxID_YES then
|
|
if not SaveFile(editor, filePath) then
|
|
return wx.wxID_CANCEL -- cancel if canceled save dialog
|
|
end
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function SaveOnExit(allow_cancel)
|
|
for _, document in pairs(openDocuments) do
|
|
if (SaveModifiedDialog(document.editor, allow_cancel) == wx.wxID_CANCEL) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- if all documents have been saved or refused to save, then mark those that
|
|
-- are still modified as not modified (they don't need to be saved)
|
|
-- to keep their tab names correct
|
|
for id, document in pairs(openDocuments) do
|
|
if document.isModified then SetDocumentModified(id, false) end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- circle through "fold all" => "hide base lines" => "unfold all"
|
|
function FoldSome()
|
|
local editor = GetEditor()
|
|
editor:Colourise(0, -1) -- update doc's folding info
|
|
local foldall = false -- at least on header unfolded => fold all
|
|
local hidebase = false -- at least one base is visible => hide all
|
|
|
|
for ln = 0, editor.LineCount - 1 do
|
|
local foldRaw = editor:GetFoldLevel(ln)
|
|
local foldLvl = foldRaw % 4096
|
|
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
|
|
|
|
-- at least one header is expanded
|
|
foldall = foldall or (foldHdr and editor:GetFoldExpanded(ln))
|
|
|
|
-- at least one base can be hidden
|
|
hidebase = hidebase or (
|
|
not foldHdr
|
|
and ln > 1 -- first line can't be hidden, so ignore it
|
|
and foldLvl == wxstc.wxSTC_FOLDLEVELBASE
|
|
and bit.band(foldRaw, wxstc.wxSTC_FOLDLEVELWHITEFLAG) == 0
|
|
and editor:GetLineVisible(ln))
|
|
end
|
|
|
|
-- shows lines; this doesn't change fold status for folded lines
|
|
if not foldall and not hidebase then editor:ShowLines(0, editor.LineCount-1) end
|
|
|
|
for ln = 0, editor.LineCount-1 do
|
|
local foldRaw = editor:GetFoldLevel(ln)
|
|
local foldLvl = foldRaw % 4096
|
|
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
|
|
|
|
if foldall then
|
|
if foldHdr and editor:GetFoldExpanded(ln) then
|
|
editor:ToggleFold(ln)
|
|
end
|
|
elseif hidebase then
|
|
if not foldHdr and (foldLvl == wxstc.wxSTC_FOLDLEVELBASE) then
|
|
editor:HideLines(ln, ln)
|
|
end
|
|
else -- unfold all
|
|
if foldHdr and not editor:GetFoldExpanded(ln) then
|
|
editor:ToggleFold(ln)
|
|
end
|
|
end
|
|
end
|
|
editor:EnsureCaretVisible()
|
|
end
|
|
|
|
function EnsureRangeVisible(posStart, posEnd)
|
|
local editor = GetEditor()
|
|
if posStart > posEnd then
|
|
posStart, posEnd = posEnd, posStart
|
|
end
|
|
|
|
local lineStart = editor:LineFromPosition(posStart)
|
|
local lineEnd = editor:LineFromPosition(posEnd)
|
|
for line = lineStart, lineEnd do
|
|
editor:EnsureVisibleEnforcePolicy(line)
|
|
end
|
|
end
|
|
|
|
function SetAllEditorsReadOnly(enable)
|
|
for _, document in pairs(openDocuments) do
|
|
document.editor:SetReadOnly(enable)
|
|
end
|
|
end
|
|
|
|
-----------------
|
|
-- Debug related
|
|
|
|
function ClearAllCurrentLineMarkers()
|
|
for _, document in pairs(openDocuments) do
|
|
document.editor:MarkerDeleteAll(CURRENT_LINE_MARKER)
|
|
end
|
|
end
|
|
|
|
-- remove shebang line (#!) as it throws a compilation error as
|
|
-- loadstring() doesn't allow it even though lua/loadfile accepts it.
|
|
-- replace with a new line to keep the number of lines the same.
|
|
function StripShebang(code) return (code:gsub("^#!.-\n", "\n")) end
|
|
|
|
local compileOk, compileTotal = 0, 0
|
|
function CompileProgram(editor, params)
|
|
local params = { jumponerror = (params or {}).jumponerror ~= false,
|
|
reportstats = (params or {}).reportstats ~= false }
|
|
local id = editor:GetId()
|
|
local filePath = DebuggerMakeFileName(editor, openDocuments[id].filePath)
|
|
local func, err = loadstring(StripShebang(editor:GetText()), '@'..filePath)
|
|
local line = not func and tonumber(err:match(":(%d+)%s*:")) or nil
|
|
|
|
if ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT) then ClearOutput() end
|
|
|
|
compileTotal = compileTotal + 1
|
|
if func then
|
|
compileOk = compileOk + 1
|
|
if params.reportstats then
|
|
DisplayOutputLn(TR("Compilation successful; %.0f%% success rate (%d/%d).")
|
|
:format(compileOk/compileTotal*100, compileOk, compileTotal))
|
|
end
|
|
else
|
|
DisplayOutputLn(TR("Compilation error").." "..TR("on line %d"):format(line)..":")
|
|
DisplayOutputLn((err:gsub("\n$", "")))
|
|
-- check for escapes invalid in LuaJIT/Lua 5.2 that are allowed in Lua 5.1
|
|
if err:find('invalid escape sequence') then
|
|
local s = editor:GetLine(line-1)
|
|
local cleaned = s
|
|
:gsub('\\[abfnrtv\\"\']', ' ')
|
|
:gsub('(\\x[0-9a-fA-F][0-9a-fA-F])', function(s) return string.rep(' ', #s) end)
|
|
:gsub('(\\%d%d?%d?)', function(s) return string.rep(' ', #s) end)
|
|
:gsub('(\\z%s*)', function(s) return string.rep(' ', #s) end)
|
|
local invalid = cleaned:find("\\")
|
|
if invalid then
|
|
DisplayOutputLn(TR("Consider removing backslash from escape sequence '%s'.")
|
|
:format(s:sub(invalid,invalid+1)))
|
|
end
|
|
end
|
|
if line and params.jumponerror and line-1 ~= editor:GetCurrentLine() then
|
|
editor:GotoLine(line-1)
|
|
end
|
|
end
|
|
|
|
return func ~= nil -- return true if it compiled ok
|
|
end
|
|
|
|
------------------
|
|
-- Save & Close
|
|
|
|
function SaveIfModified(editor)
|
|
local id = editor:GetId()
|
|
if openDocuments[id].isModified then
|
|
local saved = false
|
|
if not openDocuments[id].filePath then
|
|
local ret = wx.wxMessageBox(
|
|
TR("You must save the program first.").."\n"..TR("Press cancel to abort."),
|
|
TR("Save file?"), wx.wxOK + wx.wxCANCEL + wx.wxCENTRE, ide.frame)
|
|
if ret == wx.wxOK then
|
|
saved = SaveFileAs(editor)
|
|
end
|
|
else
|
|
saved = SaveFile(editor, openDocuments[id].filePath)
|
|
end
|
|
|
|
if saved then
|
|
openDocuments[id].isModified = false
|
|
else
|
|
return false -- not saved
|
|
end
|
|
end
|
|
|
|
return true -- saved
|
|
end
|
|
|
|
function GetOpenFiles()
|
|
local opendocs = {}
|
|
for _, document in pairs(ide.openDocuments) do
|
|
if (document.filePath) then
|
|
local wxfname = wx.wxFileName(document.filePath)
|
|
wxfname:Normalize()
|
|
|
|
table.insert(opendocs, {filename=wxfname:GetFullPath(),
|
|
id=document.index, cursorpos = document.editor:GetCurrentPos()})
|
|
end
|
|
end
|
|
|
|
-- to keep tab order
|
|
table.sort(opendocs,function(a,b) return (a.id < b.id) end)
|
|
|
|
local id = GetEditor()
|
|
id = id and id:GetId()
|
|
return opendocs, {index = (id and openDocuments[id].index or 0)}
|
|
end
|
|
|
|
function SetOpenFiles(nametab,params)
|
|
for _, doc in ipairs(nametab) do
|
|
local editor = LoadFile(doc.filename,nil,true,true) -- skip selection
|
|
if editor then editor:GotoPosDelayed(doc.cursorpos or 0) end
|
|
end
|
|
notebook:SetSelection(params and params.index or 0)
|
|
SetEditorSelection()
|
|
end
|
|
|
|
local beforeFullScreenPerspective
|
|
local statusbarShown
|
|
|
|
function ShowFullScreen(setFullScreen)
|
|
if setFullScreen then
|
|
beforeFullScreenPerspective = uimgr:SavePerspective()
|
|
|
|
local panes = frame.uimgr:GetAllPanes()
|
|
for index = 0, panes:GetCount()-1 do
|
|
local name = panes:Item(index).name
|
|
if name ~= "notebook" then frame.uimgr:GetPane(name):Hide() end
|
|
end
|
|
uimgr:Update()
|
|
SetEditorSelection() -- make sure the focus is on the editor
|
|
elseif beforeFullScreenPerspective then
|
|
uimgr:LoadPerspective(beforeFullScreenPerspective, true)
|
|
beforeFullScreenPerspective = nil
|
|
end
|
|
|
|
-- On OSX, status bar is not hidden when switched to
|
|
-- full screen: http://trac.wxwidgets.org/ticket/14259; do manually.
|
|
-- need to turn off before showing full screen and turn on after,
|
|
-- otherwise the window is restored incorrectly and is reduced in size.
|
|
if ide.osname == 'Macintosh' and setFullScreen then
|
|
statusbarShown = frame:GetStatusBar():IsShown()
|
|
frame:GetStatusBar():Hide()
|
|
end
|
|
|
|
-- protect from systems that don't have ShowFullScreen (GTK on linux?)
|
|
pcall(function() frame:ShowFullScreen(setFullScreen) end)
|
|
|
|
if ide.osname == 'Macintosh' and not setFullScreen then
|
|
if statusbarShown then
|
|
frame:GetStatusBar():Show()
|
|
-- refresh AuiManager as the statusbar may be shown below the border
|
|
uimgr:Update()
|
|
end
|
|
end
|
|
end
|
|
|
|
function ProjectConfig(dir, config)
|
|
if config then ide.session.projects[dir] = config
|
|
else return unpack(ide.session.projects[dir] or {}) end
|
|
end
|
|
|
|
function SetOpenTabs(params)
|
|
local recovery, nametab = LoadSafe("return "..params.recovery)
|
|
if not recovery then
|
|
DisplayOutputLn(TR("Can't process auto-recovery record; invalid format: %s."):format(nametab))
|
|
return
|
|
end
|
|
DisplayOutputLn(TR("Found auto-recovery record and restored saved session."))
|
|
for _,doc in ipairs(nametab) do
|
|
local editor = doc.filename and LoadFile(doc.filename,nil,true,true) or NewFile()
|
|
local opendoc = openDocuments[editor:GetId()]
|
|
if doc.content then
|
|
notebook:SetPageText(opendoc.index, doc.tabname)
|
|
editor:SetText(doc.content)
|
|
if doc.filename and opendoc.modTime and doc.modified < opendoc.modTime:GetTicks() then
|
|
DisplayOutputLn(TR("File '%s' has more recent timestamp than restored '%s'; please review before saving.")
|
|
:format(doc.filename, doc.tabname))
|
|
end
|
|
end
|
|
editor:GotoPosDelayed(doc.cursorpos or 0)
|
|
end
|
|
notebook:SetSelection(params and params.index or 0)
|
|
SetEditorSelection()
|
|
end
|
|
|
|
local function getOpenTabs()
|
|
local opendocs = {}
|
|
for _, document in pairs(ide.openDocuments) do
|
|
table.insert(opendocs, {
|
|
filename = document.filePath,
|
|
tabname = notebook:GetPageText(document.index),
|
|
modified = document.modTime and document.modTime:GetTicks(), -- get number of seconds
|
|
content = document.isModified and document.editor:GetText() or nil,
|
|
id = document.index, cursorpos = document.editor:GetCurrentPos()})
|
|
end
|
|
|
|
-- to keep tab order
|
|
table.sort(opendocs, function(a,b) return (a.id < b.id) end)
|
|
|
|
local id = GetEditor()
|
|
id = id and id:GetId()
|
|
return opendocs, {index = (id and openDocuments[id].index or 0)}
|
|
end
|
|
|
|
function SetAutoRecoveryMark()
|
|
ide.session.lastupdated = os.time()
|
|
end
|
|
|
|
local function saveAutoRecovery(event)
|
|
local lastupdated = ide.session.lastupdated
|
|
if not ide.config.autorecoverinactivity or not lastupdated then return end
|
|
if lastupdated < (ide.session.lastsaved or 0) then return end
|
|
|
|
local now = os.time()
|
|
if lastupdated + ide.config.autorecoverinactivity > now then return end
|
|
|
|
-- find all open modified files and save them
|
|
local opentabs, params = getOpenTabs()
|
|
if #opentabs > 0 then
|
|
params.recovery = require('mobdebug').line(opentabs, {comment = false})
|
|
SettingsSaveAll()
|
|
SettingsSaveFileSession({}, params)
|
|
ide.settings:Flush()
|
|
end
|
|
ide.session.lastsaved = now
|
|
ide.frame.statusBar:SetStatusText(
|
|
TR("Saved auto-recover at %s."):format(os.date("%H:%M:%S")), 1)
|
|
end
|
|
|
|
local function fastWrap(func, ...)
|
|
-- ignore SetEditorSelection that is not needed as `func` may work on
|
|
-- multipe files, but editor needs to be selected once.
|
|
local SES = SetEditorSelection
|
|
SetEditorSelection = function() end
|
|
func(...)
|
|
SetEditorSelection = SES
|
|
end
|
|
|
|
function StoreRestoreProjectTabs(curdir, newdir)
|
|
local win = ide.osname == 'Windows'
|
|
local interpreter = ide.interpreter.fname
|
|
local current, closing, restore = notebook:GetSelection(), 0, false
|
|
|
|
if ide.osname ~= 'Macintosh' then notebook:Freeze() end
|
|
|
|
if curdir and #curdir > 0 then
|
|
local lowcurdir = win and string.lower(curdir) or curdir
|
|
local lownewdir = win and string.lower(newdir) or newdir
|
|
local projdocs, closdocs = {}, {}
|
|
for _, document in ipairs(GetOpenFiles()) do
|
|
local dpath = win and string.lower(document.filename) or document.filename
|
|
-- check if the filename is in the same folder
|
|
if dpath:find(lowcurdir, 1, true) == 1
|
|
and dpath:find("^[\\/]", #lowcurdir+1) then
|
|
table.insert(projdocs, document)
|
|
closing = closing + (document.id < current and 1 or 0)
|
|
-- only close if the file is not in new project as it would be reopened
|
|
if not dpath:find(lownewdir, 1, true)
|
|
or not dpath:find("^[\\/]", #lownewdir+1) then
|
|
table.insert(closdocs, document)
|
|
end
|
|
elseif document.id == current then restore = true end
|
|
end
|
|
|
|
-- adjust for the number of closing tabs on the left from the current one
|
|
current = current - closing
|
|
|
|
-- save opened files from this project
|
|
ProjectConfig(curdir, {projdocs,
|
|
{index = notebook:GetSelection() - current, interpreter = interpreter}})
|
|
|
|
-- close pages for those files that match the project in the reverse order
|
|
-- (as ids shift when pages are closed)
|
|
for i = #closdocs, 1, -1 do fastWrap(ClosePage, closdocs[i].id) end
|
|
end
|
|
|
|
local files, params = ProjectConfig(newdir)
|
|
if files then
|
|
-- provide fake index so that it doesn't activate it as the index may be not
|
|
-- quite correct if some of the existing files are already open in the IDE.
|
|
fastWrap(SetOpenFiles, files, {index = #files + notebook:GetPageCount()})
|
|
end
|
|
|
|
if params and params.interpreter then
|
|
ProjectSetInterpreter(params.interpreter) -- set the interpreter
|
|
end
|
|
|
|
if ide.osname ~= 'Macintosh' then notebook:Thaw() end
|
|
|
|
local index = params and params.index
|
|
if notebook:GetPageCount() == 0 then NewFile()
|
|
elseif restore and current >= 0 then notebook:SetSelection(current)
|
|
elseif index and index >= 0 and files[index+1] then
|
|
-- move the editor tab to the front with the file from the config
|
|
LoadFile(files[index+1].filename, nil, true)
|
|
SetEditorSelection() -- activate the editor in the active tab
|
|
end
|
|
|
|
-- remove current config as it may change; the current configuration is
|
|
-- stored with the general config.
|
|
-- The project configuration will be updated when the project is changed.
|
|
ProjectConfig(newdir, {})
|
|
end
|
|
|
|
local function closeWindow(event)
|
|
-- if the app is already exiting, then help it exit; wxwidgets on Windows
|
|
-- is supposed to report Shutdown/logoff events by setting CanVeto() to
|
|
-- false, but it doesn't happen. We simply leverage the fact that
|
|
-- CloseWindow is called several times in this case and exit. Similar
|
|
-- behavior has been also seen on Linux, so this logic applies everywhere.
|
|
if ide.exitingProgram then os.exit() end
|
|
|
|
ide.exitingProgram = true -- don't handle focus events
|
|
|
|
if not SaveOnExit(event:CanVeto()) then
|
|
event:Veto()
|
|
ide.exitingProgram = false
|
|
return
|
|
end
|
|
|
|
ShowFullScreen(false)
|
|
|
|
PackageEventHandle("onAppClose")
|
|
|
|
-- first need to detach all processes IDE has launched as the current
|
|
-- process is likely to terminate before child processes are terminated,
|
|
-- which may lead to a crash when EVT_END_PROCESS event is called.
|
|
DetachChildProcess()
|
|
DebuggerShutdown()
|
|
|
|
SettingsSaveAll()
|
|
ide.settings:Flush()
|
|
|
|
do -- hide all floating panes first
|
|
local panes = frame.uimgr:GetAllPanes()
|
|
for index = 0, panes:GetCount()-1 do
|
|
local pane = frame.uimgr:GetPane(panes:Item(index).name)
|
|
if pane:IsFloating() then pane:Hide() end
|
|
end
|
|
end
|
|
frame.uimgr:Update() -- hide floating panes
|
|
frame.uimgr:UnInit()
|
|
frame:Hide() -- hide the main frame while the IDE exits
|
|
|
|
if ide.session.timer then ide.session.timer:Stop() end
|
|
|
|
event:Skip()
|
|
end
|
|
frame:Connect(wx.wxEVT_CLOSE_WINDOW, closeWindow)
|
|
|
|
frame:Connect(wx.wxEVT_TIMER, saveAutoRecovery)
|
|
|
|
-- in the presence of wxAuiToolbar, when (1) the app gets focus,
|
|
-- (2) a floating panel is closed or (3) a toolbar dropdown is closed,
|
|
-- the focus is always on the toolbar when the app gets focus,
|
|
-- so to restore the focus correctly, need to track where the control is
|
|
-- and to set the focus to the last element that had focus.
|
|
-- it would be easier to track KILL_FOCUS events, but controls on OSX
|
|
-- don't always generate KILL_FOCUS events (see relevant wxwidgets
|
|
-- tickets: http://trac.wxwidgets.org/ticket/14142
|
|
-- and http://trac.wxwidgets.org/ticket/14269)
|
|
|
|
local infocus
|
|
ide.editorApp:Connect(wx.wxEVT_SET_FOCUS, function(event)
|
|
local win = ide.frame:FindFocus()
|
|
if win then
|
|
local class = win:GetClassInfo():GetClassName()
|
|
-- don't set focus on the main frame or toolbar
|
|
if infocus and (class == 'wxAuiToolBar' or class == 'wxFrame') then
|
|
infocus:SetFocus()
|
|
return
|
|
end
|
|
|
|
-- keep track of the current control in focus, but only on the main frame
|
|
local grandparent = win:GetGrandParent()
|
|
if grandparent and grandparent:GetId() == ide.frame:GetId() then
|
|
infocus = win
|
|
end
|
|
end
|
|
|
|
event:Skip()
|
|
end)
|
|
|
|
ide.editorApp:Connect(wx.wxEVT_ACTIVATE_APP,
|
|
function(event)
|
|
if not ide.exitingProgram then
|
|
if ide.osname == 'Macintosh' and infocus and event:GetActive() then
|
|
-- restore focus to the last element that received it;
|
|
-- wrap into pcall in case the element has disappeared
|
|
-- while the application was out of focus
|
|
pcall(function() infocus:SetFocus() end)
|
|
end
|
|
|
|
local event = event:GetActive() and "onAppFocusSet" or "onAppFocusLost"
|
|
PackageEventHandle(event, ide.editorApp)
|
|
end
|
|
event:Skip()
|
|
end)
|
|
|
|
if ide.config.autorecoverinactivity then
|
|
ide.session.timer = wx.wxTimer(frame)
|
|
-- check at least 5s to be never more than 5s off
|
|
ide.session.timer:Start(math.min(5, ide.config.autorecoverinactivity)*1000)
|
|
end
|
|
|
|
function PaneFloatToggle(window)
|
|
local pane = uimgr:GetPane(window)
|
|
if pane:IsFloating() then
|
|
pane:Dock()
|
|
else
|
|
pane:Float()
|
|
pane:FloatingPosition(pane.window:GetScreenPosition())
|
|
pane:FloatingSize(pane.window:GetSize())
|
|
end
|
|
uimgr:Update()
|
|
end
|