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
;; 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),otppwill 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/,otppwill create a tab namedbackendfor this particular project. Now, you opened a second project under/home/user/project2/backend/,otppwill detect that the name of the projectbackendis the same as the previously opened one, but it have a different path. In this case,otppwill create a tab namedbackend[project2]and renames the previously opened tab tobackend[project1]. This conflict resolution is provided by theotpp-uniq-*routines. -
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-dirand 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-tabto 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-bufferis non-nil, in this case,otppcreates 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.cppis part of another projectY, theproject-find-filewill prompt you with a list ofYs files even though we are in theXtab. -
If
test.cppisn't part of any project,project-find-filewill 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 (i.e., 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-inprojectto support keeping projects isolated in named tab groups.otppis inspired by this package, but instead of setting the tab groups,otppintroduces a new attribute in the tab namedotpp-root-dirwhere 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-barand with an integration withproject. Contrary tootppandproject-tab-groups,tabspacesdon't create tabs automatically, you need to call specific commands liketabspaces-open-or-create-project-and-workspace. Also,tabspacesbehavior isn't predictable when you open several projects with the same directory name.
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 projectless.
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.
Rename the initial tab to the default name.
When otpp-mode is enabled and only one tab exists, rename it to
otpp-default-tab-name.
A regular expression to detect project-aware commands in otpp-prefix.
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-localsis 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-localsis a not a function and it is non-nil.- The
dir-locals-filefile is stored in the project root, i.e., the project root is the same as thedir-locals-filedirectory. Then, this function checks in this order:
- If the local variable
otpp-project-nameis 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.
Return a list of tabs that have DIR as otpp-root-dir attribute.
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.
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.