Scite Merge On Change

lua-users home
wiki

Checks if the file being edited has been changed on disk, and if so, tries to perform a three-way merge to apply the changes made to the file to the text in the editor. If the merge creates any conflicts, bookmarks will be set for the lines they occur on.

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.

Source

 -- 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 

RecentChanges · preferences
edit · history
Last edited October 26, 2008 11:34 pm GMT (diff)