Skip to content

CarlOlson/structured-editing

Repository files navigation

About

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.

Documentation

Expected Skills

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).

Overview

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.

Terminology

<<<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.

Local Variables and Hooks

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.

Quick Start Major Mode

Loading the Library

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")))

Creating a Major Mode

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.

Expected Protocol

  • 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.

Selection Example

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.

Parse Behavior Example

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.

About

create Emacs modes that utilize parse information

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published