A major mode for GNAT Project (GPR) files, which utilizes the Emacs built-in support for tree-sitter, first available starting with Emacs 29. The tree-sitter functionality is used to build an in-memory concrete syntax tree of the parsed language, allowing operations such as syntax highlighting to be performed more accurately than historical methods (e.g., regular expressions).
This major mode provides support for syntax highlighting, indentation,
navigation, Imenu, “which function” (i.e., displaying the current
function name in the mode line) and outlining (via
outline-minor-mode
).
Note: This major mode is based on the Emacs 29 (or newer) built-in tree-sitter support, not to be confused with the separate Emacs tree-sitter package. The two are not compatible with each other.
Note: Outlining is made available through Emacs 30 (or newer) built-in
support for outlining within tree-sitter major modes, therefore it is
required for outline-minor-mode
to work with gpr-ts-mode
.
There are a couple of requirements which must be met in order to use tree-sitter powered major modes. The Emacs documentation should be consulted which will provide complete details. The following are the main points to consider:
- Emacs must have been built with tree-sitter support. Versions of Emacs prior to Emacs 29 do not have built-in support. The built-in support is optionally enabled when Emacs is built. It is almost guaranteed that any pre-built Emacs 29 or newer executable supplied as part of an OS package manager (e.g., APT, Homebrew, MSYS2, etc.) will have been built with this capability enabled.
- The tree-sitter shared library must be installed on your system.
The specifics of how to do this will vary based on the Operating
System. However, a pre-built Emacs 29 or newer executable that is
installed by the OS package manager will likely have identified this
library as a prerequisite and have installed it along with Emacs.
Therefore, it is unlikely you will need to manually install this
library if you installed Emacs with an OS package manager.
The following command can be used to determine if tree-sitter support is enabled in Emacs and whether the tree-sitter library can be found on your system:
M-: (treesit-available-p) RET
- Make sure it indicates
t
in the echo area instead ofnil
.
There are multiple ways in which a package can be installed in Emacs.
The most convenient way is to use a package archive, however
installation directly from the git repository is also possible. In
addition, there are multiple third party package managers available,
but installation instructions in this section will focus only on the
built-in package manager (i.e., package.el
). It is assumed that
power-users will not need direction as to how to use other package
managers.
In addition to package management, it is also common practice to
perform package configuration. There are also multiple third party
packages for managing your package configuration, however use-package
is now built-in to Emacs. Refer to the example configuration section
for ideas on how to utilize use-package
to setup your own personal
configuration.
This package can be installed from the MELPA package archive using the
Emacs built-in package manager (i.e., package.el
). MELPA is not
configured in the package manager by default, but the following can be
used to configure the use of the MELPA archive. Refer to Getting
Started for additional details on configuring and using the MELPA
package archive.
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
Once configured as above, instruct the package manager to refresh the available packages and to perform the installation, as follows:
M-x package-refresh-contents RET
M-x package-install RET gpr-ts-mode RET
Installation directly from the source repository is possible using
package-vc-install
. The following command can be used to perform this
installation:
M-x package-vc-install RET https://github.com/brownts/gpr-ts-mode RET
In order for gpr-ts-mode to be useful, it needs to have the specific
tree-sitter GPR language grammar library installed. This library is
different from the tree-sitter library mentioned in the prerequisites
section (e.g., libtree-sitter.so
vs libtree-sitter-gpr.so
). The
library is not bundled with gpr-ts-mode, but is maintained separately.
With the default configuration, the first time gpr-ts-mode is loaded
(in the absence of an existing installed library) it will prompt to
download, build and install the grammar library. The following
settings provide control over this activity.
- User Option: gpr-ts-mode-grammar
- Location of the tree-sitter GPR language grammar to be used by gpr-ts-mode.
- User Option: gpr-ts-mode-grammar-install
- Controls the level of automation in installing the grammar library (automatic, prompt first, etc).
In order to build the library, you will need to have a C compiler
installed. Refer to the Emacs documentation surrounding
treesit-install-language-grammar
, as gpr-ts-mode uses the built-in
Emacs functionality to perform the download, building and installation
of the library.
It’s also possible to skip this step if you already have a pre-built
library for the language. In which case, placing the pre-built
library in the correct location will allow gpr-ts-mode to find and use
the library. You can customize treesit-extra-load-path
to add extra
locations to search for libraries.
The README for the GPR language grammar, provides instructions on
building the library from source if you’d rather perform that step
manually or don’t have the correct toolchain installed in order for
this to be performed automatically. You will only be prompted if the
library can’t be found in one of the expected locations. The
prompting can also be controlled by changing the
gpr-ts-mode-grammar-install
setting.
If manually installing, or troubleshooting the installation of the GPR language grammar, you can use the following to check whether Emacs can locate the library:
M-: (treesit-ready-p 'gpr t) RET
- Make sure it indicates
t
in the echo area instead ofnil
.
There are 4 different levels of syntax highlighting available,
providing an increasing amount of highlighting. By default in Emacs,
level 3 (controlled by treesit-font-lock-level
) is used to provide a
compromise between providing too little and too much fontification.
It should be noted that the levels are cumulative, meaning that each
level also includes all of the fontification in the levels below it.
The following provides the list of features and how they are mapped to
the different font lock levels.
- Level 1
- comment, definition
- Level 2
- keyword, string, type
- Level 3
- attribute, function, number, operator, package, variable
- Level 4
- bracket, delimiter, error
The following user options can be customized to modify the syntax highlighting characteristics:
- User Option: gpr-ts-mode-package-names
- List of known package
names to highlight within variable references. There are
ambiguities in the syntax tree for variable references. For
example,
Foo'Bar
could be a reference to theBar
attribute in the packageFoo
of the current project, or it could be a reference to the top-levelBar
attribute in the projectFoo
. There are additional ambiguities when child packages are involved since references such asFoo.X'Bar
exhibit the same problem. This ambiguity applies not just to variable attribute references but also to regular variable references since variables can exist at the project level as well as within a package. If package names in variable references are not properly highlighted, this causes an inconsistency between highlighting of package names in package declarations and not properly highlighting package names in variable references. Actively monitoring package names in the current project as well as all withed projects is considered too heavy handed and instead we settle for maintaining a list of package names. Since custom package names can be introduced (although uncommon), the list of names can be customized, but is initialized with the set of well-known names.
Indentation follows the nesting structure of the language. Each nested level is indented a fixed amount. Thus the general indentation offset governs the amount of this indentation. Additional configurations address special cases, such as the indentation of a construct spanning multiple lines (i.e., broken indent). The following configurations can be used to customize these indentation levels to match your own style.
- User Option: gpr-ts-mode-indent-offset
- Indentation used for structural visualization
- User Option: gpr-ts-mode-indent-when-offset
- Indentation for case items and comments, relative to a case construction.
- User Option: gpr-ts-mode-indent-broken-offset
- Continuation indentation when item does not completely reside on a single line.
- User Option: gpr-ts-mode-indent-exp-item-offset
- Continuation indentation for partial expressions (i.e., terms, concatenation, etc).
- User Option: gpr-ts-mode-indent-strategy
-
Indentation strategy to use, typically when
RET
orTAB
are pressed during editing. Historically, this would drive a line-based indentation, however more complex indentation strategies can be used due to the availability of the syntax tree.
It should be noted that the offsets defined above are described in
terms of each other, so that by customizing the standard indentation
offset gpr-ts-mode-indent-offset
, the other indentation offsets will
be adjusted accordingly. In other words, those settings either use
the same value or a value derived from it. Therefore, customizing the
base value will have an affect on the remaining values as well. If
this is not the desired outcome, the other offsets should be
customized as well.
Stacking of a list of items on subsequent lines is supported and the indentation will align stacked items under each other. This applies to lists, function call parameters, import lists, discrete choice lists in case constructions, etc.
Indentation rules used in gpr-ts-mode
, as in all tree-sitter based
modes, are based on the syntax of the language. When what currently
exists in the buffer is not syntactically correct, the in-memory
syntax tree will contain errors, since the buffer doesn’t adhere to
the grammar rules of the language (i.e., it contains syntax errors).
Due to these errors, the indentation may not be applied correctly as
the in-memory syntax tree may not accurately reflect the language
(i.e., the tree will contain an ERROR
node instead of a language
specific node) and indentation rules may not be applied when they
should, due to these errors.
To help combat this issue, specific indentation error recovery is used
to help maintain indentation even when portions of the syntax are
missing, which provides a best-effort approach to maintain accurate
indentation. To further increase indentation accuracy, it is
suggested to use functionality that can help to reduce the number of
syntax errors that might exist in the buffer at a particular point in
time. Functionality such as enabling electric-pair-mode
to insert
matching parenthesis, quotation marks, etc. or using snippets (e.g.,
gpr-yasnippets) to automatically insert multi-line control constructs
(e.g., project declarations, package declarations, case statements,
etc.) are highly recommended. Not only can this help keep your buffer
closer to a syntactically correct state, you also benefit from the
productivity gains as well.
The indentation strategy can help recover from previously incorrect
indentation that has occurred while the buffer was in a syntactically
invalid state. The default declaration
setting for
gpr-ts-mode-indent-strategy
will re-indent at the declaration level
once the declaration is in a syntactically valid state. When invalid
syntax exists within a declaration, this strategy reverts back to
“best effort” line-based indentation. Once the syntax becomes valid,
indentation will be applied at the declaration level.
If none of the existing indentation strategies are sufficient, a
custom strategy can be created and used. In order to create a
strategy, a new strategy symbol should be specified in
gpr-ts-mode-indent-strategy
, and an implementation of
gpr-ts-mode-indent
should be created, specializing on the new strategy
symbol name. Refer to existing instances of this function to
understand how current strategy functions are implemented.
The following specialized navigation functions exist and are applied
to GPR projects. Since function declarations don’t exist for GPR
project files, this is repurposed to navigate packages and projects
instead. This re-purposing of function to project/package is also
extended to which-function-mode
support and will show the current
project and package in the mode line, when enabled.
- Key: C-M-a (treesit-beginning-of-defun)
- Move backward to beginning of package or project
- Key: C-M-e (treesit-end-of-defun)
- Move forward to next end of package or project
Starting with Emacs 30, additional navigation functions are provided
for S-expression
and sentence
traversal using the standard Emacs
commands (i.e., forward-sexp
, forward-sentence
, etc).
With the provided Imenu support, additional options are available for ease of navigation within a single GPR file. Imenu supports indexing of attributes, packages, projects, type declarations, variable declarations and with clauses. Custom categories can also be defined.
- User Option: gpr-ts-mode-imenu-categories
- The set of categories to be used for Imenu. Since there are a number of different categories supported, it may be a distraction to display categories that aren’t desired. Therefore, the set of categories can be customized to reduce clutter or to increase performance. The order in which the categories are listed will be respected when the Imenu indexing is performed. This is helpful if specific ordering of categories is desired.
- User Option: gpr-ts-mode-imenu-category-name-alist
- The mapping between categories and the displayed name for the category. This customization may be helpful if you are expecting a specific name for a category, use plural instead of singular nouns, or want to customize for internationalization.
The items in each Imenu category can be sorted for each nesting level.
The specific ordering is controlled via imenu-sort-function
, which can
be customized to specify the desired sorting function. When
imenu-sort-function
is nil, items are listed in the order they appear
in the buffer. It should be noted that while the mode applies the
sorting as specified by imenu-sort-function
as well as the category
ordering specified in gpr-ts-mode-imenu-categories
, downstream
functionality (such as completion candidates in the minibuffer) may
rearrange the order of items.
If none of the existing categories are sufficient, or an additional
category is desired, a custom category can be created and used. In
order to create a category, a new category symbol should be added to
gpr-ts-mode-imenu-categories
, a name mapping should be added to
gpr-ts-mode-imenu-category-name-alist
, and an implementation of
gpr-ts-mode-imenu-index
should be created, specializing on the new
category symbol name. Refer to existing instances of this function to
understand how current category index functions are implemented.
When Org Mode doesn’t know the major mode for the language of a source
block, it will guess by appending “-mode” to the end of the language
name. If we use a language name of “gpr”, this means it will look for
a major mode named “gpr-mode”. This default behavior doesn’t work if
we want to use Tree-Sitter enabled modes. Maybe in the future it will
be aware of these modes, but in the meantime, we can explicitly
configure Org Mode to map to the Tree-Sitter major mode using the
customization variable org-src-lang-modes
.
The following can be added to your configuration to persist the setting:
(with-eval-after-load 'org-src
(add-to-list 'org-src-lang-modes '("gpr" . gpr-ts)))
The mode’s Imenu support might be overridden if an LSP client is used
with the major mode. The mode typically provides a more organized and
configurable Imenu experience than that provided by the Language
Server. In such cases, in order to use the mode’s built-in Imenu
support rather than that provided via the Language Server, Imenu
support must be disabled in the LSP client’s configuration. For
Eglot, LSP-provided Imenu is disabled by adding the imenu
symbol to
the list in the eglot-stay-out-of
variable.
(setq eglot-stay-out-of '(imenu))
For lsp-mode
, LSP-provided Imenu is disabled by clearing the
lsp-enable-imenu
user option.
(setq lsp-enable-imenu nil)
The following is an example configuration using use-package
to manage
this configuration. It assumes that package.el
is your package
manager. This checks to make sure tree-sitter support is enabled in
Emacs before attempting to install/configure the package, thus your
configuration will remain compatible with versions of Emacs which
don’t yet support tree-sitter, and will not install and configure this
package in its absence. Additionally, this also includes installation
and configuration of recommended supporting packages and modes.
(when (and (fboundp 'treesit-available-p)
(treesit-available-p))
(use-package gpr-ts-mode
:ensure t
:defer t ; autoload updates `auto-mode-alist'
:init
;; Configure source blocks for Org Mode.
(with-eval-after-load 'org-src
(add-to-list 'org-src-lang-modes '("gpr" . gpr-ts)))))
;; Configure Imenu
(use-package imenu
:ensure nil ; built-in
:custom (imenu-auto-rescan t)
:hook (gpr-ts-mode . imenu-add-menubar-index))
;; Configure Electric Pair
(use-package elec-pair
:ensure nil ; built-in
:hook (gpr-ts-mode . electric-pair-local-mode))
;; Configure snippets
(use-package gpr-yasnippets
:ensure t
:defer t) ; autoload hooks into yasnippet
(use-package yasnippet
:ensure t
:hook (gpr-ts-mode . yas-minor-mode-on))