Scite 合并更改

lua-users home
wiki

检查正在编辑的文件是否已在磁盘上更改,如果已更改,则尝试执行三方合并以将对文件的更改应用到编辑器中的文本。如果合并创建任何冲突,则会在冲突发生的行的书签上设置书签。

对于在打开同一文件多次时避免删除最近的更改,或者在更新存储库时其文件已打开时,这很有用。

Unix 版本使用 statdiffdiff3 来检测和合并更改。

我在 Windows 中找不到 stat 的等效项,因此 Windows 版本使用 md5sum 来检测更改;您需要 md5sumdiffdiff3 的 Windows 端口 [GnuWin32],并且它们的 bin 目录需要在您的 PATH 环境变量中,以便脚本可以执行它们。

来源

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

最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2008 年 10 月 27 日凌晨 4:34 GMT (差异)