One tab per project, with unique names
This is a lightweight workspace management package that provides a thin layer
between builtin packages project
and tab-bar
. The whole idea consists of
creating a tab per opened project while ensuring unique names for the
created tabs (when multiple opened projects have the same name).
This package is inspired by project-tab-groups
which creates a "tab group"
per project.
This package is available on MELPA.
(use-package otpp
:straight t
:after project
:init
;; If you like to define some aliases for better user experience
(defalias 'one-tab-per-project-mode 'otpp-mode)
(defalias 'one-tab-per-project-override-mode 'otpp-override-mode)
;; Enable `otpp-mode` globally
(otpp-mode 1)
;; If you want to advice the commands in `otpp-override-commands`
;; to be run in the current's tab (so, current project's) root directory
(otpp-override-mode 1))
The usage is quite straightforward, there is no extra commands to learn to be
able to use it. When otpp-mode
global minor mode is enabled, you will have
this:
-
When you switch to a project
project-switch-project
(bound by default toC-x p p
),otpp
will create a tab with the project name. -
When you kill a project with all its buffers with
project-kill-buffers
, the tab is closed. -
Lets say you've switched to the project under
/home/user/project1/backend/
,otpp
will create a tab namedbackend
for this particular project. Now, you opened a second project under/home/user/project2/backend/
,otpp
will detect that the name of the projectbackend
is the same as the previously opened one, but it have a different path. In this case,otpp
will create a tab namedbackend[project2]
and renames the previously opened tab tobackend[project1]
. This conflict resolution is provided by theotpp-uniq
library. -
For some cases, you might need to attach a manually created tab (by
tab-bar-new-tab
) to an opened project so you have two tabs dedicated to the same project (with different windows layouts for example). In this case, you can call the commandotpp-change-tab-root-dir
and select the path of the project to attach to. -
When you use some commands to jump to a file (
find-file
,xref-find-definitions
, etc.), you can end up with a buffer belonging to a different project (lets sayB
) but displayed in the current project's tab (A
). In this case, you can callotpp-detach-buffer-to-tab
to create a new tab dedicated to the buffer's projectB
. When the opened buffer is project-less (not part of a project), the command will signal a user error unlessotpp-allow-detach-projectless-buffer
is non-nil, in this case,otpp
creates a new project-less tab for the buffer.
Consider this use case: supposing you are using otpp-mode
and you've run
project-switch-project
to open the X
project in a new X
tab. Now you
M-x find-file
then you open the test.cpp
file outside the current X
project. Now, if you run project-find-file
, you will be in one of these two
situations:
-
If
test.cpp
is part of another projectY
, theproject-find-file
will prompt you with a list ofY
s files even though we are in theX
tab. -
If
test.cpp
isn't part of any project,project-find-file
will prompt you to select a project first, then to select a file.
For this, otpp
provides otpp-prefix
(we recommend to bind it to some key,
like C-x t P
, using otpp-prefix
from M-x
can have some limitations).
When you run otpp-prefix
followed by C-x p f
for example, you will be
prompted for files in the current's tab project files even if you are
visiting a file outside of the current project.
In my workflow, I would like to always restrict the commands like
project-find-file
and project-kill-buffers
to the project bound to the
current tab, even if I'm visiting a file which is not part of this project.
If you like this behavior, you can enable the otpp-override-mode
. This mode
will advice all the commands defined in otpp-override-commands
to be ran in
the current's tab root directory (a.k.a., in the project bound to the
current tab).
When otpp-override-mode
is enabled, the otpp-prefix
acts inversely. While
all otpp-override-commands
are restricted to the current's tab project by
default, running a command with otpp-prefix
will disable this behavior,
which results of the next command to be run in the default-directory
depending on the visited buffer.
This section is not exhaustive, it includes only the packages that I used before.
-
project-tab-groups
: This package provides a mode that enhances the Emacs built-inproject
to support keeping projects isolated in named tab groups.otpp
is inspired by this package, but instead of setting the tab groups,otpp
introduces a new attribute in the tab namedotpp-root-dir
where it stores the root directory of the project bound to the tab. This allows keeping the tabs updated in case another project with the same name (but a different path) is opened. -
tabspaces
: This package provide workspace management withtab-bar
and with an integration withproject
. Contrary tootpp
andproject-tab-groups
,tabspaces
don't create tabs automatically, you need to call specific commands liketabspaces-open-or-create-project-and-workspace
. Also,tabspaces
behavior isn't predictable when you open several projects with the same directory name.
When non-nil, preserve the current rootless tab when switching projects.
Bury the current buffer when killed but it is opened in another tab.
When non-nil, this modifies the behavior of kill-buffer
when killing
the current buffer. If the current buffer is opened in another tab, we
bury it instead of killing it. This only affects the current buffer,
when we explicitly select another buffer to kill, otpp
assumes that we
have a good reason to kill it.
Whether to reconnect a disconnected tab when switching to it.
When set to a function's symbol, that function will be called with the switched-to project's root directory as its single argument.
When non-nil, show the project dispatch menu instead.
Whether to strictly obey local variables.
Set a nil (default value) to only respect the local variables when they
are defined in the project's root (the dir-locals-file
is located in
the project's root).
Set to a function that takes DIR, PROJECT-ROOT and DIR-LOCALS-ROOT as
arguments in this order, see the function otpp-project-name
. The
function should return non-nil to take the local variables into account.
This can be useful when the project include sub-projects (a Git repository with sub-modules, a Git repository with other Git repos inside, a Repo workspace, etc).
List of functions to call after changing the otpp-root-dir
of a tab.
This hook is run at the end of the function otpp-change-tab-root-dir
.
The current tab is supplied as an argument.
Derive project name from a directory.
This function receives a directory and return the project name for the project that includes this path.
Allow detaching a buffer to a new tab even if it is not part of a project. This can also be set to a function that receives the buffer, and return non-nil if we should allow the tab creation.
A list of commands to be advised in otpp-override-mode
.
These commands will be run with default-directory
set the to current's
tab directory.
The default tab name to use when the last otpp tab is killed.
Get the root directory set to the TAB, default to the current tab.
Get the project name from DIR.
This function extracts the project root. Then, it tries to find a
dir-locals-file
file that can be applied to files inside the directory
DIR. When found, the local variables are read if any of these conditions
is correct:
otpp-strictly-obey-dir-locals
is set to a function, and calling it returns non-nil (we pass to this function the DIR, the project root and the directory containing thedir-locals-file
).otpp-strictly-obey-dir-locals
is a not a function and it is non-nil.- The
dir-locals-file
file is stored in the project root, a.k.a., the project root is the same as thedir-locals-file
directory. Then, this function checks in this order:
- If the local variable
otpp-project-name
is set locally in thedir-locals-file
, use it as project name. - Same with the local variable
project-vc-name
. - Return the directory name of the project's root. When DIR isn't part of any project, returns nil.
Create or switch to the tab corresponding to the project of BUFFER. When called with the a prefix, it asks for the buffer.
Change the otpp-root-dir
attribute to DIR.
If if the absolute TAB-NUMBER is provided, set it, otherwise, set the
current tab.
When DIR is empty or nil, delete it from the tab.
Return a list of tabs that have DIR as otpp-root-dir
attribute.
Select or create the tab with root directory DIR. Returns non-nil if a new tab was created, and nil otherwise.
Run the next command in the tab's root directory (or not!).
The actual behavior depends on otpp-override-mode
. For
instance, when you execute M-x otpp-prefix followed by
C-x p f, if the otpp-override-mode
is
enabled, this will run the project-find-file
command in the
default-directory
, otherwise, it will bind the default-directory
to
the current's tab directory before executing project-find-file
.