Skip to content

Language grammar tokenizer and theming/syntax highlighter with integrated editor.

License

Notifications You must be signed in to change notification settings

mattDavo/Editor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Editor

Custom language grammar tokenizer and theming/syntax highlighter with integrated editor written in Swift, designed for use in both macOS and iOS.

Based on the Textmate Grammar language and vscode's implementation. Contains a subset of the textmate grammar features with it's own extensions.

Goal: To create an flexible advanced text editor framework so that any app that needs to create an editor with non-trivial features, small or little, can add them easily.

Installation

Currently Editor is only available through the Swift Package Manager tooling and yet to have a major release. So add the following to your Package.swift file:

.package(url: "https://github.com/mattDavo/Editor", .branch("master"))

Example Usage

Head over to EditorExample to see Editor used in a larger project example.

We recommend reading the full documentation to best understand how to create your best editor. However, here is a quick example of what you can use editor to do:

EditorReadMeExampleGif

This is all possible with the following snippets of code.

First you will create a grammar. This is the definition of your language:

import EditorCore

let readMeExampleGrammar = Grammar(
    scopeName: "source.example",
    fileTypes: [],
    patterns: [
        MatchRule(name: "keyword.special.class", match: "\\bclass\\b"),
        MatchRule(name: "keyword.special.let", match: "\\blet\\b"),
        MatchRule(name: "keyword.special.var", match: "\\bvar\\b"),
        BeginEndRule(
            name: "string.quoted.double",
            begin: "\"",
            end: "\"",
            patterns: [
                MatchRule(name: "source.example", match: #"\\\(.*\)"#, captures: [
                    Capture(patterns: [IncludeGrammarPattern(scopeName: "source.example")])
                ])
            ]
        ),
        BeginEndRule(name: "comment.line.double-slash", begin: "//", end: "\\n", patterns: [IncludeRulePattern(include: "todo")]),
        BeginEndRule(name: "comment.block", begin: "/\\*", end: "\\*/", patterns: [IncludeRulePattern(include: "todo")])
    ],
    repository: Repository(patterns: [
        "todo": MatchRule(name: "comment.keyword.todo", match: "TODO")
    ])
)

Next you will create a Theme. This is how the scopes of your tokens (text divided based on the grammar) are formatted:

import EditorCore
import EditorUI

let readMeExampleTheme = Theme(name: "basic", settings: [
    ThemeSetting(scope: "comment", parentScopes: [], attributes: [
        ColorThemeAttribute(color: .systemGreen)
    ]),
    ThemeSetting(scope: "keyword", parentScopes: [], attributes: [
        ColorThemeAttribute(color: .systemBlue)
    ]),
    ThemeSetting(scope: "string", parentScopes: [], attributes: [
        ColorThemeAttribute(color: .systemRed)
    ]),
    ThemeSetting(scope: "source", parentScopes: [], attributes: [
        ColorThemeAttribute(color: .textColor),
        FontThemeAttribute(font: .monospacedSystemFont(ofSize: 18)),
        TailIndentThemeAttribute(value: -30)
    ]),
    ThemeSetting(scope: "comment.keyword", parentScopes: [], attributes: [
        ColorThemeAttribute(color: .systemTeal)
    ])
])

Finally we will take our NSTextView subclass EditorTextView and give it to our Editor with the grammar and theme.

import Cocoa
import EditorCore
import EditorUI

class ViewController: NSViewController {

    @IBOutlet var textView: EditorTextView!
    var editor: Editor!
    var parser: Parser!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView.insertionPointColor = .systemBlue
        textView.replace(lineNumberGutter: LineNumberGutter(withTextView: textView))
        
        parser = Parser(grammars: [readMeExampleGrammar])
        editor = Editor(textView: textView, parser: parser, baseGrammar: readMeExampleGrammar, theme: exampleTheme)
    }
}

We can also apply the same Grammar and Theme to an iOS version of the app, like so:

import UIKit
import EditorCore
import EditorUI

class ViewController: UIViewController {

    var textView: EditorTextView!
    var parser: Parser!
    var editor: Editor!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView = .create(frame: view.frame)
        view.addSubview(textView)
        
        textView.text = bigText
        textView.linkTextAttributes?.removeValue(forKey: .foregroundColor)
        
        parser = Parser(grammars: [exampleGrammar, basicSwiftGrammar])
        parser.shouldDebug = false
        editor = Editor(textView: textView, parser: parser, baseGrammar: exampleGrammar, theme: exampleTheme)
    }
}

However, Editor for iOS does contain the full breadth of features that are for macOS.

And voilà! With the appropriate settings in the interface builder this will produce the nice editor above.

Be sure to read the Documentation to understand what the above code is doing so that you can create your own editors!

Features

Apply any NSAttributedString attributes to a given token.

Using any of the pre-defined ThemeAttributes defined in EditorUI or write your own by implementing the TokenThemeAttribute or LineThemeAttribute protocol.

Apply custom Editor attributes to a given token.

Rounded background colors

Customizable corner radius and style.

Token only:

Full line:

Hidden attributes.

Hide certain tokens.

Actions.

Make tokens clickable, and add a handler.

Apply different attributes to a given token when the cursor is and isn't in the paragraph.

Cursor in the paragraph:

Cursor out of the paragraph.

Subscribe to all the MatchRule tokens.

For example you can easily get all the tags in the document.

editor.subscribe(toToken: "tag")

This works for all MatchRule tokens, even with captures in them. This makes getting all of the tokens of a certain type much easier when a MatchRule has complex captures. For example, a Swift string with interpolation.

Customizable Editor

Add line numbers

textView.replace(lineNumberGutter: LineNumberGutter(withTextView: textView))

Indent Using Spaces

textView.indentUsingSpaces = true
textView.tabWidth = 4

Auto Indent

textView.autoIndent = true

Customize Caret Width

textView.caretSize = 4

Contributing

Contributions are welcomed and encouraged. Feel free to raise pull requests, raise issues for bugs or new features, write tests or contact me if you think you can help.

TODO

EditorCore
  • Captures for BeginEndRule
  • Folding stop and start markers
  • Parent scopes for ThemeSettings
  • Refactor Rule matching into the protocol
EditorUI
  • Clickable/tappable tokens with handlers
  • Token replacements, take a token and replace the text.
  • State-conditional formatting: based on the position of the cursor
  • Use temporary attributes instead of the EditorTextStorage where possible for better performance.
All
  • Subscribe to tokens, changes
  • Auto-completion and suggestions
  • Smarter auto-indent, based on scope to determine depth of indent.
Optimization of Syntax Highlighting

Recommended Reading for EditorCore

To best understand how textmate grammars work and the parsers are implemented, look over the following:

Recommended Reading for EditorUI

TextKit and in particular, subclassing the various TextKit models can be difficult and confusing at times, here are some good links to look over if you're trying to digest something in the codebase or why certain behaviour is the way it is.

License

Available under the MIT License

Releases

No releases published

Packages

No packages published

Languages