This is a basis for building Emacs modes that utilize parse information in order to better edit and navigate code.
CEDET and Paredit are similar, but se-based modes attempt to be easy to implement. Se-based modes are suitable for new languages more than Java; however, in cases like Java one may use se-mode as a minor mode and may still enjoy all the advantages.
Documentation best read in Emacs org-mode.
One should be familiar with looking up documentation with `C-h f’, all useful functions have been documented. When reading this in Emacs org-mode, it is possible follow links with `C-c C-o’. It may be helpful to get more information on modes and processes by consulting the Elisp manual (see also the introduction to programming in Emacs Lisp).
The purpose of the project is to make taking advantage of parse information simple. While se-mode is modular, it is expected that one follow the precedent as much as possible. The following paragraph will be written as if there is one way se-modes work, but everything is able to be changed through variables and hooks.
A se-based mode starts a process that Emacs communicates with through standard I/O. Emacs will send a newline terminated file name to the process. The process will send back a newline terminated response with parse information formatted as JSON. This information is used to construct a parse tree for se-mode to use. With a parse tree se-mode can offer code navigation and manipulation features.
The expected protocol specifics are explained here.
<<<se-mode>>> While the git repo is called structured-editing, the general name for this package is se-mode. This includes the mode named se-mode, but much more as well.
<<<point>>> A point is a single integer value corresponding to a position in a buffer. Points in buffers begin at one.
<<<span>>> A label or name, a start, and end point of a syntactic region. End points are exclusive. A span may also contain extra information.
<<<node>>> A parent span and a list of children spans. The parent span should encapsulate all children spans. The children spans should be ordered.
<<<parse tree>>> A tree structure created as nodes from span information. Starts as either a node or list of nodes. `nil’ is considered an empty parse tree.
<<<term>>> A term is a span, node, or list of nodes. Parse trees are by definition a subset, but think of terms as the contents of parse trees. Points are not terms.
<<<JSON>>> JavaScript Object Notation, a human readable and writable format.
Most customization in se-mode comes from buffer local variables and using hooks effectively. Both of these topics are covered in depth in the Elisp manual. It should be noted add-hook is able to make the change buffer local, this should be done when able.
Customizing new modes through buffer local variables is important for keeping compatibility with other modes. There may be multiple se-based modes running at the same time, changing a variable carelessly could ruin other modes.
Add the following code to your `~/.emacs’ file to automatically load the se-mode files.
(let ((se-path "<PATH-TO-SE-DIRECTORY>")) ;; change to correct path
(add-to-list 'load-path se-path)
(add-to-list 'load-path (concat se-path "/json.el")))
The code first:
(require 'se-mode)
(eval-when-compile (require 'se-macros))
(se-create-mode "se-Ruby" ruby-mode
"A structured editing based mode for Ruby files."
(se-inf-start
(or (get-buffer-process "*se-ruby-mode*") ;; reuse existing process
(start-process "se-ruby-mode" "*se-ruby-mode*"
"irb" "--inf-ruby-mode" "-r" se-ruby-program-name))))
This demonstrates the preferred way of creating major modes.
se-create-mode is a macro that contains the best practices for
creating an se-based mode. By using se-create-macro
changes to the
se-mode library will not break se-modes.
se-create-mode
expects a block of code to setup your mode; the only
expectation is the setup of your process, preferably by passing a
process to se-inf-start. One should read the documentation for both
of these for a better understanding.
se-create-mode
does three things at the start of your mode: it
evaluates your code block, starts the se-mode minor mode, and adds a
parse function to se-navigation-mode-hook
. The minor mode provides
the `M-s’ hotkey to start se-navigation-mode
. The parse function is
evaluated at the start of navigation mode to ensure current parse
information.
- Each request is one line terminated by a newline character
- Each response is one line terminated by a newline character
- All responses are valid JSON
- The default parse request is just the file name
The expect response from the default parse request is a JSON
object. JSON was chosen because of the wide support and simplicity to
build. Certain key-value pairs have predefined behavior. spans
should contain an array of spans as arrays. A span has the pattern
[label, start, end, extra]. The fourth element is optional but
expected to be valid JSON.
{
"spans":[["span1",1,100],
["span2",1,30,{"type":"method"}]]
}
error
should contain any error message you want displayed to the
user.
{
"error":"Unable to open file."
}
error-span
can contain a single span or list of spans of where
errors happened. The errors will be highlighted for the user.
{
"error":"Unable to parse fully.",
"error-span":["error",32,64]
}
To add new behaviors on certain key-value pairs add functions to the
se-inf-response-hook with add-hook
. Functions will be given one
parameter, the parse JSON object as an association list. The Parse
Behavior Example shows how to write an extension.
The methods inside se.el are for manipulating parse trees. The
methods inside se-mode.el are applications of se.el
methods that
manipulate the buffer.
To properly use se-mode.el
, the se-mode-parse-tree variable must
have current information about the buffer. It is expected that code
is parsed upon entering se-navigation-mode. Using a method only in
navigation mode is a good way to ensure a current parse tree.
A common behavior is to select an enclosing region. There is a function to help with that, se-mode-select-name. The function’s name parameter is the label of the span. The following example demonstrates this:
(defun se-ruby-select-method ()
"Select the current method."
(interactive)
(or (se-mode-select-name "def")
(se-mode-select-name "defs")))
(se-navi-define-key 'se-ruby-mode (kbd "m") #'se-ruby-select-method)
This example also shows the usage of se-navi-define-key.
se-navi-define-key
should be used whenever adding key bindings to
navigation-mode. Navigation mode is intended to be shared by many
se-based modes, using se-navi-define-key
allows keys to be defined
per major mode (unlike define-key
).
This example is only suitable for use in navigation mode, to allow usage anywhere surround parse tree dependent code with a se-mode-progn call:
(defun se-ruby-select-method ()
"Select the current method."
(interactive)
(se-mode-progn
(or (se-mode-select-name "def")
(se-mode-select-name "defs"))))
(define-key se-ruby-mode-map (kbd "C-c m") #'se-ruby-select-method)
Now se-ruby-select-method
can be called anytime. se-mode-progn
ensures that the parse tree is current after every evaluated enclosing
statement. There are only two catches: the file must be in a parsable
state and the user must wait for possibly many parses.
This is acceptable, but se-inf-parse-and-wait may be used if one wants more control over parsing.
One common features is the evaluation of arbitrary bits of code. To do this in an se-based mode one must add a new request and a new parse behavior. The following code shows how a new request can be made:
(defun cl-eval-expr (expr)
"Evaluates combinatory logic expression."
(interactive "MEval: ") ;; M prefix asks for strings
(se-inf-ask (concat "EVAL\t" expr)))
(define-key cl-mode-map (kbd "C-c C-x") #'cl-eval-expr)
se-inf-ask sends a string to the current process, appending a new line if not already new line terminated. It is best to stick to this convention. Once a response is returned it is parsed as JSON and passed to the se-inf-response-hook functions. For this example, one might do the following:
(defun cl-process-result (json)
(let ((msg (cdr (assoc 'result json))))
(se-mode-popup-window "*cl-result*" msg)))
(se-create-mode "CL" nil
;; body removed
(add-hook 'se-inf-response-hook #'cl-process-result nil t))
This example shows the best way to add code to se-inf-response-hook
,
in the body of se-create-mode
. Doing it there ensures that you can
make the modification buffer local. It should be noted that lambdas
can be passed to add-hook
, but shouldn’t. Lambdas don’t allow for
functions to be removed with remove-hook
. This is important to
allow users the freedom to customize.