diff options
Diffstat (limited to 'tools/emacs-pkgs/treecrumbs/treecrumbs.el')
-rw-r--r-- | tools/emacs-pkgs/treecrumbs/treecrumbs.el | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/tools/emacs-pkgs/treecrumbs/treecrumbs.el b/tools/emacs-pkgs/treecrumbs/treecrumbs.el new file mode 100644 index 0000000000..cd49324ad7 --- /dev/null +++ b/tools/emacs-pkgs/treecrumbs/treecrumbs.el @@ -0,0 +1,202 @@ +;; treecrumbs.el --- Fast, tree-sitter based breadcrumbs -*- lexical-binding: t; -*- +;; +;; Copyright (C) Free Software Foundation, Inc. +;; SPDX-License-Identifier: GPL-3.0-or-later +;; +;; Author: Vincent Ambo <tazjin@tvl.su> +;; Created: 2024-03-08 +;; Version: 1.0 +;; Keywords: convenience +;; Package-Requires: ((emacs "29.1")) +;; URL: https://code.tvl.fyi/tree/tools/emacs-pkgs/treecrumbs +;; +;; This file is not (yet) part of GNU Emacs. + +;;; Commentary: + +;; This package provides a tree-sitter based implementation of "breadcrumbs", +;; that is indicators displaying where in the semantic structure of a document +;; the point is currently located. +;; +;; Imagine a large YAML-document where the names of the parent keys are far out +;; of view: Treecrumbs can quickly display the hierarchy of keys (e.g. `foo < [] +;; < baz') and help figure out where point is. +;; +;; Treecrumbs only works if a tree-sitter parser for the target language is +;; available in the buffer, and the language is supported in the +;; `treecrumbs-languages'. Adding a new language is not difficult, and patches +;; for this are welcome. +;; +;; To active treecrumbs, enable `treecrumbs-mode'. This buffer-local minor mode +;; adds the crumbs to the buffer's `header-line-format'. Alternatively, users +;; can also use the `treecrumbs-line-segment' either in their own header-line, +;; tab-line or mode-line configuration. + +;;; Code: + +(require 'seq) +(require 'treesit) + +(defvar treecrumbs-languages nil + "Describes the tree-sitter language grammars supported by +treecrumbs, and how the breadcrumbs for their node types are +generated. + +Alist of symbols representing tree-sitter languages (e.g. `yaml') +to another alist (the \"node type list\") describing how +different node types should be displayed in the crumbs. + +See `define-treecrumbs-language' for more details on how to add a +language.") + +(defmacro define-treecrumbs-language (lang &rest clauses) + "Defines a new language for use in treecrumbs. LANG should be a +symbol representing the language as understood by treesit (e.g. +`yaml'). + +Each of CLAUSES is a cons cell mapping the name of a tree +node (in string format) to one of either: + +1. a static string, which will become the breadcrumb verbatim + +2. a tree-sitter query (in S-expression syntax) which must capture + exactly one argument named `@key' that will become the + breadcrumb (e.g. the name of a function, the key in a map, ...) + +Treecrumbs will only consider node types that are mentioned in +CLAUSES. All other nodes are ignored when constructing the +crumbs. + +The defined languages are stored in `treecrumbs-languages'." + + (declare (indent 1)) + (let ((compiled + (seq-map (lambda (clause) + (if (stringp (cdr clause)) + `(cons ,(car clause) ,(cdr clause)) + `(cons ,(car clause) + (treesit-query-compile ',lang ',(cdr clause))))) + clauses))) + `(setf (alist-get ',lang treecrumbs-languages nil nil #'equal) (list ,@compiled)))) + +(define-treecrumbs-language yaml + ;; In YAML documents, crumbs are generated from the keys of maps, and from + ;; elements of arrays. "block"-nodes are standard YAML syntax, "flow"-nodes + ;; are inline JSON-ish syntax. + ("block_mapping_pair" . ((block_mapping_pair key: (_) @key))) + ("block_sequence" . "[]") + + ;; TODO: Why can this query not match on to (flow_pair)? + ("flow_pair" . ((_) key: (_) @key)) + ("flow_sequence" . "[]")) + +(define-treecrumbs-language json + ;; In JSON documents, crumbs are generated from key names and array fields. + ("pair" . ((pair key: (string (string_content) @key)))) + ("array" . "[]")) + +(define-treecrumbs-language toml + ;; TOML has sections, key names and arrays. Sections are the only + ;; relevant difference to YAML. Nested keys are not parsed, and just + ;; displayed as-is. + ("table" . ((table (_) @key)) ) + ;; TODO: query cannot match on pair in inline_table, hence matching + ;; directly on keys + ("pair" . ([(dotted_key) + (quoted_key) + (bare_key)])) + ("array" . "[]")) + +(define-treecrumbs-language cpp + ;; In C++ files, crumbs are generated from namespaces and + ;; identifier declarations. + ("namespace_definition" . ([(namespace_definition + name: (namespace_identifier) @key) + (namespace_definition + "namespace" @key + !name)])) + + ("function_definition" . ((function_definition + declarator: + (function_declarator + declarator: (_) @key)))) + + ("class_specifier" . ((class_specifier + name: (type_identifier) @key))) + + ("struct_specifier" . ((struct_specifier + name: (type_identifier) @key))) + + ("field_declaration" . ((field_declaration + declarator: (_) @key))) + + ("init_declarator" . ((init_declarator + declarator: (_) @key)))) + +(defvar-local treecrumbs--current-crumbs nil + "Current crumbs to display in the header line. Only updated when +the node under point changes.") + +(defun treecrumbs--crumbs-for (node) + "Construct the crumbs for the given NODE, if its language is +supported in `treecrumbs-languages'. This functions return value +is undefined, it directly updates the buffer-local +`treecrumbs--current-crumbs'." + (let ((lang (cdr (assoc (treesit-node-language node) treecrumbs-languages)))) + (unless lang + (user-error "No supported treecrumbs language at point!")) + + (setq-local treecrumbs--current-crumbs "") + (treesit-parent-while + node + (lambda (parent) + (when-let ((query (cdr (assoc (treesit-node-type parent) lang)))) + + (setq-local treecrumbs--current-crumbs + (concat treecrumbs--current-crumbs + (if (string-empty-p treecrumbs--current-crumbs) "" + " < ") + + (if (stringp query) + query + (substring-no-properties + (treesit-node-text (cdar (treesit-query-capture parent query)))))))) + t)))) + + +(defvar-local treecrumbs--last-node nil + "Caches the node that was last seen at point.") + +(defun treecrumbs-at-point () + "Returns the treecrumbs at point as a string, if point is on a +node in a language supported in `treecrumbs-languages'. + +The last known crumbs in a given buffer are cached, and only if +the node under point changes are they updated." + (let ((node (treesit-node-at (point)))) + (when (or (not treecrumbs--current-crumbs) + (not (equal treecrumbs--last-node node))) + (setq-local treecrumbs--last-node node) + (treecrumbs--crumbs-for node))) + + treecrumbs--current-crumbs) + +(defvar treecrumbs-line-segment + '(:eval (treecrumbs-at-point)) + + "Treecrumbs segment for use in the header-line or mode-line.") + +;;;###autoload +(define-minor-mode treecrumbs-mode + "Display header line hints about current position in structure." + :init-value nil + :lighter " Crumbs" + (if treecrumbs-mode + (if (treesit-parser-list) + (push treecrumbs-line-segment header-line-format) + (user-error "Treecrumbs mode works only in tree-sitter based buffers!")) + (setq header-line-format + (delq treecrumbs-line-segment header-line-format)))) + +(provide 'treecrumbs) +;;; treecrumbs.el ends here |