From f0a01822850bf7324326a3424f6b9a6892e5a768 Mon Sep 17 00:00:00 2001 From: tao <2471314@gmail.com> Date: Sat, 8 Aug 2026 07:54:29 +0800 Subject: [PATCH] refactor(path): pathcmp() #41035 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Redudant code Solution: Add `path_fold_char()` to normalize path sep. Refactor `pathcmp()` and `path_fnamencmp()` into `path_cmp`. | feature | pathcmp | path_fnamencmp | path_cmp(now) | | ------------------------ | -------------- | -------------- | ------------- | | case folding | `mb_toupper()` | `utf_fold()` | `utf_fold()` | | consults fileignorecase | ✅ | ✅ | `ic` param | | `maxlen` | ✅ | ✅ | ✅ | | `/` == `\` | ✅ | ✅ | ✅ | | MSWIN drive letter | ❌ | ✅ | ✅ | | sep affects sorting | ✅ | ❌ | ✅ | | ignores a trailing slash | ✅ | ❌ | ✅ | Refactor `path_fnamecmp` and `path_full_compare` into `path_equal`, with flags controlling "no filesystem" comparison (i.e. `path_cmp`), env variables expansion, absolute path resolution and filesystem access. --- src/nvim/arglist.c | 17 +- src/nvim/autocmd.c | 7 +- src/nvim/buffer.c | 2 +- src/nvim/bufwrite.c | 2 +- src/nvim/eval/fs.c | 2 +- src/nvim/eval/userfunc.c | 2 +- src/nvim/ex_docmd.c | 4 +- src/nvim/file_search.c | 12 +- src/nvim/fileio.c | 2 +- src/nvim/garray.c | 2 +- src/nvim/help.c | 2 +- src/nvim/main.c | 4 +- src/nvim/map.c | 58 ++-- src/nvim/mark.c | 2 +- src/nvim/memline.c | 9 +- src/nvim/os/env.c | 4 +- src/nvim/os/stdpaths.c | 2 +- src/nvim/path.c | 313 +++++++----------- src/nvim/path.h | 14 +- src/nvim/quickfix.c | 2 +- src/nvim/runtime.c | 6 +- src/nvim/search.c | 3 +- src/nvim/shada.c | 4 +- src/nvim/spell.c | 12 +- src/nvim/spellfile.c | 7 +- src/nvim/tag.c | 4 +- src/nvim/window.c | 4 +- test/functional/autocmd/autocmd_spec.lua | 13 + test/functional/shada/shada_spec.lua | 27 +- .../functional/vimscript/fnamemodify_spec.lua | 12 + test/unit/path_spec.lua | 76 ++++- 31 files changed, 306 insertions(+), 324 deletions(-) diff --git a/src/nvim/arglist.c b/src/nvim/arglist.c index 90cf929908..4c9d9848a9 100644 --- a/src/nvim/arglist.c +++ b/src/nvim/arglist.c @@ -466,9 +466,8 @@ bool editing_arg_idx(win_T *win) || (win->w_buffer->b_fnum != WARGLIST(win)[win->w_arg_idx].ae_fnum && (win->w_buffer->b_ffname == NULL - || !(path_full_compare(alist_name(&WARGLIST(win)[win->w_arg_idx]), - win->w_buffer->b_ffname, true, - true) & kEqualFiles)))); + || !path_equal(alist_name(&WARGLIST(win)[win->w_arg_idx]), + win->w_buffer->b_ffname, kPathCmpExpand | kPathCmpFull)))); } /// Check if window "win" is editing the w_arg_idx file in its argument list. @@ -485,9 +484,8 @@ void check_arg_idx(win_T *win) && win->w_arg_idx < GARGCOUNT && (win->w_buffer->b_fnum == GARGLIST[GARGCOUNT - 1].ae_fnum || (win->w_buffer->b_ffname != NULL - && (path_full_compare(alist_name(&GARGLIST[GARGCOUNT - 1]), - win->w_buffer->b_ffname, true, true) - & kEqualFiles)))) { + && path_equal(alist_name(&GARGLIST[GARGCOUNT - 1]), + win->w_buffer->b_ffname, kPathCmpExpand | kPathCmpFull)))) { arg_had_last = true; } } else { @@ -701,7 +699,7 @@ void ex_argdedupe(exarg_T *eap FUNC_ATTR_UNUSED) for (int j = i + 1; j < ARGCOUNT; j++) { char *secondFullname = FullName_save(ARGLIST[j].ae_fname, false); - bool areNamesDuplicate = path_fnamecmp(firstFullname, secondFullname) == 0; + bool areNamesDuplicate = path_equal(firstFullname, secondFullname, kPathCmpLiteral); xfree(secondFullname); if (areNamesDuplicate) { @@ -868,9 +866,8 @@ static void arg_all_close_unused_windows(arg_all_state_T *aall) for (i = 0; i < aall->opened_len; i++) { if (i < aall->alist->al_ga.ga_len && (AARGLIST(aall->alist)[i].ae_fnum == buf->b_fnum - || path_full_compare(alist_name(&AARGLIST(aall->alist)[i]), - buf->b_ffname, - true, true) & kEqualFiles)) { + || path_equal(alist_name(&AARGLIST(aall->alist)[i]), + buf->b_ffname, kPathCmpExpand | kPathCmpFull))) { int weight = 1; if (old_curtab == curtab) { diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 6a80162eae..4cd1485ff1 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -37,6 +37,7 @@ #include "nvim/lua/executor.h" #include "nvim/main.h" #include "nvim/map_defs.h" +#include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/option.h" @@ -2256,7 +2257,7 @@ bool au_exists(const char *const arg) } // if pattern is "", special handling is needed which uses curbuf - // for pattern ", path_fnamecmp() will work fine + // for pattern ", mb_strcmp_ic() will work fine if (pattern != NULL && STRICMP(pattern, "") == 0) { buflocal_buf = curbuf; } @@ -2265,12 +2266,12 @@ bool au_exists(const char *const arg) for (size_t i = 0; i < kv_size(*acs); i++) { AutoPat *const ap = kv_A(*acs, i).pat; // Only use a pattern when it has not been removed. - // For buffer-local autocommands, path_fnamecmp() works fine. + // Patterns are matched verbatim (like do_autocmd): mb_strcmp_ic instead of path_equal. if (ap != NULL && (group == AUGROUP_ALL || ap->group == group) && (pattern == NULL || (buflocal_buf == NULL - ? path_fnamecmp(ap->pat, pattern) == 0 + ? mb_strcmp_ic(p_fic, ap->pat, pattern) == 0 : ap->buflocal_nr == buflocal_buf->b_fnum))) { retval = true; break; diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 4c01f8ad93..4c0efb4569 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -3263,7 +3263,7 @@ static bool otherfile_buf(buf_T *buf, char *ffname, FileID *file_id_p, bool file if (ffname == NULL || *ffname == NUL || buf->b_ffname == NULL) { return true; } - if (path_fnamecmp(ffname, buf->b_ffname) == 0) { + if (path_equal(ffname, buf->b_ffname, kPathCmpLiteral)) { return false; } { diff --git a/src/nvim/bufwrite.c b/src/nvim/bufwrite.c index ddeda3c074..d2e2ff5032 100644 --- a/src/nvim/bufwrite.c +++ b/src/nvim/bufwrite.c @@ -1051,7 +1051,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en #endif // true if writing over original - bool overwriting = buf->b_ffname != NULL && path_fnamecmp(ffname, buf->b_ffname) == 0; + bool overwriting = buf->b_ffname != NULL && path_equal(ffname, buf->b_ffname, kPathCmpLiteral); no_wait_return++; // don't wait for return yet diff --git a/src/nvim/eval/fs.c b/src/nvim/eval/fs.c index bb3a6ca78f..5c9f1fef19 100644 --- a/src/nvim/eval/fs.c +++ b/src/nvim/eval/fs.c @@ -180,7 +180,7 @@ repeat: // Do not call shorten_fname() here since it removes the prefix // even though the path does not have a prefix. - if (path_fnamencmp(p, dirname, dirnamelen) == 0) { + if (path_cmp(p_fic, p, dirname, dirnamelen) == 0) { p += dirnamelen; if (vim_ispathsep(*p)) { while (*p && vim_ispathsep(*p)) { diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 3aed7f991b..4ba8cc0563 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -2917,7 +2917,7 @@ void ex_function(exarg_T *eap) p = vim_strchr(scriptname, '/'); int plen = (int)strlen(p); int slen = (int)strlen(SOURCING_NAME); - if (slen > plen && path_fnamecmp(p, SOURCING_NAME + slen - plen) == 0) { + if (slen > plen && path_equal(p, SOURCING_NAME + slen - plen, kPathCmpLiteral)) { j = OK; } xfree(scriptname); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 9504b78a13..ba3074353d 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -257,7 +257,7 @@ static bool is_other_file(int fnum, char *ffname) && *curbuf->b_sfname != NUL) { // This occurs with unsaved buffers. In which case `ffname` // actually corresponds to curbuf->b_sfname - return path_fnamecmp(ffname, curbuf->b_sfname) != 0; + return !path_equal(ffname, curbuf->b_sfname, kPathCmpLiteral); } return otherfile(ffname); @@ -6324,7 +6324,7 @@ bool do_chdir(char *new_dir, CdScope scope) // Buffer-local CWD is never "cleared" by :lcd/:tcd/:cd, so it stays in effect. const bool bcd_active = scope != kCdScopeBuffer && curbuf->b_localdir != NULL; - bool dir_differs = pdir == NULL || pathcmp(pdir, new_dir, -1) != 0; + bool dir_differs = pdir == NULL || !path_equal(pdir, new_dir, kPathCmpLiteral); if (dir_differs) { if (!bcd_active) { do_autocmd_dirchanged(new_dir, scope, kCdCauseManual, true); diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index fc4ee0b3cf..cff8451913 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -934,8 +934,8 @@ char *vim_findfile(void *search_ctx_arg) if (strncmp(stackp->ffs_wc_path.data, "**", 2) == 0) { for (int i = stackp->ffs_filearray_cur; i < stackp->ffs_filearray_size; i++) { - if (path_fnamecmp(stackp->ffs_filearray[i], - stackp->ffs_fix_path.data) == 0) { + if (path_equal(stackp->ffs_filearray[i], + stackp->ffs_fix_path.data, kPathCmpLiteral)) { continue; // don't repush same directory } if (!os_isdir(stackp->ffs_filearray[i])) { @@ -1071,7 +1071,7 @@ static ff_visited_list_hdr_T *ff_get_visited_list(char *filename, size_t filenam if (*list_headp != NULL) { retptr = *list_headp; while (retptr != NULL) { - if (path_fnamecmp(filename, retptr->ffvl_filename) == 0) { + if (path_equal(filename, retptr->ffvl_filename, kPathCmpLiteral)) { #ifdef FF_VERBOSE if (p_verbose >= 5) { verbose_enter_scroll(); @@ -1171,7 +1171,7 @@ static int ff_check_visited(ff_visited_T **visited_list, char *fname, size_t fna // check against list of already visited files for (vp = *visited_list; vp != NULL; vp = vp->ffv_next) { - if ((url && path_fnamecmp(vp->ffv_fname, ff_expand_buffer.data) == 0) + if ((url && path_equal(vp->ffv_fname, ff_expand_buffer.data, kPathCmpLiteral)) || (!url && vp->file_id_valid && os_fileid_equal(&(vp->file_id), &file_id))) { // are the wildcard parts equal @@ -1327,7 +1327,7 @@ static bool ff_path_in_stoplist(char *path, size_t path_len, String *stopdirs_v) // match for parent directory. So '/home' also matches // '/home/rks'. Check for PATHSEP in stopdirs_v[i], else // '/home/r' would also match '/home/rks' - if (path_fnamencmp(stopdirs_v[i].data, path, path_len) == 0 + if (path_cmp(p_fic, stopdirs_v[i].data, path, path_len) == 0 && (stopdirs_v[i].size <= path_len || vim_ispathsep(stopdirs_v[i].data[path_len]))) { return true; @@ -1909,7 +1909,7 @@ int vim_chdirfile(char *fname, CdCause cause) NameBuff[0] = NUL; } - if (pathcmp(dir, NameBuff, -1) == 0) { + if (path_equal(dir, NameBuff, kPathCmpLiteral)) { // nothing to do return OK; } diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 238b89a7c5..087e890bc5 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -2715,7 +2715,7 @@ int vim_rename(const char *from, const char *to) // When the names are identical, there is nothing to do. When they refer // to the same file (ignoring case and slash/backslash differences) but // the file name differs we need to go through a temp file. - if (path_fnamecmp(from, to) == 0) { + if (path_equal(from, to, kPathCmpLiteral)) { if (p_fic && (strcmp(path_tail(from), path_tail(to)) != 0)) { use_tmp_file = true; } else { diff --git a/src/nvim/garray.c b/src/nvim/garray.c index daa5566e7a..872344d6cc 100644 --- a/src/nvim/garray.c +++ b/src/nvim/garray.c @@ -110,7 +110,7 @@ void ga_remove_duplicate_strings(garray_T *gap) // loop over the growing array in reverse for (int i = gap->ga_len - 1; i > 0; i--) { - if (path_fnamecmp(fnames[i - 1], fnames[i]) == 0) { + if (path_equal(fnames[i - 1], fnames[i], kPathCmpLiteral)) { xfree(fnames[i]); // close the gap (move all strings one slot lower) diff --git a/src/nvim/help.c b/src/nvim/help.c index a6b5f5b5d9..fcf32d7263 100644 --- a/src/nvim/help.c +++ b/src/nvim/help.c @@ -539,7 +539,7 @@ static void helptags_one(char *dir, const char *ext, const char *tagfname, bool // add the "help-tags" tag. ga_init(&ga, (int)sizeof(char *), 100); if (add_help_tags - || path_full_compare("$VIMRUNTIME/doc", dir, false, true) == kEqualFiles) { + || path_equal("$VIMRUNTIME/doc", dir, kPathCmpExpand)) { size_t s_len = 18 + strlen(tagfname); s = xmalloc(s_len); snprintf(s, s_len, "help-tags\t%s\t1\n", tagfname); diff --git a/src/nvim/main.c b/src/nvim/main.c index 6d068ffd42..467d9ddb54 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -2131,7 +2131,7 @@ static bool do_user_initialization(void) if (do_source(user_vimrc, true, DOSO_VIMRC, NULL) != FAIL) { do_exrc = p_exrc; if (do_exrc) { - do_exrc = (path_full_compare(VIMRC_FILE, user_vimrc, false, true) != kEqualFiles); + do_exrc = !path_equal(VIMRC_FILE, user_vimrc, kPathCmpExpand); } xfree(user_vimrc); return do_exrc; @@ -2187,7 +2187,7 @@ static bool do_user_initialization(void) if (do_source(init_vim, true, DOSO_VIMRC, NULL) != FAIL) { do_exrc = p_exrc; if (do_exrc) { - do_exrc = (path_full_compare(VIMRC_FILE, init_vim, false, true) != kEqualFiles); + do_exrc = !path_equal(VIMRC_FILE, init_vim, kPathCmpExpand); } xfree(init_vim); xfree(config_dirs); diff --git a/src/nvim/map.c b/src/nvim/map.c index 4dfb549910..acd1d35903 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -9,6 +9,7 @@ #include #include "auto/config.h" +#include "nvim/ascii_defs.h" #include "nvim/charset.h" #include "nvim/map_defs.h" #include "nvim/mbyte.h" @@ -46,34 +47,38 @@ static inline uint32_t hash_cstr_t(const char *s) #define equal_cstr_t strequal /// Hash/equality for path strings. On case-insensitive platforms, case-fold first (via -/// str_foldcase, the same fold used by mb_stricmp/path_fnamencmp) so the hash↔equality invariant -/// holds. +/// path_fold_char, the same fold used by path_cmp) so the hash↔equality invariant holds. /// -/// On Windows we additionally: -/// - fold '\\' -> '/' (path_fnamencmp treats them as equal) -/// - drop a leading drive letter "[A-Za-z]:" so that "C:\foo" and "\foo" land in the same bucket -/// (path_fnamencmp considers them equal when the current drive matches the explicit drive -/// letter). Two paths with *different* explicit drives ("C:\foo" vs "D:\foo") may now -/// share a bucket — that's a permitted collision; equal_path_t still rejects them. -/// -/// path_fnamencmp's non-Windows branch consults &fileignorecase, which is runtime-mutable and would -/// break the hash invariant if flipped. So the macOS branch deliberately bypasses path_fnamencmp -/// and uses the compile-time CASE_INSENSITIVE_FILENAME switch instead. +/// We additionally: +/// - drop a leading drive letter "[A-Za-z]:" on Windows, so that "C:\foo" and "\foo" land in +/// the same bucket (path_cmp considers them equal when the current drive matches the +/// explicit drive letter). Two paths with *different* explicit drives ("C:\foo" vs "D:\foo") +/// may now share a bucket — that's a permitted collision; equal_path_t still rejects them. +/// - ignore a single trailing path separator. static inline uint32_t hash_path_t(const char *p) { -#ifdef BACKSLASH_IN_FILENAME - if (p[1] == ':' && ASCII_ISALPHA(p[0])) { + uint32_t h = 0; +#ifdef MSWIN + if (ASCII_ISALPHA(*p) && p[1] == ':') { p += 2; } #endif + bool ic = false; #ifdef CASE_INSENSITIVE_FILENAME - char *folded = str_foldcase((char *)p, (int)strlen(p), NULL, 0); - uint32_t h = hash_cstr_t(folded); - xfree(folded); - return h; -#else - return hash_cstr_t(p); + ic = true; #endif + const char *start = p; + for (int len = 0; *p; p += len) { + if (vim_ispathsep_nocolon(*p) + && p[1] == NUL + && start != p + && !vim_ispathsep(p[-1])) { + break; + } + int c = path_fold_char(ic, p, &len); + h = (h << 5) - h + (uint32_t)c; + } + return h; } static inline bool equal_path_t(const char *a, const char *b) @@ -84,16 +89,11 @@ static inline bool equal_path_t(const char *a, const char *b) if (a == NULL || b == NULL) { return false; } -#ifdef BACKSLASH_IN_FILENAME - // Inherit Windows-mode slash, drive-letter, and case folding. - size_t la = strlen(a); - size_t lb = strlen(b); - return path_fnamencmp(a, b, MAX(la, lb)) == 0; -#elif defined(CASE_INSENSITIVE_FILENAME) - return mb_stricmp(a, b) == 0; -#else - return strequal(a, b); + bool ic = false; +#ifdef CASE_INSENSITIVE_FILENAME + ic = true; #endif + return path_cmp(ic, a, b, MAXPATHL) == 0; } static inline uint32_t hash_HlEntry(HlEntry ae) diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 99d24be8f5..69c081b687 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -797,7 +797,7 @@ static void fmarks_check_one(xfmark_T *fm, char *name, buf_T *buf) { if (fm->fmark.fnum == 0 && fm->fname != NULL - && path_fnamecmp(name, fm->fname) == 0) { + && path_equal(name, fm->fname, kPathCmpLiteral)) { fm->fmark.fnum = buf->b_fnum; XFREE_CLEAR(fm->fname); } diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 809905c7c4..3c6a15fc40 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -429,7 +429,7 @@ void ml_setname(buf_T *buf) } // if the file name is the same we don't have to do anything - if (path_fnamecmp(fname, mfp->mf_fname) == 0) { + if (path_equal(fname, mfp->mf_fname, kPathCmpLiteral)) { xfree(fname); success = true; break; @@ -1398,7 +1398,7 @@ void recover_names(char *fname, bool skip_curbuf, list_T *ret_list) for (int i = 0; i < num_files; i++) { // Do not expand wildcards, on Windows would try to expand // "%tmp%" in "%tmp%file" - if (path_full_compare(p, files[i], true, false) & kEqualFiles) { + if (path_equal(p, files[i], kPathCmpFull)) { // Remove the name from files[i]. Move further entries // down. When the array becomes empty free it here, since // FreeWild() won't be called below. @@ -3479,7 +3479,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ } // A file name equal to old_fname is OK to use. - if (old_fname != NULL && path_fnamecmp(fname, old_fname) == 0) { + if (old_fname != NULL && path_equal(fname, old_fname, kPathCmpLiteral)) { break; } @@ -3505,8 +3505,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ // buffer don't compare the directory names, they can // have a different mountpoint. if (b0.b0_flags & B0_SAME_DIR) { - if (path_fnamecmp(path_tail(buf->b_ffname), - path_tail(b0.b0_fname)) != 0 + if (!path_equal(path_tail(buf->b_ffname), path_tail(b0.b0_fname), kPathCmpLiteral) || !same_directory(fname, buf->b_ffname)) { // Symlinks may point to the same file even // when the name differs, need to check the diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index a223a4425b..ae09d2dec2 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -764,7 +764,7 @@ static char *remove_tail(char *path, char *pend, char *dirname) char *new_tail = pend - len - 1; if (new_tail >= path - && path_fnamencmp(new_tail, dirname, len) == 0 + && path_cmp(p_fic, new_tail, dirname, len) == 0 && (new_tail == path || after_pathsep(path, new_tail))) { return new_tail; } @@ -1063,7 +1063,7 @@ size_t home_replace(const buf_T *const buf, const char *src, char *const dst, si size_t len = dirlen; while (true) { if (len - && path_fnamencmp(src, p, len) == 0 + && path_cmp(p_fic, src, p, len) == 0 && (vim_ispathsep(src[len]) || (!one && (src[len] == ',' || src[len] == ' ')) || src[len] == NUL)) { diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c index ae8f09895c..aa1ff31be3 100644 --- a/src/nvim/os/stdpaths.c +++ b/src/nvim/os/stdpaths.c @@ -116,7 +116,7 @@ static char *xdg_remove_duplicate(char *ret, const char *sep) // Check if the directory is not already in the list bool is_duplicate = false; for (size_t i = 0; i < data.size; i++) { - if (path_fnamecmp(kv_A(data, i), token) == 0) { + if (path_equal(kv_A(data, i), token, kPathCmpLiteral)) { is_duplicate = true; break; } diff --git a/src/nvim/path.c b/src/nvim/path.c index 0c6ea67a34..d9009a0909 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -47,47 +47,43 @@ enum { #include "path.c.generated.h" -/// Compare two file names. +/// Checks whether two paths refer to the same file. /// -/// @param s1 First file name. Environment variables in this name will be expanded. -/// @param s2 Second file name. -/// @param checkname When both files don't exist, only compare their names. -/// @param expandenv Whether to expand environment variables in file names. -/// @return Enum of type FileComparison. @see FileComparison. -FileComparison path_full_compare(char *const s1, char *const s2, const bool checkname, - const bool expandenv) +/// @param s1 First path. Environment variables in this path may be expanded. +/// @param s2 Second path. +/// @param flags Path comparison Flags. +/// @return true if the paths are equal. +bool path_equal(const char *s1, const char *s2, PathCmpFlags flags) FUNC_ATTR_NONNULL_ALL { - char expand1[MAXPATHL]; + char expanded_s1[MAXPATHL]; char full1[MAXPATHL]; char full2[MAXPATHL]; - FileID file_id_1, file_id_2; + FileID file_id1, file_id2; - if (expandenv) { - expand_env(s1, expand1, MAXPATHL); - } else { - xstrlcpy(expand1, s1, MAXPATHL); + assert(!(flags & kPathCmpLiteral) || flags == kPathCmpLiteral); + + if (flags == kPathCmpLiteral) { + return path_cmp(p_fic, s1, s2, MAXPATHL) == 0; } - bool id_ok_1 = os_fileid(expand1, &file_id_1); - bool id_ok_2 = os_fileid(s2, &file_id_2); - if (!id_ok_1 && !id_ok_2) { - // If os_fileid() doesn't work, may compare the names. - if (checkname) { - vim_FullName(expand1, full1, MAXPATHL, false); - vim_FullName(s2, full2, MAXPATHL, false); - if (path_fnamecmp(full1, full2) == 0) { - return kEqualFileNames; - } - } - return kBothFilesMissing; + + if (flags & kPathCmpExpand) { + expand_env_esc(s1, expanded_s1, MAXPATHL, NULL, false, NULL); + } else if (flags) { + xstrlcpy(expanded_s1, s1, MAXPATHL); } - if (!id_ok_1 || !id_ok_2) { - return kOneFileMissing; + + bool id_ok1 = os_fileid(expanded_s1, &file_id1); + bool id_ok2 = os_fileid(s2, &file_id2); + if (id_ok1 && id_ok2 && os_fileid_equal(&file_id1, &file_id2)) { + return true; } - if (os_fileid_equal(&file_id_1, &file_id_2)) { - return kEqualFiles; + if (!id_ok1 && !id_ok2 && (flags & kPathCmpFull)) { + vim_FullName(expanded_s1, full1, MAXPATHL, false); + vim_FullName(s2, full2, MAXPATHL, false); + return path_cmp(p_fic, full1, full2, MAXPATHL) == 0; } - return kDifferentFiles; + return false; } /// Gets the tail (filename segment) of path `fname`. @@ -365,105 +361,6 @@ bool dir_of_file_exists(char *fname) return retval; } -/// Compare two file names -/// -/// On some systems case in a file name does not matter, on others it does. -/// -/// @note Does not account for maximum name lengths and things like "../dir", -/// thus it is not 100% accurate. OS may also use different algorithm for -/// case-insensitive comparison. -/// -/// Handles '/' and '\\' correctly and deals with &fileignorecase option. -/// -/// @param[in] fname1 First file name. -/// @param[in] fname2 Second file name. -/// -/// @return 0 if they are equal, non-zero otherwise. -int path_fnamecmp(const char *fname1, const char *fname2) - FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT -{ -#ifdef BACKSLASH_IN_FILENAME - const size_t len1 = strlen(fname1); - const size_t len2 = strlen(fname2); - return path_fnamencmp(fname1, fname2, MAX(len1, len2)); -#else - return pathcmp(fname1, fname2, -1); -#endif -} - -/// Compare two file names -/// -/// Handles '/' and '\\' correctly and deals with &fileignorecase option. -/// -/// @param[in] fname1 First file name. -/// @param[in] fname2 Second file name. -/// @param[in] len Compare at most len bytes. -/// -/// @return 0 if they are equal, non-zero otherwise. -int path_fnamencmp(const char *const fname1, const char *const fname2, size_t len) - FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT -{ -#ifdef BACKSLASH_IN_FILENAME - int c1 = NUL; - int c2 = NUL; - - const char *p1 = fname1; - const char *p2 = fname2; - -# ifdef MSWIN - // To allow proper comparison of absolute paths: - // - one with explicit drive letter C:\xxx - // - another with implicit drive letter \xxx - // advance the pointer, of the explicit one, to skip the drive - for (int swap = 0, drive = NUL; swap < 2; swap++) { - // Handle absolute paths with implicit drive letter - c1 = utf_ptr2char(p1); - c2 = utf_ptr2char(p2); - - if ((c1 == '/' || c1 == '\\') && ASCII_ISALPHA(c2)) { - drive = mb_toupper(c2) - 'A' + 1; - - // Check for the colon - p2 += utfc_ptr2len(p2); - c2 = utf_ptr2char(p2); - if (c2 == ':' && drive == _getdrive()) { // skip the drive for comparison - p2 += utfc_ptr2len(p2); - break; - } else { // ignore - p2 -= utfc_ptr2len(p2); - } - } - - // swap pointers - const char *tmp = p1; - p1 = p2; - p2 = tmp; - } -# endif - - while (len > 0) { - c1 = utf_ptr2char(p1); - c2 = utf_ptr2char(p2); - if (c1 == NUL - || c2 == NUL - || (c1 != c2 - && ((c1 != '/' && c1 != '\\') || (c2 != '/' && c2 != '\\')) - && (!p_fic || utf_fold(c1) != utf_fold(c2)))) { - break; - } - len -= (size_t)utfc_ptr2len(p1); - p1 += utfc_ptr2len(p1); - p2 += utfc_ptr2len(p2); - } - return p_fic ? utf_fold(c1) - utf_fold(c2) : c1 - c2; -#else - if (p_fic) { - return mb_strnicmp(fname1, fname2, len); - } - return strncmp(fname1, fname2, len); -#endif -} - /// Append fname2 to fname1 /// /// @param[in] fname1 First fname to append to. @@ -616,7 +513,7 @@ bool path_has_wildcard(const char *p, bool all) static int pstrcmp(const void *a, const void *b) { - return pathcmp(*(char **)a, *(char **)b, -1); + return path_cmp(p_fic, *(char **)a, *(char **)b, MAXPATHL); } /// Recursively expands one path component into all matching files and/or @@ -786,7 +683,7 @@ static size_t do_path_expand(garray_T *gap, const char *path, size_t wildoff, in && (name[1] != '.' || name[2] != NUL))) && ((regmatch.regprog != NULL && vim_regexec(®match, name, 0)) || ((flags & EW_NOTWILD) - && path_fnamencmp(path + len, name, (size_t)(e - s)) == 0))) { + && path_cmp(p_fic, path + len, name, (size_t)(e - s)) == 0))) { len += (size_t)vim_snprintf(s, buflen - len, "%s", name); if (len + 1 >= buflen) { continue; @@ -881,7 +778,7 @@ static bool is_unique(char *maybe_unique, garray_T *gap, int i) continue; // it's different when it's shorter } char *rival = other_paths[j] + other_path_len - candidate_len; - if (path_fnamecmp(maybe_unique, rival) == 0 + if (path_equal(maybe_unique, rival, kPathCmpLiteral) && (rival == other_paths[j] || vim_ispathsep(*(rival - 1)))) { return false; // match } @@ -1047,7 +944,7 @@ static void uniquefy_paths(garray_T *gap, char *pattern, char *path_option) const char *dir_end = gettail_dir(path); len = strlen(path); - bool is_in_curdir = path_fnamencmp(curdir, path, (size_t)(dir_end - path)) == 0 + bool is_in_curdir = path_cmp(p_fic, curdir, path, (size_t)(dir_end - path)) == 0 && curdir[dir_end - path] == NUL; if (is_in_curdir) { in_curdir[i] = xmemdupz(path, len); @@ -2011,81 +1908,93 @@ bool same_directory(char *f1, char *f2) t1 = path_tail_with_sep(ffname); t2 = path_tail_with_sep(f2); return t1 - ffname == t2 - f2 - && pathcmp(ffname, f2, (int)(t1 - ffname)) == 0; + && path_cmp(p_fic, ffname, f2, (size_t)(t1 - ffname)) == 0; } -// Compare path "p[]" to "q[]". -// If `maxlen` >= 0 compare `p[maxlen]` to `q[maxlen]` -// Return value like strcmp(p, q), but consider path separators. -// -// See also `path_full_compare`. -int pathcmp(const char *p, const char *q, int maxlen) +int path_fold_char(bool ic, const char *p, int *len) + FUNC_ATTR_NONNULL_ALL { - int i, j; - const char *s = NULL; - - for (i = 0, j = 0; maxlen < 0 || (i < maxlen && j < maxlen);) { - int c1 = utf_ptr2char(p + i); - int c2 = utf_ptr2char(q + j); - - // End of "p": check if "q" also ends or just has a slash. - if (c1 == NUL) { - if (c2 == NUL) { // full match - return 0; - } - s = q; - i = j; - break; - } - - // End of "q": check if "p" just has a slash. - if (c2 == NUL) { - s = p; - break; - } - - if ((p_fic ? mb_toupper(c1) != mb_toupper(c2) : c1 != c2) -#ifdef BACKSLASH_IN_FILENAME - // consider '/' and '\\' to be equal - && !((c1 == '/' && c2 == '\\') - || (c1 == '\\' && c2 == '/')) -#endif - ) { - if (vim_ispathsep(c1)) { - return -1; - } - if (vim_ispathsep(c2)) { - return 1; - } - return p_fic ? mb_toupper(c1) - mb_toupper(c2) - : c1 - c2; // no match - } - - i += utfc_ptr2len(p + i); - j += utfc_ptr2len(q + j); + if (vim_ispathsep_nocolon(*p)) { + *len = 1; + return PATHSEP; } - if (s == NULL) { // "i" or "j" ran into "maxlen" + *len = ic ? utfc_ptr2len(p) : 1; + return ic ? utf_fold(utf_ptr2char(p)) : (uint8_t)*p; +} + +/// Compares filepaths (like `strncmp()`). Unlike `path_equal` this does not make filesystem +/// calls: only the names are compared (in a path-aware manner). +/// +/// Extensions: +/// - Treats "/" and "\" as equal on Windows. +/// - Consults 'fileignorecase': when set, characters are compared with +/// `utf_fold()`; otherwise verbatim. +/// - Ignores a single trailing path separator, e.g. +/// "foo" == "foo/", but "foo/" != "foo//". +/// - A path separator sorts before any other character, e.g. +/// "foo/bar" < "foobar". +/// - On Windows, consults the current drive when comparing an explicit drive +/// path with an implicit one, e.g. +/// "C:/foo" == "/foo" when the current drive is "C:". +/// +/// @param ic True if case is to be ignored. +/// @param p First path. +/// @param q Second path. +/// @param maxlen Maximum number of bytes to compare. +/// +/// @return 0 if the paths are equal, non-zero otherwise. +int path_cmp(bool ic, const char *p, const char *q, size_t maxlen) +{ + const char *s = NULL; + int c1 = NUL; + int c2 = NUL; + size_t len = 0; + +#ifdef MSWIN + const char **pp = NULL; + if (vim_ispathsep_nocolon(*p) && ASCII_ISALPHA(*q) && q[1] == ':') { + pp = &q; + } else if (vim_ispathsep_nocolon(*q) && ASCII_ISALPHA(*p) && p[1] == ':') { + pp = &p; + } + if (pp && TOLOWER_ASC(**pp) == _getdrive() + 'a' - 1) { + *pp += 2; // advance the pointer of the explicit one, to skip the drive + } +#endif + + for (int i = 0, j = 0; len < maxlen; len += (size_t)i) { + c1 = path_fold_char(ic, p, &i); + c2 = path_fold_char(ic, q, &j); + if (c1 == NUL || c2 == NUL || c1 != c2) { + break; + } + p += i; + q += j; + } + + if ((c1 == NUL && c2 == NUL) || len >= maxlen) { return 0; } - int c1 = utf_ptr2char(s + i); - int c2 = utf_ptr2char(s + i + utfc_ptr2len(s + i)); - // ignore a trailing slash, but not "//" or ":/" - if (c2 == NUL - && i > 0 - && !after_pathsep(s, s + i) -#ifdef BACKSLASH_IN_FILENAME - && (c1 == '/' || c1 == '\\') -#else - && c1 == '/' -#endif - ) { - return 0; // match with trailing slash + if (c1 != NUL && c2 != NUL) { + if (vim_ispathsep(c1)) { + return -1; + } + if (vim_ispathsep(c2)) { + return 1; + } + return c1 - c2; } - if (s == q) { - return -1; // no match + + s = c1 == NUL ? q : p; + // match with a single trailing slash, but not "//" or ":/" + if (vim_ispathsep_nocolon(*s) + && s[1] == NUL + && len > 0 + && !vim_ispathsep(s[-1])) { + return 0; } - return 1; + return s == q ? -1 : 1; } /// Try to find a shortname by comparing the fullname with the current @@ -2129,7 +2038,7 @@ char *path_shorten_fname(char *full_path, char *dir_name) // If full_path and dir_name do not match, it's impossible to make one // relative to the other. - if (path_fnamencmp(dir_name, full_path, len) != 0) { + if (path_cmp(p_fic, dir_name, full_path, len) != 0) { return NULL; } @@ -2300,7 +2209,7 @@ bool match_suffix(char *fname) } } else { if (fnamelen >= setsuflen - && path_fnamencmp(suf_buf, fname + fnamelen - setsuflen, setsuflen) == 0) { + && path_cmp(p_fic, suf_buf, fname + fnamelen - setsuflen, setsuflen) == 0) { break; } setsuflen = 0; diff --git a/src/nvim/path.h b/src/nvim/path.h index b5c5c17e1c..dbc961c4bb 100644 --- a/src/nvim/path.h +++ b/src/nvim/path.h @@ -32,14 +32,12 @@ enum { // Note: mostly EW_NOTFOUND and EW_SILENT are mutually exclusive: EW_NOTFOUND // is used when executing commands and EW_SILENT for interactive expanding. -/// Return value for the comparison of two files. Also @see path_full_compare. -typedef enum file_comparison { - kEqualFiles = 1, ///< Both exist and are the same file. - kDifferentFiles = 2, ///< Both exist and are different files. - kBothFilesMissing = 4, ///< Both don't exist. - kOneFileMissing = 6, ///< One of them doesn't exist. - kEqualFileNames = 7, ///< Both don't exist and file names are same. -} FileComparison; +/// path_equal() flags +typedef enum { + kPathCmpLiteral = 1 << 0, ///< Compare paths literally. + kPathCmpExpand = 1 << 1, ///< Expand env vars in the first path. + kPathCmpFull = 1 << 2, ///< Compare full names when FileIds are unavailable. +} PathCmpFlags; #ifdef BACKSLASH_IN_FILENAME # define TO_SLASH(p) path_to_slash(p) diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 00a2cd9154..7884f06bfc 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -1948,7 +1948,7 @@ static int qf_add_entry(qf_list_T *qfl, char *dir, char *fname, char *module, in } qfp->qf_fname = NULL; if (buf != NULL && buf->b_ffname != NULL && fullname != NULL) { - if (path_fnamecmp(fullname, buf->b_ffname) != 0) { + if (!path_equal(fullname, buf->b_ffname, kPathCmpLiteral)) { p = path_try_shorten_fname(fullname); if (p != NULL) { qfp->qf_fname = xstrdup(p); diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 9890703b17..4936c37558 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -1102,7 +1102,7 @@ static int add_pack_dir_to_rtp(char *fname, bool is_pack) if (rtp_ffname == NULL) { goto theend; } - if (path_fnamencmp(rtp_ffname, ffname, fname_len) == 0) { + if (path_cmp(p_fic, rtp_ffname, ffname, fname_len) == 0) { // Insert "ffname" after this entry (and comma). insp = entry; } @@ -1283,7 +1283,7 @@ static void add_pack_plugins(bool opt, int num_fnames, char **fnames, bool all, const char *p = p_rtp; while (*p != NUL) { copy_option_part((char **)&p, buf, MAXPATHL, ","); - if (path_fnamecmp(buf, fnames[i]) == 0) { + if (path_equal(buf, fnames[i], kPathCmpLiteral)) { found = true; break; } @@ -2519,7 +2519,7 @@ int find_script_by_name(char *name) // - If a script is deleted and another script is written, with a // different name, the inode may be re-used. scriptitem_T *si = SCRIPT_ITEM(sid); - if (si->sn_name != NULL && path_fnamecmp(si->sn_name, name) == 0) { + if (si->sn_name != NULL && path_equal(si->sn_name, name, kPathCmpLiteral)) { return sid; } } diff --git a/src/nvim/search.c b/src/nvim/search.c index 132a34da5d..c2e8f3842f 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3060,8 +3060,7 @@ void find_pattern_in_path(char *ptr, Direction dir, size_t len, bool whole, bool if (i == max_path_depth) { break; } - if (path_full_compare(new_fname, files[i].name, true, - true) & kEqualFiles) { + if (path_equal(new_fname, files[i].name, kPathCmpExpand | kPathCmpFull)) { if (type != CHECK_PATH && action == ACTION_SHOW_ALL && files[i].matched) { msg_putchar('\n'); // cursor below last one diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 41c269bc0b..d3d277a064 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -849,7 +849,7 @@ static buf_T *find_buffer(PMap(cstr_t) *const fname_bufs, const char *const fnam FOR_ALL_BUFFERS(buf) { if (buf->b_ffname != NULL) { - if (path_fnamecmp(fname, buf->b_ffname) == 0) { + if (path_equal(fname, buf->b_ffname, kPathCmpLiteral)) { *ref = buf; return buf; } @@ -1917,7 +1917,7 @@ static inline ShaDaWriteResult shada_read_when_writing(FileDescriptor *const sd_ } else { FOR_ALL_BUFFERS(buf) { if (buf->b_ffname != NULL - && path_fnamecmp(entry.data.filemark.fname, buf->b_ffname) == 0) { + && path_equal(entry.data.filemark.fname, buf->b_ffname, kPathCmpLiteral)) { fmark_T fm; mark_get(buf, curwin, &fm, kMarkBufLocal, (int)entry.data.filemark.name); if (fm.timestamp >= entry.timestamp) { diff --git a/src/nvim/spell.c b/src/nvim/spell.c index 4975a331a8..8a37bc85cd 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -1974,7 +1974,7 @@ char *parse_spelllang(win_T *wp) // If the name ends in ".spl" use it as the name of the spell file. // If there is a region name let "region" point to it and remove it // from the name. - if (len > 4 && path_fnamecmp(lang + len - 4, ".spl") == 0) { + if (len > 4 && path_equal(lang + len - 4, ".spl", kPathCmpLiteral)) { filename = true; // Locate a region and remove it from the file name. @@ -1990,8 +1990,7 @@ char *parse_spelllang(win_T *wp) // Check if we loaded this language before. for (slang = first_lang; slang != NULL; slang = slang->sl_next) { - if (path_full_compare(lang, slang->sl_fname, false, true) - == kEqualFiles) { + if (path_equal(lang, slang->sl_fname, kPathCmpExpand)) { break; } } @@ -2039,7 +2038,7 @@ char *parse_spelllang(win_T *wp) // Loop over the languages, there can be several files for "lang". for (slang = first_lang; slang != NULL; slang = slang->sl_next) { if (filename - ? path_full_compare(lang, slang->sl_fname, false, true) == kEqualFiles + ? path_equal(lang, slang->sl_fname, kPathCmpExpand) : STRICMP(lang, slang->sl_name) == 0) { int region_mask = REGION_ALL; if (!filename && region != NULL) { @@ -2098,7 +2097,7 @@ char *parse_spelllang(win_T *wp) for (c = 0; c < ga.ga_len; c++) { char *p = LANGP_ENTRY(ga, c)->lp_slang->sl_fname; if (p != NULL - && path_full_compare(spf_name, p, false, true) == kEqualFiles) { + && path_equal(spf_name, p, kPathCmpExpand)) { break; } } @@ -2111,8 +2110,7 @@ char *parse_spelllang(win_T *wp) // Check if it was loaded already. for (slang = first_lang; slang != NULL; slang = slang->sl_next) { - if (path_full_compare(spf_name, slang->sl_fname, false, true) - == kEqualFiles) { + if (path_equal(spf_name, slang->sl_fname, kPathCmpExpand)) { break; } } diff --git a/src/nvim/spellfile.c b/src/nvim/spellfile.c index 632b995143..cfc7534e2b 100644 --- a/src/nvim/spellfile.c +++ b/src/nvim/spellfile.c @@ -918,7 +918,7 @@ void suggest_load_files(void) slang->sl_sugloaded = true; char *dotp = strrchr(slang->sl_fname, '.'); - if (dotp == NULL || path_fnamecmp(dotp, ".spl") != 0) { + if (dotp == NULL || !path_equal(dotp, ".spl", kPathCmpLiteral)) { continue; } STRCPY(dotp, ".sug"); @@ -1840,7 +1840,7 @@ static void spell_reload_one(char *fname, bool added_word) bool didit = false; for (slang_T *slang = first_lang; slang != NULL; slang = slang->sl_next) { - if (path_full_compare(fname, slang->sl_fname, false, true) == kEqualFiles) { + if (path_equal(fname, slang->sl_fname, kPathCmpExpand)) { slang_clear(slang); if (spell_load_file(fname, NULL, slang, false) == NULL) { // reloading failed, clear the language @@ -4876,8 +4876,7 @@ static void spell_make_sugfile(spellinfo_T *spin, char *wfname) // of the code for the soundfolding stuff. // It might have been done already by spell_reload_one(). for (slang = first_lang; slang != NULL; slang = slang->sl_next) { - if (path_full_compare(wfname, slang->sl_fname, false, true) - == kEqualFiles) { + if (path_equal(wfname, slang->sl_fname, kPathCmpExpand)) { break; } } diff --git a/src/nvim/tag.c b/src/nvim/tag.c index e00b9b874d..5cb723e299 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -2982,7 +2982,7 @@ static char *expand_tag_fname(char *fname, char *const tag_fname, const bool exp } /// Check if we have a tag for the buffer with name "buf_ffname". -/// This is a bit slow, because of the full path compare in path_full_compare(). +/// This is a bit slow, because of the full path compare in path_equal(). /// /// @return true if tag for file "fname" if tag file "tag_fname" is for current /// file. @@ -2997,7 +2997,7 @@ static int test_for_current(char *fname, char *fname_end, char *tag_fname, char *fname_end = NUL; } char *fullname = expand_tag_fname(fname, tag_fname, true); - retval = (path_full_compare(fullname, buf_ffname, true, true) & kEqualFiles); + retval = path_equal(fullname, buf_ffname, kPathCmpExpand | kPathCmpFull); xfree(fullname); *fname_end = c; } diff --git a/src/nvim/window.c b/src/nvim/window.c index 37662d3b35..d0531e6589 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -5389,7 +5389,7 @@ void update_cwd(CdCause cause) globaldir = xstrdup(cwd); } } - bool dir_differs = pathcmp(new_dir, cwd, -1) != 0; + bool dir_differs = !path_equal(new_dir, cwd, kPathCmpLiteral); if (!p_acd && dir_differs) { do_autocmd_dirchanged(new_dir, scope, cause, true); } @@ -5403,7 +5403,7 @@ void update_cwd(CdCause cause) } else if (globaldir != NULL) { // Window nor buffer have a local directory and we are not in the global // directory: Change to the global directory. - bool dir_differs = pathcmp(globaldir, cwd, -1) != 0; + bool dir_differs = !path_equal(globaldir, cwd, kPathCmpLiteral); if (!p_acd && dir_differs) { do_autocmd_dirchanged(globaldir, kCdScopeGlobal, cause, true); } diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index 7814687add..6fb96331a6 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -869,4 +869,17 @@ describe('autocmd', function() eq(vim.fs.normalize('~'), cmds[1].pattern) eq(vim.fs.normalize(path), cmds[2].pattern) end) + + it('exists() consults &fileignorecase', function() + command([[autocmd User foo/bar echo]]) + eq(0, fn.exists([[#User#foo/bar/]])) + eq(1, fn.exists([[#User#foo/bar]])) + -- Even on Windows, `/` should be used as the path sep + eq(0, fn.exists([[#User#foo\bar]])) + + command([[set fileignorecase]]) + eq(1, fn.exists([[#User#Foo/Bar]])) + command([[set nofileignorecase]]) + eq(0, fn.exists([[#User#Foo/Bar]])) + end) end) diff --git a/test/functional/shada/shada_spec.lua b/test/functional/shada/shada_spec.lua index f04644858a..9bcfbde99b 100644 --- a/test/functional/shada/shada_spec.lua +++ b/test/functional/shada/shada_spec.lua @@ -305,26 +305,37 @@ describe('ShaDa support code', function() end) it('deduplicates items on case-insensitive systems', function() - local file = ('%s/nonexist/dir/héllo'):format(dirname) + local file = ('%s/nonexistent/héllo'):format(dirname) nvim_command(('edit %s'):format(file)) file = api.nvim_buf_get_name(0) + + local file_upper = fn.toupper(file) + if t.is_os('win') then + file_upper = file_upper:sub(3) -- drop drive letter + end + + nvim_command(('edit %s'):format(file_upper)) feed('i1') nvim_command(('wshada! %s'):format(dirshada)) nvim_command('bw!') - local upper = fn.toupper(file) - if t.is_os('win') then - upper = upper:sub(3) - end - nvim_command(('edit %s'):format(upper)) + + -- path_cmp() ignores a single trailing slash + local file_slash = ('%s/'):format(file) + nvim_command(('edit %s'):format(file_slash)) feed('i123') nvim_command('mark a') nvim_command(('wshada %s'):format(dirshada)) nvim_command('bw!') nvim_command(('rshada! %s'):format(dirshada)) local oldfiles = api.nvim_get_vvar('oldfiles') - eq((t.is_os('win') or t.is_os('mac')) and 1 or 2, #oldfiles) -- Both filenames appear in shada file, but iteration order is unspecified. - t.ok(oldfiles[1] == file or oldfiles[1] == upper) + if t.is_os('win') or t.is_os('mac') then + eq(1, #oldfiles) + t.ok(oldfiles[1] == file or oldfiles[1] == file_slash) + else + eq(2, #oldfiles) + t.ok(oldfiles[1] == file or oldfiles[1] == file_upper) + end end) end) diff --git a/test/functional/vimscript/fnamemodify_spec.lua b/test/functional/vimscript/fnamemodify_spec.lua index c9b93dd31d..d4a27e084b 100644 --- a/test/functional/vimscript/fnamemodify_spec.lua +++ b/test/functional/vimscript/fnamemodify_spec.lua @@ -205,6 +205,18 @@ describe('fnamemodify()', function() eq('txt', fnamemodify('path/to/hello.txt', ':e')) end) + it(':~', function() + local cwd = vim.fs.normalize(fnamemodify('.', ':p')) + local full_path = ('%s/foo'):format(cwd) + + command(([[let $HOME='%s']]):format(cwd)) + eq('~/foo', fnamemodify(full_path, ':~')) + + command(([[let $HOME='%s/']]):format(cwd)) -- a trailing slash + -- `init_home` calls `os_realpath` on Unix, which removes the trailing slash + eq(is_os('win') and full_path or '~/foo', fnamemodify(full_path, ':~')) + end) + it('regex replacements', function() eq('content-there-here.txt', fnamemodify('content-here-here.txt', ':s/here/there/')) eq('content-there-there.txt', fnamemodify('content-here-here.txt', ':gs/here/there/')) diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index d3956ec502..5d9edc8d9e 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -109,11 +109,11 @@ describe('path.c', function() end) end) - describe('path_full_compare', function() - local function path_full_compare(s1, s2, cn, ee) + describe('path_equal', function() + local function path_equal(s1, s2, flags) s1 = to_cstr(s1) s2 = to_cstr(s2) - return cimp.path_full_compare(s1, s2, cn or 0, ee or 1) + return cimp.path_equal(s1, s2, flags or cimp.kPathCmpExpand) end local f1 = 'f1.o' @@ -129,26 +129,72 @@ describe('path.c', function() os.remove(f2) end) - itp('returns kEqualFiles when passed the same file', function() - eq(cimp.kEqualFiles, (path_full_compare(f1, f1))) + itp('returns true when passed the same existing file', function() + eq(true, path_equal(f1, f1)) + eq(true, path_equal(f1, ('%s/%s'):format(uv.fs_realpath('.'), f1))) end) - itp('returns kEqualFileNames when files that dont exist and have same name', function() - eq(cimp.kEqualFileNames, (path_full_compare('null.txt', 'null.txt', true))) + itp( + 'returns true for nonexistent same-name files via fullname fallback (kPathCmpFull)', + function() + eq(true, path_equal('null.txt', 'null.txt', cimp.kPathCmpFull)) + end + ) + + itp('returns false for nonexistent same-name files without fullname fallback', function() + eq(false, path_equal('null.txt', 'null.txt')) end) - itp('returns kBothFilesMissing when files that dont exist', function() - eq(cimp.kBothFilesMissing, (path_full_compare('null.txt', 'null.txt'))) + itp('returns false when passed different files', function() + eq(false, path_equal(f1, f2)) + eq(false, path_equal(f2, f1)) end) - itp('returns kDifferentFiles when passed different files', function() - eq(cimp.kDifferentFiles, (path_full_compare(f1, f2))) - eq(cimp.kDifferentFiles, (path_full_compare(f2, f1))) + itp('returns false if only one does not exist', function() + eq(false, path_equal(f1, 'null.txt')) + eq(false, path_equal('null.txt', f1)) end) - itp('returns kOneFileMissing if only one does not exist', function() - eq(cimp.kOneFileMissing, (path_full_compare(f1, 'null.txt'))) - eq(cimp.kOneFileMissing, (path_full_compare('null.txt', f1))) + itp('returns false if two files differ literally', function() + eq(false, path_equal('null1.txt', 'null2.txt', cimp.kPathCmpLiteral)) + end) + + itp("respects 'fileignorecase' option", function() + options.p_fic = false + eq(false, path_equal('Foo', 'foo', cimp.kPathCmpLiteral)) + options.p_fic = true + eq(true, path_equal('Foo', 'foo', cimp.kPathCmpLiteral)) + -- mb_toupper considers ß and ẞ equal, but not İ and i. + -- utf_fold keeps them as they are. + eq(false, path_equal('foß', 'foẞ', cimp.kPathCmpLiteral)) + eq(false, path_equal('foİ', 'foi', cimp.kPathCmpLiteral)) + end) + end) + + describe('path_cmp', function() + local function path_cmp(a, b, maxlen) + return cimp.path_cmp(options.p_fic, to_cstr(a), to_cstr(b), maxlen) + end + + itp('returns 0 when passed same paths', function() + eq(0, path_cmp('foo/bar', 'foo/bar', 7)) + end) + + itp('returns non-zero when passed different paths', function() + eq(1, path_cmp('foobar', 'foo/bar', 7)) + eq(-1, path_cmp('foo/bar', 'foobar', 7)) + neq(0, path_cmp('foo/bar', 'foo/baz', 7)) + end) + + itp('returns 0 when maxlen truncates to a common prefix', function() + eq(0, path_cmp('foo/bar', 'foo/baz', 6)) + end) + + itp('ignores a single trailing path sep', function() + eq(0, path_cmp('foo', 'foo/', 4)) + neq(0, path_cmp('/', '//', 2)) + neq(0, path_cmp('foo', 'foo//', 5)) + neq(0, path_cmp('foo/', 'foo//', 5)) end) end)