From 1063f3d2008f22d02ccfa9dab83a23db52febbdc Mon Sep 17 00:00:00 2001 From: Bram Moolenaar Date: Tue, 7 May 2019 22:06:52 +0200 Subject: [PATCH] patch 8.1.1291: not easy to change directory and restore Problem: Not easy to change directory and restore. Solution: Add the chdir() function. (Yegappan Lakshmanan, closes #4358) --- runtime/doc/eval.txt | 22 +++++ runtime/doc/usr_41.txt | 1 + src/evalfunc.c | 41 +++++++++ src/ex_docmd.c | 180 +++++++++++++++++++++++----------------- src/if_py_both.h | 2 +- src/proto/ex_docmd.pro | 3 +- src/structs.h | 7 ++ src/testdir/test_cd.vim | 43 +++++++++- src/version.c | 2 + 9 files changed, 220 insertions(+), 81 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index a39f72b55..84001b7a0 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2273,6 +2273,7 @@ ch_status({handle} [, {options}]) String status of channel {handle} changenr() Number current change number char2nr({expr} [, {utf8}]) Number ASCII/UTF8 value of first char in {expr} +chdir({dir}) String change current working directory cindent({lnum}) Number C indent for line {lnum} clearmatches([{win}]) none clear all matches col({expr}) Number column nr of cursor or mark @@ -3469,6 +3470,27 @@ char2nr({expr} [, {utf8}]) *char2nr()* let list = map(split(str, '\zs'), {_, val -> char2nr(val)}) < Result: [65, 66, 67] +chdir({dir}) *chdir()* + Change the current working directory to {dir}. The scope of + the directory change depends on the directory of the current + window: + - If the current window has a window-local directory + (|:lcd|), then changes the window local directory. + - Otherwise, if the current tabpage has a local + directory (|:tcd|) then changes the tabpage local + directory. + - Otherwise, changes the global directory. + If successful, returns the previous working directory. Pass + this to another chdir() to restore the directory. + On failure, returns an empty string. + + Example: > + let save_dir = chdir(newdir) + if save_dir + " ... do some work + call chdir(save_dir) + endif +< cindent({lnum}) *cindent()* Get the amount of indent for line {lnum} according the C indenting rules, as with 'cindent'. diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index cef6fd793..f874b073b 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -769,6 +769,7 @@ System functions and manipulation of files: haslocaldir() check if current window used |:lcd| or |:tcd| tempname() get the name of a temporary file mkdir() create a new directory + chdir() change current working directory delete() delete a file rename() rename a file system() get the result of a shell command as a string diff --git a/src/evalfunc.c b/src/evalfunc.c index ba3db95c9..ca412f754 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -107,6 +107,7 @@ static void f_ch_status(typval_T *argvars, typval_T *rettv); #endif static void f_changenr(typval_T *argvars, typval_T *rettv); static void f_char2nr(typval_T *argvars, typval_T *rettv); +static void f_chdir(typval_T *argvars, typval_T *rettv); static void f_cindent(typval_T *argvars, typval_T *rettv); static void f_clearmatches(typval_T *argvars, typval_T *rettv); static void f_col(typval_T *argvars, typval_T *rettv); @@ -597,6 +598,7 @@ static struct fst #endif {"changenr", 0, 0, f_changenr}, {"char2nr", 1, 2, f_char2nr}, + {"chdir", 1, 1, f_chdir}, {"cindent", 1, 1, f_cindent}, {"clearmatches", 0, 1, f_clearmatches}, {"col", 1, 1, f_col}, @@ -2490,6 +2492,45 @@ f_char2nr(typval_T *argvars, typval_T *rettv) rettv->vval.v_number = tv_get_string(&argvars[0])[0]; } +/* + * "chdir(dir)" function + */ + static void +f_chdir(typval_T *argvars, typval_T *rettv) +{ + char_u *cwd; + cdscope_T scope = CDSCOPE_GLOBAL; + + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + + if (argvars[0].v_type != VAR_STRING) + return; + + // Return the current directory + cwd = alloc(MAXPATHL); + if (cwd != NULL) + { + if (mch_dirname(cwd, MAXPATHL) != FAIL) + { +#ifdef BACKSLASH_IN_FILENAME + slash_adjust(cwd); +#endif + rettv->vval.v_string = vim_strsave(cwd); + } + vim_free(cwd); + } + + if (curwin->w_localdir != NULL) + scope = CDSCOPE_WINDOW; + else if (curtab->tp_localdir != NULL) + scope = CDSCOPE_TABPAGE; + + if (!changedir_func(argvars[0].vval.v_string, TRUE, scope)) + // Directory change failed + VIM_CLEAR(rettv->vval.v_string); +} + /* * "cindent(lnum)" function */ diff --git a/src/ex_docmd.c b/src/ex_docmd.c index 1393d0e2f..a2b302d92 100644 --- a/src/ex_docmd.c +++ b/src/ex_docmd.c @@ -7513,17 +7513,17 @@ free_cd_dir(void) /* * Deal with the side effects of changing the current directory. - * When "tablocal" is TRUE then this was after an ":tcd" command. - * When "winlocal" is TRUE then this was after an ":lcd" command. + * When 'scope' is CDSCOPE_TABPAGE then this was after an ":tcd" command. + * When 'scope' is CDSCOPE_WINDOW then this was after an ":lcd" command. */ void -post_chdir(int tablocal, int winlocal) +post_chdir(cdscope_T scope) { - if (!winlocal) + if (scope != CDSCOPE_WINDOW) // Clear tab local directory for both :cd and :tcd VIM_CLEAR(curtab->tp_localdir); VIM_CLEAR(curwin->w_localdir); - if (winlocal || tablocal) + if (scope != CDSCOPE_GLOBAL) { /* If still in global directory, need to remember current * directory as global directory. */ @@ -7532,7 +7532,7 @@ post_chdir(int tablocal, int winlocal) /* Remember this local directory for the window. */ if (mch_dirname(NameBuff, MAXPATHL) == OK) { - if (tablocal) + if (scope == CDSCOPE_TABPAGE) curtab->tp_localdir = vim_strsave(NameBuff); else curwin->w_localdir = vim_strsave(NameBuff); @@ -7548,102 +7548,126 @@ post_chdir(int tablocal, int winlocal) shorten_fnames(TRUE); } - /* - * ":cd", ":tcd", ":lcd", ":chdir" ":tchdir" and ":lchdir". + * Change directory function used by :cd/:tcd/:lcd Ex commands and the + * chdir() function. If 'winlocaldir' is TRUE, then changes the window-local + * directory. If 'tablocaldir' is TRUE, then changes the tab-local directory. + * Otherwise changes the global directory. + * Returns TRUE if the directory is successfully changed. */ - void -ex_cd(exarg_T *eap) + int +changedir_func( + char_u *new_dir, + int forceit, + cdscope_T scope) { - char_u *new_dir; char_u *tofree; int dir_differs; + int retval = FALSE; - new_dir = eap->arg; -#if !defined(UNIX) && !defined(VMS) - /* for non-UNIX ":cd" means: print current directory */ - if (*new_dir == NUL) - ex_pwd(NULL); - else -#endif + if (allbuf_locked()) + return FALSE; + + if (vim_strchr(p_cpo, CPO_CHDIR) != NULL && curbufIsChanged() && !forceit) { - if (allbuf_locked()) - return; - if (vim_strchr(p_cpo, CPO_CHDIR) != NULL && curbufIsChanged() - && !eap->forceit) - { - emsg(_("E747: Cannot change directory, buffer is modified (add ! to override)")); - return; - } + emsg(_("E747: Cannot change directory, buffer is modified (add ! to override)")); + return FALSE; + } - /* ":cd -": Change to previous directory */ - if (STRCMP(new_dir, "-") == 0) + // ":cd -": Change to previous directory + if (STRCMP(new_dir, "-") == 0) + { + if (prev_dir == NULL) { - if (prev_dir == NULL) - { - emsg(_("E186: No previous directory")); - return; - } - new_dir = prev_dir; + emsg(_("E186: No previous directory")); + return FALSE; } + new_dir = prev_dir; + } - /* Save current directory for next ":cd -" */ - tofree = prev_dir; - if (mch_dirname(NameBuff, MAXPATHL) == OK) - prev_dir = vim_strsave(NameBuff); - else - prev_dir = NULL; + // Save current directory for next ":cd -" + tofree = prev_dir; + if (mch_dirname(NameBuff, MAXPATHL) == OK) + prev_dir = vim_strsave(NameBuff); + else + prev_dir = NULL; #if defined(UNIX) || defined(VMS) - /* for UNIX ":cd" means: go to home directory */ - if (*new_dir == NUL) - { - /* use NameBuff for home directory name */ + // for UNIX ":cd" means: go to home directory + if (*new_dir == NUL) + { + // use NameBuff for home directory name # ifdef VMS - char_u *p; + char_u *p; - p = mch_getenv((char_u *)"SYS$LOGIN"); - if (p == NULL || *p == NUL) /* empty is the same as not set */ - NameBuff[0] = NUL; - else - vim_strncpy(NameBuff, p, MAXPATHL - 1); + p = mch_getenv((char_u *)"SYS$LOGIN"); + if (p == NULL || *p == NUL) // empty is the same as not set + NameBuff[0] = NUL; + else + vim_strncpy(NameBuff, p, MAXPATHL - 1); # else - expand_env((char_u *)"$HOME", NameBuff, MAXPATHL); + expand_env((char_u *)"$HOME", NameBuff, MAXPATHL); # endif - new_dir = NameBuff; - } + new_dir = NameBuff; + } #endif - dir_differs = new_dir == NULL || prev_dir == NULL - || pathcmp((char *)prev_dir, (char *)new_dir, -1) != 0; - if (new_dir == NULL || (dir_differs && vim_chdir(new_dir))) - emsg(_(e_failed)); - else + dir_differs = new_dir == NULL || prev_dir == NULL + || pathcmp((char *)prev_dir, (char *)new_dir, -1) != 0; + if (new_dir == NULL || (dir_differs && vim_chdir(new_dir))) + emsg(_(e_failed)); + else + { + char_u *acmd_fname; + + post_chdir(scope); + + if (dir_differs) { - char_u *acmd_fname; - int is_winlocal_chdir = eap->cmdidx == CMD_lcd - || eap->cmdidx == CMD_lchdir; - int is_tablocal_chdir = eap->cmdidx == CMD_tcd - || eap->cmdidx == CMD_tchdir; + if (scope == CDSCOPE_WINDOW) + acmd_fname = (char_u *)"window"; + else if (scope == CDSCOPE_TABPAGE) + acmd_fname = (char_u *)"tabpage"; + else + acmd_fname = (char_u *)"global"; + apply_autocmds(EVENT_DIRCHANGED, acmd_fname, new_dir, FALSE, + curbuf); + } + retval = TRUE; + } + vim_free(tofree); + + return retval; +} + +/* + * ":cd", ":tcd", ":lcd", ":chdir" ":tchdir" and ":lchdir". + */ + void +ex_cd(exarg_T *eap) +{ + char_u *new_dir; - post_chdir(is_tablocal_chdir, is_winlocal_chdir); + new_dir = eap->arg; +#if !defined(UNIX) && !defined(VMS) + // for non-UNIX ":cd" means: print current directory + if (*new_dir == NUL) + ex_pwd(NULL); + else +#endif + { + cdscope_T scope = CDSCOPE_GLOBAL; - /* Echo the new current directory if the command was typed. */ + if (eap->cmdidx == CMD_lcd || eap->cmdidx == CMD_lchdir) + scope = CDSCOPE_WINDOW; + else if (eap->cmdidx == CMD_tcd || eap->cmdidx == CMD_tchdir) + scope = CDSCOPE_TABPAGE; + + if (changedir_func(new_dir, eap->forceit, scope)) + { + // Echo the new current directory if the command was typed. if (KeyTyped || p_verbose >= 5) ex_pwd(eap); - - if (dir_differs) - { - if (is_winlocal_chdir) - acmd_fname = (char_u *)"window"; - else if (is_tablocal_chdir) - acmd_fname = (char_u *)"tabpage"; - else - acmd_fname = (char_u *)"global"; - apply_autocmds(EVENT_DIRCHANGED, acmd_fname, - new_dir, FALSE, curbuf); - } } - vim_free(tofree); } } diff --git a/src/if_py_both.h b/src/if_py_both.h index ede2f5cde..498972d54 100644 --- a/src/if_py_both.h +++ b/src/if_py_both.h @@ -1032,7 +1032,7 @@ _VimChdir(PyObject *_chdir, PyObject *args, PyObject *kwargs) Py_DECREF(newwd); Py_XDECREF(todecref); - post_chdir(FALSE, FALSE); + post_chdir(CDSCOPE_GLOBAL); if (VimTryEnd()) { diff --git a/src/proto/ex_docmd.pro b/src/proto/ex_docmd.pro index 9934d60fc..6714b38c4 100644 --- a/src/proto/ex_docmd.pro +++ b/src/proto/ex_docmd.pro @@ -37,7 +37,8 @@ void ex_splitview(exarg_T *eap); void tabpage_new(void); void do_exedit(exarg_T *eap, win_T *old_curwin); void free_cd_dir(void); -void post_chdir(int tablocal, int winlocal); +void post_chdir(cdscope_T cdscope); +int changedir_func(char_u *new_dir, int forceit, cdscope_T cdscope); void ex_cd(exarg_T *eap); void do_sleep(long msec); void ex_may_print(exarg_T *eap); diff --git a/src/structs.h b/src/structs.h index fa8a7655f..ca678f83b 100644 --- a/src/structs.h +++ b/src/structs.h @@ -3555,3 +3555,10 @@ typedef struct { varnumber_T vv_count; varnumber_T vv_count1; } vimvars_save_T; + +// Scope for changing directory +typedef enum { + CDSCOPE_GLOBAL, // :cd + CDSCOPE_TABPAGE, // :tcd + CDSCOPE_WINDOW // :lcd +} cdscope_T; diff --git a/src/testdir/test_cd.vim b/src/testdir/test_cd.vim index c63f0060f..31859542e 100644 --- a/src/testdir/test_cd.vim +++ b/src/testdir/test_cd.vim @@ -1,4 +1,4 @@ -" Test for :cd +" Test for :cd and chdir() func Test_cd_large_path() " This used to crash with a heap write overflow. @@ -65,3 +65,44 @@ func Test_cd_with_cpo_chdir() set cpo& bw! endfunc + +" Test for chdir() +func Test_chdir_func() + let topdir = getcwd() + call mkdir('Xdir/y/z', 'p') + + " Create a few tabpages and windows with different directories + new + cd Xdir + tabnew + tcd y + below new + below new + lcd z + + tabfirst + call chdir('..') + call assert_equal('y', fnamemodify(getcwd(1, 2), ':t')) + call assert_equal('z', fnamemodify(getcwd(3, 2), ':t')) + tabnext | wincmd t + call chdir('..') + call assert_equal('Xdir', fnamemodify(getcwd(1, 2), ':t')) + call assert_equal('Xdir', fnamemodify(getcwd(2, 2), ':t')) + call assert_equal('z', fnamemodify(getcwd(3, 2), ':t')) + call assert_equal('testdir', fnamemodify(getcwd(1, 1), ':t')) + 3wincmd w + call chdir('..') + call assert_equal('Xdir', fnamemodify(getcwd(1, 2), ':t')) + call assert_equal('Xdir', fnamemodify(getcwd(2, 2), ':t')) + call assert_equal('y', fnamemodify(getcwd(3, 2), ':t')) + call assert_equal('testdir', fnamemodify(getcwd(1, 1), ':t')) + + " Error case + call assert_fails("call chdir('dir-abcd')", 'E472:') + silent! let d = chdir("dir_abcd") + call assert_equal("", d) + + only | tabonly + exe 'cd ' . topdir + call delete('Xdir', 'rf') +endfunc diff --git a/src/version.c b/src/version.c index 676c38306..09bb1be85 100644 --- a/src/version.c +++ b/src/version.c @@ -767,6 +767,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1291, /**/ 1290, /**/ -- 2.40.0