Skip to content

Commit

Permalink
Copy normalize from neovim into path utils and swap vim.fs.normalize …
Browse files Browse the repository at this point in the history
…-> core.utils.path.normalize
  • Loading branch information
chipsenkbeil committed Apr 30, 2024
1 parent 98fd895 commit 9fd0927
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 27 deletions.
2 changes: 1 addition & 1 deletion lua/org-roam/api/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ local function make_expansions(roam)
return roam.config.directory
end,
["%R"] = function()
return vim.fs.normalize(vim.fn.resolve(roam.config.directory))
return roam.utils.normalize(vim.fn.resolve(roam.config.directory))
end,
}
end
Expand Down
2 changes: 1 addition & 1 deletion lua/org-roam/core/utils/io.lua
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ function M.walk(path, opts)
entry_path = vim.fn.resolve(entry_path)
end

entry_path = vim.fs.normalize(entry_path, { expand_env = true })
entry_path = path_utils.normalize(entry_path, { expand_env = true })

return {
name = name,
Expand Down
179 changes: 164 additions & 15 deletions lua/org-roam/core/utils/path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,10 @@

local M = {}

-- From plenary.nvim, determines the path separator.
local SEP = (function()
if jit then
local os = string.lower(jit.os)
if os ~= "windows" then
return "/"
else
return "\\"
end
else
return package.config:sub(1, 1)
end
end)()
local uv = vim.uv or vim.loop

local ISWIN = uv.os_uname().sysname == "Windows_NT"
local SEP = ISWIN and "\\" or "/"

---Returns the path separator.
---@return string
Expand Down Expand Up @@ -50,7 +41,7 @@ function M.join(...)

for _, p in ipairs({ ... }) do
-- Convert \ into /
p = vim.fs.normalize(p)
p = M.normalize(p)

if path == "" or is_absolute(p) then
path = p
Expand All @@ -59,7 +50,165 @@ function M.join(...)
end
end

return vim.fs.normalize(path)
return M.normalize(path)
end

---Helper function taken from neovim 0.10 nightly.
---@param path string Path to split.
---@return string, string, boolean : prefix, body, whether path is invalid.
local function split_windows_path(path)
local prefix = ""

--- Match pattern. If there is a match, move the matched pattern from the path to the prefix.
--- Returns the matched pattern.
---
--- @param pattern string Pattern to match.
--- @return string|nil Matched pattern
local function match_to_prefix(pattern)
local match = path:match(pattern)

if match then
prefix = prefix .. match --[[ @as string ]]
path = path:sub(#match + 1)
end

return match
end

local function process_unc_path()
return match_to_prefix("[^/]+/+[^/]+/+")
end

if match_to_prefix("^//[?.]/") then
-- Device paths
local device = match_to_prefix("[^/]+/+")

-- Return early if device pattern doesn"t match, or if device is UNC and it"s not a valid path
if not device or (device:match("^UNC/+$") and not process_unc_path()) then
return prefix, path, false
end
elseif match_to_prefix("^//") then
-- Process UNC path, return early if it"s invalid
if not process_unc_path() then
return prefix, path, false
end
elseif path:match("^%w:") then
-- Drive paths
prefix, path = path:sub(1, 2), path:sub(3)
end

-- If there are slashes at the end of the prefix, move them to the start of the body. This is to
-- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no
-- slashes at the end of the prefix, so it will be treated as a relative path, as it should be.
local trailing_slash = prefix:match("/+$")

if trailing_slash then
prefix = prefix:sub(1, -1 - #trailing_slash)
path = trailing_slash .. path --[[ @as string ]]
end

return prefix, path, true
end

---Helper function taken from neovim 0.10 nightly.
---@param path string Path to resolve.
---@return string Resolved path.
local function path_resolve_dot(path)
local is_path_absolute = vim.startswith(path, "/")
-- Split the path into components and process them
local path_components = vim.split(path, "/")
local new_path_components = {}

for _, component in ipairs(path_components) do
if component == "." or component == "" then -- luacheck: ignore 542
-- Skip `.` components and empty components
elseif component == ".." then
if #new_path_components > 0 and new_path_components[#new_path_components] ~= ".." then
-- For `..`, remove the last component if we"re still inside the current directory, except
-- when the last component is `..` itself
table.remove(new_path_components)
elseif is_path_absolute then -- luacheck: ignore 542
-- Reached the root directory in absolute path, do nothing
else
-- Reached current directory in relative path, add `..` to the path
table.insert(new_path_components, component)
end
else
table.insert(new_path_components, component)
end
end

return (is_path_absolute and "/" or "") .. table.concat(new_path_components, "/")
end

---Noramlizes a path. Taken from neovim 0.10 nightly.
---
---@param path (string) Path to normalize
---@param opts? {expand_env?:boolean, win?:boolean}
---@return (string) : Normalized path
function M.normalize(path, opts)
opts = opts or {}

vim.validate({
path = { path, { "string" } },
expand_env = { opts.expand_env, { "boolean" }, true },
win = { opts.win, { "boolean" }, true },
})

local win = opts.win == nil and ISWIN or not not opts.win
local os_sep_local = win and "\\" or "/"

-- Empty path is already normalized
if path == "" then
return ""
end

-- Expand ~ to users home directory
if vim.startswith(path, "~") then
local home = uv.os_homedir() or "~"
if home:sub(-1) == os_sep_local then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end

-- Expand environment variables if `opts.expand_env` isn"t `false`
if opts.expand_env == nil or opts.expand_env then
path = path:gsub("%$([%w_]+)", uv.os_getenv)
end

-- Convert path separator to `/`
path = path:gsub(os_sep_local, "/")

-- Check for double slashes at the start of the path because they have special meaning
local double_slash = vim.startswith(path, "//") and not vim.startswith(path, "///")
local prefix = ""

if win then
local is_valid --- @type boolean
-- Split Windows paths into prefix and body to make processing easier
prefix, path, is_valid = split_windows_path(path)

-- If path is not valid, return it as-is
if not is_valid then
return prefix .. path
end

-- Remove extraneous slashes from the prefix
prefix = prefix:gsub("/+", "/")
end

-- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix
-- and path. Preserve leading double slashes as they indicate UNC paths and DOS device paths in
-- Windows and have implementation-defined behavior in POSIX.
path = (double_slash and "/" or "") .. prefix .. path_resolve_dot(path)

-- Change empty path to `.`
if path == "" then
path = "."
end

return path
end

return M
2 changes: 1 addition & 1 deletion lua/org-roam/setup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ local function merge_config(roam, config)

-- Normalize the roam directory before storing it
---@diagnostic disable-next-line:inject-field
config.directory = vim.fs.normalize(config.directory)
config.directory = roam.utils.normalize(config.directory)

-- Merge our configuration options into our global config
roam.config:replace(config)
Expand Down
3 changes: 3 additions & 0 deletions lua/org-roam/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,7 @@ function M.get_visual_selection(opts)
return lines, ranges
end

M.join = require("org-roam.core.utils.path").join
M.normalize = require("org-roam.core.utils.path").normalize

return M
28 changes: 19 additions & 9 deletions spec/core_utils_path_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,27 @@ describe("org-roam.core.utils.path", function()
-- Restore old separator function
path.separator = sep

assert.are.equal(
"C:/Users/senkwich/orgfiles/roam/20240429235641-test.org",
actual
)
-- On Mac/Linux, we don't escape \ into /
if path.separator() == "\\" then
assert.are.equal(
"C:/Users/senkwich/orgfiles/roam/20240429235641-test.org",
actual
)
else
assert.are.equal(
"C:\\\\Users\\senkwich\\orgfiles\\roam\\20240429235641-test.org",
actual
)
end
end)

it("should convert \\ to / when joining paths", function()
assert.are.equal(
"C:/some/path",
path.join("C:\\some\\path")
)
it("should convert \\ to / when joining paths on Windows", function()
local test_path = path.join("C:\\some\\path")
if path.separator() == "\\" then
assert.are.equal("C:/some/path", test_path)
elseif path.separator() == "/" then
assert.are.equal("C:\\some\\path", test_path)
end
end)
end)
end)

0 comments on commit 9fd0927

Please sign in to comment.