packages feed

graft-0.1.0.0: README.md

# Graft

Lossless, immutable concrete syntax trees for Haskell, modelled on
rust-analyzer's [Rowan](https://github.com/rust-analyzer/rowan).

A Graft tree keeps every byte of the source: whitespace, comments and malformed
fragments are ordinary tokens. You can navigate the tree, find the syntax at a
byte offset, edit it and print it back, and everything you didn't touch comes
out exactly as it went in. That makes it a base for formatters, refactoring
and codemod tools, and language servers. You supply the lexer and
parser; Graft supplies the tree.

Graft is experimental. The API may change before 1.0.

## Getting started

Add `graft` to your `build-depends`. If your Hackage index doesn't have the
release you want, take it from its tag in `cabal.project`:

```cabal
source-repository-package
  type: git
  location: https://github.com/andreabedini/graft
  tag: v0.1.0.0
```

This program builds a tree, finds the number at byte offset 5, replaces it,
and prints the source before and after:

```haskell
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where

import qualified Data.Text.IO as Text
import Graft
import qualified Graft.Build as Build
import qualified Graft.Cursor as Cursor
import qualified Graft.Edit as Edit
import qualified Graft.Green as Green
import qualified Graft.Range as Range

data Kind = Root | Binding | Name | Number | Trivia
  deriving (Eq, Show)

-- A parser would emit these events; here they are written by hand.
parsed :: Either BuilderError (GreenNode Kind)
parsed = Build.exec $ Build.node Root $ do
  Build.node Binding $ do
    Build.token Name "x"
    Build.token Trivia "  = "
    Build.token Number "1"
  Build.token Trivia " -- keep\r\n"

main :: IO ()
main = do
  tree <- either (fail . show) pure parsed
  Text.putStr (Green.renderNode tree)
  case Cursor.covering (Range.fromLength 5 1) (Cursor.fromGreen (Node tree)) of
    Just number -> do
      let edits = [Edit.replace number (Token (Green.token Number "42"))]
      edited <- either (fail . show) pure (Edit.apply (Node tree) edits)
      Text.putStr (Green.render edited)
    Nothing -> fail "nothing at offset 5"
```

It prints:

```text
x  = 1 -- keep
x  = 42 -- keep
```

The double space, the comment and the CRLF line ending survive the edit.

`Cursor.covering` returns the innermost element that contains a range, here
the one byte at offset 5. `Cursor.tokenAt 5` would return both tokens that
touch offset 5, `"  = "` and `"1"`, as an editor's caret between them touches
both. `Edit.replace` describes a change at a cursor, and `Edit.apply` makes a
list of such changes to the tree they were made on, all at once, sharing
everything the changes don't touch. `Edit.textEdits` gives the same changes
as edits to the source text, for an editor.

## How it fits together

- **Green trees** (`Graft.Green`) are immutable nodes and tokens tagged with
  your own syntax-kind type. Each node caches its length in UTF-8 bytes and the
  offset of each child, so length and child lookups don't traverse anything.
  Green values have no parents or positions, so equal subtrees can be shared,
  within a tree and between versions of it. `Green.foldM`, `Green.fold`,
  `Green.map` and `Green.traverse` cover a whole tree.
- **Cursors** (`Graft.Cursor`) are zippers over a green tree. They know their
  parent, siblings and absolute byte range, and find tokens by offset.
- **Edits** (`Graft.Edit`) are described against a tree through its cursors
  and made all at once: a plan of replacements, splices, insertions and
  removals, or a top-down rewrite that replaces elements as it goes. Every
  decision reads the original tree, unchanged subtrees are shared with it,
  and a plan also yields the edits to the source text.
- **Builders** (`Graft.Build`, `Graft.Builder`) turn a parser's start/token/finish
  events into a tree, in a monad or as a pure value. Checkpoints let a parser
  wrap elements it has already emitted, as an operator-precedence parser needs
  to.
- **Typed views** (`Graft.Ast`) are newtypes over cursors that give your
  grammar's nodes typed accessors, without building a separate AST.
- **Byte ranges** (`Graft.Range`) describe where things are.
- **Line index** (`Graft.LineIndex`) converts byte offsets to and from line and
  column positions, including the UTF-16 columns used by the Language Server
  Protocol.

`Graft` exports the types and classes. The functions have short names meant
for qualified import, as in the example: `Cursor.parent`, `Green.node`,
`Range.start`. Several, such as `Green.length` and `Green.map`, clash with the
Prelude. All offsets and lengths are `TextLength`, a count of UTF-8 bytes,
never characters.

## Learn more

- The [API documentation](https://andreabedini.github.io/graft/) of the latest
  release gives each function's contract and cost. The introductions to
  `Graft.Build`, `Graft.Cursor`, `Graft.Edit`, `Graft.Ast` and
  `Graft.LineIndex` each start with an example: wrapping an operand at a
  checkpoint, finding a token by offset and reading its ancestors, applying a
  plan and renaming every matching token in one pass, a typed view with a
  field, and converting Language Server Protocol positions.
- [`examples/ExactUpdate.hs`](https://github.com/andreabedini/graft/blob/main/examples/ExactUpdate.hs)
  uses typed views to change a number's value while keeping its spelling
  (`001` stays `001` if the value doesn't change).
- [`docs/architecture.md`](https://github.com/andreabedini/graft/blob/main/docs/architecture.md)
  explains the design, how it differs from Rowan, and what is left to do.
- [`docs/performance.md`](https://github.com/andreabedini/graft/blob/main/docs/performance.md)
  records benchmark results, and
  [`bench/README.md`](https://github.com/andreabedini/graft/blob/main/bench/README.md)
  explains how to reproduce them.
- [`CONTRIBUTING.md`](https://github.com/andreabedini/graft/blob/main/CONTRIBUTING.md)
  describes how to build and test Graft, and how releases are made.

## License

[MIT](https://github.com/andreabedini/graft/blob/main/LICENSE). Graft is a port of ideas from Rowan, which is MIT or Apache-2.0;
see [NOTICE](https://github.com/andreabedini/graft/blob/main/NOTICE) and [licenses/ROWAN-MIT](https://github.com/andreabedini/graft/blob/main/licenses/ROWAN-MIT).