changeset 16576:bcc343175103 v8.1.1291

patch 8.1.1291: not easy to change directory and restore commit https://github.com/vim/vim/commit/1063f3d2008f22d02ccfa9dab83a23db52febbdc Author: Bram Moolenaar <Bram@vim.org> Date: Tue May 7 22:06:52 2019 +0200 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 https://github.com/vim/vim/issues/4358)
author Bram Moolenaar <Bram@vim.org>
date Tue, 07 May 2019 22:15:05 +0200
parents e0baba49b677
children 3afc140c9ef6
files runtime/doc/eval.txt runtime/doc/usr_41.txt src/evalfunc.c src/ex_docmd.c src/if_py_both.h src/proto/ex_docmd.pro src/structs.h src/testdir/test_cd.vim src/version.c
diffstat 9 files changed, 227 insertions(+), 88 deletions(-) [+]
line wrap: on
line diff
--- 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'.
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -769,6 +769,7 @@ System functions and manipulation of fil
 	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
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -107,6 +107,7 @@ static void f_ch_status(typval_T *argvar
 #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},
@@ -2491,6 +2493,45 @@ f_char2nr(typval_T *argvars, typval_T *r
 }
 
 /*
+ * "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
  */
     static void
--- 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)
-{
-    if (!winlocal)
+post_chdir(cdscope_T scope)
+{
+    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,6 +7548,96 @@ post_chdir(int tablocal, int winlocal)
     shorten_fnames(TRUE);
 }
 
+/*
+ * 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.
+ */
+    int
+changedir_func(
+	char_u		*new_dir,
+	int		forceit,
+	cdscope_T	scope)
+{
+    char_u	*tofree;
+    int		dir_differs;
+    int		retval = FALSE;
+
+    if (allbuf_locked())
+	return FALSE;
+
+    if (vim_strchr(p_cpo, CPO_CHDIR) != NULL && curbufIsChanged() && !forceit)
+    {
+	emsg(_("E747: Cannot change directory, buffer is modified (add ! to override)"));
+	return FALSE;
+    }
+
+    // ":cd -": Change to previous directory
+    if (STRCMP(new_dir, "-") == 0)
+    {
+	if (prev_dir == NULL)
+	{
+	    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;
+
+#if defined(UNIX) || defined(VMS)
+    // for UNIX ":cd" means: go to home directory
+    if (*new_dir == NUL)
+    {
+	// use NameBuff for home directory name
+# ifdef VMS
+	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);
+# else
+	expand_env((char_u *)"$HOME", NameBuff, MAXPATHL);
+# endif
+	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
+    {
+	char_u  *acmd_fname;
+
+	post_chdir(scope);
+
+	if (dir_differs)
+	{
+	    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".
@@ -7556,94 +7646,28 @@ post_chdir(int tablocal, int winlocal)
 ex_cd(exarg_T *eap)
 {
     char_u	*new_dir;
-    char_u	*tofree;
-    int		dir_differs;
 
     new_dir = eap->arg;
 #if !defined(UNIX) && !defined(VMS)
-    /* for non-UNIX ":cd" means: print current directory */
+    // for non-UNIX ":cd" means: print current directory
     if (*new_dir == NUL)
 	ex_pwd(NULL);
     else
 #endif
     {
-	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;
-	}
-
-	/* ":cd -": Change to previous directory */
-	if (STRCMP(new_dir, "-") == 0)
-	{
-	    if (prev_dir == NULL)
-	    {
-		emsg(_("E186: No previous directory"));
-		return;
-	    }
-	    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;
-
-#if defined(UNIX) || defined(VMS)
-	/* for UNIX ":cd" means: go to home directory */
-	if (*new_dir == NUL)
-	{
-	    /* use NameBuff for home directory name */
-# ifdef VMS
-	    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);
-# else
-	    expand_env((char_u *)"$HOME", NameBuff, MAXPATHL);
-# endif
-	    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
-	{
-	    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;
-
-	    post_chdir(is_tablocal_chdir, is_winlocal_chdir);
-
-	    /* Echo the new current directory if the command was typed. */
+	cdscope_T	scope = CDSCOPE_GLOBAL;
+
+	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);
+	}
     }
 }
 
--- a/src/if_py_both.h
+++ b/src/if_py_both.h
@@ -1032,7 +1032,7 @@ VimStrwidth(PyObject *self UNUSED, PyObj
     Py_DECREF(newwd);
     Py_XDECREF(todecref);
 
-    post_chdir(FALSE, FALSE);
+    post_chdir(CDSCOPE_GLOBAL);
 
     if (VimTryEnd())
     {
--- 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);
--- 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;
--- 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
--- a/src/version.c
+++ b/src/version.c
@@ -768,6 +768,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1291,
+/**/
     1290,
 /**/
     1289,