-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local ide = ide -- --------------------------------------------------------------------------- -- Create the Edit menu and attach the callback functions local frame = ide.frame local menuBar = frame.menuBar local editMenu = ide:MakeMenu { { ID_UNDO, TR("&Undo")..KSC(ID_UNDO), TR("Undo last edit") }, { ID_REDO, TR("&Redo")..KSC(ID_REDO), TR("Redo last edit undone") }, { }, { ID_CUT, TR("Cu&t")..KSC(ID_CUT), TR("Cut selected text to clipboard") }, { ID_COPY, TR("&Copy")..KSC(ID_COPY), TR("Copy selected text to clipboard") }, { ID_PASTE, TR("&Paste")..KSC(ID_PASTE), TR("Paste text from the clipboard") }, { ID_SELECTALL, TR("Select &All")..KSC(ID_SELECTALL), TR("Select all text in the editor") }, { }, { ID_SHOWTOOLTIP, TR("Show &Tooltip")..KSC(ID_SHOWTOOLTIP), TR("Show tooltip for current position; place cursor after opening bracket of function") }, { ID_AUTOCOMPLETE, TR("Complete &Identifier")..KSC(ID_AUTOCOMPLETE), TR("Complete the current identifier") }, { ID_AUTOCOMPLETEENABLE, TR("Auto Complete Identifiers")..KSC(ID_AUTOCOMPLETEENABLE), TR("Auto complete while typing"), wx.wxITEM_CHECK }, { }, { ID_SOURCE, TR("Source"), "", { { ID_COMMENT, TR("C&omment/Uncomment")..KSC(ID_COMMENT), TR("Comment or uncomment current or selected lines") }, { ID_REINDENT, TR("Correct &Indentation")..KSC(ID_REINDENT), TR("Re-indent selected lines") }, { ID_FOLD, TR("&Fold/Unfold All")..KSC(ID_FOLD), TR("Fold or unfold all code folds") }, { ID_FOLDLINE, TR("Fold/Unfold Current &Line")..KSC(ID_FOLDLINE), TR("Fold or unfold current line") }, { ID_SORT, TR("&Sort")..KSC(ID_SORT), TR("Sort selected lines") }, } }, { ID_BOOKMARK, TR("Bookmark"), "", { { ID_BOOKMARKTOGGLE, TR("Toggle Bookmark")..KSC(ID_BOOKMARKTOGGLE), TR("Toggle bookmark") }, { ID_BOOKMARKNEXT, TR("Go To Next Bookmark")..KSC(ID_BOOKMARKNEXT) }, { ID_BOOKMARKPREV, TR("Go To Previous Bookmark")..KSC(ID_BOOKMARKPREV) }, } }, { }, { ID_PREFERENCES, TR("Preferences"), "", { { ID_PREFERENCESSYSTEM, TR("Settings: System")..KSC(ID_PREFERENCESSYSTEM) }, { ID_PREFERENCESUSER, TR("Settings: User")..KSC(ID_PREFERENCESUSER) }, } }, } menuBar:Append(editMenu, TR("&Edit")) editMenu:Check(ID_AUTOCOMPLETEENABLE, ide.config.autocomplete) local function getCtrlWithFocus(edType) local ctrl = ide:GetMainFrame():FindFocus() return ctrl and ctrl:GetClassInfo():GetClassName() == edType and ctrl:DynamicCast(edType) or nil end local function onUpdateUIEditorInFocus(event) event:Enable(ide:GetEditorWithFocus(ide:GetEditor()) ~= nil) end local function onUpdateUIEditMenu(event) local menu_id = event:GetId() local editor = ide:GetEditorWithFocus() if editor == nil then local editor = getCtrlWithFocus("wxTextCtrl") event:Enable(editor and ( menu_id == ID_PASTE and editor:CanPaste() or menu_id == ID_UNDO and editor:CanUndo() or menu_id == ID_REDO and editor:CanRedo() or menu_id == ID_CUT and editor:CanCut() or menu_id == ID_COPY and editor:CanCopy() or menu_id == ID_SELECTALL and true ) or false) return end local alwaysOn = { [ID_SELECTALL] = true, -- allow Cut and Copy commands as these work on a line if no selection [ID_COPY] = true, [ID_CUT] = true, } local enable = -- pasting is allowed when the document is not read-only and the selection -- (if any) has no protected text; since pasting handles protected text, -- use GetReadOnly() instead of CanPaste() menu_id == ID_PASTE and (not editor:GetReadOnly()) or menu_id == ID_UNDO and editor:CanUndo() or menu_id == ID_REDO and editor:CanRedo() or alwaysOn[menu_id] event:Enable(enable) end local function onEditMenu(event) local menu_id = event:GetId() local editor = ide:GetEditorWithFocus() if editor == nil then local editor = getCtrlWithFocus("wxTextCtrl") if not editor or not ( menu_id == ID_PASTE and editor:Paste() or menu_id == ID_UNDO and editor:Undo() or menu_id == ID_REDO and editor:Redo() or menu_id == ID_CUT and editor:Cut() or menu_id == ID_COPY and editor:Copy() or menu_id == ID_SELECTALL and editor:SetSelection(-1, -1) or true ) then event:Skip() end return end if PackageEventHandle("onEditorAction", editor, event) == false then return end local copytext if (menu_id == ID_CUT or menu_id == ID_COPY) and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then local main = editor:GetMainSelection() copytext = editor:GetTextRangeDyn(editor:GetSelectionNStart(main), editor:GetSelectionNEnd(main)) for s = 0, editor:GetSelections()-1 do if copytext ~= editor:GetTextRangeDyn(editor:GetSelectionNStart(s), editor:GetSelectionNEnd(s)) then copytext = nil break end end end local spos, epos = editor:GetSelectionStart(), editor:GetSelectionEnd() if menu_id == ID_CUT then if spos == epos then if ide.config.editor.linecopy then editor:LineCopy() end else editor:CopyDyn() end if spos == epos and ide.config.editor.linecopy then local line = editor:LineFromPosition(spos) spos, epos = editor:PositionFromLine(line), editor:PositionFromLine(line+1) editor:SetSelectionStart(spos) editor:SetSelectionEnd(epos) end if spos ~= epos then editor:ClearAny() end elseif menu_id == ID_COPY then if spos == epos then if ide.config.editor.linecopy then editor:LineCopy() end else editor:CopyDyn() end elseif menu_id == ID_PASTE then -- first clear the text in case there is any hidden markup if spos ~= epos then editor:ClearAny() end editor:PasteDyn() elseif menu_id == ID_SELECTALL then editor:SelectAll() elseif menu_id == ID_UNDO then editor:Undo() elseif menu_id == ID_REDO then editor:Redo() end if copytext then editor:CopyText(#copytext, copytext) end end for _, event in pairs({ID_CUT, ID_COPY, ID_PASTE, ID_SELECTALL, ID_UNDO, ID_REDO}) do frame:Connect(event, wx.wxEVT_COMMAND_MENU_SELECTED, onEditMenu) frame:Connect(event, wx.wxEVT_UPDATE_UI, onUpdateUIEditMenu) end for _, event in pairs({ ID_BOOKMARKTOGGLE, ID_BOOKMARKNEXT, ID_BOOKMARKPREV, ID_AUTOCOMPLETE, ID_SORT, ID_REINDENT, ID_SHOWTOOLTIP, }) do frame:Connect(event, wx.wxEVT_UPDATE_UI, onUpdateUIEditorInFocus) end frame:Connect(ID_COMMENT, wx.wxEVT_UPDATE_UI, function(event) local editor = ide:GetEditorWithFocus(ide:GetEditor()) event:Enable(editor ~= nil and ide:IsValidProperty(editor, "spec") and editor.spec and editor.spec.linecomment and true or false) end) local function generateConfigMessage(type) return ([==[--[[-- Use this file to specify **%s** preferences. Review [examples](+%s) or check [online documentation](%s) for details. --]]-- ]==]) :format(type, MergeFullPath(ide.editorFilename, "../cfg/user-sample.lua"), "http://studio.zerobrane.com/documentation.html") end frame:Connect(ID_PREFERENCESSYSTEM, wx.wxEVT_COMMAND_MENU_SELECTED, function () local editor = LoadFile(ide.configs.system) if editor and editor:GetLength() == 0 then editor:AddTextDyn(generateConfigMessage("System")) end end) frame:Connect(ID_PREFERENCESUSER, wx.wxEVT_COMMAND_MENU_SELECTED, function () local editor = LoadFile(ide.configs.user) if editor and editor:GetLength() == 0 then editor:AddTextDyn(generateConfigMessage("User")) end end) frame:Connect(ID_PREFERENCESUSER, wx.wxEVT_UPDATE_UI, function (event) event:Enable(ide.configs.user ~= nil) end) frame:Connect(ID_CLEARDYNAMICWORDS, wx.wxEVT_COMMAND_MENU_SELECTED, function () DynamicWordsReset() end) frame:Connect(ID_SHOWTOOLTIP, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) local editor = ide:GetEditor() if (editor:CallTipActive()) then editor:CallTipCancel() return end EditorCallTip(editor, editor:GetCurrentPos()) end) frame:Connect(ID_AUTOCOMPLETE, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) EditorAutoComplete(ide:GetEditor()) end) frame:Connect(ID_AUTOCOMPLETEENABLE, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) ide.config.autocomplete = event:IsChecked() end) frame:Connect(ID_COMMENT, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) local editor = ide:GetEditor() local lc = editor.spec.linecomment if not lc then return end -- for multi-line selection, always start the first line at the beginning local ssel, esel = editor:GetSelectionStart(), editor:GetSelectionEnd() local sline = editor:LineFromPosition(ssel) local eline = editor:LineFromPosition(esel) local sel = ssel ~= esel local rect = editor:SelectionIsRectangle() local qlc = lc:gsub(".", "%%%1") -- figure out how to toggle comments; if there is at least one non-empty -- line that doesn't start with a comment, need to comment local comment = false for line = sline, eline do local pos = sel and (sline == eline or rect) and ssel-editor:PositionFromLine(sline)+1 or 1 local text = editor:GetLineDyn(line) local _, cpos = text:find("^%s*"..qlc, pos) if not cpos and text:find("%S") -- ignore last line when the end of selection is at the first position and (line == sline or line < eline or esel-editor:PositionFromLine(line) > 0) then comment = true break end end local linetoggle = ide.config.editor.commentlinetoggle editor:BeginUndoAction() -- go last to first as selection positions we captured may be affected -- by text changes for line = eline, sline, -1 do local pos = sel and (sline == eline or rect) and ssel-editor:PositionFromLine(sline)+1 or 1 local text = editor:GetLineDyn(line) local validline = (line == sline or line < eline or esel-editor:PositionFromLine(line) > 0) local _, cpos = text:find("^%s*"..qlc, pos) if (linetoggle or not comment) and cpos and validline then editor:DeleteRange(cpos-#lc+editor:PositionFromLine(line), #lc) elseif (linetoggle or comment) and text:find("%S") and validline then editor:SetTargetStart(pos+editor:PositionFromLine(line)-1) editor:SetTargetEnd(editor:GetTargetStart()) editor:ReplaceTarget(lc) end end editor:EndUndoAction() end) local function processSelection(editor, func) local text = editor:GetSelectedTextDyn() local line = editor:GetCurrentLine() local posinline = editor:GetCurrentPos() - editor:PositionFromLine(line) if #text == 0 then editor:SelectAll() text = editor:GetSelectedTextDyn() end local wholeline = text:find("\n$") local buf = {} for ln in string.gmatch(text..(wholeline and "" or "\n"), "(.-\r?\n)") do table.insert(buf, ln) end if #buf > 0 then if func then func(buf) end -- add new line at the end if it was there local newtext = table.concat(buf, ""):gsub("(\r?\n)$", wholeline and "%1" or "") -- straightforward editor:ReplaceSelection() doesn't work reliably as -- it sometimes doubles the context when the entire file is selected. -- this seems like Scintilla issue, so use ReplaceTarget instead. -- Since this doesn't work with rectangular selection, which -- ReplaceSelection should handle (after wxwidgets 3.x upgrade), this -- will need to be revisited when ReplaceSelection is updated. if newtext ~= text then editor:BeginUndoAction() -- if there is at least one marker, then use a different mechanism to preserve them -- simply saving markers, replacing text, and reapplying markers doesn't work as -- they get reset after `undo/redo` operations. local ssel, esel = editor:GetSelectionStart(), editor:GetSelectionEnd() local sline = editor:LineFromPosition(ssel) local eline = editor:LineFromPosition(esel) if #editor:MarkerGetAll(nil, sline, eline) > 0 then for line = #buf, 1, -1 do editor:SetTargetStart(line == 1 and ssel or editor:PositionFromLine(sline+line-1)) editor:SetTargetEnd(line == eline-sline+1 and esel or editor:GetLineEndPosition(sline+line-1)) editor:ReplaceTargetDyn((buf[line]:gsub("\r?\n$", ""))) end else editor:TargetFromSelection() editor:ReplaceTargetDyn(newtext) end editor:EndUndoAction() end end editor:GotoPosEnforcePolicy(math.min( editor:PositionFromLine(line)+posinline, editor:GetLineEndPosition(line))) end frame:Connect(ID_SORT, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) processSelection(ide:GetEditor(), table.sort) end) local function reIndent(editor, buf) local decindent, incindent = editor.spec.isdecindent, editor.spec.isincindent if not (decindent and incindent) then return end local edline = editor:LineFromPosition(editor:GetSelectionStart()) local indent = 0 local text = "" -- find the last non-empty line in the previous block (if any) for n = edline-1, 1, -1 do indent = editor:GetLineIndentation(n) text = editor:GetLineDyn(n) if text:match("[^\r\n]") then break end end local ut = editor:GetUseTabs() local tw = ut and editor:GetTabWidth() or editor:GetIndent() local indents = {} local isstatic = {} for line = 1, #buf+1 do local ls = editor:PositionFromLine(edline+line-1) local style = bit.band(editor:GetStyleAt(ls), ide.STYLEMASK) -- don't reformat multi-line comments or strings isstatic[line] = (editor.spec.iscomment[style] or editor.spec.isstring[style] or (MarkupIsAny and MarkupIsAny(style))) if not isstatic[line] or line == 1 or not isstatic[line-1] then local closed, blockend = decindent(text) local opened = incindent(text) -- ignore impact from initial block endings as they are already indented if line == 1 then blockend = 0 end -- this only needs to be done for 2, #buf+1; do it and get out when done if line > 1 then indents[line-1] = indents[line-1] - tw * closed end if line > #buf then break end indent = indent + tw * (opened - blockend) if indent < 0 then indent = 0 end end indents[line] = indent text = buf[line] end for line = 1, #buf do if not isstatic[line] then buf[line] = buf[line]:gsub("^[ \t]*", not buf[line]:match("%S") and "" or ut and ("\t"):rep(indents[line] / tw) or (" "):rep(indents[line])) end end end frame:Connect(ID_REINDENT, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) local editor = ide:GetEditor() processSelection(editor, function(buf) reIndent(editor, buf) end) end) local function canfold(event) local editor = ide:GetEditorWithFocus() event:Enable(editor and editor:CanFold() or false) end frame:Connect(ID_FOLD, wx.wxEVT_UPDATE_UI, canfold) frame:Connect(ID_FOLD, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) ide:GetEditorWithFocus():FoldSome() end) frame:Connect(ID_FOLDLINE, wx.wxEVT_UPDATE_UI, canfold) frame:Connect(ID_FOLDLINE, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) local editor = ide:GetEditorWithFocus() local current = editor:GetCurrentLine() editor:ToggleFold(current) -- move up to the parent line if the current one is not visible local visible = editor:GetLineVisible(current) if not visible and editor:GetFoldParent(current) ~= wx.wxNOT_FOUND then editor:LineUp() end end) local BOOKMARK_MARKER = StylesGetMarker("bookmark") frame:Connect(ID_BOOKMARKTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED, function() ide:GetEditor():BookmarkToggle() end) frame:Connect(ID_BOOKMARKNEXT, wx.wxEVT_COMMAND_MENU_SELECTED, function() ide:GetEditor():MarkerGotoNext(BOOKMARK_MARKER) end) frame:Connect(ID_BOOKMARKPREV, wx.wxEVT_COMMAND_MENU_SELECTED, function() ide:GetEditor():MarkerGotoPrev(BOOKMARK_MARKER) end)