changeset 16427:8c3a1bd270bb v8.1.1218

patch 8.1.1218: cannot set a directory for a tab page commit https://github.com/vim/vim/commit/00aa069db8132851a91cfc5ca7f58ef945c75c73 Author: Bram Moolenaar <Bram@vim.org> Date: Sat Apr 27 20:37:57 2019 +0200 patch 8.1.1218: cannot set a directory for a tab page Problem: Cannot set a directory for a tab page. Solution: Add the tab-local directory. (Yegappan Lakshmanan, closes https://github.com/vim/vim/issues/4212)
author Bram Moolenaar <Bram@vim.org>
date Sat, 27 Apr 2019 20:45:05 +0200
parents 21cad471f04a
children 6f69ef2913d7
files runtime/doc/autocmd.txt runtime/doc/editing.txt runtime/doc/eval.txt runtime/doc/index.txt runtime/doc/options.txt runtime/doc/usr_22.txt runtime/doc/usr_41.txt src/eval.c src/evalfunc.c src/ex_cmdidxs.h src/ex_cmds.h src/ex_docmd.c src/if_py_both.h src/proto/eval.pro src/proto/ex_docmd.pro src/structs.h src/testdir/test_getcwd.vim src/testdir/test_mksession.vim src/version.c src/window.c
diffstat 20 files changed, 445 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -1,4 +1,4 @@
-*autocmd.txt*   For Vim version 8.1.  Last change: 2019 Apr 08
+*autocmd.txt*   For Vim version 8.1.  Last change: 2019 Apr 27
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -690,13 +690,14 @@ DiffUpdated			After diffs have been upda
 				change or when doing |:diffupdate|.
 							*DirChanged*
 DirChanged			The working directory has changed in response
-				to the |:cd| or |:lcd| commands, or as a
-				result of the 'autochdir' option.
+				to the |:cd| or |:tcd| or |:lcd| commands, or
+				as a result of the 'autochdir' option.
 				The pattern can be:
-					"window" to trigger on `:lcd`
-					"global" to trigger on `:cd`
-					"auto"   to trigger on 'autochdir'.
-					"drop"	 to trigger on editing a file
+					"window"  to trigger on `:lcd`
+					"tabpage" to trigger on `:tcd`
+					"global"  to trigger on `:cd`
+					"auto"    to trigger on 'autochdir'.
+					"drop"	  to trigger on editing a file
 				<afile> is set to the new directory name.
 							*ExitPre*
 ExitPre				When using `:quit`, `:wq` in a way it makes
--- a/runtime/doc/editing.txt
+++ b/runtime/doc/editing.txt
@@ -1304,9 +1304,10 @@ use has("browsefilter"): >
 ==============================================================================
 7. The current directory				*current-directory*
 
-You may use the |:cd| and |:lcd| commands to change to another directory, so
-you will not have to type that directory name in front of the file names.  It
-also makes a difference for executing external commands, e.g. ":!ls".
+You can use the |:cd|, |:tcd| and |:lcd| commands to change to another
+directory, so you will not have to type that directory name in front of the
+file names.  It also makes a difference for executing external commands, e.g.
+":!ls".
 
 Changing directory fails when the current buffer is modified, the '.' flag is
 present in 'cpoptions' and "!" is not used in the command.
@@ -1334,6 +1335,17 @@ present in 'cpoptions' and "!" is not us
 							*:chd* *:chdir*
 :chd[ir][!] [path]	Same as |:cd|.
 
+							*:tcd*
+:tcd[!] {path}		Like |:cd|, but only set the directory for the current
+			tab.  The current window will also use this directory.
+			The current directory is not changed for windows in
+			other tabs and for windows in the current tab that
+			have their own window-local directory.
+			{not in Vi}
+
+							*:tch* *:tchdir*
+:tch[dir][!]		Same as |:tcd|. {not in Vi}
+
 							*:lc* *:lcd*
 :lc[d][!] {path}	Like |:cd|, but only set the current directory when
 			the cursor is in the current window.  The current
@@ -1348,17 +1360,26 @@ present in 'cpoptions' and "!" is not us
 :pw[d]			Print the current directory name.  {Vi: no pwd}
 			Also see |getcwd()|.
 
-So long as no |:lcd| command has been used, all windows share the same current
-directory.  Using a command to jump to another window doesn't change anything
-for the current directory.
+So long as no |:lcd| or |:tcd| command has been used, all windows share the
+same current directory.  Using a command to jump to another window doesn't
+change anything for the current directory.
+
 When a |:lcd| command has been used for a window, the specified directory
 becomes the current directory for that window.  Windows where the |:lcd|
-command has not been used stick to the global current directory.  When jumping
-to another window the current directory will become the last specified local
-current directory.  If none was specified, the global current directory is
-used.
-When a |:cd| command is used, the current window will lose his local current
-directory and will use the global current directory from now on.
+command has not been used stick to the global or tab-local current directory.
+When jumping to another window the current directory will become the last
+specified local current directory.  If none was specified, the global or
+tab-local current directory is used.
+
+When a |:tcd| command has been used for a tab page, the specified directory
+becomes the current directory for the current tab page and the current window.
+The current directory of other tab pages is not affected.  When jumping to
+another tab page, the current directory will become the last specified local
+directory for that tab page. If the current tab has no local current directory
+the global current directory is used.
+
+When a |:cd| command is used, the current window and tab page will lose the
+local current directory and will use the global current directory from now on.
 
 After using |:cd| the full path name will be used for reading and writing
 files.  On some networked file systems this may cause problems.  The result of
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2398,6 +2398,7 @@ has({feature})			Number	|TRUE| if featur
 has_key({dict}, {key})		Number	|TRUE| if {dict} has entry {key}
 haslocaldir([{winnr} [, {tabnr}]])
 				Number	|TRUE| if the window executed |:lcd|
+					or |:tcd|
 hasmapto({what} [, {mode} [, {abbr}]])
 				Number	|TRUE| if mapping to {what} exists
 histadd({history}, {item})	String	add an item to a history
@@ -4918,9 +4919,28 @@ getcwd([{winnr} [, {tabnr}]])
 		directory.  See also |haslocaldir()|.
 
 		With {winnr} and {tabnr} return the local current directory of
-		the window in the specified tab page.
+		the window in the specified tab page. If {winnr} is -1 return
+		the working directory of the tabpage.
+		If {winnr} is zero use the current window, if {tabnr} is zero
+		use the current tabpage.
+		Without any arguments, return the working directory of the
+		current window.
 		Return an empty string if the arguments are invalid.
 
+		Examples: >
+			" Get the working directory of the current window
+			:echo getcwd()
+			:echo getcwd(0)
+			:echo getcwd(0, 0)
+			" Get the working directory of window 3 in tabpage 2
+			:echo getcwd(3, 2)
+			" Get the global working directory
+			:echo getcwd(-1)
+			" Get the working directory of tabpage 3
+			:echo getcwd(-1, 3)
+			" Get the working directory of current tabpage
+			:echo getcwd(-1, 0)
+<
 getfsize({fname})					*getfsize()*
 		The result is a Number, which is the size in bytes of the
 		given file {fname}.
@@ -5478,16 +5498,39 @@ has_key({dict}, {key})					*has_key()*
 		an entry with key {key}.  Zero otherwise.
 
 haslocaldir([{winnr} [, {tabnr}]])			*haslocaldir()*
-		The result is a Number, which is 1 when the window has set a
-		local path via |:lcd|, and 0 otherwise.
+		The result is a Number:
+		    1   when the window has set a local directory via |:lcd|
+		    2   when the tab-page has set a local directory via |:tcd|
+		    0   otherwise.
 
 		Without arguments use the current window.
 		With {winnr} use this window in the current tab page.
 		With {winnr} and {tabnr} use the window in the specified tab
 		page.
 		{winnr} can be the window number or the |window-ID|.
+		If {winnr} is -1 it is ignored and only the tabpage is used.
 		Return 0 if the arguments are invalid.
-
+		Examples: >
+			if haslocaldir() == 1
+			  " window local directory case
+			elseif haslocaldir() == 2
+			  " tab-local directory case
+			else
+			  " global directory case
+			endif
+
+			" current window
+			:echo haslocaldir()
+			:echo haslocaldir(0)
+			:echo haslocaldir(0, 0)
+			" window n in current tab page
+			:echo haslocaldir(n)
+			:echo haslocaldir(n, 0)
+			" window n in tab page m
+			:echo haslocaldir(n, m)
+			" tab page m
+			:echo haslocaldir(-1, m)
+<
 hasmapto({what} [, {mode} [, {abbr}]])			*hasmapto()*
 		The result is a Number, which is 1 if there is a mapping that
 		contains {what} in somewhere in the rhs (what it is mapped to)
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -1623,6 +1623,8 @@ tag		command		action ~
 |:tab|		:tab		create new tab when opening new window
 |:tag|		:ta[g]		jump to tag
 |:tags|		:tags		show the contents of the tag stack
+|:tcd|		:tcd		change directory for tab page
+|:tchdir|	:tch[dir]	change directory for tab page
 |:tcl|		:tc[l]		execute Tcl command
 |:tcldo|	:tcld[o]	execute Tcl command for each line
 |:tclfile|	:tclf[ile]	execute Tcl script file
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1455,9 +1455,9 @@ A jump table for the options with a shor
 			{not available when compiled without the
 			|+file_in_path| feature}
 	This is a list of directories which will be searched when using the
-	|:cd| and |:lcd| commands, provided that the directory being searched
-	for has a relative path, not an absolute part starting with "/", "./"
-	or "../", the 'cdpath' option is not used then.
+	|:cd|, |:tcd| and |:lcd| commands, provided that the directory being
+	searched for has a relative path, not an absolute part starting with
+	"/", "./" or "../", the 'cdpath' option is not used then.
 	The 'cdpath' option's value has the same form and semantics as
 	|'path'|.  Also see |file-searching|.
 	The default value is taken from $CDPATH, with a "," prepended to look
--- a/runtime/doc/usr_22.txt
+++ b/runtime/doc/usr_22.txt
@@ -202,14 +202,28 @@ the other window.  This is called a loca
 	:pwd
 	/home/Bram/VeryLongFileName
 
-So long as no ":lcd" command has been used, all windows share the same current
-directory.  Doing a ":cd" command in one window will also change the current
+So long as no `:lcd` command has been used, all windows share the same current
+directory.  Doing a `:cd` command in one window will also change the current
 directory of the other window.
-   For a window where ":lcd" has been used a different current directory is
-remembered.  Using ":cd" or ":lcd" in other windows will not change it.
-   When using a ":cd" command in a window that uses a different current
+   For a window where `:lcd` has been used a different current directory is
+remembered.  Using `:cd` or `:lcd` in other windows will not change it.
+   When using a `:cd` command in a window that uses a different current
 directory, it will go back to using the shared directory.
 
+
+TAB LOCAL DIRECTORY
+
+When you open a new tab page, it uses the directory of the window in the
+previous tab page from which the new tab page was opened. You can change the
+directory of the current tab page using the `:tcd` command. All the windows in
+a tab page share this directory except for windows with a window-local
+directory. Any new windows opened in this tab page will use this directory as
+the current working directory. Using a `:cd` command in a tab page will not
+change the working directory of tab pages which have a tab local directory.
+When the global working directory is changed using the ":cd" command in a tab
+page, it will also change the current tab page working directory.
+
+
 ==============================================================================
 *22.3*	Finding a file
 
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -766,7 +766,7 @@ System functions and manipulation of fil
 	isdirectory()		check if a directory exists
 	getfsize()		get the size of a file
 	getcwd()		get the current working directory
-	haslocaldir()		check if current window used |:lcd|
+	haslocaldir()		check if current window used |:lcd| or |:tcd|
 	tempname()		get the name of a temporary file
 	mkdir()			create a new directory
 	delete()		delete a file
--- a/src/eval.c
+++ b/src/eval.c
@@ -8704,11 +8704,13 @@ find_win_by_nr_or_id(typval_T *vp)
 
 /*
  * Find window specified by "wvp" in tabpage "tvp".
+ * Returns the tab page in 'ptp'
  */
     win_T *
 find_tabwin(
-    typval_T	*wvp,	/* VAR_UNKNOWN for current window */
-    typval_T	*tvp)	/* VAR_UNKNOWN for current tab page */
+    typval_T	*wvp,	// VAR_UNKNOWN for current window
+    typval_T	*tvp,	// VAR_UNKNOWN for current tab page
+    tabpage_T	**ptp)
 {
     win_T	*wp = NULL;
     tabpage_T	*tp = NULL;
@@ -8726,10 +8728,22 @@ find_tabwin(
 	    tp = curtab;
 
 	if (tp != NULL)
+	{
 	    wp = find_win_by_nr(wvp, tp);
+	    if (wp == NULL && wvp->v_type == VAR_NUMBER
+						&& wvp->vval.v_number != -1)
+		// A window with the specified number is not found
+		tp = NULL;
+	}
     }
     else
+    {
 	wp = curwin;
+	tp = curtab;
+    }
+
+    if (ptp != NULL)
+	*ptp = tp;
 
     return wp;
 }
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -1529,7 +1529,7 @@ f_arglistid(typval_T *argvars, typval_T 
     win_T	*wp;
 
     rettv->vval.v_number = -1;
-    wp = find_tabwin(&argvars[0], &argvars[1]);
+    wp = find_tabwin(&argvars[0], &argvars[1], NULL);
     if (wp != NULL)
 	rettv->vval.v_number = wp->w_alist->id;
 }
@@ -5126,25 +5126,44 @@ f_getcompletion(typval_T *argvars, typva
 
 /*
  * "getcwd()" function
+ *
+ * Return the current working directory of a window in a tab page.
+ * First optional argument 'winnr' is the window number or -1 and the second
+ * optional argument 'tabnr' is the tab page number.
+ *
+ * If no arguments are supplied, then return the directory of the current
+ * window.
+ * If only 'winnr' is specified and is not -1 or 0 then return the directory of
+ * the specified window.
+ * If 'winnr' is 0 then return the directory of the current window.
+ * If both 'winnr and 'tabnr' are specified and 'winnr' is -1 then return the
+ * directory of the specified tab page.  Otherwise return the directory of the
+ * specified window in the specified tab page.
+ * If the window or the tab page doesn't exist then return NULL.
  */
     static void
 f_getcwd(typval_T *argvars, typval_T *rettv)
 {
     win_T	*wp = NULL;
+    tabpage_T	*tp = NULL;
     char_u	*cwd;
     int		global = FALSE;
 
     rettv->v_type = VAR_STRING;
     rettv->vval.v_string = NULL;
 
-    if (argvars[0].v_type == VAR_NUMBER && argvars[0].vval.v_number == -1)
+    if (argvars[0].v_type == VAR_NUMBER
+	    && argvars[0].vval.v_number == -1
+	    && argvars[1].v_type == VAR_UNKNOWN)
 	global = TRUE;
     else
-	wp = find_tabwin(&argvars[0], &argvars[1]);
+	wp = find_tabwin(&argvars[0], &argvars[1], &tp);
 
     if (wp != NULL && wp->w_localdir != NULL)
 	rettv->vval.v_string = vim_strsave(wp->w_localdir);
-    else if (wp != NULL || global)
+    else if (tp != NULL && tp->tp_localdir != NULL)
+	rettv->vval.v_string = vim_strsave(tp->tp_localdir);
+    else if (wp != NULL || tp != NULL || global)
     {
 	if (globaldir != NULL)
 	    rettv->vval.v_string = vim_strsave(globaldir);
@@ -5333,7 +5352,7 @@ f_getjumplist(typval_T *argvars, typval_
 	return;
 
 #ifdef FEAT_JUMPLIST
-    wp = find_tabwin(&argvars[0], &argvars[1]);
+    wp = find_tabwin(&argvars[0], &argvars[1], NULL);
     if (wp == NULL)
 	return;
 
@@ -6824,10 +6843,18 @@ f_has_key(typval_T *argvars, typval_T *r
     static void
 f_haslocaldir(typval_T *argvars, typval_T *rettv)
 {
+    tabpage_T	*tp = NULL;
     win_T	*wp = NULL;
 
-    wp = find_tabwin(&argvars[0], &argvars[1]);
-    rettv->vval.v_number = (wp != NULL && wp->w_localdir != NULL);
+    wp = find_tabwin(&argvars[0], &argvars[1], &tp);
+
+    // Check for window-local and tab-local directories
+    if (wp != NULL && wp->w_localdir != NULL)
+	rettv->vval.v_number = 1;
+    else if (tp != NULL && tp->tp_localdir != NULL)
+	rettv->vval.v_number = 2;
+    else
+	rettv->vval.v_number = 0;
 }
 
 /*
--- a/src/ex_cmdidxs.h
+++ b/src/ex_cmdidxs.h
@@ -25,12 +25,12 @@ static const unsigned short cmdidxs1[26]
   /* r */ 351,
   /* s */ 371,
   /* t */ 439,
-  /* u */ 482,
-  /* v */ 493,
-  /* w */ 511,
-  /* x */ 525,
-  /* y */ 534,
-  /* z */ 535
+  /* u */ 484,
+  /* v */ 495,
+  /* w */ 513,
+  /* x */ 527,
+  /* y */ 536,
+  /* z */ 537
 };
 
 /*
@@ -60,7 +60,7 @@ static const unsigned char cmdidxs2[26][
   /* q */ {  2,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 },
   /* r */ {  0,  0,  0,  0,  0,  0,  0,  0, 12,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 14, 19,  0,  0,  0,  0 },
   /* s */ {  2,  6, 15,  0, 19, 23,  0, 25, 26,  0,  0, 29, 31, 35, 39, 41,  0, 49,  0, 50,  0, 62, 63,  0, 64,  0 },
-  /* t */ {  2,  0, 19,  0, 22, 24,  0, 25,  0, 26,  0, 27, 31, 34, 36, 37,  0, 38, 40,  0, 41,  0,  0,  0,  0,  0 },
+  /* t */ {  2,  0, 19,  0, 24, 26,  0, 27,  0, 28,  0, 29, 33, 36, 38, 39,  0, 40, 42,  0, 43,  0,  0,  0,  0,  0 },
   /* u */ {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 10,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 },
   /* v */ {  0,  0,  0,  0,  1,  0,  0,  0,  4,  0,  0,  0,  9, 12,  0,  0,  0,  0, 15,  0, 16,  0,  0,  0,  0,  0 },
   /* w */ {  2,  0,  0,  0,  0,  0,  0,  3,  4,  0,  0,  0,  0,  8,  0,  9, 10,  0,  0,  0, 12, 13,  0,  0,  0,  0 },
@@ -69,4 +69,4 @@ static const unsigned char cmdidxs2[26][
   /* z */ {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 }
 };
 
-static const int command_count = 548;
+static const int command_count = 550;
--- a/src/ex_cmds.h
+++ b/src/ex_cmds.h
@@ -1479,6 +1479,12 @@ EX(CMD_tabrewind,	"tabrewind",	ex_tabnex
 EX(CMD_tabs,		"tabs",		ex_tabs,
 			TRLBAR|CMDWIN,
 			ADDR_TABS),
+EX(CMD_tcd,		"tcd",		ex_cd,
+			BANG|FILE1|TRLBAR|CMDWIN,
+			ADDR_OTHER),
+EX(CMD_tchdir,		"tchdir",	ex_cd,
+			BANG|FILE1|TRLBAR|CMDWIN,
+			ADDR_OTHER),
 EX(CMD_tcl,		"tcl",		ex_tcl,
 			RANGE|EXTRA|NEEDARG|CMDWIN|RESTRICT,
 			ADDR_LINES),
--- a/src/ex_docmd.c
+++ b/src/ex_docmd.c
@@ -3692,6 +3692,8 @@ set_one_cmd_context(
 	    break;
 	case CMD_cd:
 	case CMD_chdir:
+	case CMD_tcd:
+	case CMD_tchdir:
 	case CMD_lcd:
 	case CMD_lchdir:
 	    if (xp->xp_context == EXPAND_FILES)
@@ -7435,13 +7437,17 @@ free_cd_dir(void)
 
 /*
  * Deal with the side effects of changing the current directory.
- * When "local" is TRUE then this was after an ":lcd" command.
+ * When "tablocal" is TRUE then this was after an ":tcd" command.
+ * When "winlocal" is TRUE then this was after an ":lcd" command.
  */
     void
-post_chdir(int local)
-{
+post_chdir(int tablocal, int winlocal)
+{
+    if (!winlocal)
+	// Clear tab local directory for both :cd and :tcd
+	VIM_CLEAR(curtab->tp_localdir);
     VIM_CLEAR(curwin->w_localdir);
-    if (local)
+    if (winlocal || tablocal)
     {
 	/* If still in global directory, need to remember current
 	 * directory as global directory. */
@@ -7449,7 +7455,12 @@ post_chdir(int local)
 	    globaldir = vim_strsave(prev_dir);
 	/* Remember this local directory for the window. */
 	if (mch_dirname(NameBuff, MAXPATHL) == OK)
-	    curwin->w_localdir = vim_strsave(NameBuff);
+	{
+	    if (tablocal)
+		curtab->tp_localdir = vim_strsave(NameBuff);
+	    else
+		curwin->w_localdir = vim_strsave(NameBuff);
+	}
     }
     else
     {
@@ -7463,7 +7474,7 @@ post_chdir(int local)
 
 
 /*
- * ":cd", ":lcd", ":chdir" and ":lchdir".
+ * ":cd", ":tcd", ":lcd", ":chdir" ":tchdir" and ":lchdir".
  */
     void
 ex_cd(exarg_T *eap)
@@ -7532,19 +7543,29 @@ ex_cd(exarg_T *eap)
 	    emsg(_(e_failed));
 	else
 	{
-	    int is_local_chdir = eap->cmdidx == CMD_lcd
+	    char_u  *acmd_fname;
+	    int is_winlocal_chdir = eap->cmdidx == CMD_lcd
 						  || eap->cmdidx == CMD_lchdir;
-
-	    post_chdir(is_local_chdir);
+	    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. */
 	    if (KeyTyped || p_verbose >= 5)
 		ex_pwd(eap);
 
 	    if (dir_differs)
-		apply_autocmds(EVENT_DIRCHANGED,
-		      is_local_chdir ? (char_u *)"window" : (char_u *)"global",
+	    {
+		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);
     }
@@ -9729,12 +9750,13 @@ makeopens(
     }
     for (tabnr = 1; ; ++tabnr)
     {
+	tabpage_T *tp = NULL;
 	int	need_tabnext = FALSE;
 	int	cnr = 1;
 
 	if ((ssop_flags & SSOP_TABPAGES))
 	{
-	    tabpage_T *tp = find_tabpage(tabnr);
+	    tp = find_tabpage(tabnr);
 
 	    if (tp == NULL)
 		break;		/* done all tab pages */
@@ -9833,6 +9855,18 @@ makeopens(
 	if (nr > 1 && ses_winsizes(fd, restore_size, tab_firstwin) == FAIL)
 	    return FAIL;
 
+	// Restore the tab-local working directory if specified
+	// Do this before the windows, so that the window-local directory can
+	// override the tab-local directory.
+	if (tp != NULL && tp->tp_localdir != NULL && ssop_flags & SSOP_CURDIR)
+	{
+	    if (fputs("tcd ", fd) < 0
+		    || ses_put_fname(fd, tp->tp_localdir, &ssop_flags) == FAIL
+		    || put_eol(fd) == FAIL)
+		return FAIL;
+	    did_lcd = TRUE;
+	}
+
 	/*
 	 * Restore the view of the window (options, file, cursor, etc.).
 	 */
--- 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);
+    post_chdir(FALSE, FALSE);
 
     if (VimTryEnd())
     {
--- a/src/proto/eval.pro
+++ b/src/proto/eval.pro
@@ -116,7 +116,7 @@ void ex_echohl(exarg_T *eap);
 void ex_execute(exarg_T *eap);
 win_T *find_win_by_nr(typval_T *vp, tabpage_T *tp);
 win_T *find_win_by_nr_or_id(typval_T *vp);
-win_T *find_tabwin(typval_T *wvp, typval_T *tvp);
+win_T *find_tabwin(typval_T *wvp, typval_T *tvp, tabpage_T **ptp);
 void getwinvar(typval_T *argvars, typval_T *rettv, int off);
 void setwinvar(typval_T *argvars, typval_T *rettv, int off);
 char_u *autoload_name(char_u *name);
--- a/src/proto/ex_docmd.pro
+++ b/src/proto/ex_docmd.pro
@@ -37,7 +37,7 @@ 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 local);
+void post_chdir(int tablocal, int winlocal);
 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
@@ -2574,6 +2574,9 @@ struct tabpage_S
     int		    tp_prev_which_scrollbars[3];
 				    /* previous value of which_scrollbars */
 #endif
+
+    char_u	    *tp_localdir;	// absolute path of local directory or
+					// NULL
 #ifdef FEAT_DIFF
     diff_T	    *tp_first_diff;
     buf_T	    *(tp_diffbuf[DB_COUNT]);
--- a/src/testdir/test_getcwd.vim
+++ b/src/testdir/test_getcwd.vim
@@ -97,6 +97,17 @@ function Test_GetCwd()
   call assert_equal("y Xdir2 1", GetCwdInfo(2, tp_nr))
   call assert_equal("z Xdir3 1", GetCwdInfo(1, tp_nr))
   call assert_equal(g:topdir, getcwd(-1))
+  " Non existing windows and tab pages
+  call assert_equal('', getcwd(100))
+  call assert_equal(0, haslocaldir(100))
+  call assert_equal('', getcwd(10, 1))
+  call assert_equal(0, haslocaldir(10, 1))
+  call assert_equal('', getcwd(1, 5))
+  call assert_equal(0, haslocaldir(1, 5))
+  call assert_fails('call getcwd([])', 'E745:')
+  call assert_fails('call getcwd(1, [])', 'E745:')
+  call assert_fails('call haslocaldir([])', 'E745:')
+  call assert_fails('call haslocaldir(1, [])', 'E745:')
 endfunc
 
 function Test_GetCwd_lcd_shellslash()
@@ -110,3 +121,144 @@ function Test_GetCwd_lcd_shellslash()
     call assert_equal(cwd[-1:], '\')
   endif
 endfunc
+
+" Test for :tcd
+function Test_Tab_Local_Cwd()
+  enew | only | tabonly
+
+  call mkdir('Xtabdir1')
+  call mkdir('Xtabdir2')
+  call mkdir('Xwindir1')
+  call mkdir('Xwindir2')
+  call mkdir('Xwindir3')
+
+  " Create three tabpages with three windows each
+  edit a
+  botright new b
+  botright new c
+  tabnew m
+  botright new n
+  botright new o
+  tabnew x
+  botright new y
+  botright new z
+
+  " Setup different directories for the tab pages and windows
+  tabrewind
+  1wincmd w
+  lcd Xwindir1
+  tabnext
+  tcd Xtabdir1
+  2wincmd w
+  lcd ../Xwindir2
+  tabnext
+  tcd Xtabdir2
+  3wincmd w
+  lcd ../Xwindir3
+
+  " Check the directories of various windows
+  call assert_equal("a Xwindir1 1", GetCwdInfo(1, 1))
+  call assert_equal("b Xtopdir 0", GetCwdInfo(2, 1))
+  call assert_equal("c Xtopdir 0", GetCwdInfo(3, 1))
+  call assert_equal("m Xtabdir1 2", GetCwdInfo(1, 2))
+  call assert_equal("n Xwindir2 1", GetCwdInfo(2, 2))
+  call assert_equal("o Xtabdir1 2", GetCwdInfo(3, 2))
+  call assert_equal("x Xtabdir2 2", GetCwdInfo(1, 3))
+  call assert_equal("y Xtabdir2 2", GetCwdInfo(2, 3))
+  call assert_equal("z Xwindir3 1", GetCwdInfo(3, 3))
+
+  " Check the tabpage directories
+  call assert_equal('Xtopdir', fnamemodify(getcwd(-1, 1), ':t'))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(-1, 2), ':t'))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(-1, 3), ':t'))
+  call assert_equal('', fnamemodify(getcwd(-1, 4), ':t'))
+
+  " Jump to different windows in the tab pages and check the current directory
+  tabrewind | 1wincmd w
+  call assert_equal('Xwindir1', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xwindir1', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xwindir1', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_true(haslocaldir(0))
+  call assert_equal(0, haslocaldir(-1, 0))
+  call assert_equal('Xtopdir', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+  2wincmd w
+  call assert_equal('Xtopdir', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xtopdir', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xtopdir', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_false(haslocaldir(0))
+  call assert_equal(0, haslocaldir(-1, 0))
+  call assert_equal('Xtopdir', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+  tabnext | 1wincmd w
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_true(haslocaldir(0))
+  call assert_equal(2, haslocaldir(-1, 0))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+  2wincmd w
+  call assert_equal('Xwindir2', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xwindir2', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xwindir2', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_true(haslocaldir(0))
+  call assert_equal(2, haslocaldir(-1, 0))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+  tabnext | 1wincmd w
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_true(haslocaldir(0))
+  call assert_equal(2, haslocaldir(-1, 0))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+  3wincmd w
+  call assert_equal('Xwindir3', fnamemodify(getcwd(), ':t'))
+  call assert_equal('Xwindir3', fnamemodify(getcwd(0), ':t'))
+  call assert_equal('Xwindir3', fnamemodify(getcwd(0, 0), ':t'))
+  call assert_true(haslocaldir(0))
+  call assert_equal(2, haslocaldir(-1, 0))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(-1, 0), ':t'))
+  call assert_equal(g:topdir, getcwd(-1))
+
+  " A new tab page should inherit the directory of the current tab page
+  tabrewind | 1wincmd w
+  tabnew g
+  call assert_equal("g Xwindir1 1", GetCwdInfo(0, 0))
+  tabclose | tabrewind
+  2wincmd w
+  tabnew h
+  call assert_equal("h Xtopdir 0", GetCwdInfo(0, 0))
+  tabclose
+  tabnext 2 | 1wincmd w
+  tabnew j
+  call assert_equal("j Xtabdir1 2", GetCwdInfo(0, 0))
+  tabclose
+
+  " Change the global directory for the first tab page
+  tabrewind | 1wincmd w
+  cd ../Xdir1
+  call assert_equal("a Xdir1 0", GetCwdInfo(1, 1))
+  call assert_equal("b Xdir1 0", GetCwdInfo(2, 1))
+  call assert_equal("m Xtabdir1 2", GetCwdInfo(1, 2))
+  call assert_equal("n Xwindir2 1", GetCwdInfo(2, 2))
+
+  " Change the global directory for the second tab page
+  tabnext | 1wincmd w
+  cd ../Xdir3
+  call assert_equal("m Xdir3 0", GetCwdInfo(1, 2))
+  call assert_equal("n Xwindir2 1", GetCwdInfo(2, 2))
+  call assert_equal("o Xdir3 0", GetCwdInfo(3, 2))
+
+  " Change the tab-local directory for the third tab page
+  tabnext | 1wincmd w
+  cd ../Xdir1
+  call assert_equal("x Xdir1 0", GetCwdInfo(1, 3))
+  call assert_equal("y Xdir1 0", GetCwdInfo(2, 3))
+  call assert_equal("z Xwindir3 1", GetCwdInfo(3, 3))
+
+  enew | only | tabonly
+  new
+endfunc
--- a/src/testdir/test_mksession.vim
+++ b/src/testdir/test_mksession.vim
@@ -210,6 +210,48 @@ func Test_mksession_lcd_multiple_tabs()
   call delete('Xtest_mks.out')
 endfunc
 
+" Test for tabpage-local directory
+func Test_mksession_tcd_multiple_tabs()
+  let save_cwd = getcwd()
+  call mkdir('Xtopdir')
+  cd Xtopdir
+  call mkdir('Xtabdir1')
+  call mkdir('Xtabdir2')
+  call mkdir('Xtabdir3')
+  call mkdir('Xwindir1')
+  call mkdir('Xwindir2')
+  call mkdir('Xwindir3')
+  tcd Xtabdir1
+  botright new
+  wincmd t
+  lcd ../Xwindir1
+  tabnew
+  tcd ../Xtabdir2
+  botright new
+  lcd ../Xwindir2
+  tabnew
+  tcd ../Xtabdir3
+  botright new
+  lcd ../Xwindir3
+  tabfirst
+  1wincmd w
+  mksession! Xtest_mks.out
+  only | tabonly
+  source Xtest_mks.out
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(-1, 1), ':t'))
+  call assert_equal('Xwindir1', fnamemodify(getcwd(1, 1), ':t'))
+  call assert_equal('Xtabdir1', fnamemodify(getcwd(2, 1), ':t'))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(-1, 2), ':t'))
+  call assert_equal('Xtabdir2', fnamemodify(getcwd(1, 2), ':t'))
+  call assert_equal('Xwindir2', fnamemodify(getcwd(2, 2), ':t'))
+  call assert_equal('Xtabdir3', fnamemodify(getcwd(-1, 3), ':t'))
+  call assert_equal('Xtabdir3', fnamemodify(getcwd(1, 3), ':t'))
+  call assert_equal('Xwindir3', fnamemodify(getcwd(2, 3), ':t'))
+  only | tabonly
+  exe 'cd ' . save_cwd
+  call delete("Xtopdir", "rf")
+endfunc
+
 func Test_mksession_blank_tabs()
   tabnew
   tabnew
--- 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 */
 /**/
+    1218,
+/**/
     1217,
 /**/
     1216,
--- a/src/window.c
+++ b/src/window.c
@@ -3625,6 +3625,8 @@ free_tabpage(tabpage_T *tp)
     unref_var_dict(tp->tp_vars);
 #endif
 
+    vim_free(tp->tp_localdir);
+
 #ifdef FEAT_PYTHON
     python_tabpage_free(tp);
 #endif
@@ -3662,6 +3664,8 @@ win_new_tabpage(int after)
     }
     curtab = newtp;
 
+    newtp->tp_localdir = (tp->tp_localdir == NULL)
+				    ? NULL : vim_strsave(tp->tp_localdir);
     /* Create a new empty window. */
     if (win_alloc_firstwin(tp->tp_curwin) == OK)
     {
@@ -3839,6 +3843,9 @@ find_tabpage(int n)
     tabpage_T	*tp;
     int		i = 1;
 
+    if (n == 0)
+	return curtab;
+
     for (tp = first_tabpage; tp != NULL && i != n; tp = tp->tp_next)
 	++i;
     return tp;
@@ -4451,11 +4458,13 @@ win_enter_ext(
 	curwin->w_cursor.coladd = 0;
     changed_line_abv_curs();	/* assume cursor position needs updating */
 
-    if (curwin->w_localdir != NULL)
-    {
-	/* Window has a local directory: Save current directory as global
-	 * directory (unless that was done already) and change to the local
-	 * directory. */
+    if (curwin->w_localdir != NULL || curtab->tp_localdir != NULL)
+    {
+	char_u	*dirname;
+
+	// Window or tab has a local directory: Save current directory as
+	// global directory (unless that was done already) and change to the
+	// local directory.
 	if (globaldir == NULL)
 	{
 	    char_u	cwd[MAXPATHL];
@@ -4463,7 +4472,12 @@ win_enter_ext(
 	    if (mch_dirname(cwd, MAXPATHL) == OK)
 		globaldir = vim_strsave(cwd);
 	}
-	if (mch_chdir((char *)curwin->w_localdir) == 0)
+	if (curwin->w_localdir != NULL)
+	    dirname = curwin->w_localdir;
+	else
+	    dirname = curtab->tp_localdir;
+
+	if (mch_chdir((char *)dirname) == 0)
 	    shorten_fnames(TRUE);
     }
     else if (globaldir != NULL)