Scite Merge On Change |
|
Useful for saving yourself from deleting recent changes if you have the same file opened multiple times, or when updating a repository when its files are already open.
The Unix version uses stat, diff, and diff3 to detect and merge changes.
I couldn't find an equivalent of stat in Windows, so the Windows version uses md5sum to detect changes instead; you'll need Windows ports of md5sum, diff, and diff3 [GnuWin32], and their bin directory needs to be in your PATH environment variable so that the script can execute them.
-- Will be replaced by a function for escaping shell strings, once we know know how local shellString = nil -- Will be replaced by a function for generating a string for a file that will change when that file changes. local fileState = nil local shell = os.getenv("SHELL") if shell then shell = shell:match("([^\\/]+)$") end if not shell then if not os.getenv("WinDir") then error("$SHELL is undefined, and this doesn't seem to be Windows.") end -- Assume the shell is cmd local function shellEscapeCharacter(c) -- Escape character doesn't work in quoted strings, and spaces can't be escaped? Tragic! -- So I quote the spaces and don't quote the rest of the text. Not pretty, but it seems to work. return (c == " " and "\" \"") or (c:find("[^\\/%.%-%a%d]") and "^"..c) end shellString = function(filename) return filename == "" and "\"\"" or filename:gsub(".", shellEscapeCharacter) end fileState = function(filename) -- Use md5sum; slower than checking date, but I don't know of a -- good way to do that. local stream = io.popen(("md5sum -- %s"):format(shellString(filename))) if stream then local result = stream:read("*line") stream:close() return result end return end elseif shell == "sh" or shell == "bash" then local function shellEscapeCharacter(c) return c:find("[^/%.%-%a%d]") and "\\"..c end shellString = function(filename) return filename == "" and "\"\"" or filename:gsub(".", shellEscapeCharacter) end fileState = function(filename) local stream = io.popen(("stat -Lc %%y -- %s"):format(shellString(filename))) if stream then local result = stream:read("*line") io.close(stream) return result or "" end return "" end else error("Don't know how to safely escape strings for shell '"..shell.."'.") end -- Holds information about files that are open. local buffers = {} -- Returns a string containing the contents of a file. local function fileData(filename) local stream = io.open(filename) if stream then local result = stream:read("*all") io.close(stream) return result or "" end return "" end -- Returns the last known state of a file, or sets up a new state if the file wasn't known. local function getBuffer(file) local buffer = buffers[file] if not buffer then buffer = {} buffers[file] = buffer buffer.state = fileState(file) buffer.data = fileData(file) end return buffer end -- Returns the name of a temporary file containing the passed string. local function dataToFile(data) local file = os.tmpname() local stream = io.open(file, "w") stream:write(data) stream:close() return file end -- Merges some strings, and returns the result. -- orig is the state of the file before editing occured -- new is what the file on disk currently looks like local function mergeData(orig, new) local current = editor:GetText() current, orig, new = dataToFile(current), dataToFile(orig), dataToFile(new) -- We use diff3 to merge the files together, and -- then we use diff to discover the changes needed to transform -- the text in the buffer into the merged file. -- Then we manually apply those changes, rather than dumping the -- merged file into the buffer, so that folds, bookmarks, and selections -- are (more or less) preserved. local stream = io.popen(("diff3 -mE --strip-trailing-cr -L Local -L Original -L Disk %s %s %s | diff %s -") :format(shellString(current), shellString(orig), shellString(new), shellString(current))) if stream then local conflicts = {} local eol = "\n" if editor.EOLMode == 0 then eol = "\r\n" elseif editor.EOLMode == 1 then eol = "\r" end local p = 1 local line = stream:read("*line") editor:BeginUndoAction() while line do local action, pos = line:match("^%d[,%d]-([acd])(%d+)") if action then p = tonumber(pos) if action == "d" then -- Position of deleted text is kind of inconsistant in my opinion, but -- considering non-existent things don't usually have positions, -- I suppose I should be greatful. p = p + 1 end end local cmd, txt = line:match("^(.).(.*)$") if cmd == "<" then local a = editor.Anchor editor.TargetStart = editor:PositionFromLine(p-1) editor.TargetEnd = editor.TargetStart+editor:LineLength(p-1) if a >= editor.TargetStart then if a >= editor.TargetEnd then a = a - (editor.TargetEnd-editor.TargetStart) else a = editor.TargetStart end end editor:ReplaceTarget("") editor.Anchor = a elseif cmd == ">" then local a = editor.Anchor local pos = editor:PositionFromLine(p-1) editor:InsertText(pos, txt..eol) if a >= pos then a = a + txt:len() + eol:len() end editor.Anchor = a if txt == "=======" then table.insert(conflicts, p) editor:MarkerAdd(p-1, 1) -- And a bookmark for this conflict. end p = p + 1 end line = stream:read("*line") end editor:EndUndoAction() if #conflicts > 0 then print("Merge conflicts on lines: "..table.concat(conflicts, ", ").."\n") end stream:close() end os.remove(current) os.remove(orig) os.remove(new) end -- Check if a file has been modified, and merge it if needed. local function recheckFile(file) -- The file being checked damn well better be the file in the editor. assert(file == props["FilePath"]) local buffer = getBuffer(file) local state = fileState(file) if state ~= buffer.state then local data= fileData(file) if data ~= buffer.data then mergeData(buffer.data, data) end buffer.state = state buffer.data = data end end local function onSwitch(file) recheckFile(file) end local function onClose(file) buffers[file] = nil end local function onOpen(file) onClose(file) -- Forget everything we know about the file. getBuffer(file) -- This will recreate the state information for the file. end local function onBeforeSave(file) recheckFile(file) end local function onSave(file) -- Pretend the file was just opened. onOpen(file) end local function onFocus() recheckFile(props["FilePath"]) end local function register(name, func) if _G["scite_"..name] then -- Use extman's register function if it exists. _G["scite_"..name](func) else local orig = _G[name] if orig then -- If there is already a function, replace it with a new one that will call both -- ours and the original. _G[name] = function(...) return func(...) or orig(...) end else -- If the function doesn't exist, use our own. _G[name] = func end end end register("OnOpen", onOpen) register("OnBeforeSave", onBeforeSave) register("OnSave", onSave) register("OnClose", onClose) register("OnSwitchFile", onSwitch) -- Don't do this on Windows, because it makes the command prompt flash over the screen, -- which is annoying. if shell then -- I'd rather only check when SciTE regains focus after the user returns to it -- after using another program, but this will have to do. register("OnUpdateUI", onFocus) end _G.moc_checkFile = function() recheckFile(props["FilePath"]) end if scite_Command then -- Add shortcut using extman. scite_Command("Merge External Changes|moc_checkFile") else -- Add shortcut manually. local i = 1 while props["command.name."..i..".*"] ~= "" and -- Search for unused index, props["command.name."..i..".*"] ~= "Merge External Changes" do -- Or our old index if SciTE reloaded this script. i = i + 1 end props["command.name."..i..".*"] = "Merge External Changes" props["command."..i..".*"] = "moc_checkFile" props["command.subsystem."..i..".*"] = "3" props["command.mode."..i..".*"]="savebefore:no" end