258 lines
7.3 KiB
Lua
258 lines
7.3 KiB
Lua
local cache = require("oil.cache")
|
|
local columns = require("oil.columns")
|
|
local fs = require("oil.fs")
|
|
local util = require("oil.util")
|
|
local view = require("oil.view")
|
|
local FIELD = require("oil.constants").FIELD
|
|
local M = {}
|
|
|
|
---@alias oil.Diff oil.DiffNew|oil.DiffDelete|oil.DiffChange
|
|
|
|
---@class oil.DiffNew
|
|
---@field type "new"
|
|
---@field name string
|
|
---@field entry_type oil.EntryType
|
|
---@field id nil|integer
|
|
---@field link nil|string
|
|
|
|
---@class oil.DiffDelete
|
|
---@field type "delete"
|
|
---@field name string
|
|
---@field id integer
|
|
---
|
|
---@class oil.DiffChange
|
|
---@field type "change"
|
|
---@field entry_type oil.EntryType
|
|
---@field name string
|
|
---@field column string
|
|
---@field value any
|
|
|
|
---@param name string
|
|
---@return string
|
|
---@return boolean
|
|
local function parsedir(name)
|
|
local isdir = vim.endswith(name, "/")
|
|
if isdir then
|
|
name = name:sub(1, name:len() - 1)
|
|
end
|
|
return name, isdir
|
|
end
|
|
|
|
---@class oil.ParseResult
|
|
---@field data table Parsed entry data
|
|
---@field ranges table<string, integer[]> Locations of the various columns
|
|
---@field entry nil|oil.InternalEntry If the entry already exists
|
|
|
|
---Parse a single line in a buffer
|
|
---@param adapter oil.Adapter
|
|
---@param line string
|
|
---@param column_defs oil.ColumnSpec[]
|
|
---@return nil|oil.ParseResult
|
|
---@return nil|string Error
|
|
M.parse_line = function(adapter, line, column_defs)
|
|
local ret = {}
|
|
local ranges = {}
|
|
local start = 1
|
|
local value, rem = line:match("^/(%d+) (.+)$")
|
|
if not value then
|
|
return nil, "Malformed ID at start of line"
|
|
end
|
|
ranges.id = { start, value:len() + 1 }
|
|
start = ranges.id[2] + 1
|
|
ret.id = tonumber(value)
|
|
for _, def in ipairs(column_defs) do
|
|
local name = util.split_config(def)
|
|
local range = { start }
|
|
local start_len = string.len(rem)
|
|
value, rem = columns.parse_col(adapter, rem, def)
|
|
if not value or not rem then
|
|
return nil, string.format("Parsing %s failed", name)
|
|
end
|
|
ret[name] = value
|
|
range[2] = range[1] + start_len - string.len(rem) - 1
|
|
ranges[name] = range
|
|
start = range[2] + 1
|
|
end
|
|
local name = rem
|
|
if name then
|
|
local isdir
|
|
name, isdir = parsedir(vim.trim(name))
|
|
if name ~= "" then
|
|
ret.name = name
|
|
end
|
|
ret._type = isdir and "directory" or "file"
|
|
end
|
|
local entry = cache.get_entry_by_id(ret.id)
|
|
ranges.name = { start, start + string.len(rem) - 1 }
|
|
if not entry then
|
|
return { data = ret, ranges = ranges }
|
|
end
|
|
|
|
-- Parse the symlink syntax
|
|
local meta = entry[FIELD.meta]
|
|
local entry_type = entry[FIELD.type]
|
|
if entry_type == "link" and meta and meta.link then
|
|
local name_pieces = vim.split(ret.name, " -> ", { plain = true })
|
|
if #name_pieces ~= 2 then
|
|
ret.name = ""
|
|
return { data = ret, ranges = ranges }
|
|
end
|
|
ranges.name = { start, start + string.len(name_pieces[1]) - 1 }
|
|
ret.name = parsedir(vim.trim(name_pieces[1]))
|
|
ret.link_target = name_pieces[2]
|
|
ret._type = "link"
|
|
end
|
|
|
|
-- Try to keep the same file type
|
|
if entry_type ~= "directory" and entry_type ~= "file" and ret._type ~= "directory" then
|
|
ret._type = entry[FIELD.type]
|
|
end
|
|
|
|
return { data = ret, entry = entry, ranges = ranges }
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return oil.Diff[]
|
|
---@return table[] Parsing errors
|
|
M.parse = function(bufnr)
|
|
local diffs = {}
|
|
local errors = {}
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
local adapter = util.get_adapter(bufnr)
|
|
if not adapter then
|
|
table.insert(errors, {
|
|
lnum = 0,
|
|
col = 0,
|
|
message = string.format("Cannot parse buffer '%s': No adapter", bufname),
|
|
})
|
|
return diffs, errors
|
|
end
|
|
local scheme, path = util.parse_url(bufname)
|
|
local parent_url = scheme .. path
|
|
local column_defs = columns.get_supported_columns(adapter)
|
|
local children = cache.list_url(parent_url)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
|
local original_entries = {}
|
|
for _, child in pairs(children) do
|
|
if view.should_display(child, bufnr) then
|
|
original_entries[child[FIELD.name]] = child[FIELD.id]
|
|
end
|
|
end
|
|
local seen_names = {}
|
|
local function check_dupe(name, i)
|
|
if fs.is_mac or fs.is_windows then
|
|
-- mac and windows use case-insensitive filesystems
|
|
name = name:lower()
|
|
end
|
|
if seen_names[name] then
|
|
table.insert(errors, { message = "Duplicate filename", lnum = i - 1, col = 0 })
|
|
else
|
|
seen_names[name] = true
|
|
end
|
|
end
|
|
for i, line in ipairs(lines) do
|
|
if line:match("^/%d+") then
|
|
local result, err = M.parse_line(adapter, line, column_defs)
|
|
if not result or err then
|
|
table.insert(errors, {
|
|
message = err,
|
|
lnum = i - 1,
|
|
col = 0,
|
|
})
|
|
goto continue
|
|
end
|
|
local parsed_entry = result.data
|
|
local entry = result.entry
|
|
if not parsed_entry.name or parsed_entry.name:match("/") or not entry then
|
|
local message
|
|
if not parsed_entry.name then
|
|
message = "No filename found"
|
|
elseif not entry then
|
|
message = "Could not find existing entry (was the ID changed?)"
|
|
else
|
|
message = "Filename cannot contain '/'"
|
|
end
|
|
table.insert(errors, {
|
|
message = message,
|
|
lnum = i - 1,
|
|
col = 0,
|
|
})
|
|
goto continue
|
|
end
|
|
check_dupe(parsed_entry.name, i)
|
|
local meta = entry[FIELD.meta]
|
|
if original_entries[parsed_entry.name] == parsed_entry.id then
|
|
if entry[FIELD.type] == "link" and (not meta or meta.link ~= parsed_entry.link_target) then
|
|
table.insert(diffs, {
|
|
type = "new",
|
|
name = parsed_entry.name,
|
|
entry_type = "link",
|
|
link = parsed_entry.link_target,
|
|
})
|
|
else
|
|
original_entries[parsed_entry.name] = nil
|
|
end
|
|
else
|
|
table.insert(diffs, {
|
|
type = "new",
|
|
name = parsed_entry.name,
|
|
entry_type = parsed_entry._type,
|
|
id = parsed_entry.id,
|
|
link = parsed_entry.link_target,
|
|
})
|
|
end
|
|
|
|
for _, col_def in ipairs(column_defs) do
|
|
local col_name = util.split_config(col_def)
|
|
if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then
|
|
table.insert(diffs, {
|
|
type = "change",
|
|
name = parsed_entry.name,
|
|
entry_type = entry[FIELD.type],
|
|
column = col_name,
|
|
value = parsed_entry[col_name],
|
|
})
|
|
end
|
|
end
|
|
else
|
|
local name, isdir = parsedir(vim.trim(line))
|
|
if vim.startswith(name, "/") then
|
|
table.insert(errors, {
|
|
message = "Paths cannot start with '/'",
|
|
lnum = i - 1,
|
|
col = 0,
|
|
})
|
|
goto continue
|
|
end
|
|
if name ~= "" then
|
|
local link_pieces = vim.split(name, " -> ", { plain = true })
|
|
local entry_type = isdir and "directory" or "file"
|
|
local link
|
|
if #link_pieces == 2 then
|
|
entry_type = "link"
|
|
name, link = unpack(link_pieces)
|
|
end
|
|
check_dupe(name, i)
|
|
table.insert(diffs, {
|
|
type = "new",
|
|
name = name,
|
|
entry_type = entry_type,
|
|
link = link,
|
|
})
|
|
end
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
for name, child_id in pairs(original_entries) do
|
|
table.insert(diffs, {
|
|
type = "delete",
|
|
name = name,
|
|
id = child_id,
|
|
})
|
|
end
|
|
|
|
return diffs, errors
|
|
end
|
|
|
|
return M
|